library-picturebook-activity/lesingle-aicreate-backend-demo/enterprise-sim.html
En fa42eca339 feat: 数据库注释补全、常量枚举重构及多模块优化
- 新增 Flyway V6/V7 迁移脚本,为全部 42 张表、591 个列添加中文注释
- 抽取公共常量类(BaseEntityConstants、CacheConstants、RoleConstants、TenantConstants)
- 新增业务枚举(CommonStatus、RegistrationStatus、WorkStatus 等 11 个)
- 优化赛事/作业/评审/UGC 等模块服务层代码
- 更新乐读派(leai)模块配置与 API 客户端
- 更新 e2e 测试用例及 demo 文件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 13:37:14 +08:00

236 lines
11 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><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>企业模拟C端</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, 'Segoe UI', sans-serif; background: #f5f5f5; height: 100vh; display: flex; flex-direction: column; max-width: 450px; margin: 0 auto; position: relative; }
/* 顶部信息栏 */
.header { background: white; padding: 8px 16px; border-bottom: 1px solid #eee; font-size: 12px; color: #666; line-height: 1.6; flex-shrink: 0; }
.header .phone { color: #7C3AED; font-weight: 600; }
.header .org { color: #999; font-size: 11px; }
/* 内容区 */
.content { flex: 1; overflow: hidden; position: relative; }
/* 广场/我的 占位 */
.placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: #bbb; font-size: 16px; flex-direction: column; gap: 8px; }
.placeholder .icon { font-size: 40px; }
/* 创作模块 iframe */
#creation-iframe { width: 100%; height: 100%; border: none; }
/* 作品列表 */
.works { padding: 16px; overflow-y: auto; height: 100%; }
.works h3 { color: #333; margin-bottom: 12px; font-size: 16px; }
.work-card { background: white; border-radius: 12px; padding: 12px; margin-bottom: 10px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.work-thumb { width: 56px; height: 56px; border-radius: 8px; background: #f3f4f6; object-fit: cover; flex-shrink: 0; }
.work-info { flex: 1; min-width: 0; }
.work-title { font-size: 14px; font-weight: 600; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.work-meta { font-size: 11px; color: #bbb; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.work-status { padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
.status-3 { background: #FEF3C7; color: #D97706; }
.status-4 { background: #DBEAFE; color: #2563EB; }
.status-5 { background: #D1FAE5; color: #059669; }
.status-2 { background: #FFF7ED; color: #EA580C; }
.work-btn { padding: 5px 12px; background: #7C3AED; color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 11px; font-weight: 600; flex-shrink: 0; }
/* 底部TabBar */
.tabbar { display: flex; background: white; border-top: 1px solid #eee; flex-shrink: 0; padding-bottom: env(safe-area-inset-bottom, 0); }
.tabbar .tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 6px 0 4px; cursor: pointer; transition: color 0.2s; color: #999; }
.tabbar .tab-item.active { color: #7C3AED; }
.tabbar .tab-icon { font-size: 22px; line-height: 1; }
.tabbar .tab-label { font-size: 10px; margin-top: 2px; font-weight: 500; }
/* 日志面板(可折叠) */
#log-panel { position: fixed; bottom: 56px; left: 50%; transform: translateX(-50%); width: 440px; max-width: 100%; max-height: 180px; background: rgba(0,0,0,0.92); color: #0f0; font-size: 10px; font-family: monospace; padding: 6px 10px; overflow-y: auto; z-index: 999; border-radius: 8px 8px 0 0; display: none; }
#log-toggle { position: fixed; bottom: 60px; right: 8px; z-index: 1000; background: rgba(0,0,0,0.6); color: #0f0; border: none; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-family: monospace; cursor: pointer; }
</style>
</head>
<body>
<!-- 顶部用户信息 -->
<div class="header">
<div>用户: <span class="phone">18911223344</span></div>
<div class="org">机构: LESINGLE888888888</div>
</div>
<!-- 内容区 -->
<div class="content" id="content"></div>
<!-- 底部TabBar -->
<div class="tabbar">
<div class="tab-item" data-tab="square" onclick="switchTab('square')">
<div class="tab-icon">🏠</div>
<div class="tab-label">广场</div>
</div>
<div class="tab-item active" data-tab="create" onclick="switchTab('create')">
<div class="tab-icon"></div>
<div class="tab-label">创作</div>
</div>
<div class="tab-item" data-tab="works" onclick="switchTab('works')">
<div class="tab-icon">📚</div>
<div class="tab-label">作品</div>
</div>
<div class="tab-item" data-tab="mine" onclick="switchTab('mine')">
<div class="tab-icon">👤</div>
<div class="tab-label">我的</div>
</div>
</div>
<!-- 日志 -->
<button id="log-toggle" onclick="toggleLog()">LOG</button>
<div id="log-panel"><div id="log"></div></div>
<script>
const CONFIG = {
ORG_ID: 'LESINGLE888888888',
PHONE: '18911223344',
ENTERPRISE_URL: 'http://192.168.1.120:9090', // 企业后端本Demo
API_URL: 'http://192.168.1.120:8080', // 乐读派后端(仅作品列表查询用)
H5_URL: 'http://192.168.1.120:3001' // 乐读派H5前端
// 注意: APP_SECRET 不再出现在前端,仅在企业后端(9090)中使用
}
let currentToken = ''
let currentTab = 'create'
let logVisible = false
function toggleLog() {
logVisible = !logVisible
document.getElementById('log-panel').style.display = logVisible ? 'block' : 'none'
}
function log(msg, type) {
const t = new Date().toLocaleTimeString()
const color = type === 'recv' ? '#0f0' : type === 'send' ? '#0af' : type === 'warn' ? '#fa0' : '#999'
document.getElementById('log').innerHTML =
`<div style="color:${color}">[${t}] ${msg}</div>` + document.getElementById('log').innerHTML
}
async function exchangeToken() {
// 通过企业后端(9090)换取tokenAPP_SECRET不暴露到前端
const res = await fetch(CONFIG.ENTERPRISE_URL + '/api/refresh-token?phone=' + CONFIG.PHONE)
const data = await res.json()
if (!data.token) throw new Error('token交换失败')
currentToken = data.token
log('Token: ' + currentToken.substring(0, 16) + '...', 'send')
return currentToken
}
function switchTab(tab) {
currentTab = tab
document.querySelectorAll('.tab-item').forEach(t => t.classList.toggle('active', t.dataset.tab === tab))
if (tab === 'create') showCreation()
else if (tab === 'works') showWorks()
else {
const icons = { square: '🏠', mine: '👤' }
const names = { square: '广场', mine: '我的' }
document.getElementById('content').innerHTML =
`<div class="placeholder"><div class="icon">${icons[tab]}</div>${names[tab]}模块(企业自建)</div>`
}
}
async function showCreation(path, from, workId) {
const content = document.getElementById('content')
content.innerHTML = '<div class="placeholder"><div class="icon">⏳</div>加载中...</div>'
try {
const token = await exchangeToken()
const src = CONFIG.H5_URL + (path || '') + '?token=' + encodeURIComponent(token)
+ '&orgId=' + encodeURIComponent(CONFIG.ORG_ID)
+ '&phone=' + encodeURIComponent(CONFIG.PHONE) + '&embed=1'
+ (from ? '&from=' + from : '')
+ (workId ? '&workId=' + encodeURIComponent(workId) : '')
content.innerHTML = `<iframe id="creation-iframe" src="${src}" allow="camera;microphone"></iframe>`
log('iframe: ' + (path || '新建创作'), 'send')
} catch (e) {
content.innerHTML = `<div class="placeholder" style="color:#e55">加载失败: ${e.message}</div>`
}
}
async function showWorks() {
const content = document.getElementById('content')
content.innerHTML = '<div class="placeholder"><div class="icon">⏳</div>加载作品...</div>'
try {
const token = await exchangeToken()
const res = await fetch(CONFIG.API_URL + '/api/v1/query/works?orgId=' + CONFIG.ORG_ID + '&pageSize=50', {
headers: { 'Authorization': 'Bearer ' + token }
})
const data = await res.json()
const records = data.data?.records || []
log('作品: ' + records.length + '个', 'recv')
const statusMap = { '-1': '失败', 1: '排队中', 2: '创作中', 3: '待编目', 4: '待配音', 5: '已完成' }
const actionMap = { 1: '创作中', 2: '创作中', 3: '去编目', 4: '去配音', 5: '查看' }
let html = '<div class="works"><h3>📚 我的作品</h3>'
if (!records.length) {
html += '<div class="placeholder" style="height:auto;padding:40px 0;"><div class="icon">📭</div>暂无作品<br><span style="font-size:12px;">去「创作」模块创建第一个绘本</span></div>'
}
for (const w of records) {
const s = w.status || 0
const sc = 'status-' + (s >= 5 ? 5 : s <= 2 ? 2 : s)
html += `<div class="work-card" onclick="openWork('${w.workId}',${s})">
<img class="work-thumb" src="${w.coverImageUrl || ''}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2256%22 height=%2256%22><rect fill=%22%23f3f4f6%22 width=%2256%22 height=%2256%22 rx=%228%22/><text x=%2228%22 y=%2232%22 text-anchor=%22middle%22 fill=%22%23ccc%22 font-size=%2220%22>📖</text></svg>'" />
<div class="work-info">
<div class="work-title">${w.title || '未命名'}</div>
<div class="work-meta">${w.workId}</div>
</div>
${s >= 1 && s <= 4
? `<button class="work-btn" onclick="event.stopPropagation();openWork('${w.workId}',${s})">${actionMap[s]}</button>`
: `<div class="work-status ${sc}">${statusMap[s] || '?'}</div>`}
</div>`
}
html += '</div>'
content.innerHTML = html
} catch (e) {
content.innerHTML = `<div class="placeholder" style="color:#e55">加载失败: ${e.message}</div>`
}
}
function openWork(workId, status) {
log('跳转: ' + workId + ' status=' + status, 'send')
const pathMap = { 3: '/edit-info/', 4: '/dubbing/', 5: '/read/' }
if (!pathMap[status] && status !== 2 && status !== 1) return
// status=1/2: 创作进度页通过query传workId让Creating.vue恢复轮询
if (status === 1 || status === 2) {
// 创作中跳creating页额外传workId让H5恢复轮询
showCreation('/creating', 'works', workId)
} else {
showCreation(pathMap[status] + workId, 'works')
}
}
// postMessage 监听
window.addEventListener('message', function(event) {
const msg = event.data
if (!msg || msg.source !== 'leai-creation') return
log('[PM] ' + msg.type + ' ' + JSON.stringify(msg.payload || {}).substring(0, 80), 'recv')
if (msg.type === 'TOKEN_EXPIRED') {
log('Token过期, 刷新中...', 'warn')
exchangeToken().then(function(t) {
const f = document.getElementById('creation-iframe')
if (f && f.contentWindow) {
f.contentWindow.postMessage({
source: 'leai-creation', version: 1, type: 'TOKEN_REFRESHED',
payload: { messageId: msg.payload.messageId, token: t, orgId: CONFIG.ORG_ID, phone: CONFIG.PHONE }
}, '*')
log('Token已刷新', 'send')
}
}).catch(e => log('刷新失败: ' + e, 'warn'))
}
if (msg.type === 'NAVIGATE_BACK') {
log('H5请求返回 → 切到作品列表', 'recv')
switchTab('works')
}
})
switchTab('create')
log('启动 | ' + CONFIG.ORG_ID + ' | ' + CONFIG.PHONE, 'info')
</script>
</body></html>