Files
akmon/demo_message_system.html
2026-01-20 08:04:15 +08:00

851 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>