Initial commit: 投注游戏平台初始化

This commit is contained in:
li
2026-02-25 01:26:58 +08:00
commit 77ca2cc8b3
275 changed files with 237479 additions and 0 deletions
+740
View File
@@ -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 => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
} [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>
+142
View File
@@ -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>
+113
View File
@@ -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&timestamp=$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);
}
}
+38
View File
@@ -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>