后台界面同步:以线上 /mnt/web/admin 为准,补入仓库缺失/更新的页面(mall、socket、琉璃树等)

This commit is contained in:
2026-10-19 02:02:36 +08:00
parent 466a4b1cea
commit a4279a1f33
7 changed files with 1102 additions and 2 deletions
@@ -0,0 +1,610 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SignalR WebSocket 协议文档</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #f5f6fa; color: #333; }
.layout { display: flex; height: 100vh; }
.sidebar { width: 220px; background: #1e2a3a; color: #fff; overflow-y: auto; flex-shrink: 0; }
.sidebar .logo { padding: 20px; font-size: 18px; font-weight: bold; text-align: center; border-bottom: 1px solid #2a3a4a; }
.sidebar .nav-item { padding: 12px 20px; cursor: pointer; font-size: 14px; border-left: 3px solid transparent; }
.sidebar .nav-item:hover { background: #2a3a4a; }
.sidebar .nav-item.active { background: #2a3a4a; border-left-color: #3498db; color: #3498db; }
.sidebar .nav-item .badge { float: right; background: #3498db; color: #fff; border-radius: 10px; padding: 0 8px; font-size: 11px; line-height: 18px; }
.main { flex: 1; overflow-y: auto; padding: 20px 30px; }
.hub-section { display: none; }
.hub-section.active { display: block; }
.hub-card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 25px; overflow: hidden; }
.hub-header { padding: 18px 20px; color: #fff; }
.hub-header .path { font-size: 20px; font-weight: bold; }
.hub-header .detail { font-size: 13px; opacity: .9; margin-top: 6px; }
h2 { font-size: 18px; margin: 25px 0 12px; padding-left: 12px; border-left: 4px solid #3498db; }
h3 { font-size: 15px; margin: 20px 0 10px; color: #555; }
table { width: 100%; border-collapse: collapse; margin-bottom: 5px; }
th { background: #f0f4f8; text-align: left; padding: 10px 12px; font-size: 13px; color: #555; border-bottom: 2px solid #ddd; }
td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; vertical-align: top; }
tr:hover td { background: #f8faff; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-right: 4px; }
.tag-cs { background: #e3f2fd; color: #1565c0; }
.tag-sc { background: #fce4ec; color: #c62828; }
.tag-new { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: bold; background: #e74c3c; color: #fff; margin-left: 4px; vertical-align: middle; }
tr.row-new { border-left: 4px solid #e74c3c; background: #fff5f5 !important; }
tr.row-new td { background: #fff5f5 !important; }
tr.row-new:hover td { background: #ffe8e8 !important; }
code { background: #f0f0f0; padding: 1px 5px; border-radius: 3px; font-size: 12px; color: #c7254e; }
pre { background: #f8f9fa; padding: 12px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.6; border: 1px solid #eee; }
.info-box { background: #eef9ff; border: 1px solid #b3d8ef; border-radius: 6px; padding: 18px 22px; margin-bottom: 20px; }
ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
.top-bar { background: #fff; border-radius: 8px; padding: 18px 22px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.top-bar h1 { font-size: 22px; color: #2c3e50; }
.top-bar p { color: #888; font-size: 13px; margin-top: 6px; }
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="logo">📡 协议文档</div>
<div class="nav-item active" data-target="hub-login" onclick="switchTab(this)">LoginHub <span class="badge">/loginhub</span></div>
<div class="nav-item" data-target="hub-fishgame" onclick="switchTab(this)">FishGameHub <span class="badge">/fishinghub</span></div>
<div class="nav-item" data-target="hub-fishroom" onclick="switchTab(this)">FishRoomHub <span class="badge">/fishingroomhub</span></div>
<div class="nav-item" data-target="hub-chessgame" onclick="switchTab(this)">ChessGameHub <span class="badge">/chessgamehub</span></div>
<div class="nav-item" data-target="hub-customer" onclick="switchTab(this)">CustomerHub <span class="badge">/customerHub</span></div>
<div class="nav-item" data-target="hub-common" onclick="switchTab(this)">公共说明</div>
</div>
<div class="main">
<div class="top-bar">
<h1>SignalR WebSocket 协议文档</h1>
<p>所有 Hub 支持 WebSocket 传输。LoginHub 和 CustomerHub 客服端需 JWT Token(<code>access_token</code> 参数)。本轮不调整现有时间字段的单位和解析方式。</p>
</div>
<!-- ==================== LoginHub ==================== -->
<div id="hub-login" class="hub-section active">
<div class="hub-card">
<div class="hub-header" style="background:#3498db;">
<div class="path">LoginHub — <code>/loginhub</code></div>
<div class="detail">WS + JWT,用于大厅资产同步、全服广播、邮件通知</div>
</div>
<h3>📤 客户端 → 服务器</h3>
<table>
<tr><th style="width:80px">方法</th><th style="width:280px">参数</th><th>说明</th></tr>
<tr><td><span class="tag tag-cs">C→S</span> Bind</td><td><code>string userId</code></td><td>必须:连接后立即调用,将连接与 userId 绑定。服务端加入 userId 命名的 SignalR 组,后续定向推送通过此组发送。</td></tr>
<tr class="row-new"><td><span class="tag tag-cs">C→S</span> DismissPendingRoom<span class="tag-new">NEW</span></td><td><code>string userId</code></td><td>用户拒绝返回断线房间。登录后若收到 PendingRoomNotice,用户选择"离开"时调用此方法,服务端清理旧房间残留。</td></tr>
</table>
<h3>📥 服务器 → 客户端</h3>
<table>
<tr><th style="width:150px">事件</th><th>数据</th><th>发送时机</th></tr>
<tr><td><span class="tag tag-sc">S→C</span> Broadcast</td>
<td><pre>{
UserName: string, // 昵称
AvatarUrl: string, // 头像URL
Level: int, // 等级
VipLevel: int, // VIP等级
Message: string, // 消息内容
Time: long // Unix时间戳(秒)
}</pre></td><td>大喇叭全服广播</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> KillAnnouncement</td>
<td><pre>{
Text: string, // 公告文案: "恭喜玩家momo在风雷之渊高级场捕获 金龙*2000炮倍 100000000大奖"
UserName: string, // 玩家名: "momo"
GameTypeName: string, // 游戏类型: "风雷之渊"
RoomName: string, // 房间名: "高级场"
FishName: string, // 鱼名: "金龙"
Multiple: int, // 倍数: 2000
Gold: long, // 获得金币: 100000000
Time: long // Unix时间戳(秒)
}</pre></td><td>全局击杀公告(特殊Boss击杀 / 机器人模拟击杀),走 LoginHub</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> AssetUpdated</td>
<td><pre>{
CurrencyType: int, // 货币类型: 1=金币 2=保险箱 3=钻石
ChangeType: int, // 变动类型: 1=增加 2=减少
Amount: long, // 变动数量
NewGold: long, // 最新金币
NewDiamond: int, // 最新钻石
Source: string, // 来源: "游戏金币结算"、"救济金"、"签到" 等
Remark: string // 备注(房间名等)
}</pre></td><td>资产变更:退出房间结算 / 后台充值扣币 / 救济金 / 签到 / 捕鱼返利</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> LevelChanged<span class="tag-new">NEW</span></td>
<td><pre>{
NewLevel: int, // 最新等级
NewExp: long, // 最新经验
NewGold: long, // 最新金币
NewDiamond: int, // 最新钻石
Source: string, // 来源
Remark: string // 备注
}</pre></td><td>后台修改玩家等级</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> BackpackUpdated</td>
<td><pre>{
Action: string, // "add" | "update" | "delete"
PropNumber: string, // 道具编号
Count: int // 数量
}</pre></td><td>道具背包变更(后台操作)</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> NewEmail</td>
<td><pre>{
Title: string, // 邮件标题
EmailId: string // 邮件ID
}</pre></td><td>收到新邮件通知</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> ChangeConfig<span class="tag-new">NEW</span></td>
<td><pre>changeType: int
1 = 活动配置更新
2 = 游戏配置更新</pre></td><td>后台活动配置或游戏配置保存成功并发布 CAP 后,由 UserHost 通过 LoginHub 向所有在线玩家广播。前端收到后应按 changeType 重新请求对应配置。监听示例:<code>connection.on("ChangeConfig", changeType =&gt; { ... })</code></td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> AccountKicked<span class="tag-new">NEW</span></td>
<td><pre>{
userId: string // 被顶的玩家ID
}</pre></td><td>账号被顶通知。该账号在其他设备登录时,旧连接收到此事件。前端应提示"账号已在其他地方登录"并清空房间 UI、断开游戏连接。</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> KickToMainPage<span class="tag-new">NEW</span></td>
<td><pre>{
userId: string // 被踢的玩家ID
}</pre></td><td>强制退出到主页面。当前账号的连接已失效(被顶号/用户不匹配),前端收到后应立即清除所有游戏 UI 并导航到主页面/登录页。</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> PendingRoomNotice<span class="tag-new">NEW</span></td>
<td><pre>{
roomId: string, // 房间ID
gameType: string, // 游戏类型: "风雷之渊"
roomName: string // 房间名: "高级场"
}</pre></td><td>登录后检测到有未完成的断线游戏。金币已结算至当前余额。前端应弹窗提示用户选择"返回房间"(调 Reconnect)或"离开"(调 DismissPendingRoom),60秒无操作自动离开。</td></tr>
</table>
</div>
</div>
<!-- ==================== FishGameHub ==================== -->
<div id="hub-fishgame" class="hub-section">
<div class="hub-card">
<div class="hub-header" style="background:#27ae60;">
<div class="path">FishGameHub — <code>/fishinghub</code></div>
<div class="detail">WS,捕鱼游戏核心 Hub。连接后先调 Reconnect 绑定用户。</div>
</div>
<p style="padding:12px 20px;margin:0;background:#f0faf0;border-bottom:1px solid #d0e8d0;"><strong>🤖 机器人说明:</strong>捕鱼房间中的机器人(<code>IsBot=true</code>)由服务端完全模拟。客户端无需发送机器人指令,机器人在协议层面与真人一致——同样触发 RoomFireNotice、RoomKillNotice 等事件。</p>
<h3>📤 客户端 → 服务器</h3>
<table>
<tr><th style="width:100px">方法</th><th style="width:380px">参数</th><th>返回</th><th>说明</th></tr>
<tr><td><span class="tag tag-cs">C→S</span> Reconnect</td><td><code>string userId</code></td><td>无</td><td>断线重连必须先调用。服务端返回 RoomInfo(含 activeFishes)并恢复房间状态</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> JoinRoom</td><td><code>string roomId, string userId, int posituin</code></td><td>无</td><td>加入房间。posituin=-1 自动分配座位</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> FireCannon</td><td><code>string roomId, string userId, long gold, int count, long timestamp, string sign, object? extra</code></td><td><pre>{ bullets: [{ bulletId: string, gold: long }], gold: long, timestamp: long }</pre></td><td>发炮。每颗返回子弹携带本次发射炮倍;外层 gold 为玩家剩余金币。sign=MD5签名</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> KillFish</td><td><code>string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign</code></td><td><pre>{ gold: long, timestamp: long }</pre></td><td>击杀鱼</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> LeaveRoom</td><td><code>string roomId, string userId</code></td><td>无</td><td>退出房间(金币结算后自动 Context.Abort 断开)</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> UseProp</td><td><code>string roomId, string userId, string propNumber, int diamond=0</code></td><td><code>int</code> 剩余数量</td><td>使用道具(钻石购买时传 diamond)。返回背包剩余数量,-1 表示失败。冰冻道具(40003)触发 FishFrozen 事件</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> ChangeGunMultiple</td><td><code>string roomId, string userId, int multiple</code></td><td>无</td><td>更改炮倍</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GetRoomInfo</td><td><code>string roomId</code></td><td>无</td><td>结果通过 GetRoomInfoResult 返回</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GetPlayerInfo</td><td><code>string roomId, string userId</code></td><td>无</td><td>获取玩家信息</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> SyncGameData</td><td><code>string roomId, object data</code></td><td>无</td><td>同步游戏数据</td></tr>
<tr class="row-new"><td><span class="tag tag-cs">C→S</span> SetPlayerAfk<span class="tag-new">NEW</span></td><td><code>string roomId, string userId</code></td><td>无</td><td>玩家挂机,触发 PlayerAfkNotice</td></tr>
<tr class="row-new"><td><span class="tag tag-cs">C→S</span> CancelPlayerAfk<span class="tag-new">NEW</span></td><td><code>string roomId, string userId</code></td><td>无</td><td>取消挂机,触发 PlayerCancelAfkNotice,返回 RoomInfo(含 activeFishes)同步当前鱼状态</td></tr>
</table>
<h3>📥 服务器 → 客户端</h3>
<table>
<tr><th style="width:170px">事件</th><th>数据</th><th>目标</th><th>说明</th></tr>
<tr><td><span class="tag tag-sc">S→C</span> BindSuccess</td><td><code>string</code></td><td>调用者</td><td>重连绑定成功</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> Exception / ErrorMessage</td><td><code>{ code: int, message: string }</code></td><td>调用者</td><td>错误通知</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomInfo</td><td><pre>RedisGameRoom 扩展字段,包含首次及当前活跃鱼:
{
RoomId, RoomName, GameType, RoomLevel, RoomPartition,
RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
RobotCount, Players[], CreateUserId, SystemFactor,
activeFishes: [{ // 首次进房/后续进房/重连/取消挂机时包含
fishId: number,
fishIds: number[],
fishNumber: string,
routeId: number,
fishArrayNumber: string,
createdAt: number, // 毫秒
expireAt: number, // 毫秒
remainingSeconds: int
}]
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息。先用 activeFishes 初始化完整鱼群;服务端约 300ms 后仅兼容补发普通散鱼的 OnFishSpawn(init),Boss、黄金鱼、特殊鱼和鱼阵不会重复补发。</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> SyncPlayerGold</td><td><code>long</code></td><td>调用者</td><td>金币同步(金币≤0时触发)</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> SendPlayerInfoResult</td><td><code>long?</code></td><td>调用者</td><td>玩家Gold值</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GetRoomInfoResult</td><td><code>RedisGameRoom?</code></td><td>调用者</td><td>房间信息查询结果</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomKillNotice</td>
<td><pre>{
fishNumber: string, userId: string, gold: long,
fishId: string, timespan: long, killtime: long,
msg: string, // "时间:X秒"
beishu: double,
skillsGold: long, // 炮倍金币
fishScore: int,
param: object?, // Boss爆奖结果
drop: object? // Boss掉落 { propNumber, propName, count }
}</pre></td><td>房间组</td><td>击杀通知</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomFireNotice</td><td><code>{ userId, bulletIds[], gold, count, extra, isBot }</code></td><td>房间组</td><td>发炮通知</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomJoinNotice</td><td><pre>{
userId: string, // 玩家ID
userName: string, // 玩家名字
userNumber: string, // 玩家编号
avatar: string, // 玩家头像
isRedName: bool, // 是否红名
level: int, // 玩家等级
exp: long, // 经验值
maxExp: long, // 当前等级最大经验值
vipLevel: int, // VIP等级
gold: long, // 金币
diamond: int, // 钻石
cannonId: string, // 炮台
wingsId: string, // 翅膀
warGodId: string, // 战神
cannonMultiple: int, // 炮倍
position: int, // 位置
beginCoin: long, // 进场金币
maxGold: long, // 最高金币
beginTime: long, // 进场时间
state: int, // 状态 0正常 1断线中
playerFactor: object, // 玩家因子
totalRechargeGold: long, // 累计充值
safeBoxGold: long, // 保险箱金币
isBot: bool, // 是否机器人
isAfk: bool // 是否挂机
}</pre></td><td>房间组</td><td>玩家加入</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomLeaveNotice</td><td><code>string userId</code></td><td>房间组</td><td>玩家离开</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> BotLeaveRoomNotice<span class="tag-new">NEW</span></td><td><pre>{ userId: string }</pre></td><td>房间组</td><td>机器人退出房间</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> HostDisconnect<span class="tag-new">NEW</span></td><td>无</td><td>房间组</td><td>房主断线,其他玩家可接受机器人控制权</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomDisconnectNotice</td><td><code>string userId</code></td><td>房间组</td><td>玩家断线</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> ChangeGunMultipleNotice</td><td><code>{ userId, multiple }</code></td><td>房间组</td><td>炮倍变更</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> ChangeControllerUser</td><td><code>string userId</code></td><td>房间组</td><td>房主控制权移交</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> UsePropNotice</td><td><code>{ userId, propNumber }</code></td><td>房间组</td><td>使用道具</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> SummonCardFishNotice<span class="tag-new">NEW</span></td><td><pre>{
roomId: string, // 房间ID
userId: string, // 使用召唤卡的玩家ID
userNumber: string, // 玩家编号
userName: string, // 玩家昵称
fishType: int, // 2=Boss鱼,3=黄金鱼
fishTypeName: string, // "Boss鱼"或"黄金鱼"
fishNumber: string, // 召唤出的鱼配置编号
fishId: number // 本次生成的鱼实例ID
}</pre></td><td>房间组</td><td>召唤卡成功生成鱼后广播。鱼的路径和生命周期仍由 OnFishSpawn 处理;本事件用于显示“哪个玩家召唤了什么鱼”等提示。</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> PlayerAfkNotice<span class="tag-new">NEW</span></td><td><code>{ userId: string }</code></td><td>房间组</td><td>玩家挂机通知</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> PlayerCancelAfkNotice<span class="tag-new">NEW</span></td><td><code>{ userId: string }</code></td><td>房间组</td><td>取消挂机通知</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> TorpedoResultNotice<span class="tag-new">NEW</span></td><td><pre>{ userId: string, propNumber: string, gold: long, color: string }</pre></td><td>房间组</td><td>鱼雷使用结果通知</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> OnPlayerInfo<span class="tag-new">NEW</span></td><td><pre>{ userId, userName, gold, diamond, viplvl, cannonMultiple, cannonId, wingsId, position }</pre></td><td>调用者</td><td>玩家信息同步</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> SyncGameData<span class="tag-new">NEW</span></td><td><code>object data</code></td><td>房间组</td><td>游戏数据同步(服务端主动推送)</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> FishFrozen<span class="tag-new">NEW</span></td><td>无</td><td>房间组</td><td>冰冻道具(40003)生效,鱼存活时间延长 15 秒(客户端写死 15s 倒计时)</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> OnSeatChange</td><td><pre>// type=join 时:
{ type: "join", playerId: string, position: int, playerName: string,
nickname: string, gold: long, diamond: int, vipLevel: int,
turretId: string, turretRate: int, turretLv: int, headId: string,
roomRate: int, bulletPower: int }
// type=leave 时:
{ type: "leave", userId: string, playerId: string,
position: int, showWaiting: bool? }</pre></td><td>房间组</td><td>座位变更(加入/离开)</td></tr>
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> OnFishSpawn<span class="tag-new">NEW</span></td><td><pre>{
type: "init" | "spawn" | "sync", // init=进房补发,spawn=定时出鱼,sync=主动同步
timestamp: number, // 服务器当前时间戳(毫秒)
fish: [{
fishId: number, // 普通鱼ID;鱼阵时为首条普通鱼ID(兼容旧客户端)
fishNumber: string, // 普通鱼=鱼编号;鱼阵=""
routeId: number, // 普通鱼=路径编号;鱼阵=固定阵型编号(如71001)
fishArrayNumber: string,// 鱼阵编号(非鱼阵时为"")
fishIds: number[], // 所有出鱼必传;普通鱼=[fishId],鱼阵=阵内全部鱼ID
createdAt: number, // 鱼创建时间戳(毫秒),前端用它算路径进度
expireAt: number, // 鱼过期时间戳(毫秒)
remainingSeconds: int // 鱼剩余存活秒数
}]
}</pre></td><td>房间组(init/spawn)/调用者(sync)</td><td><code>fishIds</code> 在首次出鱼、定时出鱼、入房及重连同步中都必须提供。普通鱼为单元素集合 <code>[fishId]</code>;鱼阵按鱼阵配置的生成顺序使用 ID 集合。鱼阵的 <code>fishNumber</code> 为空,鱼信息由 <code>fishArrayNumber</code> 对应的本地配置确定。击中时把对应 ID 作为 KillFish 的 <code>fihsId</code> 原样传回。</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> KillNotice</td><td><pre>{
FishId: string, // 鱼编号
userName: string, // 玩家名
gold: long, // 金币
// 机器人模拟时额外字段:
fishName: string?, // 鱼名
multiplier: int? // 倍数
}</pre></td><td>全部</td><td>全局击杀公告(真人击杀/机器人模拟)</td></tr>
</table>
</div>
</div>
<!-- ==================== FishRoomHub ==================== -->
<div id="hub-fishroom" class="hub-section">
<div class="hub-card">
<div class="hub-header" style="background:#e67e22;">
<div class="path">FishRoomHub — <code>/fishingroomhub</code></div>
<div class="detail">WS,房间列表</div>
</div>
<h3>📤 客户端 → 服务器</h3>
<table>
<tr><th style="width:110px">方法</th><th style="width:380px">参数</th><th>说明</th></tr>
<tr><td><span class="tag tag-cs">C→S</span> GetRoomList</td><td><code>string userId, int gameType, int roomLevel, int roomPartition</code></td><td>获取房间列表。结果通过 GetRoomListResult 事件返回</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GetRoomInfo</td><td><code>string roomId</code></td><td>获取房间详情</td></tr>
</table>
<h3>📥 服务器 → 客户端</h3>
<table>
<tr><th style="width:170px">事件</th><th>数据</th><th>说明</th></tr>
<tr><td><span class="tag tag-sc">S→C</span> GetRoomListResult</td><td><code>List&lt;GameRoomReponse&gt;</code><br>GameRoomReponse: { RoomId, MaxPlayerCount, CurrentOnlinePlayerCount, RobotCount, RoomNumber }</td><td>房间列表(仅统计真人)</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RoomChange</td><td><code>RedisGameRoom</code></td><td>房间变化通知(人数、状态变更)</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GetRoomInfoResult</td><td><code>RedisGameRoom?</code></td><td>房间详情</td></tr>
</table>
</div>
</div>
<!-- ==================== ChessGameHub ==================== -->
<div id="hub-chessgame" class="hub-section">
<div class="hub-card">
<div class="hub-header" style="background:#8e44ad;">
<div class="path">ChessGameHub — <code>/chessgamehub</code></div>
<div class="detail">WS,中国象棋</div>
</div>
<h3>📤 客户端 → 服务器</h3>
<table>
<tr><th style="width:120px">方法</th><th style="width:380px">参数</th><th>说明</th></tr>
<tr><td><span class="tag tag-cs">C→S</span> CreateRoom</td><td><code>CreateRoomParam { roomId, userId, gameType(1=我方黑 2=我方红), password, gold }</code></td><td>创建房间</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> JoinGame</td><td><code>string gameId, string pwd, string userId</code></td><td>加入游戏</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GameReady</td><td><code>string gameId, string userId</code></td><td>准备</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> EndGame</td><td><code>string gameId, string winUserId</code></td><td>直接结束(对方获胜)</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> RequestEndGame</td><td><code>string gameId, string userId, string winUserId</code></td><td>请求结束(需双方确认)</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> RestartGame</td><td><code>string gameId, string userId</code></td><td>对局结束后重新开始</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GoldConfig</td><td><code>string gameId, long gold, string userId</code></td><td>房主设置金币</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> ExitRoom</td><td><code>string gameId, string userId</code></td><td>退出(房主退出清空房间,非房主保留房间)</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> Heartbeat</td><td><code>string gameId</code></td><td>心跳,更新活跃时间</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> CheckTimeout</td><td><code>string gameId</code></td><td>检查超时(60秒断线判负)</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GetRoomList</td><td><code>int partition=0</code></td><td>房间列表;0=全部分区,正数=指定分区</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> SyncGameData</td><td><code>string gameId, string data</code></td><td>同步棋子移动</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> GameOperation</td><td><code>string gameId, int type, string data</code><br>type: 1=求和 2=认输(自动结束) 3=悔棋</td><td>游戏操作</td></tr>
</table>
<h3>📥 服务器 → 客户端</h3>
<table>
<tr><th style="width:170px">事件</th><th>数据</th><th>目标</th></tr>
<tr><td><span class="tag tag-sc">S→C</span> GameError</td><td><code>string</code></td><td>调用者</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> BeginGame</td><td><code>ChessGameRoom</code></td><td>调用者/房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> JoinRoom</td><td><code>ChessGameRoom</code></td><td>调用者</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> JoinRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> Reconnect</td><td><code>{ userId, userName }</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> Disconnect</td><td><code>{ userId, disconnectTime, timeoutSeconds }</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> ReadyNotice</td><td><code>string userId</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> AllBeginTime</td><td><code>ChessGameRoom</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GameEnd</td><td><code>{ winUserId, room }</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> EndGameRequestSent</td><td><code>string</code></td><td>调用者</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> OpponentRequestEnd</td><td><code>string userId</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RestartGameSuccess</td><td><code>ChessGameRoom</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> RestartRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> ExitRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> ExitRoomSuccess</td><td><code>string userId</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> CreateRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GoldConfigSuccess</td><td><code>ChessGameRoom</code></td><td>房间组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GoldConfigRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GetRoomList</td><td><code>List&lt;ChessGameRoom&gt;</code></td><td>调用者</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> SyncGameData</td><td><code>string</code></td><td>全部</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> GameOperation</td><td><code>{ type, data }</code></td><td>全部</td></tr>
</table>
</div>
</div>
<!-- ==================== CustomerHub ==================== -->
<div id="hub-customer" class="hub-section">
<div class="hub-card">
<div class="hub-header" style="background:#16a085;">
<div class="path">CustomerHub — <code>/customerHub</code></div>
<div class="detail">WS,客服即时通讯。客服端需 JWT,玩家端无需 Token。</div>
</div>
<h3>📤 客户端 → 服务器</h3>
<table>
<tr><th style="width:140px">方法</th><th style="width:380px">参数</th><th>说明</th></tr>
<tr><td><span class="tag tag-cs">C→S</span> JoinConversation</td><td><code>string conversationId</code></td><td>加入会话房间组</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> LeaveConversation</td><td><code>string conversationId</code></td><td>离开会话房间组</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> SendMessage</td><td><code>string conversationId, int senderType, string senderId, string senderName, string? senderAvatar, int contentType, string content, string guId=""</code><br>senderType: 1=用户 2=客服<br>contentType: 1=文字 2=图片<br>conversationId 为空时自动创建新会话</td><td>发送消息</td></tr>
<tr><td><span class="tag tag-cs">C→S</span> StartConversation</td><td><code>string userId, string userName</code></td><td>主动发起新会话</td></tr>
</table>
<h3>📥 服务器 → 客户端</h3>
<table>
<tr><th style="width:170px">事件</th><th>数据</th><th>目标</th></tr>
<tr><td><span class="tag tag-sc">S→C</span> OnMessageReceived</td>
<td><pre>{
GuId: string,
ConversationId: string,
SenderType: int,
SenderId: string,
SenderName: string,
SenderAvatar: string?,
ContentType: int,
Content: string,
CreateTime: DateTime
}</pre></td><td>会话组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> OnConversationStarted</td><td><code>ConversationEntity</code></td><td>调用者</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> OnNewConversation</td><td><code>{ conversationId, customerId?, customerName?, userId?, userName?, status, createTime }</code></td><td>客服组</td></tr>
<tr><td><span class="tag tag-sc">S→C</span> ErrorMessage</td><td><code>string</code></td><td>调用者</td></tr>
</table>
<p style="padding:8px 12px;margin:10px 0 0;background:#f0faf0;border-radius:4px;"><strong>注意:</strong>客服连接时携带 JWT,服务端自动从 <code>uid</code> Claim 加入通知组,无需手动 JoinCustomerQueue。</p>
</div>
</div>
<!-- ==================== 公共说明 ==================== -->
<div id="hub-common" class="hub-section">
<div class="info-box">
<h3>🔗 Hub 路由总览</h3>
<table>
<tr><th>Hub</th><th>路由</th><th>认证</th></tr>
<tr><td>LoginHub</td><td><code>/loginhub</code></td><td>JWT</td></tr>
<tr><td>FishGameHub</td><td><code>/fishinghub</code></td><td>方法传参 userId</td></tr>
<tr><td>FishRoomHub</td><td><code>/fishingroomhub</code></td><td>方法传参 userId</td></tr>
<tr><td>ChessGameHub</td><td><code>/chessgamehub</code></td><td>方法传参 userId</td></tr>
<tr><td>CustomerHub</td><td><code>/customerHub</code></td><td>客服需JWT,玩家无需</td></tr>
</table>
</div>
<div class="info-box">
<h3>📦 通用数据结构</h3>
<table>
<tr><th style="width:180px">名称</th><th>字段</th></tr>
<tr><td><code>RedisGameRoom</code></td>
<td><pre>{
RoomId, RoomName, GameType, RoomLevel, RoomPartition,
RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
RobotCount, Players[], CreateUserId,
SystemFactor: double // 当前系统因子(根据当日盈利动态调整)
}</pre></td></tr>
<tr><td><code>RedisGamePlayer</code></td>
<td><pre>{
UserId, UserName, UserNumber, Avatar, IsRedName,
Level, VipLevel, Gold, Diamond, Position,
CannonId, WingsId, WarGodId, CannonMultiple,
BeginCoin, MaxGold, BeginTime, State(0=正常 1=断线),
CId, PlayerFactor, TotalRechargeGold, SafeBoxGold,
IsBot, IsAfk
}</pre></td></tr>
<tr><td><code>ChessGameRoom</code></td>
<td><pre>{
RoomId, state(1=等待 2=游戏中 3=已结束), users[],
gameType(1=我方黑 2=我方红), gold, password,
createUserId, robotCount, isGameEnded,
timeoutSeconds(默认60), endGameRequests, pendingWinUserId
}</pre></td></tr>
<tr><td><code>ConversationEntity</code></td>
<td><pre>{
UserId?, UserName?, CustomerId?, CustomerName?,
Status(0=等待 1=进行中 2=已结束), CreateTime,
EndTime?, LastReadTime?
}</pre></td></tr>
</table>
</div>
<div class="info-box">
<h3>📤 发送基础设施(IHubSend&lt;T&gt;)</h3>
<table>
<tr><th>方法</th><th>说明</th></tr>
<tr><td><code>SendUserCId(cid, method, obj)</code></td><td>发送给指定连接</td></tr>
<tr><td><code>SendUserUserId(userId, method, obj)</code></td><td>通过 userId 查找 CId 后发送</td></tr>
<tr><td><code>SendGroup(groupId, method, obj)</code></td><td>发送给 SignalR 群组</td></tr>
<tr><td><code>SendAll(method, obj)</code></td><td>广播给所有客户端</td></tr>
<tr><td><code>GroupExcept(cid, groupId, method, obj)</code></td><td>发送给群组排除指定连接</td></tr>
</table>
</div>
<div class="info-box">
<h3>流程说明</h3>
<h4>1. 登录 / Bind 流程 (LoginHub)</h4>
<ol>
<li>客户端连接 /loginhub (JWT)</li>
<li>调用 Bind(userId),加入 userId 组</li>
<li>Bind 读取 Redis LoginCId:
<ul>
<li>若存在旧 LoginCId 且不等于当前连接 -> 顶号:
<ul>
<li>发 AccountKicked 给旧 LoginHub 连接</li>
<li>查旧 LoginHub 关联的游戏 Hub CId,发 KickToMainPage</li>
</ul>
</li>
<li>更新 Redis LoginCId = 当前连接 CId</li>
</ul>
</li>
<li>检测断线房间 (PendingRoomId):
<ul>
<li>有 -> 发 PendingRoomNotice(弹窗:返回房间 / 离开)</li>
<li>无 -> 进入大厅</li>
</ul>
</li>
</ol>
<h4>2. 顶号流程</h4>
<ul>
<li>设备 A 登录:Bind -> LoginCId = cid_A</li>
<li>设备 B 登录:Bind -> LoginCId = cid_A != cid_B -> 发 AccountKicked 到 cid_A -> 更新 LoginCId = cid_B -> A 被顶</li>
<li>设备 C 登录:Bind -> LoginCId = cid_B != cid_C -> 发 AccountKicked 到 cid_B -> 更新 LoginCId = cid_C -> B 被顶</li>
<li>关键:仅 LoginHub.Bind 负责发 AccountKicked,FishGameHub 不再发送</li>
</ul>
<h4>3. JoinRoom 流程 (FishGameHub)</h4>
<ol>
<li>客户端调 JoinRoom(roomId, userId, posituin)</li>
<li>检查玩家是否已在房间 -> 是则返回</li>
<li>获取/创建房间 roomInfo</li>
<li>判断是否首个真人进房:
<ul>
<li>是 -> RegisterRoom(roomId, roomLevel) 注册出鱼服务</li>
<li>否 -> GetActiveFishes(roomId) 获取当前活跃鱼列表</li>
</ul>
</li>
<li>发 RoomInfo(cid, room, activeFishes) 给调用者</li>
<li>发 OnSeatChange(join) 通知房间组</li>
</ol>
<h4>4. Reconnect 流程 (FishGameHub)</h4>
<ol>
<li>客户端连接 FishGameHub 后立即调 Reconnect(userId)</li>
<li>查 Redis 有无断线房间 (PendingRoomId):
<ul>
<li>有 -> 恢复玩家状态 State=0,走 JoinRoom 逻辑(含鱼同步)</li>
<li>无 -> 无操作,等待后续指令</li>
</ul>
</li>
<li>返回结果:RoomInfo(首次完整鱼群放入 activeFishes;约 300ms 后仅兼容补发普通散鱼,Boss、黄金鱼、特殊鱼和鱼阵不重复补发)</li>
</ol>
<h4>5. CancelPlayerAfk 流程</h4>
<ol>
<li>客户端调 CancelPlayerAfk(roomId, userId)</li>
<li>清除玩家 IsAfk 标记</li>
<li>GetActiveFishes(roomId) 获取当前活跃鱼</li>
<li>发 RoomInfo(cid, room, activeFishes) 给调用者(同步鱼状态)</li>
<li>发 PlayerCancelAfkNotice 通知房间组</li>
</ol>
<h4>6. LeaveRoom / 退出流程</h4>
<ol>
<li>客户端调 LeaveRoom(roomId, userId)</li>
<li>金币结算,从房间移除玩家</li>
<li>发 RoomLeaveNotice 通知房间组</li>
<li>检查剩余真人:
<ul>
<li>无真人 -> UnregisterRoom(roomId) 注销出鱼 + 清理房间</li>
<li>有真人 -> 房主转移(若原房主退出),发 ChangeControllerUser</li>
</ul>
</li>
</ol>
<h4>7. 房主断线 / 所有权转移</h4>
<ol>
<li>房主断线触发 HandleDisconnect</li>
<li>查找下一个非挂机真人作为新房主:
<ul>
<li>找到 -> 转移所有权,发 ChangeControllerUser(新房主 userId) 通知房间组</li>
<li>未找到 -> 发 HostDisconnect(前端禁用发炮/出鱼,显示等待提示)</li>
</ul>
</li>
</ol>
<h4>8. 出鱼服务 (FishSpawnService)</h4>
<ul>
<li>首个真人进房 -> RegisterRoom -> 启动定时器持续出鱼</li>
<li>定时出鱼 -> OnFishSpawn(type=spawn) 广播房间组</li>
<li>后续真人进房/重连/取消挂机 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回,并约 300ms 后仅补发普通散鱼 OnFishSpawn(type=init)</li>
<li>最后一个真人退出/断线 -> UnregisterRoom -> 停止定时器 + 清除活跃鱼</li>
</ul>
</div>
</div>
</div><!-- /main -->
</div><!-- /layout -->
<script>
function switchTab(el) {
document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
el.classList.add('active');
document.querySelectorAll('.hub-section').forEach(function(s) { s.classList.remove('active'); });
var target = document.getElementById(el.getAttribute('data-target'));
if (target) target.classList.add('active');
}
</script>
</body>
</html>
@@ -0,0 +1,125 @@
<title>钻石充值列表</title>
<div class="layui-card layadmin-header">
<div class="layui-breadcrumb" lay-filter="breadcrumb">
<a lay-href="">主页</a>
<a><cite>商城</cite></a>
<a><cite>钻石充值列表</cite></a>
</div>
</div>
<table id="playerconfig" lay-filter="playerconfig"></table>
<script type="text/html" id="table-admin">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit"><i class="layui-icon layui-icon-edit"></i>修改</a>
</script>
<script>
layui.use(['table', 'admin'], function () {
var $ = layui.$
, admin = layui.admin
, view = layui.view
, table = layui.table
, setter = layui.setter
, form = layui.form;
//第一个实例
table.render({
elem: '#playerconfig',
url: setter.baseurl + "adminapi/mall/GetOrderList",
method: "post",
contentType: "application/json",
page: true,
where: {
keyWord: "",
state: -1
},
request: {
pageName: 'pageIndex' //页码的参数名称,默认:page
, limitName: 'pageSize' //每页数据量的参数名,默认:limit
},
limit: 10,
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
cols: [
[
{
field: "name",
title: "用户名",
templet: function (d) {
return d.createInfo.userName
}
}, {
field: "productName",
title: "商品",
templet: function (d) {
return d.productInfo.name
}
}, {
field: "price",
title: "单价"
},
{
field: "payCount",
title: "购买数量"
},
{
field: "payMoney",
title: "支付总额"
}
, {
field: "registerTime",
title: "购买时间",
templet: function (d) {
return formatTimestamp(d.createInfo.optionTime);
}
}]
],
text: "对不起,加载出现异常!",
response: {
statusName: 'code', //数据状态的字段名称,默认:code
statusCode: 0, //成功的状态码,默认:0
msgName: 'info', //状态信息的字段名称,默认:msg
countName: 'count', //数据总数的字段名称,默认:count
dataName: 'data' //数据列表的字段名称,默认:data
}
});
});
// 将时间戳(秒)转换为指定格式的时间
function formatTimestamp(timestamp, format = 'yyyy-MM-dd hh:mm:ss') {
if (!timestamp) return '';
// 时间戳是秒数,需要转为毫秒
const date = new Date(timestamp * 1000);
const o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S': date.getMilliseconds() // 毫秒
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(format)) {
format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ?
(o[k]) : (('00' + o[k]).substr(('' + o[k]).length)));
}
}
return format;
}
</script>
@@ -0,0 +1,251 @@
<div class="layui-form" lay-filter="layuiadmin-form-admin" style="padding: 20px 30px 0 0;">
<div class="layui-form-item">
<label class="layui-form-label">产品名称</label>
<div class="layui-input-inline">
<script type="text/html" template>
<input type="text" name="id" value="{{ d.params.id}}" class="layui-input" style="display:none">
</script>
<script type="text/html" template>
<input type="text" name="name" value="{{ d.params.name || '' }}" lay-verify="required" placeholder="请输入产品名称" autocomplete="off" class="layui-input">
</script>
<script type="text/html" lay-done="sendParams(d.params)" template>
</script>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">图片</label>
<div class="layui-input-inline">
<script type="text/html" template>
<button type="button" class="layui-btn" id="layuiadmin-upload-admin">
<i class="layui-icon layui-icon-upload"></i> 图片上传
</button>
<div style="width: 132px;">
<div class="layui-upload-list">
<img class="layui-upload-img" id="ID-upload-demo-img" src="{{ d.params.imageUrl || '' }}" style="width: 100%; height: 92px;">
<div id="ID-upload-demo-text"></div>
<input type="text" name="imageUrl" id="ID-upload-demo-img-v" value="{{ d.params.imageUrl || '' }}" style="display:none" />
</div>
<div class="layui-progress layui-progress-big" lay-showPercent="yes" lay-filter="filter-demo">
<div class="layui-progress-bar" lay-percent=""></div>
</div>
</div>
</script>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">道具</label>
<div class="layui-input-inline">
<script type="text/html" template>
<select name="baseProduct" id="baseProduct" class="layui-select" lay-filter="baseProduct">
<option value="" {{ d.params.detail?.productInfo.number==''?"selected":"" }}> 未选择</option>
</select>
</script>
</div>
</div>
<script type="text/html" template>
<div class="layui-form-item">
<label class="layui-form-label">现金</label>
<div class="layui-input-inline">
<input type="number" name="moneyPrice" value="{{d.params.moneySaleInfo?.price??0}}" placeholder="金额" autocomplete="off" step="0" class="layui-input">
</div>
<label class="layui-form-label">现金限购</label>
<div class="layui-input-inline">
<input type="number" name="moneyLimitCount" value="{{d.params.moneySaleInfo?.limitCount??0}}" placeholder="限购" autocomplete="off" step="0" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">钻石</label>
<div class="layui-input-inline">
<input type="number" name="diamondPrice" value="{{d.params.diamondSaleInfo?.price??0}}" placeholder="金额" autocomplete="off" step="0" class="layui-input">
</div>
<label class="layui-form-label">钻石限购</label>
<div class="layui-input-inline">
<input type="number" name="diamondLimitCount" value="{{d.params.diamondSaleInfo?.limitCount??0}}" placeholder="限购" autocomplete="off" step="0" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">积分</label>
<div class="layui-input-inline">
<input type="number" name="scorePrice" value="{{d.params.scoreSaleInfo?.price??0}}" placeholder="金额" autocomplete="off" step="0" class="layui-input">
</div>
<label class="layui-form-label">积分限购</label>
<div class="layui-input-inline">
<input type="number" name="scoreLimitCount" value="{{d.params.scoreSaleInfo?.limitCount??0}}" placeholder="限购" autocomplete="off" step="0" class="layui-input">
</div>
</div>
</script>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-inline">
<input type="button" lay-submit lay-filter="LAY-product-back-submit" value="确认" class="layui-btn">
</div>
</div>
</div>
<script>
var productId = '';
var isLayUI = false;
function sendParams(params) {
productId = params.productInfo.number;
if (isLayUI) {
layuiLoad();
}
}
layuiLoad();
function layuiLoad() {
layui.use(['admin', 'form', 'upload'], function () {
var $ = layui.$
, form = layui.form
, admin = layui.admin
, setter = layui.setter
, upload = layui.upload
, element = layui.element;
bindSelect();
function bindSelect() {
admin.req({
method: "get",
contentType: "application/json;charset=UTF-8",
dataType: "json",
url: setter.baseurl + "adminapi/GameConfig",//实际使用请改成服务端真实接口
async: false,
done: function (res) {
$('#baseProduct').append("<optgroup label='宝箱' selected>");
res.data.boxConfig.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='钻石'>");
res.data.diamondRecharge.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='鱼食'>");
res.data.fishFood.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='鱼苗'>");
res.data.fishSeeds.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='锦鲤碎片'>");
res.data.fishFragments.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='养护道具'>");
res.data.careProp.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='装饰道具'>");
res.data.decorations.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
$('#baseProduct').append("<optgroup label='锦鲤'>");
res.data.fishConfig.forEach(function (option) {
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
});
$('#baseProduct').append("</optgroup>");
form.render('select');
}
})
}
form.on('select(baseProduct)', function (data) {
//console.log(data.elem); //得到select原始DOM对象
//console.log(data.value); //得到被选中的值
console.log(data.othis); //得到美化后的DOM对象
});
var uploadInst = upload.render({
elem: '#layuiadmin-upload-admin',
url: setter.baseurl + "adminapi/ImageUpload/upload", // 实际使用时改成您自己的上传接口即可。
before: function (obj) {
obj.preview(function (index, file, result) {
$('#ID-upload-demo-img').attr('src', result); // 图片链接(base64)
});
element.progress('filter-demo', '0%'); // 进度条复位
layer.msg('上传中', { icon: 16, time: 0 });
},
done: function (res) {
// 若上传失败
if (res.code > 0) {
return layer.msg('上传失败');
}
console.log($('#ID-upload-demo-img-v').val())
// 上传成功的一些操作
$('#ID-upload-demo-img-v').val(setter.baseurl + res.path);
$('#ID-upload-demo-text').html(res.path); // 置空上传失败的状态
},
error: function () {
// 演示失败状态,并实现重传
var demoText = $('#ID-upload-demo-text');
demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-xs demo-reload">重试</a>');
demoText.find('.demo-reload').on('click', function () {
uploadInst.upload();
});
},
// 进度条
progress: function (n, elem, e) {
element.progress('filter-demo', n + '%'); // 可配合 layui 进度条元素使用
if (n == 100) {
layer.msg('上传完毕', { icon: 1 });
}
}
});
})
}
</script>
@@ -0,0 +1,110 @@
<title>商品列表</title>
<div class="layui-card layadmin-header">
<div class="layui-breadcrumb" lay-filter="breadcrumb">
<a lay-href="">主页</a>
<a><cite>商城</cite></a>
<a><cite>商品列表</cite></a>
</div>
</div>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-body">
<div style="padding-bottom: 10px;">
<button class="layui-btn layuiadmin-btn-admin" data-type="add" >添加</button>
</div>
<table id="LAY-user-back-manage" lay-filter="LAY-user-back-manage"></table>
<script type="text/html" id="table-product-admin">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit"><i class="layui-icon layui-icon-edit"></i>编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del"><i class="layui-icon layui-icon-delete"></i>删除</a>
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="state"><i class="layui-icon layui-icon-edit"></i>上下架</a>
</script>
</div>
</div>
</div>
<script>
layui.use('productlistjs', layui.factory('productlistjs')).use(['admin', 'table'], function () {
var $ = layui.$
, admin = layui.admin
, view = layui.view
, table = layui.table
, setter = layui.setter
, form = layui.form;
form.render(null, 'layadmin-userfront-formlist');
//事件
var active = {
add: function () {
admin.popup({
title: '添加商品'
, area: ["500px", "800px"]
, id: 'LAY-popup-product-add'
, success: function (layero, index) {
view(this.id).render('mall/productform').done(function () {
form.render(null, 'layuiadmin-form-admin');
//监听提交
form.on('submit(LAY-product-back-submit)', function (data) {
var aa = $('#baseProduct').val();
console.log(aa)
console.log(data)
var param = {
name: data.field.name,
imageUrl: data.field.imageUrl,
detailType: parseInt(data.field.detailType),
number: data.field.baseProduct
};
param.moneySaleInfo = { limitCount: data.field.moneyLimitCount,price: parseFloat(data.field.moneyPrice) }
param.diamondSaleInfo = { limitCount: data.field.diamondLimitCount, price: parseFloat(data.field.diamondPrice) }
param.scoreSaleInfo = { limitCount: data.field.scoreLimitCount, price: parseFloat(data.field.scorePrice) }
//提交数据
admin.req({
method: "post",
contentType: "application/json;charset=UTF-8",
dataType: "json",
headers: {
'token': layui.data(setter.tableName)[setter.request.tokenName]
},
url: setter.baseurl + "adminapi/mall/AddProduct",//实际使用请改成服务端真实接口
async: false,
data: JSON.stringify(param),
done: function (res) {
//$.ajax({});
layui.table.reload('LAY-user-back-manage'); //重载表格
layer.close(index); //执行关闭
}
})
//提交 Ajax 成功后,关闭当前弹层并重载表格
});
});
}
});
}
};
$('.layui-btn.layuiadmin-btn-admin').on('click', function () {
var type = $(this).data('type');
active[type] ? active[type].call(this) : '';
});
});
</script>
@@ -0,0 +1,2 @@
该目录存放【设置】的视图文件
@@ -0,0 +1,2 @@
该目录存放【用户】的视图文件