Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

850
demo_message_system.html Normal file
View File

@@ -0,0 +1,850 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Supabase 消息系统演示</title>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
padding: 40px;
max-width: 900px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
margin-bottom: 10px;
font-size: 2.5rem;
}
.header p {
color: #666;
font-size: 1.1rem;
}
.user-info {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
border-left: 4px solid #007bff;
}
.user-info h3 {
color: #333;
margin-bottom: 10px;
}
.user-info .info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
align-items: center;
gap: 10px;
}
.info-label {
font-weight: 700;
color: #555;
}
.info-value {
color: #333;
background: white;
padding: 5px 10px;
border-radius: 5px;
font-family: monospace;
}
.role-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
}
.role-admin { background: #dc3545; color: white; }
.role-teacher { background: #28a745; color: white; }
.role-student { background: #007bff; color: white; }
.section {
margin-bottom: 30px;
}
.section h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.3rem;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 400;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: #0056b3;
transform: translateY(-2px);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.btn-success { background: #28a745; }
.btn-success:hover { background: #1e7e34; }
.btn-warning { background: #ffc107; color: #333; }
.btn-warning:hover { background: #e0a800; }
.btn-danger { background: #dc3545; }
.btn-danger:hover { background: #c82333; }
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 700;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #007bff;
}
.results {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
border: 1px solid #e0e0e0;
}
.results h4 {
color: #333;
margin-bottom: 15px;
}
.results pre {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
font-size: 0.9rem;
line-height: 1.4;
}
.message-list {
max-height: 400px;
overflow-y: auto;
}
.message-item {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
border-left: 4px solid #007bff;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.message-title {
font-weight: 700;
color: #333;
}
.message-time {
color: #666;
font-size: 0.8rem;
}
.message-content {
color: #555;
line-height: 1.5;
}
.hidden {
display: none;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 8px;
font-weight: 400;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1> Supabase 消息系统</h1>
<p>完整的角色管理与权限控制演示</p>
</div>
<!-- 登录表单 -->
<div id="login-section" class="section">
<h3> 用户登录</h3>
<div class="form-group">
<label>邮箱地址</label>
<input type="email" id="email" placeholder="请输入邮箱" value="teacher@example.com">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" placeholder="请输入密码" value="password123">
</div>
<div class="button-group">
<button class="btn" onclick="signIn()">
<span></span> 登录
</button>
<button class="btn btn-success" onclick="signUp()">
<span></span> 注册
</button>
</div>
<div class="alert alert-info">
<strong>测试账户:</strong><br>
教师: teacher@example.com / password123<br>
学生: student@example.com / password123
</div>
</div>
<!-- 用户信息 -->
<div id="user-section" class="section hidden">
<div class="user-info">
<h3> 当前用户信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">邮箱:</span>
<span class="info-value" id="user-email">-</span>
</div>
<div class="info-item">
<span class="info-label">角色:</span>
<span class="role-badge" id="user-role">-</span>
</div>
<div class="info-item">
<span class="info-label">用户ID:</span>
<span class="info-value" id="user-id">-</span>
</div>
<div class="info-item">
<span class="info-label">部门:</span>
<span class="info-value" id="user-department">-</span>
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-danger" onclick="signOut()">
<span></span> 退出登录
</button>
<button class="btn btn-warning" onclick="testPermissions()">
<span></span> 测试权限
</button>
<button class="btn" onclick="refreshUserInfo()">
<span></span> 刷新信息
</button>
</div>
</div>
<!-- 消息功能 -->
<div id="message-section" class="section hidden">
<h3> 消息功能</h3>
<!-- 发送消息 -->
<div class="form-group">
<label>消息标题</label>
<input type="text" id="message-title" placeholder="请输入消息标题">
</div>
<div class="form-group">
<label>消息内容</label>
<textarea id="message-content" rows="3" placeholder="请输入消息内容"></textarea>
</div>
<div class="form-group">
<label>接收者类型</label>
<select id="receiver-type">
<option value="user">指定用户</option>
<option value="broadcast">广播消息</option>
<option value="group">群组消息</option>
</select>
</div>
<div class="form-group">
<label>接收者ID用户ID或群组ID</label>
<input type="text" id="receiver-id" placeholder="请输入接收者ID">
</div>
<div class="button-group">
<button class="btn" onclick="sendMessage()">
<span></span> 发送消息
</button>
<button class="btn btn-success" onclick="loadMessages()">
<span></span> 刷新消息
</button>
</div>
<!-- 消息列表 -->
<div class="message-list" id="message-list">
<h4> 消息列表</h4>
<div id="messages-container">
<p>点击"刷新消息"加载消息列表</p>
</div>
</div>
</div>
<!-- 管理员功能 -->
<div id="admin-section" class="section hidden">
<h3>⚙️ 管理员功能</h3>
<div class="form-group">
<label>目标用户ID</label>
<input type="text" id="target-user-id" placeholder="请输入要管理的用户ID">
</div>
<div class="form-group">
<label>新角色</label>
<select id="new-role">
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="button-group">
<button class="btn" onclick="updateUserRole()">
<span></span> 更新用户角色
</button>
<button class="btn btn-success" onclick="getAllUsers()">
<span></span> 查看所有用户
</button>
</div>
</div>
<!-- 结果显示 -->
<div id="results" class="results hidden">
<h4> 操作结果</h4>
<pre id="results-content"></pre>
</div>
</div>
<script>
// Supabase 配置 - 请替换为你的实际配置
const SUPABASE_URL = 'YOUR_SUPABASE_URL'
const SUPABASE_ANON_KEY = 'YOUR_SUPABASE_ANON_KEY'
// 如果没有配置,使用演示模式
if (SUPABASE_URL === 'YOUR_SUPABASE_URL') {
document.querySelector('.container').innerHTML = `
<div class="header">
<h1>⚠️ 配置需要</h1>
<p>请在代码中配置你的 Supabase URL 和 API Key</p>
</div>
<div class="alert alert-error">
<strong>配置步骤:</strong><br>
1. 替换 SUPABASE_URL 为你的项目URL<br>
2. 替换 SUPABASE_ANON_KEY 为你的匿名密钥<br>
3. 执行 deploy_complete_system.sql 部署数据库<br>
4. 创建测试用户账户
</div>
`
return
}
const supabase = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
let currentUser = null
let currentRole = null
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
console.log(' 初始化消息系统演示')
// 检查当前用户
const { data: { user } } = await supabase.auth.getUser()
if (user) {
await handleUserSignedIn(user)
}
// 监听认证状态变化
supabase.auth.onAuthStateChange(async (event, session) => {
console.log(' 认证状态变化:', event)
if (event === 'SIGNED_IN' && session?.user) {
await handleUserSignedIn(session.user)
} else if (event === 'SIGNED_OUT') {
handleUserSignedOut()
}
})
})
// 用户登录处理
async function handleUserSignedIn(user) {
currentUser = user
currentRole = await getUserRole(user.id)
// 显示/隐藏相应区域
document.getElementById('login-section').classList.add('hidden')
document.getElementById('user-section').classList.remove('hidden')
document.getElementById('message-section').classList.remove('hidden')
if (currentRole === 'admin') {
document.getElementById('admin-section').classList.remove('hidden')
}
// 更新用户信息显示
updateUserInfoDisplay()
showAlert('登录成功!', 'success')
}
// 用户退出处理
function handleUserSignedOut() {
currentUser = null
currentRole = null
// 显示/隐藏相应区域
document.getElementById('login-section').classList.remove('hidden')
document.getElementById('user-section').classList.add('hidden')
document.getElementById('message-section').classList.add('hidden')
document.getElementById('admin-section').classList.add('hidden')
document.getElementById('results').classList.add('hidden')
showAlert('已退出登录', 'info')
}
// 更新用户信息显示
function updateUserInfoDisplay() {
if (!currentUser) return
document.getElementById('user-email').textContent = currentUser.email
document.getElementById('user-id').textContent = currentUser.id.substring(0, 8) + '...'
const roleElement = document.getElementById('user-role')
roleElement.textContent = currentRole || 'unknown'
roleElement.className = `role-badge role-${currentRole || 'unknown'}`
// 从元数据获取部门信息
const department = currentUser.user_metadata?.department ||
currentUser.raw_user_meta_data?.department ||
'未设置'
document.getElementById('user-department').textContent = department
}
// 获取用户角色
async function getUserRole(userId) {
try {
const { data, error } = await supabase
.rpc('get_user_role', { target_user_id: userId })
if (error) throw error
return data || 'student'
} catch (error) {
console.error('获取用户角色失败:', error)
return 'student'
}
}
// 登录
async function signIn() {
const email = document.getElementById('email').value
const password = document.getElementById('password').value
if (!email || !password) {
showAlert('请输入邮箱和密码', 'error')
return
}
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
// 用户登录成功会触发 onAuthStateChange
} catch (error) {
console.error('登录失败:', error)
showAlert(`登录失败: ${error.message}`, 'error')
}
}
// 注册
async function signUp() {
const email = document.getElementById('email').value
const password = document.getElementById('password').value
if (!email || !password) {
showAlert('请输入邮箱和密码', 'error')
return
}
try {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
department: 'Demo Department'
}
}
})
if (error) throw error
showAlert('注册成功!请检查邮箱验证链接', 'success')
} catch (error) {
console.error('注册失败:', error)
showAlert(`注册失败: ${error.message}`, 'error')
}
}
// 退出登录
async function signOut() {
try {
const { error } = await supabase.auth.signOut()
if (error) throw error
// 退出成功会触发 onAuthStateChange
} catch (error) {
console.error('退出失败:', error)
showAlert(`退出失败: ${error.message}`, 'error')
}
}
// 测试权限
async function testPermissions() {
if (!currentUser) return
try {
const { data, error } = await supabase
.rpc('test_message_permissions', { test_user_id: currentUser.id })
if (error) throw error
showResults('权限测试结果', data)
} catch (error) {
console.error('权限测试失败:', error)
showAlert(`权限测试失败: ${error.message}`, 'error')
}
}
// 刷新用户信息
async function refreshUserInfo() {
if (!currentUser) return
try {
const { data: { user } } = await supabase.auth.getUser()
if (user) {
currentUser = user
currentRole = await getUserRole(user.id)
updateUserInfoDisplay()
showAlert('用户信息已刷新', 'success')
}
} catch (error) {
console.error('刷新用户信息失败:', error)
showAlert(`刷新失败: ${error.message}`, 'error')
}
}
// 发送消息
async function sendMessage() {
const title = document.getElementById('message-title').value
const content = document.getElementById('message-content').value
const receiverType = document.getElementById('receiver-type').value
const receiverId = document.getElementById('receiver-id').value
if (!title || !content) {
showAlert('请输入消息标题和内容', 'error')
return
}
if (receiverType !== 'broadcast' && !receiverId) {
showAlert('请输入接收者ID', 'error')
return
}
try {
// 获取消息类型ID
const { data: messageTypes } = await supabase
.from('ak_message_types')
.select('id')
.eq('type_name', 'notification')
.limit(1)
const messageTypeId = messageTypes?.[0]?.id
if (!messageTypeId) {
throw new Error('未找到消息类型')
}
const { data, error } = await supabase
.rpc('send_secure_message', {
message_type_id: messageTypeId,
receiver_type: receiverType,
receiver_id: receiverType === 'broadcast' ? null : receiverId,
title,
content,
metadata_json: { source: 'web_demo' }
})
if (error) throw error
showAlert('消息发送成功!', 'success')
// 清空表单
document.getElementById('message-title').value = ''
document.getElementById('message-content').value = ''
document.getElementById('receiver-id').value = ''
// 刷新消息列表
await loadMessages()
} catch (error) {
console.error('发送消息失败:', error)
showAlert(`发送消息失败: ${error.message}`, 'error')
}
}
// 加载消息列表
async function loadMessages() {
try {
const { data, error } = await supabase
.from('ak_messages')
.select(`
*,
ak_message_types(type_name, display_name)
`)
.order('created_at', { ascending: false })
.limit(10)
if (error) throw error
displayMessages(data)
showAlert('消息列表已刷新', 'success')
} catch (error) {
console.error('加载消息失败:', error)
showAlert(`加载消息失败: ${error.message}`, 'error')
}
}
// 显示消息列表
function displayMessages(messages) {
const container = document.getElementById('messages-container')
if (!messages || messages.length === 0) {
container.innerHTML = '<p>暂无消息</p>'
return
}
const messagesHtml = messages.map(msg => `
<div class="message-item">
<div class="message-header">
<span class="message-title">${msg.title}</span>
<span class="message-time">${new Date(msg.created_at).toLocaleString()}</span>
</div>
<div class="message-content">${msg.content}</div>
<div style="margin-top: 10px; font-size: 0.8rem; color: #888;">
ID: ${msg.id.substring(0, 8)}... |
类型: ${msg.ak_message_types?.display_name || '未知'} |
接收者: ${msg.receiver_type}
</div>
</div>
`).join('')
container.innerHTML = messagesHtml
}
// 更新用户角色(管理员功能)
async function updateUserRole() {
const targetUserId = document.getElementById('target-user-id').value
const newRole = document.getElementById('new-role').value
if (!targetUserId) {
showAlert('请输入目标用户ID', 'error')
return
}
try {
const { data, error } = await supabase
.rpc('update_user_role', {
target_user_id: targetUserId,
new_role: newRole,
additional_data: { department: 'Updated via Demo' }
})
if (error) throw error
showAlert('用户角色更新成功!', 'success')
document.getElementById('target-user-id').value = ''
} catch (error) {
console.error('更新用户角色失败:', error)
showAlert(`更新失败: ${error.message}`, 'error')
}
}
// 获取所有用户(管理员功能)
async function getAllUsers() {
try {
const { data, error } = await supabase
.from('user_roles_detailed')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
showResults('所有用户列表', data)
} catch (error) {
console.error('获取用户列表失败:', error)
showAlert(`获取用户列表失败: ${error.message}`, 'error')
}
}
// 显示结果
function showResults(title, data) {
const resultsSection = document.getElementById('results')
const resultsContent = document.getElementById('results-content')
resultsSection.classList.remove('hidden')
resultsSection.querySelector('h4').textContent = ` ${title}`
resultsContent.textContent = JSON.stringify(data, null, 2)
// 滚动到结果区域
resultsSection.scrollIntoView({ behavior: 'smooth' })
}
// 显示提示信息
function showAlert(message, type = 'info') {
// 移除现有的alert
const existingAlert = document.querySelector('.alert')
if (existingAlert && !existingAlert.classList.contains('alert-info')) {
existingAlert.remove()
}
const alertDiv = document.createElement('div')
alertDiv.className = `alert alert-${type}`
alertDiv.textContent = message
// 插入到container的开始
const container = document.querySelector('.container')
container.insertBefore(alertDiv, container.firstChild)
// 3秒后自动移除
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove()
}
}, 3000)
}
</script>
</body>
</html>