Initial commit

This commit is contained in:
wangwangit
2025-07-06 11:35:36 +08:00
commit 9502459755
19 changed files with 5513 additions and 0 deletions
+584
View File
@@ -0,0 +1,584 @@
// 企业微信通知配置前端交互脚本
document.addEventListener('DOMContentLoaded', function () {
// 元素引用
const callbackForm = document.getElementById('callbackForm');
const configForm = document.getElementById('configForm');
const validateBtn = document.getElementById('validateBtn');
const userListSection = document.getElementById('userListSection');
const userList = document.getElementById('userList');
const lookupForm = document.getElementById('lookupForm');
const lookupResultDiv = document.getElementById('lookup-result');
const resultDiv = document.getElementById('result');
const saveAlert = document.getElementById('save-alert');
const step1Container = document.getElementById('step1-container');
const step2Container = document.getElementById('step2-container');
const callbackResult = document.getElementById('callbackResult');
let usersCache = [];
let currentCode = null; // 存储当前的code
// 第一步:生成回调URL
callbackForm.addEventListener('submit', async function (e) {
e.preventDefault();
resultDiv.innerHTML = '';
const corpid = callbackForm.corpid.value.trim();
const callbackToken = callbackForm.callback_token.value.trim();
const encodingAesKey = callbackForm.encoding_aes_key.value.trim();
if (!corpid || !callbackToken || !encodingAesKey) {
showError('请填写所有必填项');
return;
}
if (encodingAesKey.length !== 43) {
showError('EncodingAESKey必须是43位字符');
return;
}
const submitBtn = callbackForm.querySelector('button[type=submit]');
submitBtn.disabled = true;
submitBtn.textContent = '生成中...';
try {
const res = await fetch('/api/generate-callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
corpid,
callback_token: callbackToken,
encoding_aes_key: encodingAesKey
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || '生成失败');
currentCode = data.code;
showCallbackResult(data);
// 显示第二步
step2Container.classList.remove('hidden');
gsap.from(step2Container, { opacity: 0, y: 20, duration: 0.5 });
// 将CorpID传递到第二步
configForm.corpid = { value: corpid };
} catch (err) {
showError('生成回调URL失败: ' + err.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '生成回调URL';
}
});
// 验证并获取成员列表
validateBtn.addEventListener('click', async function (e) {
e.preventDefault();
resultDiv.innerHTML = '';
userList.innerHTML = '';
userListSection.classList.add('hidden');
const corpid = callbackForm.corpid.value.trim(); // 从第一步获取
const corpsecret = configForm.corpsecret.value.trim();
if (!corpid || !corpsecret) {
showError('请填写CorpSecret');
return;
}
validateBtn.disabled = true;
validateBtn.textContent = '验证中...';
try {
const res = await fetch('/api/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ corpid, corpsecret })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || '验证失败');
usersCache = data.users || [];
if (usersCache.length === 0) {
showError('未获取到任何成员');
return;
}
userList.innerHTML = usersCache.map(user =>
`<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" class="checkbox checkbox-sm" value="${user.userid}">
<span>${user.name} <span class="text-xs text-gray-400">(${user.userid})</span></span>
</label>`
).join('');
userListSection.classList.remove('hidden');
gsap.from(userListSection, { opacity: 0, y: 20, duration: 0.5 });
} catch (err) {
showError(err.message);
} finally {
validateBtn.disabled = false;
validateBtn.textContent = '验证并获取成员列表';
}
});
// 查找配置
lookupForm.addEventListener('submit', async (e) => {
e.preventDefault();
const code = lookupForm.code.value.trim();
if (!code) return;
lookupResultDiv.innerHTML = '<div class="loading loading-spinner loading-md mx-auto"></div>';
try {
const res = await fetch(`/api/configuration/${code}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error || '查找配置失败');
// 显示配置信息
const apiUrl = `/api/notify/${data.code}`;
lookupResultDiv.innerHTML = `
<div class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title flex items-center gap-2">
<i data-lucide="settings" class="h-5 w-5"></i>
配置详情
</h2>
<div class="space-y-2 mt-2">
<p><span class="font-medium">CorpID:</span> ${data.corpid}</p>
<p><span class="font-medium">AgentID:</span> ${data.agentid}</p>
<p><span class="font-medium">接收用户:</span> ${data.touser.join(', ')}</p>
<p><span class="font-medium">描述:</span> ${data.description || '无'}</p>
<p><span class="font-medium">回调状态:</span> ${data.callback_enabled ? '已启用' : '未启用'}</p>
${data.callback_enabled ? `<p><span class="font-medium">回调Token:</span> ${data.callback_token || '未设置'}</p>` : ''}
<p><span class="font-medium">创建时间:</span> ${new Date(data.created_at).toLocaleString()}</p>
</div>
<div class="card-actions justify-end mt-4">
<button class="btn btn-primary btn-sm" id="edit-config-btn" data-code="${data.code}">
<i data-lucide="edit" class="h-4 w-4"></i>
编辑配置
</button>
<button class="btn btn-outline btn-sm" id="copy-api-btn" data-code="${data.code}">
<i data-lucide="copy" class="h-4 w-4"></i>
复制API地址
</button>
</div>
<div class="divider">API使用说明</div>
<div class="space-y-4">
<div>
<h3 class="font-medium mb-2">请求方式</h3>
<div class="bg-base-200 p-3 rounded-md">
<code class="text-sm">POST ${apiUrl}</code>
</div>
</div>
<div>
<h3 class="font-medium mb-2">请求头</h3>
<div class="bg-base-200 p-3 rounded-md">
<code class="text-sm">Content-Type: application/json</code>
</div>
</div>
<div>
<h3 class="font-medium mb-2">请求参数</h3>
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>必填</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-mono">title</td>
<td>String</td>
<td>否</td>
<td>消息标题,可选</td>
</tr>
<tr>
<td class="font-mono">content</td>
<td>String</td>
<td>是</td>
<td>消息内容,必填</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 class="font-medium mb-2">请求示例</h3>
<div class="bg-base-200 p-3 rounded-md">
<pre class="text-sm whitespace-pre-wrap">curl -X POST "${apiUrl}" \\
-H "Content-Type: application/json" \\
-d '{
"title": "服务器告警",
"content": "CPU使用率超过90%,请及时处理!"
}'</pre>
</div>
</div>
<div>
<h3 class="font-medium mb-2">返回示例</h3>
<div class="bg-base-200 p-3 rounded-md">
<pre class="text-sm whitespace-pre-wrap">{
"message": "发送成功",
"response": {
"errcode": 0,
"errmsg": "ok",
"msgid": "MSGID1234567890"
}
}</pre>
</div>
</div>
</div>
</div>
</div>
`;
lucide.createIcons();
gsap.from(lookupResultDiv.firstElementChild, { opacity: 0, y: 20, duration: 0.5 });
// 绑定编辑和复制按钮事件
document.getElementById('edit-config-btn').addEventListener('click', (e) => {
const code = e.currentTarget.dataset.code;
// 这里可以添加编辑功能的实现
showToast('编辑功能待实现');
});
document.getElementById('copy-api-btn').addEventListener('click', (e) => {
const code = e.currentTarget.dataset.code;
navigator.clipboard.writeText(`/api/notify/${code}`);
showToast('API地址已复制到剪贴板');
});
} catch (err) {
lookupResultDiv.innerHTML = `
<div class="alert alert-error">
<i data-lucide="alert-circle" class="h-5 w-5"></i>
<span>${err.message}</span>
</div>
`;
lucide.createIcons();
}
});
// 第二步:完善配置
configForm.addEventListener('submit', async function (e) {
e.preventDefault();
resultDiv.innerHTML = '';
if (!currentCode) {
showError('请先完成第一步生成回调URL');
return;
}
const corpsecret = configForm.corpsecret.value.trim();
const agentid = configForm.agentid.value.trim();
const description = configForm.description.value.trim();
const checked = userListSection.classList.contains('hidden')
? []
: Array.from(userList.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value);
if (!corpsecret || !agentid || checked.length === 0) {
showError('请填写所有必填项并选择至少一个成员');
return;
}
const payload = {
code: currentCode,
corpsecret,
agentid: Number(agentid),
touser: checked,
description
};
configForm.querySelector('button[type=submit]').disabled = true;
configForm.querySelector('button[type=submit]').textContent = '完成中...';
try {
const res = await fetch('/api/complete-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || '完成失败');
showFinalResult(data);
// 显示一次性保存提醒
saveAlert.classList.remove('hidden');
gsap.from(saveAlert, { opacity: 0, y: -50, duration: 0.5 });
setTimeout(() => {
gsap.to(saveAlert, { opacity: 0, y: -50, duration: 0.5, onComplete: () => {
saveAlert.classList.add('hidden');
saveAlert.style.opacity = 1;
saveAlert.style.transform = 'none';
}});
}, 5000);
} catch (err) {
showError(err.message);
} finally {
configForm.querySelector('button[type=submit]').disabled = false;
configForm.querySelector('button[type=submit]').textContent = '完成配置';
}
});
function showError(msg) {
resultDiv.innerHTML = `<div class="alert alert-error"><span>${msg}</span></div>`;
gsap.from(resultDiv, { opacity: 0, y: 20, duration: 0.5 });
}
function showCallbackResult(data) {
callbackResult.innerHTML = `
<div class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-primary flex items-center gap-2">
<i data-lucide="check-circle" class="h-6 w-6"></i>
回调URL生成成功!
</h2>
<div class="space-y-4 mt-4">
<div>
<div class="font-medium">您的配置Code</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${data.code}</div>
</div>
<div>
<div class="font-medium">回调URL</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${window.location.origin}${data.callbackUrl}</div>
<button class="btn btn-sm btn-outline mt-1" id="copy-callback-url-btn">
<i data-lucide="copy" class="h-4 w-4 mr-1"></i>复制回调URL
</button>
</div>
</div>
<div class="alert alert-info mt-4">
<i data-lucide="info" class="h-5 w-5"></i>
<div>
<div class="font-medium">下一步操作</div>
<div class="text-sm">
1. 复制上方回调URL到企业微信管理后台<br>
2. 配置IP白名单(添加您的服务器IP<br>
3. 完成下方第二步配置
</div>
</div>
</div>
</div>
</div>
`;
// 绑定复制按钮事件
document.getElementById('copy-callback-url-btn').addEventListener('click', () => {
navigator.clipboard.writeText(window.location.origin + data.callbackUrl);
showToast('回调URL已复制到剪贴板');
});
callbackResult.classList.remove('hidden');
lucide.createIcons();
gsap.from(callbackResult.firstElementChild, { opacity: 0, y: 20, duration: 0.5 });
}
function showFinalResult(data) {
resultDiv.innerHTML = `
<div class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-success flex items-center gap-2">
<i data-lucide="check-circle-2" class="h-6 w-6"></i>
配置完成!
</h2>
<div class="space-y-4 mt-4">
<div>
<div class="font-medium">配置Code</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${data.code}</div>
</div>
<div>
<div class="font-medium">通知API地址</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${window.location.origin}${data.apiUrl}</div>
<button class="btn btn-sm btn-outline mt-1" id="copy-api-url-btn">
<i data-lucide="copy" class="h-4 w-4 mr-1"></i>复制API地址
</button>
</div>
<div>
<div class="font-medium">回调地址</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${window.location.origin}${data.callbackUrl}</div>
</div>
</div>
<div class="alert alert-success mt-4">
<i data-lucide="check" class="h-5 w-5"></i>
<span>配置已完成!您现在可以使用API发送通知,也可以接收企业微信回调消息。</span>
</div>
</div>
</div>
`;
// 绑定复制按钮事件
document.getElementById('copy-api-url-btn').addEventListener('click', () => {
navigator.clipboard.writeText(window.location.origin + data.apiUrl);
showToast('API地址已复制到剪贴板');
});
lucide.createIcons();
gsap.from(resultDiv.firstElementChild, { opacity: 0, y: 20, duration: 0.5 });
}
function showResult(data) {
resultDiv.innerHTML = `
<div class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-primary flex items-center gap-2">
<i data-lucide="check-circle" class="h-6 w-6"></i>
API生成成功!
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="space-y-2">
<div class="font-medium">您的唯一调用ID</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${data.code}</div>
</div>
<div class="space-y-2">
<div class="font-medium">API地址</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${data.apiUrl}</div>
<button class="btn btn-sm btn-outline mt-1" id="copy-new-api-btn">
<i data-lucide="copy" class="h-4 w-4 mr-1"></i>复制
</button>
</div>
${data.callbackUrl ? `
<div class="space-y-2 md:col-span-2">
<div class="font-medium">回调地址</div>
<div class="bg-base-200 p-2 rounded-md font-mono text-sm overflow-x-auto">${data.callbackUrl}</div>
<button class="btn btn-sm btn-outline mt-1" id="copy-callback-btn">
<i data-lucide="copy" class="h-4 w-4 mr-1"></i>复制回调地址
</button>
<div class="text-sm text-base-content/60 mt-1">
<i data-lucide="info" class="h-4 w-4 inline mr-1"></i>
在企业微信管理后台配置此回调地址以接收消息
</div>
</div>
` : ''}
</div>
<div class="divider">API使用说明</div>
<div class="space-y-4">
<div>
<h3 class="font-medium mb-2">请求方式</h3>
<div class="bg-base-200 p-3 rounded-md">
<code class="text-sm">POST ${data.apiUrl}</code>
</div>
</div>
<div>
<h3 class="font-medium mb-2">请求头</h3>
<div class="bg-base-200 p-3 rounded-md">
<code class="text-sm">Content-Type: application/json</code>
</div>
</div>
<div>
<h3 class="font-medium mb-2">请求参数</h3>
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>必填</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-mono">title</td>
<td>String</td>
<td>否</td>
<td>消息标题,可选</td>
</tr>
<tr>
<td class="font-mono">content</td>
<td>String</td>
<td>是</td>
<td>消息内容,必填</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 class="font-medium mb-2">请求示例</h3>
<div class="bg-base-200 p-3 rounded-md">
<pre class="text-sm whitespace-pre-wrap">curl -X POST "${data.apiUrl}" \\
-H "Content-Type: application/json" \\
-d '{
"title": "服务器告警",
"content": "CPU使用率超过90%,请及时处理!"
}'</pre>
</div>
</div>
<div>
<h3 class="font-medium mb-2">返回示例</h3>
<div class="bg-base-200 p-3 rounded-md">
<pre class="text-sm whitespace-pre-wrap">{
"message": "发送成功",
"response": {
"errcode": 0,
"errmsg": "ok",
"msgid": "MSGID1234567890"
}
}</pre>
</div>
</div>
</div>
<div class="alert alert-warning mt-4">
<i data-lucide="alert-triangle" class="h-5 w-5"></i>
<span>请妥善保存您的配置Code,它是调用API的唯一凭证。出于安全考虑,它只会显示一次!</span>
</div>
</div>
</div>
`;
// 绑定复制按钮事件
document.getElementById('copy-new-api-btn').addEventListener('click', () => {
navigator.clipboard.writeText(data.apiUrl);
showToast('API地址已复制到剪贴板');
});
// 绑定回调地址复制按钮事件(如果存在)
if (data.callbackUrl) {
document.getElementById('copy-callback-btn').addEventListener('click', () => {
navigator.clipboard.writeText(window.location.origin + data.callbackUrl);
showToast('回调地址已复制到剪贴板');
});
}
// 创建图标
lucide.createIcons();
gsap.from(resultDiv, { opacity: 0, y: 20, duration: 0.5 });
}
// 显示提示消息
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast toast-top toast-center';
toast.innerHTML = `
<div class="alert alert-info">
<span>${message}</span>
</div>
`;
document.body.appendChild(toast);
gsap.fromTo(toast,
{ opacity: 0, y: -20 },
{ opacity: 1, y: 0, duration: 0.3 }
);
setTimeout(() => {
gsap.to(toast, {
opacity: 0,
y: -20,
duration: 0.3,
onComplete: () => toast.remove()
});
}, 3000);
}
});