添加 CLAUDE.md 用于 Claude Code 项目导航,包含架构说明和开发规范。 更新 AI 创作客户端至 V4.0,新增后端对接示例项目。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
236 lines
11 KiB
HTML
236 lines
11 KiB
HTML
<!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.72:9090', // 企业后端(本Demo)
|
||
API_URL: 'http://192.168.1.72:8080', // 乐读派后端(仅作品列表查询用)
|
||
H5_URL: 'http://192.168.1.72: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)换取token,APP_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>
|