Initial commit: 投注游戏平台初始化
This commit is contained in:
Executable
+1
@@ -0,0 +1 @@
|
||||
.ace-tool/
|
||||
@@ -0,0 +1,8 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
Options +FollowSymlinks -Multiviews
|
||||
RewriteEngine On
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
|
||||
</IfModule>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
+274
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class AdminController extends AdminBaseController {
|
||||
/**
|
||||
* 构造函数 - 验证管理员权限
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 管理员列表页面
|
||||
*/
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
// 只查询管理员角色的用户
|
||||
$admins = $db->select('users', '*', [
|
||||
'role' => 'admin',
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
$this->render('Admin/admin.php', [
|
||||
'admins' => $admins,
|
||||
'title' => '管理员管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个管理员数据(用于编辑和详情)
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的管理员ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$admin = $db->get('users', '*', [
|
||||
'id' => $id,
|
||||
'role' => 'admin' // 确保只获取管理员
|
||||
]);
|
||||
|
||||
if ($admin) {
|
||||
// 移除密码字段,避免泄露
|
||||
unset($admin['password']);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $admin
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '管理员不存在'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function update() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
$isEditMode = $id > 0;
|
||||
|
||||
$username = trim($data['username'] ?? '');
|
||||
$email = trim($data['email'] ?? '');
|
||||
$status = isset($data['status']) ? (int)$data['status'] : 0;
|
||||
|
||||
if (empty($username) || empty($email)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户名和邮箱不能为空'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '邮箱格式不正确'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 统一查重(排除当前 ID)
|
||||
$exists = $db->get('users', '*', [
|
||||
'AND' => [
|
||||
'OR' => [
|
||||
'username' => $username,
|
||||
'email' => $email
|
||||
],
|
||||
'id[!]' => $id
|
||||
]
|
||||
]);
|
||||
|
||||
if ($exists) {
|
||||
if ($exists['username'] === $username) {
|
||||
$msg = '用户名已存在';
|
||||
} elseif ($exists['email'] === $email) {
|
||||
$msg = '邮箱已被使用';
|
||||
} else {
|
||||
$msg = '用户名或邮箱已被占用';
|
||||
}
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $msg
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备数据 - 强制角色为 admin
|
||||
$adminData = [
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'role' => 'admin', // 强制设置为管理员角色
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
if ($isEditMode) {
|
||||
// 验证是否真的是管理员
|
||||
$existingAdmin = $db->get('users', 'id', [
|
||||
'id' => $id,
|
||||
'role' => 'admin'
|
||||
]);
|
||||
|
||||
if (!$existingAdmin) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该用户不是管理员'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果密码不为空,更新密码
|
||||
if (!empty($data['password'])) {
|
||||
if (strlen($data['password']) < 8) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '密码长度至少8位'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$adminData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
$db->update('users', $adminData, ['id' => $id]);
|
||||
$message = '管理员更新成功';
|
||||
} else {
|
||||
// 新增时必须有密码
|
||||
if (empty($data['password']) || strlen($data['password']) < 8) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '密码长度至少8位'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$adminData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
$adminData['created_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
$id = $db->insert('users', $adminData);
|
||||
$message = '管理员创建成功';
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
*/
|
||||
public function delete($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的管理员ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁止删除自己
|
||||
session_start();
|
||||
if ($id == $_SESSION['user_id']) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '不能删除当前登录的管理员'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 确保只删除管理员角色
|
||||
$adminExists = $db->get('users', 'id', [
|
||||
'id' => $id,
|
||||
'role' => 'admin'
|
||||
]);
|
||||
|
||||
if (!$adminExists) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '管理员不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否还有其他管理员
|
||||
$adminCount = $db->count('users', [
|
||||
'role' => 'admin'
|
||||
]);
|
||||
|
||||
if ($adminCount <= 1) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '不能删除最后一个管理员'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->delete('users', [
|
||||
'id' => $id,
|
||||
'role' => 'admin'
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '管理员已删除'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class AgentController extends AdminBaseController {
|
||||
private $db;
|
||||
public function __construct(Database $db) { $this->db = $db; }
|
||||
|
||||
// 后台代理管理
|
||||
public function index() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$agents = $this->db->select('agents', '*', ['ORDER' => ['id' => 'DESC']]);
|
||||
foreach ($agents as &$a) {
|
||||
$a['user'] = $this->db->get('users', ['username','balance'], ['id' => $a['user_id']]);
|
||||
$a['player_count'] = $this->db->count('users', ['agent_id' => $a['id']]);
|
||||
}
|
||||
$this->render('Admin/agents.php', compact('agents'));
|
||||
}
|
||||
|
||||
public function update() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
if ($id > 0) {
|
||||
$this->db->update('agents', [
|
||||
'commission_rate' => floatval($data['commission_rate'] ?? 0),
|
||||
'rebate_rate' => floatval($data['rebate_rate'] ?? 0),
|
||||
'status' => (int)($data['status'] ?? 1),
|
||||
], ['id' => $id]);
|
||||
} else {
|
||||
// 创建代理:先创建用户,再创建代理记录
|
||||
$userId = (int)($data['user_id'] ?? 0);
|
||||
if (!$userId) { $this->json(['status'=>'error','message'=>'User ID required']); return; }
|
||||
$code = strtoupper(substr(md5(uniqid()), 0, 8));
|
||||
$this->db->insert('agents', [
|
||||
'user_id' => $userId,
|
||||
'parent_id' => !empty($data['parent_id']) ? (int)$data['parent_id'] : null,
|
||||
'level' => !empty($data['parent_id']) ? 2 : 1,
|
||||
'agent_code' => $code,
|
||||
'commission_rate' => floatval($data['commission_rate'] ?? 0),
|
||||
'rebate_rate' => floatval($data['rebate_rate'] ?? 0),
|
||||
'status' => 1,
|
||||
]);
|
||||
$this->db->update('users', ['role' => 'agent'], ['id' => $userId]);
|
||||
}
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$agent = $this->db->get('agents', '*', ['id' => (int)$id]);
|
||||
if ($agent) {
|
||||
$this->db->update('users', ['role' => 'user', 'agent_id' => null], ['agent_id' => $agent['id']]);
|
||||
$this->db->delete('agents', ['id' => (int)$id]);
|
||||
}
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
// 代理赔率设置
|
||||
public function odds($agentId) {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$odds = $this->db->select('agent_odds', '*', ['agent_id' => (int)$agentId]);
|
||||
$this->json(['status' => 'success', 'data' => $odds]);
|
||||
}
|
||||
|
||||
public function updateOdds() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$agentId = (int)($data['agent_id'] ?? 0);
|
||||
$odds = $data['odds'] ?? [];
|
||||
$gameId = (int)($data['game_id'] ?? 0);
|
||||
|
||||
foreach ($odds as $o) {
|
||||
$existing = $this->db->get('agent_odds', 'id', [
|
||||
'agent_id' => $agentId, 'game_id' => $gameId,
|
||||
'bet_type' => $o['bet_type'], 'bet_target' => $o['bet_target']
|
||||
]);
|
||||
if ($existing) {
|
||||
$this->db->update('agent_odds', ['odds' => floatval($o['odds'])], ['id' => $existing]);
|
||||
} else {
|
||||
$this->db->insert('agent_odds', [
|
||||
'agent_id' => $agentId, 'game_id' => $gameId,
|
||||
'bet_type' => $o['bet_type'], 'bet_target' => $o['bet_target'],
|
||||
'odds' => floatval($o['odds']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
// 佣金记录
|
||||
public function commissions($agentId) {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$records = $this->db->select('agent_commissions', '*', [
|
||||
'agent_id' => (int)$agentId, 'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
|
||||
]);
|
||||
$this->json(['status' => 'success', 'data' => $records]);
|
||||
}
|
||||
|
||||
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
|
||||
}
|
||||
Executable
+179
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace App\Controllers\admin;
|
||||
use \Db\Database;
|
||||
class AuthController {
|
||||
public function loginPage($error = '') {
|
||||
include __DIR__ . '/../../Views/Admin/login.php';
|
||||
}
|
||||
|
||||
public function loginSubmit() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rawData = file_get_contents('php://input');
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
// 检查 JSON 格式
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '提交的数据格式有误'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取输入参数
|
||||
$usernameOrEmail = trim($data['username_or_email'] ?? '');
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
if (empty($usernameOrEmail) || empty($password)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请输入用户名/邮箱和密码'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 查询用户(匹配用户名或邮箱,忽略邮箱大小写)
|
||||
$user = $db->get('users', '*', [
|
||||
'OR' => [
|
||||
'username' => $usernameOrEmail,
|
||||
'email' => strtolower($usernameOrEmail)
|
||||
]
|
||||
]);
|
||||
|
||||
if (!$user) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否禁用(假设 status=0 表示禁用)
|
||||
if (isset($user['status']) && $user['status'] == 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该账户未启用,请联系管理员'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '密码错误'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置会话
|
||||
session_start();
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
$_SESSION['last_activity'] = time();
|
||||
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
|
||||
$_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '登录成功,正在跳转...',
|
||||
'redirect' => '/admin/dashboard'
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: /admin/login');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function registerSubmit() {
|
||||
header('Content-Type: application/json');
|
||||
$rawData = file_get_contents('php://input');
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
$username = trim($data['username'] ?? '');
|
||||
$email = strtolower(trim($data['email'] ?? '')); // 统一转小写
|
||||
$password = $data['password'] ?? '';
|
||||
$confirm = $data['confirm_password'] ?? '';
|
||||
|
||||
if (!$username || !$email || !$password || !$confirm) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请完整填写注册信息'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '邮箱格式不正确'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($password !== $confirm) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '两次密码输入不一致'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 检查用户名或邮箱是否已存在(邮箱忽略大小写)
|
||||
$existing = $db->get('users', '*', [
|
||||
'OR' => [
|
||||
'username' => $username,
|
||||
'email' => $email
|
||||
]
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
if ($existing['username'] === $username) {
|
||||
$msg = '用户名已存在';
|
||||
} elseif (strtolower($existing['email']) === $email) {
|
||||
$msg = '邮箱已存在';
|
||||
} else {
|
||||
$msg = '用户名或邮箱已存在';
|
||||
}
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $msg
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
$insertResult = $db->insert('users', [
|
||||
'username' => $username,
|
||||
'email' => $email, // 已转小写
|
||||
'password' => $hashedPassword,
|
||||
'role' => 'user', // 默认角色
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
if ($insertResult) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '注册成功,请登录',
|
||||
'redirect' => '/admin/login'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '注册失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
/**
|
||||
* 自动开期设置控制器
|
||||
*/
|
||||
class AutoPeriodController extends AdminBaseController {
|
||||
private $db;
|
||||
|
||||
public function __construct(Database $db) {
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动开期设置页面
|
||||
*/
|
||||
public function index() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
|
||||
// 获取所有游戏
|
||||
$games = $this->db->select('games', '*', [
|
||||
'status' => 1,
|
||||
'ORDER' => ['id' => 'ASC']
|
||||
]);
|
||||
|
||||
// 获取最近的自动开期日志
|
||||
$logs = [];
|
||||
try {
|
||||
$logs = $this->db->select('auto_period_log', '*', [
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 50
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 表可能不存在
|
||||
}
|
||||
|
||||
$this->render('Admin/auto_period.php', compact('games', 'logs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏的自动开期设置
|
||||
*/
|
||||
public function update() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($data)) $data = $_POST;
|
||||
|
||||
$gameId = (int)($data['game_id'] ?? 0);
|
||||
if ($gameId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => '无效的游戏ID']);
|
||||
return;
|
||||
}
|
||||
|
||||
$game = $this->db->get('games', '*', ['id' => $gameId]);
|
||||
if (!$game) {
|
||||
echo json_encode(['success' => false, 'message' => '游戏不存在']);
|
||||
return;
|
||||
}
|
||||
|
||||
$updateData = [];
|
||||
|
||||
// 自动开期开关
|
||||
if (isset($data['auto_period_enabled'])) {
|
||||
$updateData['auto_period_enabled'] = (int)$data['auto_period_enabled'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// 每期时长(秒)
|
||||
if (isset($data['period_duration'])) {
|
||||
$duration = (int)$data['period_duration'];
|
||||
if ($duration < 60) $duration = 60; // 最少1分钟
|
||||
if ($duration > 3600) $duration = 3600; // 最多1小时
|
||||
$updateData['period_duration'] = $duration;
|
||||
}
|
||||
|
||||
// 封盘提前时间(秒)
|
||||
if (isset($data['lock_before_end'])) {
|
||||
$lockTime = (int)$data['lock_before_end'];
|
||||
if ($lockTime < 5) $lockTime = 5;
|
||||
if ($lockTime > 120) $lockTime = 120;
|
||||
$updateData['lock_before_end'] = $lockTime;
|
||||
}
|
||||
|
||||
if (empty($updateData)) {
|
||||
echo json_encode(['success' => false, 'message' => '无更新内容']);
|
||||
return;
|
||||
}
|
||||
|
||||
$updateData['updated_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
try {
|
||||
$this->db->update('games', $updateData, ['id' => $gameId]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置已保存',
|
||||
'data' => $updateData
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode(['success' => false, 'message' => '保存失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量启用/禁用
|
||||
*/
|
||||
public function toggleAll() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$enabled = (int)($data['enabled'] ?? 0);
|
||||
|
||||
try {
|
||||
$this->db->update('games', [
|
||||
'auto_period_enabled' => $enabled,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['status' => 1]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $enabled ? '已全部启用自动开期' : '已全部关闭自动开期'
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode(['success' => false, 'message' => '操作失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自动开期运行状态
|
||||
*/
|
||||
public function status() {
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$games = $this->db->select('games', ['id', 'name', 'type', 'auto_period_enabled', 'period_duration', 'lock_before_end'], [
|
||||
'status' => 1,
|
||||
'ORDER' => ['id' => 'ASC']
|
||||
]);
|
||||
|
||||
// 每个游戏的当前期号
|
||||
foreach ($games as &$g) {
|
||||
$g['current_period'] = $this->db->get('periods', ['id', 'period_number', 'status', 'start_time', 'end_time'], [
|
||||
'game_id' => $g['id'],
|
||||
'status' => ['pending', 'locked', 'drawn'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
}
|
||||
|
||||
// 最近日志
|
||||
$recentLogs = [];
|
||||
try {
|
||||
$recentLogs = $this->db->select('auto_period_log', '*', [
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 10
|
||||
]);
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'games' => $games,
|
||||
'logs' => $recentLogs,
|
||||
'server_time' => date('Y-m-d H:i:s'),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class BetController extends AdminBaseController {
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 投注记录列表页面
|
||||
*/
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
|
||||
// 分页参数
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$limit = 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// 筛选参数
|
||||
$where = [];
|
||||
if (isset($_GET['user_id']) && !empty($_GET['user_id'])) {
|
||||
$where['bets.user_id'] = $_GET['user_id'];
|
||||
}
|
||||
if (isset($_GET['period_number']) && !empty($_GET['period_number'])) {
|
||||
$where['bets.period_number'] = $_GET['period_number'];
|
||||
}
|
||||
if (isset($_GET['status']) && !empty($_GET['status'])) {
|
||||
$where['bets.status'] = $_GET['status'];
|
||||
}
|
||||
|
||||
// 排序
|
||||
$where['ORDER'] = ['bets.id' => 'DESC'];
|
||||
$where['LIMIT'] = [$offset, $limit];
|
||||
|
||||
try {
|
||||
// 获取投注记录,关联用户表和游戏表
|
||||
$bets = $db->select('bets', [
|
||||
'[>]users' => ['user_id' => 'id'],
|
||||
'[>]games' => ['game_id' => 'id']
|
||||
], [
|
||||
'bets.id',
|
||||
'bets.user_id',
|
||||
'users.username',
|
||||
'users.balance', // 当前余额
|
||||
'games.name(game_name)', // 游戏名称
|
||||
'bets.period_number',
|
||||
'bets.bet_type',
|
||||
'bets.bet_value',
|
||||
'bets.amount',
|
||||
'bets.odds',
|
||||
'bets.win_amount',
|
||||
'bets.status',
|
||||
'bets.created_at',
|
||||
'bets.settled_at'
|
||||
], $where);
|
||||
|
||||
// 获取总记录数用于分页(移除LIMIT)
|
||||
unset($where['LIMIT']);
|
||||
$total = $db->count('bets', [
|
||||
'[>]users' => ['user_id' => 'id'],
|
||||
'[>]games' => ['game_id' => 'id']
|
||||
], '*', $where);
|
||||
|
||||
$totalPages = ceil($total / $limit);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// 记录错误以便调试
|
||||
error_log("BetController Error: " . $e->getMessage());
|
||||
$bets = [];
|
||||
$total = 0;
|
||||
$totalPages = 0;
|
||||
}
|
||||
|
||||
$this->render('Admin/bets.php', [
|
||||
'bets' => $bets,
|
||||
'currentPage' => $page,
|
||||
'totalPages' => $totalPages,
|
||||
'title' => '投注记录'
|
||||
]);
|
||||
}
|
||||
}
|
||||
+734
@@ -0,0 +1,734 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class DicePeriodController extends AdminBaseController {
|
||||
private $gameType = 'dice';
|
||||
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 骰子游戏期号列表页面
|
||||
*/
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
try {
|
||||
// 获取骰子游戏列表
|
||||
$gamesList = $db->select('games', ['id', 'name'], [
|
||||
'type' => $this->gameType,
|
||||
'status' => 1,
|
||||
'ORDER' => ['id' => 'ASC']
|
||||
]);
|
||||
|
||||
if (!is_array($gamesList)) {
|
||||
$gamesList = [];
|
||||
}
|
||||
|
||||
// 获取所有骰子游戏的game_id
|
||||
$gameIds = array_column($gamesList, 'id');
|
||||
|
||||
// 获取骰子游戏的期号列表
|
||||
$periods = [];
|
||||
if (!empty($gameIds)) {
|
||||
$periods = $db->select('periods', '*', [
|
||||
'game_id' => $gameIds,
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 100
|
||||
]);
|
||||
}
|
||||
|
||||
if (!is_array($periods)) {
|
||||
$periods = [];
|
||||
}
|
||||
|
||||
// 构建游戏名称映射
|
||||
$games = [];
|
||||
foreach ($gamesList as $game) {
|
||||
$games[$game['id']] = $game['name'];
|
||||
}
|
||||
|
||||
// 为每个骰子游戏获取当前期号
|
||||
$currentPeriods = [];
|
||||
foreach ($gamesList as $game) {
|
||||
$currentPeriod = $db->get('periods', '*', [
|
||||
'game_id' => $game['id'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if ($currentPeriod) {
|
||||
$currentPeriods[$game['id']] = $currentPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$periods = [];
|
||||
$games = [];
|
||||
$gamesList = [];
|
||||
$currentPeriods = [];
|
||||
}
|
||||
|
||||
$this->render('Admin/dice_periods.php', [
|
||||
'currentPeriods' => $currentPeriods,
|
||||
'periods' => $periods,
|
||||
'games' => $games,
|
||||
'gamesList' => $gamesList,
|
||||
'title' => '骰子游戏期号管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个期号数据
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$period = $db->get('periods', '*', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($period) {
|
||||
// 验证是否为骰子游戏期号
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if ($game && $game['type'] === $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $period
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于骰子游戏'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '获取数据失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 录入开奖结果
|
||||
*/
|
||||
public function draw() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否为骰子游戏
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于骰子游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] === 'settled') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号已结算,无法修改'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$auto = isset($data['auto']) ? (bool)$data['auto'] : false;
|
||||
|
||||
// 骰子游戏开奖逻辑
|
||||
if ($auto) {
|
||||
$dice1 = rand(1, 6);
|
||||
$dice2 = rand(1, 6);
|
||||
$dice3 = rand(1, 6);
|
||||
} else {
|
||||
$dice1 = isset($data['dice1']) ? (int)$data['dice1'] : 0;
|
||||
$dice2 = isset($data['dice2']) ? (int)$data['dice2'] : 0;
|
||||
$dice3 = isset($data['dice3']) ? (int)$data['dice3'] : 0;
|
||||
|
||||
// 验证骰子点数
|
||||
if ($dice1 < 1 || $dice1 > 6 || $dice2 < 1 || $dice2 > 6 || $dice3 < 1 || $dice3 > 6) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '骰子点数必须在1-6之间'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总和
|
||||
$total = $dice1 + $dice2 + $dice3;
|
||||
|
||||
// 计算结果
|
||||
$result = $this->calculateResult($dice1, $dice2, $dice3, $total);
|
||||
|
||||
// 获取当前登录的管理员ID(审核人)
|
||||
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
|
||||
// 更新期号数据
|
||||
$updateData = [
|
||||
'dice1' => $dice1,
|
||||
'dice2' => $dice2,
|
||||
'dice3' => $dice3,
|
||||
'total' => $total,
|
||||
'result' => $result,
|
||||
'status' => 'drawn',
|
||||
'draw_time' => date('Y-m-d H:i:s'),
|
||||
'approved_by' => $approvedBy,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$responseData = [
|
||||
'dice1' => $dice1,
|
||||
'dice2' => $dice2,
|
||||
'dice3' => $dice3,
|
||||
'total' => $total,
|
||||
'result' => $result
|
||||
];
|
||||
|
||||
try {
|
||||
$db->update('periods', $updateData, ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '开奖结果已录入',
|
||||
'data' => $responseData
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '录入失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动新一期(开始下注)
|
||||
*/
|
||||
public function start() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
|
||||
|
||||
if ($gameId <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请选择游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 验证游戏类型
|
||||
$game = $db->get('games', ['type', 'stream_url'], ['id' => $gameId]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '游戏不属于骰子游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查该游戏是否有未结束的期号
|
||||
$activePeriod = $db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'status' => ['pending', 'locked', 'drawn']
|
||||
]);
|
||||
|
||||
if ($activePeriod) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该游戏当前有未结束的期号,无法开始新一轮'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成期号
|
||||
$periodNumber = $this->generatePeriodNumber($db, $gameId);
|
||||
|
||||
// 获取stream_url
|
||||
$streamUrl = !empty($game['stream_url']) ? $game['stream_url'] : null;
|
||||
|
||||
$periodData = [
|
||||
'period_number' => $periodNumber,
|
||||
'status' => 'pending',
|
||||
'game_id' => $gameId,
|
||||
'stream_url' => $streamUrl,
|
||||
'start_time' => date('Y-m-d H:i:s'),
|
||||
'auto_generated' => 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
$id = $db->insert('periods', $periodData);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '新一期已启动,开始下注',
|
||||
'data' => [
|
||||
'id' => $id,
|
||||
'period_number' => $periodNumber,
|
||||
'start_time' => $periodData['start_time']
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '启动失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认开奖并结算
|
||||
*/
|
||||
public function settle() {
|
||||
ob_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否为骰子游戏
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于骰子游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] !== 'drawn') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号还未开奖,无法结算'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($period['status'] === 'settled') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号已结算'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 开启事务
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
|
||||
// 获取本期所有待结算注单
|
||||
$bets = $db->select('bets', '*', [
|
||||
'period_id' => $id,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
// 遍历注单进行结算
|
||||
if ($bets) {
|
||||
foreach ($bets as $bet) {
|
||||
$checkResult = $this->checkWin($bet, $period);
|
||||
$isWin = $checkResult['win'];
|
||||
$winAmount = $checkResult['amount'];
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($isWin) {
|
||||
$payout = $bet['amount'] + $winAmount;
|
||||
|
||||
// 更新注单状态
|
||||
$db->update('bets', [
|
||||
'status' => 'win',
|
||||
'win_amount' => $winAmount,
|
||||
'settled_at' => $now,
|
||||
'updated_at' => $now
|
||||
], ['id' => $bet['id']]);
|
||||
|
||||
// 更新用户余额
|
||||
$db->update('users', [
|
||||
'balance[+]' => $payout
|
||||
], ['id' => $bet['user_id']]);
|
||||
|
||||
// 获取更新后的余额
|
||||
$user = $db->get('users', ['balance'], ['id' => $bet['user_id']]);
|
||||
$balanceAfter = $user['balance'];
|
||||
$balanceBefore = $balanceAfter - $payout;
|
||||
|
||||
// 写入资金流水
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $bet['user_id'],
|
||||
'type' => 'win',
|
||||
'amount' => $payout,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'related_id' => $bet['id'],
|
||||
'description' => "中奖 - 期号: " . $period['period_number'],
|
||||
'created_at' => $now
|
||||
]);
|
||||
|
||||
} else {
|
||||
// 未中奖
|
||||
$db->update('bets', [
|
||||
'status' => 'lose',
|
||||
'win_amount' => 0,
|
||||
'settled_at' => $now,
|
||||
'updated_at' => $now
|
||||
], ['id' => $bet['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新期号状态为已结算
|
||||
$db->update('periods', [
|
||||
'status' => 'settled',
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
// 提交事务
|
||||
$db->medoo->pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '结算成功,请点击"开始下注"启动下一期',
|
||||
'data' => [
|
||||
'current_period_id' => $id
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
if (isset($db->medoo->pdo)) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '结算失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 封盘
|
||||
*/
|
||||
public function lock() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否为骰子游戏
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于骰子游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] !== 'pending') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号状态不允许封盘'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->update('periods', [
|
||||
'status' => 'locked',
|
||||
'end_time' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '封盘成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '封盘失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成期号
|
||||
*/
|
||||
private function generatePeriodNumber($db, $gameId) {
|
||||
$now = new \DateTime();
|
||||
$dateStr = $now->format('Ymd');
|
||||
$prefix = "G{$gameId}{$dateStr}";
|
||||
|
||||
try {
|
||||
$lastPeriod = $db->get('periods', 'period_number', [
|
||||
'period_number[~]' => $prefix . '%',
|
||||
'ORDER' => ['period_number' => 'DESC']
|
||||
]);
|
||||
|
||||
if ($lastPeriod) {
|
||||
$lastSeq = substr($lastPeriod, strlen($prefix));
|
||||
$newSeq = (int)$lastSeq + 1;
|
||||
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$seqStr = '0001';
|
||||
}
|
||||
|
||||
return $prefix . $seqStr;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $prefix . time();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查注单是否中奖
|
||||
*/
|
||||
private function checkWin($bet, $period) {
|
||||
$betType = $bet['bet_type'];
|
||||
$betValue = $bet['bet_value'];
|
||||
$betAmount = (float)$bet['amount'];
|
||||
$gameId = $period['game_id'];
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取赔率配置
|
||||
static $oddsCache = [];
|
||||
if (!isset($oddsCache[$gameId])) {
|
||||
$oddsData = $db->select('game_odds', '*', ['game_id' => $gameId]);
|
||||
$oddsCache[$gameId] = [];
|
||||
foreach ($oddsData as $odd) {
|
||||
$key = $odd['type'] . '_' . $odd['target'];
|
||||
$oddsCache[$gameId][$key] = (float)$odd['odds'];
|
||||
}
|
||||
}
|
||||
|
||||
$isWin = false;
|
||||
$payoutMultiplier = 0;
|
||||
|
||||
// 获取赔率
|
||||
$getOdds = function($type, $target = 'all') use ($oddsCache, $gameId) {
|
||||
$key = $type . '_' . $target;
|
||||
if (isset($oddsCache[$gameId][$key])) {
|
||||
return $oddsCache[$gameId][$key];
|
||||
}
|
||||
$allKey = $type . '_all';
|
||||
return $oddsCache[$gameId][$allKey] ?? 0;
|
||||
};
|
||||
|
||||
// 骰子游戏结算逻辑
|
||||
$dice1 = (int)$period['dice1'];
|
||||
$dice2 = (int)$period['dice2'];
|
||||
$dice3 = (int)$period['dice3'];
|
||||
$total = (int)$period['total'];
|
||||
|
||||
switch ($betType) {
|
||||
case 'xiu': // 小 (4-10)
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3) {
|
||||
if ($dice1 <= 3) $isWin = true;
|
||||
} else {
|
||||
if ($total >= 4 && $total <= 10) $isWin = true;
|
||||
}
|
||||
$payoutMultiplier = $getOdds('xiu');
|
||||
break;
|
||||
|
||||
case 'tai': // 大 (11-17)
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3) {
|
||||
if ($dice1 >= 4) $isWin = true;
|
||||
} else {
|
||||
if ($total >= 11 && $total <= 17) $isWin = true;
|
||||
}
|
||||
$payoutMultiplier = $getOdds('tai');
|
||||
break;
|
||||
|
||||
case 'chan': // 偶数
|
||||
if ($total % 2 == 0) $isWin = true;
|
||||
$payoutMultiplier = $getOdds('chan');
|
||||
break;
|
||||
|
||||
case 'le': // 奇数
|
||||
if ($total % 2 != 0) $isWin = true;
|
||||
$payoutMultiplier = $getOdds('le');
|
||||
break;
|
||||
|
||||
case 'number': // 单数字 (Sum)
|
||||
if ($total == (int)$betValue) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('sum', $betValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dice': // 单个骰子
|
||||
$count = 0;
|
||||
if ($dice1 == (int)$betValue) $count++;
|
||||
if ($dice2 == (int)$betValue) $count++;
|
||||
if ($dice3 == (int)$betValue) $count++;
|
||||
|
||||
if ($count > 0) {
|
||||
$isWin = true;
|
||||
$baseOdds = $getOdds('dice', $betValue);
|
||||
$payoutMultiplier = $baseOdds * $count;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'combo': // 豹子
|
||||
if ($betValue === 'any_triple') {
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('combo', 'any_triple');
|
||||
}
|
||||
} else {
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3 && $total == (int)$betValue) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('combo', 'specific_triple');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($isWin && $payoutMultiplier > 0) {
|
||||
return [
|
||||
'win' => true,
|
||||
'amount' => $betAmount * $payoutMultiplier
|
||||
];
|
||||
}
|
||||
|
||||
return ['win' => false, 'amount' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算开奖结果
|
||||
*/
|
||||
private function calculateResult($dice1, $dice2, $dice3, $total) {
|
||||
// 检查是否为爆子
|
||||
if ($dice1 === $dice2 && $dice2 === $dice3) {
|
||||
if ($dice1 <= 3) {
|
||||
return 'Xỉu';
|
||||
} else {
|
||||
return 'Tài';
|
||||
}
|
||||
}
|
||||
|
||||
// 普通大小判断
|
||||
if ($total >= 4 && $total <= 10) {
|
||||
return 'Xỉu';
|
||||
} else {
|
||||
return 'Tài';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class EmployeeController extends AdminBaseController {
|
||||
private $db;
|
||||
public function __construct(Database $db) { $this->db = $db; }
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$employees = $this->db->select('employees', '*', ['ORDER' => ['id' => 'DESC']]);
|
||||
$this->render('Admin/employees.php', compact('employees'));
|
||||
}
|
||||
|
||||
public function update() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$fields = [
|
||||
'emp_code' => trim($data['emp_code'] ?? ''),
|
||||
'username' => trim($data['username'] ?? ''),
|
||||
'real_name' => trim($data['real_name'] ?? ''),
|
||||
'shift' => $data['shift'] ?? 'all',
|
||||
'permissions' => json_encode($data['permissions'] ?? ['deposit','withdraw']),
|
||||
'status' => (int)($data['status'] ?? 1),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
if (!empty($data['password'])) $fields['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
$this->db->update('employees', $fields, ['id' => $id]);
|
||||
} else {
|
||||
if (empty($data['password'])) { $this->json(['status'=>'error','message'=>'Password required']); return; }
|
||||
$fields['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
$this->db->insert('employees', $fields);
|
||||
}
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$this->db->delete('employees', ['id' => (int)$id]);
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
// 员工登录(独立入口)
|
||||
public function loginPage() {
|
||||
include ROOT_PATH . 'App/Views/Admin/employee_login.php';
|
||||
}
|
||||
|
||||
public function loginSubmit() {
|
||||
header('Content-Type: application/json');
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$db = new Database();
|
||||
$emp = $db->get('employees', '*', ['username' => trim($data['username'] ?? '')]);
|
||||
if (!$emp || !password_verify($data['password'] ?? '', $emp['password'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid credentials']); return;
|
||||
}
|
||||
if (!$emp['status']) {
|
||||
echo json_encode(['success' => false, 'message' => 'Account disabled']); return;
|
||||
}
|
||||
// 班次检查
|
||||
$hour = (int)date('H');
|
||||
if ($emp['shift'] === 'day' && ($hour < 8 || $hour >= 20)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Not your shift']); return;
|
||||
}
|
||||
if ($emp['shift'] === 'night' && ($hour >= 8 && $hour < 20)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Not your shift']); return;
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
$_SESSION['emp_id'] = $emp['id'];
|
||||
$_SESSION['emp_code'] = $emp['emp_code'];
|
||||
$_SESSION['emp_permissions'] = json_decode($emp['permissions'], true) ?: [];
|
||||
$_SESSION['emp_last_activity'] = time();
|
||||
$db->update('employees', ['last_login' => date('Y-m-d H:i:s')], ['id' => $emp['id']]);
|
||||
echo json_encode(['success' => true, 'redirect' => '/employee/dashboard']);
|
||||
}
|
||||
|
||||
// 员工操作面板(仅上下分)
|
||||
public function dashboard() {
|
||||
$this->checkEmployee();
|
||||
$users = $this->db->select('users', ['id','username','balance','is_virtual'], [
|
||||
'is_virtual' => 0, 'ORDER' => ['username' => 'ASC']
|
||||
]);
|
||||
$logs = $this->db->select('employee_logs', '*', [
|
||||
'emp_id' => $_SESSION['emp_id'],
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
|
||||
]);
|
||||
include ROOT_PATH . 'App/Views/Admin/employee_dashboard.php';
|
||||
}
|
||||
|
||||
public function adjustBalance() {
|
||||
$this->checkEmployee();
|
||||
header('Content-Type: application/json');
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$userId = (int)($data['user_id'] ?? 0);
|
||||
$action = $data['action'] ?? ''; // deposit / withdraw
|
||||
$amount = abs(floatval($data['amount'] ?? 0));
|
||||
$remark = trim($data['remark'] ?? '');
|
||||
|
||||
$perms = $_SESSION['emp_permissions'] ?? [];
|
||||
if (!in_array($action, ['deposit','withdraw']) || !in_array($action, $perms)) {
|
||||
echo json_encode(['success' => false, 'message' => 'No permission']); return;
|
||||
}
|
||||
if ($amount <= 0 || $userId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid input']); return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->medoo->pdo->beginTransaction();
|
||||
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
|
||||
$stmt->execute([':id' => $userId]);
|
||||
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!$user) { $this->db->medoo->pdo->rollBack(); echo json_encode(['success'=>false,'message'=>'User not found']); return; }
|
||||
|
||||
$old = (float)$user['balance'];
|
||||
$new = $action === 'deposit' ? $old + $amount : $old - $amount;
|
||||
if ($new < 0) { $this->db->medoo->pdo->rollBack(); echo json_encode(['success'=>false,'message'=>'Insufficient balance']); return; }
|
||||
|
||||
$this->db->update('users', ['balance' => $new], ['id' => $userId]);
|
||||
$this->db->insert('transactions', [
|
||||
'user_id' => $userId, 'type' => 'manual_' . $action,
|
||||
'amount' => $action === 'deposit' ? $amount : -$amount,
|
||||
'balance_before' => $old, 'balance_after' => $new,
|
||||
'description' => "Employee {$_SESSION['emp_code']}: $remark",
|
||||
'operator_id' => $_SESSION['emp_id'], 'operator_type' => 'employee',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$this->db->insert('employee_logs', [
|
||||
'emp_id' => $_SESSION['emp_id'], 'action' => $action,
|
||||
'target_user_id' => $userId, 'amount' => $amount,
|
||||
'remark' => $remark, 'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$this->db->medoo->pdo->commit();
|
||||
echo json_encode(['success' => true, 'new_balance' => $new]);
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->db->medoo->pdo->inTransaction()) $this->db->medoo->pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Error']);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkEmployee() {
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
if (empty($_SESSION['emp_id'])) { header('Location: /employee/login'); exit; }
|
||||
if (time() - ($_SESSION['emp_last_activity'] ?? 0) > 7200) {
|
||||
unset($_SESSION['emp_id']); header('Location: /employee/login'); exit;
|
||||
}
|
||||
$_SESSION['emp_last_activity'] = time();
|
||||
}
|
||||
|
||||
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
|
||||
}
|
||||
Executable
+357
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class FinanceController extends AdminBaseController {
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务管理页面
|
||||
*/
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
try {
|
||||
// 获取资金流水列表
|
||||
$transactions = $db->select('transactions', [
|
||||
'[>]users' => ['user_id' => 'id']
|
||||
], [
|
||||
'transactions.id',
|
||||
'transactions.user_id',
|
||||
'users.username',
|
||||
'transactions.type',
|
||||
'transactions.amount',
|
||||
'transactions.balance_before',
|
||||
'transactions.balance_after',
|
||||
'transactions.related_id',
|
||||
'transactions.description',
|
||||
'transactions.created_at'
|
||||
], [
|
||||
'ORDER' => ['transactions.id' => 'DESC'],
|
||||
'LIMIT' => 100
|
||||
]);
|
||||
|
||||
if (!is_array($transactions)) {
|
||||
$transactions = [];
|
||||
}
|
||||
|
||||
// 计算统计信息
|
||||
$stats = $this->calculateStats($db);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$transactions = [];
|
||||
$stats = [
|
||||
'total_deposit' => 0,
|
||||
'total_withdraw' => 0,
|
||||
'total_bet' => 0,
|
||||
'total_win' => 0,
|
||||
'today_deposit' => 0,
|
||||
'today_withdraw' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$this->render('Admin/finance.php', [
|
||||
'transactions' => $transactions,
|
||||
'stats' => $stats,
|
||||
'title' => '财务管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资金流水详情
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的流水ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$transaction = $db->get('transactions', [
|
||||
'[>]users' => ['user_id' => 'id']
|
||||
], [
|
||||
'transactions.id',
|
||||
'transactions.user_id',
|
||||
'users.username',
|
||||
'transactions.type',
|
||||
'transactions.amount',
|
||||
'transactions.balance_before',
|
||||
'transactions.balance_after',
|
||||
'transactions.related_id',
|
||||
'transactions.description',
|
||||
'transactions.created_at'
|
||||
], [
|
||||
'transactions.id' => $id
|
||||
]);
|
||||
|
||||
if ($transaction) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $transaction
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '流水记录不存在'
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '获取数据失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理充值
|
||||
*/
|
||||
public function deposit() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rawData = file_get_contents('php://input');
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '数据格式错误'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
|
||||
$amount = isset($data['amount']) ? floatval($data['amount']) : 0;
|
||||
$description = trim($data['description'] ?? '');
|
||||
|
||||
if ($userId <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的用户ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($amount <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '充值金额必须大于0'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
try {
|
||||
// 获取用户信息
|
||||
$user = $db->get('users', ['id', 'balance', 'username'], ['id' => $userId]);
|
||||
if (!$user) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$balanceBefore = floatval($user['balance']);
|
||||
$balanceAfter = $balanceBefore + $amount;
|
||||
|
||||
// 更新用户余额
|
||||
$db->update('users', [
|
||||
'balance' => $balanceAfter,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $userId]);
|
||||
|
||||
// 记录资金流水
|
||||
$transactionId = $db->insert('transactions', [
|
||||
'user_id' => $userId,
|
||||
'type' => 'deposit',
|
||||
'amount' => $amount,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'description' => $description ?: '管理员充值',
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '充值成功',
|
||||
'data' => [
|
||||
'transaction_id' => $transactionId,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '充值失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提现
|
||||
*/
|
||||
public function withdraw() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rawData = file_get_contents('php://input');
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '数据格式错误'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
|
||||
$amount = isset($data['amount']) ? floatval($data['amount']) : 0;
|
||||
$description = trim($data['description'] ?? '');
|
||||
|
||||
if ($userId <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的用户ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($amount <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '提现金额必须大于0'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
try {
|
||||
// 获取用户信息
|
||||
$user = $db->get('users', ['id', 'balance', 'username'], ['id' => $userId]);
|
||||
if (!$user) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$balanceBefore = floatval($user['balance']);
|
||||
|
||||
// 检查余额是否充足
|
||||
if ($balanceBefore < $amount) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户余额不足,当前余额:' . number_format($balanceBefore, 2)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$balanceAfter = $balanceBefore - $amount;
|
||||
|
||||
// 更新用户余额
|
||||
$db->update('users', [
|
||||
'balance' => $balanceAfter,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $userId]);
|
||||
|
||||
// 记录资金流水
|
||||
$transactionId = $db->insert('transactions', [
|
||||
'user_id' => $userId,
|
||||
'type' => 'withdraw',
|
||||
'amount' => -$amount, // 提现为负数
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'description' => $description ?: '管理员提现',
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '提现成功',
|
||||
'data' => [
|
||||
'transaction_id' => $transactionId,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '提现失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算财务统计
|
||||
*/
|
||||
private function calculateStats($db) {
|
||||
try {
|
||||
// 总充值 (包含普通充值和人工加款)
|
||||
$totalDeposit = $db->sum('transactions', 'amount', [
|
||||
'type' => ['deposit', 'manual_deposit']
|
||||
]) ?: 0;
|
||||
|
||||
// 总提现(取绝对值,包含普通提现和人工扣款)
|
||||
$totalWithdraw = abs($db->sum('transactions', 'amount', [
|
||||
'type' => ['withdraw', 'manual_withdraw']
|
||||
]) ?: 0);
|
||||
|
||||
// 总投注(取绝对值)
|
||||
$totalBet = abs($db->sum('transactions', 'amount', [
|
||||
'type' => 'bet'
|
||||
]) ?: 0);
|
||||
|
||||
// 总中奖
|
||||
$totalWin = $db->sum('transactions', 'amount', [
|
||||
'type' => 'win'
|
||||
]) ?: 0;
|
||||
|
||||
// 今日充值 (包含普通充值和人工加款)
|
||||
$todayDeposit = $db->sum('transactions', 'amount', [
|
||||
'type' => ['deposit', 'manual_deposit'],
|
||||
'created_at[>=]' => date('Y-m-d 00:00:00')
|
||||
]) ?: 0;
|
||||
|
||||
// 今日提现(取绝对值,包含普通提现和人工扣款)
|
||||
$todayWithdraw = abs($db->sum('transactions', 'amount', [
|
||||
'type' => ['withdraw', 'manual_withdraw'],
|
||||
'created_at[>=]' => date('Y-m-d 00:00:00')
|
||||
]) ?: 0);
|
||||
|
||||
return [
|
||||
'total_deposit' => floatval($totalDeposit),
|
||||
'total_withdraw' => floatval($totalWithdraw),
|
||||
'total_bet' => floatval($totalBet),
|
||||
'total_win' => floatval($totalWin),
|
||||
'today_deposit' => floatval($todayDeposit),
|
||||
'today_withdraw' => floatval($todayWithdraw)
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'total_deposit' => 0,
|
||||
'total_withdraw' => 0,
|
||||
'total_bet' => 0,
|
||||
'total_win' => 0,
|
||||
'today_deposit' => 0,
|
||||
'today_withdraw' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+414
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class GameController extends AdminBaseController {
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
try {
|
||||
$games = $db->select('games', '*', [
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if (!is_array($games)) {
|
||||
$games = [];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$games = [];
|
||||
}
|
||||
|
||||
$this->render('Admin/games.php', [
|
||||
'games' => $games,
|
||||
'title' => '游戏管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个游戏数据(用于编辑)
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的游戏ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$game = $db->get('games', '*', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($game) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $game
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '游戏不存在'
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '获取数据失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新游戏
|
||||
*/
|
||||
public function update() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
$isEditMode = $id > 0;
|
||||
|
||||
$name = trim($data['name'] ?? '');
|
||||
$code = trim($data['code'] ?? '');
|
||||
$streamUrl = trim($data['stream_url'] ?? '');
|
||||
$type = trim($data['type'] ?? 'dice');
|
||||
$sortOrder = isset($data['sort_order']) ? (int)$data['sort_order'] : 0;
|
||||
$status = isset($data['status']) ? (int)$data['status'] : 1; // 默认启用
|
||||
$featured = isset($data['featured']) ? (int)$data['featured'] : 1; // 默认显示在游戏大厅
|
||||
$image = trim($data['image'] ?? '');
|
||||
|
||||
// 处理标签(复选框数组转逗号分隔字符串)
|
||||
$tags = '';
|
||||
if (isset($data['tags']) && is_array($data['tags'])) {
|
||||
$tags = implode(',', array_map('trim', $data['tags']));
|
||||
} elseif (isset($data['tags'])) {
|
||||
$tags = trim($data['tags']);
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '游戏名称不能为空'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动生成游戏标识(如果为空)
|
||||
if (empty($code)) {
|
||||
// 将中文转换为拼音或使用拼音首字母,这里简化为使用时间戳+随机数
|
||||
// 实际可以使用拼音库,这里使用简化方式:去除特殊字符,转为小写,空格转下划线
|
||||
$code = $this->generateGameCode($name, $db, $id);
|
||||
}
|
||||
|
||||
// 检查code是否已存在(排除当前ID)
|
||||
try {
|
||||
$exists = $db->get('games', 'id', [
|
||||
'code' => $code,
|
||||
'id[!]' => $id
|
||||
]);
|
||||
|
||||
if ($exists) {
|
||||
// 如果已存在,添加随机后缀
|
||||
$code = $code . '_' . time();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 如果表不存在或其他错误,继续执行
|
||||
}
|
||||
|
||||
// 准备数据
|
||||
$gameData = [
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'stream_url' => $streamUrl,
|
||||
'type' => $type,
|
||||
'tags' => $tags,
|
||||
'sort_order' => $sortOrder,
|
||||
'status' => $status,
|
||||
'featured' => $featured,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 如果提供了图片路径,则添加
|
||||
if (!empty($image)) {
|
||||
$gameData['image'] = $image;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($isEditMode) {
|
||||
$db->update('games', $gameData, ['id' => $id]);
|
||||
$message = '游戏更新成功';
|
||||
} else {
|
||||
$gameData['created_at'] = date('Y-m-d H:i:s');
|
||||
$id = $db->insert('games', $gameData);
|
||||
$message = '游戏创建成功';
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除游戏
|
||||
*/
|
||||
public function delete($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的游戏ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$db->delete('games', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '游戏已删除'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换游戏启用状态
|
||||
*/
|
||||
public function toggleStatus($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的游戏ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$game = $db->get('games', ['status'], ['id' => $id]);
|
||||
if (!$game) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '游戏不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$newStatus = $game['status'] ? 0 : 1;
|
||||
$db->update('games', [
|
||||
'status' => $newStatus,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $newStatus ? '游戏已启用' : '游戏已禁用',
|
||||
'data' => ['status' => $newStatus]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏赔率配置
|
||||
*/
|
||||
public function getOdds($gameId) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($gameId) || !is_numeric($gameId)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的游戏ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
// Check if game exists
|
||||
$game = $db->get('games', ['id', 'name'], ['id' => $gameId]);
|
||||
if (!$game) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '游戏不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get odds
|
||||
$odds = $db->select('game_odds', '*', [
|
||||
'game_id' => $gameId
|
||||
]);
|
||||
|
||||
// If no odds found, return defaults structure (or empty array)
|
||||
// The frontend should handle empty/missing odds
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'game' => $game,
|
||||
'odds' => $odds
|
||||
]
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '获取赔率失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏赔率
|
||||
*/
|
||||
public function updateOdds() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($data)) {
|
||||
$data = $_POST;
|
||||
}
|
||||
|
||||
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
|
||||
$oddsData = isset($data['odds']) ? $data['odds'] : [];
|
||||
|
||||
if (empty($gameId) || empty($oddsData)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '参数不完整'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
// Begin transaction if supported (PDO usually does)
|
||||
// For simple database wrapper, we just loop update
|
||||
|
||||
foreach ($oddsData as $item) {
|
||||
$type = $item['type'];
|
||||
$target = $item['target'];
|
||||
$odds = floatval($item['odds']);
|
||||
|
||||
// Check if exists
|
||||
$exists = $db->has('game_odds', [
|
||||
'game_id' => $gameId,
|
||||
'type' => $type,
|
||||
'target' => $target
|
||||
]);
|
||||
|
||||
if ($exists) {
|
||||
$db->update('game_odds', [
|
||||
'odds' => $odds,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], [
|
||||
'game_id' => $gameId,
|
||||
'type' => $type,
|
||||
'target' => $target
|
||||
]);
|
||||
} else {
|
||||
$db->insert('game_odds', [
|
||||
'game_id' => $gameId,
|
||||
'type' => $type,
|
||||
'target' => $target,
|
||||
'odds' => $odds,
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '赔率配置已保存'
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '保存失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动生成游戏标识
|
||||
* @param string $name 游戏名称
|
||||
* @param Database $db 数据库实例
|
||||
* @param int $excludeId 排除的ID(编辑时使用)
|
||||
* @return string 生成的游戏标识
|
||||
*/
|
||||
private function generateGameCode($name, $db, $excludeId = 0) {
|
||||
// 去除特殊字符,转为小写,空格和中文标点转下划线
|
||||
$code = strtolower($name);
|
||||
// 将中文字符转为拼音(简化处理:直接使用拼音首字母或时间戳)
|
||||
// 这里使用简化的方式:去除所有非字母数字字符,替换为下划线
|
||||
$code = preg_replace('/[^a-z0-9_]+/', '_', $code);
|
||||
$code = preg_replace('/_+/', '_', $code); // 多个下划线合并为一个
|
||||
$code = trim($code, '_'); // 去除首尾下划线
|
||||
|
||||
// 如果转换后为空,使用时间戳
|
||||
if (empty($code)) {
|
||||
$code = 'game_' . time();
|
||||
}
|
||||
|
||||
// 限制长度
|
||||
if (strlen($code) > 50) {
|
||||
$code = substr($code, 0, 50);
|
||||
}
|
||||
|
||||
// 检查是否已存在,如果存在则添加随机数
|
||||
try {
|
||||
$exists = $db->get('games', 'id', [
|
||||
'code' => $code,
|
||||
'id[!]' => $excludeId
|
||||
]);
|
||||
|
||||
if ($exists) {
|
||||
$code = $code . '_' . substr(time(), -6); // 添加时间戳后缀
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
use App\Core\PluginManager;
|
||||
|
||||
class HomeController extends AdminBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->checkLogin();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$plugins = $this->pluginManager->getPluginStatusList();
|
||||
$stats = $this->calculateDashboardStats();
|
||||
|
||||
$this->render('Admin/dashboard.php', [
|
||||
'plugins' => $plugins,
|
||||
'stats' => $stats,
|
||||
'title' => '控制台'
|
||||
]);
|
||||
}
|
||||
|
||||
private function calculateDashboardStats() {
|
||||
$db = new Database();
|
||||
$todayStart = date('Y-m-d 00:00:00');
|
||||
|
||||
try {
|
||||
// 1. 今日投注总额
|
||||
$todayBetAmount = $db->sum('bets', 'amount', [
|
||||
'created_at[>=]' => $todayStart
|
||||
]) ?: 0;
|
||||
|
||||
// 2. 今日已派彩金额 (已结算且中奖的)
|
||||
$todayPayoutAmount = $db->sum('bets', 'win_amount', [
|
||||
'settled_at[>=]' => $todayStart,
|
||||
'status' => 'win'
|
||||
]) ?: 0;
|
||||
|
||||
// 3. 待处理开奖 (已封盘 waiting for draw, 或 已开奖 waiting for settle)
|
||||
// status: pending -> locked -> drawn -> settled
|
||||
$pendingDrawCount = $db->count('periods', [
|
||||
'status' => ['locked', 'drawn']
|
||||
]);
|
||||
|
||||
// 4. 待处理提现 (目前没有提现申请表,暂定为0)
|
||||
// 如果后续有 withdrawals 表或 transactions status 字段,需在此修改
|
||||
$pendingWithdrawCount = 0;
|
||||
|
||||
return [
|
||||
'today_bet_amount' => $todayBetAmount,
|
||||
'today_payout_amount' => $todayPayoutAmount,
|
||||
'pending_draw_count' => $pendingDrawCount,
|
||||
'pending_withdraw_count' => $pendingWithdrawCount
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'today_bet_amount' => 0,
|
||||
'today_payout_amount' => 0,
|
||||
'pending_draw_count' => 0,
|
||||
'pending_withdraw_count' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+376
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database; // Medoo数据库实例
|
||||
use \Exception;
|
||||
|
||||
class ImagesController extends AdminBaseController {
|
||||
protected $db;
|
||||
protected $uploadDir;
|
||||
protected array $uploadConfig;
|
||||
|
||||
// 允许的图片MIME类型
|
||||
const ALLOWED_MIME = [
|
||||
'image/jpeg', 'image/png', 'image/gif',
|
||||
'image/webp', 'image/svg+xml'
|
||||
];
|
||||
const MAX_UPLOAD_SIZE = 10485760; // 上传大小限制(10MB)
|
||||
const MAX_IMAGE_SIZE = 800; // 主图最大边长(等同缩略图大小)
|
||||
|
||||
public function __construct() {
|
||||
$this->checkLogin(); // 登录验证
|
||||
$this->db = new Database(); // 初始化数据库连接
|
||||
$domain = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http")
|
||||
. "://" . $_SERVER['HTTP_HOST'];
|
||||
|
||||
$this->uploadConfig = [
|
||||
'upload_dir' => $_SERVER['DOCUMENT_ROOT'] . '/Storage/images/',
|
||||
'url' => $domain . '/Storage/images/',
|
||||
'max_size' => 10 * 1024 * 1024, // 10MB
|
||||
'allowed_mimes' => [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/icon'
|
||||
],
|
||||
'min_quality' => 60,
|
||||
'max_quality' => 100,
|
||||
'default_quality' => 80
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片管理首页(仅渲染前端页面,数据使用AJAX加载)
|
||||
*/
|
||||
public function index() {
|
||||
// 渲染页面模板
|
||||
$this->render('Admin/images.php', [
|
||||
'title' => '图片管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片列表查询
|
||||
*/
|
||||
public function list() {
|
||||
|
||||
|
||||
try {
|
||||
// 处理分页参数
|
||||
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||
$limit = isset($_GET['limit']) ?
|
||||
min(100, max(1, (int)$_GET['limit'])) : 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// 查询总数
|
||||
$total = $this->db->count('images');
|
||||
|
||||
// 查询当前页数据
|
||||
$data = $this->db->select('images', [
|
||||
'id', 'name', 'url',
|
||||
'size', 'width', 'height', 'upload_time'
|
||||
], [
|
||||
'LIMIT' => [$offset, $limit],
|
||||
'ORDER' => ['upload_time' => 'DESC']
|
||||
]);
|
||||
|
||||
// 返回结果
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'page' => $page,
|
||||
'total' => $total,
|
||||
'data' => $data ?: []
|
||||
]);
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->jsonError('获取数据失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传处理
|
||||
*/
|
||||
public function upload() {
|
||||
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
|
||||
$this->jsonError('文件上传失败: ' . $this->getUploadError($_FILES['image']['error'] ?? -1));
|
||||
}
|
||||
|
||||
$file = $_FILES['image'];
|
||||
|
||||
$params = [
|
||||
'quality' => isset($_POST['quality']) ?
|
||||
min($this->uploadConfig['max_quality'],
|
||||
max($this->uploadConfig['min_quality'], (int)$_POST['quality'])) :
|
||||
$this->uploadConfig['default_quality'],
|
||||
'width' => isset($_POST['width']) && $_POST['width'] !== '' ? max(1, (int)$_POST['width']) : 0,
|
||||
'height' => isset($_POST['height']) && $_POST['height'] !== '' ? max(1, (int)$_POST['height']) : 0
|
||||
];
|
||||
|
||||
if ($file['size'] > $this->uploadConfig['max_size']) {
|
||||
$this->jsonError("文件过大,最大支持" . $this->formatFileSize($this->uploadConfig['max_size']));
|
||||
}
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, $this->uploadConfig['allowed_mimes'])) {
|
||||
$this->jsonError('不支持的文件类型,仅允许: ' . implode(', ', $this->uploadConfig['allowed_mimes']));
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadDir = rtrim($this->uploadConfig['upload_dir'], '/') . '/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$originalName = basename($file['name']);
|
||||
$originalExt = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$filename = date('YmdHis') . '_' . uniqid() . '.' . $originalExt;
|
||||
$targetPath = $uploadDir . $filename;
|
||||
|
||||
list($origWidth, $origHeight) = getimagesize($file['tmp_name']);
|
||||
list($newWidth, $newHeight, $srcX, $srcY, $cropW, $cropH) = $this->calculateDimensions(
|
||||
$origWidth, $origHeight,
|
||||
$params['width'], $params['height']
|
||||
);
|
||||
|
||||
// 判断是否需要裁剪
|
||||
if ($params['width'] > 0 && $params['height'] > 0) {
|
||||
$success = $this->processImageWithCrop(
|
||||
$file['tmp_name'], $mime, $targetPath,
|
||||
$newWidth, $newHeight,
|
||||
$srcX, $srcY, $cropW, $cropH,
|
||||
$params['quality']
|
||||
);
|
||||
} else {
|
||||
$success = $this->processImage(
|
||||
$file['tmp_name'], $mime, $targetPath,
|
||||
$newWidth, $newHeight, $params['quality']
|
||||
);
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
throw new Exception('图片处理失败');
|
||||
}
|
||||
|
||||
$record = [
|
||||
'name' => $originalName,
|
||||
'newname' => $filename,
|
||||
'url' => rtrim($this->uploadConfig['url'], '/') . '/' . $filename,
|
||||
'size' => filesize($targetPath),
|
||||
'width' => $newWidth,
|
||||
'height' => $newHeight,
|
||||
'upload_time' => time()
|
||||
];
|
||||
|
||||
$imageId = $this->db->insert('images', $record);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $record + ['id' => $imageId]
|
||||
]);
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($targetPath) && file_exists($targetPath)) {
|
||||
unlink($targetPath);
|
||||
}
|
||||
$this->jsonError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function calculateDimensions($origW, $origH, $targetW, $targetH) {
|
||||
if ($targetW <= 0 && $targetH <= 0) {
|
||||
return [$origW, $origH, 0, 0, $origW, $origH];
|
||||
}
|
||||
|
||||
// 只指定宽
|
||||
if ($targetW > 0 && $targetH <= 0) {
|
||||
$newW = $targetW;
|
||||
$newH = (int)($origH * ($targetW / $origW));
|
||||
return [$newW, $newH, 0, 0, $origW, $origH];
|
||||
}
|
||||
|
||||
// 只指定高
|
||||
if ($targetH > 0 && $targetW <= 0) {
|
||||
$newH = $targetH;
|
||||
$newW = (int)($origW * ($targetH / $origH));
|
||||
return [$newW, $newH, 0, 0, $origW, $origH];
|
||||
}
|
||||
|
||||
// 指定宽高 → 居中裁剪
|
||||
$srcRatio = $origW / $origH;
|
||||
$dstRatio = $targetW / $targetH;
|
||||
|
||||
if ($srcRatio > $dstRatio) {
|
||||
$cropH = $origH;
|
||||
$cropW = (int)($origH * $dstRatio);
|
||||
$srcX = (int)(($origW - $cropW) / 2);
|
||||
$srcY = 0;
|
||||
} else {
|
||||
$cropW = $origW;
|
||||
$cropH = (int)($origW / $dstRatio);
|
||||
$srcX = 0;
|
||||
$srcY = (int)(($origH - $cropH) / 2);
|
||||
}
|
||||
|
||||
return [$targetW, $targetH, $srcX, $srcY, $cropW, $cropH];
|
||||
}
|
||||
|
||||
|
||||
// 处理图片,带裁剪逻辑
|
||||
private function processImageWithCrop($srcPath, $mime, $destPath, $width, $height, $srcX, $srcY, $cropW, $cropH, $quality) {
|
||||
switch ($mime) {
|
||||
case 'image/jpeg': $src = imagecreatefromjpeg($srcPath); break;
|
||||
case 'image/png': $src = imagecreatefrompng($srcPath); imagesavealpha($src, true); break;
|
||||
case 'image/gif': $src = imagecreatefromgif($srcPath); break;
|
||||
case 'image/webp': $src = imagecreatefromwebp($srcPath); break;
|
||||
default: throw new Exception("不支持的图片格式: {$mime}");
|
||||
}
|
||||
|
||||
if (!$src) throw new Exception('无法解析图片内容');
|
||||
|
||||
$dest = imagecreatetruecolor($width, $height);
|
||||
|
||||
if ($mime === 'image/png' || $mime === 'image/gif') {
|
||||
imagealphablending($dest, false);
|
||||
imagesavealpha($dest, true);
|
||||
$transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127);
|
||||
imagefilledrectangle($dest, 0, 0, $width, $height, $transparent);
|
||||
}
|
||||
|
||||
imagecopyresampled(
|
||||
$dest, $src,
|
||||
0, 0,
|
||||
$srcX, $srcY,
|
||||
$width, $height,
|
||||
$cropW, $cropH
|
||||
);
|
||||
|
||||
$result = false;
|
||||
switch ($mime) {
|
||||
case 'image/jpeg': $result = imagejpeg($dest, $destPath, $quality); break;
|
||||
case 'image/png': $result = imagepng($dest, $destPath); break;
|
||||
case 'image/gif': $result = imagegif($dest, $destPath); break;
|
||||
case 'image/webp': $result = imagewebp($dest, $destPath, $quality); break;
|
||||
}
|
||||
|
||||
imagedestroy($src);
|
||||
imagedestroy($dest);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// 工具函数:图片处理(普通格式,不裁剪)
|
||||
private function processImage($srcPath, $mime, $destPath, $width, $height, $quality) {
|
||||
switch ($mime) {
|
||||
case 'image/jpeg': $src = imagecreatefromjpeg($srcPath); break;
|
||||
case 'image/png': $src = imagecreatefrompng($srcPath); imagesavealpha($src, true); break;
|
||||
case 'image/gif': $src = imagecreatefromgif($srcPath); break;
|
||||
case 'image/webp': $src = imagecreatefromwebp($srcPath); break;
|
||||
default: throw new Exception("不支持的图片格式: {$mime}");
|
||||
}
|
||||
|
||||
if (!$src) throw new Exception('无法解析图片内容');
|
||||
|
||||
$dest = imagecreatetruecolor($width, $height);
|
||||
|
||||
if ($mime === 'image/png' || $mime === 'image/gif') {
|
||||
imagealphablending($dest, false);
|
||||
imagesavealpha($dest, true);
|
||||
$transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127);
|
||||
imagefilledrectangle($dest, 0, 0, $width, $height, $transparent);
|
||||
}
|
||||
|
||||
imagecopyresampled($dest, $src, 0, 0, 0, 0, $width, $height, imagesx($src), imagesy($src));
|
||||
|
||||
$result = false;
|
||||
switch ($mime) {
|
||||
case 'image/jpeg': $result = imagejpeg($dest, $destPath, $quality); break;
|
||||
case 'image/png': $result = imagepng($dest, $destPath); break;
|
||||
case 'image/gif': $result = imagegif($dest, $destPath); break;
|
||||
case 'image/webp': $result = imagewebp($dest, $destPath, $quality); break;
|
||||
}
|
||||
|
||||
imagedestroy($src);
|
||||
imagedestroy($dest);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 工具函数:上传错误信息
|
||||
private function getUploadError($code) {
|
||||
$errors = [
|
||||
UPLOAD_ERR_INI_SIZE => '超过php.ini限制',
|
||||
UPLOAD_ERR_FORM_SIZE => '超过表单限制',
|
||||
UPLOAD_ERR_PARTIAL => '文件仅部分上传',
|
||||
UPLOAD_ERR_NO_FILE => '未上传文件',
|
||||
UPLOAD_ERR_NO_TMP_DIR => '缺少临时目录',
|
||||
UPLOAD_ERR_CANT_WRITE => '写入文件失败',
|
||||
UPLOAD_ERR_EXTENSION => '被扩展阻止'
|
||||
];
|
||||
return $errors[$code] ?? "未知错误(代码: {$code})";
|
||||
}
|
||||
|
||||
// 工具函数:格式化文件大小
|
||||
private function formatFileSize($bytes) {
|
||||
if ($bytes < 1024) return $bytes . 'B';
|
||||
if ($bytes < 1048576) return round($bytes / 1024, 1) . 'KB';
|
||||
return round($bytes / 1048576, 1) . 'MB';
|
||||
}
|
||||
|
||||
// 工具函数:JSON错误响应
|
||||
private function jsonError($message) {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $message
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
public function delete() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// 获取 JSON 输入
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!isset($input['ids']) || !is_array($input['ids'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => '缺少参数']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 将字符串数组转换为整数数组
|
||||
$ids = array_map('intval', $input['ids']);
|
||||
$ids = array_filter($ids); // 去除无效值
|
||||
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['status' => 'error', 'message' => '无效的 ID 列表']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询图片路径(如果你想同时删除图片文件)
|
||||
$images = $this->db->select('images', ['id', 'newname'], [
|
||||
'id' => $ids
|
||||
]);
|
||||
|
||||
foreach ($images as $img) {
|
||||
$file = $this->uploadConfig['upload_dir'] . $img['newname'];
|
||||
if (file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
$this->db->delete('images', ['id' => $ids]);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => '删除失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
Executable
+776
@@ -0,0 +1,776 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
class InstallController
|
||||
{
|
||||
protected $step;
|
||||
protected $configFile = ROOT_PATH . "Db/config.php";
|
||||
protected $installedLockFile = ROOT_PATH . "Db/installed.lock";
|
||||
protected $db;
|
||||
protected $message = '';
|
||||
protected $messageType = ''; // success, error, warning
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 关键修复:步骤4即使已安装也允许访问
|
||||
$currentStep = $_POST['step'] ?? $_GET['step'] ?? 1;
|
||||
|
||||
// 只有非步骤4且已安装时才显示已安装页面
|
||||
if ($currentStep != 4 && $this->isInstalled()) {
|
||||
$this->step = 5; // 已安装状态
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理消息提示
|
||||
if (isset($_SESSION['install_message'])) {
|
||||
$this->message = $_SESSION['install_message'];
|
||||
$this->messageType = $_SESSION['install_message_type'];
|
||||
unset($_SESSION['install_message'], $_SESSION['install_message_type']);
|
||||
}
|
||||
|
||||
// 设置当前步骤
|
||||
$this->step = $currentStep;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
// 已安装则显示提示(步骤4除外)
|
||||
if ($this->step == 5) {
|
||||
$this->renderHeader();
|
||||
$this->alreadyInstalled();
|
||||
$this->renderFooter();
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理表单提交(在输出任何内容之前)
|
||||
$this->handleSubmission();
|
||||
|
||||
// 输出页面
|
||||
$this->renderHeader();
|
||||
|
||||
switch ($this->step) {
|
||||
case 1:
|
||||
$this->checkEnvironment();
|
||||
break;
|
||||
case 2:
|
||||
$this->databaseForm();
|
||||
break;
|
||||
case 3:
|
||||
$this->adminForm();
|
||||
break;
|
||||
case 4:
|
||||
$this->finish();
|
||||
break;
|
||||
default:
|
||||
$this->setMessage('非法步骤', 'error');
|
||||
$this->showMessage();
|
||||
}
|
||||
|
||||
$this->renderFooter();
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
protected function handleSubmission()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentStep = $_POST['step'] ?? 1;
|
||||
|
||||
switch ($currentStep) {
|
||||
case 2:
|
||||
$this->processDatabaseForm();
|
||||
break;
|
||||
case 3:
|
||||
$this->processAdminForm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数据库表单提交
|
||||
protected function processDatabaseForm()
|
||||
{
|
||||
$host = $_POST['db_host'] ?? '';
|
||||
$name = $_POST['db_name'] ?? '';
|
||||
$user = $_POST['db_user'] ?? '';
|
||||
$pass = $_POST['db_pass'] ?? '';
|
||||
|
||||
try {
|
||||
// 连接数据库服务器
|
||||
$pdo = new \PDO("mysql:host=$host;charset=utf8mb4", $user, $pass);
|
||||
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// 检查数据库是否存在
|
||||
$stmt = $pdo->query("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
|
||||
$dbExists = $stmt->fetchColumn() !== false;
|
||||
|
||||
// 如果数据库存在且不为空,提示用户
|
||||
if ($dbExists) {
|
||||
$stmt = $pdo->query("USE $name");
|
||||
$stmt = $pdo->query("SHOW TABLES");
|
||||
if ($stmt->fetchColumn() !== false) {
|
||||
$this->setSessionMessage(
|
||||
"警告:数据库 '$name' 已存在且包含表。继续安装可能会覆盖现有数据。",
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 创建数据库
|
||||
$pdo->exec("CREATE DATABASE `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
}
|
||||
|
||||
// 连接到指定数据库
|
||||
$this->db = new \PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $pass);
|
||||
$this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// 保存配置文件
|
||||
$config = "<?php\nreturn [\n";
|
||||
$config .= " 'db_host' => '" . addslashes($host) . "',\n";
|
||||
$config .= " 'db_name' => '" . addslashes($name) . "',\n";
|
||||
$config .= " 'db_user' => '" . addslashes($user) . "',\n";
|
||||
$config .= " 'db_pass' => '" . addslashes($pass) . "',\n];\n";
|
||||
|
||||
if (file_put_contents($this->configFile, $config) === false) {
|
||||
throw new \Exception("无法写入配置文件,请检查权限");
|
||||
}
|
||||
|
||||
$this->setSessionMessage('数据库配置成功!即将进入管理员设置', 'success');
|
||||
header("Location: ?step=3");
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
$this->setSessionMessage('数据库配置失败:' . $e->getMessage(), 'error');
|
||||
header("Location: ?step=2");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// 处理管理员表单提交
|
||||
protected function processAdminForm()
|
||||
{
|
||||
try {
|
||||
if (!file_exists($this->configFile)) {
|
||||
throw new \Exception("数据库配置文件不存在,请先完成数据库设置");
|
||||
}
|
||||
|
||||
$cfg = require $this->configFile;
|
||||
$this->db = new \PDO(
|
||||
"mysql:host={$cfg['db_host']};dbname={$cfg['db_name']};charset=utf8mb4",
|
||||
$cfg['db_user'],
|
||||
$cfg['db_pass']
|
||||
);
|
||||
$this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// 创建images表
|
||||
$this->db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `images` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '文件原名',
|
||||
`newname` varchar(255) NOT NULL COMMENT '新文件名',
|
||||
`url` text NOT NULL COMMENT 'URL',
|
||||
`size` int(11) NOT NULL COMMENT '文件大小(字节)',
|
||||
`width` int(11) NOT NULL COMMENT '宽度(像素)',
|
||||
`height` int(11) NOT NULL COMMENT '高度(像素)',
|
||||
`upload_time` int(11) NOT NULL COMMENT '上传时间戳',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
// 创建plugins表
|
||||
$this->db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `plugins` (
|
||||
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL COMMENT '插件唯一标识,如 SamplePlugin',
|
||||
`title` varchar(255) NOT NULL COMMENT '插件显示名称',
|
||||
`description` text COMMENT '插件描述',
|
||||
`version` varchar(20) DEFAULT NULL COMMENT '插件版本号',
|
||||
`author` varchar(100) DEFAULT NULL COMMENT '作者',
|
||||
`url` varchar(128) NOT NULL,
|
||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '启用状态,1=启用,0=禁用',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件管理表';
|
||||
");
|
||||
|
||||
// 创建users表
|
||||
$this->db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`email` varchar(100) NOT NULL,
|
||||
`avatar` VARCHAR(128) DEFAULT NULL,
|
||||
`balance` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '用户余额',
|
||||
`status` int(11) NOT NULL DEFAULT '0',
|
||||
`role` varchar(20) NOT NULL DEFAULT 'user',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
UNIQUE KEY `email` (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
$email = $_POST['email'] ?? ''; // 新增邮箱字段
|
||||
|
||||
// 验证逻辑更新
|
||||
if (!$username) {
|
||||
throw new \Exception("用户名不能为空");
|
||||
}
|
||||
|
||||
if (!$password) {
|
||||
throw new \Exception("密码不能为空");
|
||||
}
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
throw new \Exception("密码长度不能少于8位");
|
||||
}
|
||||
|
||||
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \Exception("请输入有效的邮箱地址");
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
$stmt = $this->db->prepare("SELECT id FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
throw new \Exception("用户名 '$username' 已存在,请选择其他用户名");
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
throw new \Exception("邮箱 '$email' 已被使用,请选择其他邮箱");
|
||||
}
|
||||
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
// 插入管理员数据(包含邮箱和管理员角色)
|
||||
$stmt = $this->db->prepare("
|
||||
INSERT INTO users (username, password, email, role, status)
|
||||
VALUES (?, ?, ?, 'admin', 1)
|
||||
");
|
||||
$stmt->execute([$username, $hash, $email]);
|
||||
|
||||
// 创建临时标记文件
|
||||
file_put_contents(ROOT_PATH . "Db/install_completed.tmp", date('Y-m-d H:i:s') . "\n");
|
||||
|
||||
$this->setSessionMessage('管理员账号创建成功!安装已完成', 'success');
|
||||
header("Location: ?step=4");
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
$this->setSessionMessage('管理员账号创建失败:' . $e->getMessage(), 'error');
|
||||
header("Location: ?step=3");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 渲染页面头部和样式
|
||||
protected function renderHeader()
|
||||
{
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统安装向导</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
secondary: '#10b981',
|
||||
danger: '#ef4444',
|
||||
warning: '#f59e0b',
|
||||
neutral: '#f3f4f6',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.step-active { @apply bg-primary text-white border-primary; }
|
||||
.step-passed { @apply bg-secondary text-white border-secondary; }
|
||||
.step-pending { @apply bg-gray-100 text-gray-400 border-gray-200; }
|
||||
.card { @apply bg-white rounded-lg shadow-md overflow-hidden transition-all duration-300 hover:shadow-lg; }
|
||||
.btn { @apply px-4 py-2 rounded-md font-medium transition-all duration-200; }
|
||||
.btn-primary { @apply bg-primary text-white hover:bg-primary/90; }
|
||||
.btn-secondary { @apply bg-gray-600 text-white hover:bg-gray-700; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen font-sans">
|
||||
<div class="container mx-auto px-4 py-8 max-w-3xl">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||
<i class="fa fa-cogs text-2xl"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-800">系统安装向导</h1>
|
||||
<p class="text-gray-500 mt-2">请按照步骤完成系统安装</p>
|
||||
</div>
|
||||
|
||||
<!-- 步骤指示器 -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
|
||||
<?php echo $this->step == 1 ? 'step-active' : ($this->step > 1 ? 'step-passed' : 'step-pending'); ?>">
|
||||
<i class="fa fa-check"></i>
|
||||
</div>
|
||||
<span class="mt-2 text-sm font-medium text-gray-600">环境检测</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 h-1 mx-4 bg-gray-200 relative">
|
||||
<div class="absolute inset-0 bg-primary transition-all duration-500
|
||||
<?php echo $this->step > 1 ? 'w-full' : 'w-0'; ?>"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
|
||||
<?php echo $this->step == 2 ? 'step-active' : ($this->step > 2 ? 'step-passed' : 'step-pending'); ?>">
|
||||
<i class="fa fa-database"></i>
|
||||
</div>
|
||||
<span class="mt-2 text-sm font-medium text-gray-600">数据库设置</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 h-1 mx-4 bg-gray-200 relative">
|
||||
<div class="absolute inset-0 bg-primary transition-all duration-500
|
||||
<?php echo $this->step > 2 ? 'w-full' : 'w-0'; ?>"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
|
||||
<?php echo $this->step == 3 ? 'step-active' : ($this->step > 3 ? 'step-passed' : 'step-pending'); ?>">
|
||||
<i class="fa fa-user"></i>
|
||||
</div>
|
||||
<span class="mt-2 text-sm font-medium text-gray-600">管理员设置</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 h-1 mx-4 bg-gray-200 relative">
|
||||
<div class="absolute inset-0 bg-primary transition-all duration-500
|
||||
<?php echo $this->step > 3 ? 'w-full' : 'w-0'; ?>"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
|
||||
<?php echo $this->step == 4 ? 'step-active' : ($this->step > 4 ? 'step-passed' : 'step-pending'); ?>">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
</div>
|
||||
<span class="mt-2 text-sm font-medium text-gray-600">完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<?php $this->showMessage(); ?>
|
||||
|
||||
<div class="card p-6">
|
||||
<?php
|
||||
}
|
||||
|
||||
// 渲染页面底部
|
||||
protected function renderFooter()
|
||||
{
|
||||
?>
|
||||
</div>
|
||||
<div class="mt-8 text-center text-gray-500 text-sm">
|
||||
<p>© 2025 系统安装向导</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
// 显示消息提示
|
||||
protected function showMessage()
|
||||
{
|
||||
if (empty($this->message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$icon = '';
|
||||
$class = '';
|
||||
|
||||
switch ($this->messageType) {
|
||||
case 'success':
|
||||
$icon = 'fa-check-circle';
|
||||
$class = 'bg-green-50 border-green-200 text-green-700';
|
||||
break;
|
||||
case 'error':
|
||||
$icon = 'fa-exclamation-circle';
|
||||
$class = 'bg-red-50 border-red-200 text-red-700';
|
||||
break;
|
||||
case 'warning':
|
||||
$icon = 'fa-exclamation-triangle';
|
||||
$class = 'bg-yellow-50 border-yellow-200 text-yellow-700';
|
||||
break;
|
||||
default:
|
||||
$icon = 'fa-info-circle';
|
||||
$class = 'bg-blue-50 border-blue-200 text-blue-700';
|
||||
}
|
||||
|
||||
echo "<div class='mb-6 p-4 border rounded-lg $class'>
|
||||
<i class='fa $icon mr-2'></i>$this->message
|
||||
</div>";
|
||||
}
|
||||
|
||||
// 设置消息
|
||||
protected function setMessage($message, $type = 'info')
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->messageType = $type;
|
||||
}
|
||||
|
||||
// 设置会话消息(用于跳转后显示)
|
||||
protected function setSessionMessage($message, $type = 'info')
|
||||
{
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION['install_message'] = $message;
|
||||
$_SESSION['install_message_type'] = $type;
|
||||
}
|
||||
|
||||
// 检查是否已安装
|
||||
protected function isInstalled()
|
||||
{
|
||||
return file_exists($this->installedLockFile);
|
||||
}
|
||||
|
||||
// 已安装提示
|
||||
protected function alreadyInstalled()
|
||||
{
|
||||
echo "<div class='text-center py-8'>
|
||||
<div class='inline-flex items-center justify-center w-20 h-20 rounded-full bg-warning/10 text-warning mb-6'>
|
||||
<i class='fa fa-exclamation-triangle text-3xl'></i>
|
||||
</div>
|
||||
<h2 class='text-2xl font-bold text-gray-800 mb-3'>系统已安装</h2>
|
||||
<p class='text-gray-600 mb-8 max-w-md mx-auto'>
|
||||
检测到系统已经安装,如需重新安装,请先删除安装锁定文件:<br>
|
||||
<code class='bg-gray-100 px-2 py-1 rounded text-sm'>{$this->installedLockFile}</code>
|
||||
</p>
|
||||
|
||||
<div class='flex justify-center'>
|
||||
<a href='/admin/login' class='btn btn-primary inline-flex items-center justify-center'>
|
||||
<i class='fa fa-sign-in mr-2'></i> 登录后台
|
||||
</a>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
// 第一步:检测环境
|
||||
protected function checkEnvironment()
|
||||
{
|
||||
$phpVersion = PHP_VERSION;
|
||||
$extensions = [
|
||||
'pdo_mysql' => 'MySQL PDO 驱动(必须)',
|
||||
'mbstring' => '多字节字符串支持(必须)',
|
||||
'json' => 'JSON 解析支持(必须)',
|
||||
'openssl' => 'OpenSSL 加密扩展(必须)',
|
||||
'fileinfo' => '文件信息扩展(必须)',
|
||||
'pdo' => 'PDO 基础扩展(可选,一般随 pdo_mysql 自动启用)'
|
||||
];
|
||||
$writableDirs = [
|
||||
ROOT_PATH . "Db" => "数据库配置目录",
|
||||
ROOT_PATH . "Storage" => "文件存储目录"
|
||||
];
|
||||
|
||||
// 检查PHP版本
|
||||
$phpVersionOk = version_compare($phpVersion, '8.0.0', '>=');
|
||||
|
||||
// 检查扩展
|
||||
$allExtensionsOk = true;
|
||||
$extensionResults = [];
|
||||
foreach ($extensions as $ext => $desc) {
|
||||
$installed = extension_loaded($ext);
|
||||
if (!$installed && $ext !== 'pdo') { // pdo 可选
|
||||
$allExtensionsOk = false;
|
||||
}
|
||||
$extensionResults[] = [
|
||||
'name' => $ext,
|
||||
'desc' => $desc,
|
||||
'installed' => $installed
|
||||
];
|
||||
}
|
||||
|
||||
// 检查目录写权限(真实写入测试)
|
||||
$allDirsWritable = true;
|
||||
$dirResults = [];
|
||||
foreach ($writableDirs as $dir => $desc) {
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
$testFile = $dir . '/.test';
|
||||
$canWrite = @file_put_contents($testFile, 'test') !== false;
|
||||
if ($canWrite) {
|
||||
unlink($testFile);
|
||||
} else {
|
||||
$allDirsWritable = false;
|
||||
}
|
||||
$dirResults[] = [
|
||||
'path' => $dir,
|
||||
'desc' => $desc,
|
||||
'writable' => $canWrite
|
||||
];
|
||||
}
|
||||
|
||||
// 检查 PHP 配置
|
||||
$fileUploadsOk = ini_get('file_uploads');
|
||||
|
||||
echo "<h2 class='text-2xl font-bold text-gray-800 mb-6 flex items-center'>
|
||||
<i class='fa fa-server text-primary mr-3'></i>环境检测
|
||||
</h2>";
|
||||
|
||||
// PHP版本检查
|
||||
echo "<div class='mb-4 p-4 border rounded-lg " . ($phpVersionOk ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
|
||||
<div class='flex justify-between items-center'>
|
||||
<div>
|
||||
<h3 class='font-medium text-gray-800'>PHP版本</h3>
|
||||
<p class='text-sm text-gray-600 mt-1'>需要PHP 8.0.0或更高版本</p>
|
||||
</div>
|
||||
<span class='text-lg " . ($phpVersionOk ? 'text-secondary' : 'text-danger') . "'>
|
||||
" . ($phpVersionOk ? '✅' : '❌') . "
|
||||
</span>
|
||||
</div>
|
||||
<p class='mt-2 text-sm text-gray-700'>当前版本: $phpVersion</p>
|
||||
</div>";
|
||||
|
||||
// 扩展检查
|
||||
echo "<div class='mb-4'>
|
||||
<h3 class='font-medium text-gray-800 mb-3'>必要扩展</h3>
|
||||
<div class='grid grid-cols-1 md:grid-cols-2 gap-3'>";
|
||||
foreach ($extensionResults as $ext) {
|
||||
echo "<div class='p-3 border rounded-lg " . ($ext['installed'] ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
|
||||
<div class='flex justify-between items-center'>
|
||||
<span>{$ext['desc']} ({$ext['name']})</span>
|
||||
<span class='" . ($ext['installed'] ? 'text-secondary' : 'text-danger') . "'>
|
||||
" . ($ext['installed'] ? '✅' : '❌') . "
|
||||
</span>
|
||||
</div>";
|
||||
if (!$ext['installed']) {
|
||||
echo "<p class='text-xs text-gray-500 mt-1'>Linux 安装命令示例: <code>apt install php-{$ext['name']}</code></p>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
echo "</div></div>";
|
||||
|
||||
// 文件权限检查
|
||||
echo "<div class='mb-6'>
|
||||
<h3 class='font-medium text-gray-800 mb-3'>文件权限</h3>
|
||||
<div class='space-y-3'>";
|
||||
foreach ($dirResults as $dir) {
|
||||
echo "<div class='p-3 border rounded-lg " . ($dir['writable'] ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
|
||||
<div class='flex justify-between items-center'>
|
||||
<span class='text-sm truncate max-w-[70%]'>{$dir['desc']} ({$dir['path']})</span>
|
||||
<span class='" . ($dir['writable'] ? 'text-secondary' : 'text-danger') . "'>
|
||||
" . ($dir['writable'] ? '✅' : '❌') . "
|
||||
</span>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
echo "</div></div>";
|
||||
|
||||
// PHP 配置检查
|
||||
echo "<div class='mb-6 p-4 border rounded-lg " . ($fileUploadsOk ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
|
||||
<div class='flex justify-between items-center'>
|
||||
<span>文件上传 (file_uploads)</span>
|
||||
<span class='" . ($fileUploadsOk ? 'text-secondary' : 'text-danger') . "'>
|
||||
" . ($fileUploadsOk ? '✅' : '❌') . "
|
||||
</span>
|
||||
</div>
|
||||
</div>";
|
||||
|
||||
// 下一步按钮
|
||||
$allChecksPassed = $phpVersionOk && $allExtensionsOk && $allDirsWritable && $fileUploadsOk;
|
||||
echo "<div class='flex justify-end mt-8'>";
|
||||
if ($allChecksPassed) {
|
||||
echo "<a href='?step=2' class='btn btn-primary flex items-center'>
|
||||
<span>下一步:数据库设置</span>
|
||||
<i class='fa fa-arrow-right ml-2'></i>
|
||||
</a>";
|
||||
} else {
|
||||
echo "<button class='btn bg-gray-300 text-gray-500 cursor-not-allowed' disabled>
|
||||
<span>请先解决所有问题</span>
|
||||
<i class='fa fa-arrow-right ml-2'></i>
|
||||
</button>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
|
||||
|
||||
// 第二步:数据库表单
|
||||
protected function databaseForm()
|
||||
{
|
||||
echo "<h2 class='text-2xl font-bold text-gray-800 mb-6 flex items-center'>
|
||||
<i class='fa fa-database text-primary mr-3'></i>数据库设置
|
||||
</h2>";
|
||||
|
||||
echo '<form method="post" class="space-y-4">
|
||||
<input type="hidden" name="step" value="2">
|
||||
|
||||
<div>
|
||||
<label for="db_host" class="block text-sm font-medium text-gray-700 mb-1">数据库主机</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-server -mt-5"></i>
|
||||
</div>
|
||||
<input type="text" id="db_host" name="db_host" value="localhost" required
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
<p class="mt-1 text-xs text-gray-500">通常为 localhost 或 127.0.0.1</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="db_name" class="block text-sm font-medium text-gray-700 mb-1">数据库名称</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-database"></i>
|
||||
</div>
|
||||
<input type="text" id="db_name" name="db_name" required
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="db_user" class="block text-sm font-medium text-gray-700 mb-1">数据库用户名</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-user"></i>
|
||||
</div>
|
||||
<input type="text" id="db_user" name="db_user" required
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="db_pass" class="block text-sm font-medium text-gray-700 mb-1">数据库密码</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-lock -mt-5"></i>
|
||||
</div>
|
||||
<input type="password" id="db_pass" name="db_pass"
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
<p class="mt-1 text-xs text-gray-500">如果数据库无密码,请留空</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<a href="?step=1" class="btn btn-secondary flex items-center">
|
||||
<i class="fa fa-arrow-left mr-2"></i>
|
||||
<span>上一步</span>
|
||||
</a>
|
||||
|
||||
<button type="submit" class="btn btn-primary flex items-center">
|
||||
<span>保存并测试</span>
|
||||
<i class="fa fa-arrow-right ml-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>';
|
||||
}
|
||||
|
||||
// 第三步:创建管理员
|
||||
protected function adminForm()
|
||||
{
|
||||
// 检查配置文件是否存在
|
||||
if (!file_exists($this->configFile)) {
|
||||
$this->setMessage('数据库配置文件不存在,请先完成数据库设置', 'error');
|
||||
$this->showMessage();
|
||||
echo "<div class='flex justify-start mt-8'>
|
||||
<a href='?step=2' class='btn btn-secondary flex items-center'>
|
||||
<i class='fa fa-arrow-left mr-2'></i>
|
||||
<span>返回数据库设置</span>
|
||||
</a>
|
||||
</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-user text-primary mr-3"></i>创建管理员账号
|
||||
</h2>';
|
||||
|
||||
echo '<form method="post" class="space-y-4">
|
||||
<input type="hidden" name="step" value="3">
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">管理员用户名</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-user-circle"></i>
|
||||
</div>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-user-circle"></i>
|
||||
</div>
|
||||
<input type="text" id="email" name="email" required
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
</div>
|
||||
</div> <div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">管理员密码</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<i class="fa fa-shield"></i>
|
||||
</div>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
|
||||
<p class="mt-1 text-xs text-gray-500">请设置强密码(至少8位,包含字母和数字)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<a href="?step=2" class="btn btn-secondary flex items-center">
|
||||
<i class="fa fa-arrow-left mr-2"></i>
|
||||
<span>上一步</span>
|
||||
</a>
|
||||
|
||||
<button type="submit" class="btn btn-primary flex items-center">
|
||||
<span>创建管理员</span>
|
||||
<i class="fa fa-check ml-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>';
|
||||
}
|
||||
|
||||
// 第四步:完成安装
|
||||
protected function finish()
|
||||
{
|
||||
// 关键修复:在完成页面创建最终的安装锁定文件
|
||||
if (file_exists(ROOT_PATH . "Db/install_completed.tmp")) {
|
||||
rename(ROOT_PATH . "Db/install_completed.tmp", $this->installedLockFile);
|
||||
}
|
||||
|
||||
echo "<div class='text-center py-8'>
|
||||
<div class='inline-flex items-center justify-center w-20 h-20 rounded-full bg-secondary/10 text-secondary mb-6'>
|
||||
<i class='fa fa-check-circle text-3xl'></i>
|
||||
</div>
|
||||
<h2 class='text-2xl font-bold text-gray-800 mb-3'>安装完成!</h2>
|
||||
<p class='text-gray-600 mb-8 max-w-md mx-auto'>管理员账号已创建,系统安装成功,您可以登录后台开始使用了。</p>
|
||||
|
||||
<div class='flex flex-col sm:flex-row justify-center gap-4'>
|
||||
<a href='/admin/login' class='btn btn-primary inline-flex items-center justify-center'>
|
||||
<i class='fa fa-sign-in mr-2'></i> 登录后台
|
||||
</a>
|
||||
<a href='/' class='btn btn-secondary inline-flex items-center justify-center'>
|
||||
<i class='fa fa-home mr-2'></i> 访问首页
|
||||
</a>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use App\Core\PK10Algorithm;
|
||||
use Db\Database;
|
||||
|
||||
class PK10PeriodController extends AdminBaseController {
|
||||
private $db;
|
||||
|
||||
public function __construct(Database $db) {
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin();
|
||||
$gameId = $this->getGameId();
|
||||
|
||||
$periods = $this->db->select('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 50
|
||||
]);
|
||||
|
||||
// 附加pk10_results
|
||||
foreach ($periods as &$p) {
|
||||
$p['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $p['id']]);
|
||||
}
|
||||
|
||||
$current = $this->db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'status[!]' => 'settled',
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if ($current) {
|
||||
$current['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $current['id']]);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total' => $this->db->count('periods', ['game_id' => $gameId]),
|
||||
'pending' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'pending']),
|
||||
'drawn' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'drawn']),
|
||||
'settled' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'settled']),
|
||||
];
|
||||
|
||||
$this->render('Admin/pk10_periods.php', compact('periods', 'current', 'stats', 'gameId'));
|
||||
}
|
||||
|
||||
public function start() {
|
||||
$this->checkLogin();
|
||||
$gameId = $this->getGameId();
|
||||
|
||||
// 检查是否有未结算期号
|
||||
$active = $this->db->get('periods', 'id', [
|
||||
'game_id' => $gameId,
|
||||
'status[!]' => 'settled'
|
||||
]);
|
||||
if ($active) {
|
||||
$this->json(['status' => 'error', 'message' => 'There is an active period. Please settle it first.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$periodNumber = PK10Algorithm::generatePeriodNumber($gameId);
|
||||
$this->db->insert('periods', [
|
||||
'game_id' => $gameId,
|
||||
'period_number' => $periodNumber,
|
||||
'status' => 'pending',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
$this->json(['status' => 'success', 'message' => 'New period started', 'period_number' => $periodNumber]);
|
||||
}
|
||||
|
||||
public function lock() {
|
||||
$this->checkLogin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$periodId = (int)($data['period_id'] ?? 0);
|
||||
|
||||
$period = $this->db->get('periods', '*', ['id' => $periodId]);
|
||||
if (!$period || $period['status'] !== 'pending') {
|
||||
$this->json(['status' => 'error', 'message' => 'Invalid period or status']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->update('periods', ['status' => 'locked'], ['id' => $periodId]);
|
||||
$this->json(['status' => 'success', 'message' => 'Period locked']);
|
||||
}
|
||||
|
||||
public function draw() {
|
||||
$this->checkLogin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$periodId = (int)($data['period_id'] ?? 0);
|
||||
$manual = $data['manual'] ?? false;
|
||||
|
||||
$period = $this->db->get('periods', '*', ['id' => $periodId]);
|
||||
if (!$period || !in_array($period['status'], ['pending', 'locked'])) {
|
||||
$this->json(['status' => 'error', 'message' => 'Invalid period or status']);
|
||||
return;
|
||||
}
|
||||
|
||||
$gameId = $period['game_id'];
|
||||
|
||||
if ($manual && !empty($data['result'])) {
|
||||
// 手动补开奖
|
||||
$result = array_map('intval', $data['result']);
|
||||
if (count($result) !== 10 || count(array_unique($result)) !== 10) {
|
||||
$this->json(['status' => 'error', 'message' => 'Result must be 10 unique numbers 1-10']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 自动开奖(带放水机制)
|
||||
$waterConfig = [];
|
||||
$wc = $this->db->select('water_control', '*', ['game_id' => $gameId, 'enabled' => 1]);
|
||||
foreach ($wc as $w) {
|
||||
$waterConfig[$w['bet_type']] = (float)$w['win_rate_pct'];
|
||||
}
|
||||
|
||||
$bets = $this->db->select('bets', '*', [
|
||||
'period_id' => $periodId,
|
||||
'status' => 'pending',
|
||||
'is_virtual' => 0
|
||||
]);
|
||||
|
||||
if (!empty($waterConfig) && !empty($bets)) {
|
||||
$result = PK10Algorithm::generateControlledResult($bets, $waterConfig);
|
||||
} else {
|
||||
$result = PK10Algorithm::generateResult();
|
||||
}
|
||||
}
|
||||
|
||||
$sum = $result[0] + $result[1];
|
||||
$resultJson = json_encode($result);
|
||||
|
||||
$this->db->medoo->pdo->beginTransaction();
|
||||
try {
|
||||
$this->db->update('periods', [
|
||||
'status' => 'drawn',
|
||||
'result' => $resultJson,
|
||||
'draw_time' => date('Y-m-d H:i:s'),
|
||||
], ['id' => $periodId]);
|
||||
|
||||
// 删除旧结果(如果手动修改)
|
||||
$this->db->delete('pk10_results', ['period_id' => $periodId]);
|
||||
|
||||
$this->db->insert('pk10_results', [
|
||||
'period_id' => $periodId,
|
||||
'rank_1' => $result[0], 'rank_2' => $result[1], 'rank_3' => $result[2],
|
||||
'rank_4' => $result[3], 'rank_5' => $result[4], 'rank_6' => $result[5],
|
||||
'rank_7' => $result[6], 'rank_8' => $result[7], 'rank_9' => $result[8],
|
||||
'rank_10' => $result[9], 'champion_sum' => $sum,
|
||||
]);
|
||||
|
||||
$this->db->medoo->pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->medoo->pdo->rollBack();
|
||||
$this->json(['status' => 'error', 'message' => 'Draw failed: ' . $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->json(['status' => 'success', 'message' => 'Draw completed', 'result' => $result, 'sum' => $sum]);
|
||||
}
|
||||
|
||||
public function settle() {
|
||||
$this->checkLogin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$periodId = (int)($data['period_id'] ?? 0);
|
||||
|
||||
$period = $this->db->get('periods', '*', ['id' => $periodId]);
|
||||
if (!$period || $period['status'] !== 'drawn') {
|
||||
$this->json(['status' => 'error', 'message' => 'Period must be drawn first']);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = json_decode($period['result'], true);
|
||||
if (!$result || count($result) !== 10) {
|
||||
$this->json(['status' => 'error', 'message' => 'Invalid result data']);
|
||||
return;
|
||||
}
|
||||
|
||||
$bets = $this->db->select('bets', '*', ['period_id' => $periodId, 'status' => 'pending']);
|
||||
if (empty($bets)) {
|
||||
$this->db->update('periods', ['status' => 'settled'], ['id' => $periodId]);
|
||||
$this->json(['status' => 'success', 'message' => 'No bets to settle', 'wins' => 0, 'losses' => 0]);
|
||||
return;
|
||||
}
|
||||
|
||||
$wins = 0; $losses = 0; $totalPayout = 0;
|
||||
|
||||
$this->db->medoo->pdo->beginTransaction();
|
||||
try {
|
||||
foreach ($bets as $bet) {
|
||||
$isWin = PK10Algorithm::checkWin($result, $bet['bet_type'], $bet['bet_value']);
|
||||
$betAmount = (float)$bet['amount'];
|
||||
$odds = (float)$bet['odds'];
|
||||
|
||||
if ($isWin) {
|
||||
$winAmount = $betAmount * $odds;
|
||||
$payout = $betAmount + $winAmount;
|
||||
$this->db->update('bets', [
|
||||
'status' => 'win',
|
||||
'win_amount' => $winAmount,
|
||||
], ['id' => $bet['id']]);
|
||||
|
||||
// 非虚拟账户才更新余额
|
||||
if (!(int)$bet['is_virtual']) {
|
||||
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
|
||||
$stmt->execute([':id' => $bet['user_id']]);
|
||||
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$newBalance = (float)$user['balance'] + $payout;
|
||||
|
||||
$this->db->update('users', ['balance' => $newBalance], ['id' => $bet['user_id']]);
|
||||
$this->db->insert('transactions', [
|
||||
'user_id' => $bet['user_id'],
|
||||
'type' => 'win',
|
||||
'amount' => $payout,
|
||||
'balance_before' => $user['balance'],
|
||||
'balance_after' => $newBalance,
|
||||
'related_id' => $periodId,
|
||||
'description' => 'PK10 Win - Period ' . $period['period_number'],
|
||||
'is_virtual' => $bet['is_virtual'],
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$totalPayout += $payout;
|
||||
}
|
||||
$wins++;
|
||||
} else {
|
||||
$this->db->update('bets', ['status' => 'lose', 'win_amount' => 0], ['id' => $bet['id']]);
|
||||
$losses++;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算代理佣金
|
||||
$this->settleAgentCommissions($bets, $periodId);
|
||||
|
||||
$this->db->update('periods', ['status' => 'settled'], ['id' => $periodId]);
|
||||
$this->db->medoo->pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->medoo->pdo->rollBack();
|
||||
$this->json(['status' => 'error', 'message' => 'Settle failed: ' . $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->json(['status' => 'success', 'message' => 'Settlement completed',
|
||||
'wins' => $wins, 'losses' => $losses, 'total_payout' => $totalPayout]);
|
||||
}
|
||||
|
||||
private function settleAgentCommissions(array $bets, int $periodId) {
|
||||
$agentBets = [];
|
||||
foreach ($bets as $bet) {
|
||||
if ((int)$bet['is_virtual']) continue;
|
||||
$user = $this->db->get('users', ['agent_id'], ['id' => $bet['user_id']]);
|
||||
if (!$user || !$user['agent_id']) continue;
|
||||
$agentId = $user['agent_id'];
|
||||
if (!isset($agentBets[$agentId])) $agentBets[$agentId] = 0;
|
||||
$agentBets[$agentId] += (float)$bet['amount'];
|
||||
}
|
||||
|
||||
foreach ($agentBets as $agentId => $totalBet) {
|
||||
$agent = $this->db->get('agents', '*', ['id' => $agentId, 'status' => 1]);
|
||||
if (!$agent) continue;
|
||||
$commission = $totalBet * $agent['commission_rate'] / 100;
|
||||
if ($commission <= 0) continue;
|
||||
|
||||
$this->db->insert('agent_commissions', [
|
||||
'agent_id' => $agentId,
|
||||
'from_user_id' => 0,
|
||||
'period_id' => $periodId,
|
||||
'bet_amount' => $totalBet,
|
||||
'commission' => $commission,
|
||||
'type' => 'bet',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
// 佣金加到代理用户余额
|
||||
$agentUserId = $agent['user_id'];
|
||||
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
|
||||
$stmt->execute([':id' => $agentUserId]);
|
||||
$au = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$newBal = (float)$au['balance'] + $commission;
|
||||
$this->db->update('users', ['balance' => $newBal], ['id' => $agentUserId]);
|
||||
$this->db->insert('transactions', [
|
||||
'user_id' => $agentUserId, 'type' => 'commission',
|
||||
'amount' => $commission, 'balance_before' => $au['balance'],
|
||||
'balance_after' => $newBal, 'related_id' => $periodId,
|
||||
'description' => 'Agent commission', 'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get($id) {
|
||||
$this->checkLogin();
|
||||
$period = $this->db->get('periods', '*', ['id' => (int)$id]);
|
||||
if ($period) {
|
||||
$period['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $period['id']]);
|
||||
}
|
||||
$this->json(['status' => 'success', 'data' => $period]);
|
||||
}
|
||||
|
||||
private function getGameId(): int {
|
||||
$game = $this->db->get('games', 'id', ['code' => 'pk10']);
|
||||
return $game ? (int)$game : 0;
|
||||
}
|
||||
|
||||
private function json(array $data) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
}
|
||||
}
|
||||
Executable
+957
@@ -0,0 +1,957 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class PeriodController extends AdminBaseController {
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 期号列表页面
|
||||
*/
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
try {
|
||||
// 获取所有期号列表
|
||||
$periods = $db->select('periods', '*', [
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 100
|
||||
]);
|
||||
|
||||
if (!is_array($periods)) {
|
||||
$periods = [];
|
||||
}
|
||||
|
||||
// 获取游戏列表(用于关联显示)
|
||||
$games = [];
|
||||
$gamesList = [];
|
||||
try {
|
||||
$gamesList = $db->select('games', ['id', 'name'], [
|
||||
'status' => 1,
|
||||
'ORDER' => ['id' => 'ASC']
|
||||
]);
|
||||
if (is_array($gamesList)) {
|
||||
foreach ($gamesList as $game) {
|
||||
$games[$game['id']] = $game['name'];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
// 为每个游戏获取当前期号
|
||||
$currentPeriods = [];
|
||||
foreach ($gamesList as $game) {
|
||||
$currentPeriod = $db->get('periods', '*', [
|
||||
'game_id' => $game['id'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if ($currentPeriod) {
|
||||
$currentPeriods[$game['id']] = $currentPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$periods = [];
|
||||
$games = [];
|
||||
$gamesList = [];
|
||||
$currentPeriods = [];
|
||||
}
|
||||
|
||||
$this->render('Admin/periods.php', [
|
||||
'currentPeriods' => $currentPeriods, // 改为多个游戏的当前期号
|
||||
'periods' => $periods,
|
||||
'games' => $games,
|
||||
'gamesList' => $gamesList, // 完整的游戏列表
|
||||
'title' => '期号管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个期号数据
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$period = $db->get('periods', '*', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($period) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $period
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '获取数据失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建期号
|
||||
*/
|
||||
public function create() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : null;
|
||||
|
||||
// 如果关联了游戏,从游戏中获取直播流地址
|
||||
$streamUrl = null;
|
||||
if ($gameId) {
|
||||
try {
|
||||
$game = $db->get('games', ['stream_url'], ['id' => $gameId]);
|
||||
if ($game && !empty($game['stream_url'])) {
|
||||
$streamUrl = $game['stream_url'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略错误,继续使用 null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前登录的管理员ID
|
||||
$createdBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
|
||||
// 生成期号
|
||||
$periodNumber = $this->generatePeriodNumber($db, $gameId);
|
||||
|
||||
// 准备数据
|
||||
$periodData = [
|
||||
'period_number' => $periodNumber,
|
||||
'status' => 'pending',
|
||||
'game_id' => $gameId ?: null,
|
||||
'stream_url' => $streamUrl,
|
||||
'start_time' => date('Y-m-d H:i:s'),
|
||||
'created_by' => $createdBy,
|
||||
'auto_generated' => 0, // 手动创建
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
$id = $db->insert('periods', $periodData);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '期号创建成功',
|
||||
'data' => ['id' => $id, 'period_number' => $periodNumber]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 录入开奖结果
|
||||
*/
|
||||
public function draw() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] === 'settled') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号已结算,无法修改'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取游戏类型
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
$gameType = $game['type'] ?? 'dice';
|
||||
|
||||
$auto = isset($data['auto']) ? (bool)$data['auto'] : false;
|
||||
|
||||
// 根据游戏类型处理开奖结果
|
||||
if ($gameType === 'xocdia') {
|
||||
// Xóc Đĩa 开奖逻辑
|
||||
if ($auto) {
|
||||
// 自动生成:4个硬币随机红/白
|
||||
$coins = [];
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$coins[] = (rand(0, 1) === 0) ? 'red' : 'white';
|
||||
}
|
||||
} else {
|
||||
// 手动输入:从前端获取
|
||||
$coins = isset($data['coins']) ? $data['coins'] : [];
|
||||
if (!is_array($coins) || count($coins) !== 4) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Xóc Đĩa 必须提供4个硬币的颜色(red/white)'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
// 验证颜色
|
||||
foreach ($coins as $coin) {
|
||||
if (!in_array($coin, ['red', 'white'])) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '硬币颜色只能是 red 或 white'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算结果
|
||||
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
|
||||
$result = $this->calculateXocdiaResult($redCount);
|
||||
|
||||
// 获取当前登录的管理员ID(审核人)
|
||||
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
|
||||
// 更新期号数据
|
||||
$updateData = [
|
||||
'result' => json_encode($coins), // 存储JSON数组
|
||||
'dice1' => $redCount, // 复用字段:红色数量
|
||||
'dice2' => 4 - $redCount, // 复用字段:白色数量
|
||||
'dice3' => null,
|
||||
'total' => null,
|
||||
'status' => 'drawn',
|
||||
'draw_time' => date('Y-m-d H:i:s'),
|
||||
'approved_by' => $approvedBy,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$responseData = [
|
||||
'coins' => $coins,
|
||||
'red_count' => $redCount,
|
||||
'white_count' => 4 - $redCount,
|
||||
'result' => $result
|
||||
];
|
||||
} else {
|
||||
// 骰子游戏开奖逻辑(原逻辑)
|
||||
if ($auto) {
|
||||
$dice1 = rand(1, 6);
|
||||
$dice2 = rand(1, 6);
|
||||
$dice3 = rand(1, 6);
|
||||
} else {
|
||||
$dice1 = isset($data['dice1']) ? (int)$data['dice1'] : 0;
|
||||
$dice2 = isset($data['dice2']) ? (int)$data['dice2'] : 0;
|
||||
$dice3 = isset($data['dice3']) ? (int)$data['dice3'] : 0;
|
||||
|
||||
// 验证骰子点数
|
||||
if ($dice1 < 1 || $dice1 > 6 || $dice2 < 1 || $dice2 > 6 || $dice3 < 1 || $dice3 > 6) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '骰子点数必须在1-6之间'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总和
|
||||
$total = $dice1 + $dice2 + $dice3;
|
||||
|
||||
// 计算结果
|
||||
$result = $this->calculateResult($dice1, $dice2, $dice3, $total);
|
||||
|
||||
// 获取当前登录的管理员ID(审核人)
|
||||
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
|
||||
// 更新期号数据
|
||||
$updateData = [
|
||||
'dice1' => $dice1,
|
||||
'dice2' => $dice2,
|
||||
'dice3' => $dice3,
|
||||
'total' => $total,
|
||||
'result' => $result,
|
||||
'status' => 'drawn',
|
||||
'draw_time' => date('Y-m-d H:i:s'),
|
||||
'approved_by' => $approvedBy,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$responseData = [
|
||||
'dice1' => $dice1,
|
||||
'dice2' => $dice2,
|
||||
'dice3' => $dice3,
|
||||
'total' => $total,
|
||||
'result' => $result
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$db->update('periods', $updateData, ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '开奖结果已录入',
|
||||
'data' => $responseData
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '录入失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动新一期(开始下注)
|
||||
*/
|
||||
public function start() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
|
||||
|
||||
if ($gameId <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请选择游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 检查该游戏是否有未结束的期号
|
||||
$activePeriod = $db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'status' => ['pending', 'locked', 'drawn']
|
||||
]);
|
||||
|
||||
if ($activePeriod) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该游戏当前有未结束的期号,无法开始新一轮'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成期号
|
||||
$periodNumber = $this->generatePeriodNumber($db, $gameId);
|
||||
|
||||
// 从游戏配置��取stream_url
|
||||
$streamUrl = null;
|
||||
$game = $db->get('games', ['stream_url'], ['id' => $gameId]);
|
||||
if ($game && !empty($game['stream_url'])) {
|
||||
$streamUrl = $game['stream_url'];
|
||||
}
|
||||
|
||||
$periodData = [
|
||||
'period_number' => $periodNumber,
|
||||
'status' => 'pending',
|
||||
'game_id' => $gameId,
|
||||
'stream_url' => $streamUrl,
|
||||
'start_time' => date('Y-m-d H:i:s'),
|
||||
'auto_generated' => 0, // 手动点击开始
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
$id = $db->insert('periods', $periodData);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '新一期已启动,开始下注',
|
||||
'data' => [
|
||||
'id' => $id,
|
||||
'period_number' => $periodNumber,
|
||||
'start_time' => $periodData['start_time']
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '启动失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认开奖并结算(自动创建下一期)
|
||||
*/
|
||||
public function settle() {
|
||||
ob_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] !== 'drawn') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号还未开奖,无法结算'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($period['status'] === 'settled') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号已结算'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 开启事务
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
|
||||
// 1. 获取本期所有待结算注单
|
||||
$bets = $db->select('bets', '*', [
|
||||
'period_id' => $id,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
// 2. 遍历注单进行结算
|
||||
if ($bets) {
|
||||
foreach ($bets as $bet) {
|
||||
$checkResult = $this->checkWin($bet, $period);
|
||||
$isWin = $checkResult['win'];
|
||||
$winAmount = $checkResult['amount']; // 纯赢金额
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($isWin) {
|
||||
$payout = $bet['amount'] + $winAmount; // 本金 + 盈利
|
||||
|
||||
// 更新注单状态
|
||||
$db->update('bets', [
|
||||
'status' => 'win',
|
||||
'win_amount' => $winAmount,
|
||||
'settled_at' => $now,
|
||||
'updated_at' => $now
|
||||
], ['id' => $bet['id']]);
|
||||
|
||||
// 更新用户余额
|
||||
$db->update('users', [
|
||||
'balance[+]' => $payout
|
||||
], ['id' => $bet['user_id']]);
|
||||
|
||||
// 获取更新后的余额(用于记录流水)
|
||||
$user = $db->get('users', ['balance'], ['id' => $bet['user_id']]);
|
||||
$balanceAfter = $user['balance'];
|
||||
$balanceBefore = $balanceAfter - $payout;
|
||||
|
||||
// 写入资金流水
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $bet['user_id'],
|
||||
'type' => 'win',
|
||||
'amount' => $payout,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'related_id' => $bet['id'],
|
||||
'description' => "中奖 - 期号: " . $period['period_number'],
|
||||
'created_at' => $now
|
||||
]);
|
||||
|
||||
} else {
|
||||
// 未中奖
|
||||
$db->update('bets', [
|
||||
'status' => 'lose',
|
||||
'win_amount' => 0,
|
||||
'settled_at' => $now,
|
||||
'updated_at' => $now
|
||||
], ['id' => $bet['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新期号状态为已结算
|
||||
$db->update('periods', [
|
||||
'status' => 'settled',
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
// 移除自动创建下一期的逻辑,改为由管理员手动点击"开始下注"
|
||||
/*
|
||||
// 自动创建下一期
|
||||
$nextPeriodNumber = $this->generatePeriodNumber($db);
|
||||
|
||||
// 如果当前期号没有 stream_url,尝试从关联游戏中获取
|
||||
$nextStreamUrl = $period['stream_url'];
|
||||
if (empty($nextStreamUrl) && !empty($period['game_id'])) {
|
||||
try {
|
||||
$game = $db->get('games', ['stream_url'], ['id' => $period['game_id']]);
|
||||
if ($game && !empty($game['stream_url'])) {
|
||||
$nextStreamUrl = $game['stream_url'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略错误,继续使用原值
|
||||
}
|
||||
}
|
||||
|
||||
$nextPeriodData = [
|
||||
'period_number' => $nextPeriodNumber,
|
||||
'status' => 'pending',
|
||||
'game_id' => $period['game_id'],
|
||||
'stream_url' => $nextStreamUrl,
|
||||
'start_time' => date('Y-m-d H:i:s'),
|
||||
'auto_generated' => 1, // 自动生成
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$nextPeriodId = $db->insert('periods', $nextPeriodData);
|
||||
*/
|
||||
$nextPeriodId = 0;
|
||||
$nextPeriodNumber = '';
|
||||
|
||||
// 提交事务
|
||||
$db->medoo->pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '结算成功,请点击"开始下注"启动下一期',
|
||||
'data' => [
|
||||
'current_period_id' => $id,
|
||||
// 'next_period_id' => $nextPeriodId,
|
||||
// 'next_period_number' => $nextPeriodNumber
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
if (isset($db->medoo->pdo)) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '结算失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 封盘
|
||||
*/
|
||||
public function lock() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] !== 'pending') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号状态不允许封盘'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->update('periods', [
|
||||
'status' => 'locked',
|
||||
'end_time' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '封盘成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '封盘失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成期号
|
||||
* 格式:G{GameID}{YYYYMMDD}{NNNN}
|
||||
* 例如:G1202512240001
|
||||
*/
|
||||
private function generatePeriodNumber($db, $gameId = null) {
|
||||
// 如果没有指定游戏ID,默认使用1
|
||||
$gameId = $gameId ?: 1;
|
||||
|
||||
$now = new \DateTime();
|
||||
$dateStr = $now->format('Ymd');
|
||||
$prefix = "G{$gameId}{$dateStr}";
|
||||
|
||||
// 查询当天该游戏最大的期号
|
||||
try {
|
||||
$lastPeriod = $db->get('periods', 'period_number', [
|
||||
'period_number[~]' => $prefix . '%',
|
||||
'ORDER' => ['period_number' => 'DESC']
|
||||
]);
|
||||
|
||||
if ($lastPeriod) {
|
||||
// 提取序号并+1
|
||||
$lastSeq = substr($lastPeriod, strlen($prefix));
|
||||
$newSeq = (int)$lastSeq + 1;
|
||||
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
// 当天第一期
|
||||
$seqStr = '0001';
|
||||
}
|
||||
|
||||
return $prefix . $seqStr;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// 发生错误时回退到时间戳随机数,防止阻塞
|
||||
return $prefix . time();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查注单是否中奖
|
||||
*/
|
||||
private function checkWin($bet, $period) {
|
||||
$betType = $bet['bet_type'];
|
||||
$betValue = $bet['bet_value'];
|
||||
$betAmount = (float)$bet['amount'];
|
||||
$gameId = $period['game_id'] ?? 1;
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取游戏类型
|
||||
$game = $db->get('games', ['type'], ['id' => $gameId]);
|
||||
$gameType = $game['type'] ?? 'dice';
|
||||
|
||||
// 获取该游戏的所有赔率配置
|
||||
static $oddsCache = [];
|
||||
if (!isset($oddsCache[$gameId])) {
|
||||
$oddsData = $db->select('game_odds', '*', ['game_id' => $gameId]);
|
||||
$oddsCache[$gameId] = [];
|
||||
foreach ($oddsData as $odd) {
|
||||
$key = $odd['type'] . '_' . $odd['target'];
|
||||
$oddsCache[$gameId][$key] = (float)$odd['odds'];
|
||||
}
|
||||
}
|
||||
|
||||
$isWin = false;
|
||||
$payoutMultiplier = 0;
|
||||
|
||||
// 辅助函数:获取赔率
|
||||
$getOdds = function($type, $target = 'all') use ($oddsCache, $gameId) {
|
||||
$key = $type . '_' . $target;
|
||||
// 如果找不到特定target的赔率,尝试查找 'all'
|
||||
if (isset($oddsCache[$gameId][$key])) {
|
||||
return $oddsCache[$gameId][$key];
|
||||
}
|
||||
$allKey = $type . '_all';
|
||||
return $oddsCache[$gameId][$allKey] ?? 0;
|
||||
};
|
||||
|
||||
// Xóc Đĩa 游戏结算逻辑
|
||||
if ($gameType === 'xocdia') {
|
||||
// 从 result 字段解析硬币颜色数组
|
||||
$coins = json_decode($period['result'], true);
|
||||
if (!is_array($coins) || count($coins) !== 4) {
|
||||
return ['win' => false, 'amount' => 0];
|
||||
}
|
||||
|
||||
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
|
||||
$whiteCount = 4 - $redCount;
|
||||
|
||||
switch ($betType) {
|
||||
case 'chan': // 双(偶): 4红 或 4白 或 2红2白
|
||||
if (in_array($redCount, [0, 2, 4])) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('chan', $betValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'le': // 单(奇): 3红1白 或 3白1红
|
||||
if (in_array($redCount, [1, 3])) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('le', $betValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'exact': // 精确颜色组合
|
||||
switch ($betValue) {
|
||||
case '4red':
|
||||
if ($redCount === 4) $isWin = true;
|
||||
break;
|
||||
case '4white':
|
||||
if ($redCount === 0) $isWin = true;
|
||||
break;
|
||||
case '3red1white':
|
||||
if ($redCount === 3) $isWin = true;
|
||||
break;
|
||||
case '3white1red':
|
||||
if ($redCount === 1) $isWin = true;
|
||||
break;
|
||||
}
|
||||
if ($isWin) {
|
||||
$payoutMultiplier = $getOdds('exact', $betValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($isWin && $payoutMultiplier > 0) {
|
||||
return [
|
||||
'win' => true,
|
||||
'amount' => $betAmount * $payoutMultiplier
|
||||
];
|
||||
}
|
||||
|
||||
return ['win' => false, 'amount' => 0];
|
||||
}
|
||||
|
||||
// 骰子游戏结算逻辑(原逻辑)
|
||||
$dice1 = (int)$period['dice1'];
|
||||
$dice2 = (int)$period['dice2'];
|
||||
$dice3 = (int)$period['dice3'];
|
||||
$total = (int)$period['total'];
|
||||
|
||||
switch ($betType) {
|
||||
case 'xiu': // 小 (4-10)
|
||||
// 爆子处理:1-3为小,4-6为大
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3) {
|
||||
if ($dice1 <= 3) $isWin = true;
|
||||
} else {
|
||||
if ($total >= 4 && $total <= 10) $isWin = true;
|
||||
}
|
||||
$payoutMultiplier = $getOdds('xiu');
|
||||
break;
|
||||
|
||||
case 'tai': // 大 (11-17)
|
||||
// 爆子处理:1-3为小,4-6为大
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3) {
|
||||
if ($dice1 >= 4) $isWin = true;
|
||||
} else {
|
||||
if ($total >= 11 && $total <= 17) $isWin = true;
|
||||
}
|
||||
$payoutMultiplier = $getOdds('tai');
|
||||
break;
|
||||
|
||||
case 'chan': // 偶数
|
||||
if ($total % 2 == 0) $isWin = true;
|
||||
$payoutMultiplier = $getOdds('chan');
|
||||
break;
|
||||
|
||||
case 'le': // 奇数
|
||||
if ($total % 2 != 0) $isWin = true;
|
||||
$payoutMultiplier = $getOdds('le');
|
||||
break;
|
||||
|
||||
case 'number': // 单数字 (Sum)
|
||||
if ($total == (int)$betValue) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('sum', $betValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dice': // 单个骰子
|
||||
$count = 0;
|
||||
if ($dice1 == (int)$betValue) $count++;
|
||||
if ($dice2 == (int)$betValue) $count++;
|
||||
if ($dice3 == (int)$betValue) $count++;
|
||||
|
||||
if ($count > 0) {
|
||||
$isWin = true;
|
||||
// 单个骰子赔率通常是 1:1, 1:2, 1:3
|
||||
// 但数据库中只存了基础赔率 (e.g. 0.97 or 1.0)
|
||||
// 这里我们假设数据库存的是 1赔X 的 X
|
||||
// 如果是双骰或三骰,通常规则是:
|
||||
// 1个: 1倍
|
||||
// 2个: 2倍
|
||||
// 3个: 3倍
|
||||
// 我们读取基础赔率,然后乘以数量
|
||||
$baseOdds = $getOdds('dice', $betValue);
|
||||
$payoutMultiplier = $baseOdds * $count;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'combo': // 豹子 (Specific Triple)
|
||||
// betValue 存储的是总和 (3, 6, ..., 18) 或 'any_triple'
|
||||
if ($betValue === 'any_triple') {
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('combo', 'any_triple');
|
||||
}
|
||||
} else {
|
||||
if ($dice1 == $dice2 && $dice2 == $dice3 && $total == (int)$betValue) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('combo', 'specific_triple');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($isWin && $payoutMultiplier > 0) {
|
||||
return [
|
||||
'win' => true,
|
||||
'amount' => $betAmount * $payoutMultiplier
|
||||
];
|
||||
}
|
||||
|
||||
return ['win' => false, 'amount' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算开奖结果(骰子游戏)
|
||||
* @param int $dice1 骰子1点数
|
||||
* @param int $dice2 骰子2点数
|
||||
* @param int $dice3 骰子3点数
|
||||
* @param int $total 总和
|
||||
* @return string 结果:Xỉu/Tài/Bão
|
||||
*/
|
||||
private function calculateResult($dice1, $dice2, $dice3, $total) {
|
||||
// 检查是否为爆子(三个骰子相同)
|
||||
if ($dice1 === $dice2 && $dice2 === $dice3) {
|
||||
// 爆子根据点数判断:1-3为Xỉu,4-6为Tài
|
||||
if ($dice1 <= 3) {
|
||||
return 'Xỉu';
|
||||
} else {
|
||||
return 'Tài';
|
||||
}
|
||||
}
|
||||
|
||||
// 普通大小判断:4-10点为小(Xỉu),11-17点为大(Tài)
|
||||
if ($total >= 4 && $total <= 10) {
|
||||
return 'Xỉu';
|
||||
} else {
|
||||
return 'Tài';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Xóc Đĩa 开奖结果
|
||||
* @param int $redCount 红色硬币数量
|
||||
* @return string 结果描述
|
||||
*/
|
||||
private function calculateXocdiaResult($redCount) {
|
||||
switch ($redCount) {
|
||||
case 0:
|
||||
return '4 Trắng (Chẵn)';
|
||||
case 1:
|
||||
return '3 Trắng 1 Đỏ (Lẻ)';
|
||||
case 2:
|
||||
return '2 Trắng 2 Đỏ (Chẵn)';
|
||||
case 3:
|
||||
return '3 Đỏ 1 Trắng (Lẻ)';
|
||||
case 4:
|
||||
return '4 Đỏ (Chẵn)';
|
||||
default:
|
||||
return 'Không xác định';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
use App\Core\PluginManager;
|
||||
|
||||
class PluginController extends AdminBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
|
||||
}
|
||||
|
||||
public function manage() {
|
||||
$plugins = $this->pluginManager->getPluginStatusList();
|
||||
$icons = $this->pluginManager->getAllPluginIcons();
|
||||
|
||||
foreach ($plugins as &$plugin) {
|
||||
$pluginName = $plugin['name']; // 获取插件名
|
||||
$plugin['icon'] = $icons[$pluginName] ?? 'fa fa-plug';
|
||||
}
|
||||
unset($plugin);
|
||||
|
||||
$this->render('Admin/plugins.php', [
|
||||
'plugins' => $plugins,
|
||||
'title' => '插件管理'
|
||||
]);
|
||||
}
|
||||
|
||||
public function upload() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '插件上传失败']);
|
||||
return;
|
||||
}
|
||||
|
||||
$zipPath = $_FILES['plugin_zip']['tmp_name'];
|
||||
$fileName = basename($_FILES['plugin_zip']['name']);
|
||||
$pluginName = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
|
||||
$pluginDir = __DIR__ . '/../../Plugins/' . $pluginName;
|
||||
if (is_dir($pluginDir)) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => '插件已存在,请先删除或改名']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mkdir($pluginDir, 0777, true) && !is_dir($pluginDir)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '无法创建插件目录']);
|
||||
return;
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive;
|
||||
if ($zip->open($zipPath) === TRUE) {
|
||||
$zip->extractTo($pluginDir);
|
||||
$zip->close();
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => '解压失败']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists($pluginDir . '/mian.php')) {
|
||||
$this->deleteFolder($pluginDir);
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => '无效插件包,缺少 mian.php']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pluginManager->clearCache();
|
||||
$plugins = $this->pluginManager->scanPlugins();
|
||||
if (!isset($plugins[$pluginName])) {
|
||||
$this->deleteFolder($pluginDir);
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' =>$this->pluginManager->getLastError() ?: "插件缺少必要信息,请检查插件是否规范!"
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '插件上传成功']);
|
||||
}
|
||||
|
||||
|
||||
public function toggleStatus($name) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$installedPlugins = $this->pluginManager->getInstalledPlugins();
|
||||
if (!isset($installedPlugins[$name])) {
|
||||
echo json_encode(['success' => false, 'message' => "插件 {$name} 不存在"]);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentStatus = $installedPlugins[$name]['status'];
|
||||
$newStatus = $currentStatus ? 0 : 1;
|
||||
|
||||
$this->db->update('plugins', ['status' => $newStatus], ['name' => $name]);
|
||||
|
||||
$msg = $newStatus ? "插件 {$name} 已启用" : "插件 {$name} 已禁用";
|
||||
echo json_encode(['success' => true, 'message' => $msg]);
|
||||
}
|
||||
|
||||
public function install($name) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$allPlugins = $this->pluginManager->getAllPlugins();
|
||||
if (!isset($allPlugins[$name])) {
|
||||
echo json_encode(['success' => false, 'message' => "插件 {$name} 不存在"]);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->pluginManager->installPlugin($name);
|
||||
if ($result !== true) {
|
||||
echo json_encode(['success' => false, 'message' => $result]);
|
||||
return;
|
||||
}
|
||||
|
||||
$pluginFile = $allPlugins[$name]['path'] . '/mian.php';
|
||||
if (file_exists($pluginFile)) {
|
||||
$pluginInfo = require $pluginFile;
|
||||
if (isset($pluginInfo['activate']) && is_callable($pluginInfo['activate'])) {
|
||||
call_user_func($pluginInfo['activate'], $this->db);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => "插件 {$name} 安装成功"]);
|
||||
}
|
||||
|
||||
public function uninstall($name) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$allPlugins = $this->pluginManager->getAllPlugins();
|
||||
if (!isset($allPlugins[$name])) {
|
||||
echo json_encode(['success' => false, 'message' => "插件 {$name} 不存在"]);
|
||||
return;
|
||||
}
|
||||
|
||||
$pluginFile = $allPlugins[$name]['path'] . '/mian.php';
|
||||
if (file_exists($pluginFile)) {
|
||||
$pluginInfo = require $pluginFile;
|
||||
if (isset($pluginInfo['deactivate']) && is_callable($pluginInfo['deactivate'])) {
|
||||
call_user_func($pluginInfo['deactivate'], $this->db);
|
||||
}
|
||||
}
|
||||
|
||||
$this->pluginManager->uninstallPlugin($name);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => "插件 {$name} 已卸载。"]);
|
||||
}
|
||||
|
||||
|
||||
public function delete($name) {
|
||||
if (!$name) {
|
||||
return json_encode(['success' => false, 'message' => '插件名称不能为空']);
|
||||
}
|
||||
|
||||
$pluginDir = PLUGIN_PATH . $name;
|
||||
|
||||
// 判断插件目录是否存在
|
||||
if (!is_dir($pluginDir)) {
|
||||
return json_encode(['success' => false, 'message' => '插件目录不存在']);
|
||||
}
|
||||
|
||||
// 尝试删除目录
|
||||
if ($this->deleteFolder($pluginDir)) {
|
||||
echo json_encode(['success' => true, 'message' => "插件 {$name} 已卸载并删除"]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => "插件 {$name} 删除失败,请检查权限或是否被占用"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function deleteFolder($dir) {
|
||||
if (!file_exists($dir)) {
|
||||
return true; // 不存在当作删除成功
|
||||
}
|
||||
|
||||
// 如果是文件或符号链接,直接删除
|
||||
if (is_file($dir) || is_link($dir)) {
|
||||
return @unlink($dir);
|
||||
}
|
||||
|
||||
// 扫描目录内容
|
||||
$items = array_diff(scandir($dir), ['.', '..']);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
if (!$this->deleteFolder($path)) {
|
||||
return false; // 子目录删除失败
|
||||
}
|
||||
} else {
|
||||
if (!@unlink($path)) {
|
||||
return false; // 删除文件失败
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除目录本身
|
||||
return @rmdir($dir);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class ReportController extends AdminBaseController {
|
||||
private $db;
|
||||
public function __construct(Database $db) { $this->db = $db; }
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$dateFrom = $_GET['from'] ?? date('Y-m-d', strtotime('-7 days'));
|
||||
$dateTo = $_GET['to'] ?? date('Y-m-d');
|
||||
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
|
||||
|
||||
// 总投注(排除虚拟)
|
||||
$totalBet = $this->db->sum('bets', 'amount', [
|
||||
'is_virtual' => 0, 'created_at[<>]' => $range
|
||||
]) ?: 0;
|
||||
|
||||
// 总派奖
|
||||
$totalWin = $this->db->sum('bets', 'win_amount', [
|
||||
'is_virtual' => 0, 'status' => 'win', 'created_at[<>]' => $range
|
||||
]) ?: 0;
|
||||
|
||||
// 平台利润
|
||||
$profit = $totalBet - $totalWin;
|
||||
|
||||
// 总充值
|
||||
$totalDeposit = $this->db->sum('transactions', 'amount', [
|
||||
'is_virtual' => 0, 'type[~]' => '%deposit%', 'amount[>]' => 0,
|
||||
'created_at[<>]' => $range
|
||||
]) ?: 0;
|
||||
|
||||
// 总提现
|
||||
$totalWithdraw = abs($this->db->sum('transactions', 'amount', [
|
||||
'is_virtual' => 0, 'type[~]' => '%withdraw%', 'amount[<]' => 0,
|
||||
'created_at[<>]' => $range
|
||||
]) ?: 0);
|
||||
|
||||
// 代理佣金
|
||||
$totalCommission = $this->db->sum('agent_commissions', 'commission', [
|
||||
'created_at[<>]' => $range
|
||||
]) ?: 0;
|
||||
|
||||
// 反水
|
||||
$totalRebate = $this->db->sum('agent_commissions', 'commission', [
|
||||
'type' => 'rebate', 'created_at[<>]' => $range
|
||||
]) ?: 0;
|
||||
|
||||
// 用户统计
|
||||
$totalUsers = $this->db->count('users', ['is_virtual' => 0]);
|
||||
$newUsers = $this->db->count('users', [
|
||||
'is_virtual' => 0, 'created_at[<>]' => $range
|
||||
]);
|
||||
|
||||
// 每日明细
|
||||
$dailyStats = [];
|
||||
$start = new \DateTime($dateFrom);
|
||||
$end = new \DateTime($dateTo);
|
||||
$end->modify('+1 day');
|
||||
$interval = new \DateInterval('P1D');
|
||||
$period = new \DatePeriod($start, $interval, $end);
|
||||
foreach ($period as $day) {
|
||||
$d = $day->format('Y-m-d');
|
||||
$dr = [$d . ' 00:00:00', $d . ' 23:59:59'];
|
||||
$dailyStats[] = [
|
||||
'date' => $d,
|
||||
'bets' => $this->db->sum('bets', 'amount', ['is_virtual'=>0,'created_at[<>]'=>$dr]) ?: 0,
|
||||
'wins' => $this->db->sum('bets', 'win_amount', ['is_virtual'=>0,'status'=>'win','created_at[<>]'=>$dr]) ?: 0,
|
||||
'deposits' => $this->db->sum('transactions', 'amount', ['is_virtual'=>0,'type[~]'=>'%deposit%','amount[>]'=>0,'created_at[<>]'=>$dr]) ?: 0,
|
||||
'withdraws' => abs($this->db->sum('transactions', 'amount', ['is_virtual'=>0,'type[~]'=>'%withdraw%','amount[<]'=>0,'created_at[<>]'=>$dr]) ?: 0),
|
||||
];
|
||||
}
|
||||
|
||||
// 代理报表
|
||||
$agentStats = [];
|
||||
$agents = $this->db->select('agents', '*', ['status' => 1]);
|
||||
foreach ($agents as $ag) {
|
||||
$playerIds = $this->db->select('users', 'id', ['agent_id' => $ag['id'], 'is_virtual' => 0]);
|
||||
$agBet = 0; $agWin = 0;
|
||||
if (!empty($playerIds)) {
|
||||
$agBet = $this->db->sum('bets', 'amount', ['user_id' => $playerIds, 'is_virtual'=>0, 'created_at[<>]'=>$range]) ?: 0;
|
||||
$agWin = $this->db->sum('bets', 'win_amount', ['user_id' => $playerIds, 'is_virtual'=>0, 'status'=>'win', 'created_at[<>]'=>$range]) ?: 0;
|
||||
}
|
||||
$agComm = $this->db->sum('agent_commissions', 'commission', ['agent_id'=>$ag['id'],'created_at[<>]'=>$range]) ?: 0;
|
||||
$agentStats[] = [
|
||||
'agent' => $ag,
|
||||
'user' => $this->db->get('users', ['username'], ['id' => $ag['user_id']]),
|
||||
'players' => count($playerIds),
|
||||
'bets' => $agBet, 'wins' => $agWin, 'commission' => $agComm,
|
||||
'profit' => $agBet - $agWin - $agComm,
|
||||
];
|
||||
}
|
||||
|
||||
$this->render('Admin/reports.php', compact(
|
||||
'dateFrom','dateTo','totalBet','totalWin','profit',
|
||||
'totalDeposit','totalWithdraw','totalCommission','totalRebate',
|
||||
'totalUsers','newUsers','dailyStats','agentStats'
|
||||
));
|
||||
}
|
||||
|
||||
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
|
||||
}
|
||||
Executable
+273
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
use \Exception;
|
||||
|
||||
class SettingsController extends AdminBaseController {
|
||||
protected $db;
|
||||
protected $uploadConfig;
|
||||
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->db = new Database();
|
||||
$this->initSettingsTable();
|
||||
|
||||
$this->uploadConfig = [
|
||||
'upload_dir' => dirname(dirname(dirname(__DIR__))) . '/Static/img/',
|
||||
'max_size' => 5 * 1024 * 1024, // 5MB
|
||||
'allowed_mimes' => [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/x-icon', 'image/vnd.microsoft.icon'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统设置表
|
||||
*/
|
||||
private function initSettingsTable() {
|
||||
try {
|
||||
$this->db->query("
|
||||
CREATE TABLE IF NOT EXISTS `system_settings` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`setting_key` varchar(100) NOT NULL,
|
||||
`setting_value` text,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_setting_key` (`setting_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
} catch (Exception $e) {
|
||||
// 表已存在或其他错误,忽略
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统设置页面
|
||||
*/
|
||||
public function index() {
|
||||
$settings = $this->getAllSettings();
|
||||
|
||||
$this->render('Admin/settings.php', [
|
||||
'title' => '系统设置',
|
||||
'settings' => $settings
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有设置
|
||||
*/
|
||||
public function get() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$settings = $this->getAllSettings();
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $settings
|
||||
]);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$this->jsonError('获取设置失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存系统设置
|
||||
*/
|
||||
public function save() {
|
||||
header('Content-Type: application/json');
|
||||
$this->checkAdmin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->jsonError('仅支持POST请求');
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$data) {
|
||||
$data = $_POST;
|
||||
}
|
||||
|
||||
// 处理logo上传(只支持site_logo和site_favicon)
|
||||
$logoTypes = ['site_logo', 'site_favicon'];
|
||||
foreach ($logoTypes as $logoType) {
|
||||
$fileKey = $logoType . '_file';
|
||||
if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
|
||||
$_POST['logo_type'] = $logoType;
|
||||
$logoUrl = $this->handleLogoUpload($_FILES[$fileKey]);
|
||||
$this->saveSetting($logoType, $logoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存其他设置
|
||||
$allowedKeys = [
|
||||
'site_title', 'site_description', 'site_keywords',
|
||||
'site_logo', 'site_favicon', 'site_copyright',
|
||||
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass',
|
||||
'smtp_from', 'smtp_from_name', 'smtp_encryption',
|
||||
];
|
||||
|
||||
foreach ($allowedKeys as $key) {
|
||||
if (isset($data[$key])) {
|
||||
$this->saveSetting($key, $data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除SettingsHelper缓存
|
||||
\App\Core\SettingsHelper::clearCache();
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => '设置保存成功',
|
||||
'data' => $this->getAllSettings()
|
||||
]);
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->jsonError('保存设置失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理logo上传
|
||||
*/
|
||||
private function handleLogoUpload($file) {
|
||||
// 验证文件大小
|
||||
if ($file['size'] > $this->uploadConfig['max_size']) {
|
||||
throw new Exception('文件过大,最大支持5MB');
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, $this->uploadConfig['allowed_mimes'])) {
|
||||
throw new Exception('不支持的文件类型,仅允许: JPG, PNG, GIF, WEBP, ICO');
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
$uploadDir = rtrim($this->uploadConfig['upload_dir'], '/') . '/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
$originalName = basename($file['name']);
|
||||
$originalExt = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$filename = 'logo_' . date('YmdHis') . '_' . uniqid() . '.' . $originalExt;
|
||||
$targetPath = $uploadDir . $filename;
|
||||
|
||||
// 移动文件
|
||||
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||
throw new Exception('文件上传失败');
|
||||
}
|
||||
|
||||
// 返回相对路径(不含域名,兼容任何域名/端口)
|
||||
return '/Static/img/' . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个设置值
|
||||
*/
|
||||
private function getSetting($key, $default = '') {
|
||||
try {
|
||||
$setting = $this->db->get('system_settings', 'setting_value', [
|
||||
'setting_key' => $key
|
||||
]);
|
||||
return $setting !== false ? $setting : $default;
|
||||
} catch (Exception $e) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存单个设置
|
||||
*/
|
||||
private function saveSetting($key, $value) {
|
||||
try {
|
||||
$existing = $this->db->get('system_settings', 'id', [
|
||||
'setting_key' => $key
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
// 更新
|
||||
$this->db->update('system_settings', [
|
||||
'setting_value' => $value
|
||||
], [
|
||||
'setting_key' => $key
|
||||
]);
|
||||
} else {
|
||||
// 插入
|
||||
$this->db->insert('system_settings', [
|
||||
'setting_key' => $key,
|
||||
'setting_value' => $value
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('保存设置失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有设置(以关联数组形式返回)
|
||||
*/
|
||||
private function getAllSettings() {
|
||||
try {
|
||||
$settings = $this->db->select('system_settings', ['setting_key', 'setting_value']);
|
||||
$result = [];
|
||||
|
||||
foreach ($settings as $setting) {
|
||||
$result[$setting['setting_key']] = $setting['setting_value'];
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
$defaults = [
|
||||
'site_title' => 'Tài Xỉu Online - TM68',
|
||||
'site_description' => 'Trang game tài xỉu đổi thưởng, nạp rút nhanh chóng',
|
||||
'site_keywords' => 'tài xỉu, game online, đổi thưởng',
|
||||
'site_logo' => '/Static/tm68/logo_tm68.png.png',
|
||||
'site_favicon' => '/Static/css/favicon.ico',
|
||||
'site_copyright' => '© 2024 TM68. All rights reserved.'
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $default) {
|
||||
if (!isset($result[$key])) {
|
||||
$result[$key] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 测试发信
|
||||
*/
|
||||
public function smtpTest() {
|
||||
header('Content-Type: application/json');
|
||||
$this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$email = trim($data['email'] ?? '');
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->jsonError('Invalid email');
|
||||
}
|
||||
\App\Core\SettingsHelper::clearCache();
|
||||
$result = \App\Core\Mailer::test($email);
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON错误响应
|
||||
*/
|
||||
private function jsonError($message) {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $message
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Executable
+359
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class UserController extends AdminBaseController {
|
||||
/**
|
||||
* 构造函数 - 验证管理员权限
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户列表页面
|
||||
*/
|
||||
public function index() {
|
||||
$this->checkAdmin();
|
||||
$db = new Database();
|
||||
// 只查询普通用户,排除管理员
|
||||
$users = $db->select('users', '*', [
|
||||
'role[!]' => 'admin',
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
$this->render('Admin/user.php', [
|
||||
'users' => $users,
|
||||
'title' => '用户管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个用户数据(用于编辑和详情)
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的用户ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$user = $db->get('users', '*', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
// 移除密码字段,避免泄露
|
||||
unset($user['password']);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $user
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户不存在'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function update() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
$isEditMode = $id > 0;
|
||||
|
||||
$username = trim($data['username'] ?? '');
|
||||
$email = trim($data['email'] ?? '');
|
||||
$role = $data['role'] ?? 'user';
|
||||
$status = isset($data['status']) ? (int)$data['status'] : 0;
|
||||
|
||||
if (empty($username) || empty($email)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户名和邮箱不能为空'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '邮箱格式不正确'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 统一查重(排除当前 ID)
|
||||
$exists = $db->get('users', '*', [
|
||||
'AND' => [
|
||||
'OR' => [
|
||||
'username' => $username,
|
||||
'email' => $email
|
||||
],
|
||||
'id[!]' => $id
|
||||
]
|
||||
]);
|
||||
|
||||
if ($exists) {
|
||||
if ($exists['username'] === $username) {
|
||||
$msg = '用户名已存在';
|
||||
} elseif ($exists['email'] === $email) {
|
||||
$msg = '邮箱已被使用';
|
||||
} else {
|
||||
$msg = '用户名或邮箱已被占用';
|
||||
}
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $msg
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备数据
|
||||
$userData = [
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'role' => $role,
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
if ($isEditMode) {
|
||||
// 如果密码不为空,更新密码
|
||||
if (!empty($data['password'])) {
|
||||
if (strlen($data['password']) < 8) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '密码长度至少8位'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$userData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
$db->update('users', $userData, ['id' => $id]);
|
||||
$message = '用户更新成功';
|
||||
} else {
|
||||
// 新增时必须有密码
|
||||
if (empty($data['password']) || strlen($data['password']) < 8) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '密码长度至少8位'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$userData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
$userData['created_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
$id = $db->insert('users', $userData);
|
||||
$message = '用户创建成功';
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调整用户余额
|
||||
*/
|
||||
public function adjustBalance($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的用户ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$rawData = file_get_contents('php://input');
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '数据格式错误'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$amount = isset($data['amount']) ? floatval($data['amount']) : 0;
|
||||
$action = $data['action'] ?? ''; // 'increase' or 'decrease'
|
||||
|
||||
if ($amount <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '金额必须大于0'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($action, ['increase', 'decrease'])) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的操作类型'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取当前用户信息
|
||||
$user = $db->get('users', ['id', 'balance', 'username'], [
|
||||
'id' => $id,
|
||||
'role[!]' => 'admin' // 只能调整普通用户的余额
|
||||
]);
|
||||
|
||||
if (!$user) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户不存在或无法操作'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentBalance = floatval($user['balance'] ?? 0);
|
||||
$newBalance = $action === 'increase'
|
||||
? $currentBalance + $amount
|
||||
: $currentBalance - $amount;
|
||||
|
||||
// 检查减少后余额不能为负数
|
||||
if ($newBalance < 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '余额不足,当前余额:' . number_format($currentBalance, 2)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
|
||||
// 更新用户余额
|
||||
$db->update('users', [
|
||||
'balance' => $newBalance,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
// 写入资金流水
|
||||
// 修改这里:使用 manual_deposit 和 manual_withdraw 区分人工操作
|
||||
$transactionType = $action === 'increase' ? 'manual_deposit' : 'manual_withdraw';
|
||||
$transactionAmount = $action === 'increase' ? $amount : -$amount;
|
||||
$description = $action === 'increase' ? '管理员手动增加余额' : '管理员手动减少余额';
|
||||
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $id,
|
||||
'type' => $transactionType,
|
||||
'amount' => $transactionAmount,
|
||||
'balance_before' => $currentBalance,
|
||||
'balance_after' => $newBalance,
|
||||
'description' => $description,
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
$db->medoo->pdo->commit();
|
||||
|
||||
$actionText = $action === 'increase' ? '增加' : '减少';
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "余额{$actionText}成功",
|
||||
'data' => [
|
||||
'old_balance' => $currentBalance,
|
||||
'new_balance' => $newBalance,
|
||||
'amount' => $amount
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
public function delete($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的用户ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁止删除自己
|
||||
session_start();
|
||||
if ($id == $_SESSION['user_id']) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '不能删除当前登录用户'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$userExists = $db->get('users', 'id', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if (!$userExists) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '用户不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->delete('users', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '用户已删除'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class VirtualAccountController extends AdminBaseController {
|
||||
private $db;
|
||||
public function __construct(Database $db) { $this->db = $db; }
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$virtuals = $this->db->select('users', '*', ['is_virtual' => 1, 'ORDER' => ['id' => 'DESC']]);
|
||||
$this->render('Admin/virtual_accounts.php', compact('virtuals'));
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$username = trim($data['username'] ?? '');
|
||||
if (empty($username)) { $this->json(['status'=>'error','message'=>'Username required']); return; }
|
||||
if ($this->db->get('users', 'id', ['username' => $username])) {
|
||||
$this->json(['status'=>'error','message'=>'Username exists']); return;
|
||||
}
|
||||
|
||||
$this->db->insert('users', [
|
||||
'username' => $username,
|
||||
'password' => password_hash($data['password'] ?? '123456', PASSWORD_DEFAULT),
|
||||
'email' => $username . '@virtual.local',
|
||||
'role' => 'user', 'status' => 1, 'is_virtual' => 1,
|
||||
'email_verified' => 1,
|
||||
'balance' => floatval($data['balance'] ?? 100000),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
public function adjustBalance() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$amount = floatval($data['amount'] ?? 0);
|
||||
$user = $this->db->get('users', '*', ['id' => $id, 'is_virtual' => 1]);
|
||||
if (!$user) { $this->json(['status'=>'error','message'=>'Not found']); return; }
|
||||
$new = (float)$user['balance'] + $amount;
|
||||
$this->db->update('users', ['balance' => $new], ['id' => $id]);
|
||||
$this->json(['status' => 'success', 'new_balance' => $new]);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$this->db->delete('users', ['id' => (int)$id, 'is_virtual' => 1]);
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class WaterController extends AdminBaseController {
|
||||
private $db;
|
||||
public function __construct(Database $db) { $this->db = $db; }
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$gameId = $this->getGameId();
|
||||
$configs = $this->db->select('water_control', '*', ['game_id' => $gameId]);
|
||||
$limits = $this->db->select('bet_limits', '*', ['game_id' => $gameId]);
|
||||
$this->render('Admin/water_control.php', compact('configs', 'limits', 'gameId'));
|
||||
}
|
||||
|
||||
public function updateWater() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
foreach ($data['items'] ?? [] as $item) {
|
||||
$existing = $this->db->get('water_control', 'id', [
|
||||
'game_id' => (int)$item['game_id'], 'bet_type' => $item['bet_type']
|
||||
]);
|
||||
$fields = [
|
||||
'win_rate_pct' => floatval($item['win_rate_pct']),
|
||||
'enabled' => (int)($item['enabled'] ?? 0),
|
||||
];
|
||||
if ($existing) {
|
||||
$this->db->update('water_control', $fields, ['id' => $existing]);
|
||||
} else {
|
||||
$fields['game_id'] = (int)$item['game_id'];
|
||||
$fields['bet_type'] = $item['bet_type'];
|
||||
$this->db->insert('water_control', $fields);
|
||||
}
|
||||
}
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
public function updateLimits() {
|
||||
$this->checkLogin(); $this->checkAdmin();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
foreach ($data['items'] ?? [] as $item) {
|
||||
$existing = $this->db->get('bet_limits', 'id', [
|
||||
'game_id' => (int)$item['game_id'], 'bet_type' => $item['bet_type']
|
||||
]);
|
||||
$fields = [
|
||||
'min_amount' => floatval($item['min_amount']),
|
||||
'max_amount' => floatval($item['max_amount']),
|
||||
'max_per_period' => floatval($item['max_per_period'] ?? 500000),
|
||||
];
|
||||
if ($existing) {
|
||||
$this->db->update('bet_limits', $fields, ['id' => $existing]);
|
||||
} else {
|
||||
$fields['game_id'] = (int)$item['game_id'];
|
||||
$fields['bet_type'] = $item['bet_type'];
|
||||
$this->db->insert('bet_limits', $fields);
|
||||
}
|
||||
}
|
||||
$this->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
private function getGameId(): int {
|
||||
if (!empty($_GET['game_id'])) return (int)$_GET['game_id'];
|
||||
return (int)($this->db->get('games', 'id', ['code' => 'pk10']) ?: 0);
|
||||
}
|
||||
private function json($d) { header('Content-Type: application/json'); echo json_encode($d); }
|
||||
}
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
<?php
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Core\AdminBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class XocdiaPeriodController extends AdminBaseController {
|
||||
private $gameType = 'xocdia';
|
||||
|
||||
public function __construct() {
|
||||
$this->checkLogin();
|
||||
$this->checkAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Xóc Đĩa 游戏期号列表页面
|
||||
*/
|
||||
public function index() {
|
||||
$db = new Database();
|
||||
try {
|
||||
// 获取 Xóc Đĩa 游戏列表
|
||||
$gamesList = $db->select('games', ['id', 'name'], [
|
||||
'type' => $this->gameType,
|
||||
'status' => 1,
|
||||
'ORDER' => ['id' => 'ASC']
|
||||
]);
|
||||
|
||||
if (!is_array($gamesList)) {
|
||||
$gamesList = [];
|
||||
}
|
||||
|
||||
// 获取所有 Xóc Đĩa 游戏的game_id
|
||||
$gameIds = array_column($gamesList, 'id');
|
||||
|
||||
// 获取 Xóc Đĩa 游戏的期号列表
|
||||
$periods = [];
|
||||
if (!empty($gameIds)) {
|
||||
$periods = $db->select('periods', '*', [
|
||||
'game_id' => $gameIds,
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 100
|
||||
]);
|
||||
}
|
||||
|
||||
if (!is_array($periods)) {
|
||||
$periods = [];
|
||||
}
|
||||
|
||||
// 构建游戏名称映射
|
||||
$games = [];
|
||||
foreach ($gamesList as $game) {
|
||||
$games[$game['id']] = $game['name'];
|
||||
}
|
||||
|
||||
// 为每个 Xóc Đĩa 游戏获取当前期号
|
||||
$currentPeriods = [];
|
||||
foreach ($gamesList as $game) {
|
||||
$currentPeriod = $db->get('periods', '*', [
|
||||
'game_id' => $game['id'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if ($currentPeriod) {
|
||||
$currentPeriods[$game['id']] = $currentPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$periods = [];
|
||||
$games = [];
|
||||
$gamesList = [];
|
||||
$currentPeriods = [];
|
||||
}
|
||||
|
||||
$this->render('Admin/xocdia_periods.php', [
|
||||
'currentPeriods' => $currentPeriods,
|
||||
'periods' => $periods,
|
||||
'games' => $games,
|
||||
'gamesList' => $gamesList,
|
||||
'title' => 'Xóc Đĩa 游戏期号管理'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个期号数据
|
||||
*/
|
||||
public function get($id) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
try {
|
||||
$period = $db->get('periods', '*', [
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
if ($period) {
|
||||
// 验证是否为 Xóc Đĩa 游戏期号
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if ($game && $game['type'] === $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $period
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于 Xóc Đĩa 游戏'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '获取数据失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 录入开奖结果
|
||||
*/
|
||||
public function draw() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '未接收到数据'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否为 Xóc Đĩa 游戏
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于 Xóc Đĩa 游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] === 'settled') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号已结算,无法修改'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$auto = isset($data['auto']) ? (bool)$data['auto'] : false;
|
||||
|
||||
// Xóc Đĩa 开奖逻辑
|
||||
if ($auto) {
|
||||
// 自动生成:4个硬币随机红/白
|
||||
$coins = [];
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$coins[] = (rand(0, 1) === 0) ? 'red' : 'white';
|
||||
}
|
||||
} else {
|
||||
// 手动输入:从前端获取
|
||||
$coins = isset($data['coins']) ? $data['coins'] : [];
|
||||
if (!is_array($coins) || count($coins) !== 4) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Xóc Đĩa 必须提供4个硬币的颜色(red/white)'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
// 验证颜色
|
||||
foreach ($coins as $coin) {
|
||||
if (!in_array($coin, ['red', 'white'])) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '硬币颜色只能是 red 或 white'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算结果
|
||||
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
|
||||
$result = $this->calculateXocdiaResult($redCount);
|
||||
|
||||
// 获取当前登录的管理员ID(审核人)
|
||||
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
|
||||
// 更新期号数据
|
||||
$updateData = [
|
||||
'result' => json_encode($coins),
|
||||
'dice1' => $redCount,
|
||||
'dice2' => 4 - $redCount,
|
||||
'dice3' => null,
|
||||
'total' => null,
|
||||
'status' => 'drawn',
|
||||
'draw_time' => date('Y-m-d H:i:s'),
|
||||
'approved_by' => $approvedBy,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$responseData = [
|
||||
'coins' => $coins,
|
||||
'red_count' => $redCount,
|
||||
'white_count' => 4 - $redCount,
|
||||
'result' => $result
|
||||
];
|
||||
|
||||
try {
|
||||
$db->update('periods', $updateData, ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '开奖结果已录入',
|
||||
'data' => $responseData
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '录入失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动新一期(开始下注)
|
||||
*/
|
||||
public function start() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
|
||||
|
||||
if ($gameId <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请选择游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 验证游戏类型
|
||||
$game = $db->get('games', ['type', 'stream_url'], ['id' => $gameId]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '游戏不属于 Xóc Đĩa 游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查该游戏是否有未结束的期号
|
||||
$activePeriod = $db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'status' => ['pending', 'locked', 'drawn']
|
||||
]);
|
||||
|
||||
if ($activePeriod) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该游戏当前有未结束的期号,无法开始新一轮'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成期号
|
||||
$periodNumber = $this->generatePeriodNumber($db, $gameId);
|
||||
|
||||
// 获取stream_url
|
||||
$streamUrl = !empty($game['stream_url']) ? $game['stream_url'] : null;
|
||||
|
||||
$periodData = [
|
||||
'period_number' => $periodNumber,
|
||||
'status' => 'pending',
|
||||
'game_id' => $gameId,
|
||||
'stream_url' => $streamUrl,
|
||||
'start_time' => date('Y-m-d H:i:s'),
|
||||
'auto_generated' => 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
$id = $db->insert('periods', $periodData);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '新一期已启动,开始下注',
|
||||
'data' => [
|
||||
'id' => $id,
|
||||
'period_number' => $periodNumber,
|
||||
'start_time' => $periodData['start_time']
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '启动失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认开奖并结算
|
||||
*/
|
||||
public function settle() {
|
||||
ob_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否为 Xóc Đĩa 游戏
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于 Xóc Đĩa 游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] !== 'drawn') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号还未开奖,无法结算'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($period['status'] === 'settled') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号已结算'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 开启事务
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
|
||||
// 获取本期所有待结算注单
|
||||
$bets = $db->select('bets', '*', [
|
||||
'period_id' => $id,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
// 遍历注单进行结算
|
||||
if ($bets) {
|
||||
foreach ($bets as $bet) {
|
||||
$checkResult = $this->checkWin($bet, $period);
|
||||
$isWin = $checkResult['win'];
|
||||
$winAmount = $checkResult['amount'];
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($isWin) {
|
||||
$payout = $bet['amount'] + $winAmount;
|
||||
|
||||
// 更新注单状态
|
||||
$db->update('bets', [
|
||||
'status' => 'win',
|
||||
'win_amount' => $winAmount,
|
||||
'settled_at' => $now,
|
||||
'updated_at' => $now
|
||||
], ['id' => $bet['id']]);
|
||||
|
||||
// 更新用户余额
|
||||
$db->update('users', [
|
||||
'balance[+]' => $payout
|
||||
], ['id' => $bet['user_id']]);
|
||||
|
||||
// 获取更新后的余额
|
||||
$user = $db->get('users', ['balance'], ['id' => $bet['user_id']]);
|
||||
$balanceAfter = $user['balance'];
|
||||
$balanceBefore = $balanceAfter - $payout;
|
||||
|
||||
// 写入资金流水
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $bet['user_id'],
|
||||
'type' => 'win',
|
||||
'amount' => $payout,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'related_id' => $bet['id'],
|
||||
'description' => "中奖 - 期号: " . $period['period_number'],
|
||||
'created_at' => $now
|
||||
]);
|
||||
|
||||
} else {
|
||||
// 未中奖
|
||||
$db->update('bets', [
|
||||
'status' => 'lose',
|
||||
'win_amount' => 0,
|
||||
'settled_at' => $now,
|
||||
'updated_at' => $now
|
||||
], ['id' => $bet['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新期号状态为已结算
|
||||
$db->update('periods', [
|
||||
'status' => 'settled',
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
// 提交事务
|
||||
$db->medoo->pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '结算成功,请点击"开始下注"启动下一期',
|
||||
'data' => [
|
||||
'current_period_id' => $id
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
if (isset($db->medoo->pdo)) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '结算失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 封盘
|
||||
*/
|
||||
public function lock() {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = $_POST;
|
||||
if (empty($data)) {
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
}
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的期号ID'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取期号信息
|
||||
$period = $db->get('periods', '*', ['id' => $id]);
|
||||
if (!$period) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不存在'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否为 Xóc Đĩa 游戏
|
||||
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
|
||||
if (!$game || $game['type'] !== $this->gameType) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '期号不属于 Xóc Đĩa 游戏'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查期号状态
|
||||
if ($period['status'] !== 'pending') {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '该期号状态不允许封盘'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->update('periods', [
|
||||
'status' => 'locked',
|
||||
'end_time' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
], ['id' => $id]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '封盘成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '封盘失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成期号
|
||||
*/
|
||||
private function generatePeriodNumber($db, $gameId) {
|
||||
$now = new \DateTime();
|
||||
$dateStr = $now->format('Ymd');
|
||||
$prefix = "G{$gameId}{$dateStr}";
|
||||
|
||||
try {
|
||||
$lastPeriod = $db->get('periods', 'period_number', [
|
||||
'period_number[~]' => $prefix . '%',
|
||||
'ORDER' => ['period_number' => 'DESC']
|
||||
]);
|
||||
|
||||
if ($lastPeriod) {
|
||||
$lastSeq = substr($lastPeriod, strlen($prefix));
|
||||
$newSeq = (int)$lastSeq + 1;
|
||||
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$seqStr = '0001';
|
||||
}
|
||||
|
||||
return $prefix . $seqStr;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $prefix . time();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查注单是否中奖
|
||||
*/
|
||||
private function checkWin($bet, $period) {
|
||||
$betType = $bet['bet_type'];
|
||||
$betValue = $bet['bet_value'];
|
||||
$betAmount = (float)$bet['amount'];
|
||||
$gameId = $period['game_id'];
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 获取赔率配置
|
||||
static $oddsCache = [];
|
||||
if (!isset($oddsCache[$gameId])) {
|
||||
$oddsData = $db->select('game_odds', '*', ['game_id' => $gameId]);
|
||||
$oddsCache[$gameId] = [];
|
||||
foreach ($oddsData as $odd) {
|
||||
$key = $odd['type'] . '_' . $odd['target'];
|
||||
$oddsCache[$gameId][$key] = (float)$odd['odds'];
|
||||
}
|
||||
}
|
||||
|
||||
$isWin = false;
|
||||
$payoutMultiplier = 0;
|
||||
|
||||
// 获取赔率
|
||||
$getOdds = function($type, $target = 'all') use ($oddsCache, $gameId) {
|
||||
$key = $type . '_' . $target;
|
||||
if (isset($oddsCache[$gameId][$key])) {
|
||||
return $oddsCache[$gameId][$key];
|
||||
}
|
||||
$allKey = $type . '_all';
|
||||
return $oddsCache[$gameId][$allKey] ?? 0;
|
||||
};
|
||||
|
||||
// Xóc Đĩa 游戏结算逻辑
|
||||
$coins = json_decode($period['result'], true);
|
||||
if (!is_array($coins) || count($coins) !== 4) {
|
||||
return ['win' => false, 'amount' => 0];
|
||||
}
|
||||
|
||||
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
|
||||
$whiteCount = 4 - $redCount;
|
||||
|
||||
switch ($betType) {
|
||||
case 'chan': // 双(偶): 4红 或 4白 或 2红2白
|
||||
if (in_array($redCount, [0, 2, 4])) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('chan', $betValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'le': // 单(奇): 3红1白 或 3白1红
|
||||
if (in_array($redCount, [1, 3])) {
|
||||
$isWin = true;
|
||||
$payoutMultiplier = $getOdds('le', $betValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'exact': // 精确颜色组合
|
||||
switch ($betValue) {
|
||||
case '4red':
|
||||
if ($redCount === 4) $isWin = true;
|
||||
break;
|
||||
case '4white':
|
||||
if ($redCount === 0) $isWin = true;
|
||||
break;
|
||||
case '3red1white':
|
||||
if ($redCount === 3) $isWin = true;
|
||||
break;
|
||||
case '3white1red':
|
||||
if ($redCount === 1) $isWin = true;
|
||||
break;
|
||||
}
|
||||
if ($isWin) {
|
||||
$payoutMultiplier = $getOdds('exact', $betValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($isWin && $payoutMultiplier > 0) {
|
||||
return [
|
||||
'win' => true,
|
||||
'amount' => $betAmount * $payoutMultiplier
|
||||
];
|
||||
}
|
||||
|
||||
return ['win' => false, 'amount' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Xóc Đĩa 开奖结果
|
||||
*/
|
||||
private function calculateXocdiaResult($redCount) {
|
||||
switch ($redCount) {
|
||||
case 0:
|
||||
return '4 Trắng (Chẵn)';
|
||||
case 1:
|
||||
return '3 Trắng 1 Đỏ (Lẻ)';
|
||||
case 2:
|
||||
return '2 Trắng 2 Đỏ (Chẵn)';
|
||||
case 3:
|
||||
return '3 Đỏ 1 Trắng (Lẻ)';
|
||||
case 4:
|
||||
return '4 Đỏ (Chẵn)';
|
||||
default:
|
||||
return 'Không xác định';
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
class ApiController {
|
||||
public function data() {
|
||||
$db = Database::get();
|
||||
$data = $db->select('sample_table', '*');
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace App\Controllers\Web;
|
||||
|
||||
use App\Core\WebBaseController;
|
||||
use App\Core\I18n;
|
||||
use Db\Database;
|
||||
|
||||
class AgentController extends WebBaseController {
|
||||
|
||||
private function getAgent() {
|
||||
$db = new Database();
|
||||
$agent = $db->get('agents', '*', ['user_id' => $this->getCurrentUserId(), 'status' => 1]);
|
||||
if (!$agent) { header('Location: /'); exit; }
|
||||
return $agent;
|
||||
}
|
||||
|
||||
// 代理后台主页
|
||||
public function dashboard() {
|
||||
$this->checkWebLogin();
|
||||
$agent = $this->getAgent();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
|
||||
|
||||
$players = $db->select('users', ['id','username','balance','created_at','status'], [
|
||||
'agent_id' => $agent['id'], 'is_virtual' => 0, 'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$todayRange = [$today . ' 00:00:00', $today . ' 23:59:59'];
|
||||
$todayComm = $db->sum('agent_commissions', 'commission', [
|
||||
'agent_id' => $agent['id'], 'created_at[<>]' => $todayRange
|
||||
]) ?: 0;
|
||||
$totalComm = $db->sum('agent_commissions', 'commission', ['agent_id' => $agent['id']]) ?: 0;
|
||||
|
||||
// 玩家总投注额
|
||||
$totalBets = $db->sum('bets', 'amount', ['agent_id' => $agent['id'], 'is_virtual' => 0]) ?: 0;
|
||||
|
||||
// 下级代理
|
||||
$subAgents = [];
|
||||
if ($agent['level'] == 1) {
|
||||
$subAgents = $db->select('agents', '*', ['parent_id' => $agent['id']]);
|
||||
foreach ($subAgents as &$sa) {
|
||||
$sa['user'] = $db->get('users', ['username','balance'], ['id' => $sa['user_id']]);
|
||||
$sa['player_count'] = $db->count('users', ['agent_id' => $sa['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
$page = 'home';
|
||||
extract(compact('agent','user','players','todayComm','totalComm','totalBets','subAgents','page'));
|
||||
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
|
||||
}
|
||||
|
||||
// 赔率管理页
|
||||
public function odds() {
|
||||
$this->checkWebLogin();
|
||||
$agent = $this->getAgent();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
|
||||
$game = $db->get('games', '*', ['code' => 'pk10']);
|
||||
|
||||
// 系统赔率
|
||||
$sysOdds = $db->select('game_odds', '*', ['game_id' => $game['id']]);
|
||||
$sysMap = [];
|
||||
foreach ($sysOdds as $s) $sysMap[$s['type'].'_'.$s['target']] = (float)$s['odds'];
|
||||
|
||||
// 上级赔率
|
||||
$parentMap = $sysMap;
|
||||
if ($agent['parent_id']) {
|
||||
$po = $db->select('agent_odds', '*', ['agent_id' => $agent['parent_id'], 'game_id' => $game['id']]);
|
||||
foreach ($po as $p) $parentMap[$p['bet_type'].'_'.$p['bet_target']] = (float)$p['odds'];
|
||||
}
|
||||
|
||||
// 当前代理赔率
|
||||
$myOdds = $db->select('agent_odds', '*', ['agent_id' => $agent['id'], 'game_id' => $game['id']]);
|
||||
$myMap = [];
|
||||
foreach ($myOdds as $m) $myMap[$m['bet_type'].'_'.$m['bet_target']] = (float)$m['odds'];
|
||||
|
||||
$page = 'odds';
|
||||
extract(compact('agent','user','game','sysMap','parentMap','myMap','page'));
|
||||
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
|
||||
}
|
||||
|
||||
// 保存赔率
|
||||
public function setOdds() {
|
||||
$this->checkWebLogin();
|
||||
$agent = $this->getAgent();
|
||||
header('Content-Type: application/json');
|
||||
$db = new Database();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$gameId = (int)($data['game_id'] ?? 0);
|
||||
|
||||
// 上级赔率限制
|
||||
$parentOdds = [];
|
||||
if ($agent['parent_id']) {
|
||||
$po = $db->select('agent_odds', '*', ['agent_id' => $agent['parent_id'], 'game_id' => $gameId]);
|
||||
foreach ($po as $p) $parentOdds[$p['bet_type'].'_'.$p['bet_target']] = (float)$p['odds'];
|
||||
}
|
||||
if (empty($parentOdds)) {
|
||||
$so = $db->select('game_odds', '*', ['game_id' => $gameId]);
|
||||
foreach ($so as $s) $parentOdds[$s['type'].'_'.$s['target']] = (float)$s['odds'];
|
||||
}
|
||||
|
||||
foreach ($data['odds'] ?? [] as $o) {
|
||||
$key = $o['bet_type'].'_'.$o['bet_target'];
|
||||
$newOdds = floatval($o['odds']);
|
||||
if (isset($parentOdds[$key]) && $newOdds > $parentOdds[$key]) {
|
||||
echo json_encode(['success'=>false,'message'=>"赔率不能超过上级: $key 上限 ".$parentOdds[$key]]);
|
||||
return;
|
||||
}
|
||||
$existing = $db->get('agent_odds', 'id', [
|
||||
'agent_id'=>$agent['id'],'game_id'=>$gameId,'bet_type'=>$o['bet_type'],'bet_target'=>$o['bet_target']
|
||||
]);
|
||||
if ($existing) {
|
||||
$db->update('agent_odds', ['odds'=>$newOdds], ['id'=>$existing]);
|
||||
} else {
|
||||
$db->insert('agent_odds', [
|
||||
'agent_id'=>$agent['id'],'game_id'=>$gameId,
|
||||
'bet_type'=>$o['bet_type'],'bet_target'=>$o['bet_target'],'odds'=>$newOdds
|
||||
]);
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true, 'message' => '赔率已更新']);
|
||||
}
|
||||
|
||||
// 玩家投注记录
|
||||
public function bets() {
|
||||
$this->checkWebLogin();
|
||||
$agent = $this->getAgent();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
|
||||
|
||||
$betRecords = $db->select('bets', '*', [
|
||||
'agent_id' => $agent['id'], 'is_virtual' => 0,
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 200
|
||||
]);
|
||||
foreach ($betRecords as &$b) {
|
||||
$b['username'] = $db->get('users', 'username', ['id' => $b['user_id']]) ?: '?';
|
||||
}
|
||||
|
||||
$page = 'bets';
|
||||
extract(compact('agent','user','betRecords','page'));
|
||||
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
|
||||
}
|
||||
|
||||
// 佣金明细
|
||||
public function commissions() {
|
||||
$this->checkWebLogin();
|
||||
$agent = $this->getAgent();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
|
||||
|
||||
$records = $db->select('agent_commissions', '*', [
|
||||
'agent_id' => $agent['id'], 'ORDER' => ['id' => 'DESC'], 'LIMIT' => 200
|
||||
]);
|
||||
foreach ($records as &$r) {
|
||||
$r['username'] = $db->get('users', 'username', ['id' => $r['from_user_id']]) ?: '?';
|
||||
}
|
||||
|
||||
$page = 'commissions';
|
||||
extract(compact('agent','user','records','page'));
|
||||
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
|
||||
}
|
||||
|
||||
// 佣金API(JSON)
|
||||
public function commissionsApi() {
|
||||
$this->checkWebLogin();
|
||||
$agent = $this->getAgent();
|
||||
header('Content-Type: application/json');
|
||||
$db = new Database();
|
||||
$records = $db->select('agent_commissions', '*', [
|
||||
'agent_id' => $agent['id'], 'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
|
||||
]);
|
||||
echo json_encode(['success' => true, 'data' => $records]);
|
||||
}
|
||||
}
|
||||
Executable
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
namespace App\Controllers\Web;
|
||||
|
||||
use Db\Database;
|
||||
use App\Core\WebBaseController;
|
||||
use App\Core\I18n;
|
||||
|
||||
class AuthController extends WebBaseController {
|
||||
|
||||
public function loginPage() {
|
||||
if ($this->isLoggedIn()) { header('Location: /'); exit; }
|
||||
I18n::init();
|
||||
include __DIR__ . '/../../Views/Web/login.php';
|
||||
}
|
||||
|
||||
public function loginSubmit() {
|
||||
header('Content-Type: application/json');
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = $data['password'] ?? '';
|
||||
$remember = $data['remember'] ?? false;
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Please fill in all fields']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
$user = $db->get('users', '*', ['username' => $username]);
|
||||
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid username or password']);
|
||||
return;
|
||||
}
|
||||
if (isset($user['status']) && $user['status'] == 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Account disabled']);
|
||||
return;
|
||||
}
|
||||
// 邮箱验证检查(虚拟账户跳过)
|
||||
if (empty($user['is_virtual']) && isset($user['email_verified']) && !$user['email_verified']) {
|
||||
echo json_encode(['success' => false, 'message' => 'Please verify your email first', 'need_verify' => true]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
$_SESSION['web_user_id'] = $user['id'];
|
||||
$_SESSION['web_username'] = $user['username'];
|
||||
$_SESSION['web_role'] = $user['role'] ?? 'user';
|
||||
$_SESSION['web_last_activity'] = time();
|
||||
$_SESSION['web_ip'] = $_SERVER['REMOTE_ADDR'];
|
||||
$_SESSION['web_ua'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
if ($remember) $_SESSION['web_remember'] = true;
|
||||
if (!empty($user['lang'])) $_SESSION['lang'] = $user['lang'];
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Login successful', 'redirect' => '/']);
|
||||
}
|
||||
|
||||
public function registerSubmit() {
|
||||
header('Content-Type: application/json');
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = $data['password'] ?? '';
|
||||
$confirmPassword = $data['confirm_password'] ?? '';
|
||||
$email = trim($data['email'] ?? '');
|
||||
$inviteCode = trim($data['invite_code'] ?? '');
|
||||
$verifyCode = trim($data['verify_code'] ?? '');
|
||||
|
||||
if (empty($username) || empty($password) || empty($email)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Please fill in all required fields']);
|
||||
return;
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Username: 3-20 chars, letters/numbers/underscore only']);
|
||||
return;
|
||||
}
|
||||
if (strlen($password) < 6) {
|
||||
echo json_encode(['success' => false, 'message' => 'Password must be at least 6 characters']);
|
||||
return;
|
||||
}
|
||||
if ($password !== $confirmPassword) {
|
||||
echo json_encode(['success' => false, 'message' => 'Passwords do not match']);
|
||||
return;
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid email address']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = new Database();
|
||||
|
||||
// 唯一性检查
|
||||
if ($db->get('users', 'id', ['username' => $username])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Username already exists']);
|
||||
return;
|
||||
}
|
||||
if ($db->get('users', 'id', ['email' => $email])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Email already registered']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
$ev = $db->get('email_verifications', '*', [
|
||||
'email' => $email, 'code' => $verifyCode, 'used' => 0,
|
||||
'expires_at[>]' => date('Y-m-d H:i:s'),
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if (!$ev) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid or expired verification code']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理邀请码 → 代理关联
|
||||
$agentId = null;
|
||||
if (!empty($inviteCode)) {
|
||||
$agent = $db->get('agents', '*', ['agent_code' => $inviteCode, 'status' => 1]);
|
||||
if ($agent) $agentId = $agent['id'];
|
||||
}
|
||||
|
||||
$db->insert('users', [
|
||||
'username' => $username,
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'email' => $email,
|
||||
'role' => 'user',
|
||||
'status' => 1,
|
||||
'email_verified' => 1,
|
||||
'agent_id' => $agentId,
|
||||
'lang' => 'en',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
// 标记验证码已用
|
||||
$db->update('email_verifications', ['used' => 1], ['id' => $ev['id']]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Registration successful', 'redirect' => '/login']);
|
||||
}
|
||||
|
||||
public function sendVerifyCode() {
|
||||
header('Content-Type: application/json');
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$email = trim($data['email'] ?? '');
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid email']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 频率限制: 60秒内只能发一次
|
||||
$db = new Database();
|
||||
$recent = $db->get('email_verifications', '*', [
|
||||
'email' => $email,
|
||||
'created_at[>]' => date('Y-m-d H:i:s', time() - 60),
|
||||
]);
|
||||
if ($recent) {
|
||||
echo json_encode(['success' => false, 'message' => 'Please wait 60 seconds']);
|
||||
return;
|
||||
}
|
||||
|
||||
$code = str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$db->insert('email_verifications', [
|
||||
'email' => $email,
|
||||
'code' => $code,
|
||||
'expires_at' => date('Y-m-d H:i:s', time() + 600), // 10分钟
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
// 发送邮件
|
||||
$subject = 'Email Verification Code';
|
||||
$body = "Your verification code is: <b>$code</b><br>Valid for 10 minutes.";
|
||||
$sent = \App\Core\Mailer::send($email, $subject, $body);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Verification code sent']);
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
$keys = ['web_user_id','web_username','web_role','web_last_activity','web_ip','web_ua','web_remember','lang'];
|
||||
foreach ($keys as $k) unset($_SESSION[$k]);
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
private function isLoggedIn(): bool {
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
return isset($_SESSION['web_user_id']);
|
||||
}
|
||||
}
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
namespace App\Controllers\Web;
|
||||
|
||||
use App\Core\WebBaseController;
|
||||
use Db\Database;
|
||||
|
||||
class BetController extends WebBaseController {
|
||||
|
||||
public function placeBet() {
|
||||
$this->checkWebLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$userId = $this->getCurrentUserId();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($data['bets']) || empty($data['period_number']) || empty($data['game_id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Missing required fields']);
|
||||
return;
|
||||
}
|
||||
|
||||
$gameId = (int)$data['game_id'];
|
||||
$periodNumber = $data['period_number'];
|
||||
$bets = $data['bets'];
|
||||
$db = new Database();
|
||||
|
||||
// 验证期号
|
||||
$period = $db->get('periods', '*', ['period_number' => $periodNumber, 'game_id' => $gameId]);
|
||||
if (!$period || $period['status'] !== 'pending') {
|
||||
echo json_encode(['success' => false, 'message' => 'Period is not open for betting']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 赔率映射
|
||||
$oddsList = $db->select('game_odds', '*', ['game_id' => $gameId]);
|
||||
$oddsMap = [];
|
||||
foreach ($oddsList as $o) {
|
||||
$oddsMap[$o['type'] . '_' . $o['target']] = (float)$o['odds'];
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
$user = $db->get('users', '*', ['id' => $userId]);
|
||||
$isVirtual = (int)($user['is_virtual'] ?? 0);
|
||||
|
||||
// 代理专属赔率覆盖
|
||||
if (!empty($user['agent_id'])) {
|
||||
$agentOdds = $db->select('agent_odds', '*', ['agent_id' => $user['agent_id'], 'game_id' => $gameId]);
|
||||
foreach ($agentOdds as $ao) {
|
||||
$oddsMap[$ao['bet_type'] . '_' . $ao['bet_target']] = (float)$ao['odds'];
|
||||
}
|
||||
}
|
||||
|
||||
// 限额
|
||||
$limitsRaw = $db->select('bet_limits', '*', ['game_id' => $gameId]);
|
||||
$limits = [];
|
||||
foreach ($limitsRaw as $l) { $limits[$l['bet_type']] = $l; }
|
||||
|
||||
// 验证投注
|
||||
$totalAmount = 0;
|
||||
$validBets = [];
|
||||
foreach ($bets as $bet) {
|
||||
$type = $bet['type'] ?? '';
|
||||
$target = $bet['value'] ?? $bet['target'] ?? '';
|
||||
$amount = floatval($bet['amount'] ?? 0);
|
||||
if ($amount <= 0) continue;
|
||||
|
||||
$key = $type . '_' . $target;
|
||||
$odds = $oddsMap[$key] ?? null;
|
||||
if ($odds === null) {
|
||||
echo json_encode(['success' => false, 'message' => "Invalid bet: $key"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 限额检查
|
||||
if (isset($limits[$type])) {
|
||||
if ($amount < (float)$limits[$type]['min_amount']) {
|
||||
echo json_encode(['success' => false, 'message' => "Min bet for $type: " . $limits[$type]['min_amount']]);
|
||||
return;
|
||||
}
|
||||
if ($amount > (float)$limits[$type]['max_amount']) {
|
||||
echo json_encode(['success' => false, 'message' => "Max bet for $type: " . $limits[$type]['max_amount']]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$totalAmount += $amount;
|
||||
$validBets[] = ['type' => $type, 'target' => $target, 'amount' => $amount, 'odds' => $odds];
|
||||
}
|
||||
|
||||
if (empty($validBets)) {
|
||||
echo json_encode(['success' => false, 'message' => 'No valid bets']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 事务
|
||||
try {
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
|
||||
$stmt = $db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
|
||||
$stmt->execute([':id' => $userId]);
|
||||
$userRow = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$currentBalance = (float)$userRow['balance'];
|
||||
|
||||
if ($currentBalance < $totalAmount) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Insufficient balance']);
|
||||
return;
|
||||
}
|
||||
|
||||
$newBalance = $currentBalance - $totalAmount;
|
||||
$db->update('users', ['balance' => $newBalance], ['id' => $userId]);
|
||||
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $userId, 'type' => 'bet', 'amount' => -$totalAmount,
|
||||
'balance_before' => $currentBalance, 'balance_after' => $newBalance,
|
||||
'related_id' => $period['id'], 'description' => ($db->get('games', 'name', ['id' => $gameId]) ?: 'Game') . ' Bet - ' . $periodNumber,
|
||||
'is_virtual' => $isVirtual, 'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
foreach ($validBets as $vb) {
|
||||
$db->insert('bets', [
|
||||
'user_id' => $userId, 'game_id' => $gameId,
|
||||
'period_number' => $periodNumber, 'period_id' => $period['id'],
|
||||
'bet_type' => $vb['type'], 'bet_value' => $vb['target'],
|
||||
'amount' => $vb['amount'], 'odds' => $vb['odds'],
|
||||
'status' => 'pending', 'is_virtual' => $isVirtual,
|
||||
'agent_id' => $user['agent_id'] ?? null,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
$db->medoo->pdo->commit();
|
||||
echo json_encode(['success' => true, 'message' => 'Bet placed successfully', 'new_balance' => $newBalance]);
|
||||
} catch (\Throwable $e) {
|
||||
if ($db->medoo->pdo->inTransaction()) $db->medoo->pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'System error']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace App\Controllers\Web;
|
||||
|
||||
use App\Core\WebBaseController;
|
||||
use App\Core\GameFactory;
|
||||
use App\Core\I18n;
|
||||
use Db\Database;
|
||||
|
||||
class GameController extends WebBaseController {
|
||||
|
||||
public function play($code) {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
|
||||
$game = $db->get('games', '*', ['code' => $code, 'status' => 1]);
|
||||
if (!$game) { header('Location: /'); return; }
|
||||
$isAgent = !!$db->get('agents', 'id', ['user_id' => $userId, 'status' => 1]);
|
||||
extract([
|
||||
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
|
||||
'game' => $game, 'isAgent' => $isAgent,
|
||||
]);
|
||||
$viewFile = __DIR__ . "/../../Views/Web/{$code}.php";
|
||||
if (!file_exists($viewFile)) $viewFile = __DIR__ . "/../../Views/Web/game_generic.php";
|
||||
include $viewFile;
|
||||
}
|
||||
|
||||
public function stats($code) {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$game = $db->get('games', '*', ['code' => $code, 'status' => 1]);
|
||||
if (!$game) { header('Location: /'); return; }
|
||||
$gameType = $game['type'] ?? $code;
|
||||
$algoClass = GameFactory::getAlgorithm($gameType);
|
||||
$resultTable = $algoClass::getResultTable();
|
||||
$gameId = $game['id'];
|
||||
|
||||
$periods = $db->select('periods', ['id','period_number'], [
|
||||
'game_id' => $gameId, 'status' => 'settled',
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
|
||||
]);
|
||||
$results = [];
|
||||
foreach ($periods as $p) {
|
||||
if ($resultTable) {
|
||||
$row = $db->get($resultTable, '*', ['period_id' => $p['id']]);
|
||||
} else {
|
||||
$row = $db->get('periods', '*', ['id' => $p['id']]);
|
||||
}
|
||||
if ($row) { $row['period_number'] = $p['period_number']; $results[] = $row; }
|
||||
}
|
||||
extract(compact('game', 'results'));
|
||||
$viewFile = __DIR__ . "/../../Views/Web/{$code}_stats.php";
|
||||
if (!file_exists($viewFile)) $viewFile = __DIR__ . "/../../Views/Web/game_stats_generic.php";
|
||||
include $viewFile;
|
||||
}
|
||||
}
|
||||
Executable
+170
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
namespace App\Controllers\Web;
|
||||
|
||||
use App\Core\WebBaseController;
|
||||
use App\Core\I18n;
|
||||
use Db\Database;
|
||||
|
||||
class HomeController extends WebBaseController {
|
||||
|
||||
public function index() {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
|
||||
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
|
||||
extract([
|
||||
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
|
||||
'game' => $game,
|
||||
]);
|
||||
include __DIR__ . '/../../Views/Web/home.php';
|
||||
}
|
||||
|
||||
public function pk10Game() {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
|
||||
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
|
||||
$isAgent = !!$db->get('agents', 'id', ['user_id' => $userId, 'status' => 1]);
|
||||
extract([
|
||||
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
|
||||
'game' => $game,
|
||||
'isAgent' => $isAgent,
|
||||
]);
|
||||
include __DIR__ . '/../../Views/Web/pk10.php';
|
||||
}
|
||||
|
||||
public function profile() {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$user = $db->get('users', '*', ['id' => $userId]);
|
||||
|
||||
// 最近交易
|
||||
$transactions = $db->select('transactions', '*', [
|
||||
'user_id' => $userId, 'is_virtual' => 0,
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 20
|
||||
]);
|
||||
|
||||
// 最近投注
|
||||
$bets = $db->select('bets', '*', [
|
||||
'user_id' => $userId,
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 20
|
||||
]);
|
||||
|
||||
extract(compact('user', 'transactions', 'bets'));
|
||||
include __DIR__ . '/../../Views/Web/profile.php';
|
||||
}
|
||||
|
||||
public function bindUsdt() {
|
||||
$this->checkWebLogin();
|
||||
header('Content-Type: application/json');
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$address = trim($data['usdt_address'] ?? '');
|
||||
|
||||
// TRC20地址验证: 以T开头, 34字符, base58
|
||||
if (!preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $address)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid TRC20 address']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 唯一性
|
||||
$exists = $db->get('users', 'id', ['usdt_address' => $address, 'id[!]' => $userId]);
|
||||
if ($exists) {
|
||||
echo json_encode(['success' => false, 'message' => 'Address already bound to another account']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db->update('users', ['usdt_address' => $address, 'usdt_chain' => 'TRC20'], ['id' => $userId]);
|
||||
echo json_encode(['success' => true, 'message' => 'Address bound successfully']);
|
||||
}
|
||||
|
||||
public function setLang() {
|
||||
header('Content-Type: application/json');
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$lang = $data['lang'] ?? 'en';
|
||||
if (isset(I18n::LANGUAGES[$lang])) {
|
||||
$_SESSION['lang'] = $lang;
|
||||
setcookie('lang', $lang, time() + 86400 * 365, '/');
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
if ($userId) $db->update('users', ['lang' => $lang], ['id' => $userId]);
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid language']);
|
||||
}
|
||||
}
|
||||
|
||||
public function lottery() {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$gameCode = $_GET['game'] ?? 'pk10';
|
||||
$game = $db->get('games', '*', ['code' => $gameCode, 'status' => 1]);
|
||||
if (!$game) $game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
|
||||
$gameId = $game['id'] ?? 0;
|
||||
$gameType = $game['type'] ?? 'pk10';
|
||||
$algoClass = \App\Core\GameFactory::getAlgorithm($gameType);
|
||||
$resultTable = $algoClass::getResultTable();
|
||||
|
||||
$periods = $db->select('periods', '*', [
|
||||
'game_id' => $gameId, 'status' => 'settled',
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
|
||||
]);
|
||||
foreach ($periods as &$p) {
|
||||
if ($resultTable) {
|
||||
$pk = $db->get($resultTable, '*', ['period_id' => $p['id']]);
|
||||
if ($pk) { foreach ($pk as $k => $v) $p[$k] = $v; }
|
||||
}
|
||||
}
|
||||
unset($p);
|
||||
extract(compact('game', 'periods'));
|
||||
include __DIR__ . '/../../Views/Web/lottery.php';
|
||||
}
|
||||
|
||||
public function pk10Stats() {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
|
||||
$gameId = $game['id'] ?? 0;
|
||||
// 最近100期已结算结果
|
||||
$periods = $db->select('periods', ['id','period_number'], [
|
||||
'game_id' => $gameId, 'status' => 'settled',
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
|
||||
]);
|
||||
$results = [];
|
||||
foreach ($periods as $p) {
|
||||
$pk = $db->get('pk10_results', '*', ['period_id' => $p['id']]);
|
||||
if ($pk) {
|
||||
$pk['period_number'] = $p['period_number'];
|
||||
$results[] = $pk;
|
||||
}
|
||||
}
|
||||
extract(compact('game', 'results'));
|
||||
include __DIR__ . '/../../Views/Web/pk10_stats.php';
|
||||
}
|
||||
|
||||
public function details() {
|
||||
$this->checkWebLogin();
|
||||
I18n::init();
|
||||
$db = new Database();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$settled = $db->select('bets', '*', [
|
||||
'user_id' => $userId, 'status[!]' => 'pending',
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
|
||||
]);
|
||||
$pending = $db->select('bets', '*', [
|
||||
'user_id' => $userId, 'status' => 'pending',
|
||||
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
|
||||
]);
|
||||
extract(compact('settled', 'pending'));
|
||||
include __DIR__ . '/../../Views/Web/details.php';
|
||||
}
|
||||
}
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
namespace App\Controllers\Web;
|
||||
|
||||
use App\Core\WebBaseController;
|
||||
use App\Core\GameFactory;
|
||||
use Db\Database;
|
||||
|
||||
class PeriodController extends WebBaseController {
|
||||
|
||||
public function getCurrent() {
|
||||
header('Content-Type: application/json');
|
||||
$db = new Database();
|
||||
$gameId = (int)($_GET['game_id'] ?? 0);
|
||||
if (!$gameId) {
|
||||
$gameId = (int)($db->get('games', 'id', ['code' => 'pk10']) ?: 0);
|
||||
}
|
||||
if (!$gameId) {
|
||||
echo json_encode(['success' => false, 'message' => 'Game not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取游戏类型和算法
|
||||
$game = $db->get('games', ['id', 'type', 'period_duration', 'lock_before_end'], ['id' => $gameId]);
|
||||
$gameType = $game['type'] ?? 'pk10';
|
||||
$algoClass = GameFactory::getAlgorithm($gameType);
|
||||
$resultTable = $algoClass::getResultTable();
|
||||
|
||||
$period = $db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
|
||||
$now = time();
|
||||
$countdown = 0;
|
||||
$lockCountdown = 0;
|
||||
if ($period && in_array($period['status'], ['pending', 'locked'])) {
|
||||
$start = strtotime($period['start_time'] ?? $period['created_at']);
|
||||
$countdown = max(0, !empty($period['end_time']) ? strtotime($period['end_time']) - $now : 300 - ($now - $start));
|
||||
$periodDuration = (int)($game['period_duration'] ?? 300);
|
||||
$lockBefore = (int)($game['lock_before_end'] ?? 30);
|
||||
$lockCountdown = max(0, ($periodDuration - $lockBefore) - ($now - $start));
|
||||
}
|
||||
|
||||
// 获取结果的通用方法
|
||||
$getResult = function($periodRow) use ($db, $resultTable, $algoClass) {
|
||||
if (!$periodRow) return null;
|
||||
if ($resultTable) {
|
||||
return $db->get($resultTable, '*', ['period_id' => $periodRow['id']]);
|
||||
}
|
||||
// dice/xocdia: 结果在 periods 表中
|
||||
return $periodRow;
|
||||
};
|
||||
|
||||
// ====== 最近开奖结果 ======
|
||||
$lastResult = null;
|
||||
$lastResultStatus = null;
|
||||
$lastDrawn = $db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'status' => ['drawn', 'settled'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
if ($lastDrawn) {
|
||||
$pk10 = $getResult($lastDrawn);
|
||||
if ($pk10) {
|
||||
$lastResult = $pk10;
|
||||
$lastResult['period_number'] = $lastDrawn['period_number'];
|
||||
$lastResultStatus = $lastDrawn['status'];
|
||||
}
|
||||
}
|
||||
|
||||
// ====== locked 状态预开奖结果 ======
|
||||
$raceResult = null;
|
||||
if ($period && $period['status'] === 'locked' && !empty($period['result'])) {
|
||||
$pk10Current = $getResult($period);
|
||||
if ($pk10Current) {
|
||||
$raceResult = $pk10Current;
|
||||
$raceResult['period_number'] = $period['period_number'];
|
||||
}
|
||||
}
|
||||
|
||||
// history
|
||||
$history = [];
|
||||
$recentPeriods = $db->select('periods', ['id','period_number','result','status'], [
|
||||
'game_id' => $gameId,
|
||||
'status' => ['drawn', 'settled'],
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 10
|
||||
]);
|
||||
foreach ($recentPeriods as $s) {
|
||||
$pk = $getResult($s);
|
||||
if ($pk) { $pk['period_number'] = $s['period_number']; $history[] = $pk; }
|
||||
}
|
||||
|
||||
// user bets + balance
|
||||
$myBets = [];
|
||||
$balance = null;
|
||||
$userId = $this->getWebUserId();
|
||||
if ($userId && $period) {
|
||||
$betsRaw = $db->select('bets', ['id', 'bet_type', 'bet_value', 'amount', 'odds', 'status', 'win_amount'], [
|
||||
'user_id' => $userId,
|
||||
'period_id' => $period['id'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
$myBets = $betsRaw ?: [];
|
||||
$userRow = $db->get('users', ['balance'], ['id' => $userId]);
|
||||
$balance = $userRow ? (float)$userRow['balance'] : null;
|
||||
}
|
||||
|
||||
$lastMyBets = [];
|
||||
if ($userId && $lastDrawn && $lastDrawn['status'] === 'settled') {
|
||||
$lastBetsRaw = $db->select('bets', ['id', 'bet_type', 'bet_value', 'amount', 'odds', 'status', 'win_amount'], [
|
||||
'user_id' => $userId,
|
||||
'period_id' => $lastDrawn['id'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
$lastMyBets = $lastBetsRaw ?: [];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'id' => $period['id'] ?? 0,
|
||||
'period_number' => $period['period_number'] ?? '',
|
||||
'status' => $period['status'] ?? 'none',
|
||||
'remaining_seconds' => $countdown,
|
||||
'lock_countdown' => $lockCountdown,
|
||||
'auto_generated' => (int)($period['auto_generated'] ?? 0),
|
||||
'server_time' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
'last_result' => $lastResult,
|
||||
'last_result_status' => $lastResultStatus,
|
||||
'race_result' => $raceResult,
|
||||
'history' => $history,
|
||||
'my_bets' => $myBets,
|
||||
'last_my_bets' => $lastMyBets,
|
||||
'balance' => $balance,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getWebUserId(): int {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return (int)($_SESSION['web_user_id'] ?? 0);
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class AdminBaseController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// 检测是否是管理员
|
||||
protected function checkAdmin()
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => '您没有权限访问该功能'
|
||||
]);
|
||||
} else {
|
||||
echo "<script>alert('您没有权限访问该功能');history.back();</script>";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
class BaseController {
|
||||
|
||||
/**
|
||||
* 渲染视图
|
||||
* @param string $view 视图名,支持如 admin.dashboard(映射为 app/views/admin/dashboard.php)
|
||||
* @param array $data 传递给视图的数据
|
||||
*/
|
||||
protected function render($viewPath, $data = []) {
|
||||
if (strpos($viewPath, '/') === 0 || preg_match('/^[a-zA-Z]:\\\\/', $viewPath)) {
|
||||
$fullPath = $viewPath;
|
||||
} else {
|
||||
$viewsDir = __DIR__ . '/../Views/';
|
||||
$fullPath = $viewsDir . $viewPath;
|
||||
}
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
throw new \Exception("视图文件不存在: {$fullPath}", 500);
|
||||
}
|
||||
|
||||
extract($data);
|
||||
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
include $fullPath;
|
||||
} else {
|
||||
ob_start();
|
||||
include $fullPath;
|
||||
$Content = ob_get_clean();
|
||||
|
||||
include __DIR__ . '/../Views/Admin/index.php'; // 主后台模板
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查用户登录
|
||||
*/
|
||||
protected function checkLogin() {
|
||||
|
||||
session_start();
|
||||
|
||||
$timeout = 7200;
|
||||
|
||||
// 提取 IP 前三段(IPv4)
|
||||
function get_ip_prefix($ip, $segments = 3) {
|
||||
$parts = explode('.', $ip);
|
||||
return implode('.', array_slice($parts, 0, $segments));
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['username'])) {
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 宽松 IP 检查(只比对前三段,例如 192.168.1.xxx)
|
||||
$current_ip_prefix = get_ip_prefix($_SERVER['REMOTE_ADDR'], 3);
|
||||
$session_ip_prefix = get_ip_prefix($_SESSION['ip'] ?? '', 3);
|
||||
|
||||
if ($current_ip_prefix !== $session_ip_prefix) {
|
||||
session_destroy();
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SESSION['ua'] !== $_SERVER['HTTP_USER_AGENT']) {
|
||||
session_destroy();
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (time() - ($_SESSION['last_activity'] ?? 0) > $timeout) {
|
||||
session_destroy();
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 404 页面
|
||||
*/
|
||||
public function show404($msg = '') {
|
||||
http_response_code(404);
|
||||
echo "<h1>404 Not Found</h1>";
|
||||
if ($msg) echo "<p>$msg</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function showError($errorMessage) {
|
||||
// 错误视图文件路径(根据实际项目目录调整)
|
||||
$errorViewPath = __DIR__ . '/../Views/Web/error.php';
|
||||
|
||||
// 检查错误视图文件是否存在
|
||||
if (!file_exists($errorViewPath)) {
|
||||
die("错误:找不到错误视图文件,请检查路径是否正确");
|
||||
}
|
||||
|
||||
// 传递错误信息到视图
|
||||
$error = $errorMessage;
|
||||
|
||||
// 加载错误视图(通过include将变量传入视图)
|
||||
include $errorViewPath;
|
||||
|
||||
// 终止后续代码执行
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class DiceAlgorithm implements GameAlgorithmInterface {
|
||||
|
||||
public static function generateResult(): array {
|
||||
return [rand(1, 6), rand(1, 6), rand(1, 6)];
|
||||
}
|
||||
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array {
|
||||
if (empty($bets) || empty($waterConfig)) return self::generateResult();
|
||||
$bestResult = null;
|
||||
$bestProfit = PHP_INT_MIN;
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$result = self::generateResult();
|
||||
$profit = self::calculatePlatformProfit($result, $bets);
|
||||
if ($profit > $bestProfit) { $bestProfit = $profit; $bestResult = $result; }
|
||||
}
|
||||
return $bestResult;
|
||||
}
|
||||
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
||||
$total = array_sum($result);
|
||||
switch ($betType) {
|
||||
case 'big_small':
|
||||
return ($betTarget === 'big' && $total >= 11) || ($betTarget === 'small' && $total <= 10);
|
||||
case 'odd_even':
|
||||
return ($betTarget === 'odd' && $total % 2 === 1) || ($betTarget === 'even' && $total % 2 === 0);
|
||||
case 'sum':
|
||||
return (int)$betTarget === $total;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float {
|
||||
$totalBet = 0; $totalPayout = 0;
|
||||
foreach ($bets as $bet) {
|
||||
$amount = (float)$bet['amount'];
|
||||
$totalBet += $amount;
|
||||
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'] ?? $bet['bet_value'] ?? '')) {
|
||||
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
||||
}
|
||||
}
|
||||
return $totalBet - $totalPayout;
|
||||
}
|
||||
|
||||
public static function getResultTable(): ?string { return null; }
|
||||
|
||||
public static function formatResultForStorage(array $result, int $periodId): array {
|
||||
return [
|
||||
'dice1' => $result[0], 'dice2' => $result[1], 'dice3' => $result[2],
|
||||
'total' => array_sum($result),
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseResultFromDb(array $row): array {
|
||||
return [(int)($row['dice1'] ?? 0), (int)($row['dice2'] ?? 0), (int)($row['dice3'] ?? 0)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
interface GameAlgorithmInterface {
|
||||
public static function generateResult(): array;
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array;
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool;
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float;
|
||||
public static function getResultTable(): ?string;
|
||||
public static function formatResultForStorage(array $result, int $periodId): array;
|
||||
public static function parseResultFromDb(array $row): array;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class GameFactory {
|
||||
private static $map = [
|
||||
'pk10' => PK10Algorithm::class,
|
||||
'dice' => DiceAlgorithm::class,
|
||||
'xocdia' => XocDiaAlgorithm::class,
|
||||
];
|
||||
|
||||
public static function getAlgorithm(string $gameType): string {
|
||||
$class = self::$map[$gameType] ?? null;
|
||||
if (!$class) throw new \RuntimeException("Unknown game type: {$gameType}");
|
||||
return $class;
|
||||
}
|
||||
|
||||
public static function register(string $gameType, string $class): void {
|
||||
self::$map[$gameType] = $class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class I18n {
|
||||
private static $lang = 'en';
|
||||
private static $translations = [];
|
||||
private static $fallback = [];
|
||||
private static $loaded = false;
|
||||
|
||||
// 支持的语言列表
|
||||
const LANGUAGES = [
|
||||
'en' => 'English',
|
||||
'th' => 'ไทย',
|
||||
'vi' => 'Tiếng Việt',
|
||||
'zh' => '中文',
|
||||
'ms' => 'Bahasa Melayu',
|
||||
'fil' => 'Filipino',
|
||||
'bn' => 'বাংলা',
|
||||
];
|
||||
|
||||
public static function init($db = null) {
|
||||
if (self::$loaded) return;
|
||||
|
||||
// 优先级: URL参数 > Session > Cookie > 浏览器 > 默认en
|
||||
if (!empty($_GET['lang']) && isset(self::LANGUAGES[$_GET['lang']])) {
|
||||
self::$lang = $_GET['lang'];
|
||||
} elseif (!empty($_SESSION['lang'])) {
|
||||
self::$lang = $_SESSION['lang'];
|
||||
} elseif (!empty($_COOKIE['lang'])) {
|
||||
self::$lang = $_COOKIE['lang'];
|
||||
} else {
|
||||
self::$lang = self::detectBrowserLang();
|
||||
}
|
||||
|
||||
$_SESSION['lang'] = self::$lang;
|
||||
setcookie('lang', self::$lang, time() + 86400 * 365, '/');
|
||||
|
||||
self::loadFromFile();
|
||||
self::$loaded = true;
|
||||
}
|
||||
|
||||
private static function detectBrowserLang(): string {
|
||||
$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
||||
foreach (self::LANGUAGES as $code => $name) {
|
||||
if (stripos($accept, $code) !== false) return $code;
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
private static function loadFromFile() {
|
||||
$file = ROOT_PATH . 'Lang/' . self::$lang . '.php';
|
||||
if (file_exists($file)) {
|
||||
self::$translations = require $file;
|
||||
}
|
||||
// 始终加载英文作为fallback
|
||||
$enFile = ROOT_PATH . 'Lang/en.php';
|
||||
if (file_exists($enFile)) {
|
||||
self::$fallback = require $enFile;
|
||||
}
|
||||
}
|
||||
|
||||
public static function t(string $key, array $params = []): string {
|
||||
$text = self::$translations[$key] ?? self::$fallback[$key] ?? $key;
|
||||
foreach ($params as $k => $v) {
|
||||
$text = str_replace(':' . $k, $v, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public static function getLang(): string { return self::$lang; }
|
||||
public static function setLang(string $lang) {
|
||||
if (isset(self::LANGUAGES[$lang])) {
|
||||
self::$lang = $lang;
|
||||
self::$loaded = false;
|
||||
self::init();
|
||||
}
|
||||
}
|
||||
public static function getLanguages(): array { return self::LANGUAGES; }
|
||||
}
|
||||
|
||||
// 全局快捷函数
|
||||
function __($key, $params = []) { return I18n::t($key, $params); }
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
use Db\Database;
|
||||
|
||||
class Mailer {
|
||||
|
||||
public static function send(string $to, string $subject, string $htmlBody): bool {
|
||||
$cfg = self::getConfig();
|
||||
if (empty($cfg['smtp_host']) || empty($cfg['smtp_user'])) {
|
||||
// fallback to mail()
|
||||
$headers = "MIME-Version: 1.0\r\nContent-type:text/html;charset=UTF-8\r\nFrom: {$cfg['smtp_from']}\r\n";
|
||||
return @mail($to, $subject, $htmlBody, $headers);
|
||||
}
|
||||
|
||||
$host = $cfg['smtp_host'];
|
||||
$port = (int)($cfg['smtp_port'] ?: 465);
|
||||
$user = $cfg['smtp_user'];
|
||||
$pass = $cfg['smtp_pass'];
|
||||
$from = $cfg['smtp_from'] ?: $user;
|
||||
$fromName = $cfg['smtp_from_name'] ?: 'System';
|
||||
$encryption = $cfg['smtp_encryption'] ?: 'ssl';
|
||||
|
||||
try {
|
||||
$target = ($encryption === 'ssl') ? "ssl://{$host}" : $host;
|
||||
$sock = @fsockopen($target, $port, $errno, $errstr, 10);
|
||||
if (!$sock) throw new \RuntimeException("Connect failed: {$errstr}");
|
||||
|
||||
self::readLine($sock);
|
||||
self::cmd($sock, "EHLO localhost");
|
||||
|
||||
// STARTTLS for tls mode
|
||||
if ($encryption === 'tls') {
|
||||
self::cmd($sock, "STARTTLS");
|
||||
stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
self::cmd($sock, "EHLO localhost");
|
||||
}
|
||||
|
||||
// AUTH LOGIN
|
||||
self::cmd($sock, "AUTH LOGIN");
|
||||
self::cmd($sock, base64_encode($user));
|
||||
self::cmd($sock, base64_encode($pass));
|
||||
|
||||
self::cmd($sock, "MAIL FROM:<{$from}>");
|
||||
self::cmd($sock, "RCPT TO:<{$to}>");
|
||||
self::cmd($sock, "DATA");
|
||||
|
||||
$msg = "From: {$fromName} <{$from}>\r\n"
|
||||
. "To: {$to}\r\n"
|
||||
. "Subject: {$subject}\r\n"
|
||||
. "MIME-Version: 1.0\r\n"
|
||||
. "Content-Type: text/html; charset=UTF-8\r\n"
|
||||
. "\r\n"
|
||||
. $htmlBody . "\r\n.\r\n";
|
||||
fwrite($sock, $msg);
|
||||
self::readLine($sock);
|
||||
|
||||
self::cmd($sock, "QUIT");
|
||||
fclose($sock);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Mailer error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static function cmd($sock, string $cmd): string {
|
||||
fwrite($sock, $cmd . "\r\n");
|
||||
return self::readLine($sock);
|
||||
}
|
||||
|
||||
private static function readLine($sock): string {
|
||||
$resp = '';
|
||||
while ($line = fgets($sock, 512)) {
|
||||
$resp .= $line;
|
||||
if (isset($line[3]) && $line[3] === ' ') break;
|
||||
}
|
||||
return $resp;
|
||||
}
|
||||
|
||||
public static function getConfig(): array {
|
||||
$defaults = [
|
||||
'smtp_host' => '', 'smtp_port' => '465', 'smtp_user' => '',
|
||||
'smtp_pass' => '', 'smtp_from' => '', 'smtp_from_name' => 'PK10',
|
||||
'smtp_encryption' => 'ssl',
|
||||
];
|
||||
try {
|
||||
$db = new Database();
|
||||
$rows = $db->select('system_settings', ['setting_key', 'setting_value'], [
|
||||
'setting_key[~]' => 'smtp_%'
|
||||
]);
|
||||
foreach ($rows as $r) $defaults[$r['setting_key']] = $r['setting_value'];
|
||||
} catch (\Throwable $e) {}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public static function test(string $to): array {
|
||||
$ok = self::send($to, 'SMTP Test', '<h2>SMTP configuration is working!</h2><p>Time: ' . date('Y-m-d H:i:s') . '</p>');
|
||||
return ['success' => $ok, 'message' => $ok ? 'Test email sent' : 'Failed to send, check SMTP settings'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class PK10Algorithm implements GameAlgorithmInterface {
|
||||
|
||||
/**
|
||||
* 生成随机开奖结果 (1-10的排列)
|
||||
*/
|
||||
public static function generateResult(): array {
|
||||
$cars = range(1, 10);
|
||||
shuffle($cars);
|
||||
return $cars; // index 0=冠军, 1=亚军, ..., 9=第十名
|
||||
}
|
||||
|
||||
/**
|
||||
* 带放水机制的开奖结果生成
|
||||
* @param array $bets 当期所有投注 [{bet_type, bet_target, amount, odds}, ...]
|
||||
* @param array $waterConfig [{bet_type => win_rate_pct}, ...]
|
||||
* @param int $attempts 最大尝试次数
|
||||
*/
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array {
|
||||
if (empty($bets) || empty($waterConfig)) {
|
||||
return self::generateResult();
|
||||
}
|
||||
|
||||
$bestResult = null;
|
||||
$bestProfit = PHP_INT_MIN;
|
||||
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$result = self::generateResult();
|
||||
$profit = self::calculatePlatformProfit($result, $bets);
|
||||
|
||||
// 选择平台利润最高的结果
|
||||
if ($profit > $bestProfit) {
|
||||
$bestProfit = $profit;
|
||||
$bestResult = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算某个开奖结果下平台的利润
|
||||
*/
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float {
|
||||
$totalBet = 0;
|
||||
$totalPayout = 0;
|
||||
foreach ($bets as $bet) {
|
||||
$amount = (float)$bet['amount'];
|
||||
$totalBet += $amount;
|
||||
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'])) {
|
||||
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
||||
}
|
||||
}
|
||||
return $totalBet - $totalPayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心中奖判定
|
||||
* @param array $result 开奖排名 [冠军车号, 亚军车号, ..., 第十名车号]
|
||||
* @param string $betType 投注类型: rank/bs/oe/dt/sum/sum_bs
|
||||
* @param string $betTarget 投注目标: rank1_5/rank1_big/dt1_dragon/sum_11/sum_big...
|
||||
*/
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
||||
switch ($betType) {
|
||||
case 'rank': return self::checkRank($result, $betTarget);
|
||||
case 'bs': return self::checkBigSmall($result, $betTarget);
|
||||
case 'oe': return self::checkOddEven($result, $betTarget);
|
||||
case 'dt': return self::checkDragonTiger($result, $betTarget);
|
||||
case 'sum': return self::checkSum($result, $betTarget);
|
||||
case 'sum_bs': return self::checkSumBigSmall($result, $betTarget);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 名次投注: rank{N}_{carNo} 如 rank1_5 = 冠军是5号车
|
||||
private static function checkRank(array $r, string $target): bool {
|
||||
if (!preg_match('/^rank(\d+)_(\d+)$/', $target, $m)) return false;
|
||||
$pos = (int)$m[1] - 1; // 0-indexed
|
||||
$car = (int)$m[2];
|
||||
return isset($r[$pos]) && $r[$pos] === $car;
|
||||
}
|
||||
|
||||
// 大小: rank{N}_big/small, 车号>=6为大, <=5为小
|
||||
private static function checkBigSmall(array $r, string $target): bool {
|
||||
if (!preg_match('/^rank(\d+)_(big|small)$/', $target, $m)) return false;
|
||||
$pos = (int)$m[1] - 1;
|
||||
if (!isset($r[$pos])) return false;
|
||||
$car = $r[$pos];
|
||||
return $m[2] === 'big' ? $car >= 6 : $car <= 5;
|
||||
}
|
||||
|
||||
// 单双: rank{N}_odd/even
|
||||
private static function checkOddEven(array $r, string $target): bool {
|
||||
if (!preg_match('/^rank(\d+)_(odd|even)$/', $target, $m)) return false;
|
||||
$pos = (int)$m[1] - 1;
|
||||
if (!isset($r[$pos])) return false;
|
||||
$car = $r[$pos];
|
||||
return $m[2] === 'odd' ? ($car % 2 === 1) : ($car % 2 === 0);
|
||||
}
|
||||
|
||||
// 龙虎: dt{N}_dragon/tiger, N=1-5, 对应 1vs10, 2vs9, 3vs8, 4vs7, 5vs6
|
||||
private static function checkDragonTiger(array $r, string $target): bool {
|
||||
if (!preg_match('/^dt(\d)_(dragon|tiger)$/', $target, $m)) return false;
|
||||
$pair = (int)$m[1]; // 1-5
|
||||
$frontPos = $pair - 1; // 0,1,2,3,4
|
||||
$backPos = 10 - $pair; // 9,8,7,6,5
|
||||
if (!isset($r[$frontPos], $r[$backPos])) return false;
|
||||
$front = $r[$frontPos];
|
||||
$back = $r[$backPos];
|
||||
if ($front === $back) return false; // 和局(PK10不会出现)
|
||||
return $m[2] === 'dragon' ? $front > $back : $front < $back;
|
||||
}
|
||||
|
||||
// 冠亚和值: sum_{3-19}
|
||||
private static function checkSum(array $r, string $target): bool {
|
||||
if (!preg_match('/^sum_(\d+)$/', $target, $m)) return false;
|
||||
$sumVal = $r[0] + $r[1]; // 冠军+亚军
|
||||
return $sumVal === (int)$m[1];
|
||||
}
|
||||
|
||||
// 冠亚和大小单双: sum_big/small/odd/even, 和>=12为大, <=11为小
|
||||
private static function checkSumBigSmall(array $r, string $target): bool {
|
||||
$sumVal = $r[0] + $r[1];
|
||||
switch ($target) {
|
||||
case 'sum_big': return $sumVal >= 12;
|
||||
case 'sum_small': return $sumVal <= 11;
|
||||
case 'sum_odd': return $sumVal % 2 === 1;
|
||||
case 'sum_even': return $sumVal % 2 === 0;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取名次中文/英文名称
|
||||
*/
|
||||
public static function getRankName(int $pos): string {
|
||||
$names = [1=>'champion',2=>'runner_up',3=>'rank_n',4=>'rank_n',5=>'rank_n',
|
||||
6=>'rank_n',7=>'rank_n',8=>'rank_n',9=>'rank_n',10=>'rank_n'];
|
||||
return $names[$pos] ?? 'rank_n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 龙虎对应关系
|
||||
*/
|
||||
public static function getDragonTigerPairs(): array {
|
||||
return [
|
||||
1 => [1, 10], // 第1名 vs 第10名
|
||||
2 => [2, 9],
|
||||
3 => [3, 8],
|
||||
4 => [4, 7],
|
||||
5 => [5, 6],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 期号生成
|
||||
*/
|
||||
public static function generatePeriodNumber(int $gameId): string {
|
||||
return 'PK' . $gameId . date('Ymd') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public static function getResultTable(): ?string { return 'pk10_results'; }
|
||||
|
||||
public static function formatResultForStorage(array $result, int $periodId): array {
|
||||
return [
|
||||
'period_id' => $periodId,
|
||||
'rank_1' => $result[0], 'rank_2' => $result[1], 'rank_3' => $result[2],
|
||||
'rank_4' => $result[3], 'rank_5' => $result[4], 'rank_6' => $result[5],
|
||||
'rank_7' => $result[6], 'rank_8' => $result[7], 'rank_9' => $result[8],
|
||||
'rank_10' => $result[9], 'champion_sum' => $result[0] + $result[1],
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseResultFromDb(array $row): array {
|
||||
$r = [];
|
||||
for ($i = 1; $i <= 10; $i++) $r[] = (int)($row['rank_' . $i] ?? 0);
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class PluginBaseController extends BaseController {
|
||||
/**
|
||||
* 渲染插件视图文件
|
||||
*
|
||||
* @param string $pluginName 插件名称(必须与插件目录一致)
|
||||
* @param string $viewFile 视图相对路径,如 'Admin/list.php'
|
||||
* @param array $data 传递给视图的变量数组
|
||||
*/
|
||||
protected function renderPluginView($pluginName, $viewFile, $data = []) {
|
||||
$viewPath = PLUGIN_PATH . $pluginName . '/Views/' . $viewFile;
|
||||
// 调用基类的渲染方法
|
||||
$this->render($viewPath, $data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Executable
+682
@@ -0,0 +1,682 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class PluginManager {
|
||||
protected $db;
|
||||
protected $pluginDir;
|
||||
protected $loadedPlugins = [];
|
||||
protected $router;
|
||||
protected $logFile;
|
||||
protected $installedPluginsCache = null;
|
||||
protected $registeredRoutes = [];
|
||||
protected $plugins = null;
|
||||
protected $enabledPlugins = [];
|
||||
protected $systemRoutes = [];
|
||||
protected $enabledRoutes = [];
|
||||
private $lastError = '';
|
||||
|
||||
private function setError(string $msg) {
|
||||
$this->lastError = $msg;
|
||||
$this->log("[ERROR] $msg");
|
||||
}
|
||||
|
||||
public function getLastError(): string {
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param object $db 数据库对象
|
||||
* @param object $router 路由对象
|
||||
* @param string $pluginDir 插件目录
|
||||
*/
|
||||
public function __construct($db, $router, string $pluginDir) {
|
||||
$this->db = $db;
|
||||
$this->router = $router;
|
||||
$this->pluginDir = rtrim($pluginDir, '/');
|
||||
|
||||
$rootDir = realpath(__DIR__ . '/../../');
|
||||
$logDir = $rootDir . '/Storage/log';
|
||||
|
||||
// 确保日志目录(如不存在则创建)
|
||||
if (!is_dir($logDir)) {
|
||||
if (!mkdir($logDir, 0755, true) && !is_dir($logDir)) {
|
||||
throw new \RuntimeException("无法创建日志目录: $logDir ,请检查权限");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目录可写性
|
||||
if (!is_writable($logDir)) {
|
||||
throw new \RuntimeException("日志目录不可写: $logDir ,请检查权限");
|
||||
}
|
||||
|
||||
$this->logFile = $logDir . '/plugin_manager.log';
|
||||
|
||||
// 检查日志文件大小并自动清理(大于2MB时)
|
||||
$this->cleanupLogFile(2); // 传入最大允许的MB数
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理日志日志文件清理
|
||||
* @param int $maxSizeMB 最大允许的文件大小(MB)
|
||||
*/
|
||||
private function cleanupLogFile(int $maxSizeMB) {
|
||||
// 检查文件是否存在
|
||||
if (!file_exists($this->logFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换MB为字节
|
||||
$maxSizeBytes = $maxSizeMB * 1024 * 1024;
|
||||
|
||||
// 获取当前文件大小
|
||||
$currentSize = filesize($this->logFile);
|
||||
|
||||
// 如果文件大小超过限制,清空文件
|
||||
if ($currentSize > $maxSizeBytes) {
|
||||
// 先备份当前日志内容(可选)
|
||||
$backupFile = $this->logFile . '.bak_' . date('YmdHis');
|
||||
copy($this->logFile, $backupFile);
|
||||
|
||||
// 清空日志文件
|
||||
file_put_contents($this->logFile, '');
|
||||
|
||||
// 记录清理日志
|
||||
$message = "[" . date('Y-m-d H:i:s') . "] 日志文件超过{$maxSizeMB}MB,已自动清理\n";
|
||||
file_put_contents($this->logFile, $message, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置系统核心路由
|
||||
* @param array $routes 格式: [['GET', '/path', 'handler'], ...]
|
||||
*/
|
||||
public function setSystemRoutes(array $routes): void {
|
||||
$this->systemRoutes = [];
|
||||
foreach ($routes as $route) {
|
||||
if (count($route) < 2) continue;
|
||||
[$method, $path] = $route;
|
||||
$key = strtoupper(trim($method)) . ' ' . trim($path);
|
||||
$this->systemRoutes[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册路由(系统+已启用插件)
|
||||
* @return array 路由键名数组
|
||||
*/
|
||||
public function getAllRegisteredRoutes(): array {
|
||||
return array_merge(
|
||||
array_keys($this->systemRoutes),
|
||||
array_keys($this->enabledRoutes)
|
||||
);
|
||||
}
|
||||
|
||||
public function getDB() {
|
||||
return $this->db;
|
||||
}
|
||||
|
||||
protected function log(string $msg, string $level = 'INFO'): void {
|
||||
$date = date('Y-m-d H:i:s');
|
||||
$logMsg = "[$date] [$level] $msg\n";
|
||||
$logDir = dirname($this->logFile);
|
||||
|
||||
try {
|
||||
if (is_dir($logDir) && is_writable($logDir)) {
|
||||
file_put_contents($this->logFile, $logMsg, FILE_APPEND);
|
||||
} else {
|
||||
error_log("PluginManager log directory not writable: $logDir");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Failed to write plugin log: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有扫描到的插件信息
|
||||
* @return array
|
||||
*/
|
||||
public function getAllPlugins(): array {
|
||||
$this->scanPlugins();
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
public function scanPlugins() {
|
||||
if ($this->plugins !== null) {
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
$this->log("scanPlugins called");
|
||||
$this->plugins = [];
|
||||
|
||||
if (!is_dir($this->pluginDir)) {
|
||||
mkdir($this->pluginDir, 0755, true);
|
||||
$this->log("Plugin directory created: {$this->pluginDir}");
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
$dirs = scandir($this->pluginDir);
|
||||
foreach ($dirs as $dir) {
|
||||
if ($dir === '.' || $dir === '..') continue;
|
||||
|
||||
$pluginPath = $this->pluginDir . '/' . $dir;
|
||||
$pluginFile = $pluginPath . '/mian.php';
|
||||
|
||||
if (is_dir($pluginPath) && is_file($pluginFile)) {
|
||||
$pluginConfig = $this->getPluginInfo($pluginFile, $dir);
|
||||
|
||||
if (empty($pluginConfig)) {
|
||||
$this->log("[WARN] 插件 {$dir} 不规范,已跳过");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->plugins[$dir] = array_merge([
|
||||
'dir' => $dir,
|
||||
'path' => $pluginPath,
|
||||
], $pluginConfig);
|
||||
|
||||
$this->log("Plugin found: $dir ({$pluginConfig['name']})");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
|
||||
public function getAllPluginIcons(): array
|
||||
{
|
||||
$pluginDirs = array_filter(scandir(PLUGIN_PATH . '/'), function($dir) {
|
||||
return $dir !== '.' && $dir !== '..';
|
||||
});
|
||||
|
||||
$icons = [];
|
||||
foreach ($pluginDirs as $pluginDir) {
|
||||
$pluginFile = PLUGIN_PATH . "/{$pluginDir}/mian.php";
|
||||
if (file_exists($pluginFile)) {
|
||||
$info = include $pluginFile;
|
||||
$icons[$pluginDir] = $info['menus'][0]['icon'] ?? 'fa fa-plug';
|
||||
}
|
||||
}
|
||||
|
||||
return $icons;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从插件文件头部注释获取插件信息
|
||||
*/
|
||||
private function getPluginInfo(string $pluginFile, string $pluginDirName): array {
|
||||
$info = [];
|
||||
$arrayConfig = @include $pluginFile;
|
||||
if (!is_array($arrayConfig)) {
|
||||
$this->setError("[ERROR] Plugin file {$pluginFile} must return an array");
|
||||
return [];
|
||||
}
|
||||
$lines = file($pluginFile);
|
||||
if (!$lines) {
|
||||
$this->setError("[ERROR] Cannot read plugin file: {$pluginFile}");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 读取前 30 行,兼容大部分注释头
|
||||
$header = implode('', array_slice($lines, 0, 30));
|
||||
//$this->log("Header of {$pluginFile}:\n" . $header);
|
||||
if (preg_match_all('/^\s*\*\s*([A-Za-z ]+):\s*(.+)$/m', $header, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$key = strtolower(str_replace(' ', '_', trim($match[1])));
|
||||
$value = trim($match[2]);
|
||||
$info[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 必填字段列表
|
||||
$requiredFields = ['plugin_name', 'version', 'description', 'author', 'plugin_url'];
|
||||
|
||||
// 检查必要字段是否存在且非空
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($info[$field])) {
|
||||
$this->setError("[ERROR] Plugin {$pluginFile} 缺少必要字段或为空: {$field}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 插件文件夹名必须和插件名一致
|
||||
if ($pluginDirName !== $info['plugin_name']) {
|
||||
$this->setError("[ERROR] 插件目录名 {$pluginDirName} 与插件名 {$info['plugin_name']} 不一致");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (isset($info['plugin_name'])) {
|
||||
$arrayConfig['name'] = $info['plugin_name'];
|
||||
}
|
||||
if (isset($info['description'])) {
|
||||
$arrayConfig['description'] = $info['description'];
|
||||
}
|
||||
if (isset($info['version'])) {
|
||||
$arrayConfig['version'] = $info['version'];
|
||||
}
|
||||
if (isset($info['author'])) {
|
||||
$arrayConfig['author'] = $info['author'];
|
||||
}
|
||||
if (isset($info['plugin_url'])) {
|
||||
$arrayConfig['url'] = $info['plugin_url'];
|
||||
}
|
||||
|
||||
return $arrayConfig;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function clearCache() {
|
||||
$this->plugins = null;
|
||||
}
|
||||
|
||||
public function getEnabledPluginMenus(): array {
|
||||
$menus = [];
|
||||
|
||||
foreach ($this->enabledPlugins as $plugin) {
|
||||
if (isset($plugin['menus']) && is_array($plugin['menus'])) {
|
||||
$menus = array_merge($menus, $plugin['menus']);
|
||||
}
|
||||
}
|
||||
|
||||
return $menus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装插件列表
|
||||
* @return array
|
||||
*/
|
||||
public function getInstalledPlugins(): array {
|
||||
// 如果数据库对象不存在或未初始化,直接返回空数组
|
||||
if (!$this->db || !$this->db->medoo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->installedPluginsCache !== null) {
|
||||
return $this->installedPluginsCache;
|
||||
}
|
||||
|
||||
try {
|
||||
$installed = $this->db->select('plugins', '*');
|
||||
if (!is_array($installed)) {
|
||||
$installed = [];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 捕获数据库异常,返回空数组
|
||||
$installed = [];
|
||||
}
|
||||
|
||||
$installedPlugins = [];
|
||||
foreach ($installed as $row) {
|
||||
$installedPlugins[$row['name']] = $row;
|
||||
}
|
||||
|
||||
$this->installedPluginsCache = $installedPlugins;
|
||||
return $installedPlugins;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有插件状态信息
|
||||
* @return array
|
||||
*/
|
||||
public function getPluginStatusList(): array {
|
||||
$this->scanPlugins();
|
||||
$installedPlugins = $this->getInstalledPlugins();
|
||||
$result = [];
|
||||
foreach ($this->plugins as $name => $plugin) {
|
||||
$installed = isset($installedPlugins[$name]);
|
||||
$status = $installed ? (int)$installedPlugins[$name]['status'] : 0;
|
||||
$result[] = [
|
||||
'name' => $name,
|
||||
'installed' => $installed ? 1 : 0,
|
||||
'status' => $status,
|
||||
'path' => $plugin['path'],
|
||||
'title' => $installed ? $installedPlugins[$name]['title'] : ($plugin['title'] ?? $name),
|
||||
'version' => $installed ? $installedPlugins[$name]['version'] : ($plugin['version'] ?? ''),
|
||||
'description' => $installed ? $installedPlugins[$name]['description'] : ($plugin['description'] ?? ''),
|
||||
'author' => $installed ? $installedPlugins[$name]['author'] : ($plugin['author'] ?? ''),
|
||||
'url' => $installed ? $installedPlugins[$name]['url'] : ($plugin['url'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的插件名称列表
|
||||
* @return array
|
||||
*/
|
||||
public function getEnabledPlugins(): array {
|
||||
$enabled = [];
|
||||
$installed = $this->getInstalledPlugins();
|
||||
foreach ($this->plugins as $name => $plugin) {
|
||||
if (isset($installed[$name]) && $installed[$name]['status'] == 1) {
|
||||
$enabled[] = $name;
|
||||
}
|
||||
}
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归加载控制器目录内所有PHP文件
|
||||
* @param string $dir
|
||||
*/
|
||||
protected function loadControllersRecursively(string $dir): void {
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($dir);
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') continue;
|
||||
|
||||
$fullPath = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
if (is_dir($fullPath)) {
|
||||
$this->loadControllersRecursively($fullPath);
|
||||
} elseif (is_file($fullPath) && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
|
||||
require_once $fullPath;
|
||||
$this->log("Loaded controller: $fullPath");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function isAssoc(array $arr): bool {
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
|
||||
public function loadEnabledPlugins(): void {
|
||||
static $loaded = false;
|
||||
if ($loaded) {
|
||||
return;
|
||||
}
|
||||
$loaded = true;
|
||||
|
||||
$this->scanPlugins();
|
||||
$installed = $this->getInstalledPlugins();
|
||||
|
||||
$this->enabledPlugins = [];
|
||||
$this->registeredRoutes = [];
|
||||
$this->enabledRoutes = [];
|
||||
|
||||
foreach ($this->plugins as $pluginName => $pluginData) {
|
||||
if (isset($installed[$pluginName]) && $installed[$pluginName]['status'] == 1) {
|
||||
// 激活插件(调用init)
|
||||
if (!empty($pluginData['init']) && is_callable($pluginData['init'])) {
|
||||
try {
|
||||
call_user_func($pluginData['init']);
|
||||
//$this->log("Initialized plugin $pluginName");
|
||||
} catch (\Throwable $e) {
|
||||
$this->log("[ERROR] Exception in init of plugin $pluginName: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 注册普通路由
|
||||
if (!empty($pluginData['routes']) && is_array($pluginData['routes'])) {
|
||||
foreach ($pluginData['routes'] as $route) {
|
||||
if (count($route) < 3) {
|
||||
$this->log("[WARNING] Invalid route config for plugin $pluginName");
|
||||
continue;
|
||||
}
|
||||
[$method, $path, $handler] = $route;
|
||||
$method = strtoupper($method);
|
||||
$routeKey = $method . ' ' . $path;
|
||||
|
||||
if (in_array($routeKey, $this->registeredRoutes, true)) {
|
||||
$this->log("[WARNING] Route conflict: [$method] $path");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->router->add($method, $path, $handler);
|
||||
$this->registeredRoutes[] = $routeKey;
|
||||
$this->enabledRoutes[$routeKey] = true;
|
||||
//$this->log("Registered route [$method] $path for plugin $pluginName");
|
||||
}
|
||||
}
|
||||
|
||||
// 注册路由组
|
||||
if (!empty($pluginData['route_group'])) {
|
||||
$groups = $this->isAssoc($pluginData['route_group'])
|
||||
? [$pluginData['route_group']]
|
||||
: $pluginData['route_group'];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if (isset($group['prefix'], $group['namespace'], $group['routes']) && is_array($group['routes'])) {
|
||||
$this->router->group(
|
||||
$group['prefix'],
|
||||
$group['namespace'],
|
||||
$group['routes']
|
||||
);
|
||||
//$this->log("Registered route group [prefix={$group['prefix']}] for plugin $pluginName");
|
||||
} else {
|
||||
$this->log("[WARNING] Invalid route_group config in plugin $pluginName");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->enabledPlugins[] = $pluginData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function installPlugin(string $name): bool|string {
|
||||
if (!isset($this->plugins[$name])) {
|
||||
$this->log("Install failed: plugin $name not found");
|
||||
return false;
|
||||
}
|
||||
$plugin = $this->plugins[$name];
|
||||
|
||||
// 1. 路由冲突检测
|
||||
if (!empty($plugin['route_group']) && is_array($plugin['route_group'])) {
|
||||
$prefixes = [];
|
||||
foreach ($plugin['route_group'] as $group) {
|
||||
if (isset($group['prefix'])) {
|
||||
$prefixes[] = $group['prefix'];
|
||||
}
|
||||
}
|
||||
$prefixes = array_unique($prefixes);
|
||||
|
||||
if (!$this->checkRouteConflicts($prefixes)) {
|
||||
return false; // 路由冲突阻止安装
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 表冲突检测
|
||||
if (!empty($plugin['tables']) && is_array($plugin['tables'])) {
|
||||
$conflictTables = [];
|
||||
|
||||
foreach ($plugin['tables'] as $table) {
|
||||
$stmt = $this->db->query("SHOW TABLES LIKE '{$table}'");
|
||||
if ($stmt && $stmt->fetch()) {
|
||||
$conflictTables[] = $table;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($conflictTables)) {
|
||||
$conflictList = implode(', ', $conflictTables);
|
||||
$errorMsg = "插件 {$name} 安装失败:以下数据表已存在 -> {$conflictList}";
|
||||
$this->log("[ERROR] $errorMsg");
|
||||
return $errorMsg; // 返回错误消息字符串阻止安装
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 插件元信息写入数据库
|
||||
$pluginData = [
|
||||
'name' => $name,
|
||||
'title' => $plugin['title'] ?? $name,
|
||||
'description' => $plugin['description'] ?? '',
|
||||
'version' => $plugin['version'] ?? '',
|
||||
'author' => $plugin['author'] ?? '',
|
||||
'url' => $plugin['url'] ?? '',
|
||||
'status' => 1,
|
||||
];
|
||||
|
||||
$count = $this->db->count('plugins', '*', ['name' => $name]);
|
||||
if ($count > 0) {
|
||||
$this->db->update('plugins', $pluginData, ['name' => $name]);
|
||||
} else {
|
||||
$this->db->insert('plugins', $pluginData);
|
||||
}
|
||||
|
||||
$this->installedPluginsCache = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 路由冲突检测
|
||||
*/
|
||||
public function checkRouteConflicts(array $newPrefixes): bool {
|
||||
$installedPlugins = $this->getInstalledPlugins();
|
||||
$installedPrefixes = [];
|
||||
|
||||
foreach ($this->plugins as $pluginName => $plugin) {
|
||||
if (!isset($installedPlugins[$pluginName])) continue;
|
||||
|
||||
if (!empty($plugin['route_group']) && is_array($plugin['route_group'])) {
|
||||
if (isset($plugin['route_group'][0]) && is_array($plugin['route_group'][0])) {
|
||||
foreach ($plugin['route_group'] as $group) {
|
||||
$prefix = $group['prefix'] ?? '';
|
||||
if ($prefix) $installedPrefixes[] = $prefix;
|
||||
}
|
||||
} else {
|
||||
$prefix = $plugin['route_group']['prefix'] ?? '';
|
||||
if ($prefix) $installedPrefixes[] = $prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$installedPrefixes = array_unique($installedPrefixes);
|
||||
$conflictNewPrefixes = [];
|
||||
$conflictInstalledPrefixes = [];
|
||||
|
||||
foreach ($newPrefixes as $newPrefix) {
|
||||
foreach ($installedPrefixes as $installedPrefix) {
|
||||
if ($this->isPrefixConflict($newPrefix, $installedPrefix)) {
|
||||
$conflictNewPrefixes[] = $newPrefix;
|
||||
$conflictInstalledPrefixes[] = $installedPrefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($conflictNewPrefixes)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "路由前缀冲突:新插件的前缀(" . implode(', ', array_unique($conflictNewPrefixes)) . ")与已安装插件的前缀(" . implode(', ', array_unique($conflictInstalledPrefixes)) . ")重复,请修改后再安装。"
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 判断两个路由前缀是否冲突
|
||||
*/
|
||||
protected function isPrefixConflict(string $prefixA, string $prefixB): bool {
|
||||
$prefixA = rtrim(trim($prefixA), '/');
|
||||
$prefixB = rtrim(trim($prefixB), '/');
|
||||
|
||||
// 两个都是空字符串,视为冲突
|
||||
if ($prefixA === '' && $prefixB === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 完全相等才算冲突
|
||||
if ($prefixA === $prefixB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不再判断包含关系为冲突,直接返回不冲突
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstallPlugin(string $name): bool {
|
||||
if (!isset($this->plugins[$name])) {
|
||||
$this->log("Uninstall failed: plugin $name not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
$plugin = $this->plugins[$name];
|
||||
|
||||
// 删除插件相关的数据库表
|
||||
if (!empty($plugin['tables']) && is_array($plugin['tables'])) {
|
||||
foreach ($plugin['tables'] as $table) {
|
||||
$exists = $this->db->query("SHOW TABLES LIKE '{$table}'")->fetch();
|
||||
if ($exists) {
|
||||
try {
|
||||
$this->db->query("DROP TABLE IF EXISTS `{$table}`");
|
||||
$this->log("Dropped table: {$table}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->log("[ERROR] Failed to drop table {$table}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除插件记录
|
||||
$this->db->delete('plugins', ['name' => $name]);
|
||||
|
||||
// 清除已安装插件缓存
|
||||
$this->installedPluginsCache = null;
|
||||
|
||||
$this->log("Uninstalled plugin {$name}");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启用插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function enablePlugin(string $name): bool {
|
||||
$count = $this->db->count('plugins', '*', ['name' => $name]);
|
||||
if ($count == 0) {
|
||||
$this->log("Enable failed: plugin $name not installed");
|
||||
return false;
|
||||
}
|
||||
$this->db->update('plugins', ['status' => 1], ['name' => $name]);
|
||||
$this->installedPluginsCache = null;
|
||||
$this->log("Enabled plugin $name");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function disablePlugin(string $name): bool {
|
||||
$count = $this->db->count('plugins', '*', ['name' => $name]);
|
||||
if ($count == 0) {
|
||||
$this->log("Disable failed: plugin $name not installed");
|
||||
return false;
|
||||
}
|
||||
$this->db->update('plugins', ['status' => 0], ['name' => $name]);
|
||||
$this->installedPluginsCache = null;
|
||||
$this->log("Disabled plugin $name");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class Router {
|
||||
private array $routes = [];
|
||||
private array $dependencies = [];
|
||||
|
||||
private function handle404() {
|
||||
$controller = new BaseController();
|
||||
$controller->show404();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加路由
|
||||
* @param string $method 请求方法 GET/POST 等
|
||||
* @param string $path 路径,必须以 / 开头,尾部无 /
|
||||
* @param callable|string $callback 处理器,格式:Controller@method 或回调函数
|
||||
*/
|
||||
|
||||
public function add($method, $path, $callback) {
|
||||
$methods = explode('|', strtoupper($method)); // 支持多方法
|
||||
$normalizedPath = $this->normalizePath($path);
|
||||
|
||||
foreach ($methods as $m) {
|
||||
$this->routes[$m][$normalizedPath] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
public function get($path, $callback) {
|
||||
$this->add('GET', $path, $callback);
|
||||
}
|
||||
|
||||
public function post($path, $callback) {
|
||||
$this->add('POST', $path, $callback);
|
||||
}
|
||||
|
||||
public function hasRoute($method, $path): bool {
|
||||
$method = strtoupper($method);
|
||||
$normalizedPath = $this->normalizePath($path);
|
||||
return isset($this->routes[$method][$normalizedPath]);
|
||||
}
|
||||
|
||||
|
||||
public function setDependencies(array $deps) {
|
||||
$this->dependencies = $deps;
|
||||
}
|
||||
/**
|
||||
* 路由分发
|
||||
* @param string $method 请求方法
|
||||
* @param string $uri 请求路径
|
||||
* @param array $dependencies 依赖注入数组,键为类名,值为实例(可选)
|
||||
*/
|
||||
public function dispatch($method, $uri, $dependencies = []) {
|
||||
$dependencies = array_merge($this->dependencies, $dependencies);
|
||||
$method = strtoupper($method);
|
||||
$normalizedPath = $this->normalizePath($uri);
|
||||
|
||||
if (!isset($this->routes[$method])) {
|
||||
header("HTTP/1.1 404 Not Found");
|
||||
echo "请求方法 [$method] 无任何注册路由。";
|
||||
return;
|
||||
}
|
||||
|
||||
// 精确匹配
|
||||
if (isset($this->routes[$method][$normalizedPath])) {
|
||||
$this->callHandler($this->routes[$method][$normalizedPath], [], $dependencies);
|
||||
return;
|
||||
}
|
||||
|
||||
// 模糊匹配带参数路由
|
||||
foreach ($this->routes[$method] as $routePath => $callback) {
|
||||
$pattern = $this->convertToRegex($routePath);
|
||||
if (preg_match($pattern, $normalizedPath, $matches)) {
|
||||
array_shift($matches); // 去掉完整匹配
|
||||
$this->callHandler($callback, $matches, $dependencies);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
$this->handle404();
|
||||
}
|
||||
|
||||
public function group(string $prefixUri, string $controllerNamespace, array $routes)
|
||||
{
|
||||
foreach ($routes as $route) {
|
||||
if (count($route) < 3) {
|
||||
throw new \InvalidArgumentException("每个子路由必须包含 method、uri、handler");
|
||||
}
|
||||
|
||||
[$method, $subUri, $handler] = $route;
|
||||
|
||||
// 构造完整 URI
|
||||
$uri = rtrim($prefixUri, '/') . '/' . ltrim($subUri, '/');
|
||||
|
||||
// 构造完整处理器(加命名空间)
|
||||
if (strpos($handler, '@') !== false) {
|
||||
[$controller, $action] = explode('@', $handler);
|
||||
$fullHandler = $controllerNamespace . '\\' . $controller . '@' . $action;
|
||||
} else {
|
||||
$fullHandler = $controllerNamespace . '\\' . $handler;
|
||||
}
|
||||
|
||||
$this->add($method, $uri, $fullHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用处理器,支持构造函数依赖注入
|
||||
* @param callable|string $callback
|
||||
* @param array $params 传给方法的参数
|
||||
* @param array $dependencies 依赖注入映射,key: 类名,value: 实例
|
||||
*/
|
||||
private function callHandler($callback, $params = [], $dependencies = []) {
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? '未知请求方法';
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '未知请求路径';
|
||||
|
||||
if (is_string($callback) && strpos($callback, '@') !== false) {
|
||||
list($class, $method) = explode('@', $callback);
|
||||
|
||||
if (class_exists($class) && method_exists($class, $method)) {
|
||||
try {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
$instance = null;
|
||||
$constructor = $reflection->getConstructor();
|
||||
|
||||
if ($constructor) {
|
||||
$ctorParams = $constructor->getParameters();
|
||||
$args = [];
|
||||
|
||||
foreach ($ctorParams as $param) {
|
||||
$paramType = $param->getType();
|
||||
if ($paramType && !$paramType->isBuiltin()) {
|
||||
$paramClassName = $paramType->getName();
|
||||
if (isset($dependencies[$paramClassName])) {
|
||||
$args[] = $dependencies[$paramClassName];
|
||||
} elseif ($param->isDefaultValueAvailable()) {
|
||||
$args[] = $param->getDefaultValue();
|
||||
} else {
|
||||
throw new \Exception("依赖注入失败:未提供 {$paramClassName} 实例");
|
||||
}
|
||||
} else {
|
||||
$args[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
|
||||
}
|
||||
}
|
||||
|
||||
$instance = $reflection->newInstanceArgs($args);
|
||||
} else {
|
||||
$instance = new $class();
|
||||
}
|
||||
|
||||
// ⚠️ 此处加入详细异常捕获
|
||||
try {
|
||||
call_user_func_array([$instance, $method], $params);
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo "处理错误:<br>";
|
||||
echo "<strong>" . htmlspecialchars($e->getMessage()) . "</strong><br>";
|
||||
echo "文件:" . $e->getFile() . " 第 " . $e->getLine() . " 行<br>";
|
||||
echo "<pre>" . $e->getTraceAsString() . "</pre>";
|
||||
exit;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo "控制器初始化错误:<br>";
|
||||
echo "<strong>" . htmlspecialchars($e->getMessage()) . "</strong><br>";
|
||||
echo "文件:" . $e->getFile() . " 第 " . $e->getLine() . " 行<br>";
|
||||
echo "<pre>" . $e->getTraceAsString() . "</pre>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(500);
|
||||
echo "处理错误:类 <strong>" . htmlspecialchars($class) . "</strong> 或方法 <strong>" . htmlspecialchars($method) . "</strong> 未找到。<br>";
|
||||
echo "请求方法:<strong>{$requestMethod}</strong><br>";
|
||||
echo "请求路径:<strong>{$requestUri}</strong><br>";
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_callable($callback)) {
|
||||
try {
|
||||
call_user_func_array($callback, $params);
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo "回调执行错误:<br>";
|
||||
echo "<strong>" . htmlspecialchars($e->getMessage()) . "</strong><br>";
|
||||
echo "文件:" . $e->getFile() . " 第 " . $e->getLine() . " 行<br>";
|
||||
echo "<pre>" . $e->getTraceAsString() . "</pre>";
|
||||
exit;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(500);
|
||||
echo "无效的路由处理器。<br>";
|
||||
echo "请求方法:<strong>{$requestMethod}</strong><br>";
|
||||
echo "请求路径:<strong>{$requestUri}</strong><br>";
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,保证统一格式:
|
||||
* - 开头带 /
|
||||
* - 尾部无 /
|
||||
* - 根路径保持 /
|
||||
*/
|
||||
private function normalizePath($path) {
|
||||
$path = trim($path);
|
||||
if ($path === '' || $path === '/') {
|
||||
return '/';
|
||||
}
|
||||
return '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将路由路径转为正则表达式,支持 {param} 动态参数
|
||||
* @param string $routePath
|
||||
* @return string 正则表达式
|
||||
*/
|
||||
private function convertToRegex($routePath) {
|
||||
$pattern = preg_replace('/\{[a-zA-Z0-9_]+\}/', '([^/]+)', $routePath);
|
||||
return '/^' . str_replace('/', '\/', $pattern) . '$/';
|
||||
}
|
||||
}
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
use Db\Database;
|
||||
|
||||
class SettingsHelper {
|
||||
private static $settings = null;
|
||||
|
||||
/**
|
||||
* 获取所有系统设置
|
||||
*/
|
||||
public static function getAll() {
|
||||
if (self::$settings !== null) {
|
||||
return self::$settings;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
// 尝试查询设置(如果表不存在会抛出异常,被catch捕获)
|
||||
try {
|
||||
$settings = $db->select('system_settings', ['setting_key', 'setting_value']);
|
||||
} catch (\Exception $e) {
|
||||
// 表不存在或其他错误,返回默认值
|
||||
self::$settings = self::getDefaults();
|
||||
return self::$settings;
|
||||
}
|
||||
$result = [];
|
||||
|
||||
foreach ($settings as $setting) {
|
||||
$result[$setting['setting_key']] = $setting['setting_value'];
|
||||
}
|
||||
|
||||
// 合并默认值
|
||||
$defaults = self::getDefaults();
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (!isset($result[$key])) {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
self::$settings = $result;
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
// 出错时返回默认值
|
||||
return self::getDefaults();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个设置值
|
||||
*/
|
||||
public static function get($key, $default = '') {
|
||||
$settings = self::getAll();
|
||||
return isset($settings[$key]) ? $settings[$key] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认设置值
|
||||
*/
|
||||
private static function getDefaults() {
|
||||
return [
|
||||
'site_title' => 'PK10 Speed Racing',
|
||||
'site_description' => 'PK10 Speed Racing - Online Betting Platform',
|
||||
'site_keywords' => 'pk10, speed racing, betting, online game',
|
||||
'site_logo' => '/Static/images/logo.png',
|
||||
'site_favicon' => '/Static/css/favicon.ico',
|
||||
'site_copyright' => '© 2025 PK10 Racing. All rights reserved.'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存(当设置更新后调用)
|
||||
*/
|
||||
public static function clearCache() {
|
||||
self::$settings = null;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class WebBaseController extends BaseController {
|
||||
|
||||
protected function render($viewPath, $data = []) {
|
||||
// 如果是绝对路径,直接使用
|
||||
if (strpos($viewPath, '/') === 0 || preg_match('/^[a-zA-Z]:\\\\/', $viewPath)) {
|
||||
$fullPath = $viewPath;
|
||||
} else {
|
||||
// 相对路径,按默认视图目录拼接
|
||||
$viewsDir = __DIR__ . '/../views/';
|
||||
$fullPath = $viewsDir . $viewPath;
|
||||
}
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
throw new Exception("视图文件不存在: $fullPath");
|
||||
}
|
||||
|
||||
extract($data);
|
||||
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
include $fullPath;
|
||||
} else {
|
||||
ob_start();
|
||||
include $fullPath;
|
||||
$Content = ob_get_clean();
|
||||
|
||||
include __DIR__ . '/../views/Web/index.php'; // 主后台模板
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查前台用户登录状态
|
||||
* 如果未登录,重定向到登录页面
|
||||
*/
|
||||
protected function checkWebLogin() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$timeout = isset($_SESSION['web_remember']) && $_SESSION['web_remember'] ? 604800 : 7200; // 记住我:7天,否则2小时
|
||||
|
||||
// 检查是否有用户ID
|
||||
if (!isset($_SESSION['web_user_id'])) {
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取 IP 前三段(IPv4)
|
||||
function get_ip_prefix($ip, $segments = 3) {
|
||||
if (empty($ip)) return '';
|
||||
$parts = explode('.', $ip);
|
||||
if (count($parts) < 4) return $ip; // IPv6 或其他格式,直接返回
|
||||
return implode('.', array_slice($parts, 0, $segments));
|
||||
}
|
||||
|
||||
// 宽松 IP 检查(只比对前三段,例如 192.168.1.xxx)
|
||||
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$sessionIp = $_SESSION['web_ip'] ?? '';
|
||||
|
||||
if (!empty($currentIp) && !empty($sessionIp)) {
|
||||
$current_ip_prefix = get_ip_prefix($currentIp, 3);
|
||||
$session_ip_prefix = get_ip_prefix($sessionIp, 3);
|
||||
|
||||
if ($current_ip_prefix !== $session_ip_prefix) {
|
||||
session_destroy();
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// User Agent 检查
|
||||
$currentUa = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$sessionUa = $_SESSION['web_ua'] ?? '';
|
||||
|
||||
if (!empty($currentUa) && !empty($sessionUa) && $currentUa !== $sessionUa) {
|
||||
session_destroy();
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 会话超时检查
|
||||
$lastActivity = $_SESSION['web_last_activity'] ?? 0;
|
||||
if (time() - $lastActivity > $timeout) {
|
||||
session_destroy();
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新最后活动时间
|
||||
$_SESSION['web_last_activity'] = time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重定向到登录页面
|
||||
*/
|
||||
private function redirectToLogin() {
|
||||
$currentUrl = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$loginUrl = '/login?redirect=' . urlencode($currentUrl);
|
||||
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
// AJAX 请求,返回 JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'redirect' => $loginUrl,
|
||||
'message' => 'Phiên đăng nhập đã hết hạn, vui lòng đăng nhập lại!'
|
||||
]);
|
||||
} else {
|
||||
// 普通请求,重定向
|
||||
header('Location: ' . $loginUrl);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
*/
|
||||
protected function getCurrentUserId() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return $_SESSION['web_user_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户名
|
||||
*/
|
||||
protected function getCurrentUsername() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return $_SESSION['web_username'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class XocDiaAlgorithm implements GameAlgorithmInterface {
|
||||
|
||||
public static function generateResult(): array {
|
||||
$coins = [];
|
||||
for ($i = 0; $i < 4; $i++) $coins[] = (rand(0, 1) === 0) ? 'red' : 'white';
|
||||
return $coins;
|
||||
}
|
||||
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array {
|
||||
if (empty($bets) || empty($waterConfig)) return self::generateResult();
|
||||
$bestResult = null;
|
||||
$bestProfit = PHP_INT_MIN;
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$result = self::generateResult();
|
||||
$profit = self::calculatePlatformProfit($result, $bets);
|
||||
if ($profit > $bestProfit) { $bestProfit = $profit; $bestResult = $result; }
|
||||
}
|
||||
return $bestResult;
|
||||
}
|
||||
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
||||
$redCount = count(array_filter($result, fn($c) => $c === 'red'));
|
||||
switch ($betTarget) {
|
||||
case 'even': return ($redCount % 2 === 0);
|
||||
case 'odd': return ($redCount % 2 === 1);
|
||||
case '4red': return ($redCount === 4);
|
||||
case '4white': return ($redCount === 0);
|
||||
case '3red1white': return ($redCount === 3);
|
||||
case '1red3white': return ($redCount === 1);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float {
|
||||
$totalBet = 0; $totalPayout = 0;
|
||||
foreach ($bets as $bet) {
|
||||
$amount = (float)$bet['amount'];
|
||||
$totalBet += $amount;
|
||||
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'] ?? $bet['bet_value'] ?? '')) {
|
||||
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
||||
}
|
||||
}
|
||||
return $totalBet - $totalPayout;
|
||||
}
|
||||
|
||||
public static function getResultTable(): ?string { return null; }
|
||||
|
||||
public static function formatResultForStorage(array $result, int $periodId): array {
|
||||
$redCount = count(array_filter($result, fn($c) => $c === 'red'));
|
||||
return [
|
||||
'dice1' => ($result[0] === 'red') ? 1 : 0,
|
||||
'dice2' => ($result[1] === 'red') ? 1 : 0,
|
||||
'dice3' => ($result[2] === 'red') ? 1 : 0,
|
||||
'total' => $redCount,
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseResultFromDb(array $row): array {
|
||||
return [
|
||||
((int)($row['dice1'] ?? 0)) ? 'red' : 'white',
|
||||
((int)($row['dice2'] ?? 0)) ? 'red' : 'white',
|
||||
((int)($row['dice3'] ?? 0)) ? 'red' : 'white',
|
||||
// 第4个硬币从 result JSON 恢复,或根据 total 推断
|
||||
'white', // fallback
|
||||
];
|
||||
}
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\SoUrl\Controllers\Admin;
|
||||
|
||||
use App\Core\PluginBaseController;
|
||||
|
||||
class SoUrlController extends PluginBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); // 登录保护
|
||||
|
||||
// 获取配置信息(带默认值,避免空值)
|
||||
$settings = $this->db->get('sourl_settings', '*', [
|
||||
"ORDER" => ["id" => "ASC"],
|
||||
"LIMIT" => 1
|
||||
]) ?: []; // 如果没有数据,返回空数组
|
||||
|
||||
// 处理域名逻辑
|
||||
$currentDomain = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$useDomain = !empty($settings['domain']) ? $settings['domain'] : $currentDomain;
|
||||
$protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
|
||||
$fullDomain = "{$protocol}://{$useDomain}";
|
||||
|
||||
// 传数据给视图(统一封装在data中)
|
||||
$this->renderPluginView('SoUrl', 'Admin/index.php', [
|
||||
'data' => [
|
||||
'settings' => $settings, // 配置信息
|
||||
'domain' => $fullDomain // 带协议的完整域名
|
||||
],
|
||||
'title' => '短链管理中心'
|
||||
]);
|
||||
}
|
||||
|
||||
public function list() {
|
||||
$this->checkLogin(); // 登录保护
|
||||
|
||||
// 获取分页参数
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$pageSize = isset($_GET['page_size']) ? max(1, min(100, intval($_GET['page_size']))) : 10;
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
// 1. 获取总记录数
|
||||
$totalItems = $this->db->count('sourl_list', '*');
|
||||
|
||||
// 2. 获取当前页数据
|
||||
$shortlinks = $this->db->select('sourl_list', '*', [
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => [$offset, $pageSize]
|
||||
]);
|
||||
|
||||
// 3. 计算分页信息
|
||||
$totalPages = max(1, ceil($totalItems / $pageSize));
|
||||
|
||||
// 4. 返回JSON格式数据
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'items' => $shortlinks,
|
||||
'current_page' => $page,
|
||||
'total_pages' => $totalPages,
|
||||
'total_items' => $totalItems,
|
||||
'page_size' => $pageSize,
|
||||
'has_prev' => $page > 1,
|
||||
'has_next' => $page < $totalPages
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function get($id) {
|
||||
$this->checkLogin();
|
||||
// 设置响应内容类型为JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$row = $this->db->get('sourl_list', '*', ['id' => $id]);
|
||||
|
||||
if ($row) {
|
||||
// 成功响应,包含状态和数据
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的ID或短链不存在'
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
public function update() {
|
||||
// 设置JSON响应头
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$this->checkLogin();
|
||||
|
||||
// 获取表单数据
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
|
||||
try {
|
||||
// 有ID则更新
|
||||
if ($id > 0) {
|
||||
$data = [
|
||||
'url' => $url,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'is_active' => $isActive,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
|
||||
$result = $this->db->update('sourl_list', $data, ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
$qrcode = $this->db->get('sourl_list', '*', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '短链更新成功',
|
||||
'data' => $qrcode,
|
||||
'shortUrl' => '/so/' . $qrcode['code']
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '更新失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
|
||||
$insertId = $this->db->insert('sourl_list', [
|
||||
'code' => $code,
|
||||
'url' => $url,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'is_active' => 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($insertId) {
|
||||
$shortUrl = '/so/' . $code;
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '短链创建成功',
|
||||
'data' => [
|
||||
'id' => $insertId,
|
||||
'code' => $code,
|
||||
'url' => $url,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'is_active' => $isActive
|
||||
],
|
||||
'shortUrl' => $shortUrl
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public function settings() {
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = intval($_REQUEST['id'] ?? 0);
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// 获取第一条设置记录
|
||||
$row = $this->db->get('sourl_settings', '*', [
|
||||
"ORDER" => ["id" => "ASC"]
|
||||
]);
|
||||
|
||||
if ($row) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '设置不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 获取提交数据
|
||||
$domain = trim($_POST['domain'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
/* if (empty($domain)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '域名不能为空'
|
||||
]);
|
||||
exit;
|
||||
}*/
|
||||
|
||||
if ($id > 0) {
|
||||
// 更新
|
||||
$data = [
|
||||
'domain' => $domain,
|
||||
'is_active' => $isActive,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$result = $this->db->update('sourl_settings', $data, ['id' => $id]);
|
||||
|
||||
if ($result->rowCount() > 0) {
|
||||
$setting = $this->db->get('sourl_settings', '*', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置更新成功',
|
||||
'data' => $setting
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '更新失败或数据无变化'
|
||||
]);
|
||||
}
|
||||
|
||||
} else {
|
||||
// 新增
|
||||
$insertId = $this->db->insert('sourl_settings', [
|
||||
'domain' => $domain,
|
||||
'is_active' =>1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($insertId) {
|
||||
$setting = $this->db->get('sourl_settings', '*');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置创建成功',
|
||||
'data' => $setting
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
http_response_code(405);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '只支持 GET 和 POST 请求'
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
public function deletelink($id) {
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 检查是否存在
|
||||
$row = $this->db->get('sourl_list', '*', ['id' => $id]);
|
||||
if (!$row) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '要删除的短链不存在'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
$result = $this->db->delete('sourl_list', ['id' => $id]);
|
||||
|
||||
if ($result->rowCount() > 0) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '短链已删除'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败,请稍后再试'
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\SoUrl\Controllers\Web;
|
||||
|
||||
use App\Core\WebBaseController;
|
||||
|
||||
class SoUrlController extends WebBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
}
|
||||
|
||||
|
||||
public function redirect($code) {
|
||||
$row = $this->db->get('sourl_list', '*', ['code' => $code]);
|
||||
|
||||
if ($row) {
|
||||
// 检查链接是否处于激活状态
|
||||
if ($row['is_active'] != 1) {
|
||||
$this->showError($code . ' 此链接已被停用!');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 若激活,则更新访问量并跳转
|
||||
$update = $this->db->update('sourl_list', ['views[+]' => 1 ], ['id' => $row['id'] ]);
|
||||
if ($update->rowCount() > 0) {
|
||||
header("Location: " . $row['url']);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$this->showError( $code . ' 此链接不存在!');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Executable
+596
@@ -0,0 +1,596 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-chain text-primary mr-3"></i>
|
||||
短链管理中心
|
||||
</h1>
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<!-- 消息提示框 -->
|
||||
<div id="message" class="mb-4 px-4 py-3 rounded-lg hidden"></div>
|
||||
<!-- 选项卡导航 -->
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<ul class="flex flex-wrap -mb-px" id="tabs" role="tablist">
|
||||
<li class="mr-2" role="presentation">
|
||||
<button id="list-tab" class="inline-block py-4 px-5 border-b-2 border-primary text-sm font-medium text-primary" onclick="switchTab('list')" aria-selected="true">
|
||||
短链列表
|
||||
</button>
|
||||
</li>
|
||||
<li class="mr-2" role="presentation">
|
||||
<button id="settings-tab" class="inline-block py-4 px-5 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300" onclick="switchTab('settings')" aria-selected="false">
|
||||
系统设置
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 短链列表内容 -->
|
||||
<div id="list-content" class="tab-content">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700">短链列表</h2>
|
||||
<!-- 创建新短链按钮 -->
|
||||
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>新增短链</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名称</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">短码</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">跳转链接</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">访问</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="shortlinkList">
|
||||
<!-- 内容将通过JavaScript动态生成 -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="loading" class="hidden py-10 text-center"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>
|
||||
<div id="empty" class="hidden">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-12">没有找到短链</td>
|
||||
</tr>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="pagination" class="flex justify-between items-center mt-6 hidden">
|
||||
<div class="text-sm text-gray-500">
|
||||
显示 <span id="showingRange">0-0</span> 条,共 <span id="totalItems">0</span> 条
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="prevPage" class="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50" disabled>上一页</button>
|
||||
<div id="pageNumbers" class="flex space-x-1"></div>
|
||||
<button id="nextPage" class="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50" disabled>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设置选项卡内容 -->
|
||||
<div id="settings-content" class="tab-content hidden">
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-6">系统设置</h2>
|
||||
<form id="settingsForm" class="space-y-6">
|
||||
<input type="hidden" id="settingsId" name="id">
|
||||
<div class="bg-gray-50 p-5 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
|
||||
<i class="fa fa-link text-primary mr-2"></i>短链接设置
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="default_domain" class="block text-sm font-medium text-gray-700 mb-1">默认域名</label>
|
||||
<input type="text" id="default_domain" name="domain" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="例如: https://t.cn 如果为空则为当前主域名">
|
||||
<p class="mt-1 text-xs text-gray-500">域名需要解析到当前网站才可使用</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200">
|
||||
<button type="button" id="resetSettingsBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
重置
|
||||
</button>
|
||||
<button type="button" id="saveSettingsBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表单弹窗背景 -->
|
||||
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
|
||||
<!-- 短链表单弹窗 -->
|
||||
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-plus-circle text-primary mr-2"></i>
|
||||
创建新短链
|
||||
</h3>
|
||||
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="shortlinkForm" class="space-y-5">
|
||||
<input type="hidden" id="shortlinkId" name="id">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">短链名称 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="name" name="name" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="请输入短链名称">
|
||||
</div>
|
||||
<div>
|
||||
<label for="url" class="block text-sm font-medium text-gray-700 mb-1">目标链接 <span class="text-red-500">*</span></label>
|
||||
<!-- 移除 textarea 的 type 属性,因为它不适用 -->
|
||||
<textarea id="url" name="url" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition transition-colors resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-700 mb-1">自定义短码(可选)</label>
|
||||
<div class="flex">
|
||||
<span class="inline-flex items-center px-3 rounded-l-lg border border-r-0 border-gray-300 bg-gray-50 text-gray-500">
|
||||
/so/
|
||||
</span>
|
||||
<input type="text" id="code" name="code" class="flex-1 px-4 py-2 border border-gray-300 rounded-r-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="留空则自动生成">
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">仅支持字母、数字和短横线,不超过20个字符</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">描述(可选)</label>
|
||||
<textarea id="description" name="description" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-none" placeholder="请输入短链描述信息"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<!-- 移除默认的checked属性,避免覆盖JS设置的状态 -->
|
||||
<input type="checkbox" id="is_active" name="is_active" value="1" class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用状态</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="submitBtn" type="button" class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存短链
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
// 分页相关功能
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 1,
|
||||
totalPages = 1;
|
||||
|
||||
// 分页DOM元素
|
||||
const listEl = document.getElementById('shortlinkList');
|
||||
const [paginationEl, prevBtn, nextBtn, pageNumbers] = ['pagination', 'prevPage', 'nextPage', 'pageNumbers'].map(id => document.getElementById(id));
|
||||
const [rangeEl, totalEl, loadingEl, emptyEl] = ['showingRange', 'totalItems', 'loading', 'empty'].map(id => document.getElementById(id));
|
||||
|
||||
// 初始化分页
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadPage(1);
|
||||
prevBtn.onclick = () => currentPage > 1 && loadPage(currentPage - 1);
|
||||
nextBtn.onclick = () => currentPage < totalPages && loadPage(currentPage + 1);
|
||||
});
|
||||
|
||||
// 加载分页数据
|
||||
async function loadPage(page) {
|
||||
// 显示加载状态
|
||||
loadingEl.classList.remove('hidden');
|
||||
listEl.innerHTML = '';
|
||||
paginationEl.classList.add('hidden');
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// 请求数据
|
||||
const res = await fetch(`/admin/sourl/list?page=${page}&page_size=${PAGE_SIZE}`);
|
||||
const { data } = await res.json();
|
||||
// 更新分页信息
|
||||
currentPage = data.current_page;
|
||||
totalPages = data.total_pages;
|
||||
totalEl.textContent = data.total_items;
|
||||
|
||||
// 渲染列表
|
||||
if (data.items.length) {
|
||||
data.items.forEach(link => {
|
||||
const statusClass = link.is_active == 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
const statusText = link.is_active == 1 ? '启用' : '停用';
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-gray-50 transition-colors';
|
||||
tr.innerHTML = `
|
||||
<!-- 短链信息单元格 - 包含名称和描述 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(link.name ?? '未命名')}</div>
|
||||
<div class="text-xs text-gray-500 truncate max-w-xs">
|
||||
${escapeHtml(link.description ?? '无描述')}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 短码单元格 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<button class="copy-code-btn text-primary hover:text-primary/80 hover:underline font-medium text-sm flex items-center"
|
||||
data-code="${link.code}"
|
||||
title="点击或按Enter复制短码"
|
||||
tabindex="0"
|
||||
onclick="copyToClipboard('<?= htmlspecialchars($data['domain'] ?? '', ENT_QUOTES) ?>/so/${escapeHtml(link.code)}', '短码')"
|
||||
<span>${escapeHtml(link.code)}</span>
|
||||
<i class="fa fa-copy ml-1 opacity-70 text-primary"></i>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<!-- 跳转链接 - 小屏幕隐藏 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<div class="text-sm text-gray-500 truncate max-w-xs">
|
||||
${escapeHtml(link.url)}
|
||||
</div>
|
||||
</td>
|
||||
<!-- 状态 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<!-- 访问计数 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">
|
||||
${link.views} 次
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button class="edit-btn text-gray-500 hover:text-blue-500"
|
||||
data-id="${link.id}" title="编辑">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button class="delete-btn text-gray-500 hover:text-red-500"
|
||||
data-id="${link.id}" title="删除">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
listEl.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
listEl.innerHTML = `<tr><td colspan="5" class="text-center py-12">没有查到短链;请创建后查看!</td></tr>`;
|
||||
}
|
||||
|
||||
// 更新分页控件
|
||||
rangeEl.textContent = `${(page-1)*PAGE_SIZE+1}-${Math.min(page*PAGE_SIZE, data.total_items)}`;
|
||||
renderPageNumbers();
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
paginationEl.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<tr><td colspan="5" class="text-center py-12">加载失败: ${e.message}</td></tr>`;
|
||||
} finally {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染页码按钮
|
||||
function renderPageNumbers() {
|
||||
pageNumbers.innerHTML = '';
|
||||
const start = Math.max(1, currentPage - 2);
|
||||
const end = Math.min(totalPages, start + 4);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `px-3 py-1 rounded ${i === currentPage ? 'bg-primary text-white' : 'border'}`;
|
||||
btn.textContent = i;
|
||||
btn.onclick = () => loadPage(i);
|
||||
pageNumbers.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义函数
|
||||
function escapeHtml(str) {
|
||||
return str ? str.toString().replace(/[&<>"']/g, c => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
} [c])) : '';
|
||||
}
|
||||
|
||||
function generateCode(length = 8) {
|
||||
const chars = '1234567890ACDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let code = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
// 选项卡切换功能
|
||||
function switchTab(tabName) {
|
||||
document.getElementById('list-content').classList.add('hidden');
|
||||
document.getElementById('settings-content').classList.add('hidden');
|
||||
|
||||
document.getElementById('list-tab').classList.remove('border-primary', 'text-primary');
|
||||
document.getElementById('list-tab').classList.add('border-transparent', 'text-gray-500');
|
||||
document.getElementById('settings-tab').classList.remove('border-primary', 'text-primary');
|
||||
document.getElementById('settings-tab').classList.add('border-transparent', 'text-gray-500');
|
||||
|
||||
document.getElementById(`${tabName}-content`).classList.remove('hidden');
|
||||
document.getElementById(`${tabName}-tab`).classList.remove('border-transparent', 'text-gray-500');
|
||||
document.getElementById(`${tabName}-tab`).classList.add('border-primary', 'text-primary');
|
||||
|
||||
if (tabName === 'settings' && !window.settingsLoaded) {
|
||||
loadSettings();
|
||||
window.settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 主功能逻辑
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const formModal = document.getElementById('formModal');
|
||||
const formBackdrop = document.getElementById('formBackdrop');
|
||||
const openFormBtn = document.getElementById('openFormBtn');
|
||||
const closeFormBtn = document.getElementById('closeFormBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const formTitle = document.getElementById('formTitle');
|
||||
const shortlinkForm = document.getElementById('shortlinkForm');
|
||||
const shortlinkList = document.getElementById('shortlinkList');
|
||||
const settingsForm = document.getElementById('settingsForm');
|
||||
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||
window.settingsLoaded = false;
|
||||
|
||||
// 检查必要元素
|
||||
function checkElements() {
|
||||
const elements = [formModal, formBackdrop, openFormBtn, closeFormBtn, cancelBtn, submitBtn];
|
||||
const missing = elements.filter(el => !el);
|
||||
if (missing.length > 0) {
|
||||
console.error('缺少必要的DOM元素,弹窗功能无法正常工作');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 打开表单弹窗
|
||||
function openFormModal() {
|
||||
if (!checkElements()) return;
|
||||
resetForm();
|
||||
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
void formModal.offsetWidth; // 强制重绘
|
||||
document.getElementById('code').value = generateCode();
|
||||
}
|
||||
|
||||
// 关闭表单弹窗
|
||||
function closeFormModal() {
|
||||
if (!checkElements()) return;
|
||||
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
shortlinkForm.reset();
|
||||
document.getElementById('shortlinkId').value = '';
|
||||
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新短链';
|
||||
submitBtn.innerHTML = '保存短链';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
window.loadSettings = async function() {
|
||||
try {
|
||||
const response = await fetch('/admin/sourl/settings');
|
||||
if (!response.ok) throw new Error('获取设置失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const settings = data.data;
|
||||
document.getElementById('settingsId').value = settings.id || '';
|
||||
document.getElementById('default_domain').value = settings.domain || '';
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm(formElement) {
|
||||
if (formElement.id === 'shortlinkForm') {
|
||||
const name = formElement.querySelector('#name').value.trim();
|
||||
const url = formElement.querySelector('#url').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showMessage('请输入短链名称', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
showMessage('请输入目标链接', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 简单URL验证
|
||||
const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/;
|
||||
if (!urlPattern.test(url)) {
|
||||
showMessage('请输入有效的URL地址', 'error');
|
||||
return false;
|
||||
}
|
||||
} else if (formElement.id === 'settingsForm') {
|
||||
// 设置表单验证
|
||||
const domain = formElement.querySelector('#default_domain').value.trim();
|
||||
if (!domain) {
|
||||
showMessage('请输入默认域名', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
async function submitFormData(url, formElement, successMsg) {
|
||||
if (!validateForm(formElement)) return;
|
||||
|
||||
const submitButton = formElement.id === 'shortlinkForm' ? submitBtn : document.getElementById('saveSettingsBtn');
|
||||
const originalText = submitButton.innerHTML;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(formElement);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'info');
|
||||
closeFormModal();
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
throw new Error(data.message || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载短链数据(编辑用)
|
||||
async function loadShortlinkData(id) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/sourl/get/${id}`);
|
||||
if (!response.ok) throw new Error('获取数据失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { id, name, url, code, description, is_active } = data.data;
|
||||
document.getElementById('shortlinkId').value = id;
|
||||
document.getElementById('name').value = name || '';
|
||||
document.getElementById('url').value = url || '';
|
||||
document.getElementById('code').value = code || '';
|
||||
document.getElementById('description').value = description || '';
|
||||
document.getElementById('is_active').checked = Boolean(Number(is_active));
|
||||
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑短链';
|
||||
} else {
|
||||
throw new Error(data.message || '获取数据失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
closeFormModal();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存短链';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除短链
|
||||
async function deleteLink(id) {
|
||||
if (!confirm('确定要删除该短链吗?此操作不可恢复!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/sourl/delete/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage('短链已删除', 'info');
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
if (row) {
|
||||
// 添加删除动画
|
||||
row.classList.add('opacity-0', 'transform', 'translate-x-4', 'transition-all', 'duration-300');
|
||||
setTimeout(() => {
|
||||
row.remove();
|
||||
const rows = shortlinkList.querySelectorAll('tr:not(:last-child)');
|
||||
if (rows.length === 0) {
|
||||
shortlinkList.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fa fa-link text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">没有找到短链</h3>
|
||||
<p class="mt-1 text-gray-500">尝试调整筛选条件或添加新短链</p>
|
||||
<button class="mt-4 bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center" id="openFormBtn">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>新增短链</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
if (checkElements()) {
|
||||
// 打开表单
|
||||
openFormBtn.addEventListener('click', openFormModal);
|
||||
|
||||
// 关闭表单
|
||||
closeFormBtn.addEventListener('click', closeFormModal);
|
||||
cancelBtn.addEventListener('click', closeFormModal);
|
||||
formBackdrop.addEventListener('click', closeFormModal);
|
||||
|
||||
// 短链表单提交
|
||||
submitBtn.addEventListener('click', function() {
|
||||
submitFormData('/admin/sourl/update', shortlinkForm, '短链保存成功');
|
||||
});
|
||||
|
||||
// 设置表单提交
|
||||
saveSettingsBtn.addEventListener('click', function() {
|
||||
submitFormData('/admin/sourl/settings', settingsForm, '设置保存成功');
|
||||
});
|
||||
|
||||
// 列表操作事件委托
|
||||
shortlinkList.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.edit-btn');
|
||||
const deleteBtn = e.target.closest('.delete-btn');
|
||||
|
||||
if (editBtn) {
|
||||
const id = editBtn.getAttribute('data-id');
|
||||
if (id) {
|
||||
openFormModal();
|
||||
setTimeout(() => loadShortlinkData(id), 300);
|
||||
}
|
||||
} else if (deleteBtn) {
|
||||
const id = deleteBtn.getAttribute('data-id');
|
||||
if (id) deleteLink(id);
|
||||
}
|
||||
});
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && !formModal.classList.contains('invisible')) {
|
||||
closeFormModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止表单默认提交
|
||||
shortlinkForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: SoUrl
|
||||
* Description: 用来缩短网址链接或者说是跳转到新地址的插件。
|
||||
* Version: 1.0.0
|
||||
* Author: JuheDev
|
||||
* Plugin URL: https://plugins.juhe.me/sourl
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'menus' => [
|
||||
[
|
||||
'title' => '缩短链接',
|
||||
'icon' => 'fa fa-chain',
|
||||
'path' => '/admin/sourl',
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'route_group' => [
|
||||
[
|
||||
'prefix' => '/so',
|
||||
'namespace' => 'Plugins\SoUrl\Controllers\Web',
|
||||
'routes' => [
|
||||
['GET', '/{code}', 'SoUrlController@redirect'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'prefix' => '/admin/sourl',
|
||||
'namespace' => 'Plugins\SoUrl\Controllers\Admin',
|
||||
'routes' => [
|
||||
['GET', '/', 'SoUrlController@index'],
|
||||
['GET', '/get/{id}', 'SoUrlController@get'],
|
||||
['GET', '/list', 'SoUrlController@list'],
|
||||
['GET', '/delete/{id}', 'SoUrlController@deletelink'],
|
||||
['POST', '/update', 'SoUrlController@update'],
|
||||
['GET|POST', '/settings', 'SoUrlController@settings'],
|
||||
],
|
||||
],
|
||||
],
|
||||
// 插件所需表
|
||||
'tables' => ['sourl_list', 'sourl_settings'],
|
||||
'init' => function () {
|
||||
// 插件初始化,可选
|
||||
// require_once __DIR__ . '/helpers.php';
|
||||
},
|
||||
|
||||
'activate' => function ($db) {
|
||||
$db->query("
|
||||
CREATE TABLE IF NOT EXISTS `sourl_list` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(64) NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`name` varchar(255) NOT NULL DEFAULT '',
|
||||
`description` text,
|
||||
`views` int(6) DEFAULT 0,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT '1',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
$db->query("
|
||||
CREATE TABLE IF NOT EXISTS `sourl_settings` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`domain` text NOT NULL,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT '1',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
},
|
||||
|
||||
'deactivate' => function ($db) {
|
||||
// 插件被停用时执行
|
||||
},
|
||||
|
||||
];
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
namespace Plugins\WxGCode\Controllers\Admin;
|
||||
use App\Core\PluginBaseController;
|
||||
|
||||
class WxGCodeController extends PluginBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); // 登录保护
|
||||
|
||||
// 获取配置信息(带默认值,避免空值)
|
||||
$settings = $this->db->get('wxgcode_settings', '*') ?: []; // 如果没有数据,返回空数组
|
||||
|
||||
|
||||
|
||||
// 传数据给视图(统一封装在data中)
|
||||
$this->renderPluginView('WxGCode', 'Admin/index.php', [
|
||||
'data' => [
|
||||
'settings' => '', // 配置信息
|
||||
'domain' => '' // 带协议的完整域名
|
||||
],
|
||||
'title' => '扫一扫管理中心'
|
||||
]);
|
||||
}
|
||||
|
||||
public function list() {
|
||||
$this->checkLogin(); // 登录保护
|
||||
|
||||
// 获取分页参数
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$pageSize = isset($_GET['page_size']) ? max(1, min(100, intval($_GET['page_size']))) : 10;
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
// 1. 获取总记录数
|
||||
$totalItems = $this->db->count('wxgcode_list', '*');
|
||||
|
||||
// 2. 获取当前页数据
|
||||
$shortlinks = $this->db->select('wxgcode_list', '*', [
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => [$offset, $pageSize]
|
||||
]);
|
||||
|
||||
// 3. 计算分页信息
|
||||
$totalPages = max(1, ceil($totalItems / $pageSize));
|
||||
|
||||
// 4. 返回JSON格式数据
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'items' => $shortlinks,
|
||||
'current_page' => $page,
|
||||
'total_pages' => $totalPages,
|
||||
'total_items' => $totalItems,
|
||||
'page_size' => $pageSize,
|
||||
'has_prev' => $page > 1,
|
||||
'has_next' => $page < $totalPages
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function get($id) {
|
||||
$this->checkLogin();
|
||||
// 设置响应内容类型为JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$row = $this->db->get('wxgcode_list', '*', ['id' => $id]);
|
||||
|
||||
if ($row) {
|
||||
// 成功响应,包含状态和数据
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的ID或记录不存在'
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
public function update() {
|
||||
// 设置JSON响应头
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$this->checkLogin();
|
||||
|
||||
// 获取表单数据(使用前端页面对应的字段名)
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$wx_group_name = trim($_POST['wx_group_name'] ?? '');
|
||||
$qrcode_url = trim($_POST['qrcode_url'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$max_scans = intval($_POST['max_scans'] ?? 0);
|
||||
$max_members = intval($_POST['max_members'] ?? 0);
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$status = isset($_POST['status']) ? 1 : 0;
|
||||
|
||||
|
||||
try {
|
||||
// 数据验证
|
||||
if (empty($name)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请输入活码名称'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($wx_group_name)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请输入微信群名称'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($qrcode_url)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '请输入群二维码URL'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($code)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '活码编码不能为空'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 有ID则更新
|
||||
if ($id > 0) {
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'wx_group_name' => $wx_group_name,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'code' => $code,
|
||||
'max_scans' => $max_scans,
|
||||
'max_members' => $max_members,
|
||||
'description' => $description,
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$result = $this->db->update('wxgcode_list', $data, ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
$qrcode = $this->db->get('wxgcode_list', '*', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '更新成功',
|
||||
'data' => $qrcode
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '更新失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
// 无ID则新增
|
||||
else {
|
||||
// 检查编码是否已存在
|
||||
$exists = $this->db->has('wxgcode_list', ['code' => $code]);
|
||||
if ($exists) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '活码编码已存在,请更换'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$insertId = $this->db->insert('wxgcode_list', [
|
||||
'name' => $name,
|
||||
'wx_group_name' => $wx_group_name,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'code' => $code,
|
||||
'max_scans' => $max_scans,
|
||||
'max_members' => $max_members,
|
||||
'description' => $description,
|
||||
'status' => $status,
|
||||
'total_views' => 0, // 对应前端显示的访问量
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($insertId) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '创建成功',
|
||||
'data' => [
|
||||
'id' => $insertId,
|
||||
'name' => $name,
|
||||
'wx_group_name' => $wx_group_name,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'code' => $code,
|
||||
'max_scans' => $max_scans,
|
||||
'max_members' => $max_members,
|
||||
'description' => $description,
|
||||
'status' => $status,
|
||||
'total_views' => 0
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public function settings() {
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = intval($_REQUEST['id'] ?? 0);
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// 获取第一条设置记录
|
||||
$row = $this->db->get('wxgcode_settings', '*');
|
||||
|
||||
if ($row) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '设置不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 获取提交数据
|
||||
$wechat_name = trim($_POST['wechat_name'] ?? '');
|
||||
$wechat_account = trim($_POST['wechat_account'] ?? '');
|
||||
$appid = trim($_POST['appid'] ?? '');
|
||||
$appsecret = trim($_POST['appsecret'] ?? '');
|
||||
$token = trim($_POST['token'] ?? '');
|
||||
$encoding_aes_key = trim($_POST['encoding_aes_key'] ?? '');
|
||||
$qrcode_url = trim($_POST['qrcode_url'] ?? '');
|
||||
$wechat_type = trim($_POST['wechat_type'] ?? 'service');
|
||||
$status = isset($_POST['status']) ? 1 : 0;
|
||||
|
||||
// 验证必填项
|
||||
if (empty($wechat_name) || empty($wechat_account) || empty($appid) || empty($appsecret)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '公众号名称、原始ID、AppID和AppSecret为必填项'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
// 更新
|
||||
$data = [
|
||||
'wechat_name' => $wechat_name,
|
||||
'wechat_account' => $wechat_account,
|
||||
'appid' => $appid,
|
||||
'appsecret' => $appsecret,
|
||||
'token' => $token,
|
||||
'encoding_aes_key' => $encoding_aes_key,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'wechat_type' => $wechat_type,
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$result = $this->db->update('wxgcode_settings', $data, ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
$setting = $this->db->get('wxgcode_settings', '*', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置更新成功',
|
||||
'data' => $setting
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '更新失败或数据无变化'
|
||||
]);
|
||||
}
|
||||
|
||||
} else {
|
||||
// 新增
|
||||
$insertId = $this->db->insert('wxgcode_settings', [
|
||||
'wechat_name' => $wechat_name,
|
||||
'wechat_account' => $wechat_account,
|
||||
'appid' => $appid,
|
||||
'appsecret' => $appsecret,
|
||||
'token' => $token,
|
||||
'encoding_aes_key' => $encoding_aes_key,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'wechat_type' => $wechat_type,
|
||||
'status' => 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($insertId) {
|
||||
$setting = $this->db->get('wxgcode_settings', '*');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置创建成功',
|
||||
'data' => $setting
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
http_response_code(405);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '只支持 GET 和 POST 请求'
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 检查是否存在
|
||||
$row = $this->db->get('wxgcode_list', '*', ['id' => $id]);
|
||||
if (!$row) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '要删除的记录不存在'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
$result = $this->db->delete('wxgcode_list', ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '记录已删除'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败,请稍后再试'
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
namespace Plugins\WxGCode\Controllers\Web;
|
||||
use App\Core\WebBaseController;
|
||||
|
||||
class WxGCodeController extends WebBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
}
|
||||
|
||||
public function index($code) {
|
||||
|
||||
$code = $code;
|
||||
include __DIR__ . '/../../Views/Web/index.php';
|
||||
}
|
||||
public function get($code) {
|
||||
// 设置响应内容类型为JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// 验证code格式 (假设code是字母数字组合)
|
||||
if (empty($code) || !preg_match('/^[A-Za-z0-9]+$/', $code)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的活码编码'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 查询有效的活码记录 (只查询启用状态的)
|
||||
$row = $this->db->get('wxgcode_list', '*', [
|
||||
'AND' => [
|
||||
'code' => $code,
|
||||
'status' => 1 // 只显示启用状态的活码
|
||||
]
|
||||
]);
|
||||
|
||||
if ($row) {
|
||||
// 记录访问量
|
||||
$this->increaseViewCount($row['id']);
|
||||
|
||||
// 检查是否需要切换到备用活码 (如果当前活码达到最大扫码次数)
|
||||
if ($row['max_scans'] > 0 && $row['total_scans'] >= $row['max_scans'] && !empty($row['backup_id'])) {
|
||||
$backupRow = $this->db->get('wxgcode_list', '*', [
|
||||
'AND' => [
|
||||
'id' => $row['backup_id'],
|
||||
'status' => 1
|
||||
]
|
||||
]);
|
||||
if ($backupRow) {
|
||||
$row = $backupRow;
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数据
|
||||
if (isset($row['created_at'])) {
|
||||
$row['created_at'] = date('Y-m-d', strtotime($row['created_at']));
|
||||
}
|
||||
|
||||
// 返回活码信息
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '活码不存在或已被禁用'
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* 增加活码访问量
|
||||
* @param int $id 活码ID
|
||||
*/
|
||||
protected function increaseViewCount($id) {
|
||||
// 增加扫码次数
|
||||
$this->db->update('wxgcode_list', [
|
||||
'total_views[+]' => 1
|
||||
], ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新二维码
|
||||
* @param string $code 活码编码
|
||||
*/
|
||||
public function refreshQrcode($code) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$row = $this->db->get('wxgcode_list', ['id', 'qrcode_url', 'code'], [
|
||||
'AND' => [
|
||||
'code' => $code,
|
||||
'status' => 1
|
||||
]
|
||||
]);
|
||||
|
||||
if ($row) {
|
||||
// 这里可以添加调用微信接口生成新二维码的逻辑
|
||||
// 示例:$newQrcodeUrl = $this->generateNewQrcode($row['id']);
|
||||
|
||||
// 简单模拟刷新(实际项目中应替换为真实逻辑)
|
||||
$newQrcodeUrl = $row['qrcode_url'] . '?t=' . time();
|
||||
|
||||
// 更新数据库中的二维码URL
|
||||
$this->db->update('wxgcode_list', [
|
||||
'qrcode_url' => $newQrcodeUrl,
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
], ['id' => $row['id']]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'qrcode_url' => $newQrcodeUrl
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '刷新失败,活码不存在或已被禁用'
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php exit();?>{"expire_time":1754852111,"access_token":"95_rXIZ_RT-sfFba-lFUL3IyhSMbDb32bqoFpYzJCYhxLI-7HzWv6v6GqxIfn2V_8qJ1nkGFA-9RtcLNdiEIWHGwNFuuUZntG8CM_OmB4F2tb0teS2QI8EArS4VlOAZRSbAEANFG"}
|
||||
@@ -0,0 +1 @@
|
||||
<?php exit();?>{"expire_time":1754852112,"jsapi_ticket":"LIKLckvwlJT9cWIhEQTwfMAhJFYw3_TwrJw6wWpdGhRHwl7AqiNprXTimrsImS13T-xXctQR4na76SuT9Pkxgg"}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
namespace Plugins\WxGCode\Controllers\lib;
|
||||
class JSSDK {
|
||||
private $appId;
|
||||
private $appSecret;
|
||||
// 缓存文件路径(使用绝对路径避免问题)
|
||||
private $cacheDir;
|
||||
|
||||
public function __construct($appId, $appSecret) {
|
||||
$this->appId = $appId;
|
||||
$this->appSecret = $appSecret;
|
||||
// 初始化缓存目录(与jssdk.php同目录)
|
||||
$this->cacheDir = dirname(__FILE__) . '/';
|
||||
// 确保缓存目录可写
|
||||
$this->checkCacheDir();
|
||||
}
|
||||
|
||||
// 检查缓存目录是否存在且可写
|
||||
private function checkCacheDir() {
|
||||
if (!is_dir($this->cacheDir)) {
|
||||
mkdir($this->cacheDir, 0755, true);
|
||||
}
|
||||
if (!is_writable($this->cacheDir)) {
|
||||
throw new \Exception("缓存目录不可写:{$this->cacheDir}");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并创建缓存文件
|
||||
private function checkCacheFile($filename) {
|
||||
$filePath = $this->cacheDir . $filename;
|
||||
// 如果文件不存在则创建并初始化
|
||||
if (!file_exists($filePath)) {
|
||||
$initialData = json_encode([
|
||||
'expire_time' => 0,
|
||||
'access_token' => '',
|
||||
'jsapi_ticket' => ''
|
||||
]);
|
||||
$this->set_php_file($filename, $initialData);
|
||||
}
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
public function getSignPackage() {
|
||||
$jsapiTicket = $this->getJsApiTicket();
|
||||
|
||||
// 动态获取当前URL
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
|
||||
|
||||
$timestamp = time();
|
||||
$nonceStr = $this->createNonceStr();
|
||||
|
||||
// 按ASCII码升序排序
|
||||
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
|
||||
$signature = sha1($string);
|
||||
|
||||
return [
|
||||
"appId" => $this->appId,
|
||||
"nonceStr" => $nonceStr,
|
||||
"timestamp" => $timestamp,
|
||||
"url" => $url,
|
||||
"signature" => $signature,
|
||||
"rawString" => $string
|
||||
];
|
||||
}
|
||||
|
||||
private function createNonceStr($length = 16) {
|
||||
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
$str = "";
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
private function getJsApiTicket() {
|
||||
// 先检查并创建缓存文件
|
||||
$this->checkCacheFile("jsapi_ticket.php");
|
||||
|
||||
$data = json_decode($this->get_php_file("jsapi_ticket.php"));
|
||||
|
||||
// 处理空数据或过期情况
|
||||
if (empty($data) || $data->expire_time < time()) {
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
|
||||
if (isset($res->ticket)) {
|
||||
$data = new \stdClass(); // 初始化空对象
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->jsapi_ticket = $res->ticket;
|
||||
$this->set_php_file("jsapi_ticket.php", json_encode($data));
|
||||
} else {
|
||||
throw new \Exception("获取jsapi_ticket失败: " . json_encode($res));
|
||||
}
|
||||
}
|
||||
|
||||
return $data->jsapi_ticket ?? '';
|
||||
}
|
||||
|
||||
private function getAccessToken() {
|
||||
// 先检查并创建缓存文件
|
||||
$this->checkCacheFile("access_token.php");
|
||||
|
||||
$data = json_decode($this->get_php_file("access_token.php"));
|
||||
|
||||
// 处理空数据或过期情况
|
||||
if (empty($data) || $data->expire_time < time()) {
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
|
||||
if (isset($res->access_token)) {
|
||||
$data = new \stdClass(); // 初始化空对象
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->access_token = $res->access_token;
|
||||
$this->set_php_file("access_token.php", json_encode($data));
|
||||
} else {
|
||||
throw new \Exception("获取access_token失败: " . json_encode($res));
|
||||
}
|
||||
}
|
||||
|
||||
return $data->access_token ?? '';
|
||||
}
|
||||
|
||||
private function httpGet($url) {
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, true);
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
$res = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function get_php_file($filename) {
|
||||
$filePath = $this->cacheDir . $filename;
|
||||
return trim(substr(file_get_contents($filePath), 15));
|
||||
}
|
||||
|
||||
private function set_php_file($filename, $content) {
|
||||
$filePath = $this->cacheDir . $filename;
|
||||
$fp = fopen($filePath, "w");
|
||||
fwrite($fp, "<?php exit();?>" . $content);
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
Executable
+754
@@ -0,0 +1,754 @@
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-qrcode text-primary mr-3"></i>
|
||||
微信群活码管理中心
|
||||
</h1>
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<!-- 消息提示框 -->
|
||||
<div id="message" class="mb-4 px-4 py-3 rounded-lg hidden"></div>
|
||||
<!-- 选项卡导航 -->
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<ul class="flex flex-wrap -mb-px" id="tabs" role="tablist">
|
||||
<li class="mr-2" role="presentation">
|
||||
<button id="list-tab" class="inline-block py-4 px-5 border-b-2 border-primary text-sm font-medium text-primary" onclick="switchTab('list')" aria-selected="true">
|
||||
活码列表
|
||||
</button>
|
||||
</li>
|
||||
<li class="mr-2" role="presentation">
|
||||
<button id="settings-tab" class="inline-block py-4 px-5 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300" onclick="switchTab('settings')" aria-selected="false">
|
||||
系统设置
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 活码列表内容 -->
|
||||
<div id="list-content" class="tab-content">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700">活码列表</h2>
|
||||
<!-- 创建新活码按钮 -->
|
||||
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>新增群活码</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">活码名称</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">活码编码</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">微信群名称</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">访问量</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="shortlinkList">
|
||||
<!-- 内容将通过JavaScript动态生成 -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="loading" class="hidden py-10 text-center"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>
|
||||
<div id="empty" class="hidden">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-12">没有找到活码</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页和状态控件 -->
|
||||
<div id="pagination" class="flex justify-between items-center mt-6 hidden">
|
||||
<div class="text-sm text-gray-500">
|
||||
显示 <span id="showingRange">0-0</span> 条,共 <span id="totalItems">0</span> 条
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="prevPage" class="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50" disabled>上一页</button>
|
||||
<div id="pageNumbers" class="flex space-x-1"></div>
|
||||
<button id="nextPage" class="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50" disabled>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设置选项卡内容 -->
|
||||
<div id="settings-content" class="tab-content hidden">
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-6">系统设置</h2>
|
||||
<form id="settingsForm" class="space-y-6">
|
||||
<input type="hidden" id="settingsId" name="id">
|
||||
<!-- 公众号基本信息 -->
|
||||
<div class="bg-gray-50 p-5 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
|
||||
<i class="fa fa-wechat text-primary mr-2"></i>公众号基本信息
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="wechat_name" class="block text-sm font-medium text-gray-700 mb-1">公众号名称 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="wechat_name" name="wechat_name" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="请输入公众号名称">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wechat_account" class="block text-sm font-medium text-gray-700 mb-1">公众号原始ID <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="wechat_account" name="wechat_account" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="格式为gh_xxxx">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wechat_type" class="block text-sm font-medium text-gray-700 mb-1">公众号类型 <span class="text-red-500">*</span></label>
|
||||
<select id="wechat_type" name="wechat_type" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors">
|
||||
<option value="subscription">订阅号</option>
|
||||
<option value="service" selected>服务号</option>
|
||||
<option value="enterprise">企业号</option>
|
||||
<option value="test">测试号</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="qrcode_url" class="block text-sm font-medium text-gray-700 mb-1">公众号二维码URL</label>
|
||||
<div class="flex items-center">
|
||||
<input type="url" id="qrcode_url" name="qrcode_url"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="https://example.com/share.jpg">
|
||||
<div class="relative">
|
||||
<button type="button"
|
||||
class="w-10 h-10 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center justify-center"
|
||||
onclick="OpenGallery('qrcode_url', 'setting_qrimg')">
|
||||
<img src="https://cdn-icons-png.flaticon.com/128/10054/10054290.png" alt="预览图" class=" object-cover rounded">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img class="w-[120px] h-[120px] object-contain" id='setting_qrimg' src="" alt="公众号二维码预览" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="setting_status" name="status" value="1" class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用当前公众号配置</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公众号接口配置 -->
|
||||
<div class="bg-gray-50 p-5 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
|
||||
<i class="fa fa-plug text-primary mr-2"></i>接口配置信息
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="appid" class="block text-sm font-medium text-gray-700 mb-1">AppID <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="appid" name="appid" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="公众号的AppID">
|
||||
</div>
|
||||
<div>
|
||||
<label for="appsecret" class="block text-sm font-medium text-gray-700 mb-1">AppSecret <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="appsecret" name="appsecret" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="公众号的AppSecret">
|
||||
</div>
|
||||
<div>
|
||||
<label for="token" class="block text-sm font-medium text-gray-700 mb-1">Token</label>
|
||||
<input type="text" id="token" name="token" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="用于接口调用的Token">
|
||||
<p class="mt-1 text-xs text-gray-500">由开发者自定义,用于生成签名</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="encoding_aes_key" class="block text-sm font-medium text-gray-700 mb-1">EncodingAESKey</label>
|
||||
<input type="text" id="encoding_aes_key" name="encoding_aes_key" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="消息加密密钥">
|
||||
<p class="mt-1 text-xs text-gray-500">消息加解密时使用,43位字符</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200">
|
||||
<button type="button" id="saveSettingsBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表单弹窗背景 -->
|
||||
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
|
||||
<!-- 活码表单弹窗 -->
|
||||
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-plus-circle text-primary mr-2"></i>
|
||||
创建新活码
|
||||
</h3>
|
||||
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="shortlinkForm" class="space-y-5">
|
||||
<input type="hidden" id="shortlinkId" name="id">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">活码名称 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="name" name="name" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="请输入活码名称(如:技术交流群)">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wx_group_name" class="block text-sm font-medium text-gray-700 mb-1">微信群名称 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="wx_group_name" name="wx_group_name" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="请输入微信群名称">
|
||||
</div>
|
||||
<div>
|
||||
<label for="form_qrcode_url" class="block text-sm font-medium text-gray-700 mb-1">群二维码URL <span class="text-red-500">*</span></label>
|
||||
<div class="flex gap-2">
|
||||
<input type="url" id="form_qrcode_url" name="qrcode_url"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="https://example.com/share.jpg">
|
||||
|
||||
<div class="relative">
|
||||
<button type="button"
|
||||
class="bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2"
|
||||
onclick="OpenGallery('form_qrcode_url', 'image-preview')">
|
||||
<img id="image-preview" src="" alt="预览图" class="w-10 h-10 object-cover rounded">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-700 mb-1">活码编码 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="code" name="code" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" value="" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_scans" class="block text-sm font-medium text-gray-700 mb-1">最大扫码次数(0为无限制)</label>
|
||||
<input type="number" id="max_scans" name="max_scans" min="0" value="0" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="0">
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_members" class="block text-sm font-medium text-gray-700 mb-1">群最大人数</label>
|
||||
<input type="number" id="max_members" name="max_members" min="1" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="如:200">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">描述(可选)</label>
|
||||
<textarea id="description" name="description" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-none" placeholder="请输入活码描述(如:技术交流一群,满200人自动切换)"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="form_status" name="status" value="1" checked class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用状态</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="submitBtn" type="button" class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存活码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qrCodeModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white p-6 rounded-lg shadow-xl max-w-xs w-full mx-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4 text-center">扫码分享</h3>
|
||||
<div class="flex justify-center mb-4" id="qrCodeContainer"></div>
|
||||
<p onclick="copyToClipboard(this.innerText, '分享链接')"id="textId" class="text-sm text-gray-500 text-center mb-4"></p>
|
||||
<button onclick="hideQrCode()" class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 py-2 px-4 rounded transition-colors">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
// 分页相关功能
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 1,
|
||||
totalPages = 1;
|
||||
|
||||
// 分页DOM元素
|
||||
const listEl = document.getElementById('shortlinkList');
|
||||
const [paginationEl, prevBtn, nextBtn, pageNumbers] = ['pagination', 'prevPage', 'nextPage', 'pageNumbers'].map(id => document.getElementById(id));
|
||||
const [rangeEl, totalEl, loadingEl, emptyEl] = ['showingRange', 'totalItems', 'loading', 'empty'].map(id => document.getElementById(id));
|
||||
|
||||
// 初始化分页
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadPage(1);
|
||||
prevBtn.onclick = () => currentPage > 1 && loadPage(currentPage - 1);
|
||||
nextBtn.onclick = () => currentPage < totalPages && loadPage(currentPage + 1);
|
||||
});
|
||||
|
||||
// 加载分页数据
|
||||
async function loadPage(page) {
|
||||
// 显示加载状态
|
||||
loadingEl.classList.remove('hidden');
|
||||
listEl.innerHTML = '';
|
||||
paginationEl.classList.add('hidden');
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/admin/wxgcode/list?page=${page}&page_size=${PAGE_SIZE}`);
|
||||
const { data } = await res.json();
|
||||
// 更新分页信息
|
||||
currentPage = data.current_page;
|
||||
totalPages = data.total_pages;
|
||||
totalEl.textContent = data.total_items;
|
||||
|
||||
// 渲染列表
|
||||
if (data.items.length) {
|
||||
data.items.forEach(link => {
|
||||
const statusClass = link.status == 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
const statusText = link.status == 1 ? '启用' : '停用';
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-gray-50 transition-colors';
|
||||
tr.setAttribute('data-id', link.id);
|
||||
tr.innerHTML = `
|
||||
<!-- 活码名称和描述 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(link.name ?? '未命名')}</div>
|
||||
<div class="text-xs text-gray-500 truncate max-w-xs">
|
||||
${escapeHtml(link.description ?? '无描述')}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 活码编码 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-primary truncate max-w-md cursor-pointer" onclick="showQrCode('${escapeHtml(getQrcodeUrl(link.code))}')">
|
||||
${link.code}
|
||||
<i class="fa fa-qrcode ml-1 opacity-70"></i>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 微信群名称(小屏幕隐藏) -->
|
||||
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<div class="text-sm text-gray-500 truncate max-w-md">
|
||||
${escapeHtml(link.wx_group_name ?? '未设置')}
|
||||
</div>
|
||||
</td>
|
||||
<!-- 状态 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<!-- 访问量 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">
|
||||
${link.total_views} 次
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button class="edit-btn text-gray-500 hover:text-blue-500"
|
||||
data-id="${link.id}" title="编辑">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button class="delete-btn text-gray-500 hover:text-red-500"
|
||||
data-id="${link.id}" title="删除">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
listEl.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
listEl.innerHTML = `<tr><td colspan="5" class="text-center py-12">没有查到活码;请创建后查看!</td></tr>`;
|
||||
}
|
||||
|
||||
// 更新分页控件
|
||||
rangeEl.textContent = `${(page-1)*PAGE_SIZE+1}-${Math.min(page*PAGE_SIZE, data.total_items)}`;
|
||||
renderPageNumbers();
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
paginationEl.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<tr><td colspan="5" class="text-center py-12">加载失败: ${e.message}</td></tr>`;
|
||||
} finally {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 生成活码编码
|
||||
function generateCode(length = 8) {
|
||||
const chars = '1234567890ACDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let code = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// 生成活码访问链接
|
||||
function getQrcodeUrl(code) {
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
return `${protocol}//${host}/wxgcode/${code}`;
|
||||
}
|
||||
|
||||
// 渲染页码按钮
|
||||
function renderPageNumbers() {
|
||||
pageNumbers.innerHTML = '';
|
||||
const start = Math.max(1, currentPage - 2);
|
||||
const end = Math.min(totalPages, start + 4);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `px-3 py-1 rounded ${i === currentPage ? 'bg-primary text-white' : 'border'}`;
|
||||
btn.textContent = i;
|
||||
btn.onclick = () => loadPage(i);
|
||||
pageNumbers.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义函数
|
||||
function escapeHtml(str) {
|
||||
return str ? str.toString().replace(/[&<>"']/g, c => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
} [c])) : '';
|
||||
}
|
||||
|
||||
// 二维码相关功能
|
||||
function showQrCode(url) {
|
||||
// 清空之前的二维码
|
||||
document.getElementById('qrCodeContainer').innerHTML = '';
|
||||
|
||||
// 生成新的二维码
|
||||
QRCode.toCanvas(url, {
|
||||
width: 200,
|
||||
margin: 1
|
||||
}, function(error, canvas) {
|
||||
if (error) {
|
||||
console.error('生成二维码失败:', error);
|
||||
return;
|
||||
}
|
||||
document.getElementById('qrCodeContainer').appendChild(canvas);
|
||||
});
|
||||
|
||||
// 显示弹窗
|
||||
document.getElementById('qrCodeModal').classList.remove('hidden');
|
||||
document.getElementById('textId').textContent = url;
|
||||
// 阻止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideQrCode() {
|
||||
document.getElementById('qrCodeModal').classList.add('hidden');
|
||||
// 恢复页面滚动
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 点击弹窗外部关闭
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 二维码弹窗事件绑定
|
||||
const qrCodeModal = document.getElementById('qrCodeModal');
|
||||
if (qrCodeModal) {
|
||||
qrCodeModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideQrCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 选项卡切换功能
|
||||
function switchTab(tabName) {
|
||||
document.getElementById('list-content').classList.add('hidden');
|
||||
document.getElementById('settings-content').classList.add('hidden');
|
||||
|
||||
document.getElementById('list-tab').classList.remove('border-primary', 'text-primary');
|
||||
document.getElementById('list-tab').classList.add('border-transparent', 'text-gray-500');
|
||||
document.getElementById('settings-tab').classList.remove('border-primary', 'text-primary');
|
||||
document.getElementById('settings-tab').classList.add('border-transparent', 'text-gray-500');
|
||||
|
||||
document.getElementById(`${tabName}-content`).classList.remove('hidden');
|
||||
document.getElementById(`${tabName}-tab`).classList.remove('border-transparent', 'text-gray-500');
|
||||
document.getElementById(`${tabName}-tab`).classList.add('border-primary', 'text-primary');
|
||||
|
||||
if (tabName === 'settings' && !window.settingsLoaded) {
|
||||
loadSettings();
|
||||
window.settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 主功能逻辑
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const formModal = document.getElementById('formModal');
|
||||
const formBackdrop = document.getElementById('formBackdrop');
|
||||
const openFormBtn = document.getElementById('openFormBtn');
|
||||
const closeFormBtn = document.getElementById('closeFormBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const formTitle = document.getElementById('formTitle');
|
||||
const shortlinkForm = document.getElementById('shortlinkForm');
|
||||
const shortlinkList = document.getElementById('shortlinkList');
|
||||
const settingsForm = document.getElementById('settingsForm');
|
||||
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||
window.settingsLoaded = false;
|
||||
|
||||
// 检查必要元素
|
||||
function checkElements() {
|
||||
const elements = [formModal, formBackdrop, openFormBtn, closeFormBtn, cancelBtn, submitBtn];
|
||||
const missing = elements.filter(el => !el);
|
||||
if (missing.length > 0) {
|
||||
console.error('缺少必要的DOM元素,弹窗功能无法正常工作');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 打开表单弹窗
|
||||
function openFormModal() {
|
||||
if (!checkElements()) return;
|
||||
resetForm();
|
||||
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
void formModal.offsetWidth; // 强制重绘
|
||||
document.getElementById('code').value = generateCode();
|
||||
}
|
||||
|
||||
// 关闭表单弹窗
|
||||
function closeFormModal() {
|
||||
if (!checkElements()) return;
|
||||
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
shortlinkForm.reset();
|
||||
document.getElementById('shortlinkId').value = '';
|
||||
document.getElementById('image-preview').src = 'https://cdn-icons-png.flaticon.com/128/10054/10054290.png';
|
||||
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新活码';
|
||||
submitBtn.innerHTML = '保存活码';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
window.loadSettings = async function() {
|
||||
try {
|
||||
const response = await fetch('/admin/wxgcode/settings');
|
||||
if (!response.ok) throw new Error('获取设置失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const settings = data.data;
|
||||
|
||||
// 回填公众号基本信息
|
||||
document.getElementById('settingsId').value = settings.id || '';
|
||||
document.getElementById('wechat_name').value = settings.wechat_name || '';
|
||||
document.getElementById('wechat_account').value = settings.wechat_account || '';
|
||||
document.getElementById('wechat_type').value = settings.wechat_type || 'service';
|
||||
document.getElementById('setting_qrcode_url').value = settings.qrcode_url || '';
|
||||
document.getElementById('setting_qrimg').src = settings.qrcode_url || '';
|
||||
document.getElementById('setting_status').checked = Boolean(Number(settings.status));
|
||||
|
||||
// 回填接口配置信息
|
||||
document.getElementById('appid').value = settings.appid || '';
|
||||
document.getElementById('appsecret').value = settings.appsecret || '';
|
||||
document.getElementById('token').value = settings.token || '';
|
||||
document.getElementById('encoding_aes_key').value = settings.encoding_aes_key || '';
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm(formElement) {
|
||||
if (formElement.id === 'shortlinkForm') {
|
||||
const name = formElement.querySelector('#name').value.trim();
|
||||
const wxGroupName = formElement.querySelector('#wx_group_name').value.trim();
|
||||
const qrcodeUrl = formElement.querySelector('#form_qrcode_url').value.trim();
|
||||
const code = formElement.querySelector('#code').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showMessage('请输入活码名称', 'error');
|
||||
return false;
|
||||
}
|
||||
if (!wxGroupName) {
|
||||
showMessage('请输入微信群名称', 'error');
|
||||
return false;
|
||||
}
|
||||
if (!qrcodeUrl) {
|
||||
showMessage('请输入群二维码URL', 'error');
|
||||
return false;
|
||||
}
|
||||
// 验证二维码URL格式
|
||||
const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/;
|
||||
if (!urlPattern.test(qrcodeUrl)) {
|
||||
showMessage('请输入有效的群二维码URL', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (formElement.id === 'settingsForm') {
|
||||
// 验证实际必填项
|
||||
const wechatName = formElement.querySelector('#wechat_name').value.trim();
|
||||
const wechatAccount = formElement.querySelector('#wechat_account').value.trim();
|
||||
const appid = formElement.querySelector('#appid').value.trim();
|
||||
const appsecret = formElement.querySelector('#appsecret').value.trim();
|
||||
|
||||
if (!wechatName) { showMessage('请输入公众号名称', 'error'); return false; }
|
||||
if (!wechatAccount) { showMessage('请输入公众号原始ID', 'error'); return false; }
|
||||
if (!appid) { showMessage('请输入AppID', 'error'); return false; }
|
||||
if (!appsecret) { showMessage('请输入AppSecret', 'error'); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
async function submitFormData(url, formElement, successMsg) {
|
||||
if (!validateForm(formElement)) return;
|
||||
|
||||
const submitButton = formElement.id === 'shortlinkForm' ? submitBtn : document.getElementById('saveSettingsBtn');
|
||||
const originalText = submitButton.innerHTML;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(formElement);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage(data.message || successMsg, 'success');
|
||||
closeFormModal();
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
|
||||
} else {
|
||||
throw new Error(data.message || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载活码数据(编辑用)
|
||||
async function loadShortlinkData(id) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/wxgcode/get/${id}`);
|
||||
if (!response.ok) throw new Error('获取数据失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
// 活码表字段回填
|
||||
const { id, name, code, wx_group_name, qrcode_url, max_scans, max_members, description, status } = data.data;
|
||||
document.getElementById('shortlinkId').value = id;
|
||||
document.getElementById('name').value = name || '';
|
||||
document.getElementById('code').value = code || '';
|
||||
document.getElementById('wx_group_name').value = wx_group_name || '';
|
||||
document.getElementById('form_qrcode_url').value = qrcode_url || '';
|
||||
document.getElementById('image-preview').src = qrcode_url || '';
|
||||
document.getElementById('max_scans').value = max_scans || 0;
|
||||
document.getElementById('max_members').value = max_members || '';
|
||||
document.getElementById('description').value = description || '';
|
||||
document.getElementById('form_status').checked = Boolean(Number(status));
|
||||
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑活码';
|
||||
} else {
|
||||
throw new Error(data.message || '获取数据失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
closeFormModal();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存活码';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除活码
|
||||
async function deleteLink(id) {
|
||||
if (!confirm('确定要删除该活码吗?此操作不可恢复!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/wxgcode/delete/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage('活码已删除', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
if (checkElements()) {
|
||||
// 打开表单
|
||||
openFormBtn.addEventListener('click', openFormModal);
|
||||
|
||||
// 关闭表单
|
||||
closeFormBtn.addEventListener('click', closeFormModal);
|
||||
cancelBtn.addEventListener('click', closeFormModal);
|
||||
formBackdrop.addEventListener('click', closeFormModal);
|
||||
|
||||
// 活码表单提交
|
||||
submitBtn.addEventListener('click', function() {
|
||||
submitFormData('/admin/wxgcode/update', shortlinkForm, '活码保存成功');
|
||||
});
|
||||
|
||||
// 设置表单提交
|
||||
saveSettingsBtn.addEventListener('click', function() {
|
||||
submitFormData('/admin/wxgcode/settings', settingsForm, '设置保存成功');
|
||||
});
|
||||
|
||||
// 列表操作事件委托
|
||||
shortlinkList.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.edit-btn');
|
||||
const deleteBtn = e.target.closest('.delete-btn');
|
||||
|
||||
if (editBtn) {
|
||||
const id = editBtn.getAttribute('data-id');
|
||||
if (id) {
|
||||
openFormModal();
|
||||
// 监听动画结束后加载数据
|
||||
const loadData = () => {
|
||||
loadShortlinkData(id);
|
||||
formModal.removeEventListener('transitionend', loadData);
|
||||
};
|
||||
formModal.addEventListener('transitionend', loadData, { once: true });
|
||||
}
|
||||
} else if (deleteBtn) {
|
||||
const id = deleteBtn.getAttribute('data-id');
|
||||
if (id) deleteLink(id);
|
||||
}
|
||||
});
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && !formModal.classList.contains('invisible')) {
|
||||
closeFormModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止表单默认提交
|
||||
shortlinkForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 复制到剪贴板功能
|
||||
window.copyToClipboard = function(text, message) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showMessage('已复制: ' + message, 'success');
|
||||
}).catch(err => {
|
||||
showMessage('复制失败: ' + err.message, 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
// 检查是否有 data 参数
|
||||
if (isset($_POST['data'])) {
|
||||
$scanResult = htmlspecialchars($_POST['data']);
|
||||
} else {
|
||||
$scanResult = '未获取到扫描结果';
|
||||
}
|
||||
?>
|
||||
<!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>
|
||||
body {font-family: Arial, sans-serif; display: flex; flex-direction: column; height: 100vh; margin: 0; background-color: #f4f4f4; }
|
||||
.result-container {background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); text-align: center; width: 90%; max-width: 400px; margin: auto; cursor: pointer; margin-top: 200px; }
|
||||
.back-button-container {position: fixed; bottom: 0; width: 100%; text-align: center; padding: 20px 0; background-color: #007BFF; color: white; cursor: pointer; transition: background-color 0.3s ease; } .back-button-container:hover {background-color: #0056b3; }
|
||||
.copy-toast {position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 4px; opacity: 0; transition: opacity 0.3s ease; }
|
||||
.result-container p:nth-child(2) {word-wrap: break-word; word-break: break-all; white-space: pre-wrap; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="result-container" onclick="copyResult()">
|
||||
<p id="scan-result">扫描结果:</p>
|
||||
<p> <?php echo $scanResult; ?></p>
|
||||
</div>
|
||||
<div class="back-button-container" onclick="history.back()">
|
||||
扫一扫
|
||||
</div>
|
||||
<div id="copy-toast" class="copy-toast">复制成功</div>
|
||||
|
||||
<script>
|
||||
function copyResult() {
|
||||
const resultElement = document.querySelector('.result-container p:nth-child(2)');
|
||||
const textToCopy = resultElement.textContent;
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = textToCopy;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
const toast = document.getElementById('copy-toast');
|
||||
toast.style.opacity = 1;
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = 0;
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Executable
+518
@@ -0,0 +1,518 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>加入我们的微信群</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
secondary: '#10b981',
|
||||
neutral: '#f3f4f6'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.card-shadow {
|
||||
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.1), 0 8px 10px -6px rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-b from-blue-50 to-white min-h-screen">
|
||||
|
||||
|
||||
<main class="container mx-auto px-4 py-8 md:py-16">
|
||||
<!-- 加载状态 -->
|
||||
<div id="loadingContainer" class="max-w-4xl mx-auto py-16 text-center">
|
||||
<i class="fa fa-spinner fa-spin text-primary text-3xl mb-4"></i>
|
||||
<p class="text-gray-600">加载中,请稍候...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div id="errorContainer" class="max-w-4xl mx-auto py-16 text-center hidden">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-50 mb-4">
|
||||
<i class="fa fa-exclamation-triangle text-2xl text-red-400"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-1">加载失败</h3>
|
||||
<p class="text-gray-500 max-w-md mx-auto mb-4" id="errorMessage">无法加载群组数据,请稍后重试</p>
|
||||
<button id="retryBtn" class="px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 (默认隐藏) -->
|
||||
<div id="contentContainer" class="hidden">
|
||||
<!-- 页面标题 -->
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-[clamp(1.8rem,5vw,3rem)] font-bold text-gray-800 mb-4">加入我们的微信群</h1>
|
||||
<p class="text-gray-600 max-w-2xl mx-auto text-lg">扫码加入感兴趣的群组,与志同道合的朋友交流互动</p>
|
||||
</div>
|
||||
|
||||
<!-- 活码展示区 -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- 主要活码卡片 -->
|
||||
<div id="mainQrcodeCard" class="bg-white rounded-2xl p-6 md:p-8 card-shadow mb-10 transform transition-all duration-300 hover:scale-[1.01]">
|
||||
<div class="flex flex-col md:flex-row items-center gap-8">
|
||||
<!-- 二维码区域 -->
|
||||
<div class="w-full md:w-1/3 flex justify-center">
|
||||
<div class="bg-white p-4 rounded-xl border border-gray-100 shadow-md animate-float">
|
||||
<div id="qrcodeContainer" class="w-56 h-56 mx-auto">
|
||||
<!-- 二维码将通过JS动态生成 -->
|
||||
</div>
|
||||
<p class="text-center mt-3 text-sm text-gray-500">扫码加入群聊</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群信息区域 -->
|
||||
<div class="w-full md:w-2/3">
|
||||
<div class="flex items-center mb-4">
|
||||
<span id="groupStatusBadge" class="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium mr-3">
|
||||
<i class="fa fa-check-circle mr-1"></i> 活跃中
|
||||
</span>
|
||||
<span class="text-gray-500 text-sm"><i class="fa fa-eye mr-1"></i> 已被查看 <span id="viewCount">1,234</span> 次</span>
|
||||
</div>
|
||||
|
||||
<h2 id="groupName" class="text-2xl font-bold text-gray-800 mb-3">技术交流微信群</h2>
|
||||
|
||||
<p id="groupDescription" class="text-gray-600 mb-6">
|
||||
这是一个技术爱好者交流群,欢迎大家分享编程经验、解决技术难题,一起学习进步。群内禁止广告和无关话题。
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-neutral rounded-lg p-3 text-center">
|
||||
<p class="text-gray-500 text-sm">群人数</p>
|
||||
<p id="memberCount" class="font-semibold text-gray-800">186人</p>
|
||||
</div>
|
||||
<div class="bg-neutral rounded-lg p-3 text-center">
|
||||
<p class="text-gray-500 text-sm">创建时间</p>
|
||||
<p id="createTime" class="font-semibold text-gray-800">2023-05-12</p>
|
||||
</div>
|
||||
<div class="bg-neutral rounded-lg p-3 text-center">
|
||||
<p class="text-gray-500 text-sm">最大人数</p>
|
||||
<p id="maxMembers" class="font-semibold text-gray-800">200人</p>
|
||||
</div>
|
||||
<div class="bg-neutral rounded-lg p-3 text-center">
|
||||
<p class="text-gray-500 text-sm">今日新增</p>
|
||||
<p id="todayNew" class="font-semibold text-gray-800">8人</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap hidden gap-3">
|
||||
<button id="refreshQrcodeBtn" class="px-5 py-2.5 bg-primary hover:bg-primary/90 text-white rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center">
|
||||
<i class="fa fa-refresh mr-2"></i> 刷新二维码
|
||||
</button>
|
||||
<button id="shareBtn" class="px-5 py-2.5 border border-gray-300 hover:bg-gray-50 text-gray-700 rounded-lg transition-all duration-200 flex items-center">
|
||||
<i class="fa fa-share-alt mr-2"></i> 分享
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群规则说明 -->
|
||||
<div class="bg-blue-50 rounded-2xl p-6 mb-10">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fa fa-info-circle text-primary mr-2"></i> 入群须知
|
||||
</h3>
|
||||
<ul class="space-y-2 text-gray-600">
|
||||
<li class="flex items-start">
|
||||
<i class="fa fa-check-circle text-secondary mt-1 mr-2"></i>
|
||||
<span>请遵守群规,文明交流,友善互动</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<i class="fa fa-check-circle text-secondary mt-1 mr-2"></i>
|
||||
<span>禁止发布广告、色情、暴力等违规内容</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<i class="fa fa-check-circle text-secondary mt-1 mr-2"></i>
|
||||
<span>本群二维码有效期为7天,过期请重新获取</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<i class="fa fa-check-circle text-secondary mt-1 mr-2"></i>
|
||||
<span>群满200人后将自动切换至新群,请重新扫码</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 推荐群组 -->
|
||||
<div class="mb-10 hidden">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-th-large text-primary mr-2"></i> 推荐群组
|
||||
</h3>
|
||||
|
||||
<div id="recommendedGroups" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- 推荐群将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
<!-- 分享弹窗 -->
|
||||
<div id="shareModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6 transform transition-transform duration-300 scale-95">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<h3 class="text-xl font-bold text-gray-800">分享群二维码</h3>
|
||||
<button id="closeShareModal" class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fa fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="p-3 bg-gray-50 rounded-lg text-center">
|
||||
<p class="text-gray-600 mb-2">通过以下方式分享</p>
|
||||
<div class="flex justify-center space-x-6">
|
||||
<a href="#" class="flex flex-col items-center text-gray-600 hover:text-green-500 transition-colors">
|
||||
<i class="fa fa-weixin text-2xl mb-1"></i>
|
||||
<span class="text-sm">微信</span>
|
||||
</a>
|
||||
<a href="#" class="flex flex-col items-center text-gray-600 hover:text-blue-500 transition-colors">
|
||||
<i class="fa fa-qq text-2xl mb-1"></i>
|
||||
<span class="text-sm">QQ</span>
|
||||
</a>
|
||||
<a href="#" class="flex flex-col items-center text-gray-600 hover:text-red-500 transition-colors">
|
||||
<i class="fa fa-weibo text-2xl mb-1"></i>
|
||||
<span class="text-sm">微博</span>
|
||||
</a>
|
||||
<a href="#" class="flex flex-col items-center text-gray-600 hover:text-gray-800 transition-colors">
|
||||
<i class="fa fa-link text-2xl mb-1"></i>
|
||||
<span class="text-sm">复制链接</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<p class="text-gray-600 text-sm">
|
||||
<i class="fa fa-info-circle text-primary mr-1"></i>
|
||||
分享后,好友可以通过您分享的链接加入相同的群组
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
|
||||
<script>
|
||||
// 全局变量存储群组数据
|
||||
let groupData = null;
|
||||
// API基础地址 - 请根据实际情况修改
|
||||
const API_BASE_URL = '';
|
||||
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 从URL获取活码编码(假设URL格式为 ...?code=XXX)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = "<?= $code ?>";
|
||||
|
||||
if (!code) {
|
||||
showError('未找到活码编码,请检查链接是否正确');
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载群组数据
|
||||
loadGroupData(code);
|
||||
|
||||
// 绑定按钮事件
|
||||
document.getElementById('refreshQrcodeBtn').addEventListener('click', refreshQrcode);
|
||||
document.getElementById('shareBtn').addEventListener('click', openShareModal);
|
||||
document.getElementById('closeShareModal').addEventListener('click', closeShareModal);
|
||||
document.getElementById('retryBtn').addEventListener('click', () => loadGroupData(code));
|
||||
|
||||
// 点击分享弹窗外部关闭
|
||||
document.getElementById('shareModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeShareModal();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// 从API加载群组数据
|
||||
function loadGroupData(code) {
|
||||
showLoading();
|
||||
|
||||
// 构建API请求URL
|
||||
const apiUrl = `${API_BASE_URL}/wxgcode/get/${code}`;
|
||||
|
||||
fetch(apiUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误,状态码: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success && data.data) {
|
||||
// 保存数据
|
||||
groupData = data.data;
|
||||
// 渲染页面
|
||||
renderPage();
|
||||
// 显示内容
|
||||
showContent();
|
||||
} else {
|
||||
showError(data.message || '获取群组数据失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载群组数据失败:', error);
|
||||
showError('网络请求失败,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染页面内容
|
||||
function renderPage() {
|
||||
if (!groupData) return;
|
||||
|
||||
// 更新页面标题
|
||||
document.title = `${groupData.name || '微信群'} - 加入我们的微信群`;
|
||||
|
||||
// 填充群组信息
|
||||
document.getElementById('groupName').textContent = groupData.name || '微信群';
|
||||
document.getElementById('groupDescription').textContent = groupData.description || '暂无群组描述';
|
||||
document.getElementById('viewCount').textContent = formatNumber(groupData.total_views || 0);
|
||||
document.getElementById('memberCount').textContent = groupData.current_members ? `${groupData.current_members}人` : '未知';
|
||||
document.getElementById('createTime').textContent = groupData.created_at || '未知时间';
|
||||
document.getElementById('maxMembers').textContent = groupData.max_members ? `${groupData.max_members}人` : '无限制';
|
||||
document.getElementById('todayNew').textContent = groupData.today_new || '0人';
|
||||
|
||||
// 更新状态标签
|
||||
const statusBadge = document.getElementById('groupStatusBadge');
|
||||
if (groupData.status === 0) {
|
||||
statusBadge.className = 'px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium mr-3';
|
||||
statusBadge.innerHTML = '<i class="fa fa-times-circle mr-1"></i> 已禁用';
|
||||
} else if (groupData.is_full === 1) {
|
||||
statusBadge.className = 'px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm font-medium mr-3';
|
||||
statusBadge.innerHTML = '<i class="fa fa-exclamation-circle mr-1"></i> 已满员';
|
||||
}
|
||||
|
||||
// 生成二维码
|
||||
generateQrcode(groupData.qrcode_url);
|
||||
|
||||
// 渲染推荐群组
|
||||
renderRecommendedGroups(groupData.recommended || []);
|
||||
}
|
||||
|
||||
// 生成二维码
|
||||
function generateQrcode(qrcodeUrl) {
|
||||
const container = document.getElementById('qrcodeContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (qrcodeUrl) {
|
||||
// 如果有提供二维码URL,直接使用
|
||||
const img = document.createElement('img');
|
||||
img.src = qrcodeUrl;
|
||||
img.alt = `${groupData.name || '微信群'}的二维码`;
|
||||
img.className = 'w-full h-full object-contain';
|
||||
container.appendChild(img);
|
||||
} else {
|
||||
// 否则生成当前页面URL的二维码
|
||||
const url = window.location.href;
|
||||
|
||||
QRCode.toCanvas(url, {
|
||||
width: 220,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#333333',
|
||||
light: '#ffffff'
|
||||
}
|
||||
}, function(error, canvas) {
|
||||
if (error) {
|
||||
console.error('生成二维码失败:', error);
|
||||
container.innerHTML = '<p class="text-center text-red-500 py-10">生成二维码失败</p>';
|
||||
return;
|
||||
}
|
||||
container.appendChild(canvas);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染推荐群组
|
||||
function renderRecommendedGroups(groups) {
|
||||
const container = document.getElementById('recommendedGroups');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (groups.length === 0) {
|
||||
container.innerHTML = '<p class="col-span-full text-center text-gray-500 py-6">暂无推荐群组</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
groups.forEach(group => {
|
||||
const groupCard = document.createElement('div');
|
||||
groupCard.className = 'bg-white rounded-xl overflow-hidden shadow-md transition-all duration-300 hover:shadow-lg hover:-translate-y-1';
|
||||
groupCard.innerHTML = `
|
||||
<div class="p-5">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<h4 class="font-semibold text-gray-800">${group.name || '未命名群组'}</h4>
|
||||
<span class="px-2 py-0.5 bg-${getCategoryColor(group.category)}-100 text-${getCategoryColor(group.category)}-800 rounded-full text-xs">${group.category || '其他'}</span>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm mb-4 line-clamp-2">${group.description || '暂无群组描述'}</p>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-500"><i class="fa fa-users mr-1"></i> ${group.member_count || 0}人</span>
|
||||
<a href="${group.url || '#'}" class="text-primary hover:text-primary/80 transition-colors">查看 <i class="fa fa-arrow-right ml-1"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(groupCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新二维码
|
||||
function refreshQrcode() {
|
||||
if (!groupData || !groupData.code) {
|
||||
showNotification('无法获取活码信息,刷新失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('refreshQrcodeBtn');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
// 显示加载状态
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 刷新中...';
|
||||
|
||||
// 发送请求刷新二维码
|
||||
fetch(`${API_BASE_URL}/wxgcode/refresh/${groupData.code}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.data && data.data.qrcode_url) {
|
||||
// 更新本地数据
|
||||
groupData.qrcode_url = data.data.qrcode_url;
|
||||
// 更新二维码
|
||||
generateQrcode(data.data.qrcode_url);
|
||||
// 显示成功提示
|
||||
showNotification('二维码已刷新', 'success');
|
||||
} else {
|
||||
// 显示错误信息
|
||||
showNotification(data.message || '刷新二维码失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('刷新二维码错误:', error);
|
||||
showNotification('网络错误,刷新失败', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
// 打开分享弹窗
|
||||
function openShareModal() {
|
||||
const modal = document.getElementById('shareModal');
|
||||
modal.classList.remove('opacity-0', 'pointer-events-none');
|
||||
modal.querySelector('div').classList.remove('scale-95');
|
||||
modal.querySelector('div').classList.add('scale-100');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// 关闭分享弹窗
|
||||
function closeShareModal() {
|
||||
const modal = document.getElementById('shareModal');
|
||||
modal.classList.add('opacity-0', 'pointer-events-none');
|
||||
modal.querySelector('div').classList.remove('scale-100');
|
||||
modal.querySelector('div').classList.add('scale-95');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 显示通知消息
|
||||
function showNotification(message, type = 'info') {
|
||||
// 创建通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full`;
|
||||
|
||||
// 设置通知类型样式
|
||||
if (type === 'success') {
|
||||
notification.classList.add('bg-green-50', 'text-green-800', 'border', 'border-green-200');
|
||||
notification.innerHTML = `<i class="fa fa-check-circle mr-2"></i>${message}`;
|
||||
} else if (type === 'error') {
|
||||
notification.classList.add('bg-red-50', 'text-red-800', 'border', 'border-red-200');
|
||||
notification.innerHTML = `<i class="fa fa-exclamation-circle mr-2"></i>${message}`;
|
||||
} else {
|
||||
notification.classList.add('bg-blue-50', 'text-blue-800', 'border', 'border-blue-200');
|
||||
notification.innerHTML = `<i class="fa fa-info-circle mr-2"></i>${message}`;
|
||||
}
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
document.getElementById('loadingContainer').classList.remove('hidden');
|
||||
document.getElementById('contentContainer').classList.add('hidden');
|
||||
document.getElementById('errorContainer').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 显示内容
|
||||
function showContent() {
|
||||
document.getElementById('loadingContainer').classList.add('hidden');
|
||||
document.getElementById('contentContainer').classList.remove('hidden');
|
||||
document.getElementById('errorContainer').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function showError(message) {
|
||||
document.getElementById('loadingContainer').classList.add('hidden');
|
||||
document.getElementById('contentContainer').classList.add('hidden');
|
||||
document.getElementById('errorContainer').classList.remove('hidden');
|
||||
document.getElementById('errorMessage').textContent = message;
|
||||
}
|
||||
|
||||
// 格式化数字(添加千位分隔符)
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
// 根据分类获取颜色
|
||||
function getCategoryColor(category) {
|
||||
const colorMap = {
|
||||
'技术': 'blue',
|
||||
'产品': 'purple',
|
||||
'商业': 'amber',
|
||||
'设计': 'pink',
|
||||
'教育': 'green',
|
||||
'生活': 'teal'
|
||||
};
|
||||
return colorMap[category] || 'gray';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
class JSSDK {
|
||||
private $appId;
|
||||
private $appSecret;
|
||||
|
||||
public function __construct($appId, $appSecret) {
|
||||
$this->appId = $appId;
|
||||
$this->appSecret = $appSecret;
|
||||
}
|
||||
|
||||
public function getSignPackage() {
|
||||
$jsapiTicket = $this->getJsApiTicket();
|
||||
|
||||
// 注意 URL 一定要动态获取,不能 hardcode.
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
|
||||
|
||||
$timestamp = time();
|
||||
$nonceStr = $this->createNonceStr();
|
||||
|
||||
// 这里参数的顺序要按照 key 值 ASCII 码升序排序
|
||||
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
|
||||
|
||||
$signature = sha1($string);
|
||||
|
||||
$signPackage = array(
|
||||
"appId" => $this->appId,
|
||||
"nonceStr" => $nonceStr,
|
||||
"timestamp" => $timestamp,
|
||||
"url" => $url,
|
||||
"signature" => $signature,
|
||||
"rawString" => $string
|
||||
);
|
||||
return $signPackage;
|
||||
}
|
||||
|
||||
private function createNonceStr($length = 16) {
|
||||
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
$str = "";
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
private function getJsApiTicket() {
|
||||
// jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
|
||||
$data = json_decode($this->get_php_file("jsapi_ticket.php"));
|
||||
if ($data->expire_time < time()) {
|
||||
$accessToken = $this->getAccessToken();
|
||||
// 如果是企业号用以下 URL 获取 ticket
|
||||
// $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=$accessToken";
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
$ticket = $res->ticket;
|
||||
if ($ticket) {
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->jsapi_ticket = $ticket;
|
||||
$this->set_php_file("jsapi_ticket.php", json_encode($data));
|
||||
}
|
||||
} else {
|
||||
$ticket = $data->jsapi_ticket;
|
||||
}
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
private function getAccessToken() {
|
||||
// access_token 应该全局存储与更新,以下代码以写入到文件中做示例
|
||||
$data = json_decode($this->get_php_file("access_token.php"));
|
||||
if ($data->expire_time < time()) {
|
||||
// 如果是企业号用以下URL获取access_token
|
||||
// $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$this->appId&corpsecret=$this->appSecret";
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
$access_token = $res->access_token;
|
||||
if ($access_token) {
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->access_token = $access_token;
|
||||
$this->set_php_file("access_token.php", json_encode($data));
|
||||
}
|
||||
} else {
|
||||
$access_token = $data->access_token;
|
||||
}
|
||||
return $access_token;
|
||||
}
|
||||
|
||||
private function httpGet($url) {
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
|
||||
// 为保证第三方服务器与微信服务器之间数据传输的安全性,所有微信接口采用https方式调用,必须使用下面2行代码打开ssl安全校验。
|
||||
// 如果在部署过程中代码在此处验证失败,请到 http://curl.haxx.se/ca/cacert.pem 下载新的证书判别文件。
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, true);
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
$res = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function get_php_file($filename) {
|
||||
return trim(substr(file_get_contents($filename), 15));
|
||||
}
|
||||
private function set_php_file($filename, $content) {
|
||||
$fp = fopen($filename, "w");
|
||||
fwrite($fp, "<?php exit();?>" . $content);
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>扫码结果</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; display: flex; flex-direction: column; height: 100vh; margin: 0; background-color: #f4f4f4; }
|
||||
.result-container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); text-align: center; width: 90%; max-width: 400px; margin: auto; cursor: pointer; margin-top: 200px; }
|
||||
.back-button-container { position: fixed; bottom: 0; width: 100%; text-align: center; padding: 20px 0; background-color: #007BFF; color: white; cursor: pointer; transition: background-color 0.3s ease; }
|
||||
.back-button-container:hover { background-color: #0056b3; }
|
||||
.copy-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 4px; opacity: 0; transition: opacity 0.3s ease; }
|
||||
.result-container p:nth-child(2) { word-wrap: break-word; word-break: break-all; white-space: pre-wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="result-container" onclick="copyResult()">
|
||||
<p id="scan-result">扫描结果:</p>
|
||||
<p><?php echo $scanResult; ?></p>
|
||||
</div>
|
||||
<div class="back-button-container" onclick="history.back()">扫一扫</div>
|
||||
<div id="copy-toast" class="copy-toast">复制成功</div>
|
||||
<script>
|
||||
function copyResult() {
|
||||
const result = document.querySelector('.result-container p:nth-child(2)');
|
||||
const text = result.textContent;
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
const toast = document.getElementById('copy-toast');
|
||||
toast.style.opacity = 1;
|
||||
setTimeout(() => { toast.style.opacity = 0; }, 2000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WxGCode
|
||||
* Description: 用来创建微信群活码或者其它活码的插件。
|
||||
* Version: 1.0.0
|
||||
* Author: JuheDev
|
||||
* Plugin URL: https://plugins.juhe.me/wxgcode
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'menus' => [
|
||||
[
|
||||
'title' => '微信活码',
|
||||
'icon' => 'fa fa-qrcode',
|
||||
'path' => '/admin/wxgcode/',
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'route_group' => [
|
||||
[
|
||||
'prefix' => '/wxgcode',
|
||||
'namespace' => 'Plugins\WxGCode\Controllers\Web',
|
||||
'routes' => [
|
||||
['GET', '/get/{code}', 'WxGCodeController@get'],
|
||||
['POST', '/data', 'WxGCodeController@data'],
|
||||
['GET', '/{code}', 'WxGCodeController@index'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'prefix' => '/admin/wxgcode',
|
||||
'namespace' => 'Plugins\WxGCode\Controllers\Admin',
|
||||
'routes' => [
|
||||
['GET', '/', 'WxGCodeController@index'],
|
||||
['GET', '/get/{id}', 'WxGCodeController@get'],
|
||||
['GET', '/list', 'WxGCodeController@list'],
|
||||
['GET', '/delete/{id}', 'WxGCodeController@delete'],
|
||||
['POST', '/update', 'WxGCodeController@update'],
|
||||
['GET|POST', '/settings', 'WxGCodeController@settings'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'tables' => ['wxgcode_list', 'wxgcode_settings'],
|
||||
'init' => function () {},
|
||||
|
||||
'activate' => function ($db) {
|
||||
$db->query("
|
||||
CREATE TABLE IF NOT EXISTS `wxgcode_list` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '活码名称(如“技术交流群活码”)',
|
||||
`code` varchar(30) NOT NULL COMMENT '活码唯一标识(用于生成访问链接)',
|
||||
`qrcode_url` varchar(500) NOT NULL COMMENT '微信群二维码图片URL',
|
||||
`wx_group_name` varchar(100) NOT NULL COMMENT '微信群名称',
|
||||
`wx_group_id` varchar(50) DEFAULT NULL COMMENT '微信群ID(可选)',
|
||||
`total_views` int(11) NOT NULL DEFAULT 0 COMMENT '总访问次数',
|
||||
`total_scans` int(11) NOT NULL DEFAULT 0 COMMENT '总扫码次数',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
|
||||
`max_scans` int(11) DEFAULT 0 COMMENT '最大扫码次数(0为无限制)',
|
||||
`max_members` int(11) DEFAULT NULL COMMENT '群最大人数',
|
||||
`current_members` int(11) DEFAULT 0 COMMENT '当前群人数',
|
||||
`is_full` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否满人:1-是,0-否',
|
||||
`expire_time` datetime DEFAULT NULL COMMENT '过期时间(NULL为永久有效)',
|
||||
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序值(用于多群排序)',
|
||||
`description` text DEFAULT NULL COMMENT '备注描述',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_code` (`code`) COMMENT '活码标识唯一索引',
|
||||
KEY `idx_status_full` (`status`,`is_full`) COMMENT '状态和满人状态索引,用于快速筛选可用群码'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='URL管理表,存储系统中所有需要管理的URL信息';
|
||||
");
|
||||
|
||||
$db->query("
|
||||
CREATE TABLE IF NOT EXISTS `wxgcode_settings` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`wechat_name` varchar(100) NOT NULL COMMENT '公众号名称',
|
||||
`wechat_account` varchar(50) NOT NULL COMMENT '公众号原始ID(gh_xxxx格式)',
|
||||
`appid` varchar(50) NOT NULL COMMENT '公众号AppID',
|
||||
`appsecret` varchar(100) NOT NULL COMMENT '公众号AppSecret',
|
||||
`token` varchar(100) DEFAULT NULL COMMENT '接口调用Token',
|
||||
`encoding_aes_key` varchar(100) DEFAULT NULL COMMENT '消息加密密钥',
|
||||
`qrcode_url` varchar(255) DEFAULT NULL COMMENT '公众号二维码URL',
|
||||
`wechat_type` enum('subscription','service','enterprise','test') NOT NULL DEFAULT 'service' COMMENT '公众号类型',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_appid` (`appid`),
|
||||
UNIQUE KEY `uk_wechat_account` (`wechat_account`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公众号设置表,存储公众号相关ID和密钥信息';
|
||||
");
|
||||
},
|
||||
|
||||
'deactivate' => function ($db) {},
|
||||
|
||||
|
||||
];
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
namespace Plugins\WxShare\Controllers\Admin;
|
||||
use App\Core\PluginBaseController;
|
||||
|
||||
class WxShareController extends PluginBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$this->checkLogin(); // 登录保护
|
||||
|
||||
// 获取配置信息(带默认值,避免空值)
|
||||
$settings = $this->db->get('wxshare_settings', '*') ?: []; // 如果没有数据,返回空数组
|
||||
|
||||
|
||||
|
||||
// 传数据给视图(统一封装在data中)
|
||||
$this->renderPluginView('WxShare', 'Admin/index.php', [
|
||||
'data' => [
|
||||
'settings' => '', // 配置信息
|
||||
'domain' => '' // 带协议的完整域名
|
||||
],
|
||||
'title' => '微信卡片分享管理中心'
|
||||
]);
|
||||
}
|
||||
|
||||
public function list() {
|
||||
$this->checkLogin(); // 登录保护
|
||||
|
||||
// 获取分页参数
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$pageSize = isset($_GET['page_size']) ? max(1, min(100, intval($_GET['page_size']))) : 10;
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
// 1. 获取总记录数
|
||||
$totalItems = $this->db->count('wxshare_list', '*');
|
||||
|
||||
// 2. 获取当前页数据
|
||||
$shortlinks = $this->db->select('wxshare_list', '*', [
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => [$offset, $pageSize]
|
||||
]);
|
||||
|
||||
// 3. 计算分页信息
|
||||
$totalPages = max(1, ceil($totalItems / $pageSize));
|
||||
|
||||
// 4. 返回JSON格式数据
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'items' => $shortlinks,
|
||||
'current_page' => $page,
|
||||
'total_pages' => $totalPages,
|
||||
'total_items' => $totalItems,
|
||||
'page_size' => $pageSize,
|
||||
'has_prev' => $page > 1,
|
||||
'has_next' => $page < $totalPages
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function get($id) {
|
||||
$this->checkLogin();
|
||||
// 设置响应内容类型为JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$row = $this->db->get('wxshare_list', '*', ['id' => $id]);
|
||||
|
||||
if ($row) {
|
||||
// 成功响应,包含状态和数据
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '无效的ID或记录不存在'
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
public function update() {
|
||||
// 设置JSON响应头
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$this->checkLogin();
|
||||
|
||||
// 获取表单数据(使用表中定义的字段)
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$share_link = trim($_POST['share_link'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$share_title = trim($_POST['share_title'] ?? '');
|
||||
$share_desc = trim($_POST['share_desc'] ?? '');
|
||||
$share_img = trim($_POST['share_img'] ?? '');
|
||||
$status = isset($_POST['status']) ? 1 : 0;
|
||||
|
||||
// 验证参数
|
||||
$errors = [];
|
||||
if (!$share_title) $errors[] = '分享标题不能为空';
|
||||
if (!$share_link) $errors[] = '分享链接不能为空';
|
||||
if (!$code) $errors[] = 'code不能为空';
|
||||
if (!$share_desc) $errors[] = '分享描述不能为空';
|
||||
if (!$share_img) $errors[] = '分享封面图不能为空';
|
||||
|
||||
if (!empty($errors)) {
|
||||
// 返回 JSON 错误信息
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => implode('; ', $errors)
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 有ID则更新
|
||||
if ($id > 0) {
|
||||
$data = [
|
||||
'share_link' => $share_link, // 原url改为share_link
|
||||
'name' => $share_link,
|
||||
'code' => $code,
|
||||
'share_title' => $share_title, // 新增字段
|
||||
'share_desc' => $share_desc, // 原description改为share_desc
|
||||
'share_img' => $share_img, // 新增字段
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
|
||||
$result = $this->db->update('wxshare_list', $data, ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
$qrcode = $this->db->get('wxshare_list', '*', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '更新成功',
|
||||
'data' => $qrcode
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '更新失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
// 无ID则新增
|
||||
else {
|
||||
$insertId = $this->db->insert('wxshare_list', [
|
||||
'share_link' => $share_link, // 原url改为share_link
|
||||
'name' => $share_link,
|
||||
'code' => $code,
|
||||
'share_title' => $share_title, // 新增字段
|
||||
'share_desc' => $share_desc, // 原description改为share_desc
|
||||
'share_img' => $share_img, // 新增字段
|
||||
'status' => 1,
|
||||
'views' => 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($insertId) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '创建成功',
|
||||
'data' => [
|
||||
'id' => $insertId,
|
||||
'share_link' => $share_link,
|
||||
'name' => $share_link,
|
||||
'code' => $code,
|
||||
'share_title' => $share_title,
|
||||
'share_desc' => $share_desc,
|
||||
'share_img' => $share_img,
|
||||
'status' => $status,
|
||||
'views' => 0
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public function settings() {
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = intval($_REQUEST['id'] ?? 0);
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// 获取第一条设置记录
|
||||
$row = $this->db->get('wxshare_settings', '*');
|
||||
|
||||
if ($row) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '设置不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 获取提交数据
|
||||
$wechat_name = trim($_POST['wechat_name'] ?? '');
|
||||
$wechat_account = trim($_POST['wechat_account'] ?? '');
|
||||
$appid = trim($_POST['appid'] ?? '');
|
||||
$appsecret = trim($_POST['appsecret'] ?? '');
|
||||
$token = trim($_POST['token'] ?? '');
|
||||
$encoding_aes_key = trim($_POST['encoding_aes_key'] ?? '');
|
||||
$qrcode_url = trim($_POST['qrcode_url'] ?? '');
|
||||
$wechat_type = trim($_POST['wechat_type'] ?? 'service');
|
||||
$status = isset($_POST['status']) ? 1 : 0;
|
||||
|
||||
// 验证必填项
|
||||
if (empty($wechat_name) || empty($wechat_account) || empty($appid) || empty($appsecret)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '公众号名称、原始ID、AppID和AppSecret为必填项'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
// 更新
|
||||
$data = [
|
||||
'wechat_name' => $wechat_name,
|
||||
'wechat_account' => $wechat_account,
|
||||
'appid' => $appid,
|
||||
'appsecret' => $appsecret,
|
||||
'token' => $token,
|
||||
'encoding_aes_key' => $encoding_aes_key,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'wechat_type' => $wechat_type,
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$result = $this->db->update('wxshare_settings', $data, ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
$setting = $this->db->get('wxshare_settings', '*', ['id' => $id]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置更新成功',
|
||||
'data' => $setting
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '更新失败或数据无变化'
|
||||
]);
|
||||
}
|
||||
|
||||
} else {
|
||||
// 新增
|
||||
$insertId = $this->db->insert('wxshare_settings', [
|
||||
'wechat_name' => $wechat_name,
|
||||
'wechat_account' => $wechat_account,
|
||||
'appid' => $appid,
|
||||
'appsecret' => $appsecret,
|
||||
'token' => $token,
|
||||
'encoding_aes_key' => $encoding_aes_key,
|
||||
'qrcode_url' => $qrcode_url,
|
||||
'wechat_type' => $wechat_type,
|
||||
'status' => 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($insertId) {
|
||||
$setting = $this->db->get('wxshare_settings', '*');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '设置创建成功',
|
||||
'data' => $setting
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '创建失败,请稍后重试'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
http_response_code(405);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '只支持 GET 和 POST 请求'
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '操作失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
|
||||
$this->checkLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$row = $this->db->get('wxshare_list', '*', ['id' => $id]);
|
||||
if (!$row) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '要删除的记录不存在'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
$result = $this->db->delete('wxshare_list', ['id' => $id]);
|
||||
|
||||
if ($result) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '记录已删除'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败,请稍后再试'
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '删除失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace Plugins\WxShare\Controllers\Web;
|
||||
use App\Core\WebBaseController;
|
||||
|
||||
class WxShareController extends WebBaseController {
|
||||
protected $pluginManager;
|
||||
protected $db;
|
||||
|
||||
public function __construct() {
|
||||
global $pluginManager;
|
||||
$this->pluginManager = $pluginManager;
|
||||
$this->db = $this->pluginManager->getDB();
|
||||
}
|
||||
|
||||
public function index($code) {
|
||||
// 引入 JSSDK 类文件
|
||||
$jssdkPath = dirname(__FILE__) . '/../../../WxShare/Controllers/lib/jssdk.php';
|
||||
if (!file_exists($jssdkPath)) {
|
||||
$this->showError("JSSDK 文件不存在:{$jssdkPath}");
|
||||
}
|
||||
require_once($jssdkPath);
|
||||
try {
|
||||
// 1. 从wxshare_settings表获取公众号配置(假设系统中只有一条配置记录)
|
||||
$settings = $this->db->get('wxshare_settings', '*');
|
||||
if (empty($settings)) {
|
||||
$this->showError('未找到公众号配置,请先在后台完成设置');
|
||||
}
|
||||
|
||||
// 2. 验证必要配置是否存在
|
||||
$requiredFields = ['appid', 'appsecret'];
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($settings[$field])) {
|
||||
$this->showError("公众号配置不完整,缺少:{$field}");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 从wxshare_list表获取token与URL的映射关系
|
||||
$share = $this->db->get('wxshare_list', '*', ['code' => $code] );
|
||||
|
||||
// 2. 判断是否存在该类型记录
|
||||
if (empty($share)) {
|
||||
$this->showError('未找到' . $code . '的URL配置,请先添加');
|
||||
exit; // 无此类型记录,停止执行
|
||||
}
|
||||
|
||||
// 3. 检查该记录是否启用(status = 1)
|
||||
if ($share['status'] != 1) {
|
||||
$this->showError('当前' . $code . '的URL未启用,请启用后再使用');
|
||||
exit;
|
||||
}
|
||||
if (isset($_GET['rep'])) {
|
||||
// 更新访问次数
|
||||
$this->db->update('wxshare_list', [
|
||||
'views[+]' => 1
|
||||
], [
|
||||
'code' => $code,
|
||||
'status' => 1
|
||||
]);
|
||||
// 跳转
|
||||
header("Location: " . $share['share_link']);
|
||||
exit;
|
||||
}
|
||||
$url = $share['share_link'];
|
||||
$jssdk = new \Plugins\WxShare\Controllers\lib\JSSDK($settings['appid'], $settings['appsecret']);
|
||||
$signPackage = $jssdk->GetSignPackage();
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo '<script>alert("错误: '. addslashes($e->getMessage()). '"); window.close();</script>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// 传数据给视图
|
||||
include __DIR__ . '/../../Views/Web/index.php';
|
||||
}
|
||||
|
||||
public function redirect($code) {
|
||||
$row = $this->db->get('wxshare_list', '*', ['code' => $code]);
|
||||
|
||||
if ($row) {
|
||||
// 检查链接是否处于激活状态
|
||||
if ($row['is_active'] != 1) {
|
||||
$this->showError($code . ' 此链接已被停用!');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 若激活,则更新访问量并跳转
|
||||
$update = $this->db->update('wxshare_list', ['views[+]' => 1 ], ['id' => $row['id'] ]);
|
||||
if ($update->rowCount() > 0) {
|
||||
header("Location: " . $row['share_link']);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$this->showError( $code . ' 此链接不存在!');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php exit();?>{"expire_time":1755715644,"access_token":"95_eDc_CzaW9yYbHNhPFs5_UmwQIjx-j25vgX2a-m5scHEaxnFNg5d3FufMFvGS2eegNbhRhh01duV_i23i5zpz96UKWyM7f-rlohmxzqSjqRuYhhwYSZqRKCqZ1zsIRIhAHAXGX"}
|
||||
@@ -0,0 +1 @@
|
||||
<?php exit();?>{"expire_time":1755715644,"jsapi_ticket":"7mo9kzLF0zXvfXKd2ScDpJkaYOCMtHVFZ1MqnrYmJLj66DEsamUjaZ0-iUq3MJWpwWiTm_vt903SF5b7Y9dB2w"}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
namespace Plugins\WxShare\Controllers\lib;
|
||||
class JSSDK {
|
||||
private $appId;
|
||||
private $appSecret;
|
||||
// 缓存文件路径(使用绝对路径避免问题)
|
||||
private $cacheDir;
|
||||
|
||||
public function __construct($appId, $appSecret) {
|
||||
$this->appId = $appId;
|
||||
$this->appSecret = $appSecret;
|
||||
// 初始化缓存目录(与jssdk.php同目录)
|
||||
$this->cacheDir = dirname(__FILE__) . '/';
|
||||
// 确保缓存目录可写
|
||||
$this->checkCacheDir();
|
||||
}
|
||||
|
||||
// 检查缓存目录是否存在且可写
|
||||
private function checkCacheDir() {
|
||||
if (!is_dir($this->cacheDir)) {
|
||||
mkdir($this->cacheDir, 0755, true);
|
||||
}
|
||||
if (!is_writable($this->cacheDir)) {
|
||||
echo "缓存目录不可写:{$this->cacheDir}.需要手动改写权限 777";
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并创建缓存文件
|
||||
private function checkCacheFile($filename) {
|
||||
$filePath = $this->cacheDir . $filename;
|
||||
// 如果文件不存在则创建并初始化
|
||||
if (!file_exists($filePath)) {
|
||||
$initialData = json_encode([
|
||||
'expire_time' => 0,
|
||||
'access_token' => '',
|
||||
'jsapi_ticket' => ''
|
||||
]);
|
||||
$this->set_php_file($filename, $initialData);
|
||||
}
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
public function getSignPackage() {
|
||||
$jsapiTicket = $this->getJsApiTicket();
|
||||
|
||||
// 动态获取当前URL
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
|
||||
|
||||
$timestamp = time();
|
||||
$nonceStr = $this->createNonceStr();
|
||||
|
||||
// 按ASCII码升序排序
|
||||
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
|
||||
$signature = sha1($string);
|
||||
|
||||
return [
|
||||
"appId" => $this->appId,
|
||||
"nonceStr" => $nonceStr,
|
||||
"timestamp" => $timestamp,
|
||||
"url" => $url,
|
||||
"signature" => $signature,
|
||||
"rawString" => $string
|
||||
];
|
||||
}
|
||||
|
||||
private function createNonceStr($length = 16) {
|
||||
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
$str = "";
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
private function getJsApiTicket() {
|
||||
// 先检查并创建缓存文件
|
||||
$this->checkCacheFile("jsapi_ticket.php");
|
||||
|
||||
$data = json_decode($this->get_php_file("jsapi_ticket.php"));
|
||||
|
||||
// 处理空数据或过期情况
|
||||
if (empty($data) || $data->expire_time < time()) {
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
|
||||
if (isset($res->ticket)) {
|
||||
$data = new \stdClass(); // 初始化空对象
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->jsapi_ticket = $res->ticket;
|
||||
$this->set_php_file("jsapi_ticket.php", json_encode($data));
|
||||
} else {
|
||||
throw new \Exception("获取jsapi_ticket失败: " . json_encode($res));
|
||||
}
|
||||
}
|
||||
|
||||
return $data->jsapi_ticket ?? '';
|
||||
}
|
||||
|
||||
private function getAccessToken() {
|
||||
// 先检查并创建缓存文件
|
||||
$this->checkCacheFile("access_token.php");
|
||||
|
||||
$data = json_decode($this->get_php_file("access_token.php"));
|
||||
|
||||
// 处理空数据或过期情况
|
||||
if (empty($data) || $data->expire_time < time()) {
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
|
||||
if (isset($res->access_token)) {
|
||||
$data = new \stdClass(); // 初始化空对象
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->access_token = $res->access_token;
|
||||
$this->set_php_file("access_token.php", json_encode($data));
|
||||
} else {
|
||||
throw new \Exception("获取access_token失败: " . json_encode($res));
|
||||
}
|
||||
}
|
||||
|
||||
return $data->access_token ?? '';
|
||||
}
|
||||
|
||||
private function httpGet($url) {
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, true);
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
$res = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function get_php_file($filename) {
|
||||
$filePath = $this->cacheDir . $filename;
|
||||
return trim(substr(file_get_contents($filePath), 15));
|
||||
}
|
||||
|
||||
private function set_php_file($filename, $content) {
|
||||
$filePath = $this->cacheDir . $filename;
|
||||
$fp = fopen($filePath, "w");
|
||||
fwrite($fp, "<?php exit();?>" . $content);
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
Executable
+740
@@ -0,0 +1,740 @@
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-share-alt text-primary mr-3"></i>
|
||||
微信分享管理中心
|
||||
</h1>
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<!-- 消息提示框 -->
|
||||
<div id="message" class="mb-4 px-4 py-3 rounded-lg hidden"></div>
|
||||
<!-- 选项卡导航 -->
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<ul class="flex flex-wrap -mb-px" id="tabs" role="tablist">
|
||||
<li class="mr-2" role="presentation">
|
||||
<button id="list-tab" class="inline-block py-4 px-5 border-b-2 border-primary text-sm font-medium text-primary" onclick="switchTab('list')" aria-selected="true">
|
||||
微信分享列表
|
||||
</button>
|
||||
</li>
|
||||
<li class="mr-2" role="presentation">
|
||||
<button id="settings-tab" class="inline-block py-4 px-5 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300" onclick="switchTab('settings')" aria-selected="false">
|
||||
系统设置
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 分享列表内容 -->
|
||||
<div id="list-content" class="tab-content">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700">分享列表</h2>
|
||||
<!-- 创建新分享按钮 -->
|
||||
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>新增分享</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名称</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分享编码</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">跳转地址</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">访问</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="shortlinkList">
|
||||
<!-- 内容将通过JavaScript动态生成 -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="loading" class="hidden py-10 text-center"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>
|
||||
<div id="empty" class="hidden">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-12">没有找到分享</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页和状态控件 -->
|
||||
<div id="pagination" class="flex justify-between items-center mt-6 hidden">
|
||||
<div class="text-sm text-gray-500">
|
||||
显示 <span id="showingRange">0-0</span> 条,共 <span id="totalItems">0</span> 条
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="prevPage" class="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50" disabled>上一页</button>
|
||||
<div id="pageNumbers" class="flex space-x-1"></div>
|
||||
<button id="nextPage" class="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50" disabled>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设置选项卡内容 -->
|
||||
<div id="settings-content" class="tab-content hidden">
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-6">系统设置</h2>
|
||||
<form id="settingsForm" class="space-y-6">
|
||||
<input type="hidden" id="settingsId" name="id">
|
||||
<!-- 公众号基本信息 -->
|
||||
<div class="bg-gray-50 p-5 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
|
||||
<i class="fab fa-weixin text-primary mr-2"></i>公众号基本信息
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="wechat_name" class="block text-sm font-medium text-gray-700 mb-1">公众号名称 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="wechat_name" name="wechat_name" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="请输入公众号名称">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wechat_account" class="block text-sm font-medium text-gray-700 mb-1">公众号原始ID <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="wechat_account" name="wechat_account" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="格式为gh_xxxx">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wechat_type" class="block text-sm font-medium text-gray-700 mb-1">公众号类型 <span class="text-red-500">*</span></label>
|
||||
<select id="wechat_type" name="wechat_type" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors">
|
||||
<option value="subscription">订阅号</option>
|
||||
<option value="service" selected>服务号</option>
|
||||
<option value="enterprise">企业号</option>
|
||||
<option value="test">测试号</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="qrcode_url" class="block text-sm font-medium text-gray-700 mb-1">公众号二维码URL</label>
|
||||
<div class="flex items-center">
|
||||
<input type="url" id="qrcode_url" name="qrcode_url"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="https://example.com/share.jpg">
|
||||
<div class="relative">
|
||||
<button type="button"
|
||||
class="w-10 h-10 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center justify-center"
|
||||
onclick="OpenGallery('qrcode_url', 'qrimg')">
|
||||
<img src="https://cdn-icons-png.flaticon.com/128/10054/10054290.png" alt="预览图" class=" object-cover rounded">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img class="w-50 h-50" id='qrimg' src="" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="statuss" name="status" value="1" class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用当前公众号配置</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公众号接口配置 -->
|
||||
<div class="bg-gray-50 p-5 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
|
||||
<i class="fa fa-plug text-primary mr-2"></i>接口配置信息
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="appid" class="block text-sm font-medium text-gray-700 mb-1">AppID <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="appid" name="appid" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="公众号的AppID">
|
||||
</div>
|
||||
<div>
|
||||
<label for="appsecret" class="block text-sm font-medium text-gray-700 mb-1">AppSecret <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="appsecret" name="appsecret" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="公众号的AppSecret">
|
||||
</div>
|
||||
<div>
|
||||
<label for="token" class="block text-sm font-medium text-gray-700 mb-1">Token</label>
|
||||
<input type="text" id="token" name="token" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="用于接口调用的Token">
|
||||
<p class="mt-1 text-xs text-gray-500">由开发者自定义,用于生成签名</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="encoding_aes_key" class="block text-sm font-medium text-gray-700 mb-1">EncodingAESKey</label>
|
||||
<input type="text" id="encoding_aes_key" name="encoding_aes_key" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="消息加密密钥">
|
||||
<p class="mt-1 text-xs text-gray-500">消息加解密时使用,43位字符</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200">
|
||||
<button type="button" id="saveSettingsBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表单弹窗背景 -->
|
||||
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
|
||||
<!-- 分享表单弹窗 -->
|
||||
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-plus-circle text-primary mr-2"></i>
|
||||
创建新分享
|
||||
</h3>
|
||||
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="shortlinkForm" class="space-y-5">
|
||||
<input type="hidden" id="shortlinkId" name="id">
|
||||
<input type="hidden" id="code" name="code" >
|
||||
<div>
|
||||
<label for="share_title" class="block text-sm font-medium text-gray-700 mb-1">分享标题</label>
|
||||
<input type="text" id="share_title" name="share_title"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入分享时显示的标题">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="share_desc" class="block text-sm font-medium text-gray-700 mb-1">分享简介</label>
|
||||
<textarea id="share_desc" name="share_desc" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-none"
|
||||
placeholder="请输入详细描述信息"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="share_img" class="block text-sm font-medium text-gray-700 mb-1">分享封面图URL</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="url" id="share_img" name="share_img"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="https://example.com/share.jpg">
|
||||
<div class="relative">
|
||||
<button type="button"
|
||||
class="bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2"
|
||||
onclick="OpenGallery('share_img', 'image-preview')">
|
||||
<img id="image-preview" src="https://cdn-icons-png.flaticon.com/128/10054/10054290.png" alt="预览图" class="w-10 h-10 object-cover rounded">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="share_link" class="block text-sm font-medium text-gray-700 mb-1">跳转地址 </label>
|
||||
<input type="url" id="share_link" name="share_link" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="https://example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="status" name="status" value="1" checked
|
||||
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用状态</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="submitBtn" type="button" class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存分享
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qrCodeModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
<div class="bg-white p-6 rounded-lg shadow-xl max-w-xs w-full mx-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4 text-center">扫码分享</h3>
|
||||
<div class="flex justify-center mb-4" id="qrCodeContainer"></div>
|
||||
<p onclick="copyToClipboard(this.innerText, '分享链接')"id="textId" class="text-sm text-gray-500 text-center mb-4"></p>
|
||||
<button onclick="hideQrCode()" class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 py-2 px-4 rounded transition-colors">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// 分页相关功能
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 1,
|
||||
totalPages = 1;
|
||||
|
||||
// 分页DOM元素
|
||||
const listEl = document.getElementById('shortlinkList');
|
||||
const [paginationEl, prevBtn, nextBtn, pageNumbers] = ['pagination', 'prevPage', 'nextPage', 'pageNumbers'].map(id => document.getElementById(id));
|
||||
const [rangeEl, totalEl, loadingEl, emptyEl] = ['showingRange', 'totalItems', 'loading', 'empty'].map(id => document.getElementById(id));
|
||||
|
||||
// 初始化分页
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadPage(1);
|
||||
prevBtn.onclick = () => currentPage > 1 && loadPage(currentPage - 1);
|
||||
nextBtn.onclick = () => currentPage < totalPages && loadPage(currentPage + 1);
|
||||
});
|
||||
|
||||
// 加载分页数据
|
||||
async function loadPage(page) {
|
||||
// 显示加载状态
|
||||
loadingEl.classList.remove('hidden');
|
||||
listEl.innerHTML = '';
|
||||
paginationEl.classList.add('hidden');
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// 请求数据
|
||||
const res = await fetch(`/admin/wxshare/list?page=${page}&page_size=${PAGE_SIZE}`);
|
||||
const { data } = await res.json();
|
||||
// 更新分页信息
|
||||
currentPage = data.current_page;
|
||||
totalPages = data.total_pages;
|
||||
totalEl.textContent = data.total_items;
|
||||
|
||||
// 渲染列表
|
||||
if (data.items.length) {
|
||||
data.items.forEach(link => {
|
||||
const statusClass = link.status == 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
const statusText = link.status == 1 ? '启用' : '停用';
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-gray-50 transition-colors';
|
||||
tr.setAttribute('data-id', link.id);
|
||||
tr.innerHTML = `
|
||||
<!-- 分享信息单元格 - 包含名称和描述 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(link.share_title)}</div>
|
||||
<div class="text-xs text-gray-500 truncate max-w-xs">
|
||||
${escapeHtml(link.share_desc)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 分享编码 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap ">
|
||||
<div class="text-sm text-primary truncate max-w-md cursor-pointer" onclick="showQrCode('${escapeHtml(getTypeName(link.code))}')">
|
||||
${link.code}
|
||||
<i class="fa fa-qrcode ml-1 opacity-70"></i>
|
||||
</div>
|
||||
</td>
|
||||
<!-- 跳转地址 - 小屏幕隐藏 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<div class="text-sm text-gray-500 truncate max-w-md">
|
||||
${escapeHtml(link.share_link)}
|
||||
</div>
|
||||
</td>
|
||||
<!-- 状态 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<!-- 访问计数 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">
|
||||
${link.views} 次
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button class="edit-btn text-gray-500 hover:text-blue-500"
|
||||
data-id="${link.id}" title="编辑">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button class="delete-btn text-gray-500 hover:text-red-500"
|
||||
data-id="${link.id}" title="删除">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
listEl.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
listEl.innerHTML = `<tr><td colspan="5" class="text-center py-12">没有查到分享;请创建后查看!</td></tr>`;
|
||||
}
|
||||
|
||||
// 更新分页控件
|
||||
rangeEl.textContent = `${(page-1)*PAGE_SIZE+1}-${Math.min(page*PAGE_SIZE, data.total_items)}`;
|
||||
renderPageNumbers();
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
paginationEl.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<tr><td colspan="5" class="text-center py-12">加载失败: ${e.message}</td></tr>`;
|
||||
} finally {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型名称
|
||||
function generateCode(length = 8) {
|
||||
const chars = '1234567890ACDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let code = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function getTypeName(code) {
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
return `${protocol}//${host}/wxshare/${code}`;
|
||||
}
|
||||
|
||||
// 渲染页码按钮
|
||||
function renderPageNumbers() {
|
||||
pageNumbers.innerHTML = '';
|
||||
const start = Math.max(1, currentPage - 2);
|
||||
const end = Math.min(totalPages, start + 4);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `px-3 py-1 rounded ${i === currentPage ? 'bg-primary text-white' : 'border'}`;
|
||||
btn.textContent = i;
|
||||
btn.onclick = () => loadPage(i);
|
||||
pageNumbers.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义函数
|
||||
function escapeHtml(str) {
|
||||
return str ? str.toString().replace(/[&<>"']/g, c => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
} [c])) : '';
|
||||
}
|
||||
|
||||
// 二维码相关功能
|
||||
function showQrCode(url) {
|
||||
// 清空之前的二维码
|
||||
document.getElementById('qrCodeContainer').innerHTML = '';
|
||||
|
||||
// 生成新的二维码
|
||||
QRCode.toCanvas(url, {
|
||||
width: 200,
|
||||
margin: 1
|
||||
}, function(error, canvas) {
|
||||
if (error) {
|
||||
console.error('生成二维码失败:', error);
|
||||
return;
|
||||
}
|
||||
document.getElementById('qrCodeContainer').appendChild(canvas);
|
||||
});
|
||||
|
||||
// 显示弹窗
|
||||
document.getElementById('qrCodeModal').classList.remove('hidden');
|
||||
document.getElementById('textId').textContent = url;
|
||||
// 阻止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideQrCode() {
|
||||
console.log('ok');
|
||||
document.getElementById('qrCodeModal').classList.add('hidden');
|
||||
// 恢复页面滚动
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 点击弹窗外部关闭
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 二维码弹窗事件绑定
|
||||
const qrCodeModal = document.getElementById('qrCodeModal');
|
||||
if (qrCodeModal) {
|
||||
qrCodeModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideQrCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 选项卡切换功能
|
||||
function switchTab(tabName) {
|
||||
document.getElementById('list-content').classList.add('hidden');
|
||||
document.getElementById('settings-content').classList.add('hidden');
|
||||
|
||||
document.getElementById('list-tab').classList.remove('border-primary', 'text-primary');
|
||||
document.getElementById('list-tab').classList.add('border-transparent', 'text-gray-500');
|
||||
document.getElementById('settings-tab').classList.remove('border-primary', 'text-primary');
|
||||
document.getElementById('settings-tab').classList.add('border-transparent', 'text-gray-500');
|
||||
|
||||
document.getElementById(`${tabName}-content`).classList.remove('hidden');
|
||||
document.getElementById(`${tabName}-tab`).classList.remove('border-transparent', 'text-gray-500');
|
||||
document.getElementById(`${tabName}-tab`).classList.add('border-primary', 'text-primary');
|
||||
|
||||
if (tabName === 'settings' && !window.settingsLoaded) {
|
||||
loadSettings();
|
||||
window.settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 主功能逻辑
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const formModal = document.getElementById('formModal');
|
||||
const formBackdrop = document.getElementById('formBackdrop');
|
||||
const openFormBtn = document.getElementById('openFormBtn');
|
||||
const closeFormBtn = document.getElementById('closeFormBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const formTitle = document.getElementById('formTitle');
|
||||
const shortlinkForm = document.getElementById('shortlinkForm');
|
||||
const shortlinkList = document.getElementById('shortlinkList');
|
||||
const settingsForm = document.getElementById('settingsForm');
|
||||
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||
window.settingsLoaded = false;
|
||||
|
||||
// 检查必要元素
|
||||
function checkElements() {
|
||||
const elements = [formModal, formBackdrop, openFormBtn, closeFormBtn, cancelBtn, submitBtn];
|
||||
const missing = elements.filter(el => !el);
|
||||
if (missing.length > 0) {
|
||||
console.error('缺少必要的DOM元素,弹窗功能无法正常工作');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 打开表单弹窗
|
||||
function openFormModal() {
|
||||
if (!checkElements()) return;
|
||||
resetForm();
|
||||
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
void formModal.offsetWidth; // 强制重绘
|
||||
document.getElementById('code').value = generateCode();
|
||||
}
|
||||
|
||||
// 关闭表单弹窗
|
||||
function closeFormModal() {
|
||||
if (!checkElements()) return;
|
||||
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
shortlinkForm.reset();
|
||||
document.getElementById('shortlinkId').value = '';
|
||||
document.getElementById('image-preview').src = 'https://cdn-icons-png.flaticon.com/128/10054/10054290.png';
|
||||
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新分享';
|
||||
submitBtn.innerHTML = '保存分享';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
window.loadSettings = async function() {
|
||||
try {
|
||||
const response = await fetch('/admin/wxshare/settings');
|
||||
if (!response.ok) throw new Error('获取设置失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const settings = data.data;
|
||||
|
||||
// 回填公众号基本信息
|
||||
document.getElementById('settingsId').value = settings.id || '';
|
||||
document.getElementById('wechat_name').value = settings.wechat_name || '';
|
||||
document.getElementById('wechat_account').value = settings.wechat_account || '';
|
||||
document.getElementById('wechat_type').value = settings.wechat_type || 'service';
|
||||
document.getElementById('qrcode_url').value = settings.qrcode_url || '';
|
||||
document.getElementById('qrimg').src = settings.qrcode_url || '';
|
||||
document.getElementById('statuss').checked = Boolean(Number(settings.status));
|
||||
|
||||
// 回填接口配置信息
|
||||
document.getElementById('appid').value = settings.appid || '';
|
||||
document.getElementById('appsecret').value = settings.appsecret || '';
|
||||
document.getElementById('token').value = settings.token || '';
|
||||
document.getElementById('encoding_aes_key').value = settings.encoding_aes_key || '';
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm(formElement) {
|
||||
if (formElement.id === 'shortlinkForm') {
|
||||
const name = formElement.querySelector('#name').value.trim();
|
||||
const shareLink = formElement.querySelector('#share_link').value.trim();
|
||||
const code = formElement.querySelector('#code').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showMessage('请输入分享名称', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shareLink) {
|
||||
showMessage('请输入跳转地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 简单URL验证
|
||||
const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/;
|
||||
if (!urlPattern.test(shareLink)) {
|
||||
showMessage('请输入有效的URL地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (formElement.id === 'settingsForm') {
|
||||
// 验证实际必填项
|
||||
const wechatName = formElement.querySelector('#wechat_name').value.trim();
|
||||
const wechatAccount = formElement.querySelector('#wechat_account').value.trim();
|
||||
const appid = formElement.querySelector('#appid').value.trim();
|
||||
const appsecret = formElement.querySelector('#appsecret').value.trim();
|
||||
|
||||
if (!wechatName) { showMessage('请输入公众号名称', 'error'); return false; }
|
||||
if (!wechatAccount) { showMessage('请输入公众号原始ID', 'error'); return false; }
|
||||
if (!appid) { showMessage('请输入AppID', 'error'); return false; }
|
||||
if (!appsecret) { showMessage('请输入AppSecret', 'error'); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
async function submitFormData(url, formElement, successMsg) {
|
||||
if (!validateForm(formElement)) return;
|
||||
|
||||
const submitButton = formElement.id === 'shortlinkForm' ? submitBtn : document.getElementById('saveSettingsBtn');
|
||||
const originalText = submitButton.innerHTML;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(formElement);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage(data.message || successMsg, 'success');
|
||||
closeFormModal();
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
|
||||
} else {
|
||||
throw new Error(data.message || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分享数据(编辑用)
|
||||
async function loadShortlinkData(id) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/wxshare/get/${id}`);
|
||||
if (!response.ok) throw new Error('获取数据失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { id, name, share_link, code, share_desc, share_title, share_img, status } = data.data;
|
||||
document.getElementById('shortlinkId').value = id;
|
||||
document.getElementById('share_link').value = share_link || '';
|
||||
document.getElementById('code').value = code || '';
|
||||
document.getElementById('share_desc').value = share_desc || '';
|
||||
document.getElementById('share_title').value = share_title || '';
|
||||
document.getElementById('share_img').value = share_img || '';
|
||||
document.getElementById('image-preview').src = share_img || '';
|
||||
document.getElementById('status').checked = Boolean(Number(status));
|
||||
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑分享';
|
||||
} else {
|
||||
throw new Error(data.message || '获取数据失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
closeFormModal();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存分享';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分享
|
||||
async function deleteLink(id) {
|
||||
if (!confirm('确定要删除该分享吗?此操作不可恢复!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/wxshare/delete/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage('分享已删除', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
if (checkElements()) {
|
||||
// 打开表单
|
||||
openFormBtn.addEventListener('click', openFormModal);
|
||||
|
||||
// 关闭表单
|
||||
closeFormBtn.addEventListener('click', closeFormModal);
|
||||
cancelBtn.addEventListener('click', closeFormModal);
|
||||
formBackdrop.addEventListener('click', closeFormModal);
|
||||
|
||||
// 分享表单提交
|
||||
submitBtn.addEventListener('click', function() {
|
||||
submitFormData('/admin/wxshare/update', shortlinkForm, '分享保存成功');
|
||||
});
|
||||
|
||||
// 设置表单提交
|
||||
saveSettingsBtn.addEventListener('click', function() {
|
||||
submitFormData('/admin/wxshare/settings', settingsForm, '设置保存成功');
|
||||
});
|
||||
|
||||
// 列表操作事件委托
|
||||
shortlinkList.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.edit-btn');
|
||||
const deleteBtn = e.target.closest('.delete-btn');
|
||||
|
||||
if (editBtn) {
|
||||
const id = editBtn.getAttribute('data-id');
|
||||
if (id) {
|
||||
openFormModal();
|
||||
// 监听动画结束后加载数据
|
||||
const loadData = () => {
|
||||
loadShortlinkData(id);
|
||||
formModal.removeEventListener('transitionend', loadData);
|
||||
};
|
||||
formModal.addEventListener('transitionend', loadData, { once: true });
|
||||
}
|
||||
} else if (deleteBtn) {
|
||||
const id = deleteBtn.getAttribute('data-id');
|
||||
if (id) deleteLink(id);
|
||||
}
|
||||
});
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && !formModal.classList.contains('invisible')) {
|
||||
closeFormModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止表单默认提交
|
||||
shortlinkForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
$title = htmlspecialchars($share['share_title']);
|
||||
$desc = htmlspecialchars($share['share_desc']);
|
||||
$image = htmlspecialchars($share['share_img']);
|
||||
$link = htmlspecialchars($share['share_link']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>微信卡片分享预览</title>
|
||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
<style>
|
||||
/* 基础样式重置 */
|
||||
* {margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {font-family: "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 15px; background-color: #f7f7f7; color: #333; position: relative; min-height: 100vh; }
|
||||
h1 {text-align: center; margin: 20px 0 25px; font-weight: 500; font-size: 18px; color: #333; }
|
||||
.status-info {text-align: center; color: #666; font-size: 15px; padding: 10px; margin: 0 auto 20px; line-height: 1.5; }
|
||||
.blog-list-container {max-width: 640px; margin: 0 auto; }
|
||||
.blog-card {display: flex; max-width: 500px; margin: 0 auto; flex-direction: row; background-color: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 15px; transition: transform 0.2s, box-shadow 0.2s; height: 120px;}
|
||||
.blog-card:hover {transform: translateY(-2px); box-shadow: 0 3px 8px rgba(0,0,0,0.12); }
|
||||
.blog-image {width: 120px; height: 100%; flex-shrink: 0; object-fit: cover; }
|
||||
.blog-content {flex: 1;padding: 8px 18px; display: flex; flex-direction: column; justify-content: center; }
|
||||
.blog-title {font-size: 18px; color: #333; line-height: 1.5; margin-bottom: 5px; max-height: 48px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.blog-desc {font-size: 15px; color: #666; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
||||
.blog-meta {display: table; justify-content: space-between; align-items: center;margin:0 auto; }
|
||||
.blog-link {font-size: 12px; color: #999; }
|
||||
.share-guide {position: fixed; top: -25px;right: 0px; z-index: 999; pointer-events: none; }
|
||||
.share-guide img {width: 120px;height: auto; }
|
||||
@media screen and (max-width: 375px) {.blog-card {height: 100px; } .blog-image {width: 110px; } .blog-content {padding: 8px 15px; } .blog-title {font-size: 15px; margin-bottom: 3px; } .blog-desc {font-size: 12px; } .share-guide img {width: 100px;} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 分享引导GIF动画 -->
|
||||
<div class="share-guide">
|
||||
<!-- 使用指向右上角的引导动画GIF -->
|
||||
<img src="https://yanxuan.nosdn.127.net/c1bf3641f8cc21cc05a65bc978cf819b.gif" alt="点击分享引导">
|
||||
</div>
|
||||
|
||||
<h1>卡片分享预览</h1>
|
||||
<div class="blog-list-container">
|
||||
<div class="blog-card">
|
||||
<img src="<?= $image ?>" class="blog-image" alt="分享图片">
|
||||
<div class="blog-content">
|
||||
<div class="blog-title"><?= $title ?></div>
|
||||
<div class="blog-desc"><?= $desc ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="blog-meta">
|
||||
<div class="blog-link">跳转到:<a target="_blank" href="<?= $link ?>" ><?= $link ?></a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-info">
|
||||
提示:点击右上角菜单选择分享
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 从后端获取签名配置
|
||||
const signPackage = {
|
||||
appId: "<?php echo $signPackage['appId']?>",
|
||||
timestamp: "<?php echo $signPackage['timestamp']?>",
|
||||
nonceStr: "<?php echo $signPackage['nonceStr']?>",
|
||||
signature: "<?php echo $signPackage['signature']?>",
|
||||
url: "<?php echo $signPackage['url']?>"
|
||||
};
|
||||
|
||||
// 配置微信JS-SDK
|
||||
wx.config({
|
||||
debug: false,
|
||||
appId: signPackage.appId,
|
||||
timestamp: signPackage.timestamp,
|
||||
nonceStr: signPackage.nonceStr,
|
||||
signature: signPackage.signature,
|
||||
jsApiList: [
|
||||
'updateAppMessageShareData',
|
||||
'updateTimelineShareData',
|
||||
'onMenuShareAppMessage',
|
||||
'onMenuShareTimeline'
|
||||
]
|
||||
});
|
||||
|
||||
// 生成带参数的分享链接
|
||||
function getShareLink(baseUrl) {
|
||||
const param = '?rep';
|
||||
if (baseUrl && baseUrl.includes('?')) {
|
||||
return baseUrl + '&' + param.substring(1);
|
||||
}
|
||||
return (baseUrl || window.location.href) + param;
|
||||
}
|
||||
|
||||
// 分享配置参数
|
||||
const shareConfig = {
|
||||
title: '<?= $title ?>',
|
||||
desc: '<?= $desc ?>',
|
||||
link: getShareLink(signPackage.url),
|
||||
imgUrl: '<?= $image ?: 'https://picsum.photos/400/300' ?>'
|
||||
};
|
||||
|
||||
// JS-SDK初始化成功回调
|
||||
wx.ready(function() {
|
||||
// 新接口配置
|
||||
wx.updateAppMessageShareData({
|
||||
title: shareConfig.title,
|
||||
desc: shareConfig.desc,
|
||||
link: shareConfig.link,
|
||||
imgUrl: shareConfig.imgUrl,
|
||||
success: function() {
|
||||
console.log('分享给朋友配置成功');
|
||||
}
|
||||
});
|
||||
|
||||
wx.updateTimelineShareData({
|
||||
title: shareConfig.title,
|
||||
link: shareConfig.link,
|
||||
imgUrl: shareConfig.imgUrl,
|
||||
success: function() {
|
||||
console.log('分享到朋友圈配置成功');
|
||||
}
|
||||
});
|
||||
|
||||
// 兼容旧版本接口
|
||||
if (wx.onMenuShareAppMessage) {
|
||||
wx.onMenuShareAppMessage(shareConfig);
|
||||
}
|
||||
if (wx.onMenuShareTimeline) {
|
||||
wx.onMenuShareTimeline({
|
||||
title: shareConfig.title,
|
||||
link: shareConfig.link,
|
||||
imgUrl: shareConfig.imgUrl
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// JS-SDK配置失败回调
|
||||
wx.error(function(res) {
|
||||
console.error('微信JS-SDK配置失败:', res.errMsg);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
class JSSDK {
|
||||
private $appId;
|
||||
private $appSecret;
|
||||
|
||||
public function __construct($appId, $appSecret) {
|
||||
$this->appId = $appId;
|
||||
$this->appSecret = $appSecret;
|
||||
}
|
||||
|
||||
public function getSignPackage() {
|
||||
$jsapiTicket = $this->getJsApiTicket();
|
||||
|
||||
// 注意 URL 一定要动态获取,不能 hardcode.
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
|
||||
|
||||
$timestamp = time();
|
||||
$nonceStr = $this->createNonceStr();
|
||||
|
||||
// 这里参数的顺序要按照 key 值 ASCII 码升序排序
|
||||
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
|
||||
|
||||
$signature = sha1($string);
|
||||
|
||||
$signPackage = array(
|
||||
"appId" => $this->appId,
|
||||
"nonceStr" => $nonceStr,
|
||||
"timestamp" => $timestamp,
|
||||
"url" => $url,
|
||||
"signature" => $signature,
|
||||
"rawString" => $string
|
||||
);
|
||||
return $signPackage;
|
||||
}
|
||||
|
||||
private function createNonceStr($length = 16) {
|
||||
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
$str = "";
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
private function getJsApiTicket() {
|
||||
// jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
|
||||
$data = json_decode($this->get_php_file("jsapi_ticket.php"));
|
||||
if ($data->expire_time < time()) {
|
||||
$accessToken = $this->getAccessToken();
|
||||
// 如果是企业号用以下 URL 获取 ticket
|
||||
// $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=$accessToken";
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
$ticket = $res->ticket;
|
||||
if ($ticket) {
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->jsapi_ticket = $ticket;
|
||||
$this->set_php_file("jsapi_ticket.php", json_encode($data));
|
||||
}
|
||||
} else {
|
||||
$ticket = $data->jsapi_ticket;
|
||||
}
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
private function getAccessToken() {
|
||||
// access_token 应该全局存储与更新,以下代码以写入到文件中做示例
|
||||
$data = json_decode($this->get_php_file("access_token.php"));
|
||||
if ($data->expire_time < time()) {
|
||||
// 如果是企业号用以下URL获取access_token
|
||||
// $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$this->appId&corpsecret=$this->appSecret";
|
||||
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
|
||||
$res = json_decode($this->httpGet($url));
|
||||
$access_token = $res->access_token;
|
||||
if ($access_token) {
|
||||
$data->expire_time = time() + 7000;
|
||||
$data->access_token = $access_token;
|
||||
$this->set_php_file("access_token.php", json_encode($data));
|
||||
}
|
||||
} else {
|
||||
$access_token = $data->access_token;
|
||||
}
|
||||
return $access_token;
|
||||
}
|
||||
|
||||
private function httpGet($url) {
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
|
||||
// 为保证第三方服务器与微信服务器之间数据传输的安全性,所有微信接口采用https方式调用,必须使用下面2行代码打开ssl安全校验。
|
||||
// 如果在部署过程中代码在此处验证失败,请到 http://curl.haxx.se/ca/cacert.pem 下载新的证书判别文件。
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, true);
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
$res = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function get_php_file($filename) {
|
||||
return trim(substr(file_get_contents($filename), 15));
|
||||
}
|
||||
private function set_php_file($filename, $content) {
|
||||
$fp = fopen($filename, "w");
|
||||
fwrite($fp, "<?php exit();?>" . $content);
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>扫码结果</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; display: flex; flex-direction: column; height: 100vh; margin: 0; background-color: #f4f4f4; }
|
||||
.result-container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); text-align: center; width: 90%; max-width: 400px; margin: auto; cursor: pointer; margin-top: 200px; }
|
||||
.back-button-container { position: fixed; bottom: 0; width: 100%; text-align: center; padding: 20px 0; background-color: #007BFF; color: white; cursor: pointer; transition: background-color 0.3s ease; }
|
||||
.back-button-container:hover { background-color: #0056b3; }
|
||||
.copy-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 4px; opacity: 0; transition: opacity 0.3s ease; }
|
||||
.result-container p:nth-child(2) { word-wrap: break-word; word-break: break-all; white-space: pre-wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="result-container" onclick="copyResult()">
|
||||
<p id="scan-result">扫描结果:</p>
|
||||
<p><?php echo $scanResult; ?></p>
|
||||
</div>
|
||||
<div class="back-button-container" onclick="history.back()">扫一扫</div>
|
||||
<div id="copy-toast" class="copy-toast">复制成功</div>
|
||||
<script>
|
||||
function copyResult() {
|
||||
const result = document.querySelector('.result-container p:nth-child(2)');
|
||||
const text = result.textContent;
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
const toast = document.getElementById('copy-toast');
|
||||
toast.style.opacity = 1;
|
||||
setTimeout(() => { toast.style.opacity = 0; }, 2000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WxShare
|
||||
* Description: 微信分享卡片的创建。可动态调态分享后的卡片跳转的地址。
|
||||
* Version: 1.0.0
|
||||
* Author: JuheDev
|
||||
* Plugin URL: https://plugins.juhe.me/wxshare
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'menus' => [
|
||||
[
|
||||
'title' => '微信分享',
|
||||
'icon' => 'fa fa-share-alt',
|
||||
'path' => '/admin/wxshare/',
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'route_group' => [
|
||||
[
|
||||
'prefix' => '/wxshare',
|
||||
'namespace' => 'Plugins\WxShare\Controllers\Web',
|
||||
'routes' => [
|
||||
['GET', '/{code}', 'WxShareController@index'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'prefix' => '/admin/wxshare',
|
||||
'namespace' => 'Plugins\WxShare\Controllers\Admin',
|
||||
'routes' => [
|
||||
['GET', '/', 'WxShareController@index'],
|
||||
['GET', '/get/{id}', 'WxShareController@get'],
|
||||
['GET', '/list', 'WxShareController@list'],
|
||||
['POST', '/delete/{id}', 'WxShareController@delete'],
|
||||
['POST', '/update', 'WxShareController@update'],
|
||||
['GET|POST', '/settings', 'WxShareController@settings'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'tables' => ['wxshare_list', 'wxshare_settings'],
|
||||
'init' => function () {},
|
||||
|
||||
'activate' => function ($db) {
|
||||
$db->query("
|
||||
CREATE TABLE IF NOT EXISTS `wxshare_list` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '名称',
|
||||
`code` varchar(20) NOT NULL COMMENT '分享编码',
|
||||
`views` int(11) NOT NULL DEFAULT 0 COMMENT '访问次数',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
|
||||
`share_title` varchar(255) DEFAULT NULL COMMENT '分享标题',
|
||||
`share_desc` varchar(255) DEFAULT NULL COMMENT '分享描述',
|
||||
`share_img` varchar(500) DEFAULT NULL COMMENT '分享封面图URL',
|
||||
`share_link` text NOT NULL COMMENT '跳转地址',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信分享链接管理表';
|
||||
");
|
||||
|
||||
|
||||
$db->query("
|
||||
CREATE TABLE IF NOT EXISTS `wxshare_settings` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`wechat_name` varchar(100) NOT NULL COMMENT '公众号名称',
|
||||
`wechat_account` varchar(50) NOT NULL COMMENT '公众号原始ID(gh_xxxx格式)',
|
||||
`appid` varchar(50) NOT NULL COMMENT '公众号AppID',
|
||||
`appsecret` varchar(100) NOT NULL COMMENT '公众号AppSecret',
|
||||
`token` varchar(100) DEFAULT NULL COMMENT '接口调用Token',
|
||||
`encoding_aes_key` varchar(100) DEFAULT NULL COMMENT '消息加密密钥',
|
||||
`qrcode_url` varchar(255) DEFAULT NULL COMMENT '公众号二维码URL',
|
||||
`wechat_type` enum('subscription','service','enterprise','test') NOT NULL DEFAULT 'service' COMMENT '公众号类型',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_appid` (`appid`),
|
||||
UNIQUE KEY `uk_wechat_account` (`wechat_account`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公众号设置表,存储公众号相关ID和密钥信息';
|
||||
");
|
||||
},
|
||||
|
||||
'deactivate' => function ($db) {},
|
||||
|
||||
|
||||
];
|
||||
BIN
Binary file not shown.
Executable
+584
@@ -0,0 +1,584 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-user-shield text-primary mr-3"></i>
|
||||
管理员管理中心
|
||||
</h1>
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<!-- 搜索和操作区 -->
|
||||
<div class="flex justify-between items-center mb-6 gap-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700 whitespace-nowrap">管理员列表</h2>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<!-- 新增管理员按钮 -->
|
||||
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center whitespace-nowrap">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>新增管理员</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员列表表格 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">管理员信息</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">注册时间</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="adminList">
|
||||
<?php if (!empty($admins) && is_array($admins)): ?>
|
||||
<?php foreach ($admins as $admin): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors" data-id="<?php echo $admin['id']; ?>">
|
||||
<!-- 用户名和头像单元格 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="<?php
|
||||
if (!empty($admin['avatar'])) {
|
||||
echo htmlspecialchars($admin['avatar']);
|
||||
} else {
|
||||
echo "https://robohash.org/admin" . $admin['id'] . "?size=40x40";
|
||||
}
|
||||
?>"
|
||||
alt="管理员头像" class="w-10 h-10 rounded-full object-cover border border-gray-200 flex-shrink-0">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 truncate"><?php echo htmlspecialchars($admin['username']); ?></div>
|
||||
<div class="text-xs text-gray-500 truncate"><?php echo htmlspecialchars($admin['email']); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 创建时间 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<div class="text-sm text-gray-500"><?php echo date('Y-m-d H:i', strtotime($admin['created_at'])); ?></div>
|
||||
</td>
|
||||
|
||||
<!-- 状态 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<?php
|
||||
$statusClass = $admin['status'] ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
$statusText = $admin['status'] ? '启用' : '停用';
|
||||
?>
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full <?php echo $statusClass; ?>">
|
||||
<?php echo $statusText; ?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 操作按钮 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button class="view-btn text-gray-500 hover:text-purple-500"
|
||||
data-id="<?php echo $admin['id']; ?>" title="查看详情">
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
<button class="edit-btn text-gray-500 hover:text-blue-500"
|
||||
data-id="<?php echo $admin['id']; ?>" title="编辑">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button class="delete-btn text-gray-500 hover:text-red-500"
|
||||
data-id="<?php echo $admin['id']; ?>" title="删除">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fa fa-user-shield text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">没有找到管理员</h3>
|
||||
<p class="mt-1 text-gray-500">尝试调整筛选条件或添加新管理员</p>
|
||||
<button class="mt-4 bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center"
|
||||
onclick="openFormModal()">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>添加新管理员</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="flex justify-between items-center mt-6">
|
||||
<p class="text-sm text-gray-500">显示 1 至 <?php echo min(10, count($admins ?? [])); ?> 条,共 <?php echo count($admins ?? []); ?> 条</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员表单弹窗背景 -->
|
||||
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
|
||||
|
||||
<!-- 管理员表单弹窗 -->
|
||||
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-events-none pointer-none transition transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-plus-circle text-primary mr-2"></i>
|
||||
创建新管理员
|
||||
</h3>
|
||||
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="adminForm" class="space-y-5">
|
||||
<input type="hidden" id="adminId" name="id">
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">用户名 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入用户名">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">邮箱 <span class="text-red-500">*</span></label>
|
||||
<input type="email" id="email" name="email" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入邮箱地址">
|
||||
</div>
|
||||
|
||||
<!-- 管理员表单弹窗中密码字段部分 -->
|
||||
<div id="passwordField">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
密码 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入密码">
|
||||
<p class="mt-1 text-xs text-gray-500">密码长度至少8位,包含字母和数字</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="status" name="status" value="1" checked
|
||||
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用管理员</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="submitBtn" type="button"
|
||||
class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存管理员
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员详情弹窗 -->
|
||||
<div id="detailModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-user-shield text-primary mr-2"></i>
|
||||
管理员详情
|
||||
</h3>
|
||||
<button id="closeDetailBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-100px)]">
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<img id="detailAvatar" src="https://picsum.photos/seed/admin/100/100" alt="管理员头像" class="w-24 h-24 rounded-full mb-4">
|
||||
<h4 id="detailUsername" class="text-xl font-bold text-gray-800">管理员名</h4>
|
||||
<p id="detailRole" class="mt-1 px-3 py-1 text-sm rounded-full bg-red-100 text-red-800">管理员</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">ID</span>
|
||||
<span id="detailId" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">邮箱</span>
|
||||
<span id="detailEmail" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">状态</span>
|
||||
<span id="detailStatus" class="col-span-2">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">启用</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">创建时间</span>
|
||||
<span id="detailCreatedAt" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">最后登录</span>
|
||||
<span id="detailLastLogin" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end">
|
||||
<button id="closeDetailBtn2" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
console.log('管理员管理JS加载完成');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 缓存DOM元素
|
||||
const formModal = document.getElementById('formModal');
|
||||
const formBackdrop = document.getElementById('formBackdrop');
|
||||
const detailModal = document.getElementById('detailModal');
|
||||
const openFormBtn = document.getElementById('openFormBtn');
|
||||
const closeFormBtn = document.getElementById('closeFormBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const formTitle = document.getElementById('formTitle');
|
||||
const adminForm = document.getElementById('adminForm');
|
||||
const adminList = document.getElementById('adminList');
|
||||
const passwordField = document.getElementById('passwordField');
|
||||
const closeDetailBtn = document.getElementById('closeDetailBtn');
|
||||
const closeDetailBtn2 = document.getElementById('closeDetailBtn2');
|
||||
|
||||
// 检查元素是否存在
|
||||
function checkElements() {
|
||||
const elements = [
|
||||
formModal, formBackdrop, openFormBtn,
|
||||
closeFormBtn, cancelBtn, submitBtn
|
||||
];
|
||||
|
||||
const missing = elements.filter(el => !el);
|
||||
if (missing.length > 0) {
|
||||
console.error('缺少必要的DOM元素,功能无法正常工作');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 显示表单弹窗
|
||||
function openFormModal() {
|
||||
if (!checkElements()) return;
|
||||
|
||||
resetForm();
|
||||
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
void formModal.offsetWidth; // 强制重绘
|
||||
}
|
||||
|
||||
// 隐藏表单弹窗
|
||||
function closeFormModal() {
|
||||
if (!checkElements()) return;
|
||||
|
||||
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 显示详情弹窗
|
||||
function openDetailModal() {
|
||||
detailModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
detailModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
void detailModal.offsetWidth;
|
||||
}
|
||||
|
||||
// 隐藏详情弹窗
|
||||
function closeDetailModal() {
|
||||
detailModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
detailModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 重置表单(新增模式)
|
||||
function resetForm() {
|
||||
adminForm.reset();
|
||||
document.getElementById('adminId').value = '';
|
||||
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新管理员';
|
||||
|
||||
// 新增模式:密码必填设置
|
||||
const passwordLabel = document.querySelector('#passwordField label');
|
||||
const passwordInput = document.getElementById('password');
|
||||
passwordLabel.innerHTML = '密码 <span class="text-red-500">*</span>';
|
||||
passwordInput.required = true;
|
||||
passwordInput.placeholder = '请输入密码';
|
||||
passwordField.style.display = 'block';
|
||||
|
||||
submitBtn.innerHTML = '保存管理员';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 加载管理员数据(编辑模式)
|
||||
async function loadAdminData(id) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/admins/${id}`);
|
||||
if (!response.ok) throw new Error('获取数据失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { id, username, email, status } = data.data;
|
||||
document.getElementById('adminId').value = id;
|
||||
document.getElementById('username').value = username || '';
|
||||
document.getElementById('email').value = email || '';
|
||||
document.getElementById('status').checked = status == 1;
|
||||
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑管理员';
|
||||
|
||||
// 编辑模式:密码可选设置
|
||||
const passwordLabel = document.querySelector('#passwordField label');
|
||||
const passwordInput = document.getElementById('password');
|
||||
passwordLabel.innerHTML = '密码(不填则不修改)';
|
||||
passwordInput.required = false;
|
||||
passwordInput.placeholder = '不修改密码请留空';
|
||||
passwordField.style.display = 'block';
|
||||
} else {
|
||||
throw new Error(data.message || '获取数据失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
closeFormModal();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存管理员';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载管理员详情
|
||||
async function loadAdminDetail(id) {
|
||||
try {
|
||||
const response = await fetch(`/admin/admins/${id}`);
|
||||
if (!response.ok) throw new Error('获取详情失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { id, username, email, status, created_at, last_login, avatar } = data.data;
|
||||
|
||||
// 填充详情数据
|
||||
document.getElementById('detailId').textContent = id;
|
||||
document.getElementById('detailUsername').textContent = username || '未知管理员';
|
||||
document.getElementById('detailEmail').textContent = email || '未设置';
|
||||
document.getElementById('detailCreatedAt').textContent = created_at ? new Date(created_at).toLocaleString() : '未知';
|
||||
document.getElementById('detailLastLogin').textContent = last_login ? new Date(last_login).toLocaleString() : '从未登录';
|
||||
document.getElementById('detailAvatar').src = avatar || `https://robohash.org/admin${id}?size=100x100`;
|
||||
|
||||
// 设置角色标签样式(管理员固定为管理员)
|
||||
document.getElementById('detailRole').className = 'mt-1 px-3 py-1 text-sm rounded-full bg-red-100 text-red-800';
|
||||
document.getElementById('detailRole').textContent = '管理员';
|
||||
|
||||
// 设置状态标签样式
|
||||
const statusClass = status ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
const statusText = status ? '启用' : '禁用';
|
||||
document.getElementById('detailStatus').innerHTML =
|
||||
`<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">${statusText}</span>`;
|
||||
|
||||
openDetailModal();
|
||||
} else {
|
||||
throw new Error(data.message || '获取详情失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm() {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const isEditMode = !!document.getElementById('adminId').value;
|
||||
|
||||
if (!username) {
|
||||
showMessage('请输入用户名', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
showMessage('请输入邮箱地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
showMessage('请输入有效的邮箱地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 仅在新增或编辑时填写了密码的情况下验证长度
|
||||
if ((!isEditMode || password) && password.length < 8) {
|
||||
showMessage('密码长度至少8位', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 提交表单(创建/更新)
|
||||
async function submitFormData() {
|
||||
if (!validateForm()) return;
|
||||
|
||||
const formData = new FormData(adminForm);
|
||||
const isEditMode = !!document.getElementById('adminId').value;
|
||||
const statusCheckbox = document.getElementById('status');
|
||||
|
||||
formData.delete('status'); // 先删除可能存在的旧值
|
||||
formData.append('status', statusCheckbox.checked ? '1' : '0');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/admins/update', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage(isEditMode ? '管理员更新成功' : '管理员创建成功');
|
||||
closeFormModal();
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
throw new Error(data.message || (isEditMode ? '更新失败' : '创建失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存管理员';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除管理员
|
||||
async function deleteAdmin(id) {
|
||||
if (!confirm('确定要删除该管理员吗?此操作不可恢复!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/admins/delete/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage('管理员已删除');
|
||||
// 移除DOM元素
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
// 检查是否还有数据行
|
||||
const rows = adminList.querySelectorAll('tr:not(:last-child)');
|
||||
if (rows.length === 0) {
|
||||
adminList.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-10 text-center text-gray-500 border border-dashed border-gray-200">
|
||||
<div>
|
||||
<i class="fa fa-info-circle text-2xl mb-2 text-gray-300"></i>
|
||||
<p>暂无管理员数据</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 绑定事件
|
||||
if (checkElements()) {
|
||||
// 打开表单
|
||||
openFormBtn.addEventListener('click', openFormModal);
|
||||
|
||||
// 关闭表单
|
||||
closeFormBtn.addEventListener('click', closeFormModal);
|
||||
cancelBtn.addEventListener('click', closeFormModal);
|
||||
formBackdrop.addEventListener('click', () => {
|
||||
if (!formModal.classList.contains('invisible')) closeFormModal();
|
||||
if (!detailModal.classList.contains('invisible')) closeDetailModal();
|
||||
});
|
||||
|
||||
// 关闭详情
|
||||
closeDetailBtn.addEventListener('click', closeDetailModal);
|
||||
closeDetailBtn2.addEventListener('click', closeDetailModal);
|
||||
|
||||
// 提交表单
|
||||
submitBtn.addEventListener('click', submitFormData);
|
||||
|
||||
|
||||
|
||||
// 编辑、删除、查看事件委托
|
||||
adminList.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.edit-btn');
|
||||
const deleteBtn = e.target.closest('.delete-btn');
|
||||
const viewBtn = e.target.closest('.view-btn');
|
||||
|
||||
if (editBtn) {
|
||||
const id = editBtn.getAttribute('data-id');
|
||||
if (id) {
|
||||
openFormModal();
|
||||
setTimeout(() => loadAdminData(id), 300);
|
||||
}
|
||||
} else if (deleteBtn) {
|
||||
const id = deleteBtn.getAttribute('data-id');
|
||||
if (id) deleteAdmin(id);
|
||||
} else if (viewBtn) {
|
||||
const id = viewBtn.getAttribute('data-id');
|
||||
if (id) loadAdminDetail(id);
|
||||
}
|
||||
});
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') {
|
||||
if (!formModal.classList.contains('invisible')) closeFormModal();
|
||||
if (!detailModal.classList.contains('invisible')) closeDetailModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止表单默认提交
|
||||
adminForm.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
submitFormData();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">🤝 代理管理</h2>
|
||||
|
||||
<button onclick="showAddAgent()" class="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">+ 添加代理</button>
|
||||
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50"><tr>
|
||||
<th class="px-4 py-3 text-left">代理</th><th class="px-4 py-3">级别</th><th class="px-4 py-3">邀请码</th>
|
||||
<th class="px-4 py-3">佣金%</th><th class="px-4 py-3">反水%</th><th class="px-4 py-3">玩家数</th>
|
||||
<th class="px-4 py-3">状态</th><th class="px-4 py-3">操作</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($agents??[] as $a): ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2"><?=htmlspecialchars($a['user']['username']??'?')?></td>
|
||||
<td class="px-4 py-2 text-center"><?=$a['level']==1?'<span class="text-yellow-600 font-bold">总代</span>':'代理'?></td>
|
||||
<td class="px-4 py-2 font-mono text-xs"><?=$a['agent_code']?></td>
|
||||
<td class="px-4 py-2 text-center"><?=$a['commission_rate']?>%</td>
|
||||
<td class="px-4 py-2 text-center"><?=$a['rebate_rate']?>%</td>
|
||||
<td class="px-4 py-2 text-center"><?=$a['player_count']?></td>
|
||||
<td class="px-4 py-2 text-center"><?=$a['status']?'<span class="text-green-500">启用</span>':'<span class="text-red-500">禁用</span>'?></td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<button onclick="editAgent(<?=htmlspecialchars(json_encode($a))?>)" class="text-blue-500 hover:underline text-xs">编辑</button>
|
||||
<button onclick="deleteAgent(<?=$a['id']?>)" class="text-red-500 hover:underline text-xs ml-2">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<div id="agentModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl p-6 w-full max-w-md">
|
||||
<h3 class="font-bold mb-4" id="agentModalTitle">添加代理</h3>
|
||||
<input type="hidden" id="agentId" value="0">
|
||||
<div class="space-y-3">
|
||||
<div><label class="text-xs text-gray-400">用户ID</label><input type="number" id="agentUserId" class="w-full border rounded px-3 py-2"></div>
|
||||
<div><label class="text-xs text-gray-400">上级代理ID (留空=总代)</label><input type="number" id="agentParent" class="w-full border rounded px-3 py-2"></div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div><label class="text-xs text-gray-400">佣金 %</label><input type="number" step="0.1" id="agentComm" class="w-full border rounded px-3 py-2" value="1"></div>
|
||||
<div><label class="text-xs text-gray-400">反水 %</label><input type="number" step="0.1" id="agentRebate" class="w-full border rounded px-3 py-2" value="0.5"></div>
|
||||
</div>
|
||||
<div><label class="text-xs text-gray-400">状态</label><select id="agentStatus" class="w-full border rounded px-3 py-2"><option value="1">启用</option><option value="0">禁用</option></select></div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button onclick="saveAgent()" class="flex-1 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">保存</button>
|
||||
<button onclick="document.getElementById('agentModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded hover:bg-gray-300">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showAddAgent(){document.getElementById('agentId').value=0;document.getElementById('agentModalTitle').textContent='添加代理';document.getElementById('agentModal').classList.remove('hidden');}
|
||||
function editAgent(a){document.getElementById('agentId').value=a.id;document.getElementById('agentComm').value=a.commission_rate;document.getElementById('agentRebate').value=a.rebate_rate;document.getElementById('agentStatus').value=a.status;document.getElementById('agentModalTitle').textContent='编辑代理';document.getElementById('agentModal').classList.remove('hidden');}
|
||||
async function saveAgent(){
|
||||
const body={id:parseInt(document.getElementById('agentId').value),user_id:parseInt(document.getElementById('agentUserId').value),parent_id:document.getElementById('agentParent').value||null,commission_rate:parseFloat(document.getElementById('agentComm').value),rebate_rate:parseFloat(document.getElementById('agentRebate').value),status:parseInt(document.getElementById('agentStatus').value)};
|
||||
const r=await fetch('/admin/agents/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);
|
||||
}
|
||||
async function deleteAgent(id){if(!confirm('确定删除该代理?'))return;await fetch('/admin/agents/delete/'+id,{method:'POST'});location.reload();}
|
||||
</script>
|
||||
@@ -0,0 +1,169 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">⏱️ 自动开期设置</h2>
|
||||
|
||||
<!-- 宝塔配置提示 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">
|
||||
<h3 class="font-bold text-blue-700 mb-2">📋 宝塔面板定时任务配置</h3>
|
||||
<div class="text-sm text-blue-600 space-y-1">
|
||||
<p>1. 登录宝塔面板 → <b>计划任务</b></p>
|
||||
<p>2. 任务类型: <b>Shell脚本</b></p>
|
||||
<p>3. 任务名称: <b>PK10自动开期</b></p>
|
||||
<p>4. 执行周期: <b>每N分钟 → 1分钟</b></p>
|
||||
<p>5. 脚本内容:</p>
|
||||
<pre class="bg-blue-100 p-2 rounded mt-1 text-xs overflow-x-auto select-all" id="cronCmd">cd <?=ROOT_PATH?> && /usr/bin/php cron/auto_period_task.php >> Storage/log/auto_period.log 2>&1</pre>
|
||||
<button onclick="copyCmd()" class="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600">📋 复制命令</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局控制 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-gray-500">全局控制:</span>
|
||||
<button onclick="toggleAll(1)" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">✅ 全部启用</button>
|
||||
<button onclick="toggleAll(0)" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 text-sm">🚫 全部关闭</button>
|
||||
</div>
|
||||
<button onclick="refreshStatus()" class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 text-sm">🔄 刷新状态</button>
|
||||
</div>
|
||||
|
||||
<!-- 游戏列表 -->
|
||||
<div class="space-y-4 mb-8">
|
||||
<?php foreach($games ?? [] as $game): ?>
|
||||
<div class="bg-white rounded-xl p-5 shadow" id="game-<?=$game['id']?>">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-lg font-bold"><?=$game['name']?></span>
|
||||
<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-500"><?=$game['type']?></span>
|
||||
<span class="px-2 py-1 rounded text-xs <?=($game['auto_period_enabled']??0)?'bg-green-100 text-green-700':'bg-red-100 text-red-700'?>" id="status-<?=$game['id']?>">
|
||||
<?=($game['auto_period_enabled']??0)?'✅ 自动开期中':'🚫 已关闭'?>
|
||||
</span>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer" <?=($game['auto_period_enabled']??0)?'checked':''?> onchange="toggleGame(<?=$game['id']?>, this.checked)">
|
||||
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:bg-green-500 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 每期时长 -->
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">每期时长</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" min="60" max="3600" step="30" value="<?=$game['period_duration']??300?>"
|
||||
id="duration-<?=$game['id']?>" class="w-24 border rounded px-3 py-2 text-center">
|
||||
<span class="text-gray-400 text-sm">秒</span>
|
||||
<span class="text-gray-300 text-xs">( = <span id="durationMin-<?=$game['id']?>"><?=round(($game['period_duration']??300)/60, 1)?></span> 分钟)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 封盘提前时间 -->
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">结束前提前封盘</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" min="5" max="120" step="5" value="<?=$game['lock_before_end']??30?>"
|
||||
id="lock-<?=$game['id']?>" class="w-24 border rounded px-3 py-2 text-center">
|
||||
<span class="text-gray-400 text-sm">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button onclick="saveGame(<?=$game['id']?>)" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">💾 保存设置</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- 运行日志 -->
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<h3 class="font-bold px-4 py-3 bg-gray-50 border-b">📜 最近自动开期日志</h3>
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 sticky top-0"><tr>
|
||||
<th class="px-4 py-2 text-left">时间</th>
|
||||
<th class="px-4 py-2 text-left">游戏</th>
|
||||
<th class="px-4 py-2 text-left">动作</th>
|
||||
<th class="px-4 py-2 text-left">详情</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($logs ?? [] as $log): ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap"><?=$log['created_at']??''?></td>
|
||||
<td class="px-4 py-2">G<?=$log['game_id']??0?></td>
|
||||
<td class="px-4 py-2">
|
||||
<span class="px-2 py-1 rounded text-xs
|
||||
<?php
|
||||
$act = $log['action'] ?? '';
|
||||
echo match($act) {
|
||||
'start' => 'bg-green-100 text-green-700',
|
||||
'lock' => 'bg-orange-100 text-orange-700',
|
||||
'draw' => 'bg-blue-100 text-blue-700',
|
||||
'settle' => 'bg-purple-100 text-purple-700',
|
||||
'error' => 'bg-red-100 text-red-700',
|
||||
default => 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
?>">
|
||||
<?=match($act) { 'start'=>'开期', 'lock'=>'封盘', 'draw'=>'开奖', 'settle'=>'结算', 'error'=>'错误', default=>$act }?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-500 text-xs"><?=htmlspecialchars($log['message']??'')?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($logs)): ?>
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-gray-400">暂无日志记录。请先执行数据库迁移并配置宝塔定时任务。</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyCmd(){
|
||||
const t=document.getElementById('cronCmd').textContent;
|
||||
navigator.clipboard.writeText(t).then(()=>alert('已复制到剪贴板')).catch(()=>{
|
||||
const ta=document.createElement('textarea');ta.value=t;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();alert('已复制');
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleGame(gameId, enabled){
|
||||
const r=await fetch('/admin/auto-period/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:gameId,auto_period_enabled:enabled?1:0})});
|
||||
const d=await r.json();
|
||||
if(d.success){
|
||||
const el=document.getElementById('status-'+gameId);
|
||||
el.textContent=enabled?'✅ 自动开期中':'🚫 已关闭';
|
||||
el.className='px-2 py-1 rounded text-xs '+(enabled?'bg-green-100 text-green-700':'bg-red-100 text-red-700');
|
||||
}else{alert(d.message||'操作失败');}
|
||||
}
|
||||
|
||||
async function saveGame(gameId){
|
||||
const duration=parseInt(document.getElementById('duration-'+gameId).value)||300;
|
||||
const lock=parseInt(document.getElementById('lock-'+gameId).value)||30;
|
||||
const r=await fetch('/admin/auto-period/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:gameId,period_duration:duration,lock_before_end:lock})});
|
||||
const d=await r.json();
|
||||
if(d.success){
|
||||
document.getElementById('durationMin-'+gameId).textContent=(duration/60).toFixed(1);
|
||||
alert('保存成功');
|
||||
}else{alert(d.message||'保存失败');}
|
||||
}
|
||||
|
||||
async function toggleAll(enabled){
|
||||
if(!confirm(enabled?'确认启用所有游戏的自动开期?':'确认关闭所有游戏的自动开期?'))return;
|
||||
const r=await fetch('/admin/auto-period/toggle-all',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled})});
|
||||
const d=await r.json();
|
||||
if(d.success){location.reload();}else{alert(d.message||'操作失败');}
|
||||
}
|
||||
|
||||
async function refreshStatus(){
|
||||
const r=await fetch('/admin/auto-period/status');
|
||||
const d=await r.json();
|
||||
if(d.success){location.reload();}
|
||||
}
|
||||
|
||||
// 每期时长实时更新分钟显示
|
||||
document.querySelectorAll('input[id^="duration-"]').forEach(el=>{
|
||||
el.addEventListener('input',function(){
|
||||
const gid=this.id.split('-')[1];
|
||||
document.getElementById('durationMin-'+gid).textContent=(parseInt(this.value||300)/60).toFixed(1);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Executable
+214
@@ -0,0 +1,214 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-list-alt text-primary mr-3"></i>
|
||||
投注记录
|
||||
</h1>
|
||||
|
||||
<!-- 筛选工具栏 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<form class="flex flex-col md:flex-row gap-4" method="GET" action="/admin/bets">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">用户ID</label>
|
||||
<input type="text" name="user_id" value="<?= htmlspecialchars($_GET['user_id'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm" placeholder="输入用户ID">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">期号</label>
|
||||
<input type="text" name="period_number" value="<?= htmlspecialchars($_GET['period_number'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm" placeholder="输入期号">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
||||
<select name="status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending" <?= ($_GET['status'] ?? '') === 'pending' ? 'selected' : '' ?>>待结算</option>
|
||||
<option value="win" <?= ($_GET['status'] ?? '') === 'win' ? 'selected' : '' ?>>已中奖</option>
|
||||
<option value="lose" <?= ($_GET['status'] ?? '') === 'lose' ? 'selected' : '' ?>>未中奖</option>
|
||||
<option value="settled" <?= ($_GET['status'] ?? '') === 'settled' ? 'selected' : '' ?>>已结算</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button type="submit" class="bg-primary hover:bg-primary/90 text-white px-6 py-2 rounded-lg text-sm transition-colors h-[38px]">
|
||||
<i class="fas fa-search mr-2"></i>查询
|
||||
</button>
|
||||
<a href="/admin/bets" class="ml-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg text-sm transition-colors h-[38px] flex items-center">
|
||||
重置
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">游戏房</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">下注内容</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">中奖/盈利</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">下注时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<?php if (!empty($bets) && is_array($bets)): ?>
|
||||
<?php foreach ($bets as $bet): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-4 py-4 text-sm text-gray-500">
|
||||
#<?= $bet['id'] ?>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm font-medium text-gray-900"><?= htmlspecialchars($bet['username'] ?? '未知用户') ?></div>
|
||||
<div class="text-xs text-gray-500">ID: <?= $bet['user_id'] ?></div>
|
||||
<div class="text-xs text-gray-500">余额: <?= number_format($bet['balance'] ?? 0) ?></div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-700">
|
||||
<?= htmlspecialchars($bet['game_name'] ?? '未关联') ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-700">
|
||||
<?= htmlspecialchars($bet['period_number']) ?>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
// ========== 投注类型中文映射(完整版)==========
|
||||
$bt = $bet['bet_type'];
|
||||
$bv = $bet['bet_value'];
|
||||
$displayType = '';
|
||||
$displayValue = '';
|
||||
|
||||
// PK10 名次投注: rank + rank1_5
|
||||
if ($bt === 'rank' && preg_match('/^rank(\d+)_(\d+)$/', $bv, $m)) {
|
||||
$rn = (int)$m[1];
|
||||
$displayType = $rn === 1 ? '冠军' : ($rn === 2 ? '亚军' : '第'.$rn.'名');
|
||||
$displayValue = $m[2] . '号车';
|
||||
}
|
||||
// PK10 大小: bs + rank1_big
|
||||
elseif ($bt === 'bs' && preg_match('/^rank(\d+)_(big|small)$/', $bv, $m)) {
|
||||
$rn = (int)$m[1];
|
||||
$displayType = ($rn === 1 ? '冠军' : ($rn === 2 ? '亚军' : '第'.$rn.'名')) . ' 大小';
|
||||
$displayValue = $m[2] === 'big' ? '大' : '小';
|
||||
}
|
||||
// PK10 单双: oe + rank1_odd
|
||||
elseif ($bt === 'oe' && preg_match('/^rank(\d+)_(odd|even)$/', $bv, $m)) {
|
||||
$rn = (int)$m[1];
|
||||
$displayType = ($rn === 1 ? '冠军' : ($rn === 2 ? '亚军' : '第'.$rn.'名')) . ' 单双';
|
||||
$displayValue = $m[2] === 'odd' ? '单' : '双';
|
||||
}
|
||||
// PK10 龙虎: dt + dt1_dragon
|
||||
elseif ($bt === 'dt' && preg_match('/^dt(\d+)_(dragon|tiger)$/', $bv, $m)) {
|
||||
$pairs = [1=>[1,10],2=>[2,9],3=>[3,8],4=>[4,7],5=>[5,6]];
|
||||
$p = $pairs[(int)$m[1]] ?? [(int)$m[1], 11-(int)$m[1]];
|
||||
$displayType = '龙虎 '.$p[0].'vs'.$p[1];
|
||||
$displayValue = $m[2] === 'dragon' ? '龙' : '虎';
|
||||
}
|
||||
// PK10 冠亚和值: sum + sum_11
|
||||
elseif ($bt === 'sum' && preg_match('/^sum_(\d+)$/', $bv, $m)) {
|
||||
$displayType = '冠亚和';
|
||||
$displayValue = $m[1];
|
||||
}
|
||||
// PK10 冠亚和大小: sum_bs + sum_big
|
||||
elseif ($bt === 'sum_bs') {
|
||||
$displayType = '冠亚和';
|
||||
$sbMap = ['sum_big'=>'大','sum_small'=>'小','sum_odd'=>'单','sum_even'=>'双'];
|
||||
$displayValue = $sbMap[$bv] ?? $bv;
|
||||
}
|
||||
// 骰子 大小/单双/点数/单骰/豹子
|
||||
else {
|
||||
$typeMap = [
|
||||
'xiu' => '小', 'tai' => '大', 'chan' => '双', 'le' => '单',
|
||||
'number' => '点数', 'dice' => '单骰', 'combo' => '豹子',
|
||||
'big_small' => '大小', 'odd_even' => '单双',
|
||||
];
|
||||
$displayType = $typeMap[$bt] ?? $bt;
|
||||
|
||||
$valueMap = ['big'=>'大','small'=>'小','odd'=>'单','even'=>'双',
|
||||
'4red'=>'4红','4white'=>'4白','3red1white'=>'3红1白','1red3white'=>'1红3白'];
|
||||
$displayValue = $valueMap[$bv] ?? $bv;
|
||||
}
|
||||
?>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<?= htmlspecialchars($displayType) ?>
|
||||
</span>
|
||||
<span class="text-sm font-bold ml-1 text-gray-700">
|
||||
<?= htmlspecialchars($displayValue) ?>
|
||||
</span>
|
||||
<div class="text-xs text-gray-500 mt-0.5">赔率: <?= $bet['odds'] ?></div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm font-bold text-gray-900">
|
||||
<?= number_format($bet['amount']) ?>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$status = $bet['status'];
|
||||
$statusClassMap = [
|
||||
'pending' => 'bg-yellow-100 text-yellow-800',
|
||||
'win' => 'bg-green-100 text-green-800',
|
||||
'lose' => 'bg-gray-100 text-gray-800',
|
||||
'settled' => 'bg-blue-100 text-blue-800'
|
||||
];
|
||||
$statusClass = $statusClassMap[$status] ?? 'bg-gray-100 text-gray-800';
|
||||
|
||||
$statusTextMap = [
|
||||
'pending' => '待结算',
|
||||
'win' => '已中奖',
|
||||
'lose' => '未中奖',
|
||||
'settled' => '已结算'
|
||||
];
|
||||
$statusText = $statusTextMap[$status] ?? $status;
|
||||
?>
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <?= $statusClass ?>">
|
||||
<?= $statusText ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php if ($status === 'win'): ?>
|
||||
<div class="text-sm font-bold text-green-600">+<?= number_format($bet['win_amount']) ?></div>
|
||||
<div class="text-xs text-gray-500">盈利: <?= number_format($bet['win_amount'] - $bet['amount']) ?></div>
|
||||
<?php elseif ($status === 'lose'): ?>
|
||||
<div class="text-sm font-bold text-red-500">-<?= number_format($bet['amount']) ?></div>
|
||||
<?php else: ?>
|
||||
<span class="text-gray-400">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-500 hidden md:table-cell">
|
||||
<?= $bet['created_at'] ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center text-gray-500">
|
||||
暂无投注记录
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-500">
|
||||
共 <?= $totalPages ?> 页
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<?php if ($currentPage > 1): ?>
|
||||
<a href="?page=<?= $currentPage - 1 ?><?= http_build_query(array_diff_key($_GET, ['page' => ''])) ? '&' . http_build_query(array_diff_key($_GET, ['page' => ''])) : '' ?>" class="px-3 py-1 border rounded hover:bg-gray-50">上一页</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = max(1, $currentPage - 2); $i <= min($totalPages, $currentPage + 2); $i++): ?>
|
||||
<a href="?page=<?= $i ?><?= http_build_query(array_diff_key($_GET, ['page' => ''])) ? '&' . http_build_query(array_diff_key($_GET, ['page' => ''])) : '' ?>" class="px-3 py-1 border rounded <?= $i == $currentPage ? 'bg-primary text-white border-primary' : 'hover:bg-gray-50' ?>">
|
||||
<?= $i ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($currentPage < $totalPages): ?>
|
||||
<a href="?page=<?= $currentPage + 1 ?><?= http_build_query(array_diff_key($_GET, ['page' => ''])) ? '&' . http_build_query(array_diff_key($_GET, ['page' => ''])) : '' ?>" class="px-3 py-1 border rounded hover:bg-gray-50">下一页</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Executable
+268
@@ -0,0 +1,268 @@
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-2xl font-bold text-dark">
|
||||
控制台
|
||||
</h3>
|
||||
<p class="text-gray-500 mt-1">
|
||||
<?=date('Y年m月d日')?> · 今天是星期<?=['日','一','二','三','四','五','六'][date('w')]?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">今日投注总额</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($stats['today_bet_amount']) ? number_format($stats['today_bet_amount'], 0) : '0' ?>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
单位:USDT,含所有有效期号
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-coins text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">今日已派彩金额</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($stats['today_payout_amount']) ? number_format($stats['today_payout_amount'], 0) : '0' ?>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
已结算期号产生的实际派彩
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-hand-holding-usd text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">待处理开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($stats['pending_draw_count']) ? (int)$stats['pending_draw_count'] : 0 ?>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
含待录入结果与待审核期号
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||||
<i class="fas fa-trophy text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 card-shadow hover-lift">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">待处理提现</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($stats['pending_withdraw_count']) ? (int)$stats['pending_withdraw_count'] : 0 ?>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
仅统计处于待审核状态的提现申请
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
|
||||
<i class="fas fa-file-invoice-dollar text-danger"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl p-6 card-shadow lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold mb-4">业务流程概览</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
|
||||
<div class="p-4 border border-gray-100 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium">用户与资金</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-primary/10 text-primary">
|
||||
<i class="fas fa-user-shield mr-1"></i> 账户安全
|
||||
</span>
|
||||
</div>
|
||||
<p class="leading-relaxed">
|
||||
管理玩家账号、登录状态与风险标记,掌握充值、余额、提现等资金流向,
|
||||
为后续投注与开奖提供可靠资金基础。
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 border border-gray-100 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium">游戏与期号</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-warning/10 text-warning">
|
||||
<i class="fas fa-dice mr-1"></i> 核心玩法
|
||||
</span>
|
||||
</div>
|
||||
<p class="leading-relaxed">
|
||||
维护骰子游戏配置与赔率,按业务策略生成期号,控制期号生命周期
|
||||
(下注、封盘、开奖、结算)并处理异常期号。
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 border border-gray-100 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium">开奖与派彩</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-success/10 text-success">
|
||||
<i class="fas fa-trophy mr-1"></i> 结果可信
|
||||
</span>
|
||||
</div>
|
||||
<p class="leading-relaxed">
|
||||
根据直播画面录入开奖结果并上传截图,完成结果审核与锁定,
|
||||
自动触发派彩结算并与资金流水进行对账。
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 border border-gray-100 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium">风控与监控</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-danger/10 text-danger">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i> 风险预警
|
||||
</span>
|
||||
</div>
|
||||
<p class="leading-relaxed">
|
||||
依托后台数据监控高额投注、异常盈利、频繁提现等行为,
|
||||
为人工复核和规则优化提供依据,保障平台资金安全。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 card-shadow">
|
||||
<h2 class="text-lg font-semibold mb-4">运营快捷入口</h2>
|
||||
<div class="space-y-3">
|
||||
<a href="/admin/users" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-user-cog w-5 text-center mr-2 text-primary"></i>
|
||||
<span>用户管理</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
|
||||
</a>
|
||||
<a href="/admin/games" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-dice w-5 text-center mr-2 text-primary"></i>
|
||||
<span>游戏管理</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
|
||||
</a>
|
||||
<a href="/admin/periods" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-list-ol w-5 text-center mr-2 text-primary"></i>
|
||||
<span>期号管理</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
|
||||
</a>
|
||||
<a href="/admin/draws" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-trophy w-5 text-center mr-2 text-primary"></i>
|
||||
<span>开奖管理</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
|
||||
</a>
|
||||
<a href="/admin/finance" class="flex items-center justify-between px-4 py-2 rounded-lg border border-gray-200 hover:border-primary hover:bg-primary/5 transition-colors text-sm">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-yen-sign w-5 text-center mr-2 text-primary"></i>
|
||||
<span>财务管理</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right text-xs text-gray-400"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-dashed border-gray-200 text-xs text-gray-600 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 rounded-full bg-warning mr-2"></span>
|
||||
<span>待处理开奖</span>
|
||||
</span>
|
||||
<span class="font-semibold text-warning">
|
||||
<?= isset($stats['pending_draw_count']) ? (int)$stats['pending_draw_count'] : 0 ?> 期
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 rounded-full bg-danger mr-2"></span>
|
||||
<span>待处理提现</span>
|
||||
</span>
|
||||
<span class="font-semibold text-danger">
|
||||
<?= isset($stats['pending_withdraw_count']) ? (int)$stats['pending_withdraw_count'] : 0 ?> 笔
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-xl p-6 card-shadow">
|
||||
<h2 class="text-lg font-semibold mb-4">后台运营模块概览</h2>
|
||||
<div id="game-management" class="mb-4">
|
||||
<h3 class="text-sm font-medium mb-1 flex items-center">
|
||||
<i class="fas fa-dice text-primary mr-2"></i> 游戏管理
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 leading-relaxed">
|
||||
维护游戏平台中的骰子游戏配置,包括游戏列表、赔率设置、直播间绑定和启用状态,为前端提供可用游戏和玩法数据。
|
||||
</p>
|
||||
</div>
|
||||
<div id="period-management" class="mb-4">
|
||||
<h3 class="text-sm font-medium mb-1 flex items-center">
|
||||
<i class="fas fa-list-ol text-primary mr-2"></i> 期号管理
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 leading-relaxed">
|
||||
管理每天的开奖期号,配置期数、生成规则及时间区间,维护期号状态(未开始、下注中、封盘、已开奖、作废),并支持异常期号处理。
|
||||
</p>
|
||||
</div>
|
||||
<div id="draw-management" class="mb-4">
|
||||
<h3 class="text-sm font-medium mb-1 flex items-center">
|
||||
<i class="fas fa-trophy text-primary mr-2"></i> 开奖管理
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 leading-relaxed">
|
||||
开奖工作人员在后台对照直播流为指定期号录入开奖结果,上传开奖截图,完成审核与结果锁定,必要时进行重开或作废处理并记录日志。
|
||||
</p>
|
||||
</div>
|
||||
<div id="finance-management">
|
||||
<h3 class="text-sm font-medium mb-1 flex items-center">
|
||||
<i class="fas fa-yen-sign text-primary mr-2"></i> 财务管理
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 leading-relaxed">
|
||||
处理充值、提现和资金流水统计,核对用户余额变动情况,配合开奖结算结果进行对账,为风控和运营提供数据支持。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-6 card-shadow">
|
||||
<h2 class="text-lg font-semibold mb-4">操作指引</h2>
|
||||
<div class="space-y-3 text-sm text-gray-600">
|
||||
<p>
|
||||
登录管理后台后,可通过左侧菜单快速进入用户管理、游戏管理、期号管理、开奖管理、财务管理和系统管理等模块,完成日常运营工作。
|
||||
</p>
|
||||
<p>
|
||||
在正式接入真实数据前,建议优先配置游戏基本信息和直播间绑定,然后按业务流程逐步完善期号生成规则和开奖操作流程。
|
||||
</p>
|
||||
<p>
|
||||
系统管理模块用于维护管理员账号和权限,以及后续扩展的系统配置与操作日志,确保平台运行安全可控。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 页面初始化操作
|
||||
console.log('Project features page loaded successfully');
|
||||
|
||||
// 为功能卡片添加悬停动画效果
|
||||
const featureCards = document.querySelectorAll('.hover\\:border-primary\\/30');
|
||||
featureCards.forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.classList.add('transform', 'translate-y-[-5px]', 'shadow-md');
|
||||
});
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.classList.remove('transform', 'translate-y-[-5px]', 'shadow-md');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Executable
+778
@@ -0,0 +1,778 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-dice text-primary mr-3"></i>
|
||||
骰子游戏期号管理
|
||||
</h1>
|
||||
|
||||
<!-- 游戏期号管理区域 -->
|
||||
<?php if (!empty($gamesList) && is_array($gamesList)): ?>
|
||||
<?php foreach ($gamesList as $game): ?>
|
||||
<?php
|
||||
$gameId = $game['id'];
|
||||
$gameName = $game['name'];
|
||||
$currentPeriod = isset($currentPeriods[$gameId]) ? $currentPeriods[$gameId] : null;
|
||||
?>
|
||||
|
||||
<!-- 单个游戏的期号卡片 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6 border-l-4 border-primary">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
|
||||
<i class="fas fa-gamepad text-primary mr-2"></i>
|
||||
<?= htmlspecialchars($gameName) ?>
|
||||
</h2>
|
||||
|
||||
<?php if ($currentPeriod): ?>
|
||||
<!-- 有当前期号 -->
|
||||
<div class="flex gap-2">
|
||||
<?php if ($currentPeriod['status'] === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-lock-btn inline-block bg-warning hover:bg-warning/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-lock mr-2"></i>封盘
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'locked'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn inline-block bg-success hover:bg-success/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-dice mr-2"></i>开奖
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'drawn'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-settle-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-coins mr-2"></i>结算
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'settled'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-game-id="<?= $gameId ?>"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>开始新一期
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-game-id="<?= $gameId ?>"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>开始新一期
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($currentPeriod): ?>
|
||||
<!-- 当前期号信息 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">期号</p>
|
||||
<p class="text-lg font-bold text-gray-900"><?= htmlspecialchars($currentPeriod['period_number']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">状态</p>
|
||||
<?php
|
||||
$status = $currentPeriod['status'];
|
||||
$statusMap = [
|
||||
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
|
||||
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
|
||||
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
|
||||
];
|
||||
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm <?= $statusInfo['class'] ?>">
|
||||
<?= $statusInfo['text'] ?>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">开始时间</p>
|
||||
<p class="text-sm text-gray-900"><?= htmlspecialchars($currentPeriod['start_time'] ?? '-') ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">开奖结果</p>
|
||||
<?php if (!empty($currentPeriod['dice1']) && !empty($currentPeriod['dice2']) && !empty($currentPeriod['dice3'])): ?>
|
||||
<p class="text-lg font-bold text-gray-900">
|
||||
<?= $currentPeriod['dice1'] ?>.<?= $currentPeriod['dice2'] ?>.<?= $currentPeriod['dice3'] ?>
|
||||
<span class="text-sm text-gray-500">(<?= $currentPeriod['total'] ?>)</span>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="text-sm text-gray-400">未开奖</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-gray-500">暂无进行中的期号,点击"开始新一期"按钮启动</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">期号总数</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($periods) && is_array($periods) ? count($periods) : 0 ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-list text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">待开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-warning">
|
||||
<?php
|
||||
$pendingCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && $p['status'] === 'pending') {
|
||||
$pendingCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $pendingCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||||
<i class="fas fa-clock text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-success">
|
||||
<?php
|
||||
$drawnCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && ($p['status'] === 'drawn' || $p['status'] === 'settled')) {
|
||||
$drawnCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $drawnCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已结算</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-primary">
|
||||
<?php
|
||||
$settledCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && $p['status'] === 'settled') {
|
||||
$settledCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $settledCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-coins text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 期号列表 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">期号列表</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">管理骰子游戏期号和开奖结果</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">关联游戏</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开奖结果</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">创建时间</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="periodList">
|
||||
<?php if (!empty($periods) && is_array($periods)): ?>
|
||||
<?php foreach ($periods as $period): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>">
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
<?= htmlspecialchars((string)($period['period_number'] ?? '')) ?>
|
||||
</div>
|
||||
<?php if (!empty($period['auto_generated'])): ?>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600 mt-1">
|
||||
自动生成
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$gameId = $period['game_id'] ?? null;
|
||||
$gameName = $gameId && isset($games[$gameId]) ? $games[$gameId] : '未关联';
|
||||
?>
|
||||
<span class="text-sm text-gray-600"><?= htmlspecialchars($gameName) ?></span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$status = $period['status'] ?? 'pending';
|
||||
$statusMap = [
|
||||
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
|
||||
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
|
||||
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
|
||||
];
|
||||
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
|
||||
<span class="w-2 h-2 rounded-full mr-1 <?= str_replace('/10', '', $statusInfo['class']) ?>"></span>
|
||||
<?= $statusInfo['text'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php if (!empty($period['dice1']) && !empty($period['dice2']) && !empty($period['dice3'])): ?>
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="font-semibold">
|
||||
<?= htmlspecialchars((string)$period['dice1']) ?>.
|
||||
<?= htmlspecialchars((string)$period['dice2']) ?>.
|
||||
<?= htmlspecialchars((string)$period['dice3']) ?>
|
||||
</span>
|
||||
<span class="text-gray-500 ml-1">
|
||||
(<?= htmlspecialchars((string)($period['total'] ?? '')) ?>)
|
||||
</span>
|
||||
<?php if (!empty($period['result'])): ?>
|
||||
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= $period['result'] === 'Tài' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700' ?>">
|
||||
<?= htmlspecialchars((string)$period['result']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-sm text-gray-400">未开奖</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 hidden md:table-cell">
|
||||
<?php if (!empty($period['created_at'])): ?>
|
||||
<div class="text-xs text-gray-500">
|
||||
<?= date('Y-m-d H:i', strtotime((string)$period['created_at'])) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-gray-400">时间未知</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<?php if (($period['status'] ?? '') === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-lock-btn text-gray-500 hover:text-warning"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="封盘"
|
||||
>
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (($period['status'] ?? '') === 'locked' || ($period['status'] ?? '') === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn text-gray-500 hover:text-primary"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="录入开奖"
|
||||
>
|
||||
<i class="fas fa-dice"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (($period['status'] ?? '') === 'drawn'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn text-gray-500 hover:text-warning"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="修改结果"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="period-settle-btn text-gray-500 hover:text-success"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="确认结算"
|
||||
>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fas fa-dice text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">暂无期号</h3>
|
||||
<p class="mt-1 text-gray-500 text-sm">
|
||||
当前还没有任何骰子游戏期号。
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 录入开奖结果模态框 -->
|
||||
<div
|
||||
id="drawPeriodBackdrop"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="drawPeriodModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-dice text-primary mr-2"></i>
|
||||
录入开奖结果
|
||||
</h3>
|
||||
<button id="closeDrawPeriodBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="drawPeriodForm" class="space-y-4">
|
||||
<input type="hidden" id="drawPeriodId" name="id">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
期号
|
||||
</label>
|
||||
<p id="drawPeriodNumber" class="text-lg font-bold text-gray-900"></p>
|
||||
</div>
|
||||
|
||||
<!-- 骰子游戏输入 -->
|
||||
<div id="diceInputSection" class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="drawDice1" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
骰子1 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drawDice1"
|
||||
name="dice1"
|
||||
min="1"
|
||||
max="6"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
|
||||
placeholder="1-6"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="drawDice2" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
骰子2 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drawDice2"
|
||||
name="dice2"
|
||||
min="1"
|
||||
max="6"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
|
||||
placeholder="1-6"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="drawDice3" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
骰子3 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drawDice3"
|
||||
name="dice3"
|
||||
min="1"
|
||||
max="6"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
|
||||
placeholder="1-6"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="drawResultPreview" class="hidden p-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600 mb-1">开奖结果预览:</p>
|
||||
<p class="text-lg font-bold">
|
||||
<span id="drawResultText"></span>
|
||||
<span id="drawResultTotal" class="ml-2 text-gray-500"></span>
|
||||
<span id="drawResultType" class="ml-2"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
id="submitDrawPeriodBtn"
|
||||
class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-check mr-2"></i>
|
||||
确认录入
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/Static/js/admin.js"></script>
|
||||
<script>
|
||||
layui.use(['layer'], function() {
|
||||
var layer = layui.layer;
|
||||
|
||||
// 获取元素
|
||||
const closeDrawPeriodBtn = document.getElementById('closeDrawPeriodBtn');
|
||||
const drawPeriodBackdrop = document.getElementById('drawPeriodBackdrop');
|
||||
const drawPeriodModal = document.getElementById('drawPeriodModal');
|
||||
const drawPeriodForm = document.getElementById('drawPeriodForm');
|
||||
const submitDrawPeriodBtn = document.getElementById('submitDrawPeriodBtn');
|
||||
const drawDice1 = document.getElementById('drawDice1');
|
||||
const drawDice2 = document.getElementById('drawDice2');
|
||||
const drawDice3 = document.getElementById('drawDice3');
|
||||
const drawResultPreview = document.getElementById('drawResultPreview');
|
||||
const drawResultText = document.getElementById('drawResultText');
|
||||
const drawResultTotal = document.getElementById('drawResultTotal');
|
||||
const drawResultType = document.getElementById('drawResultType');
|
||||
|
||||
// 打开录入开奖模态框
|
||||
function openDrawPeriodModal(periodId) {
|
||||
fetch(`/admin/dice-periods/${periodId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const period = data.data;
|
||||
document.getElementById('drawPeriodId').value = period.id;
|
||||
document.getElementById('drawPeriodNumber').textContent = period.period_number;
|
||||
|
||||
// 填充已有的骰子数据
|
||||
drawDice1.value = period.dice1 || '';
|
||||
drawDice2.value = period.dice2 || '';
|
||||
drawDice3.value = period.dice3 || '';
|
||||
|
||||
updateDrawPreview();
|
||||
|
||||
drawPeriodBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
drawPeriodModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
drawPeriodModal.classList.add('scale-100');
|
||||
} else {
|
||||
layer.msg(data.message || '获取期号信息失败', {icon: 2});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
layer.msg('获取期号信息失败:' + e.message, {icon: 2});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭录入开奖模态框
|
||||
function closeDrawPeriodModal() {
|
||||
drawPeriodBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
drawPeriodModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
drawPeriodModal.classList.remove('scale-100');
|
||||
drawPeriodForm.reset();
|
||||
drawResultPreview.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 更新开奖结果预览
|
||||
function updateDrawPreview() {
|
||||
const d1 = parseInt(drawDice1.value) || 0;
|
||||
const d2 = parseInt(drawDice2.value) || 0;
|
||||
const d3 = parseInt(drawDice3.value) || 0;
|
||||
|
||||
if (d1 >= 1 && d1 <= 6 && d2 >= 1 && d2 <= 6 && d3 >= 1 && d3 <= 6) {
|
||||
const total = d1 + d2 + d3;
|
||||
let result = '';
|
||||
let resultClass = '';
|
||||
|
||||
// 检查是否为爆子
|
||||
if (d1 === d2 && d2 === d3) {
|
||||
if (d1 <= 3) {
|
||||
result = 'Xỉu';
|
||||
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
|
||||
} else {
|
||||
result = 'Tài';
|
||||
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
|
||||
}
|
||||
} else if (total >= 4 && total <= 10) {
|
||||
result = 'Xỉu';
|
||||
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
|
||||
} else {
|
||||
result = 'Tài';
|
||||
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
|
||||
}
|
||||
|
||||
drawResultText.textContent = `${d1}.${d2}.${d3}`;
|
||||
drawResultTotal.textContent = `(总和: ${total})`;
|
||||
drawResultType.innerHTML = `<span class="${resultClass}">${result}</span>`;
|
||||
drawResultPreview.classList.remove('hidden');
|
||||
} else {
|
||||
drawResultPreview.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 提交录入开奖
|
||||
async function submitDrawPeriod() {
|
||||
const dice1 = parseInt(drawDice1.value);
|
||||
const dice2 = parseInt(drawDice2.value);
|
||||
const dice3 = parseInt(drawDice3.value);
|
||||
|
||||
if (!dice1 || !dice2 || !dice3 || dice1 < 1 || dice1 > 6 || dice2 < 1 || dice2 > 6 || dice3 < 1 || dice3 > 6) {
|
||||
layer.msg('请输入有效的骰子点数(1-6)', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
id: document.getElementById('drawPeriodId').value,
|
||||
auto: false,
|
||||
dice1: dice1,
|
||||
dice2: dice2,
|
||||
dice3: dice3
|
||||
};
|
||||
|
||||
submitDrawPeriodBtn.disabled = true;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>录入中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/dice-periods/draw', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '开奖结果录入成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '录入失败', {icon: 2});
|
||||
submitDrawPeriodBtn.disabled = false;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('录入失败:' + e.message, {icon: 2});
|
||||
submitDrawPeriodBtn.disabled = false;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
|
||||
}
|
||||
}
|
||||
|
||||
// 封盘
|
||||
async function lockPeriod(id) {
|
||||
layer.confirm('确定要封盘吗?封盘后将无法继续投注。', {icon: 3, title: '确认封盘'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/dice-periods/lock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '封盘成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '封盘失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('封盘失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认结算
|
||||
async function settlePeriod(id) {
|
||||
layer.confirm('确定要确认结算吗?此操作不可撤销。', {icon: 3, title: '确认结算'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/dice-periods/settle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '结算成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '结算失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('结算失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 开始下注
|
||||
async function startPeriod(event) {
|
||||
const gameId = event.currentTarget.getAttribute('data-game-id');
|
||||
|
||||
if (!gameId) {
|
||||
layer.msg('游戏ID缺失', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm('确定要开始新一期下注吗?', {icon: 3, title: '开始下注'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/dice-periods/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({game_id: parseInt(gameId)})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '启动成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '启动失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('启动失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
if (closeDrawPeriodBtn) {
|
||||
closeDrawPeriodBtn.addEventListener('click', closeDrawPeriodModal);
|
||||
}
|
||||
if (drawPeriodBackdrop) {
|
||||
drawPeriodBackdrop.addEventListener('click', closeDrawPeriodModal);
|
||||
}
|
||||
if (submitDrawPeriodBtn) {
|
||||
submitDrawPeriodBtn.addEventListener('click', submitDrawPeriod);
|
||||
}
|
||||
|
||||
// 骰子输入监听
|
||||
if (drawDice1 && drawDice2 && drawDice3) {
|
||||
[drawDice1, drawDice2, drawDice3].forEach(input => {
|
||||
input.addEventListener('input', updateDrawPreview);
|
||||
});
|
||||
}
|
||||
|
||||
// 事件委托:列表操作按钮
|
||||
const periodList = document.getElementById('periodList');
|
||||
if (periodList) {
|
||||
periodList.addEventListener('click', function(e) {
|
||||
const lockBtn = e.target.closest('.period-lock-btn');
|
||||
const drawBtn = e.target.closest('.period-draw-btn');
|
||||
const settleBtn = e.target.closest('.period-settle-btn');
|
||||
|
||||
if (lockBtn) {
|
||||
const id = lockBtn.getAttribute('data-id');
|
||||
if (id) lockPeriod(id);
|
||||
}
|
||||
if (drawBtn) {
|
||||
const id = drawBtn.getAttribute('data-id');
|
||||
if (id) openDrawPeriodModal(id);
|
||||
}
|
||||
if (settleBtn) {
|
||||
const id = settleBtn.getAttribute('data-id');
|
||||
if (id) settlePeriod(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前期号操作按钮
|
||||
document.querySelectorAll('.period-lock-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) lockPeriod(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.period-draw-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) openDrawPeriodModal(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.period-settle-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) settlePeriod(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start Button
|
||||
document.querySelectorAll('.period-start-btn').forEach(btn => {
|
||||
btn.addEventListener('click', startPeriod);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php $empCode=$_SESSION['emp_code']??''; $perms=$_SESSION['emp_permissions']??[]; ?>
|
||||
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>员工面板 - <?=$empCode?></title><script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>body{background:#111827;color:#fff;font-family:system-ui}</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
<header class="bg-gray-900 border-b border-gray-700 px-4 py-3 flex justify-between items-center">
|
||||
<span class="font-bold">👷 <?=$empCode?></span>
|
||||
<span class="text-xs text-gray-400">权限:<?php $pm=['deposit'=>'充值','withdraw'=>'提现']; echo implode(', ', array_map(function($p) use($pm){return $pm[$p]??$p;}, $perms)); ?></span>
|
||||
<a href="/logout" class="text-red-400 text-sm">退出</a>
|
||||
</header>
|
||||
<div class="max-w-4xl mx-auto p-4 space-y-4">
|
||||
|
||||
<!-- 搜索用户 -->
|
||||
<div class="bg-gray-800 rounded-xl p-4">
|
||||
<input type="text" id="searchUser" placeholder="搜索用户名..." oninput="filterUsers()" class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:border-blue-400 focus:outline-none">
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="bg-gray-800 rounded-xl p-4">
|
||||
<h3 class="text-sm font-bold mb-3">用户列表</h3>
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto" id="userList">
|
||||
<?php foreach($users??[] as $u): ?>
|
||||
<div class="user-row flex items-center justify-between py-2 border-b border-gray-700 text-sm" data-name="<?=strtolower($u['username'])?>">
|
||||
<div>
|
||||
<span class="text-white"><?=htmlspecialchars($u['username'])?></span>
|
||||
<span class="text-gray-400 ml-2">余额: <span class="text-yellow-400" id="bal_<?=$u['id']?>"><?=number_format($u['balance'],2)?></span></span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<?php if(in_array('deposit',$perms)): ?>
|
||||
<button onclick="doAdjust(<?=$u['id']?>,'deposit','<?=htmlspecialchars($u['username'])?>')" class="px-3 py-1 bg-green-600 rounded text-xs hover:bg-green-700">+ 充值</button>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('withdraw',$perms)): ?>
|
||||
<button onclick="doAdjust(<?=$u['id']?>,'withdraw','<?=htmlspecialchars($u['username'])?>')" class="px-3 py-1 bg-red-600 rounded text-xs hover:bg-red-700">- 提现</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<div class="bg-gray-800 rounded-xl p-4">
|
||||
<h3 class="text-sm font-bold mb-3">操作日志</h3>
|
||||
<div class="space-y-1 max-h-48 overflow-y-auto text-xs">
|
||||
<?php foreach($logs??[] as $log): ?>
|
||||
<div class="flex justify-between py-1 border-b border-gray-700">
|
||||
<span class="<?=$log['action']==='deposit'?'text-green-400':'text-red-400'?>"><?=$log['action']==='deposit'?'充值':'提现'?> → 用户#<?=$log['target_user_id']?></span>
|
||||
<span class="text-white"><?=number_format($log['amount'],2)?></span>
|
||||
<span class="text-gray-500"><?=$log['created_at']?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterUsers(){const q=document.getElementById('searchUser').value.toLowerCase();document.querySelectorAll('.user-row').forEach(r=>r.style.display=r.dataset.name.includes(q)?'':'none');}
|
||||
async function doAdjust(uid,action,name){
|
||||
const label=action==='deposit'?'充值':'提现';
|
||||
const amount=prompt(label+'金额(用户:'+name+'):');
|
||||
if(!amount||isNaN(amount)||parseFloat(amount)<=0)return;
|
||||
const remark=prompt('备注(可选):')||'';
|
||||
const r=await fetch('/employee/adjust-balance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user_id:uid,action,amount:parseFloat(amount),remark})});
|
||||
const d=await r.json();
|
||||
if(d.success){alert('操作成功!');document.getElementById('bal_'+uid).textContent=parseFloat(d.new_balance).toFixed(2);}else alert(d.message);
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>员工登录</title><script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>body{background:linear-gradient(135deg,#1a1a2e,#16213e);min-height:100vh;font-family:system-ui}</style>
|
||||
</head>
|
||||
<body class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="w-full max-w-sm bg-white/10 backdrop-blur rounded-2xl p-8 border border-white/10">
|
||||
<h1 class="text-center text-xl font-bold text-white mb-6">👷 员工登录</h1>
|
||||
<form onsubmit="return doLogin(event)">
|
||||
<div class="space-y-4">
|
||||
<input type="text" id="user" placeholder="用户名" required class="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/30 focus:border-blue-400 focus:outline-none">
|
||||
<input type="password" id="pass" placeholder="密码" required class="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/30 focus:border-blue-400 focus:outline-none">
|
||||
<button type="submit" class="w-full py-3 bg-blue-600 text-white font-bold rounded-lg hover:bg-blue-700">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="msg" class="mt-3 text-center text-sm text-red-400 hidden"></div>
|
||||
</div>
|
||||
<script>
|
||||
async function doLogin(e){e.preventDefault();
|
||||
const r=await fetch('/employee/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('user').value,password:document.getElementById('pass').value})});
|
||||
const d=await r.json();if(d.success)window.location.href=d.redirect;else{const m=document.getElementById('msg');m.textContent=d.message;m.classList.remove('hidden');}return false;}
|
||||
</script></body></html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">👷 员工管理</h2>
|
||||
<button onclick="showAdd()" class="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">+ 添加员工</button>
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left">编号</th><th class="px-4 py-3">用户名</th><th class="px-4 py-3">姓名</th><th class="px-4 py-3">班次</th><th class="px-4 py-3">权限</th><th class="px-4 py-3">状态</th><th class="px-4 py-3">最后登录</th><th class="px-4 py-3">操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($employees??[] as $e): $perms=json_decode($e['permissions']??'[]',true); ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2 font-mono font-bold"><?=$e['emp_code']?></td>
|
||||
<td class="px-4 py-2"><?=htmlspecialchars($e['username'])?></td>
|
||||
<td class="px-4 py-2"><?=htmlspecialchars($e['real_name']??'')?></td>
|
||||
<td class="px-4 py-2 text-center"><span class="px-2 py-1 rounded text-xs <?=$e['shift']==='day'?'bg-yellow-100 text-yellow-700':($e['shift']==='night'?'bg-indigo-100 text-indigo-700':'bg-gray-100')?>"><?=$e['shift']==='day'?'白班':($e['shift']==='night'?'夜班':'全天')?></span></td>
|
||||
<td class="px-4 py-2 text-xs"><?php $permMap=['deposit'=>'充值','withdraw'=>'提现']; echo implode(', ', array_map(function($p) use($permMap){return $permMap[$p]??$p;}, $perms)); ?></td>
|
||||
<td class="px-4 py-2 text-center"><?=$e['status']?'<span class="text-green-500">✓ 启用</span>':'<span class="text-red-500">✗ 禁用</span>'?></td>
|
||||
<td class="px-4 py-2 text-xs text-gray-400"><?=$e['last_login']??'-'?></td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<button onclick='editEmp(<?=json_encode($e)?>)' class="text-blue-500 text-xs">编辑</button>
|
||||
<button onclick="delEmp(<?=$e['id']?>)" class="text-red-500 text-xs ml-1">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table></div>
|
||||
|
||||
<div id="empModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl p-6 w-full max-w-md">
|
||||
<h3 class="font-bold mb-4" id="empTitle">添加员工</h3>
|
||||
<input type="hidden" id="empId" value="0">
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div><label class="text-xs text-gray-400">编号 (如 001)</label><input id="empCode" class="w-full border rounded px-3 py-2"></div>
|
||||
<div><label class="text-xs text-gray-400">用户名</label><input id="empUser" class="w-full border rounded px-3 py-2"></div>
|
||||
</div>
|
||||
<div><label class="text-xs text-gray-400">密码 (留空则不修改)</label><input type="password" id="empPass" class="w-full border rounded px-3 py-2"></div>
|
||||
<div><label class="text-xs text-gray-400">真实姓名</label><input id="empName" class="w-full border rounded px-3 py-2"></div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div><label class="text-xs text-gray-400">班次</label><select id="empShift" class="w-full border rounded px-3 py-2"><option value="all">全天</option><option value="day">白班 (8-20)</option><option value="night">夜班 (20-8)</option></select></div>
|
||||
<div><label class="text-xs text-gray-400">状态</label><select id="empStatus" class="w-full border rounded px-3 py-2"><option value="1">启用</option><option value="0">禁用</option></select></div>
|
||||
</div>
|
||||
<div><label class="text-xs text-gray-400">权限</label>
|
||||
<label class="flex items-center gap-2 mt-1"><input type="checkbox" id="permDeposit" checked> 充值</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="permWithdraw" checked> 提现</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button onclick="saveEmp()" class="flex-1 py-2 bg-blue-500 text-white rounded">保存</button>
|
||||
<button onclick="document.getElementById('empModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded">取消</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
<script>
|
||||
function showAdd(){document.getElementById('empId').value=0;document.getElementById('empTitle').textContent='添加员工';['empCode','empUser','empPass','empName'].forEach(i=>document.getElementById(i).value='');document.getElementById('empModal').classList.remove('hidden');}
|
||||
function editEmp(e){document.getElementById('empId').value=e.id;document.getElementById('empCode').value=e.emp_code;document.getElementById('empUser').value=e.username;document.getElementById('empName').value=e.real_name||'';document.getElementById('empShift').value=e.shift;document.getElementById('empStatus').value=e.status;const p=JSON.parse(e.permissions||'[]');document.getElementById('permDeposit').checked=p.includes('deposit');document.getElementById('permWithdraw').checked=p.includes('withdraw');document.getElementById('empTitle').textContent='编辑员工';document.getElementById('empModal').classList.remove('hidden');}
|
||||
async function saveEmp(){const perms=[];if(document.getElementById('permDeposit').checked)perms.push('deposit');if(document.getElementById('permWithdraw').checked)perms.push('withdraw');const b={id:parseInt(document.getElementById('empId').value),emp_code:document.getElementById('empCode').value,username:document.getElementById('empUser').value,password:document.getElementById('empPass').value,real_name:document.getElementById('empName').value,shift:document.getElementById('empShift').value,status:parseInt(document.getElementById('empStatus').value),permissions:perms};const r=await fetch('/admin/employees/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);}
|
||||
async function delEmp(id){if(!confirm('确定删除该员工?'))return;await fetch('/admin/employees/delete/'+id,{method:'POST'});location.reload();}
|
||||
</script>
|
||||
Executable
+200
@@ -0,0 +1,200 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-yen-sign text-primary mr-3"></i>
|
||||
财务管理
|
||||
</h1>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总充值</p>
|
||||
<h3 class="text-xl font-bold mt-1 text-success">
|
||||
<?= number_format((float)($stats['total_deposit'] ?? 0), 2) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-arrow-down text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总提现</p>
|
||||
<h3 class="text-xl font-bold mt-1 text-danger">
|
||||
<?= number_format((float)($stats['total_withdraw'] ?? 0), 2) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
|
||||
<i class="fas fa-arrow-up text-danger"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总投注</p>
|
||||
<h3 class="text-xl font-bold mt-1 text-warning">
|
||||
<?= number_format((float)($stats['total_bet'] ?? 0), 2) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||||
<i class="fas fa-coins text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总中奖</p>
|
||||
<h3 class="text-xl font-bold mt-1 text-primary">
|
||||
<?= number_format((float)($stats['total_win'] ?? 0), 2) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-trophy text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">今日充值</p>
|
||||
<h3 class="text-xl font-bold mt-1 text-success">
|
||||
<?= number_format((float)($stats['today_deposit'] ?? 0), 2) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-calendar-day text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">今日提现</p>
|
||||
<h3 class="text-xl font-bold mt-1 text-danger">
|
||||
<?= number_format((float)($stats['today_withdraw'] ?? 0), 2) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
|
||||
<i class="fas fa-calendar-day text-danger"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资金流水列表 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">资金流水</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">查看所有资金变动记录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">流水ID</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">余额变动</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">描述</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell">时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="transactionList">
|
||||
<?php if (!empty($transactions) && is_array($transactions)): ?>
|
||||
<?php foreach ($transactions as $tx): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-sm font-medium text-gray-900">#<?= htmlspecialchars((string)($tx['id'] ?? '')) ?></span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
<?= htmlspecialchars((string)($tx['username'] ?? '未知')) ?>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
ID: <?= htmlspecialchars((string)($tx['user_id'] ?? '')) ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$type = $tx['type'] ?? '';
|
||||
$typeMap = [
|
||||
'deposit' => ['text' => '充值', 'class' => 'bg-success/10 text-success'],
|
||||
'withdraw' => ['text' => '提现', 'class' => 'bg-danger/10 text-danger'],
|
||||
'bet' => ['text' => '投注', 'class' => 'bg-warning/10 text-warning'],
|
||||
'win' => ['text' => '中奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'refund' => ['text' => '退款', 'class' => 'bg-gray-100 text-gray-700'],
|
||||
'manual_deposit' => ['text' => '人工加款', 'class' => 'bg-purple-100 text-purple-700'],
|
||||
'manual_withdraw' => ['text' => '人工扣款', 'class' => 'bg-orange-100 text-orange-700']
|
||||
];
|
||||
$typeInfo = $typeMap[$type] ?? ['text' => $type, 'class' => 'bg-gray-100 text-gray-700'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $typeInfo['class'] ?>">
|
||||
<?= $typeInfo['text'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$amount = floatval($tx['amount'] ?? 0);
|
||||
$amountClass = $amount >= 0 ? 'text-success' : 'text-danger';
|
||||
?>
|
||||
<span class="text-sm font-semibold <?= $amountClass ?>">
|
||||
<?= $amount >= 0 ? '+' : '' ?><?= number_format($amount, 2) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="text-gray-500"><?= number_format(floatval($tx['balance_before'] ?? 0), 2) ?></span>
|
||||
<i class="fas fa-arrow-right mx-1 text-gray-400"></i>
|
||||
<span class="font-semibold"><?= number_format(floatval($tx['balance_after'] ?? 0), 2) ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 hidden md:table-cell">
|
||||
<span class="text-sm text-gray-600">
|
||||
<?= htmlspecialchars((string)($tx['description'] ?? '-')) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 hidden lg:table-cell">
|
||||
<?php if (!empty($tx['created_at'])): ?>
|
||||
<div class="text-xs text-gray-500">
|
||||
<?= date('Y-m-d H:i:s', strtotime((string)$tx['created_at'])) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-gray-400">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fas fa-money-bill-wave text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">暂无流水记录</h3>
|
||||
<p class="mt-1 text-gray-500 text-sm">
|
||||
还没有任何资金流水记录。
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/Static/js/admin.js"></script>
|
||||
<script>
|
||||
layui.use(['layer'], function() {
|
||||
var layer = layui.layer;
|
||||
// 此处可以添加其他财务相关的JS逻辑
|
||||
});
|
||||
</script>
|
||||
|
||||
Executable
+1120
File diff suppressed because it is too large
Load Diff
Executable
+690
@@ -0,0 +1,690 @@
|
||||
<!-- 页面标题 -->
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-image text-primary mr-3"></i>图片图库管理
|
||||
</h1>
|
||||
<!-- 主内容区 -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<!-- 标签页导航 -->
|
||||
<div class="border-b border-gray-200">
|
||||
<div class="flex">
|
||||
<button id="gallery-tab-gallery" class="px-6 py-4 font-medium text-primary border-b-2 border-primary" data-tab="gallery"> <i class="fa fa-th-large mr-2"></i>图库 </button>
|
||||
<button id="gallery-upload-modal-trigger" class="px-6 py-4 font-medium text-gray-500 hover:text-gray-700"> <i class="fa fa-upload mr-2"></i>上传 </button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图库内容 -->
|
||||
<div id="gallery-content-gallery" class="p-6" data-tab="gallery">
|
||||
<!-- 批量删除按钮 -->
|
||||
<button id="gallery-batch-delete" class="mb-6 px-4 py-2 bg-red-500 text-white rounded-md flex items-center opacity-50 cursor-not-allowed" disabled> <i class="fa fa-trash mr-2"></i> 删除选中的图片 </button>
|
||||
<!-- 图片网格 -->
|
||||
<div id="gallery-media-grid" class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mb-6"></div>
|
||||
<!-- 加载更多按钮 -->
|
||||
<button id="gallery-load-more" class="w-full py-2 px-4 bg-gray-100 text-gray-800 rounded-md flex items-center justify-center hover:bg-gray-200 transition-colors"> <i class="fa fa-refresh mr-2"></i> 加载更多 </button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传弹出窗口 -->
|
||||
<div id="gallery-upload-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-800">上传图片</h2>
|
||||
<button id="gallery-close-upload-modal" class="text-gray-500 hover:text-gray-700"> <i class="fa fa-times text-xl"></i> </button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-6">
|
||||
<!-- 上传参数设置 -->
|
||||
<div class="space-y-4">
|
||||
<!-- 质量设置 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">质量设置 (1-100)</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<input type="range" id="gallery-quality" min="35" max="100" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500">
|
||||
<span id="gallery-quality-value" class="text-sm font-medium min-w-[3rem] text-center">60</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 宽度设置 -->
|
||||
<div>
|
||||
<label for="gallery-width" class="block text-sm font-medium text-gray-700 mb-2">转换宽度</label>
|
||||
<input type="number" id="gallery-width" placeholder="留空为自动" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<!-- 高度设置 -->
|
||||
<div>
|
||||
<label for="gallery-height" class="block text-sm font-medium text-gray-700 mb-2">转换高度</label>
|
||||
<input type="number" id="gallery-height" placeholder="留空为自动" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
<button id="gallery-upload-btn" class="w-full py-3 bg-blue-500 text-white rounded-md flex items-center justify-center hover:bg-blue-600 transition-colors">
|
||||
<i class="fa fa-cloud-upload mr-2"></i>
|
||||
<span id="gallery-upload-status">开始上传</span>
|
||||
</button>
|
||||
<input type="file" id="gallery-upload-input" multiple accept="image/*" class="hidden">
|
||||
<!-- 上传结果 -->
|
||||
<div id="gallery-upload-results" class="mt-6 space-y-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图片预览模态框 -->
|
||||
<div id="gallery-preview-modal" class="fixed inset-0 bg-black/90 z-50 hidden items-center justify-center p-4">
|
||||
<div class="relative max-w-5xl ">
|
||||
<!-- 图片容器 - 用于定位关闭按钮 -->
|
||||
<div class="relative inline-block">
|
||||
<img src="" alt="预览图片" class="max-w-full max-h-[80vh] bg-white mx-auto object-contain">
|
||||
<!-- 关闭按钮 - 绝对定位在图片右上角 -->
|
||||
<button id="gallery-preview-close" class="absolute -top-8 bg-white w-8 h-8 p-0 rounded-full -right-8 text-red text-2xl hover:text-gray-300 transition-colors">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 组件状态
|
||||
const galleryState = {
|
||||
targetInputId: null,
|
||||
currentPage: 1,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
mediaItems: [],
|
||||
isLoading: false,
|
||||
activeTab: 'gallery',
|
||||
selectedImageIds: []
|
||||
};
|
||||
|
||||
// DOM元素缓存 - 使用唯一ID避免冲突
|
||||
const galleryElements = {
|
||||
mediaGrid: document.getElementById('gallery-media-grid'),
|
||||
loadMoreBtn: document.getElementById('gallery-load-more'),
|
||||
batchDeleteBtn: document.getElementById('gallery-batch-delete'),
|
||||
previewModal: document.getElementById('gallery-preview-modal'),
|
||||
previewImage: document.querySelector('#gallery-preview-modal img'),
|
||||
previewCloseBtn: document.getElementById('gallery-preview-close'),
|
||||
uploadModal: document.getElementById('gallery-upload-modal'),
|
||||
uploadModalTrigger: document.getElementById('gallery-upload-modal-trigger'),
|
||||
closeUploadModal: document.getElementById('gallery-close-upload-modal'),
|
||||
uploadResults: document.getElementById('gallery-upload-results')
|
||||
};
|
||||
|
||||
// 初始化函数
|
||||
function initGallery() {
|
||||
setupGalleryEventListeners();
|
||||
fetchGalleryMediaList();
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function galleryFormatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(0)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 验证图片文件
|
||||
function galleryIsImageFile(file) {
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif', 'avif'];
|
||||
return imageExtensions.includes(extension) || /image\/.*/.test(file.type);
|
||||
}
|
||||
|
||||
// 本地存储表单状态 - 使用唯一键名
|
||||
function gallerySaveFormState(width, height) {
|
||||
localStorage.setItem('galleryFormState', JSON.stringify({ width, height }));
|
||||
}
|
||||
|
||||
function galleryLoadFormState() {
|
||||
const state = localStorage.getItem('galleryFormState');
|
||||
return state ? JSON.parse(state) : { width: '', height: '' };
|
||||
}
|
||||
|
||||
// 设置事件监听 - 全部使用唯一ID
|
||||
function setupGalleryEventListeners() {
|
||||
// 质量滑块事件
|
||||
const qualityInput = document.getElementById('gallery-quality');
|
||||
const qualityValue = document.getElementById('gallery-quality-value');
|
||||
if (qualityInput && qualityValue) {
|
||||
qualityInput.addEventListener('input', function() {
|
||||
qualityValue.textContent = this.value;
|
||||
});
|
||||
}
|
||||
|
||||
// 上传模态框控制
|
||||
galleryElements.uploadModalTrigger.addEventListener('click', () => {
|
||||
galleryElements.uploadModal.classList.remove('hidden');
|
||||
galleryElements.uploadModal.classList.add('flex');
|
||||
// 加载保存的表单状态
|
||||
const { width, height } = galleryLoadFormState();
|
||||
document.getElementById('gallery-width').value = width;
|
||||
document.getElementById('gallery-height').value = height;
|
||||
});
|
||||
|
||||
galleryElements.closeUploadModal.addEventListener('click', () => {
|
||||
galleryElements.uploadModal.classList.add('hidden');
|
||||
galleryElements.uploadModal.classList.remove('flex');
|
||||
});
|
||||
|
||||
// 点击模态框背景关闭
|
||||
galleryElements.uploadModal.addEventListener('click', (e) => {
|
||||
if (e.target === galleryElements.uploadModal) {
|
||||
galleryElements.uploadModal.classList.add('hidden');
|
||||
galleryElements.uploadModal.classList.remove('flex');
|
||||
}
|
||||
});
|
||||
|
||||
// 加载更多按钮
|
||||
galleryElements.loadMoreBtn.addEventListener('click', galleryLoadMoreImages);
|
||||
|
||||
// 批量删除按钮
|
||||
galleryElements.batchDeleteBtn.addEventListener('click', () => {
|
||||
if (galleryState.selectedImageIds.length > 0) {
|
||||
galleryConfirmDeleteImage(galleryState.selectedImageIds);
|
||||
}
|
||||
});
|
||||
|
||||
// 图片网格事件委托 - 使用数据属性识别元素类型
|
||||
galleryElements.mediaGrid.addEventListener('click', (e) => {
|
||||
// 预览图片 - 使用数据属性选择
|
||||
const previewBtn = e.target.closest('[data-action="preview"]');
|
||||
if (previewBtn) {
|
||||
const url = previewBtn.dataset.url;
|
||||
galleryPreviewFile(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// 复制链接 - 使用数据属性选择
|
||||
const copyBtn = e.target.closest('[data-action="copy"]');
|
||||
if (copyBtn) {
|
||||
const url = copyBtn.dataset.url;
|
||||
galleryCopyToClipboard(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// 图片复选框 - 使用数据属性选择
|
||||
const checkbox = e.target.closest('[data-type="image-checkbox"]');
|
||||
if (checkbox) {
|
||||
const imageId = checkbox.dataset.id;
|
||||
galleryToggleImageSelection(imageId, checkbox);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// 上传结果区域事件委托
|
||||
galleryElements.uploadResults.addEventListener('click', (e) => {
|
||||
const retryBtn = e.target.closest('[data-action="retry-upload"]');
|
||||
if (retryBtn) {
|
||||
document.getElementById('gallery-upload-input').click();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// 上传按钮
|
||||
document.getElementById('gallery-upload-btn').addEventListener('click', () => {
|
||||
document.getElementById('gallery-upload-input').click();
|
||||
});
|
||||
|
||||
// 文件选择事件
|
||||
document.getElementById('gallery-upload-input').addEventListener('change', galleryHandleFileSelect);
|
||||
|
||||
// 预览模态框关闭
|
||||
galleryElements.previewCloseBtn.addEventListener('click', () => {
|
||||
galleryElements.previewModal.classList.remove('flex');
|
||||
galleryElements.previewModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
galleryElements.previewModal.addEventListener('click', (e) => {
|
||||
if (e.target === galleryElements.previewModal) {
|
||||
galleryElements.previewModal.classList.remove('flex');
|
||||
galleryElements.previewModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换图片选择状态
|
||||
function galleryToggleImageSelection(imageId, checkbox) {
|
||||
const index = galleryState.selectedImageIds.indexOf(imageId);
|
||||
|
||||
if (index === -1) {
|
||||
// 选中
|
||||
galleryState.selectedImageIds.push(imageId);
|
||||
checkbox.checked = true;
|
||||
// 使用DOM导航找到父容器并应用样式
|
||||
checkbox.closest('[data-type="image-item"]').classList.add('ring-2', 'ring-blue-500', 'ring-offset-2');
|
||||
} else {
|
||||
// 取消选中
|
||||
galleryState.selectedImageIds.splice(index, 1);
|
||||
checkbox.checked = false;
|
||||
checkbox.closest('[data-type="image-item"]').classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2');
|
||||
}
|
||||
|
||||
// 更新批量删除按钮状态
|
||||
if (galleryState.selectedImageIds.length > 0) {
|
||||
galleryElements.batchDeleteBtn.disabled = false;
|
||||
galleryElements.batchDeleteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
galleryElements.batchDeleteBtn.classList.add('hover:bg-red-600');
|
||||
} else {
|
||||
galleryElements.batchDeleteBtn.disabled = true;
|
||||
galleryElements.batchDeleteBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
galleryElements.batchDeleteBtn.classList.remove('hover:bg-red-600');
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
function galleryConfirmDeleteImage(ids) {
|
||||
const isBatch = ids.length > 1;
|
||||
if (confirm(`确定要${isBatch ? '批量删除选中的' : '删除这张'}图片吗?此操作不可撤销。`)) {
|
||||
galleryDeleteImages(ids);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片函数
|
||||
function galleryDeleteImages(ids) {
|
||||
galleryState.isLoading = true;
|
||||
galleryShowLoading();
|
||||
|
||||
fetch('/admin/images/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showMessage(`成功删除${ids.length}张图片`, 'success');
|
||||
galleryState.currentPage = 1;
|
||||
galleryState.selectedImageIds = [];
|
||||
galleryState.isLoading = false;
|
||||
fetchGalleryMediaList();
|
||||
} else {
|
||||
showMessage(`删除失败:${data.message}`, 'error');
|
||||
galleryState.isLoading = false;
|
||||
galleryRenderMediaGrid();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('删除失败:', error);
|
||||
showMessage('网络错误,删除失败', 'error');
|
||||
galleryState.isLoading = false;
|
||||
galleryRenderMediaGrid();
|
||||
});
|
||||
}
|
||||
|
||||
// 获取图片列表
|
||||
function fetchGalleryMediaList() {
|
||||
if (galleryState.isLoading) return;
|
||||
galleryState.isLoading = true;
|
||||
|
||||
const isInitialLoad = galleryState.currentPage === 1;
|
||||
if (isInitialLoad) {
|
||||
galleryShowLoading();
|
||||
} else {
|
||||
const loadingIndicator = galleryCreateLoadingIndicator();
|
||||
galleryElements.mediaGrid.appendChild(loadingIndicator);
|
||||
}
|
||||
|
||||
const itemsPerPage = 20;
|
||||
const params = new URLSearchParams({
|
||||
page: galleryState.currentPage,
|
||||
limit: itemsPerPage
|
||||
});
|
||||
|
||||
fetch(`/admin/images/list?${params}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`列表请求失败: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
galleryState.currentPage = data.page;
|
||||
galleryState.totalItems = data.total;
|
||||
galleryState.totalPages = Math.ceil(data.total / itemsPerPage);
|
||||
|
||||
galleryState.mediaItems = isInitialLoad ? data.data : [...galleryState.mediaItems, ...data.data];
|
||||
galleryRenderMediaGrid();
|
||||
|
||||
if (galleryState.currentPage >= galleryState.totalPages) {
|
||||
galleryElements.loadMoreBtn.innerHTML = `<i class="fa fa-check mr-2"></i> 没有更多图片了`;
|
||||
galleryElements.loadMoreBtn.disabled = true;
|
||||
galleryElements.loadMoreBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
} else {
|
||||
galleryElements.loadMoreBtn.innerHTML = `<i class="fa fa-refresh mr-2"></i> 加载更多`;
|
||||
galleryElements.loadMoreBtn.disabled = false;
|
||||
galleryElements.loadMoreBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
galleryRenderMediaGrid();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取图片列表失败:', error);
|
||||
showMessage('网络错误,请重试', 'error');
|
||||
galleryRenderMediaGrid();
|
||||
})
|
||||
.finally(() => {
|
||||
galleryState.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function galleryShowLoading() {
|
||||
galleryElements.mediaGrid.innerHTML = '';
|
||||
const loadingIndicator = galleryCreateLoadingIndicator();
|
||||
galleryElements.mediaGrid.appendChild(loadingIndicator);
|
||||
}
|
||||
|
||||
// 加载更多图片
|
||||
function galleryLoadMoreImages() {
|
||||
if (galleryState.isLoading || galleryState.currentPage >= galleryState.totalPages) return;
|
||||
galleryState.currentPage++;
|
||||
fetchGalleryMediaList();
|
||||
}
|
||||
|
||||
// 创建加载指示器
|
||||
function galleryCreateLoadingIndicator() {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'col-span-full flex flex-col items-center justify-center py-12';
|
||||
indicator.innerHTML = `
|
||||
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500 mb-4"></div>
|
||||
<p class="text-gray-500">加载中...</p>
|
||||
`;
|
||||
return indicator;
|
||||
}
|
||||
|
||||
// 渲染图片网格
|
||||
function galleryRenderMediaGrid() {
|
||||
galleryElements.mediaGrid.innerHTML = '';
|
||||
const itemsToRender = galleryState.mediaItems;
|
||||
|
||||
if (itemsToRender.length === 0) {
|
||||
galleryElements.mediaGrid.innerHTML = `
|
||||
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center px-4">
|
||||
<i class="fa fa-picture-o text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="text-gray-500">没有找到图片</p>
|
||||
<button onclick="document.getElementById('gallery-upload-modal-trigger').click()" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
|
||||
<i class="fa fa-upload mr-2"></i>上传图片
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染图片项 - 使用数据属性代替自定义类名
|
||||
itemsToRender.forEach(item => {
|
||||
const isSelected = galleryState.selectedImageIds.includes(item.id.toString());
|
||||
const itemElement = document.createElement('div');
|
||||
// 使用数据属性标识元素类型,而非自定义类名
|
||||
itemElement.dataset.type = "image-item";
|
||||
itemElement.className = `bg-white rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2' : ''}`;
|
||||
itemElement.innerHTML = `
|
||||
<div class="relative aspect-[4/3] bg-gray-100 overflow-hidden">
|
||||
<!-- 复选框 - 使用数据属性标识 -->
|
||||
<input type="checkbox" data-type="image-checkbox" data-id="${item.id}"
|
||||
class="absolute top-2 left-2 z-10 w-4 h-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
|
||||
<!-- 缩略图 -->
|
||||
<img src="${item.url}" alt="${item.name}" class="w-full h-full object-cover" loading="lazy">
|
||||
|
||||
<!-- 操作按钮 - 使用数据属性标识操作类型 -->
|
||||
<div class="absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2 p-2">
|
||||
<button data-action="copy" data-url="${item.url}"
|
||||
class="bg-white w-8 h-8 p-0 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors"
|
||||
title="复制链接">
|
||||
<i class="fa fa-copy text-gray-800"></i>
|
||||
</button>
|
||||
<button data-action="preview" data-url="${item.url}"
|
||||
class="bg-white w-8 h-8 p-0 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors"
|
||||
title="预览图片">
|
||||
<i class="fa fa-eye text-gray-800"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片信息 -->
|
||||
<div class="p-2">
|
||||
<div class="text-xs font-medium text-gray-800 truncate mb-1" title="${item.name}">${item.name}</div>
|
||||
<div class="flex justify-between items-center text-xs text-gray-500">
|
||||
<span>${galleryFormatFileSize(item.size)}</span>
|
||||
<span>${item.width}*${item.height}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
galleryElements.mediaGrid.appendChild(itemElement);
|
||||
});
|
||||
|
||||
// 添加统计信息
|
||||
const statsElement = document.createElement('div');
|
||||
statsElement.className = 'col-span-full mt-6 pt-4 border-t border-gray-100 text-sm text-gray-500 flex justify-between items-center';
|
||||
statsElement.innerHTML = `
|
||||
<div>
|
||||
共计 <span class="font-semibold text-gray-800">${galleryState.totalItems}</span> 张图片
|
||||
</div>
|
||||
<div>
|
||||
已显示 <span class="font-semibold text-gray-800">${galleryState.mediaItems.length}</span> 张图片
|
||||
</div>
|
||||
`;
|
||||
galleryElements.mediaGrid.appendChild(statsElement);
|
||||
|
||||
// 更新批量删除按钮状态
|
||||
if (galleryState.selectedImageIds.length > 0) {
|
||||
galleryElements.batchDeleteBtn.disabled = false;
|
||||
galleryElements.batchDeleteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
galleryElements.batchDeleteBtn.classList.add('hover:bg-red-600');
|
||||
} else {
|
||||
galleryElements.batchDeleteBtn.disabled = true;
|
||||
galleryElements.batchDeleteBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
galleryElements.batchDeleteBtn.classList.remove('hover:bg-red-600');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
function galleryHandleFileSelect(e) {
|
||||
const files = e.target.files;
|
||||
if (!files.length) return;
|
||||
|
||||
const validFiles = [];
|
||||
const invalidFiles = [];
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
if (!galleryIsImageFile(file)) {
|
||||
invalidFiles.push({ file, reason: '不支持的文件类型,仅支持图片' });
|
||||
return;
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
invalidFiles.push({ file, reason: '文件过大,最大支持10MB' });
|
||||
return;
|
||||
}
|
||||
validFiles.push(file);
|
||||
});
|
||||
|
||||
galleryElements.uploadResults.innerHTML = '';
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
invalidFiles.forEach(({ file, reason }) => {
|
||||
const errorItem = document.createElement('div');
|
||||
errorItem.className = 'p-4 border border-red-200 bg-red-50 rounded-md';
|
||||
errorItem.innerHTML = `
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<span class="font-medium text-red-800 text-sm">${file.name}</span>
|
||||
<span class="text-red-600 text-xs">错误</span>
|
||||
</div>
|
||||
<p class="text-red-700 text-xs">${reason}</p>
|
||||
`;
|
||||
galleryElements.uploadResults.appendChild(errorItem);
|
||||
});
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
galleryUploadFiles(validFiles);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
function galleryUploadFiles(files) {
|
||||
const quality = document.getElementById('gallery-quality').value;
|
||||
const width = document.getElementById('gallery-width').value;
|
||||
const height = document.getElementById('gallery-height').value;
|
||||
const uploadStatus = document.getElementById('gallery-upload-status');
|
||||
|
||||
gallerySaveFormState(width, height);
|
||||
|
||||
// 添加上传中的指示器
|
||||
files.forEach(file => {
|
||||
const progressItem = document.createElement('div');
|
||||
progressItem.className = 'p-4 border border-gray-200 rounded-md overflow-hidden';
|
||||
progressItem.innerHTML = `
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="font-medium text-gray-800 text-sm">${file.name}</span>
|
||||
<span class="text-blue-500 text-xs">上传中</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div data-type="upload-progress" class="bg-blue-500 h-1.5 rounded-full w-0 transition-all duration-300"></div>
|
||||
</div>
|
||||
`;
|
||||
galleryElements.uploadResults.appendChild(progressItem);
|
||||
});
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('quality', quality);
|
||||
if (width) formData.append('width', width);
|
||||
if (height) formData.append('height', height);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/admin/images/upload', true);
|
||||
xhr.timeout = 120000;
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
|
||||
const progressItem = progressItems[index];
|
||||
galleryHandleUploadError(file, '等待返回超时,已在后台处理,稍后到图片列表中查看', progressItem);
|
||||
};
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
uploadStatus.textContent = `上传中 ${Math.round(percentComplete)}%`;
|
||||
|
||||
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
|
||||
const progressItem = progressItems[index];
|
||||
const progressBar = progressItem.querySelector('[data-type="upload-progress"]');
|
||||
progressBar.style.width = `${percentComplete}%`;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onload = function() {
|
||||
uploadStatus.textContent = `开始上传`;
|
||||
|
||||
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
|
||||
const progressItem = progressItems[index];
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.status === "error") {
|
||||
galleryHandleUploadError(file, response.message, progressItem);
|
||||
} else {
|
||||
galleryHandleUploadSuccess(response, progressItem);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析响应失败:', error);
|
||||
galleryHandleUploadError(file, '解析服务器响应失败', progressItem);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const responseData = JSON.parse(xhr.responseText);
|
||||
galleryHandleUploadError(file, responseData.message || '服务器错误', progressItem);
|
||||
} catch (parseError) {
|
||||
galleryHandleUploadError(file, `服务器错误 (${xhr.status})`, progressItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
uploadStatus.textContent = `开始上传`;
|
||||
const progressItems = galleryElements.uploadResults.querySelectorAll('.p-4');
|
||||
const progressItem = progressItems[index];
|
||||
galleryHandleUploadError(file, '网络错误,请重试', progressItem);
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
function galleryHandleUploadSuccess(response, progressItem) {
|
||||
if (!response.data || !response.data.url) {
|
||||
console.error('上传成功但缺少URL:', response);
|
||||
return;
|
||||
}
|
||||
const newItem = {
|
||||
id: response.data.id || Date.now(),
|
||||
name: response.data.name,
|
||||
url: response.data.url,
|
||||
size: response.data.size || 0,
|
||||
width: response.data.width || 0,
|
||||
height: response.data.height || 0
|
||||
};
|
||||
|
||||
// 添加到图库列表
|
||||
galleryState.mediaItems.unshift(newItem);
|
||||
galleryState.totalItems = galleryState.mediaItems.length;
|
||||
fetchGalleryMediaList();
|
||||
showMessage('图片上传成功', 'success');
|
||||
setTimeout(() => {
|
||||
galleryElements.uploadModal.classList.add('hidden');
|
||||
galleryElements.uploadModal.classList.remove('flex');
|
||||
galleryElements.uploadResults.innerHTML = '';
|
||||
const fileInput = document.getElementById('gallery-upload-input');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 处理上传失败
|
||||
function galleryHandleUploadError(file, message, progressItem) {
|
||||
progressItem.innerHTML = `
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<span class="font-medium text-red-800 text-sm">${file.name}</span>
|
||||
<span class="text-red-600 text-xs">失败</span>
|
||||
</div>
|
||||
<p class="text-red-700 text-sm">${message}</p>
|
||||
<button data-action="retry-upload"
|
||||
class="mt-2 text-xs px-3 py-1 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 transition-colors">
|
||||
重试
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function galleryPreviewFile(url) {
|
||||
galleryElements.previewImage.src = url;
|
||||
galleryElements.previewImage.alt = '图片预览';
|
||||
galleryElements.previewModal.classList.remove('hidden');
|
||||
galleryElements.previewModal.classList.add('flex');
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
function galleryCopyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showMessage('链接已复制', 'success');
|
||||
}).catch(err => {
|
||||
console.error('无法复制文本: ', err);
|
||||
showMessage('复制失败,请手动复制', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', initGallery);
|
||||
|
||||
// 暴露全局函数(如果需要)
|
||||
window.fetchGalleryMediaList = fetchGalleryMediaList;
|
||||
</script>
|
||||
Executable
+279
@@ -0,0 +1,279 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title> <?php echo htmlspecialchars($title ?? '后台管理'); ?> </title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
|
||||
<!-- Layui -->
|
||||
<link rel="stylesheet" href="/Static/layui/css/layui.css">
|
||||
<script src="/Static/layui/layui.js"></script>
|
||||
<!-- 配置Tailwind自定义颜色 -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {primary: '#3b82f6', secondary: '#36CFC9', success: '#52C41A', warning: '#FAAD14', danger: '#FF4D4F', dark: '#1D2129', 'gray-light': '#F2F3F5', 'gray-medium': '#C9CDD4' },
|
||||
fontFamily: {
|
||||
inter: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body id="app" class="font-inter bg-gray-50 text-dark min-h-screen flex flex-col">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="bg-white border-b border-gray-200 sticky top-0 z-40">
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/admin/dashboard" class="flex items-center text-primary font-bold text-xl">
|
||||
<?php
|
||||
$adminSettings = \App\Core\SettingsHelper::getAll();
|
||||
?>
|
||||
<img src="<?= htmlspecialchars($adminSettings['site_logo'] ?? '/Static/tm68/logo_tm68.png.png') ?>" class="w-10 mr-2">
|
||||
<span>管理后台</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 左侧区域:移动端菜单按钮 + 用户区域 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- 用户菜单 -->
|
||||
<div class="relative order-2 group">
|
||||
<!-- 触发按钮 -->
|
||||
<button class="flex items-center space-x-2 focus:outline-none user-menu-button">
|
||||
<img class="h-8 w-8 rounded-full object-cover" src="https://picsum.photos/200/200?random=1" alt="<?=htmlspecialchars($_SESSION['username'] ?? '用户')?>的头像">
|
||||
<span class="hidden md:inline-block text-sm font-medium"> <?=htmlspecialchars($_SESSION['username'] ?? '用户')?> </span>
|
||||
<i class="fas fa-chevron-down text-xs text-gray-500 transition-transform duration-200 group-hover:rotate-180"></i>
|
||||
</button>
|
||||
<!-- 下拉菜单 (默认隐藏) -->
|
||||
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-1 z-50 transform opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 origin-top-right scale-95 group-hover:scale-100">
|
||||
<!-- 个人资料 -->
|
||||
<div id="profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
||||
<i class="fas fa-user mr-2 text-gray-500"></i>个人资料
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
<!-- 退出登录 -->
|
||||
<a href="/admin/logout" class="block px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i>退出登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button id="mobile-menu-button" class="md:hidden p-2 rounded-md hover:bg-gray-light order-3">
|
||||
<i class="fas fa-bars text-gray-600"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- 主要内容区 -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- 侧边栏导航 - 固定不动 -->
|
||||
<aside id="sidebar" class="w-64 bg-white border-r border-gray-200 fixed left-0 top-16 h-[calc(100vh-4rem)] z-30 transform -translate-x-full md:translate-x-0 transition-transform duration-300 ease-in-out overflow-y-auto ">
|
||||
<div class="p-4 h-full flex flex-col">
|
||||
<nav class="space-y-1 flex-1">
|
||||
<a href="/admin/dashboard" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-home w-5 text-center"></i>
|
||||
<span>控制台首页</span>
|
||||
</a>
|
||||
<a href="/admin/users" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-users w-5 text-center"></i>
|
||||
<span>用户管理</span>
|
||||
</a>
|
||||
<?php
|
||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'):
|
||||
?>
|
||||
<a href="/admin/admins" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-user-shield w-5 text-center"></i>
|
||||
<span>管理员管理</span>
|
||||
</a>
|
||||
<?php
|
||||
endif;
|
||||
?>
|
||||
<a href="/admin/pk10-periods" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-flag-checkered w-5 text-center"></i>
|
||||
<span>PK10 期号管理</span>
|
||||
</a>
|
||||
<a href="/admin/auto-period" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-clock w-5 text-center"></i>
|
||||
<span>自动开期设置</span>
|
||||
</a>
|
||||
<a href="/admin/games" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-gamepad w-5 text-center"></i>
|
||||
<span>游戏与赔率</span>
|
||||
</a>
|
||||
<a href="/admin/water" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-sliders-h w-5 text-center"></i>
|
||||
<span>放水控制</span>
|
||||
</a>
|
||||
<a href="/admin/bets" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-list-alt w-5 text-center"></i>
|
||||
<span>投注记录</span>
|
||||
</a>
|
||||
<a href="/admin/finance" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-dollar-sign w-5 text-center"></i>
|
||||
<span>财务管理</span>
|
||||
</a>
|
||||
<a href="/admin/agents" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-handshake w-5 text-center"></i>
|
||||
<span>代理管理</span>
|
||||
</a>
|
||||
<a href="/admin/employees" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-id-badge w-5 text-center"></i>
|
||||
<span>员工管理</span>
|
||||
</a>
|
||||
<a href="/admin/virtual" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-ghost w-5 text-center"></i>
|
||||
<span>虚拟账户</span>
|
||||
</a>
|
||||
<a href="/admin/reports" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-chart-bar w-5 text-center"></i>
|
||||
<span>数据报表</span>
|
||||
</a>
|
||||
<a href="/admin/settings" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-cog w-5 text-center"></i>
|
||||
<span>系统设置</span>
|
||||
</a>
|
||||
<?php
|
||||
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'):
|
||||
?>
|
||||
<a href="/admin/plugins" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
|
||||
<i class="fas fa-puzzle-piece w-5 text-center"></i>
|
||||
<span>插件管理</span>
|
||||
</a>
|
||||
<?php
|
||||
endif;
|
||||
|
||||
global $pluginManager;
|
||||
$menus = $pluginManager->getEnabledPluginMenus();
|
||||
if (is_array($menus) && !empty($menus)) {
|
||||
echo "<div class='mt-4 px-4 text-xs text-gray-400 tracking-wide'>插件扩展</div>";
|
||||
foreach ($menus as $menu) {
|
||||
$title = htmlspecialchars($menu['title'] ?? '');
|
||||
$icon = htmlspecialchars($menu['icon'] ?? 'fa fa-plug');
|
||||
$path = htmlspecialchars($menu['path'] ?? '#');
|
||||
$hasChildren = !empty($menu['children']) && is_array($menu['children']);
|
||||
|
||||
echo "<div class='menu-group'>";
|
||||
echo "<a href='{$path}' class='flex items-center justify-between gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light plugin-menu-link main-menu-item " . ($hasChildren ? 'menu-parent' : '') . "'>";
|
||||
echo " <div class='flex items-center gap-3'> <i class='{$icon} w-5 text-center'></i> <span>{$title}</span> </div>";
|
||||
|
||||
if ($hasChildren) {
|
||||
echo "<i class='fas fa-chevron-down text-xs text-gray-500 transition-transform duration-200'></i>";
|
||||
}
|
||||
echo "</a>";
|
||||
if ($hasChildren) {
|
||||
echo "<div class='submenu hidden pl-8'>";
|
||||
foreach ($menu['children'] as $child) {
|
||||
$childTitle = htmlspecialchars($child['title'] ?? '');
|
||||
$childPath = htmlspecialchars($child['path'] ?? '#');
|
||||
$childIcon = htmlspecialchars($child['icon'] ?? 'fa fa-circle');
|
||||
|
||||
echo "<a href='{$childPath}' class='flex items-center gap-3 px-6 py-2 rounded-lg text-sm transition-all mt-1 duration-200 hover:bg-gray-light plugin-submenu-link submenu-item'>";
|
||||
echo " <i class='{$childIcon} w-4 text-center'></i> <span>{$childTitle}</span>";
|
||||
echo "</a>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
}
|
||||
?>
|
||||
</nav>
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 text-center hover:bg-danger/10">
|
||||
<span>当前版本:V 1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- 主内容 - 可滚动 -->
|
||||
<main id="main-content" class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-gray-50 md:ml-64 transition-all duration-300">
|
||||
<?php
|
||||
// 插件主体内容
|
||||
if (isset($Content)) {
|
||||
echo $Content;
|
||||
} else {
|
||||
echo "<div class='bg-white rounded-lg p-6 shadow-[0_4px_20px_rgba(0,0,0,0.08)] mb-6'>
|
||||
<h1 class='text-2xl font-semibold mb-4'>控制台首页</h1>
|
||||
<p class='text-gray-600 mb-6'>欢迎使用系统控制台,请从左侧菜单选择需要操作的功能。</p>
|
||||
<div class='space-y-6'>";
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
</div>
|
||||
<!-- 个人资料弹窗 (默认隐藏) -->
|
||||
<div id="profileModal" class="fixed inset-0 z-50 flex items-center justify-center invisible opacity-0 transition-all duration-300">
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50" id="profileModalBackdrop"></div>
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-md mx-4 transform scale-95 transition-transform duration-300">
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">编辑个人资料</h3>
|
||||
<button id="closeProfileModal" class="text-gray-500 hover:text-gray-700"> <i class="fas fa-times"></i> </button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表单内容 -->
|
||||
<form id="profileForm" class="p-6">
|
||||
<!-- 隐藏的用户ID -->
|
||||
<input type="hidden" id="profileUserId" name="id" value="<?=htmlspecialchars($_SESSION['user_id'] ?? '')?>">
|
||||
<!-- 用户名 -->
|
||||
<div class="mb-4">
|
||||
<label for="profileUsername" class="block text-sm font-medium text-gray-700 mb-1"> 用户名 </label>
|
||||
<input type="text" id="profileUsername" name="username" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" value="<?=htmlspecialchars($_SESSION['username'] ?? '')?>">
|
||||
</div>
|
||||
<!-- 邮箱 -->
|
||||
<div class="mb-4">
|
||||
<label for="profileEmail" class="block text-sm font-medium text-gray-700 mb-1"> 邮箱地址 </label>
|
||||
<input type="email" id="profileEmail" name="email" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors">
|
||||
</div>
|
||||
<!-- 密码 (可选修改) -->
|
||||
<div class="mb-6">
|
||||
<label for="profilePassword" class="block text-sm font-medium text-gray-700 mb-1"> 密码(不填则不修改) </label>
|
||||
<input type="password" id="profilePassword" name="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" placeholder="至少8位,包含字母和数字">
|
||||
<p class="mt-1 text-xs text-gray-500">不修改密码请留空</p>
|
||||
</div>
|
||||
<!-- 角色(空容器,等待JS填充) -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"> 用户角色 </label>
|
||||
<div id="userRoleDisplay" class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-gray-700"></div>
|
||||
<input type="hidden" name="role" id="userRoleInput">
|
||||
</div>
|
||||
<!-- 状态(空容器,等待JS填充) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"> 账号状态 </label>
|
||||
<div id="userStatusDisplay" class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg"></div>
|
||||
<input type="hidden" name="status" id="userStatusInput">
|
||||
</div>
|
||||
<!-- 提交按钮 -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" id="cancelProfileBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"> 取消 </button>
|
||||
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"> 保存修改 </button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-white border-t border-gray-200 py-4 md:ml-64 transition-all duration-300">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<p class="text-sm text-gray-500"> © <?=date('Y')?> PK10 极速赛车后台管理系统 </p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- 移动端菜单遮罩层 -->
|
||||
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-20 hidden md:hidden"></div>
|
||||
<script> const targetUsername = '<?= htmlspecialchars($_SESSION[' username '] ?? ' ') ?>'; </script>
|
||||
<script src="/Static/js/controller.js"> </script>
|
||||
<script src="/Static/js/admin.js"> </script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Executable
+175
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台管理登录</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- 配置Tailwind自定义颜色 -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
'primary-light': '#93c5fd',
|
||||
'primary-dark': '#2563eb',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900 p-4">
|
||||
<!-- 成功消息容器 -->
|
||||
<div id="successMessage" class="fixed top-4 right-4 px-6 py-3 rounded-lg shadow shadow-lg flex items-center z-50 transition-all duration-300 transform translate-x-full bg-primary-light border border-primary/20 text-primary-dark">
|
||||
<i class="fas fa-check-circle mr-2"></i>
|
||||
<span id="successText"></span>
|
||||
<button onclick="hideMessages()" class="ml-4 text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息容器 -->
|
||||
<div id="errorMessage" class="fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg flex items-center z-50 transition-all duration-300 transform translate-x-full bg-red-50 border border-red-200 text-red-700">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||
<span id="errorText"></span>
|
||||
<button onclick="hideMessages()" class="ml-4 text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-md">
|
||||
<div class="auth-card bg-white/95 backdrop-blur rounded-2xl overflow-hidden w-full border border-slate-200 shadow-xl shadow-slate-900/20">
|
||||
<div class="px-8 pt-8 pb-4">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<a href="#" class="flex items-center text-primary font-bold text-2xl">
|
||||
<img src="/Static/img/logo.png" class="w-11 h-11 mr-3 rounded-xl shadow-sm" alt="Admin Logo">
|
||||
<span class="text-slate-800">管理后台</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 pb-8">
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-6 relative" role="alert">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||
<span class="block sm:inline"><?=htmlspecialchars($error)?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="loginForm" method="post" action="?s=login" class="space-y-6">
|
||||
<div class="space-y-1">
|
||||
<label for="username_or_email" class="block text-sm font-medium text-gray-700 mb-1">用户名或邮箱</label>
|
||||
<input type="text" id="username_or_email" name="username_or_email" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-300"
|
||||
placeholder="请输入用户名或邮箱">
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
||||
<div class="relative">
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-300"
|
||||
placeholder="请输入您的密码">
|
||||
<button type="button" id="togglePassword"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-primary transition-colors duration-300">
|
||||
<i class="far fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>为保护数据安全,请勿在公共设备上勾选浏览器保存密码</span>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all duration-300">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-6 text-center text-[11px] text-slate-400">
|
||||
© <?= date('Y') ?> 管理系统后台 · Internal Use Only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 显示消息提示
|
||||
function showMessage(text, type = 'success') {
|
||||
// 隐藏所有消息
|
||||
hideMessages();
|
||||
|
||||
// 显示对应类型的消息
|
||||
const messageElement = document.getElementById(type + 'Message');
|
||||
const textElement = document.getElementById(type + 'Text');
|
||||
|
||||
textElement.textContent = text;
|
||||
messageElement.classList.remove('translate-x-full');
|
||||
|
||||
// 3秒后自动隐藏
|
||||
setTimeout(hideMessages, 3000);
|
||||
}
|
||||
|
||||
// 隐藏所有消息
|
||||
function hideMessages() {
|
||||
document.getElementById('successMessage').classList.add('translate-x-full');
|
||||
document.getElementById('errorMessage').classList.add('translate-x-full');
|
||||
}
|
||||
|
||||
// 密码可见性切换
|
||||
document.getElementById('togglePassword').addEventListener('click', function() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const icon = this.querySelector('i');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
icon.classList.remove('far', 'fa-eye');
|
||||
icon.classList.add('far', 'fa-eye-slash');
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
icon.classList.remove('far', 'fa-eye-slash');
|
||||
icon.classList.add('far', 'fa-eye');
|
||||
}
|
||||
});
|
||||
// 登录表单处理
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
username_or_email: document.getElementById('username_or_email').value,
|
||||
password: document.getElementById('password').value,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
// 直接解析JSON响应(与后端JSON格式匹配)
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showMessage(result.message);
|
||||
// 使用后端返回的跳转地址
|
||||
if (result.redirect) {
|
||||
setTimeout(() => {
|
||||
window.location.href = result.redirect;
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
showMessage(result.message || '登录失败,请检查账号密码', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录请求失败:', error);
|
||||
showMessage('网络错误,登录失败', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Executable
+941
@@ -0,0 +1,941 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-calendar-alt text-primary mr-3"></i>
|
||||
期号管理
|
||||
</h1>
|
||||
|
||||
<!-- 游戏期号管理区域 -->
|
||||
<?php if (!empty($gamesList) && is_array($gamesList)): ?>
|
||||
<?php foreach ($gamesList as $game): ?>
|
||||
<?php
|
||||
$gameId = $game['id'];
|
||||
$gameName = $game['name'];
|
||||
$currentPeriod = isset($currentPeriods[$gameId]) ? $currentPeriods[$gameId] : null;
|
||||
?>
|
||||
|
||||
<!-- 单个游戏的期号卡片 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6 border-l-4 border-primary">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
|
||||
<i class="fas fa-gamepad text-primary mr-2"></i>
|
||||
<?= htmlspecialchars($gameName) ?>
|
||||
</h2>
|
||||
|
||||
<?php if ($currentPeriod): ?>
|
||||
<!-- 有当前期号 -->
|
||||
<div class="flex gap-2">
|
||||
<?php if ($currentPeriod['status'] === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-lock-btn inline-block bg-warning hover:bg-warning/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-lock mr-2"></i>封盘
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'locked'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn inline-block bg-success hover:bg-success/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-dice mr-2"></i>开奖
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'drawn'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-settle-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-coins mr-2"></i>结算
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'settled'): ?>
|
||||
<!-- 已结算,显示开始新一期按钮 -->
|
||||
<button
|
||||
type="button"
|
||||
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-game-id="<?= $gameId ?>"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>开始新一期
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- 无当前期号,显示开始按钮 -->
|
||||
<button
|
||||
type="button"
|
||||
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-game-id="<?= $gameId ?>"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>开始新一期
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($currentPeriod): ?>
|
||||
<!-- 当前期号信息 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">期号</p>
|
||||
<p class="text-lg font-bold text-gray-900"><?= htmlspecialchars($currentPeriod['period_number']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">状态</p>
|
||||
<?php
|
||||
$status = $currentPeriod['status'];
|
||||
$statusMap = [
|
||||
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
|
||||
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
|
||||
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
|
||||
];
|
||||
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm <?= $statusInfo['class'] ?>">
|
||||
<?= $statusInfo['text'] ?>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">开始时间</p>
|
||||
<p class="text-sm text-gray-900"><?= htmlspecialchars($currentPeriod['start_time'] ?? '-') ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">开奖结果</p>
|
||||
<?php if (!empty($currentPeriod['dice1'])): ?>
|
||||
<p class="text-lg font-bold text-gray-900">
|
||||
<?= $currentPeriod['dice1'] ?>.<?= $currentPeriod['dice2'] ?>.<?= $currentPeriod['dice3'] ?>
|
||||
<span class="text-sm text-gray-500">(<?= $currentPeriod['total'] ?>)</span>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="text-sm text-gray-400">未开奖</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- 无当前期号提示 -->
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-gray-500">暂无进行中的期号,点击"开始新一期"按钮启动</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">期号总数</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($periods) && is_array($periods) ? count($periods) : 0 ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-list text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">待开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-warning">
|
||||
<?php
|
||||
$pendingCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && $p['status'] === 'pending') {
|
||||
$pendingCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $pendingCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||||
<i class="fas fa-clock text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-success">
|
||||
<?php
|
||||
$drawnCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && ($p['status'] === 'drawn' || $p['status'] === 'settled')) {
|
||||
$drawnCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $drawnCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已结算</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-primary">
|
||||
<?php
|
||||
$settledCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && $p['status'] === 'settled') {
|
||||
$settledCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $settledCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-coins text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 期号列表 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">期号列表</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">管理游戏期号和开奖结果</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">关联游戏</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开奖结果</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">创建时间</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="periodList">
|
||||
<?php if (!empty($periods) && is_array($periods)): ?>
|
||||
<?php foreach ($periods as $period): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>">
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
<?= htmlspecialchars((string)($period['period_number'] ?? '')) ?>
|
||||
</div>
|
||||
<?php if (!empty($period['auto_generated'])): ?>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600 mt-1">
|
||||
自动生成
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$gameId = $period['game_id'] ?? null;
|
||||
$gameName = $gameId && isset($games[$gameId]) ? $games[$gameId] : '未关联';
|
||||
?>
|
||||
<span class="text-sm text-gray-600"><?= htmlspecialchars($gameName) ?></span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$status = $period['status'] ?? 'pending';
|
||||
$statusMap = [
|
||||
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
|
||||
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
|
||||
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
|
||||
];
|
||||
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
|
||||
<span class="w-2 h-2 rounded-full mr-1 <?= str_replace('/10', '', $statusInfo['class']) ?>"></span>
|
||||
<?= $statusInfo['text'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
// 获取游戏类型
|
||||
$gameId = $period['game_id'] ?? 0;
|
||||
$gameType = 'dice';
|
||||
if (isset($games[$gameId])) {
|
||||
// 从游戏列表获取类型(需要查询数据库)
|
||||
// 简化处理:通过 dice3 是否为 null 判断
|
||||
if ($period['dice3'] === null && !empty($period['result'])) {
|
||||
$gameType = 'xocdia';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据游戏类型显示开奖结果
|
||||
if ($gameType === 'xocdia'):
|
||||
// Xóc Đĩa: 显示硬币颜色
|
||||
if (!empty($period['result'])):
|
||||
$coins = json_decode($period['result'], true);
|
||||
if (is_array($coins) && count($coins) === 4):
|
||||
$redCount = $period['dice1'] ?? 0;
|
||||
$whiteCount = $period['dice2'] ?? 0;
|
||||
?>
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="font-semibold">
|
||||
<?php foreach ($coins as $coin): ?>
|
||||
<span class="inline-block w-5 h-5 rounded-full <?= $coin === 'red' ? 'bg-red-500' : 'bg-gray-200' ?> border border-gray-300 mr-1"></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<span class="text-gray-600 ml-2">
|
||||
(<?= $redCount ?>Đ <?= $whiteCount ?>T)
|
||||
</span>
|
||||
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700' ?>">
|
||||
<?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'Chẵn' : 'Lẻ' ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
else:
|
||||
?>
|
||||
<span class="text-sm text-gray-400">数据格式错误</span>
|
||||
<?php
|
||||
endif;
|
||||
else:
|
||||
?>
|
||||
<span class="text-sm text-gray-400">未开奖</span>
|
||||
<?php
|
||||
endif;
|
||||
else:
|
||||
// 骰子游戏: 显示骰子点数
|
||||
if (!empty($period['dice1']) && !empty($period['dice2']) && !empty($period['dice3'])):
|
||||
?>
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="font-semibold">
|
||||
<?= htmlspecialchars((string)$period['dice1']) ?>.
|
||||
<?= htmlspecialchars((string)$period['dice2']) ?>.
|
||||
<?= htmlspecialchars((string)$period['dice3']) ?>
|
||||
</span>
|
||||
<span class="text-gray-500 ml-1">
|
||||
(<?= htmlspecialchars((string)($period['total'] ?? '')) ?>)
|
||||
</span>
|
||||
<?php if (!empty($period['result'])): ?>
|
||||
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= $period['result'] === 'Tài' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700' ?>">
|
||||
<?= htmlspecialchars((string)$period['result']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
else:
|
||||
?>
|
||||
<span class="text-sm text-gray-400">未开奖</span>
|
||||
<?php
|
||||
endif;
|
||||
endif;
|
||||
?>
|
||||
</td>
|
||||
<td class="px-4 py-4 hidden md:table-cell">
|
||||
<?php if (!empty($period['created_at'])): ?>
|
||||
<div class="text-xs text-gray-500">
|
||||
<?= date('Y-m-d H:i', strtotime((string)$period['created_at'])) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-gray-400">时间未知</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<?php if (($period['status'] ?? '') === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-lock-btn text-gray-500 hover:text-warning"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="封盘"
|
||||
>
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (($period['status'] ?? '') === 'locked' || ($period['status'] ?? '') === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn text-gray-500 hover:text-primary"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="录入开奖"
|
||||
>
|
||||
<i class="fas fa-dice"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (($period['status'] ?? '') === 'drawn'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn text-gray-500 hover:text-warning"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="修改结果"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="period-settle-btn text-gray-500 hover:text-success"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="确认结算"
|
||||
>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fas fa-calendar-alt text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">暂无期号</h3>
|
||||
<p class="mt-1 text-gray-500 text-sm">
|
||||
当前还没有任何期号。
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 录入开奖结果模态框 -->
|
||||
<div
|
||||
id="drawPeriodBackdrop"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="drawPeriodModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-dice text-primary mr-2"></i>
|
||||
录入开奖结果
|
||||
</h3>
|
||||
<button id="closeDrawPeriodBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="drawPeriodForm" class="space-y-4">
|
||||
<input type="hidden" id="drawPeriodId" name="id">
|
||||
<input type="hidden" id="drawGameType" name="game_type">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
期号
|
||||
</label>
|
||||
<p id="drawPeriodNumber" class="text-lg font-bold text-gray-900"></p>
|
||||
</div>
|
||||
|
||||
<!-- 骰子游戏输入 -->
|
||||
<div id="diceInputSection" class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="drawDice1" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
骰子1 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drawDice1"
|
||||
name="dice1"
|
||||
min="1"
|
||||
max="6"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
|
||||
placeholder="1-6"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="drawDice2" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
骰子2 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drawDice2"
|
||||
name="dice2"
|
||||
min="1"
|
||||
max="6"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
|
||||
placeholder="1-6"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="drawDice3" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
骰子3 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drawDice3"
|
||||
name="dice3"
|
||||
min="1"
|
||||
max="6"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm text-center text-lg font-bold"
|
||||
placeholder="1-6"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Xóc Đĩa 硬币输入 -->
|
||||
<div id="xocdiaInputSection" class="hidden space-y-3">
|
||||
<p class="text-sm text-gray-600">选择4个硬币的颜色:</p>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币1</label>
|
||||
<select id="coin1" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币2</label>
|
||||
<select id="coin2" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币3</label>
|
||||
<select id="coin3" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币4</label>
|
||||
<select id="coin4" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="drawResultPreview" class="hidden p-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600 mb-1">开奖结果预览:</p>
|
||||
<p class="text-lg font-bold">
|
||||
<span id="drawResultText"></span>
|
||||
<span id="drawResultTotal" class="ml-2 text-gray-500"></span>
|
||||
<span id="drawResultType" class="ml-2"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
id="submitDrawPeriodBtn"
|
||||
class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-check mr-2"></i>
|
||||
确认录入
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/Static/js/admin.js"></script>
|
||||
<script>
|
||||
layui.use(['layer'], function() {
|
||||
var layer = layui.layer;
|
||||
|
||||
// 获取元素
|
||||
const closeDrawPeriodBtn = document.getElementById('closeDrawPeriodBtn');
|
||||
const drawPeriodBackdrop = document.getElementById('drawPeriodBackdrop');
|
||||
const drawPeriodModal = document.getElementById('drawPeriodModal');
|
||||
const drawPeriodForm = document.getElementById('drawPeriodForm');
|
||||
const submitDrawPeriodBtn = document.getElementById('submitDrawPeriodBtn');
|
||||
const drawDice1 = document.getElementById('drawDice1');
|
||||
const drawDice2 = document.getElementById('drawDice2');
|
||||
const drawDice3 = document.getElementById('drawDice3');
|
||||
const drawResultPreview = document.getElementById('drawResultPreview');
|
||||
const drawResultText = document.getElementById('drawResultText');
|
||||
const drawResultTotal = document.getElementById('drawResultTotal');
|
||||
const drawResultType = document.getElementById('drawResultType');
|
||||
|
||||
// 打开录入开奖模态框
|
||||
function openDrawPeriodModal(periodId) {
|
||||
fetch(`/admin/periods/${periodId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const period = data.data;
|
||||
document.getElementById('drawPeriodId').value = period.id;
|
||||
document.getElementById('drawPeriodNumber').textContent = period.period_number;
|
||||
|
||||
// 获取游戏类型
|
||||
const gameId = period.game_id;
|
||||
|
||||
// 获取游戏信息(包括类型)
|
||||
fetch(`/admin/games/${gameId}`)
|
||||
.then(res => res.json())
|
||||
.then(gameData => {
|
||||
if (gameData.success && gameData.data) {
|
||||
const gameType = gameData.data.type || 'dice';
|
||||
document.getElementById('drawGameType').value = gameType;
|
||||
|
||||
// 根据游戏类型显示不同的输入界面
|
||||
const diceSection = document.getElementById('diceInputSection');
|
||||
const xocdiaSection = document.getElementById('xocdiaInputSection');
|
||||
|
||||
if (gameType === 'xocdia') {
|
||||
diceSection.classList.add('hidden');
|
||||
xocdiaSection.classList.remove('hidden');
|
||||
// 清空骰子输入
|
||||
drawDice1.value = '';
|
||||
drawDice2.value = '';
|
||||
drawDice3.value = '';
|
||||
|
||||
// 如果已有开奖结果,填充硬币颜色
|
||||
if (period.result) {
|
||||
try {
|
||||
const coins = JSON.parse(period.result);
|
||||
if (Array.isArray(coins) && coins.length === 4) {
|
||||
document.getElementById('coin1').value = coins[0];
|
||||
document.getElementById('coin2').value = coins[1];
|
||||
document.getElementById('coin3').value = coins[2];
|
||||
document.getElementById('coin4').value = coins[3];
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
} else {
|
||||
diceSection.classList.remove('hidden');
|
||||
xocdiaSection.classList.add('hidden');
|
||||
// 填充已有的骰子数据
|
||||
drawDice1.value = period.dice1 || '';
|
||||
drawDice2.value = period.dice2 || '';
|
||||
drawDice3.value = period.dice3 || '';
|
||||
}
|
||||
|
||||
updateDrawPreview();
|
||||
|
||||
// 游戏类型设置完成后再打开模态框
|
||||
drawPeriodBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
drawPeriodModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
drawPeriodModal.classList.add('scale-100');
|
||||
} else {
|
||||
layer.msg('获取游戏信息失败', {icon: 2});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
layer.msg('获取游戏信息失败:' + e.message, {icon: 2});
|
||||
});
|
||||
} else {
|
||||
layer.msg(data.message || '获取期号信息失败', {icon: 2});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
layer.msg('获取期号信息失败:' + e.message, {icon: 2});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭录入开奖模态框
|
||||
function closeDrawPeriodModal() {
|
||||
drawPeriodBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
drawPeriodModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
drawPeriodModal.classList.remove('scale-100');
|
||||
drawPeriodForm.reset();
|
||||
drawResultPreview.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 更新开奖结果预览
|
||||
function updateDrawPreview() {
|
||||
const d1 = parseInt(drawDice1.value) || 0;
|
||||
const d2 = parseInt(drawDice2.value) || 0;
|
||||
const d3 = parseInt(drawDice3.value) || 0;
|
||||
|
||||
if (d1 >= 1 && d1 <= 6 && d2 >= 1 && d2 <= 6 && d3 >= 1 && d3 <= 6) {
|
||||
const total = d1 + d2 + d3;
|
||||
let result = '';
|
||||
let resultClass = '';
|
||||
|
||||
// 检查是否为爆子
|
||||
if (d1 === d2 && d2 === d3) {
|
||||
result = 'Bão';
|
||||
resultClass = 'bg-green-100 text-green-700 px-2 py-1 rounded text-sm';
|
||||
} else if (total >= 4 && total <= 10) {
|
||||
result = 'Xỉu';
|
||||
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
|
||||
} else {
|
||||
result = 'Tài';
|
||||
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
|
||||
}
|
||||
|
||||
drawResultText.textContent = `${d1}.${d2}.${d3}`;
|
||||
drawResultTotal.textContent = `(总和: ${total})`;
|
||||
drawResultType.innerHTML = `<span class="${resultClass}">${result}</span>`;
|
||||
drawResultPreview.classList.remove('hidden');
|
||||
} else {
|
||||
drawResultPreview.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 提交录入开奖
|
||||
async function submitDrawPeriod() {
|
||||
const gameType = document.getElementById('drawGameType').value;
|
||||
const data = {
|
||||
id: document.getElementById('drawPeriodId').value,
|
||||
auto: false
|
||||
};
|
||||
|
||||
console.log('=== 开奖提交调试信息 ===');
|
||||
console.log('游戏类型:', gameType);
|
||||
console.log('期号ID:', data.id);
|
||||
|
||||
// 根据游戏类型收集数据
|
||||
if (gameType === 'xocdia') {
|
||||
// Xóc Đĩa: 收集硬币颜色
|
||||
const coins = [
|
||||
document.getElementById('coin1').value,
|
||||
document.getElementById('coin2').value,
|
||||
document.getElementById('coin3').value,
|
||||
document.getElementById('coin4').value
|
||||
];
|
||||
data.coins = coins;
|
||||
console.log('硬币颜色:', coins);
|
||||
} else {
|
||||
// 骰子游戏: 收集骰子点数
|
||||
const dice1 = parseInt(drawDice1.value);
|
||||
const dice2 = parseInt(drawDice2.value);
|
||||
const dice3 = parseInt(drawDice3.value);
|
||||
|
||||
if (!dice1 || !dice2 || !dice3 || dice1 < 1 || dice1 > 6 || dice2 < 1 || dice2 > 6 || dice3 < 1 || dice3 > 6) {
|
||||
layer.msg('请输入有效的骰子点数(1-6)', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
data.dice1 = dice1;
|
||||
data.dice2 = dice2;
|
||||
data.dice3 = dice3;
|
||||
console.log('骰子点数:', dice1, dice2, dice3);
|
||||
}
|
||||
|
||||
console.log('发送的数据:', JSON.stringify(data));
|
||||
|
||||
submitDrawPeriodBtn.disabled = true;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>录入中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/periods/draw', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('服务器响应:', result);
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '开奖结果录入成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '录入失败', {icon: 2});
|
||||
submitDrawPeriodBtn.disabled = false;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交错误:', e);
|
||||
layer.msg('录入失败:' + e.message, {icon: 2});
|
||||
submitDrawPeriodBtn.disabled = false;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
|
||||
}
|
||||
}
|
||||
|
||||
// 封盘
|
||||
async function lockPeriod(id) {
|
||||
layer.confirm('确定要封盘吗?封盘后将无法继续投注。', {icon: 3, title: '确认封盘'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/periods/lock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '封盘成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '封盘失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('封盘失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认结算
|
||||
async function settlePeriod(id) {
|
||||
layer.confirm('确定要确认结算吗?结算后将自动创建下一期,此操作不可撤销。', {icon: 3, title: '确认结算'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/periods/settle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '结算成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '结算失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('结算失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 开始下注
|
||||
async function startPeriod(event) {
|
||||
// 从按钮的 data-game-id 属性获取游戏ID
|
||||
const gameId = event.currentTarget.getAttribute('data-game-id');
|
||||
|
||||
if (!gameId) {
|
||||
layer.msg('游戏ID缺失', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm('确定要开始新一期下注吗?', {icon: 3, title: '开始下注'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/periods/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({game_id: parseInt(gameId)})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '启动成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '启动失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('启动失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
if (closeDrawPeriodBtn) {
|
||||
closeDrawPeriodBtn.addEventListener('click', closeDrawPeriodModal);
|
||||
}
|
||||
if (drawPeriodBackdrop) {
|
||||
drawPeriodBackdrop.addEventListener('click', closeDrawPeriodModal);
|
||||
}
|
||||
if (submitDrawPeriodBtn) {
|
||||
submitDrawPeriodBtn.addEventListener('click', submitDrawPeriod);
|
||||
}
|
||||
|
||||
// 骰子输入监听
|
||||
if (drawDice1 && drawDice2 && drawDice3) {
|
||||
[drawDice1, drawDice2, drawDice3].forEach(input => {
|
||||
input.addEventListener('input', updateDrawPreview);
|
||||
});
|
||||
}
|
||||
|
||||
// 事件委托:列表操作按钮
|
||||
const periodList = document.getElementById('periodList');
|
||||
if (periodList) {
|
||||
periodList.addEventListener('click', function(e) {
|
||||
const lockBtn = e.target.closest('.period-lock-btn');
|
||||
const drawBtn = e.target.closest('.period-draw-btn');
|
||||
const settleBtn = e.target.closest('.period-settle-btn');
|
||||
|
||||
if (lockBtn) {
|
||||
const id = lockBtn.getAttribute('data-id');
|
||||
if (id) lockPeriod(id);
|
||||
}
|
||||
if (drawBtn) {
|
||||
const id = drawBtn.getAttribute('data-id');
|
||||
if (id) openDrawPeriodModal(id);
|
||||
}
|
||||
if (settleBtn) {
|
||||
const id = settleBtn.getAttribute('data-id');
|
||||
if (id) settlePeriod(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前期号操作按钮(如果在页面中存在)
|
||||
document.querySelectorAll('.period-lock-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) lockPeriod(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.period-draw-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) openDrawPeriodModal(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.period-settle-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) settlePeriod(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start Button
|
||||
document.querySelectorAll('.period-start-btn').forEach(btn => {
|
||||
btn.addEventListener('click', startPeriod);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">🏎️ PK10 期号管理</h2>
|
||||
|
||||
<!-- 统计 -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">总计</div><div class="text-2xl font-bold"><?=$stats['total']??0?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">待开奖</div><div class="text-2xl font-bold text-yellow-500"><?=$stats['pending']??0?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">已开奖</div><div class="text-2xl font-bold text-blue-500"><?=$stats['drawn']??0?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-sm">已结算</div><div class="text-2xl font-bold text-green-500"><?=$stats['settled']??0?></div></div>
|
||||
</div>
|
||||
|
||||
<!-- 当前期号 -->
|
||||
<div class="bg-white rounded-xl p-6 shadow mb-6">
|
||||
<h3 class="font-bold mb-3">当前期号</h3>
|
||||
<?php if($current): ?>
|
||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<span class="text-gray-400">期号:</span> <span class="font-mono font-bold"><?=$current['period_number']?></span>
|
||||
<span class="ml-4 px-2 py-1 rounded text-xs <?=$current['status']==='pending'?'bg-yellow-100 text-yellow-700':($current['status']==='drawn'?'bg-blue-100 text-blue-700':'bg-green-100 text-green-700')?>"><?=$current['status']==='pending'?'待开奖':($current['status']==='locked'?'已封盘':($current['status']==='drawn'?'已开奖':'已结算'))?></span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<?php if($current['status']==='pending'): ?>
|
||||
<button onclick="doAction('lock',<?=$current['id']?>)" class="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 text-sm">🔒 封盘</button>
|
||||
<button onclick="doAction('draw',<?=$current['id']?>)" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">🎲 自动开奖</button>
|
||||
<button onclick="showManualDraw(<?=$current['id']?>)" class="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm">✏️ 手动开奖</button>
|
||||
<?php elseif($current['status']==='locked'): ?>
|
||||
<button onclick="doAction('draw',<?=$current['id']?>)" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">🎲 自动开奖</button>
|
||||
<button onclick="showManualDraw(<?=$current['id']?>)" class="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm">✏️ 手动开奖</button>
|
||||
<?php elseif($current['status']==='drawn'): ?>
|
||||
<div class="flex gap-1 items-center mr-4">
|
||||
<?php if($current['pk10']): for($i=1;$i<=10;$i++): $v=$current['pk10']['rank_'.$i]??0; ?>
|
||||
<span class="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-xs font-bold"><?=$v?></span>
|
||||
<?php endfor; endif; ?>
|
||||
</div>
|
||||
<button onclick="doAction('settle',<?=$current['id']?>)" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">💰 结算</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-400">暂无进行中的期号</span>
|
||||
<button onclick="doAction('start',0)" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">▶ 开始新期</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 手动开奖弹窗 -->
|
||||
<div id="manualModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl p-6 w-full max-w-md">
|
||||
<h3 class="font-bold mb-4">手动开奖 - 输入结果 (1-10)</h3>
|
||||
<div class="grid grid-cols-5 gap-2 mb-4">
|
||||
<?php for($i=1;$i<=10;$i++): ?>
|
||||
<div>
|
||||
<label class="text-xs text-gray-400">第<?=$i?>名</label>
|
||||
<input type="number" min="1" max="10" id="mr<?=$i?>" class="w-full border rounded px-2 py-1 text-center" placeholder="<?=$i?>">
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="submitManualDraw()" class="flex-1 py-2 bg-purple-500 text-white rounded hover:bg-purple-600">确认开奖</button>
|
||||
<button onclick="document.getElementById('manualModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded hover:bg-gray-300">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史列表 -->
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50"><tr>
|
||||
<th class="px-4 py-3 text-left">期号</th><th class="px-4 py-3 text-left">状态</th><th class="px-4 py-3 text-left">结果</th><th class="px-4 py-3 text-left">冠亚和</th><th class="px-4 py-3 text-left">时间</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($periods??[] as $p): ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2 font-mono text-xs"><?=$p['period_number']?></td>
|
||||
<td class="px-4 py-2"><span class="px-2 py-1 rounded text-xs <?=$p['status']==='settled'?'bg-green-100 text-green-700':($p['status']==='drawn'?'bg-blue-100 text-blue-700':'bg-yellow-100 text-yellow-700')?>"><?=$p['status']==='settled'?'已结算':($p['status']==='drawn'?'已开奖':'待开奖')?></span></td>
|
||||
<td class="px-4 py-2">
|
||||
<?php if($p['pk10']): ?>
|
||||
<div class="flex gap-1"><?php for($i=1;$i<=10;$i++): $v=$p['pk10']['rank_'.$i]??0; ?><span class="w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs"><?=$v?></span><?php endfor; ?></div>
|
||||
<?php else: ?>---<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-2"><?=$p['pk10']['champion_sum']??'-'?></td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs"><?=$p['created_at']??''?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let manualPeriodId=0;
|
||||
function showManualDraw(id){manualPeriodId=id;document.getElementById('manualModal').classList.remove('hidden');}
|
||||
async function submitManualDraw(){
|
||||
const result=[];for(let i=1;i<=10;i++){const v=parseInt(document.getElementById('mr'+i).value);if(!v||v<1||v>10){alert('每个名次请输入1-10的数字');return;}result.push(v);}
|
||||
if(new Set(result).size!==10){alert('10个名次的车号不能重复');return;}
|
||||
await doAction('draw',manualPeriodId,{manual:true,result});
|
||||
document.getElementById('manualModal').classList.add('hidden');
|
||||
}
|
||||
async function doAction(action,periodId,extra={}){
|
||||
const body={period_id:periodId,...extra};
|
||||
const url='/admin/pk10-periods/'+action;
|
||||
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const d=await r.json();
|
||||
if(d.status==='success'){location.reload();}else{alert(d.message||'操作失败');}
|
||||
}
|
||||
</script>
|
||||
Executable
+291
@@ -0,0 +1,291 @@
|
||||
<div class="mb-6 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-dark">插件管理</h1>
|
||||
<p class="text-gray-500 mt-1">管理、安装和卸载系统插件</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center gap-2 btn-effect">
|
||||
<a target="_blank" href="https://plugins.juhe.me" >
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>插件库</span>
|
||||
</a>
|
||||
</button>
|
||||
<div id="uploadOverlay" class="hidden fixed inset-0 bg-black/50 z-40 transition-opacity duration-300"></div>
|
||||
<div id="uploadForm" class="hidden fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-white rounded-lg shadow-lg p-4 z-50 border border-gray-200 transition-all duration-300 scale-95 opacity-0">
|
||||
<h3 class="font-medium mb-3">上传插件</h3>
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm text-gray-600 mb-1">选择插件包 (.zip)</label>
|
||||
<input type="file" id="pluginZip" accept=".zip" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
|
||||
<p class="text-xs text-gray-500 mt-1">支持的格式: .zip</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" id="submitUpload" class="flex-1 bg-primary text-white px-3 py-2 rounded-md text-sm btn-effect">
|
||||
确认上传
|
||||
</button>
|
||||
<button type="button" id="cancelUpload" class="px-3 py-2 border border-gray-300 rounded-md text-sm btn-effect">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 插件统计卡片 -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总插件数</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?=count($plugins) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-puzzle-piece text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已启用</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?=count(array_filter($plugins, function($p) { return $p['status']; })) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-check text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">未启用</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?=count(array_filter($plugins, function($p) { return !$p['status']; })) ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-gray-medium/30 flex items-center justify-center">
|
||||
<i class="fas fa-times text-gray-medium"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 class="text-lg font-semibold">插件列表</h2>
|
||||
<button id="uploadBtn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg
|
||||
flex items-center gap-2 btn-effect text-sm">
|
||||
<i class="fas fa-upload"></i>
|
||||
<span>上传插件</span>
|
||||
</button>
|
||||
</div>
|
||||
<?php if (empty($plugins)): ?>
|
||||
<div class="p-10 text-center border-b border-gray-200">
|
||||
<i class="fas fa-puzzle-piece text-5xl text-gray-300 mb-4"></i>
|
||||
<h3 class="text-lg font-medium mb-2">暂无插件</h3>
|
||||
<p class="text-gray-500 mb-6">请上传并安装插件来扩展系统功能</p>
|
||||
<button id="emptyStateUploadBtn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center gap-2 btn-effect mx-auto">
|
||||
<i class="fas fa-upload"></i>
|
||||
<span>上传插件</span>
|
||||
</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<?php foreach ($plugins as $plugin): ?>
|
||||
<div class="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex flex-wrap md:flex-nowrap justify-between items-start gap-4">
|
||||
<!-- 插件信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<i class="<?= htmlspecialchars($plugin['icon']) ?> text-primary text-xl"></i>
|
||||
<h3 class="font-semibold text-gray-900 truncate">
|
||||
<?= htmlspecialchars($plugin['title']) ?>
|
||||
<span class="text-sm font-normal text-gray-500 ml-2">v<?= htmlspecialchars($plugin['version']) ?></span>
|
||||
</h3>
|
||||
<?php if ($plugin['status']): ?>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
已启用
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<?= $plugin['installed'] ? '已禁用' : '未安装' ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 text-sm mb-2">
|
||||
<span class="font-medium">简介:</span>
|
||||
<?= htmlspecialchars($plugin['description']) ?>
|
||||
</p>
|
||||
<p class="text-gray-600 text-sm">
|
||||
<span class="font-medium">URL:</span>
|
||||
<a href="<?= htmlspecialchars($plugin['url']) ?>" class="text-primary hover:underline">
|
||||
<?= htmlspecialchars($plugin['url']) ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<?php if ($plugin['installed']): ?>
|
||||
<button class="px-3 py-1.5 rounded border text-sm transition-colors btn-effect
|
||||
<?= $plugin['status'] ? 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100' : 'border-green-200 bg-green-50 text-green-700 hover:bg-green-100' ?>"
|
||||
data-action="toggle"
|
||||
data-url="/admin/plugins/toggle/<?= $plugin['name'] ?>">
|
||||
<?= $plugin['status'] ? '禁用' : '启用' ?>
|
||||
</button>
|
||||
<button class="px-3 py-1.5 rounded border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 text-sm transition-colors btn-effect"
|
||||
data-action="uninstall"
|
||||
data-url="/admin/plugins/uninstall/<?= $plugin['name'] ?>"
|
||||
data-confirm="确定要卸载此插件吗?卸载此插件将会删除所有和此插件有关的数据。此操作不可恢复!">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button class="px-3 py-1.5 rounded border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 text-sm transition-colors btn-effect"
|
||||
data-action="install"
|
||||
data-url="/admin/plugins/install/<?= $plugin['name'] ?>">
|
||||
安装
|
||||
</button>
|
||||
<button class="px-3 py-1.5 rounded border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 text-sm transition-colors btn-effect"
|
||||
data-action="delete"
|
||||
data-url="/admin/plugins/delete/<?= $plugin['name'] ?>"
|
||||
data-confirm="确定要删除此插件安装包吗?">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-blue-50 border border-blue-100 rounded-xl p-5">
|
||||
<div class="flex">
|
||||
<i class="fas fa-info-circle text-primary mt-0.5 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="font-medium text-primary mb-2">插件管理说明</h3>
|
||||
<ul class="text-sm text-gray-700 space-y-1">
|
||||
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 插件以ZIP格式上传,系统会自动解压并安装</li>
|
||||
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 禁用插件不会删除数据,卸载插件将清除所有相关数据</li>
|
||||
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 未安装的插件可以直接删除安装包,不会影响系统</li>
|
||||
<li><i class="fas fa-angle-right mr-1 text-primary/70"></i> 请只安装来自可信来源的插件,以确保系统安全</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('[data-action]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const url = btn.dataset.url;
|
||||
const confirmMsg = btn.dataset.confirm;
|
||||
if (confirmMsg && !confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('请求失败');
|
||||
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
showMessage(json.message, 'success');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(json.message, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'warning');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========= 插件上传弹窗功能 =========
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const emptyStateUploadBtn = document.getElementById('emptyStateUploadBtn');
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
const uploadOverlay = document.getElementById('uploadOverlay');
|
||||
const cancelUpload = document.getElementById('cancelUpload');
|
||||
const submitUpload = document.getElementById('submitUpload');
|
||||
const pluginZip = document.getElementById('pluginZip');
|
||||
|
||||
const showUploadForm = () => {
|
||||
uploadOverlay.classList.remove('hidden');
|
||||
uploadForm.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
uploadOverlay.classList.add('opacity-100');
|
||||
uploadForm.classList.remove('scale-95', 'opacity-0');
|
||||
uploadForm.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
const hideUploadForm = () => {
|
||||
uploadOverlay.classList.remove('opacity-100');
|
||||
uploadForm.classList.remove('scale-100', 'opacity-100');
|
||||
uploadForm.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
uploadOverlay.classList.add('hidden');
|
||||
uploadForm.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
pluginZip.value = '';
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!pluginZip.files.length) return;
|
||||
const file = pluginZip.files[0];
|
||||
if (!file.name.endsWith('.zip')) return;
|
||||
|
||||
submitUpload.disabled = true;
|
||||
submitUpload.textContent = '上传中...';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('plugin_zip', file);
|
||||
|
||||
fetch('/admin/plugins/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
alert(data.message || '安装失败');
|
||||
submitUpload.disabled = false;
|
||||
submitUpload.textContent = '安装插件';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('网络错误');
|
||||
submitUpload.disabled = false;
|
||||
submitUpload.textContent = '安装插件';
|
||||
});
|
||||
};
|
||||
|
||||
if (uploadBtn) uploadBtn.addEventListener('click', showUploadForm);
|
||||
if (emptyStateUploadBtn) emptyStateUploadBtn.addEventListener('click', showUploadForm);
|
||||
if (cancelUpload) cancelUpload.addEventListener('click', hideUploadForm);
|
||||
if (submitUpload) submitUpload.addEventListener('click', handleUpload);
|
||||
if (uploadOverlay) uploadOverlay.addEventListener('click', hideUploadForm);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !uploadForm.classList.contains('hidden')) {
|
||||
hideUploadForm();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">📊 数据报表</h2>
|
||||
|
||||
<!-- 日期筛选 -->
|
||||
<div class="bg-white rounded-xl p-4 shadow mb-6 flex items-center gap-4 flex-wrap">
|
||||
<label class="text-sm text-gray-400">起始:</label><input type="date" value="<?=$dateFrom?>" id="dateFrom" class="border rounded px-3 py-2 text-sm">
|
||||
<label class="text-sm text-gray-400">截止:</label><input type="date" value="<?=$dateTo?>" id="dateTo" class="border rounded px-3 py-2 text-sm">
|
||||
<button onclick="location.href='/admin/reports?from='+document.getElementById('dateFrom').value+'&to='+document.getElementById('dateTo').value" class="px-4 py-2 bg-blue-500 text-white rounded text-sm">筛选</button>
|
||||
</div>
|
||||
|
||||
<!-- 总览卡片 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总投注</div><div class="text-xl font-bold text-blue-500"><?=number_format($totalBet,2)?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总中奖</div><div class="text-xl font-bold text-red-500"><?=number_format($totalWin,2)?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">平台盈利</div><div class="text-xl font-bold <?=$profit>=0?'text-green-500':'text-red-500'?>"><?=number_format($profit,2)?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">佣金</div><div class="text-xl font-bold text-orange-500"><?=number_format($totalCommission,2)?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总充值</div><div class="text-xl font-bold text-green-500"><?=number_format($totalDeposit,2)?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总提现</div><div class="text-xl font-bold text-red-500"><?=number_format($totalWithdraw,2)?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总用户</div><div class="text-xl font-bold"><?=$totalUsers?></div></div>
|
||||
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">新增用户</div><div class="text-xl font-bold text-blue-500"><?=$newUsers?></div></div>
|
||||
</div>
|
||||
|
||||
<!-- 每日明细 -->
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden mb-6">
|
||||
<h3 class="font-bold p-4 border-b">每日明细</h3>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">日期</th><th class="px-4 py-2">投注</th><th class="px-4 py-2">中奖</th><th class="px-4 py-2">盈利</th><th class="px-4 py-2">充值</th><th class="px-4 py-2">提现</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($dailyStats??[] as $d): $dp=$d['bets']-$d['wins']; ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2"><?=$d['date']?></td>
|
||||
<td class="px-4 py-2 text-center"><?=number_format($d['bets'],2)?></td>
|
||||
<td class="px-4 py-2 text-center"><?=number_format($d['wins'],2)?></td>
|
||||
<td class="px-4 py-2 text-center <?=$dp>=0?'text-green-500':'text-red-500'?>"><?=number_format($dp,2)?></td>
|
||||
<td class="px-4 py-2 text-center"><?=number_format($d['deposits'],2)?></td>
|
||||
<td class="px-4 py-2 text-center"><?=number_format($d['withdraws'],2)?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table></div>
|
||||
|
||||
<!-- 代理报表 -->
|
||||
<?php if(!empty($agentStats)): ?>
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<h3 class="font-bold p-4 border-b">代理报表</h3>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">代理</th><th class="px-4 py-2">玩家数</th><th class="px-4 py-2">投注</th><th class="px-4 py-2">中奖</th><th class="px-4 py-2">佣金</th><th class="px-4 py-2">盈利</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($agentStats as $as): ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2"><?=htmlspecialchars($as['user']['username']??'')?></td>
|
||||
<td class="px-4 py-2 text-center"><?=$as['players']?></td>
|
||||
<td class="px-4 py-2 text-center"><?=number_format($as['bets'],2)?></td>
|
||||
<td class="px-4 py-2 text-center"><?=number_format($as['wins'],2)?></td>
|
||||
<td class="px-4 py-2 text-center text-orange-500"><?=number_format($as['commission'],2)?></td>
|
||||
<td class="px-4 py-2 text-center <?=$as['profit']>=0?'text-green-500':'text-red-500'?>"><?=number_format($as['profit'],2)?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Executable
+388
@@ -0,0 +1,388 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-cog text-primary mr-3"></i>
|
||||
系统设置
|
||||
</h1>
|
||||
|
||||
<!-- 设置表单 -->
|
||||
<form id="settingsForm" class="space-y-6">
|
||||
<!-- Logo设置 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-image text-primary mr-2"></i>
|
||||
Logo设置
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Logo -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">网站Logo</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<img id="site_logo_preview" src="<?= htmlspecialchars($settings['site_logo'] ?? '') ?>"
|
||||
alt="Logo预览"
|
||||
class="w-32 h-32 object-contain border border-gray-200 rounded-lg bg-gray-50 p-2"
|
||||
style="<?= empty($settings['site_logo'] ?? '') ? 'display:none;' : '' ?>">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<input type="file" id="site_logo_file" name="site_logo_file" accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
class="hidden" onchange="handleLogoUpload(this, 'site_logo')">
|
||||
<button type="button" onclick="document.getElementById('site_logo_file').click()"
|
||||
class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
|
||||
<i class="fas fa-upload mr-2"></i>选择图片
|
||||
</button>
|
||||
<input type="hidden" id="site_logo" name="site_logo" value="<?= htmlspecialchars($settings['site_logo'] ?? '') ?>">
|
||||
<p class="text-xs text-gray-500 mt-2">建议尺寸:200x60px,支持JPG/PNG/GIF/WEBP,前端统一使用此Logo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favicon -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">网站图标 (Favicon)</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<img id="site_favicon_preview" src="<?= htmlspecialchars($settings['site_favicon'] ?? '') ?>"
|
||||
alt="Favicon预览"
|
||||
class="w-16 h-16 object-contain border border-gray-200 rounded-lg bg-gray-50 p-2"
|
||||
style="<?= empty($settings['site_favicon'] ?? '') ? 'display:none;' : '' ?>">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<input type="file" id="site_favicon_file" name="site_favicon_file" accept="image/x-icon,image/vnd.microsoft.icon,image/png"
|
||||
class="hidden" onchange="handleLogoUpload(this, 'site_favicon')">
|
||||
<button type="button" onclick="document.getElementById('site_favicon_file').click()"
|
||||
class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors">
|
||||
<i class="fas fa-upload mr-2"></i>选择图标
|
||||
</button>
|
||||
<input type="hidden" id="site_favicon" name="site_favicon" value="<?= htmlspecialchars($settings['site_favicon'] ?? '') ?>">
|
||||
<p class="text-xs text-gray-500 mt-2">建议尺寸:32x32px,支持ICO/PNG格式</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网站基本信息 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-info-circle text-primary mr-2"></i>
|
||||
网站基本信息
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="site_title" class="block text-sm font-medium text-gray-700 mb-1">网站标题</label>
|
||||
<input type="text" id="site_title" name="site_title"
|
||||
value="<?= htmlspecialchars($settings['site_title'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入网站标题">
|
||||
</div>
|
||||
<div>
|
||||
<label for="site_description" class="block text-sm font-medium text-gray-700 mb-1">网站描述</label>
|
||||
<textarea id="site_description" name="site_description" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-none"
|
||||
placeholder="请输入网站描述"><?= htmlspecialchars($settings['site_description'] ?? '') ?></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">用于SEO优化,建议控制在150字以内</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="site_keywords" class="block text-sm font-medium text-gray-700 mb-1">网站关键词</label>
|
||||
<input type="text" id="site_keywords" name="site_keywords"
|
||||
value="<?= htmlspecialchars($settings['site_keywords'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入关键词,用逗号分隔">
|
||||
<p class="text-xs text-gray-500 mt-1">多个关键词用逗号分隔</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="site_copyright" class="block text-sm font-medium text-gray-700 mb-1">版权信息</label>
|
||||
<input type="text" id="site_copyright" name="site_copyright"
|
||||
value="<?= htmlspecialchars($settings['site_copyright'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入版权信息">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button type="button" onclick="resetSettings()"
|
||||
class="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
<i class="fas fa-undo mr-2"></i>重置
|
||||
</button>
|
||||
<button type="button" id="saveSettingsBtn"
|
||||
class="px-6 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
<i class="fas fa-save mr-2"></i>保存设置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SMTP 邮件配置 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-envelope text-primary mr-2"></i>
|
||||
SMTP 邮件配置
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">SMTP 服务器</label>
|
||||
<input type="text" id="smtp_host" name="smtp_host" value="<?= htmlspecialchars($settings['smtp_host'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="如 smtp.gmail.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">端口</label>
|
||||
<input type="number" id="smtp_port" name="smtp_port" value="<?= htmlspecialchars($settings['smtp_port'] ?? '465') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="465">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">加密方式</label>
|
||||
<select id="smtp_encryption" name="smtp_encryption"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors">
|
||||
<option value="ssl" <?= ($settings['smtp_encryption'] ?? 'ssl') === 'ssl' ? 'selected' : '' ?>>SSL (端口465)</option>
|
||||
<option value="tls" <?= ($settings['smtp_encryption'] ?? '') === 'tls' ? 'selected' : '' ?>>TLS (端口587)</option>
|
||||
<option value="none" <?= ($settings['smtp_encryption'] ?? '') === 'none' ? 'selected' : '' ?>>无加密 (端口25)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">SMTP 用户名</label>
|
||||
<input type="text" id="smtp_user" name="smtp_user" value="<?= htmlspecialchars($settings['smtp_user'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="your@email.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">SMTP 密码</label>
|
||||
<input type="password" id="smtp_pass" name="smtp_pass" value="<?= htmlspecialchars($settings['smtp_pass'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="密码或应用专用密码">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">发件人地址</label>
|
||||
<input type="text" id="smtp_from" name="smtp_from" value="<?= htmlspecialchars($settings['smtp_from'] ?? '') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="noreply@yourdomain.com(留空则用SMTP用户名)">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">发件人名称</label>
|
||||
<input type="text" id="smtp_from_name" name="smtp_from_name" value="<?= htmlspecialchars($settings['smtp_from_name'] ?? 'PK10') ?>"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="PK10">
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button type="button" onclick="testSmtp()"
|
||||
class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
|
||||
<i class="fas fa-paper-plane mr-2"></i>发送测试邮件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-3">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
常用配置:Gmail(smtp.gmail.com:465/SSL)、Outlook(smtp-mail.outlook.com:587/TLS)、QQ邮箱(smtp.qq.com:465/SSL)、163邮箱(smtp.163.com:465/SSL)
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function testSmtp(){
|
||||
const email=prompt('输入接收测试邮件的邮箱地址:');
|
||||
if(!email)return;
|
||||
// 先保存当前配置再测试
|
||||
const data={};
|
||||
document.querySelectorAll('#settingsForm [name^="smtp_"]').forEach(el=>{data[el.name]=el.value;});
|
||||
fetch('/admin/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})
|
||||
.then(()=>fetch('/admin/settings/smtp-test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email})}))
|
||||
.then(r=>r.json())
|
||||
.then(d=>{showMessage(d.message,d.success?'success':'error');})
|
||||
.catch(()=>{showMessage('测试失败','error');});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Logo上传处理
|
||||
function handleLogoUpload(input, logoType) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// 验证文件大小(5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('文件大小不能超过5MB');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
const allowedTypes = logoType === 'site_favicon'
|
||||
? ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png']
|
||||
: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert('不支持的文件类型');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const previewId = logoType + '_preview';
|
||||
const previewImg = document.getElementById(previewId);
|
||||
if (previewImg) {
|
||||
previewImg.src = e.target.result;
|
||||
previewImg.style.display = '';
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// 立即上传
|
||||
uploadLogo(file, logoType);
|
||||
}
|
||||
|
||||
// 上传Logo到服务器
|
||||
function uploadLogo(file, logoType) {
|
||||
const formData = new FormData();
|
||||
formData.append(logoType + '_file', file);
|
||||
formData.append('logo_type', logoType);
|
||||
|
||||
// 显示上传中状态
|
||||
const saveBtn = document.getElementById('saveSettingsBtn');
|
||||
const originalText = saveBtn.innerHTML;
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>上传中...';
|
||||
|
||||
fetch('/admin/settings/save', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// 更新隐藏字段的值
|
||||
const hiddenInput = document.getElementById(logoType);
|
||||
const previewImg = document.getElementById(logoType + '_preview');
|
||||
if (data.data && data.data[logoType]) {
|
||||
if (hiddenInput) hiddenInput.value = data.data[logoType];
|
||||
if (previewImg) {
|
||||
previewImg.src = data.data[logoType];
|
||||
previewImg.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
showMessage('Logo上传成功', 'success');
|
||||
} else {
|
||||
alert('上传失败: ' + (data.message || '未知错误'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Upload error:', error);
|
||||
alert('上传失败,请稍后重试');
|
||||
})
|
||||
.finally(() => {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
document.getElementById('saveSettingsBtn').addEventListener('click', function() {
|
||||
const form = document.getElementById('settingsForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// 将FormData转换为JSON对象
|
||||
const data = {};
|
||||
for (let [key, value] of formData.entries()) {
|
||||
// 跳过文件输入
|
||||
if (key.endsWith('_file')) continue;
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
// 发送保存请求
|
||||
const saveBtn = this;
|
||||
const originalText = saveBtn.innerHTML;
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>保存中...';
|
||||
|
||||
fetch('/admin/settings/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showMessage('设置保存成功', 'success');
|
||||
// 如果有返回的数据,更新页面
|
||||
if (data.data) {
|
||||
updateSettingsPreview(data.data);
|
||||
}
|
||||
} else {
|
||||
showMessage('保存失败: ' + (data.message || '未知错误'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Save error:', error);
|
||||
showMessage('保存失败,请稍后重试', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalText;
|
||||
});
|
||||
});
|
||||
|
||||
// 重置设置
|
||||
function resetSettings() {
|
||||
if (confirm('确定要重置所有设置吗?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新预览
|
||||
function updateSettingsPreview(settings) {
|
||||
const logoPreview = document.getElementById('site_logo_preview');
|
||||
if (logoPreview) {
|
||||
if (settings.site_logo) {
|
||||
logoPreview.src = settings.site_logo;
|
||||
logoPreview.style.display = '';
|
||||
} else {
|
||||
logoPreview.src = '';
|
||||
logoPreview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const faviconPreview = document.getElementById('site_favicon_preview');
|
||||
if (faviconPreview) {
|
||||
if (settings.site_favicon) {
|
||||
faviconPreview.src = settings.site_favicon;
|
||||
faviconPreview.style.display = '';
|
||||
} else {
|
||||
faviconPreview.src = '';
|
||||
faviconPreview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息提示
|
||||
function showMessage(message, type = 'success') {
|
||||
// 创建提示元素
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
|
||||
type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'
|
||||
}`;
|
||||
messageEl.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'} mr-2"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(messageEl);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
messageEl.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 页面加载时获取设置
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 设置已经在页面加载时通过PHP变量传递,无需额外请求
|
||||
});
|
||||
</script>
|
||||
|
||||
Executable
+767
@@ -0,0 +1,767 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fa fa-users text-primary mr-3"></i>
|
||||
用户管理中心
|
||||
</h1>
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<!-- 搜索和操作区 -->
|
||||
<div class="flex justify-between items-center mb-6 gap-4">
|
||||
<h2 class="text-xl font-semibold text-gray-700 whitespace-nowrap">用户列表</h2>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<!-- 新增用户按钮 -->
|
||||
<button id="openFormBtn" class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center whitespace-nowrap">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>新增用户</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户信息</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider ">角色</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">余额</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">注册时间</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="userList">
|
||||
<?php if (!empty($users) && is_array($users)): ?>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors" data-id="<?php echo $user['id']; ?>">
|
||||
<!-- 用户名和头像单元格 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="<?php
|
||||
if (!empty($user['avatar'])) {
|
||||
echo htmlspecialchars($user['avatar']);
|
||||
} else {
|
||||
// 管理员与普通用户使用不同CDN头像
|
||||
echo $user['role'] === 'admin'
|
||||
? "https://robohash.org/admin" . $user['id'] . "?size=40x40"
|
||||
: "https://robohash.org/user" . $user['id'] . "?size=40x40";
|
||||
}
|
||||
?>"
|
||||
alt="用户头像" class="w-10 h-10 rounded-full object-cover border border-gray-200 flex-shrink-0">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 truncate"><?php echo htmlspecialchars($user['username']); ?></div>
|
||||
<div class="text-xs text-gray-500 truncate"><?php echo htmlspecialchars($user['email']); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 角色单元格 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<?php
|
||||
if ($user['role'] === 'admin') {
|
||||
$roleClass = 'bg-red-100 text-red-800';
|
||||
$roleText = '管理员';
|
||||
} else {
|
||||
$roleClass = 'bg-blue-100 text-blue-800';
|
||||
$roleText = '平台用户';
|
||||
}
|
||||
?>
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full <?php echo $roleClass; ?>">
|
||||
<?php echo $roleText; ?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 余额单元格 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-semibold text-green-600">
|
||||
<?php echo number_format($user['balance'] ?? 0, 2, ',', '.'); ?>
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<button class="balance-increase-btn text-green-500 hover:text-green-700 text-xs"
|
||||
data-id="<?php echo $user['id']; ?>"
|
||||
data-username="<?php echo htmlspecialchars($user['username']); ?>"
|
||||
title="增加余额">
|
||||
<i class="fa fa-plus-circle"></i>
|
||||
</button>
|
||||
<button class="balance-decrease-btn text-red-500 hover:text-red-700 text-xs"
|
||||
data-id="<?php echo $user['id']; ?>"
|
||||
data-username="<?php echo htmlspecialchars($user['username']); ?>"
|
||||
title="减少余额">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 创建时间 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap hidden sm:table-cell">
|
||||
<div class="text-sm text-gray-500"><?php echo date('Y-m-d H:i', strtotime($user['created_at'])); ?></div>
|
||||
</td>
|
||||
|
||||
<!-- 状态 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<?php
|
||||
$statusClass = $user['status'] ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
$statusText = $user['status'] ? '启用' : '停用';
|
||||
?>
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full <?php echo $statusClass; ?>">
|
||||
<?php echo $statusText; ?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 操作按钮 - 自适应宽度 -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button class="view-btn text-gray-500 hover:text-purple-500"
|
||||
data-id="<?php echo $user['id']; ?>" title="查看详情">
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
<button class="edit-btn text-gray-500 hover:text-blue-500"
|
||||
data-id="<?php echo $user['id']; ?>" title="编辑">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button class="delete-btn text-gray-500 hover:text-red-500"
|
||||
data-id="<?php echo $user['id']; ?>" title="删除">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fa fa-users text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">没有找到用户</h3>
|
||||
<p class="mt-1 text-gray-500">尝试调整筛选条件或添加新用户</p>
|
||||
<button class="mt-4 bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center"
|
||||
onclick="openFormModal()">
|
||||
<i class="fa fa-plus mr-2"></i>
|
||||
<span>添加新用户</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="flex justify-between items-center mt-6">
|
||||
<p class="text-sm text-gray-500">显示 1 至 <?php echo min(10, count($users ?? [])); ?> 条,共 <?php echo count($users ?? []); ?> 条</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户表单弹窗背景 -->
|
||||
<div id="formBackdrop" class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div>
|
||||
|
||||
<!-- 用户表单弹窗 -->
|
||||
<div id="formModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-events-none pointer-none transition transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 id="formTitle" class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-plus-circle text-primary mr-2"></i>
|
||||
创建新用户
|
||||
</h3>
|
||||
<button id="closeFormBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="userForm" class="space-y-5">
|
||||
<input type="hidden" id="userId" name="id">
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">用户名 <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入用户名">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">邮箱 <span class="text-red-500">*</span></label>
|
||||
<input type="email" id="email" name="email" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入邮箱地址">
|
||||
</div>
|
||||
|
||||
<!-- 用户表单表单弹窗中密码字段部分修改 -->
|
||||
<div id="passwordField">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
密码 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
||||
placeholder="请输入密码">
|
||||
<p class="mt-1 text-xs text-gray-500">密码长度至少8位,包含字母和数字</p>
|
||||
</div>
|
||||
|
||||
<!-- 用户角色固定为普通用户 -->
|
||||
<input type="hidden" id="role" name="role" value="user">
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="status" name="status" value="1" checked
|
||||
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-700">启用用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||
<button id="cancelBtn" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="submitBtn" type="button"
|
||||
class="bg-primary hover:bg-primary/90 text-white px-5 py-2 rounded-lg shadow hover:shadow-md transition-all duration-200">
|
||||
保存用户
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户详情弹窗 -->
|
||||
<div id="detailModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-user-circle text-primary mr-2"></i>
|
||||
用户详情
|
||||
</h3>
|
||||
<button id="closeDetailBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-100px)]">
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<img id="detailAvatar" src="https://picsum.photos/seed/user/100/100" alt="用户头像" class="w-24 h-24 rounded-full mb-4">
|
||||
<h4 id="detailUsername" class="text-xl font-bold text-gray-800">用户名</h4>
|
||||
<p id="detailRole" class="mt-1 px-3 py-1 text-sm rounded-full bg-green-100 text-green-800">普通用户</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">ID</span>
|
||||
<span id="detailId" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">余额</span>
|
||||
<span id="detailBalance" class="col-span-2 text-green-600 font-semibold">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">邮箱</span>
|
||||
<span id="detailEmail" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">状态</span>
|
||||
<span id="detailStatus" class="col-span-2">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">启用</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">创建时间</span>
|
||||
<span id="detailCreatedAt" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
<div class="w-full h-px bg-gray-100"></div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 items-center">
|
||||
<span class="text-sm text-gray-500">最后登录</span>
|
||||
<span id="detailLastLogin" class="col-span-2 text-gray-800">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 px-6 py-4 flex justify-end">
|
||||
<button id="closeDetailBtn2" class="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
console.log('用户管理JS加载完成');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 缓存DOM元素
|
||||
const formModal = document.getElementById('formModal');
|
||||
const formBackdrop = document.getElementById('formBackdrop');
|
||||
const detailModal = document.getElementById('detailModal');
|
||||
const openFormBtn = document.getElementById('openFormBtn');
|
||||
const closeFormBtn = document.getElementById('closeFormBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const formTitle = document.getElementById('formTitle');
|
||||
const userForm = document.getElementById('userForm');
|
||||
const userList = document.getElementById('userList');
|
||||
const passwordField = document.getElementById('passwordField');
|
||||
const closeDetailBtn = document.getElementById('closeDetailBtn');
|
||||
const closeDetailBtn2 = document.getElementById('closeDetailBtn2');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
|
||||
// 检查元素是否存在
|
||||
function checkElements() {
|
||||
const elements = [
|
||||
formModal, formBackdrop, openFormBtn,
|
||||
closeFormBtn, cancelBtn, submitBtn
|
||||
];
|
||||
|
||||
const missing = elements.filter(el => !el);
|
||||
if (missing.length > 0) {
|
||||
console.error('缺少必要的DOM元素,功能无法正常工作');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 显示表单弹窗
|
||||
function openFormModal() {
|
||||
if (!checkElements()) return;
|
||||
|
||||
resetForm();
|
||||
formModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
void formModal.offsetWidth; // 强制重绘
|
||||
}
|
||||
|
||||
// 隐藏表单弹窗
|
||||
function closeFormModal() {
|
||||
if (!checkElements()) return;
|
||||
|
||||
formModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
formModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 显示详情弹窗
|
||||
function openDetailModal() {
|
||||
detailModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
detailModal.classList.add('scale-100');
|
||||
formBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = 'hidden';
|
||||
void detailModal.offsetWidth;
|
||||
}
|
||||
|
||||
// 隐藏详情弹窗
|
||||
function closeDetailModal() {
|
||||
detailModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
detailModal.classList.remove('scale-100');
|
||||
formBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 重置表单(新增模式)
|
||||
function resetForm() {
|
||||
userForm.reset();
|
||||
document.getElementById('userId').value = '';
|
||||
formTitle.innerHTML = '<i class="fa fa-plus-circle text-primary mr-2"></i> 创建新用户';
|
||||
|
||||
// 新增模式:密码必填设置
|
||||
const passwordLabel = document.querySelector('#passwordField label');
|
||||
const passwordInput = document.getElementById('password');
|
||||
passwordLabel.innerHTML = '密码 <span class="text-red-500">*</span>';
|
||||
passwordInput.required = true;
|
||||
passwordInput.placeholder = '请输入密码';
|
||||
passwordField.style.display = 'block';
|
||||
|
||||
submitBtn.innerHTML = '保存用户';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 加载用户数据(编辑模式)
|
||||
async function loadUserData(id) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/users/${id}`);
|
||||
if (!response.ok) throw new Error('获取数据失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { id, username, email, status } = data.data;
|
||||
document.getElementById('userId').value = id;
|
||||
document.getElementById('username').value = username || '';
|
||||
document.getElementById('email').value = email || '';
|
||||
// 角色固定为普通用户,无需设置
|
||||
document.getElementById('status').checked = status == 1;
|
||||
formTitle.innerHTML = '<i class="fa fa-pencil text-primary mr-2"></i> 编辑用户';
|
||||
|
||||
// 编辑模式:密码可选设置
|
||||
const passwordLabel = document.querySelector('#passwordField label');
|
||||
const passwordInput = document.getElementById('password');
|
||||
passwordLabel.innerHTML = '密码(不填则不修改)';
|
||||
passwordInput.required = false;
|
||||
passwordInput.placeholder = '不修改密码请留空';
|
||||
passwordField.style.display = 'block';
|
||||
} else {
|
||||
throw new Error(data.message || '获取数据失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
closeFormModal();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存用户';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户详情
|
||||
async function loadUserDetail(id) {
|
||||
try {
|
||||
const response = await fetch(`/admin/users/${id}`);
|
||||
if (!response.ok) throw new Error('获取详情失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { id, username, email, role, status, created_at, last_login, avatar, balance } = data.data;
|
||||
|
||||
// 填充详情数据
|
||||
document.getElementById('detailId').textContent = id;
|
||||
document.getElementById('detailUsername').textContent = username || '未知用户';
|
||||
document.getElementById('detailEmail').textContent = email || '未设置';
|
||||
// 格式化余额为越南盾格式(点号分隔千位,逗号分隔小数)
|
||||
var balanceValue = balance !== undefined ? parseFloat(balance) : 0;
|
||||
var parts = balanceValue.toFixed(2).split('.');
|
||||
var integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
var formattedBalance = integerPart + ',' + parts[1];
|
||||
document.getElementById('detailBalance').textContent = formattedBalance;
|
||||
document.getElementById('detailCreatedAt').textContent = created_at ? new Date(created_at).toLocaleString() : '未知';
|
||||
document.getElementById('detailLastLogin').textContent = last_login ? new Date(last_login).toLocaleString() : '从未登录';
|
||||
document.getElementById('detailAvatar').src = avatar || `https://picsum.photos/seed/user${id}/100/100`;
|
||||
|
||||
// 设置角色标签样式
|
||||
let roleClass = 'bg-green-100 text-green-800';
|
||||
let roleText = '平台用户';
|
||||
if (role === 'admin') {
|
||||
roleClass = 'bg-red-100 text-red-800';
|
||||
roleText = '管理员';
|
||||
}
|
||||
document.getElementById('detailRole').className = `mt-1 px-3 py-1 text-sm rounded-full ${roleClass}`;
|
||||
document.getElementById('detailRole').textContent = roleText;
|
||||
|
||||
// 设置状态标签样式
|
||||
const statusClass = status ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
const statusText = status ? '启用' : '禁用';
|
||||
document.getElementById('detailStatus').innerHTML =
|
||||
`<span class="inline-block px-2 py-1 text-xs rounded-full ${statusClass}">${statusText}</span>`;
|
||||
|
||||
openDetailModal();
|
||||
} else {
|
||||
throw new Error(data.message || '获取详情失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm() {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const isEditMode = !!document.getElementById('userId').value;
|
||||
|
||||
if (!username) {
|
||||
showMessage('请输入用户名', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
showMessage('请输入邮箱地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
showMessage('请输入有效的邮箱地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 仅在新增或编辑时填写了密码的情况下验证长度
|
||||
if ((!isEditMode || password) && password.length < 8) {
|
||||
showMessage('密码长度至少8位', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 提交表单(创建/更新)
|
||||
async function submitFormData() {
|
||||
if (!validateForm()) return;
|
||||
|
||||
const formData = new FormData(userForm);
|
||||
const isEditMode = !!document.getElementById('userId').value;
|
||||
const statusCheckbox = document.getElementById('status');
|
||||
|
||||
formData.delete('status'); // 先除可能存在的旧值
|
||||
formData.append('status', statusCheckbox.checked ? '1' : '0');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/users/update', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage(isEditMode ? '用户更新成功' : '用户创建成功');
|
||||
closeFormModal();
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
throw new Error(data.message || (isEditMode ? '更新失败' : '创建失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '保存用户';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
async function deleteUser(id) {
|
||||
if (!confirm('确定要删除该用户吗?此操作不可恢复!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/users/delete/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage('用户已删除');
|
||||
// 移除DOM元素
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
// 检查是否还有数据行
|
||||
const rows = userList.querySelectorAll('tr:not(:last-child)');
|
||||
if (rows.length === 0) {
|
||||
userList.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-10 text-center text-gray-500 border border-dashed border-gray-200">
|
||||
<div>
|
||||
<i class="fa fa-info-circle text-2xl mb-2 text-gray-300"></i>
|
||||
<p>暂无用户数据</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 绑定事件
|
||||
if (checkElements()) {
|
||||
// 打开表单
|
||||
openFormBtn.addEventListener('click', openFormModal);
|
||||
|
||||
// 关闭表单
|
||||
closeFormBtn.addEventListener('click', closeFormModal);
|
||||
cancelBtn.addEventListener('click', closeFormModal);
|
||||
formBackdrop.addEventListener('click', () => {
|
||||
if (!formModal.classList.contains('invisible')) closeFormModal();
|
||||
if (!detailModal.classList.contains('invisible')) closeDetailModal();
|
||||
});
|
||||
|
||||
// 关闭详情
|
||||
closeDetailBtn.addEventListener('click', closeDetailModal);
|
||||
closeDetailBtn2.addEventListener('click', closeDetailModal);
|
||||
|
||||
// 提交表单
|
||||
submitBtn.addEventListener('click', submitFormData);
|
||||
|
||||
|
||||
|
||||
// 编辑、删除、查看、余额操作事件委托
|
||||
userList.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('.edit-btn');
|
||||
const deleteBtn = e.target.closest('.delete-btn');
|
||||
const viewBtn = e.target.closest('.view-btn');
|
||||
const increaseBtn = e.target.closest('.balance-increase-btn');
|
||||
const decreaseBtn = e.target.closest('.balance-decrease-btn');
|
||||
|
||||
if (editBtn) {
|
||||
const id = editBtn.getAttribute('data-id');
|
||||
if (id) {
|
||||
openFormModal();
|
||||
setTimeout(() => loadUserData(id), 300);
|
||||
}
|
||||
} else if (deleteBtn) {
|
||||
const id = deleteBtn.getAttribute('data-id');
|
||||
if (id) deleteUser(id);
|
||||
} else if (viewBtn) {
|
||||
const id = viewBtn.getAttribute('data-id');
|
||||
if (id) loadUserDetail(id);
|
||||
} else if (increaseBtn) {
|
||||
const id = increaseBtn.getAttribute('data-id');
|
||||
const username = increaseBtn.getAttribute('data-username');
|
||||
if (id) adjustBalance(id, username, 'increase');
|
||||
} else if (decreaseBtn) {
|
||||
const id = decreaseBtn.getAttribute('data-id');
|
||||
const username = decreaseBtn.getAttribute('data-username');
|
||||
if (id) adjustBalance(id, username, 'decrease');
|
||||
}
|
||||
});
|
||||
|
||||
// 余额调整功能
|
||||
function adjustBalance(userId, username, action) {
|
||||
const actionText = action === 'increase' ? '增加' : '减少';
|
||||
const actionColor = action === 'increase' ? '#5FB878' : '#FF5722';
|
||||
|
||||
// 使用 layui 的 prompt 弹窗
|
||||
layui.use('layer', function(){
|
||||
var layer = layui.layer;
|
||||
|
||||
layer.prompt({
|
||||
formType: 0, // 0=文本输入框
|
||||
value: '',
|
||||
title: actionText + '余额',
|
||||
area: ['400px', 'auto'],
|
||||
btn: ['确定', '取消'],
|
||||
btnAlign: 'c',
|
||||
yes: function(index, layero){
|
||||
var amountInput = layero.find('input');
|
||||
var amount = amountInput.val();
|
||||
|
||||
if (!amount || amount.trim() === '' || isNaN(amount) || parseFloat(amount) <= 0) {
|
||||
layer.msg('请输入有效的金额', {icon: 2, time: 2000});
|
||||
return;
|
||||
}
|
||||
|
||||
var amountValue = parseFloat(amount);
|
||||
|
||||
// 格式化金额为越南盾格式(点号分隔千位,逗号分隔小数)
|
||||
var parts = amountValue.toFixed(2).split('.');
|
||||
var integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
var formattedAmount = integerPart + ',' + parts[1];
|
||||
|
||||
// 关闭输入弹窗
|
||||
layer.close(index);
|
||||
|
||||
// 使用 layui 的 confirm 确认弹窗
|
||||
layer.confirm(
|
||||
'确定要' + actionText + '用户 <span style="color: #1890ff; font-weight: bold;">"' + username + '"</span> 的余额 <span style="color: ' + actionColor + '; font-weight: bold;">' + formattedAmount + '</span> 吗?',
|
||||
{
|
||||
icon: 3,
|
||||
title: '确认操作',
|
||||
btn: ['确定', '取消'],
|
||||
btnAlign: 'c',
|
||||
area: ['450px', 'auto']
|
||||
},
|
||||
function(confirmIndex){
|
||||
// 执行余额调整
|
||||
executeBalanceAdjust(userId, username, action, amountValue, actionText, layer, confirmIndex);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 执行余额调整请求
|
||||
async function executeBalanceAdjust(userId, username, action, amount, actionText, layer, confirmIndex) {
|
||||
// 关闭确认弹窗
|
||||
layer.close(confirmIndex);
|
||||
|
||||
// 显示加载提示
|
||||
var loadIndex = layer.load(1, {
|
||||
content: '正在处理...',
|
||||
shade: [0.3, '#000']
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/users/balance/${userId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: amount,
|
||||
action: action
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 关闭加载提示
|
||||
layer.close(loadIndex);
|
||||
|
||||
if (data.success) {
|
||||
layer.msg('余额' + actionText + '成功!', {
|
||||
icon: 1,
|
||||
time: 2000,
|
||||
shade: 0.3
|
||||
}, function(){
|
||||
// 刷新页面以更新余额显示
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(data.message || actionText + '失败', {
|
||||
icon: 2,
|
||||
time: 3000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.close(loadIndex);
|
||||
layer.msg('操作失败:' + e.message, {
|
||||
icon: 2,
|
||||
time: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') {
|
||||
if (!formModal.classList.contains('invisible')) closeFormModal();
|
||||
if (!detailModal.classList.contains('invisible')) closeDetailModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止表单默认提交
|
||||
userForm.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
submitFormData();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">👻 虚拟账户</h2>
|
||||
<button onclick="document.getElementById('addModal').classList.remove('hidden')" class="mb-4 px-4 py-2 bg-blue-500 text-white rounded text-sm">+ 创建虚拟账户</button>
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left">用户名</th><th class="px-4 py-3">余额</th><th class="px-4 py-3">创建时间</th><th class="px-4 py-3">操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($virtuals??[] as $v): ?>
|
||||
<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-4 py-2"><?=htmlspecialchars($v['username'])?> <span class="text-xs text-purple-500">虚拟</span></td>
|
||||
<td class="px-4 py-2 text-center font-mono"><?=number_format($v['balance'],2)?></td>
|
||||
<td class="px-4 py-2 text-xs text-gray-400"><?=$v['created_at']??''?></td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<button onclick="adjustBal(<?=$v['id']?>,'<?=htmlspecialchars($v['username'])?>')" class="text-blue-500 text-xs">±余额</button>
|
||||
<button onclick="delVirt(<?=$v['id']?>)" class="text-red-500 text-xs ml-1">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table></div>
|
||||
|
||||
<div id="addModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl p-6 w-full max-w-sm">
|
||||
<h3 class="font-bold mb-4">创建虚拟账户</h3>
|
||||
<div class="space-y-3">
|
||||
<input id="vUser" placeholder="用户名" class="w-full border rounded px-3 py-2">
|
||||
<input id="vPass" placeholder="密码 (默认: 123456)" class="w-full border rounded px-3 py-2">
|
||||
<input id="vBal" type="number" placeholder="初始余额" value="100000" class="w-full border rounded px-3 py-2">
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button onclick="createVirt()" class="flex-1 py-2 bg-blue-500 text-white rounded">创建</button>
|
||||
<button onclick="document.getElementById('addModal').classList.add('hidden')" class="flex-1 py-2 bg-gray-200 rounded">取消</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
<script>
|
||||
async function createVirt(){const r=await fetch('/admin/virtual/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('vUser').value,password:document.getElementById('vPass').value||'123456',balance:parseFloat(document.getElementById('vBal').value)})});const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);}
|
||||
async function adjustBal(id,name){const a=prompt('调整 '+name+' 的余额(正数=增加,负数=减少):');if(!a)return;const r=await fetch('/admin/virtual/balance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,amount:parseFloat(a)})});const d=await r.json();if(d.status==='success')location.reload();else alert(d.message);}
|
||||
async function delVirt(id){if(!confirm('确定删除?'))return;await fetch('/admin/virtual/delete/'+id,{method:'POST'});location.reload();}
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-2">🌊 放水控制 & 投注限额</h2>
|
||||
<p class="text-gray-500 text-sm mb-6">放水 = 控制玩家胜率。百分比越低,平台赢得越多。设为50%表示公平对赌。</p>
|
||||
|
||||
<?php
|
||||
// 投注类型中文映射
|
||||
$betTypeLabels = [
|
||||
// PK10
|
||||
'rank' => '🏎️ 名次(猜第N名是几号车)',
|
||||
'bs' => '🔢 大小(名次车号 ≥6大 ≤5小)',
|
||||
'oe' => '🎯 单双(名次车号的奇偶)',
|
||||
'dt' => '🐉 龙虎(前名次 vs 后名次比大小)',
|
||||
'sum' => '➕ 冠亚和值(冠军+亚军车号之和)',
|
||||
'sum_bs' => '📊 冠亚和大小(和值≥12大 ≤11小)',
|
||||
// 骰子
|
||||
'tai' => '🎲 大(总点数 ≥11)',
|
||||
'xiu' => '🎲 小(总点数 ≤10)',
|
||||
'chan' => '🎲 双(总点数为偶数)',
|
||||
'le' => '🎲 单(总点数为奇数)',
|
||||
'number' => '🔢 押点数(猜总和具体数字)',
|
||||
'dice' => '🎯 单骰(猜某颗骰子的点数)',
|
||||
'combo' => '💎 豹子(三颗相同)',
|
||||
// Xóc Đĩa
|
||||
'even' => '⚪ 偶数红',
|
||||
'odd' => '🔴 奇数红',
|
||||
'4red' => '🔴🔴🔴🔴 四红',
|
||||
'4white' => '⚪⚪⚪⚪ 四白',
|
||||
'big_small' => '🎲 大小',
|
||||
'odd_even' => '🎲 单双',
|
||||
];
|
||||
|
||||
function getBetLabel($type, $labels) {
|
||||
return $labels[$type] ?? $type;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 放水配置 -->
|
||||
<div class="bg-white rounded-xl p-6 shadow">
|
||||
<h3 class="font-bold mb-2">胜率控制</h3>
|
||||
<p class="text-gray-400 text-xs mb-4">数值含义:玩家赢的概率百分比。例如 40% = 玩家有40%概率赢,平台抽成约60%。</p>
|
||||
<div class="space-y-3" id="waterList">
|
||||
<?php foreach($configs??[] as $c): ?>
|
||||
<div class="flex items-center gap-3 py-2 border-b">
|
||||
<span class="flex-1 text-sm" title="<?=htmlspecialchars($c['bet_type'])?>"><?=getBetLabel($c['bet_type'], $betTypeLabels)?></span>
|
||||
<input type="number" step="0.1" min="0" max="100" value="<?=$c['win_rate_pct']?>" class="w-20 border rounded px-2 py-1 text-sm text-center water-pct" data-type="<?=$c['bet_type']?>" data-game="<?=$c['game_id']?>">
|
||||
<span class="text-xs text-gray-400">%</span>
|
||||
<label class="flex items-center gap-1"><input type="checkbox" class="water-enabled" data-type="<?=$c['bet_type']?>" <?=$c['enabled']?'checked':''?>><span class="text-xs">启用</span></label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($configs)): ?>
|
||||
<div class="text-gray-400 text-sm text-center py-4">暂无放水配置,请先在「游戏与赔率」中配置游戏赔率</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button onclick="saveWater()" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">💾 保存放水配置</button>
|
||||
</div>
|
||||
|
||||
<!-- 限额配置 -->
|
||||
<div class="bg-white rounded-xl p-6 shadow">
|
||||
<h3 class="font-bold mb-2">投注限额</h3>
|
||||
<p class="text-gray-400 text-xs mb-4">控制每种玩法的单笔最小/最大金额,以及每期总投注上限。</p>
|
||||
<div class="space-y-3" id="limitList">
|
||||
<?php foreach($limits??[] as $l): ?>
|
||||
<div class="flex items-center gap-2 py-2 border-b flex-wrap">
|
||||
<span class="w-full text-sm mb-1 font-medium" title="<?=htmlspecialchars($l['bet_type'])?>"><?=getBetLabel($l['bet_type'], $betTypeLabels)?></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-gray-400">单笔最小:</span>
|
||||
<input type="number" value="<?=$l['min_amount']?>" class="w-24 border rounded px-2 py-1 text-sm text-center limit-min" data-type="<?=$l['bet_type']?>" data-game="<?=$l['game_id']?>">
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-gray-400">单笔最大:</span>
|
||||
<input type="number" value="<?=$l['max_amount']?>" class="w-24 border rounded px-2 py-1 text-sm text-center limit-max" data-type="<?=$l['bet_type']?>">
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-gray-400">每期上限:</span>
|
||||
<input type="number" value="<?=$l['max_per_period']?>" class="w-24 border rounded px-2 py-1 text-sm text-center limit-period" data-type="<?=$l['bet_type']?>">
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($limits)): ?>
|
||||
<div class="text-gray-400 text-sm text-center py-4">暂无限额配置</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button onclick="saveLimits()" class="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm">💾 保存限额</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function saveWater(){
|
||||
const items=[];
|
||||
document.querySelectorAll('.water-pct').forEach(el=>{
|
||||
items.push({game_id:el.dataset.game,bet_type:el.dataset.type,win_rate_pct:parseFloat(el.value),
|
||||
enabled:document.querySelector('.water-enabled[data-type="'+el.dataset.type+'"]').checked?1:0});
|
||||
});
|
||||
const r=await fetch('/admin/water/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items})});
|
||||
const d=await r.json();alert(d.status==='success'?'保存成功!':d.message);
|
||||
}
|
||||
async function saveLimits(){
|
||||
const items=[];
|
||||
document.querySelectorAll('.limit-min').forEach(el=>{
|
||||
const t=el.dataset.type;
|
||||
items.push({game_id:el.dataset.game,bet_type:t,min_amount:parseFloat(el.value),
|
||||
max_amount:parseFloat(document.querySelector('.limit-max[data-type="'+t+'"]').value),
|
||||
max_per_period:parseFloat(document.querySelector('.limit-period[data-type="'+t+'"]').value)});
|
||||
});
|
||||
const r=await fetch('/admin/water/limits',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items})});
|
||||
const d=await r.json();alert(d.status==='success'?'保存成功!':d.message);
|
||||
}
|
||||
</script>
|
||||
Executable
+783
@@ -0,0 +1,783 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-coins text-primary mr-3"></i>
|
||||
Xóc Đĩa 游戏期号管理
|
||||
</h1>
|
||||
|
||||
<!-- 游戏期号管理区域 -->
|
||||
<?php if (!empty($gamesList) && is_array($gamesList)): ?>
|
||||
<?php foreach ($gamesList as $game): ?>
|
||||
<?php
|
||||
$gameId = $game['id'];
|
||||
$gameName = $game['name'];
|
||||
$currentPeriod = isset($currentPeriods[$gameId]) ? $currentPeriods[$gameId] : null;
|
||||
?>
|
||||
|
||||
<!-- 单个游戏的期号卡片 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6 border-l-4 border-primary">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
|
||||
<i class="fas fa-gamepad text-primary mr-2"></i>
|
||||
<?= htmlspecialchars($gameName) ?>
|
||||
</h2>
|
||||
|
||||
<?php if ($currentPeriod): ?>
|
||||
<!-- 有当前期号 -->
|
||||
<div class="flex gap-2">
|
||||
<?php if ($currentPeriod['status'] === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-lock-btn inline-block bg-warning hover:bg-warning/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-lock mr-2"></i>封盘
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'locked'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn inline-block bg-success hover:bg-success/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-coins mr-2"></i>开奖
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'drawn'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-settle-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-id="<?= $currentPeriod['id'] ?>"
|
||||
>
|
||||
<i class="fas fa-coins mr-2"></i>结算
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($currentPeriod['status'] === 'settled'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-game-id="<?= $gameId ?>"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>开始新一期
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-start-btn inline-block bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg text-sm"
|
||||
data-game-id="<?= $gameId ?>"
|
||||
>
|
||||
<i class="fas fa-play mr-2"></i>开始新一期
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($currentPeriod): ?>
|
||||
<!-- 当前期号信息 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">期号</p>
|
||||
<p class="text-lg font-bold text-gray-900"><?= htmlspecialchars($currentPeriod['period_number']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">状态</p>
|
||||
<?php
|
||||
$status = $currentPeriod['status'];
|
||||
$statusMap = [
|
||||
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
|
||||
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
|
||||
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
|
||||
];
|
||||
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm <?= $statusInfo['class'] ?>">
|
||||
<?= $statusInfo['text'] ?>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">开始时间</p>
|
||||
<p class="text-sm text-gray-900"><?= htmlspecialchars($currentPeriod['start_time'] ?? '-') ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">开奖结果</p>
|
||||
<?php if (!empty($currentPeriod['result'])): ?>
|
||||
<?php
|
||||
$coins = json_decode($currentPeriod['result'], true);
|
||||
if (is_array($coins) && count($coins) === 4):
|
||||
$redCount = $currentPeriod['dice1'] ?? 0;
|
||||
$whiteCount = $currentPeriod['dice2'] ?? 0;
|
||||
?>
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="font-semibold">
|
||||
<?php foreach ($coins as $coin): ?>
|
||||
<span class="inline-block w-5 h-5 rounded-full <?= $coin === 'red' ? 'bg-red-500' : 'bg-gray-200' ?> border border-gray-300 mr-1"></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<span class="text-gray-600 ml-2">
|
||||
(<?= $redCount ?>Đ <?= $whiteCount ?>T)
|
||||
</span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="text-sm text-gray-400">数据格式错误</p>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<p class="text-sm text-gray-400">未开奖</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-gray-500">暂无进行中的期号,点击"开始新一期"按钮启动</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">期号总数</p>
|
||||
<h3 class="text-2xl font-bold mt-1">
|
||||
<?= isset($periods) && is_array($periods) ? count($periods) : 0 ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-list text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">待开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-warning">
|
||||
<?php
|
||||
$pendingCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && $p['status'] === 'pending') {
|
||||
$pendingCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $pendingCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
|
||||
<i class="fas fa-clock text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已开奖</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-success">
|
||||
<?php
|
||||
$drawnCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && ($p['status'] === 'drawn' || $p['status'] === 'settled')) {
|
||||
$drawnCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $drawnCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">已结算</p>
|
||||
<h3 class="text-2xl font-bold mt-1 text-primary">
|
||||
<?php
|
||||
$settledCount = 0;
|
||||
if (isset($periods) && is_array($periods)) {
|
||||
foreach ($periods as $p) {
|
||||
if (isset($p['status']) && $p['status'] === 'settled') {
|
||||
$settledCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $settledCount;
|
||||
?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i class="fas fa-coins text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 期号列表 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">期号列表</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">管理Xóc Đĩa 游戏期号和开奖结果</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full bg-white rounded-xl overflow-hidden">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">关联游戏</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开奖结果</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">创建时间</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" id="periodList">
|
||||
<?php if (!empty($periods) && is_array($periods)): ?>
|
||||
<?php foreach ($periods as $period): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors" data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>">
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
<?= htmlspecialchars((string)($period['period_number'] ?? '')) ?>
|
||||
</div>
|
||||
<?php if (!empty($period['auto_generated'])): ?>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] bg-gray-100 text-gray-600 mt-1">
|
||||
自动生成
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$gameId = $period['game_id'] ?? null;
|
||||
$gameName = $gameId && isset($games[$gameId]) ? $games[$gameId] : '未关联';
|
||||
?>
|
||||
<span class="text-sm text-gray-600"><?= htmlspecialchars($gameName) ?></span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php
|
||||
$status = $period['status'] ?? 'pending';
|
||||
$statusMap = [
|
||||
'pending' => ['text' => '待开奖', 'class' => 'bg-warning/10 text-warning'],
|
||||
'locked' => ['text' => '已封盘', 'class' => 'bg-danger/10 text-danger'],
|
||||
'drawn' => ['text' => '已开奖', 'class' => 'bg-primary/10 text-primary'],
|
||||
'settled' => ['text' => '已结算', 'class' => 'bg-success/10 text-success']
|
||||
];
|
||||
$statusInfo = $statusMap[$status] ?? $statusMap['pending'];
|
||||
?>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
|
||||
<span class="w-2 h-2 rounded-full mr-1 <?= str_replace('/10', '', $statusInfo['class']) ?>"></span>
|
||||
<?= $statusInfo['text'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<?php if (!empty($period['result'])): ?>
|
||||
<?php
|
||||
$coins = json_decode($period['result'], true);
|
||||
if (is_array($coins) && count($coins) === 4):
|
||||
$redCount = $period['dice1'] ?? 0;
|
||||
$whiteCount = $period['dice2'] ?? 0;
|
||||
?>
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="font-semibold">
|
||||
<?php foreach ($coins as $coin): ?>
|
||||
<span class="inline-block w-5 h-5 rounded-full <?= $coin === 'red' ? 'bg-red-500' : 'bg-gray-200' ?> border border-gray-300 mr-1"></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<span class="text-gray-600 ml-2">
|
||||
(<?= $redCount ?>Đ <?= $whiteCount ?>T)
|
||||
</span>
|
||||
<span class="ml-1 px-2 py-0.5 rounded text-[11px] <?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700' ?>">
|
||||
<?= ($redCount == 0 || $redCount == 2 || $redCount == 4) ? 'Chẵn' : 'Lẻ' ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-sm text-gray-400">数据格式错误</span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-sm text-gray-400">未开奖</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 hidden md:table-cell">
|
||||
<?php if (!empty($period['created_at'])): ?>
|
||||
<div class="text-xs text-gray-500">
|
||||
<?= date('Y-m-d H:i', strtotime((string)$period['created_at'])) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-gray-400">时间未知</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<?php if (($period['status'] ?? '') === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-lock-btn text-gray-500 hover:text-warning"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="封盘"
|
||||
>
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (($period['status'] ?? '') === 'locked' || ($period['status'] ?? '') === 'pending'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn text-gray-500 hover:text-primary"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="录入开奖"
|
||||
>
|
||||
<i class="fas fa-coins"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (($period['status'] ?? '') === 'drawn'): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="period-draw-btn text-gray-500 hover:text-warning"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="修改结果"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="period-settle-btn text-gray-500 hover:text-success"
|
||||
data-id="<?= htmlspecialchars((string)($period['id'] ?? '')) ?>"
|
||||
title="确认结算"
|
||||
>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fas fa-coins text-gray-300 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900">暂无期号</h3>
|
||||
<p class="mt-1 text-gray-500 text-sm">
|
||||
当前还没有任何Xóc Đĩa 游戏期号。
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 录入开奖结果模态框 -->
|
||||
<div
|
||||
id="drawPeriodBackdrop"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 z-40"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="drawPeriodModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 invisible pointer-events-none transition-all duration-300 scale-95"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[90vh] overflow-hidden">
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fas fa-coins text-primary mr-2"></i>
|
||||
录入开奖结果
|
||||
</h3>
|
||||
<button id="closeDrawPeriodBtn" class="text-gray-400 hover:text-gray-600 transition-colors p-1">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5 overflow-y-auto max-h-[calc(90vh-130px)]">
|
||||
<form id="drawPeriodForm" class="space-y-4">
|
||||
<input type="hidden" id="drawPeriodId" name="id">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
期号
|
||||
</label>
|
||||
<p id="drawPeriodNumber" class="text-lg font-bold text-gray-900"></p>
|
||||
</div>
|
||||
|
||||
<!-- Xóc Đĩa 硬币输入 -->
|
||||
<div id="coinInputSection" class="space-y-3">
|
||||
<p class="text-sm text-gray-600">选择4个硬币的颜色:</p>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币1</label>
|
||||
<select id="coin1" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币2</label>
|
||||
<select id="coin2" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币3</label>
|
||||
<select id="coin3" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">硬币4</label>
|
||||
<select id="coin4" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white">
|
||||
<option value="red">红色</option>
|
||||
<option value="white">白色</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="drawResultPreview" class="hidden p-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-sm text-gray-600 mb-1">开奖结果预览:</p>
|
||||
<p class="text-lg font-bold">
|
||||
<span id="drawResultText"></span>
|
||||
<span id="drawResultTotal" class="ml-2 text-gray-500"></span>
|
||||
<span id="drawResultType" class="ml-2"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
id="submitDrawPeriodBtn"
|
||||
class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg shadow hover:shadow-md transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-check mr-2"></i>
|
||||
确认录入
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/Static/js/admin.js"></script>
|
||||
<script>
|
||||
layui.use(['layer'], function() {
|
||||
var layer = layui.layer;
|
||||
|
||||
// 获取元素
|
||||
const closeDrawPeriodBtn = document.getElementById('closeDrawPeriodBtn');
|
||||
const drawPeriodBackdrop = document.getElementById('drawPeriodBackdrop');
|
||||
const drawPeriodModal = document.getElementById('drawPeriodModal');
|
||||
const drawPeriodForm = document.getElementById('drawPeriodForm');
|
||||
const submitDrawPeriodBtn = document.getElementById('submitDrawPeriodBtn');
|
||||
const coin1 = document.getElementById('coin1');
|
||||
const coin2 = document.getElementById('coin2');
|
||||
const coin3 = document.getElementById('coin3');
|
||||
const coin4 = document.getElementById('coin4');
|
||||
const drawResultPreview = document.getElementById('drawResultPreview');
|
||||
const drawResultText = document.getElementById('drawResultText');
|
||||
const drawResultTotal = document.getElementById('drawResultTotal');
|
||||
const drawResultType = document.getElementById('drawResultType');
|
||||
|
||||
// 打开录入开奖模态框
|
||||
function openDrawPeriodModal(periodId) {
|
||||
fetch(`/admin/xocdia-periods/${periodId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const period = data.data;
|
||||
document.getElementById('drawPeriodId').value = period.id;
|
||||
document.getElementById('drawPeriodNumber').textContent = period.period_number;
|
||||
|
||||
// 填充已有的硬币数据
|
||||
if (period.result) {
|
||||
try {
|
||||
const coins = JSON.parse(period.result);
|
||||
if (Array.isArray(coins) && coins.length === 4) {
|
||||
coin1.value = coins[0];
|
||||
coin2.value = coins[1];
|
||||
coin3.value = coins[2];
|
||||
coin4.value = coins[3];
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
updateDrawPreview();
|
||||
|
||||
drawPeriodBackdrop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
drawPeriodModal.classList.remove('invisible', 'pointer-events-none', 'scale-95');
|
||||
drawPeriodModal.classList.add('scale-100');
|
||||
} else {
|
||||
layer.msg(data.message || '获取期号信息失败', {icon: 2});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
layer.msg('获取期号信息失败:' + e.message, {icon: 2});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭录入开奖模态框
|
||||
function closeDrawPeriodModal() {
|
||||
drawPeriodBackdrop.classList.add('opacity-0', 'pointer-events-none');
|
||||
drawPeriodModal.classList.add('invisible', 'pointer-events-none', 'scale-95');
|
||||
drawPeriodModal.classList.remove('scale-100');
|
||||
drawPeriodForm.reset();
|
||||
drawResultPreview.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 更新开奖结果预览
|
||||
function updateDrawPreview() {
|
||||
const coins = [coin1.value, coin2.value, coin3.value, coin4.value];
|
||||
const redCount = coins.filter(c => c === 'red').length;
|
||||
const whiteCount = 4 - redCount;
|
||||
|
||||
let result = '';
|
||||
let resultClass = '';
|
||||
|
||||
// 判断单双
|
||||
if (redCount === 0 || redCount === 2 || redCount === 4) {
|
||||
result = 'Chẵn';
|
||||
resultClass = 'bg-blue-100 text-blue-700 px-2 py-1 rounded text-sm';
|
||||
} else {
|
||||
result = 'Lẻ';
|
||||
resultClass = 'bg-red-100 text-red-700 px-2 py-1 rounded text-sm';
|
||||
}
|
||||
|
||||
// 显示预览
|
||||
let coinDisplay = coins.map(c =>
|
||||
`<span class="inline-block w-5 h-5 rounded-full ${c === 'red' ? 'bg-red-500' : 'bg-gray-200'} border border-gray-300 mr-1"></span>`
|
||||
).join('');
|
||||
|
||||
drawResultText.innerHTML = coinDisplay;
|
||||
drawResultTotal.textContent = `(${redCount}Đ ${whiteCount}T)`;
|
||||
drawResultType.innerHTML = `<span class="${resultClass}">${result}</span>`;
|
||||
drawResultPreview.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 提交录入开奖
|
||||
async function submitDrawPeriod() {
|
||||
const coins = [coin1.value, coin2.value, coin3.value, coin4.value];
|
||||
|
||||
const data = {
|
||||
id: document.getElementById('drawPeriodId').value,
|
||||
auto: false,
|
||||
coins: coins
|
||||
};
|
||||
|
||||
submitDrawPeriodBtn.disabled = true;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>录入中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/xocdia-periods/draw', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '开奖结果录入成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '录入失败', {icon: 2});
|
||||
submitDrawPeriodBtn.disabled = false;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('录入失败:' + e.message, {icon: 2});
|
||||
submitDrawPeriodBtn.disabled = false;
|
||||
submitDrawPeriodBtn.innerHTML = '<i class="fas fa-check mr-2"></i>确认录入';
|
||||
}
|
||||
}
|
||||
|
||||
// 封盘
|
||||
async function lockPeriod(id) {
|
||||
layer.confirm('确定要封盘吗?封盘后将无法继续投注。', {icon: 3, title: '确认封盘'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/xocdia-periods/lock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '封盘成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '封盘失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('封盘失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认结算
|
||||
async function settlePeriod(id) {
|
||||
layer.confirm('确定要确认结算吗?此操作不可撤销。', {icon: 3, title: '确认结算'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/xocdia-periods/settle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '结算成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '结算失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('结算失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 开始下注
|
||||
async function startPeriod(event) {
|
||||
const gameId = event.currentTarget.getAttribute('data-game-id');
|
||||
|
||||
if (!gameId) {
|
||||
layer.msg('游戏ID缺失', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm('确定要开始新一期下注吗?', {icon: 3, title: '开始下注'}, async function(index) {
|
||||
layer.close(index);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/xocdia-periods/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({game_id: parseInt(gameId)})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
layer.msg(result.message || '启动成功', {icon: 1}, function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.msg(result.message || '启动失败', {icon: 2});
|
||||
}
|
||||
} catch (e) {
|
||||
layer.msg('启动失败:' + e.message, {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
if (closeDrawPeriodBtn) {
|
||||
closeDrawPeriodBtn.addEventListener('click', closeDrawPeriodModal);
|
||||
}
|
||||
if (drawPeriodBackdrop) {
|
||||
drawPeriodBackdrop.addEventListener('click', closeDrawPeriodModal);
|
||||
}
|
||||
if (submitDrawPeriodBtn) {
|
||||
submitDrawPeriodBtn.addEventListener('click', submitDrawPeriod);
|
||||
}
|
||||
|
||||
// 硬币选择监听
|
||||
if (coin1 && coin2 && coin3 && coin4) {
|
||||
[coin1, coin2, coin3, coin4].forEach(select => {
|
||||
select.addEventListener('change', updateDrawPreview);
|
||||
});
|
||||
}
|
||||
|
||||
// 事件委托:列表操作按钮
|
||||
const periodList = document.getElementById('periodList');
|
||||
if (periodList) {
|
||||
periodList.addEventListener('click', function(e) {
|
||||
const lockBtn = e.target.closest('.period-lock-btn');
|
||||
const drawBtn = e.target.closest('.period-draw-btn');
|
||||
const settleBtn = e.target.closest('.period-settle-btn');
|
||||
|
||||
if (lockBtn) {
|
||||
const id = lockBtn.getAttribute('data-id');
|
||||
if (id) lockPeriod(id);
|
||||
}
|
||||
if (drawBtn) {
|
||||
const id = drawBtn.getAttribute('data-id');
|
||||
if (id) openDrawPeriodModal(id);
|
||||
}
|
||||
if (settleBtn) {
|
||||
const id = settleBtn.getAttribute('data-id');
|
||||
if (id) settlePeriod(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前期号操作按钮
|
||||
document.querySelectorAll('.period-lock-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) lockPeriod(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.period-draw-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) openDrawPeriodModal(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.period-settle-btn').forEach(btn => {
|
||||
if (!btn.closest('#periodList')) {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (id) settlePeriod(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start Button
|
||||
document.querySelectorAll('.period-start-btn').forEach(btn => {
|
||||
btn.addEventListener('click', startPeriod);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
<?php /** 底部5栏导航 */ $navActive = $navActive ?? ''; $t = $t ?? function($k){return $k;}; ?>
|
||||
<nav class="nav-bottom">
|
||||
<a href="/" class="nav-item <?=$navActive==='game'?'active':''?>">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></svg>
|
||||
<span><?=$t('nav_game')?></span>
|
||||
</a>
|
||||
<a href="/lottery" class="nav-item <?=$navActive==='lottery'?'active':''?>">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/></svg>
|
||||
<span><?=$t('nav_lottery')?></span>
|
||||
</a>
|
||||
<a href="/details" class="nav-item <?=$navActive==='details'?'active':''?>">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
<span><?=$t('nav_details')?></span>
|
||||
</a>
|
||||
<a href="#" class="nav-item <?=$navActive==='chat'?'active':''?>">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
<span><?=$t('nav_chat')?></span>
|
||||
</a>
|
||||
<a href="/profile" class="nav-item <?=$navActive==='profile'?'active':''?>">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span><?=$t('nav_profile')?></span>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php use App\Core\I18n; I18n::init(); $t = function($k,$p=[]){return I18n::t($k,$p);}; $page=$page??'home'; ?>
|
||||
<!DOCTYPE html><html lang="<?=I18n::getLang()?>">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>代理后台</title><script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>body{background:#0f172a;color:#fff;font-family:system-ui}.sidebar a.active{background:rgba(234,179,8,.15);color:#eab308;border-right:3px solid #eab308}</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex">
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar w-56 bg-slate-900 border-r border-white/5 min-h-screen hidden md:block shrink-0">
|
||||
<div class="p-4 border-b border-white/5">
|
||||
<div class="text-yellow-400 font-bold text-lg">🏎️ 代理后台</div>
|
||||
<div class="text-white/40 text-xs mt-1"><?=htmlspecialchars($user['username']??'')?></div>
|
||||
<div class="text-xs mt-1"><span class="text-white/30">邀请码:</span> <span class="text-yellow-400 font-mono"><?=htmlspecialchars($agent['agent_code']??'')?></span></div>
|
||||
</div>
|
||||
<nav class="p-2 space-y-1">
|
||||
<a href="/agent" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='home'?'active':''?>"><i class="fas fa-home w-5 text-center"></i>数据总览</a>
|
||||
<a href="/agent/odds" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='odds'?'active':''?>"><i class="fas fa-sliders-h w-5 text-center"></i>赔率设置</a>
|
||||
<a href="/agent/bets" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='bets'?'active':''?>"><i class="fas fa-list-alt w-5 text-center"></i>投注记录</a>
|
||||
<a href="/agent/commissions" class="flex items-center gap-3 px-3 py-2 rounded text-sm hover:bg-white/5 <?=$page==='commissions'?'active':''?>"><i class="fas fa-coins w-5 text-center"></i>佣金明细</a>
|
||||
<div class="border-t border-white/5 my-2"></div>
|
||||
<a href="/" class="flex items-center gap-3 px-3 py-2 rounded text-sm text-white/40 hover:bg-white/5"><i class="fas fa-gamepad w-5 text-center"></i>返回投注</a>
|
||||
<a href="/logout" class="flex items-center gap-3 px-3 py-2 rounded text-sm text-red-400/60 hover:bg-white/5"><i class="fas fa-sign-out-alt w-5 text-center"></i>退出</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 手机顶部导航 -->
|
||||
<div class="md:hidden fixed top-0 left-0 right-0 z-50 bg-slate-900 border-b border-white/5 px-4 py-2 flex items-center justify-between">
|
||||
<span class="text-yellow-400 font-bold">🏎️ 代理后台</span>
|
||||
<button onclick="document.getElementById('mobileMenu').classList.toggle('hidden')" class="text-white/60"><i class="fas fa-bars"></i></button>
|
||||
</div>
|
||||
<div id="mobileMenu" class="md:hidden fixed top-10 left-0 right-0 z-40 bg-slate-900 border-b border-white/5 hidden">
|
||||
<div class="flex flex-wrap gap-1 p-2">
|
||||
<a href="/agent" class="px-3 py-1 rounded text-xs <?=$page==='home'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">总览</a>
|
||||
<a href="/agent/odds" class="px-3 py-1 rounded text-xs <?=$page==='odds'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">赔率</a>
|
||||
<a href="/agent/bets" class="px-3 py-1 rounded text-xs <?=$page==='bets'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">投注</a>
|
||||
<a href="/agent/commissions" class="px-3 py-1 rounded text-xs <?=$page==='commissions'?'bg-yellow-500/20 text-yellow-400':'text-white/60'?>">佣金</a>
|
||||
<a href="/" class="px-3 py-1 rounded text-xs text-white/40">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="flex-1 p-4 md:p-6 mt-12 md:mt-0 overflow-auto">
|
||||
|
||||
<?php if($page==='home'): ?>
|
||||
<!-- ========== 数据总览 ========== -->
|
||||
<h2 class="text-xl font-bold mb-4">📊 数据总览</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<div class="bg-white/5 rounded-xl p-4 text-center">
|
||||
<div class="text-white/40 text-xs">玩家数</div>
|
||||
<div class="text-2xl font-bold mt-1"><?=count($players??[])?></div>
|
||||
</div>
|
||||
<div class="bg-white/5 rounded-xl p-4 text-center">
|
||||
<div class="text-white/40 text-xs">总投注额</div>
|
||||
<div class="text-yellow-400 font-bold text-lg mt-1"><?=number_format($totalBets??0,2)?></div>
|
||||
</div>
|
||||
<div class="bg-white/5 rounded-xl p-4 text-center">
|
||||
<div class="text-white/40 text-xs">今日佣金</div>
|
||||
<div class="text-green-400 font-bold text-lg mt-1"><?=number_format($todayComm,2)?></div>
|
||||
</div>
|
||||
<div class="bg-white/5 rounded-xl p-4 text-center">
|
||||
<div class="text-white/40 text-xs">累计佣金</div>
|
||||
<div class="text-green-400 font-bold text-lg mt-1"><?=number_format($totalComm,2)?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分享链接 -->
|
||||
<div class="bg-white/5 rounded-xl p-4 mb-4">
|
||||
<h3 class="text-sm font-bold mb-2">📎 邀请链接</h3>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" readonly value="<?=($_SERVER['REQUEST_SCHEME']??'http').'://'.$_SERVER['HTTP_HOST']?>/login?invite=<?=htmlspecialchars($agent['agent_code']??'')?>" class="flex-1 px-3 py-2 bg-white/5 border border-white/10 rounded text-xs text-white font-mono" id="shareLink">
|
||||
<button onclick="navigator.clipboard.writeText(document.getElementById('shareLink').value);this.textContent='已复制!';setTimeout(()=>this.textContent='复制',1500)" class="px-4 py-2 bg-yellow-500 text-black rounded text-xs font-bold hover:bg-yellow-400">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下级代理 -->
|
||||
<?php if(!empty($subAgents)): ?>
|
||||
<div class="bg-white/5 rounded-xl p-4 mb-4">
|
||||
<h3 class="text-sm font-bold mb-3">👥 下级代理</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-white/40 text-xs border-b border-white/5"><th class="text-left py-2 px-2">用户名</th><th class="px-2">邀请码</th><th class="px-2">佣金%</th><th class="px-2">玩家数</th><th class="px-2">状态</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($subAgents as $sa): ?>
|
||||
<tr class="border-b border-white/5 text-xs">
|
||||
<td class="py-2 px-2"><?=htmlspecialchars($sa['user']['username']??'')?></td>
|
||||
<td class="px-2 text-center font-mono text-yellow-400"><?=$sa['agent_code']?></td>
|
||||
<td class="px-2 text-center"><?=$sa['commission_rate']?>%</td>
|
||||
<td class="px-2 text-center"><?=$sa['player_count']?></td>
|
||||
<td class="px-2 text-center"><?=$sa['status']?'<span class="text-green-400">启用</span>':'<span class="text-red-400">禁用</span>'?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 玩家列表 -->
|
||||
<div class="bg-white/5 rounded-xl p-4">
|
||||
<h3 class="text-sm font-bold mb-3">🎮 我的玩家</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-white/40 text-xs border-b border-white/5"><th class="text-left py-2 px-2">ID</th><th class="text-left px-2">用户名</th><th class="px-2">余额</th><th class="px-2">注册时间</th><th class="px-2">状态</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($players??[] as $p): ?>
|
||||
<tr class="border-b border-white/5 text-xs">
|
||||
<td class="py-2 px-2 text-white/40"><?=$p['id']?></td>
|
||||
<td class="px-2"><?=htmlspecialchars($p['username'])?></td>
|
||||
<td class="px-2 text-center text-yellow-400"><?=number_format($p['balance'],2)?></td>
|
||||
<td class="px-2 text-center text-white/40"><?=date('m-d H:i',strtotime($p['created_at']))?></td>
|
||||
<td class="px-2 text-center"><?=$p['status']?'<span class="text-green-400">✓</span>':'<span class="text-red-400">✗</span>'?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($players)): ?><tr><td colspan="5" class="text-center py-6 text-white/20">暂无玩家</td></tr><?php endif; ?>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
|
||||
<?php elseif($page==='odds'): ?>
|
||||
<!-- ========== 赔率设置 ========== -->
|
||||
<h2 class="text-xl font-bold mb-4">⚙️ 赔率设置</h2>
|
||||
<p class="text-white/40 text-xs mb-4">设置你名下玩家的赔率。赔率不能超过上级设定的上限。</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<?php
|
||||
$oddsGroups = [
|
||||
'大小 (Big/Small)' => ['type'=>'bs','targets'=>[]],
|
||||
'单双 (Odd/Even)' => ['type'=>'oe','targets'=>[]],
|
||||
'龙虎 (Dragon/Tiger)' => ['type'=>'dt','targets'=>[]],
|
||||
'冠亚和大小 (Sum BS)' => ['type'=>'sum_bs','targets'=>[]],
|
||||
];
|
||||
// 大小
|
||||
for($r=1;$r<=10;$r++){$oddsGroups['大小 (Big/Small)']['targets'][]=['target'=>"rank{$r}_big",'label'=>"第{$r}名 大"];$oddsGroups['大小 (Big/Small)']['targets'][]=['target'=>"rank{$r}_small",'label'=>"第{$r}名 小"];}
|
||||
// 单双
|
||||
for($r=1;$r<=10;$r++){$oddsGroups['单双 (Odd/Even)']['targets'][]=['target'=>"rank{$r}_odd",'label'=>"第{$r}名 单"];$oddsGroups['单双 (Odd/Even)']['targets'][]=['target'=>"rank{$r}_even",'label'=>"第{$r}名 双"];}
|
||||
// 龙虎
|
||||
$dtPairs=[[1,10],[2,9],[3,8],[4,7],[5,6]];
|
||||
foreach($dtPairs as $i=>$p){$n=$i+1;$oddsGroups['龙虎 (Dragon/Tiger)']['targets'][]=['target'=>"dt{$n}_dragon",'label'=>"第{$p[0]}vs{$p[1]} 龙"];$oddsGroups['龙虎 (Dragon/Tiger)']['targets'][]=['target'=>"dt{$n}_tiger",'label'=>"第{$p[0]}vs{$p[1]} 虎"];}
|
||||
// 冠亚和大小
|
||||
$oddsGroups['冠亚和大小 (Sum BS)']['targets']=[['target'=>'sum_big','label'=>'和大'],['target'=>'sum_small','label'=>'和小'],['target'=>'sum_odd','label'=>'和单'],['target'=>'sum_even','label'=>'和双']];
|
||||
?>
|
||||
|
||||
<?php foreach($oddsGroups as $groupName=>$group): ?>
|
||||
<div class="bg-white/5 rounded-xl p-4">
|
||||
<h3 class="text-sm font-bold mb-3"><?=$groupName?></h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<?php
|
||||
// 同类型只显示一个代表(大小/单双每个名次赔率相同)
|
||||
$type=$group['type'];
|
||||
$firstTarget=$group['targets'][0]['target']??'';
|
||||
$parentKey=$type.'_'.$firstTarget;
|
||||
$parentVal=$parentMap[$parentKey]??1.95;
|
||||
$myVal=$myMap[$parentKey]??$parentVal;
|
||||
?>
|
||||
<div class="col-span-full flex items-center gap-3 bg-white/5 rounded p-3">
|
||||
<span class="text-white/40 text-xs w-24">统一赔率</span>
|
||||
<span class="text-white/30 text-xs">上限: <?=$parentVal?></span>
|
||||
<input type="number" step="0.01" min="1" max="<?=$parentVal?>" value="<?=$myVal?>" class="w-24 px-2 py-1 bg-white/10 border border-white/10 rounded text-sm text-white" data-type="<?=$type?>" data-targets='<?=json_encode(array_column($group['targets'],'target'))?>'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- 名次赔率 -->
|
||||
<div class="bg-white/5 rounded-xl p-4">
|
||||
<h3 class="text-sm font-bold mb-3">名次投注 (Rank) — 猜车号</h3>
|
||||
<?php $parentRank=$parentMap['rank_rank1_1']??9.80; $myRank=$myMap['rank_rank1_1']??$parentRank; ?>
|
||||
<div class="flex items-center gap-3 bg-white/5 rounded p-3">
|
||||
<span class="text-white/40 text-xs w-24">统一赔率</span>
|
||||
<span class="text-white/30 text-xs">上限: <?=$parentRank?></span>
|
||||
<input type="number" step="0.01" min="1" max="<?=$parentRank?>" value="<?=$myRank?>" class="w-24 px-2 py-1 bg-white/10 border border-white/10 rounded text-sm text-white" id="rankOddsInput" data-type="rank">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="saveAllOdds()" class="w-full py-3 bg-yellow-500 text-black font-bold rounded-xl hover:bg-yellow-400 text-sm">保存所有赔率</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function saveAllOdds(){
|
||||
const odds=[];
|
||||
const gameId=<?=$game['id']??0?>;
|
||||
// 收集各类型
|
||||
document.querySelectorAll('input[data-type]').forEach(inp=>{
|
||||
const type=inp.dataset.type;
|
||||
const val=parseFloat(inp.value);
|
||||
if(type==='rank'){
|
||||
// 名次:100个target
|
||||
for(let r=1;r<=10;r++)for(let c=1;c<=10;c++) odds.push({bet_type:'rank',bet_target:'rank'+r+'_'+c,odds:val});
|
||||
} else if(inp.dataset.targets){
|
||||
JSON.parse(inp.dataset.targets).forEach(t=>odds.push({bet_type:type,bet_target:t,odds:val}));
|
||||
}
|
||||
});
|
||||
const r=await fetch('/api/agent/set-odds',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({game_id:gameId,odds:odds})});
|
||||
const d=await r.json();
|
||||
alert(d.message||'完成');if(d.success)location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php elseif($page==='bets'): ?>
|
||||
<!-- ========== 投注记录 ========== -->
|
||||
<h2 class="text-xl font-bold mb-4">📋 玩家投注记录</h2>
|
||||
<div class="bg-white/5 rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-white/40 text-xs border-b border-white/5 bg-white/5">
|
||||
<th class="text-left py-3 px-3">玩家</th><th class="px-3">期号</th><th class="px-3">下注内容</th><th class="px-3">金额</th><th class="px-3">赔率</th><th class="px-3">状态</th><th class="px-3">赢额</th><th class="px-3">时间</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($betRecords??[] as $b):
|
||||
// 投注类型中文转换
|
||||
$bt=$b['bet_type']; $bv=$b['bet_value']; $label='';
|
||||
if($bt==='rank'&&preg_match('/^rank(\d+)_(\d+)$/',$bv,$m)){$rn=(int)$m[1];$label=($rn===1?'冠军':($rn===2?'亚军':'第'.$rn.'名')).' '.$m[2].'号车';}
|
||||
elseif($bt==='bs'&&preg_match('/^rank(\d+)_(big|small)$/',$bv,$m)){$rn=(int)$m[1];$label=($rn===1?'冠军':($rn===2?'亚军':'第'.$rn.'名')).' '.($m[2]==='big'?'大':'小');}
|
||||
elseif($bt==='oe'&&preg_match('/^rank(\d+)_(odd|even)$/',$bv,$m)){$rn=(int)$m[1];$label=($rn===1?'冠军':($rn===2?'亚军':'第'.$rn.'名')).' '.($m[2]==='odd'?'单':'双');}
|
||||
elseif($bt==='dt'&&preg_match('/^dt(\d+)_(dragon|tiger)$/',$bv,$m)){$ps=[1=>[1,10],2=>[2,9],3=>[3,8],4=>[4,7],5=>[5,6]];$p=$ps[(int)$m[1]]??[0,0];$label='龙虎 '.$p[0].'vs'.$p[1].' '.($m[2]==='dragon'?'龙':'虎');}
|
||||
elseif($bt==='sum'&&preg_match('/^sum_(\d+)$/',$bv,$m)){$label='冠亚和 '.$m[1];}
|
||||
elseif($bt==='sum_bs'){$sm=['sum_big'=>'和大','sum_small'=>'和小','sum_odd'=>'和单','sum_even'=>'和双'];$label=$sm[$bv]??$bv;}
|
||||
else{$tm=['xiu'=>'小','tai'=>'大','chan'=>'双','le'=>'单','number'=>'点数','dice'=>'单骰','combo'=>'豹子','big_small'=>'大小','odd_even'=>'单双'];$vm=['big'=>'大','small'=>'小','odd'=>'单','even'=>'双','4red'=>'4红','4white'=>'4白','3red1white'=>'3红1白','1red3white'=>'1红3白'];$label=($tm[$bt]??$bt).' '.($vm[$bv]??$bv);}
|
||||
?>
|
||||
<tr class="border-b border-white/5 text-xs hover:bg-white/5">
|
||||
<td class="py-2 px-3"><?=htmlspecialchars($b['username'])?></td>
|
||||
<td class="px-3 font-mono text-white/40"><?=$b['period_number']?></td>
|
||||
<td class="px-3"><?=htmlspecialchars($label)?></td>
|
||||
<td class="px-3 text-yellow-400"><?=number_format($b['amount'],2)?></td>
|
||||
<td class="px-3"><?=$b['odds']?></td>
|
||||
<td class="px-3"><?php
|
||||
$sc=['pending'=>'text-white/40','win'=>'text-green-400','lose'=>'text-red-400','settled'=>'text-blue-400'];
|
||||
$sl=['pending'=>'待开','win'=>'赢','lose'=>'输','settled'=>'已结'];
|
||||
echo '<span class="'.($sc[$b['status']]??'').'">'.($sl[$b['status']]??$b['status']).'</span>';
|
||||
?></td>
|
||||
<td class="px-3 <?=($b['win_amount']??0)>0?'text-green-400':'text-white/30'?>"><?=number_format($b['win_amount']??0,2)?></td>
|
||||
<td class="px-3 text-white/30"><?=date('m-d H:i',strtotime($b['created_at']))?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($betRecords)): ?><tr><td colspan="8" class="text-center py-8 text-white/20">暂无投注记录</td></tr><?php endif; ?>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
|
||||
<?php elseif($page==='commissions'): ?>
|
||||
<!-- ========== 佣金明细 ========== -->
|
||||
<h2 class="text-xl font-bold mb-4">💰 佣金明细</h2>
|
||||
<div class="bg-white/5 rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-white/40 text-xs border-b border-white/5 bg-white/5">
|
||||
<th class="text-left py-3 px-3">来源玩家</th><th class="px-3">投注额</th><th class="px-3">佣金</th><th class="px-3">类型</th><th class="px-3">时间</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($records??[] as $r): ?>
|
||||
<tr class="border-b border-white/5 text-xs hover:bg-white/5">
|
||||
<td class="py-2 px-3"><?=htmlspecialchars($r['username'])?></td>
|
||||
<td class="px-3 text-yellow-400"><?=number_format($r['bet_amount'],2)?></td>
|
||||
<td class="px-3 text-green-400 font-bold"><?=number_format($r['commission'],2)?></td>
|
||||
<td class="px-3"><?=$r['type']==='bet'?'投注佣金':'反水'?></td>
|
||||
<td class="px-3 text-white/30"><?=date('m-d H:i',strtotime($r['created_at']))?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($records)): ?><tr><td colspan="5" class="text-center py-8 text-white/20">暂无佣金记录</td></tr><?php endif; ?>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
</body></html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user