Initial commit: 投注游戏平台初始化
This commit is contained in:
+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) {},
|
||||
|
||||
|
||||
];
|
||||
Reference in New Issue
Block a user