<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>巧司兔Chobun小手机 · 测试版</title>
<style>
/* ========== 全局重置 ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-tap-highlight-color: transparent;
}
body {
background: #fce4ec;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
/* ========== 手机容器 ========== */
#phone {
width: 100%;
max-width: 420px;
height: 100vh;
max-height: 900px;
background: #fff9fb;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ========== 状态栏 ========== */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px 4px;
background: #fff9fb;
font-size: 14px;
font-weight: 600;
color: #5d4a4a;
flex-shrink: 0;
z-index: 50;
height: 44px;
}
.status-bar .icons {
display: flex;
gap: 6px;
align-items: center;
}
.battery-level {
width: 22px;
height: 11px;
border: 1.5px solid #5d4a4a;
border-radius: 3px;
position: relative;
padding: 1.5px;
}
.battery-level::after {
content: '';
position: absolute;
right: -4px;
top: 2px;
width: 2px;
height: 5px;
background: #5d4a4a;
border-radius: 0 1px 1px 0;
}
.battery-fill {
width: 75%;
height: 100%;
background: #5d4a4a;
border-radius: 1px;
}
/* ========== 底部交互栏 ========== */
.bottom-bar {
display: flex;
justify-content: center;
align-items: center;
gap: 80px;
padding: 10px 0 8px;
background: #fff9fb;
flex-shrink: 0;
border-top: 0.5px solid #f0dce0;
z-index: 50;
height: 48px;
}
.bottom-bar .btn-home {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1.5px solid #d4b8be;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #b89aa0;
cursor: pointer;
transition: background 0.2s;
background: transparent;
user-select: none;
}
.bottom-bar .btn-home:active {
background: #fce4ec;
}
.bottom-bar .btn-exit {
font-size: 18px;
color: #b89aa0;
cursor: pointer;
user-select: none;
padding: 4px 8px;
}
/* ========== 页面容器 & 淡入淡出 ========== */
.page {
position: absolute;
top: 44px;
left: 0;
right: 0;
bottom: 48px;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
opacity: 0;
transition: opacity 0.6s ease, transform 0.6s ease;
transform: translateY(10px);
pointer-events: none;
background: #fff9fb;
scroll-behavior: smooth;
}
.page.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.page.hidden {
display: none;
}
/* ========== 通用按钮 ========== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
border: none;
outline: none;
transition: all 0.2s;
width: 100%;
margin-bottom: 10px;
}
.btn-primary {
background: #f8a5b8;
color: #fff;
border: 2px solid #f8a5b8;
}
.btn-primary:active {
background: #f59aaf;
}
.btn-outline {
background: #fff;
color: #f8a5b8;
border: 2px solid #f8a5b8;
}
.btn-outline:active {
background: #fff0f3;
}
.btn-sm {
padding: 8px 16px;
font-size: 14px;
width: auto;
border-radius: 18px;
}
/* ========== 输入框 ========== */
.input-field {
width: 100%;
padding: 14px 16px;
border: 1.5px solid #f0dce0;
border-radius: 14px;
font-size: 15px;
background: #fff;
color: #5d4a4a;
outline: none;
margin-bottom: 12px;
transition: border 0.2s;
}
.input-field:focus {
border-color: #f8a5b8;
}
.input-field::placeholder {
color: #c4a8ae;
}
/* ========== 卡片 ========== */
.card {
background: #fff;
border-radius: 18px;
padding: 16px;
margin-bottom: 14px;
box-shadow: 0 2px 12px rgba(248, 165, 184, 0.08);
}
/* ========== 弹窗 ========== */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.25);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.35s ease;
}
.modal-overlay.show {
opacity: 1;
pointer-events: auto;
}
.modal-box {
background: #fff;
border-radius: 22px;
padding: 24px 20px;
width: 80%;
max-width: 320px;
text-align: center;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
transform: scale(0.92);
transition: transform 0.35s ease;
}
.modal-overlay.show .modal-box {
transform: scale(1);
}
.modal-box p {
font-size: 15px;
color: #5d4a4a;
margin-bottom: 18px;
line-height: 1.5;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.modal-actions .btn {
margin-bottom: 0;
flex: 1;
padding: 10px 0;
font-size: 15px;
}
/* ========== Toast ========== */
.toast {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
background: rgba(255, 255, 255, 0.96);
color: #5d4a4a;
padding: 14px 28px;
border-radius: 16px;
font-size: 15px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease, transform 0.5s ease;
white-space: nowrap;
}
.toast.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
/* ========== 密码圆点 ========== */
.password-dots {
display: flex;
gap: 14px;
justify-content: center;
margin: 24px 0;
}
.password-dots .dot {
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid #f0dce0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #5d4a4a;
transition: border 0.2s, background 0.2s;
}
.password-dots .dot.filled {
border-color: #f8a5b8;
background: #fff0f3;
}
/* ========== 聊天气泡 ========== */
.chat-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 0;
}
.message-row {
display: flex;
align-items: flex-end;
gap: 8px;
max-width: 100%;
}
.message-row.user {
flex-direction: row-reverse;
}
.message-row.ai {
flex-direction: row;
}
.msg-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: #f8a5b8;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
flex-shrink: 0;
overflow: hidden;
cursor: pointer;
}
.msg-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.msg-bubble {
max-width: 70%;
padding: 10px 14px;
border-radius: 18px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
background: #fce4ec;
color: #5d4a4a;
box-shadow: 0 1px 4px rgba(248, 165, 184, 0.15);
}
.message-row.user .msg-bubble {
background: #f8a5b8;
color: #fff;
border-bottom-right-radius: 4px;
}
.message-row.ai .msg-bubble {
background: #fce4ec;
color: #5d4a4a;
border-bottom-left-radius: 4px;
}
/* 气泡滑入动画 */
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInRight {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
.message-row.user { animation: slideInLeft 0.5s ease forwards; }
.message-row.ai { animation: slideInRight 0.5s ease forwards; }
/* ========== 桌面图标 ========== */
.desktop-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px 16px;
padding: 30px 10px;
}
.app-icon {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
}
.app-icon .icon-box {
width: 64px;
height: 64px;
border-radius: 16px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
box-shadow: 0 2px 12px rgba(248, 165, 184, 0.2);
color: #f8a5b8;
transition: transform 0.2s;
}
.app-icon:active .icon-box { transform: scale(0.92); }
.app-icon .app-name { font-size: 12px; color: #5d4a4a; }
/* ========== 底部导航 ========== */
.tab-bar {
display: flex;
justify-content: space-around;
align-items: center;
padding: 8px 0 6px;
background: #fff;
border-top: 0.5px solid #f0dce0;
flex-shrink: 0;
height: 56px;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
font-size: 11px;
color: #c4a8ae;
cursor: pointer;
transition: color 0.2s;
user-select: none;
flex: 1;
}
.tab-item .tab-icon { font-size: 20px; }
.tab-item.active { color: #f8a5b8; }
/* ========== 聊天列表 ========== */
.chat-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: #fff;
border-radius: 16px;
margin-bottom: 10px;
cursor: pointer;
box-shadow: 0 1px 6px rgba(248, 165, 184, 0.08);
}
.chat-list-item:active { background: #fff5f7; }
.chat-list-item .avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #f8a5b8;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
overflow: hidden;
flex-shrink: 0;
}
.chat-list-item .avatar img { width: 100%; height: 100%; object-fit: cover; }
.chat-list-item .info { flex: 1; min-width: 0; }
.chat-list-item .info .name { font-size: 15px; font-weight: 500; color: #5d4a4a; }
.chat-list-item .info .last-msg {
font-size: 12px; color: #c4a8ae;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-top: 2px;
}
/* ========== 世界书 ========== */
.worldbook-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: #fff;
border-radius: 14px;
margin-bottom: 10px;
box-shadow: 0 1px 6px rgba(248, 165, 184, 0.08);
}
.worldbook-item .title { font-size: 15px; color: #5d4a4a; font-weight: 500; }
.worldbook-item .actions { display: flex; gap: 8px; }
.worldbook-item .actions .btn-sm { padding: 4px 12px; font-size: 12px; }
/* ========== 辅助 ========== */
.flex-between { display: flex; justify-content: space-between; align-items: center; }
.text-muted { color: #c4a8ae; font-size: 13px; }
.text-center { text-align: center; }
.mb-10 { margin-bottom: 10px; }
.mt-10 { margin-top: 10px; }
.page::-webkit-scrollbar { width: 4px; }
.page::-webkit-scrollbar-thumb { background: #f0dce0; border-radius: 4px; }
</style>
</head>
<body>
<div id="phone">
<!-- 状态栏 -->
<div class="status-bar">
<span class="time" id="statusTime">9:41</span>
<div class="icons">
<span>📶</span>
<div class="battery">
<div class="battery-level"><div class="battery-fill"></div></div>
</div>
</div>
</div>
<!-- 页面容器 -->
<div class="page active" id="page-age">
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center;padding:20px;">
<div style="font-size:56px;margin-bottom:20px;">🔞</div>
<h2 style="color:#5d4a4a;font-size:20px;margin-bottom:12px;">用户准入提示</h2>
<p style="color:#c4a8ae;font-size:14px;line-height:1.6;margin-bottom:30px;">
本应用为模拟手机聊天工具,仅限年满18周岁的用户使用。<br>请确认您已成年。
</p>
<button class="btn btn-primary" id="btnAgeConfirm">我已年满18周岁,继续</button>
</div>
</div>
<div class="page" id="page-login">
<div style="padding:20px 0;">
<h2 style="color:#5d4a4a;font-size:22px;margin-bottom:6px;">登录</h2>
<p style="color:#c4a8ae;font-size:13px;margin-bottom:24px;">欢迎使用巧司兔Chobun</p>
<input class="input-field" id="loginEmail" type="email" placeholder="邮箱地址" />
<input class="input-field" id="loginCode" type="text" placeholder="验证码" />
<input class="input-field" id="loginActivate" type="text" placeholder="激活码" />
<button class="btn btn-primary" id="btnEmailLogin">登录</button>
<div style="text-align:center;margin:14px 0;color:#c4a8ae;font-size:12px;">— 或 —</div>
<button class="btn btn-outline" id="btnGuestLogin">游客登录</button>
</div>
</div>
<div class="page" id="page-password">
<div style="padding:20px 0;text-align:center;">
<h2 style="color:#5d4a4a;font-size:20px;margin-bottom:8px;" id="passwordTitle">请设置你的密码</h2>
<p style="color:#c4a8ae;font-size:13px;margin-bottom:20px;">6位数字密码,用于解锁应用</p>
<div class="password-dots" id="passwordDots"></div>
<button class="btn btn-primary" id="btnConfirmPassword">解锁</button>
<button class="btn btn-outline" id="btnSkipPassword" style="margin-top:6px;">跳过,暂不设置</button>
</div>
</div>
<div class="page" id="page-desktop">
<div class="desktop-grid">
<div class="app-icon" data-app="chat">
<div class="icon-box">💬</div>
<span class="app-name">聊天</span>
</div>
<div class="app-icon" data-app="settings">
<div class="icon-box">≡</div>
<span class="app-name">设置</span>
</div>
<div class="app-icon" data-app="worldbook">
<div class="icon-box">📖</div>
<span class="app-name">世界书</span>
</div>
<div class="app-icon" data-app="profile">
<div class="icon-box">👤</div>
<span class="app-name">我</span>
</div>
</div>
</div>
<div class="page" id="page-chat-app" style="padding:0;display:flex;flex-direction:column;">
<div id="chatAppContent" style="flex:1;overflow-y:auto;padding:12px 16px;"></div>
<div class="tab-bar" id="chatTabBar">
<div class="tab-item active" data-tab="chat">
<span class="tab-icon">💬</span>
<span>聊天</span>
</div>
<div class="tab-item" data-tab="contacts">
<span class="tab-icon">📇</span>
<span>通讯录</span>
</div>
<div class="tab-item" data-tab="moments">
<span class="tab-icon">🖼️</span>
<span>朋友圈</span>
</div>
<div class="tab-item" data-tab="me">
<span class="tab-icon">👤</span>
<span>我</span>
</div>
</div>
</div>
<div class="page" id="page-worldbook">
<div class="flex-between mb-10">
<h2 style="color:#5d4a4a;font-size:18px;">世界书</h2>
<button class="btn btn-primary btn-sm" id="btnNewWorldbook">+ 新建</button>
</div>
<div id="worldbookList"></div>
</div>
<div class="page" id="page-settings">
<h2 style="color:#5d4a4a;font-size:18px;margin-bottom:16px;">设置</h2>
<div class="card">
<h3 style="font-size:15px;color:#5d4a4a;margin-bottom:10px;">DeepSeek API 配置</h3>
<input class="input-field" id="setApiUrl" placeholder="API 地址" />
<input class="input-field" id="setApiKey" placeholder="API 密钥" />
<input class="input-field" id="setModel" placeholder="模型 (如 deepseek-chat)" />
<input class="input-field" id="setTemp" placeholder="温度 (0-2)" type="number" step="0.1" />
<button class="btn btn-primary" id="btnSaveSettings">保存设置</button>
</div>
<div class="card">
<h3 style="font-size:15px;color:#5d4a4a;margin-bottom:10px;">角色人设编辑</h3>
<div id="settingsRoleList"></div>
</div>
</div>
<div class="page" id="page-role-profile">
<div style="text-align:center;padding:10px 0;">
<div style="width:80px;height:80px;border-radius:50%;background:#f8a5b8;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;font-size:32px;color:#fff;overflow:hidden;" id="profileAvatar"></div>
<h2 style="font-size:22px;color:#5d4a4a;" id="profileRemark">备注名</h2>
<p style="font-size:13px;color:#c4a8ae;margin-top:4px;" id="profileOriginal">昵称</p>
<p style="font-size:13px;color:#c4a8ae;" id="profileChobun">巧司号</p>
<p style="font-size:13px;color:#c4a8ae;" id="profileRegion">地区</p>
</div>
<div class="flex-between mb-10">
<span class="text-muted">角色朋友圈</span>
<button class="btn btn-outline btn-sm" id="btnOpenRoleMoments">进入</button>
</div>
<div style="display:flex;gap:10px;justify-content:center;">
<button class="btn btn-primary btn-sm" id="btnEditCurrentRole">编辑资料</button>
<button class="btn btn-outline btn-sm" id="btnBackToChat">返回聊天</button>
</div>
</div>
<div class="bottom-bar">
<div class="btn-home" id="btnHome">⏎</div>
<div class="btn-exit" id="btnExit">✕</div>
</div>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal-box">
<p id="modalText">提示</p>
<div class="modal-actions" id="modalActions"></div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
/* ============================================================
* 全局配置
* ============================================================ */
const CONFIG = {
API_URL: "",
API_KEY: "",
MODEL: "deepseek-chat",
TEMPERATURE: 0.8,
MAX_CONTEXT: 20,
DEFAULT_PERSONA: "你是{{char}},按照创建面板设定的性格、地区、背景世界书进行对话,使用第一人称回复,口语自然,可以带动作神态描写,回复简短,像微信聊天。"
};
/* ============================================================
* 全局状态
* ============================================================ */
let currentPage = 'page-age';
let currentChatTab = 'chat';
let currentRoleId = null;
let currentChatRoleId = null;
let passwordInput = "";
let pendingPassword = "";
let isGuest = false;
/* ============================================================
* 本地存储
* ============================================================ */
const store = {
get(key, fallback = null) {
try {
const v = localStorage.getItem(key);
return v ? JSON.parse(v) : fallback;
} catch { return fallback; }
},
set(key, val) { localStorage.setItem(key, JSON.stringify(val)); },
remove(key) { localStorage.removeItem(key); }
};
function getRoles() { return store.get('chobun_roles', []); }
function saveRoles(roles) { store.set('chobun_roles', roles); }
function getWorldbooks() { return store.get('chobun_worldbooks', []); }
function saveWorldbooks(list) { store.set('chobun_worldbooks', list); }
function getMessages(roleId) { return store.get('chobun_msgs_' + roleId, []); }
function saveMessages(roleId, msgs) { store.set('chobun_msgs_' + roleId, msgs); }
function getUserProfile() { return store.get('chobun_user', { nickname: '我', avatar: '', chobun: '000000', region: '国内' }); }
function saveUserProfile(p) { store.set('chobun_user', p); }
function getPassword() { return store.get('chobun_password', ''); }
function setPassword(p) { store.set('chobun_password', p); }
function getMoments(roleId) { return store.get('chobun_moments_' + roleId, []); }
function saveMoments(roleId, list) { store.set('chobun_moments_' + roleId, list); }
/* ============================================================
* 页面路由
* ============================================================ */
function goToPage(pageId) {
if (currentPage === pageId) return;
const oldPage = document.getElementById(currentPage);
const newPage = document.getElementById(pageId);
if (!newPage) return;
if (oldPage) {
oldPage.classList.remove('active');
setTimeout(() => oldPage.classList.add('hidden'), 600);
}
newPage.classList.remove('hidden');
newPage.classList.add('active');
currentPage = pageId;
if (pageId === 'page-chat-app') renderChatApp();
else if (pageId === 'page-worldbook') renderWorldbookList();
else if (pageId === 'page-settings') renderSettings();
else if (pageId === 'page-role-profile') renderRoleProfile();
}
/* ============================================================
* Toast & Modal
* ============================================================ */
function showToast(msg, duration = 1800) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toast._timer);
toast._timer = setTimeout(() => toast.classList.remove('show'), duration);
}
function showModal(text, buttons) {
const overlay = document.getElementById('modalOverlay');
document.getElementById('modalText').textContent = text;
const actions = document.getElementById('modalActions');
actions.innerHTML = '';
buttons.forEach(btn => {
const b = document.createElement('button');
b.className = 'btn ' + (btn.className || 'btn-primary');
b.textContent = btn.text;
b.addEventListener('click', () => {
hideModal();
if (btn.onClick) btn.onClick();
});
actions.appendChild(b);
});
overlay.classList.add('show');
}
function hideModal() { document.getElementById('modalOverlay').classList.remove('show'); }
/* ============================================================
* 时间
* ============================================================ */
function updateTime() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
document.getElementById('statusTime').textContent = h + ':' + m;
}
setInterval(updateTime, 1000);
updateTime();
/* ============================================================
* 事件绑定(全部用 addEventListener,确保能点击)
* ============================================================ */
function bindEvents() {
// 准入页
document.getElementById('btnAgeConfirm').addEventListener('click', () => {
goToPage('page-login');
});
// 登录页
document.getElementById('btnEmailLogin').addEventListener('click', () => {
const email = document.getElementById('loginEmail').value.trim();
const code = document.getElementById('loginCode').value.trim();
if (!email) { showToast('请输入邮箱'); return; }
if (!code) { showToast('请输入验证码'); return; }
isGuest = false;
store.set('chobun_logged_in', true);
checkPasswordAndEnter();
});
document.getElementById('btnGuestLogin').addEventListener('click', () => {
isGuest = true;
store.set('chobun_logged_in', true);
showModal('欢迎使用巧司兔Chobun小手机【测试版】', [
{ text: '下一步', className: 'btn-primary', onClick: () => {
goToPage('page-password');
initPasswordDots();
}}
]);
});
// 密码页
document.getElementById('btnConfirmPassword').addEventListener('click', confirmPassword);
document.getElementById('btnSkipPassword').addEventListener('click', () => {
showToast('已跳过密码设置');
setTimeout(() => goToPage('page-desktop'), 600);
});
// 桌面图标
document.querySelectorAll('.app-icon').forEach(el => {
el.addEventListener('click', () => {
const app = el.dataset.app;
if (app === 'chat') { currentChatTab = 'chat'; goToPage('page-chat-app'); }
else if (app === 'settings') { goToPage('page-settings'); }
else if (app === 'worldbook') { goToPage('page-worldbook'); }
else if (app === 'profile') { currentChatTab = 'me'; goToPage('page-chat-app'); }
});
});
// 底部导航
document.querySelectorAll('#chatTabBar .tab-item').forEach(el => {
el.addEventListener('click', () => {
currentChatTab = el.dataset.tab;
renderChatApp();
});
});
// 世界书新建
document.getElementById('btnNewWorldbook').addEventListener('click', () => openWorldbookEditor());
// 设置保存
document.getElementById('btnSaveSettings').addEventListener('click', saveSettings);
// 角色资料页按钮
document.getElementById('btnOpenRoleMoments').addEventListener('click', () => {
if (currentRoleId) { openRoleMoments(currentRoleId); goToPage('page-chat-app'); }
});
document.getElementById('btnEditCurrentRole').addEventListener('click', () => {
if (currentRoleId) { openRoleEditor(currentRoleId); goToPage('page-chat-app'); }
});
document.getElementById('btnBackToChat').addEventListener('click', () => {
goToPage('page-chat-app');
});
// 底部交互栏
document.getElementById('btnHome').addEventListener('click', () => {
if (currentPage !== 'page-desktop') goToPage('page-desktop');
});
document.getElementById('btnExit').addEventListener('click', () => {
goToPage('page-login');
});
}
/* ============================================================
* 密码逻辑
* ============================================================ */
function checkPasswordAndEnter() {
const savedPwd = getPassword();
if (savedPwd) {
document.getElementById('passwordTitle').textContent = '请输入密码解锁';
goToPage('page-password');
initPasswordDots();
} else {
document.getElementById('passwordTitle').textContent = '请设置你的密码';
goToPage('page-password');
initPasswordDots();
}
}
function initPasswordDots() {
const container = document.getElementById('passwordDots');
container.innerHTML = '';
passwordInput = '';
for (let i = 0; i < 6; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
container.appendChild(dot);
}
updatePasswordDots();
}
function updatePasswordDots() {
const dots = document.querySelectorAll('#passwordDots .dot');
dots.forEach((dot, i) => {
if (i < passwordInput.length) {
dot.classList.add('filled');
dot.textContent = '●';
} else {
dot.classList.remove('filled');
dot.textContent = '';
}
});
}
document.addEventListener('keydown', (e) => {
if (currentPage !== 'page-password') return;
if (document.getElementById('modalOverlay').classList.contains('show')) return;
if (e.key >= '0' && e.key <= '9') {
if (passwordInput.length < 6) { passwordInput += e.key; updatePasswordDots(); }
e.preventDefault();
} else if (e.key === 'Backspace') {
passwordInput = passwordInput.slice(0, -1);
updatePasswordDots();
e.preventDefault();
}
});
function confirmPassword() {
const savedPwd = getPassword();
if (savedPwd) {
if (passwordInput === savedPwd) {
showToast('解锁成功');
setTimeout(() => goToPage('page-desktop'), 600);
} else {
showToast('密码错误,请重试');
passwordInput = '';
updatePasswordDots();
}
return;
}
if (passwordInput.length < 6) { showToast('请设置6位密码'); return; }
pendingPassword = passwordInput;
showModal(`你确定 ${pendingPassword} 为你的密码吗?`, [
{ text: '确定', className: 'btn-primary', onClick: () => {
setPassword(pendingPassword);
showToast('保存密码成功');
setTimeout(() => goToPage('page-desktop'), 1200);
}},
{ text: '取消', className: 'btn-outline', onClick: () => {
goToPage('page-password');
initPasswordDots();
}}
]);
}
/* ============================================================
* 聊天软件渲染
* ============================================================ */
function renderChatApp() {
const container = document.getElementById('chatAppContent');
if (currentChatTab === 'chat') renderChatList(container);
else if (currentChatTab === 'contacts') renderContacts(container);
else if (currentChatTab === 'moments') renderMomentsList(container);
else if (currentChatTab === 'me') renderMe(container);
document.querySelectorAll('#chatTabBar .tab-item').forEach(el => {
el.classList.toggle('active', el.dataset.tab === currentChatTab);
});
}
function renderChatList(container) {
const roles = getRoles();
let html = `
<div class="flex-between mb-10">
<h2 style="font-size:18px;color:#5d4a4a;">聊天</h2>
<button class="btn btn-primary btn-sm" id="btnCreateRole">+ 创建角色</button>
</div>
`;
if (roles.length === 0) {
html += `<p class="text-muted text-center" style="margin-top:40px;">暂时还没有角色,快去创建吧</p>`;
} else {
roles.forEach(r => {
const msgs = getMessages(r.id);
const last = msgs.length > 0 ? msgs[msgs.length-1].content : '暂无消息';
html += `
<div class="chat-list-item" data-role-id="${r.id}">
<div class="avatar">${r.avatar ? `<img src="${r.avatar}">` : '🐰'}</div>
<div class="info">
<div class="name">${r.remark || r.nickname}</div>
<div class="last-msg">${last}</div>
</div>
</div>
`;
});
}
container.innerHTML = html;
// 绑定创建按钮
const btnCreate = document.getElementById('btnCreateRole');
if (btnCreate) btnCreate.addEventListener('click', () => openRoleEditor());
// 绑定角色点击
container.querySelectorAll('.chat-list-item').forEach(el => {
el.addEventListener('click', () => {
currentChatRoleId = el.dataset.roleId;
renderChatWindow();
});
});
}
function renderChatWindow() {
const container = document.getElementById('chatAppContent');
const role = getRoles().find(r => r.id === currentChatRoleId);
if (!role) { renderChatList(container); return; }
const msgs = getMessages(currentChatRoleId);
let html = `
<div class="flex-between mb-10">
<button class="btn btn-outline btn-sm" id="btnBackToList">← 返回</button>
<span style="font-size:16px;color:#5d4a4a;font-weight:500;">${role.remark || role.nickname}</span>
<span style="width:60px;"></span>
</div>
<div class="chat-container" id="chatMessages">
`;
msgs.forEach(m => {
const isUser = m.role === 'user';
const avatar = isUser ? (getUserProfile().avatar || '👤') : (role.avatar || '🐰');
html += `
<div class="message-row ${isUser ? 'user' : 'ai'}">
<div class="msg-avatar" data-role-id="${!isUser ? role.id : ''}">
${avatar.startsWith('data:') ? `<img src="${avatar}">` : avatar}
</div>
<div class="msg-bubble">${m.content}</div>
</div>
`;
});
html += `</div>`;
html += `
<div style="display:flex;gap:8px;margin-top:12px;padding:8px 0;border-top:1px solid #f0dce0;">
<input class="input-field" id="chatInput" placeholder="输入消息..." style="margin-bottom:0;flex:1;" />
<button class="btn btn-primary btn-sm" id="btnSendMsg" style="width:auto;padding:10px 20px;">发送</button>
</div>
`;
container.innerHTML = html;
// 绑定返回
document.getElementById('btnBackToList').addEventListener('click', () => renderChatApp());
// 绑定发送
document.getElementById('btnSendMsg').addEventListener('click', sendChatMessage);
const input = document.getElementById('chatInput');
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChatMessage(); });
input.focus();
// 绑定头像点击
container.querySelectorAll('.msg-avatar').forEach(el => {
const rid = el.dataset.roleId;
if (rid) {
el.addEventListener('click', () => openRoleProfile(rid));
}
});
// 滚动到底部
const msgContainer = document.getElementById('chatMessages');
if (msgContainer) msgContainer.scrollTop = msgContainer.scrollHeight;
}
async function sendChatMessage() {
const input = document.getElementById('chatInput');
const text = input.value.trim();
if (!text || !currentChatRoleId) return;
input.value = '';
const msgs = getMessages(currentChatRoleId);
msgs.push({ role: 'user', content: text });
if (msgs.length > CONFIG.MAX_CONTEXT) msgs.splice(0, msgs.length - CONFIG.MAX_CONTEXT);
saveMessages(currentChatRoleId, msgs);
renderChatWindow();
// 输入中提示
const container = document.getElementById('chatMessages');
if (container) {
const typing = document.createElement('div');
typing.className = 'message-row ai';
typing.id = 'typingIndicator';
typing.innerHTML = `<div class="msg-avatar">🐰</div><div class="msg-bubble">正在输入...</div>`;
container.appendChild(typing);
container.scrollTop = container.scrollHeight;
}
const role = getRoles().find(r => r.id === currentChatRoleId);
const reply = await callDeepSeek(text, role);
const typingEl = document.getElementById('typingIndicator');
if (typingEl) typingEl.remove();
const updatedMsgs = getMessages(currentChatRoleId);
updatedMsgs.push({ role: 'assistant', content: reply });
if (updatedMsgs.length > CONFIG.MAX_CONTEXT) updatedMsgs.splice(0, updatedMsgs.length - CONFIG.MAX_CONTEXT);
saveMessages(currentChatRoleId, updatedMsgs);
renderChatWindow();
}
async function callDeepSeek(userText, role) {
const settings = store.get('chobun_settings', {});
const apiUrl = settings.apiUrl || CONFIG.API_URL;
const apiKey = settings.apiKey || CONFIG.API_KEY;
let persona = role.persona || CONFIG.DEFAULT_PERSONA;
persona = persona.replace(/{{char}}/g, role.nickname || '角色').replace(/{{user}}/g, getUserProfile().nickname || '我');
let worldContent = '';
if (role.worldbookId) {
const wb = getWorldbooks().find(w => w.id === role.worldbookId);
if (wb) worldContent = wb.content;
}
const systemPrompt = persona + (worldContent ? '\n\n背景世界书:' + worldContent : '');
const msgs = getMessages(currentChatRoleId);
const messages = [{ role: 'system', content: systemPrompt }, ...msgs.slice(-10)];
if (!apiUrl || !apiKey) {
await new Promise(r => setTimeout(r, 800));
const mockReplies = [
'*歪头看着你* 你说的我都记在心里啦~',
'*轻轻点头* 嗯嗯,然后呢?',
'*笑了笑* 你这样说,我很开心哦。',
'*眨眨眼* 真的吗?那下次我们一起去呀。',
'*认真思考* 这个问题……让我想想怎么回答你。'
];
return mockReplies[Math.floor(Math.random() * mockReplies.length)];
}
try {
const res = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
body: JSON.stringify({
model: settings.model || CONFIG.MODEL,
messages: messages,
temperature: parseFloat(settings.temp) || CONFIG.TEMPERATURE
})
});
const data = await res.json();
return data?.choices?.[0]?.message?.content || '(没有返回内容)';
} catch (err) {
console.error(err);
return '(网络出错了,请检查API配置)';
}
}
/* ============================================================
* 角色创建/编辑
* ============================================================ */
function openRoleEditor(roleId) {
const roles = getRoles();
let role = roleId ? roles.find(r => r.id === roleId) : null;
const isEdit = !!role;
if (!role) {
role = { id: 'r_' + Date.now(), nickname: '', chobun: '', gender: '女', region: '北京', persona: '', worldbookId: '', style: '', remark: '', avatar: '' };
}
window._tempAvatar = role.avatar || '';
const worldbooks = getWorldbooks();
const wbOptions = worldbooks.map(w => `<option value="${w.id}" ${role.worldbookId === w.id ? 'selected' : ''}>${w.title}</option>`).join('');
let html = `
<h3 style="font-size:17px;color:#5d4a4a;margin-bottom:14px;">${isEdit ? '编辑角色' : '创建角色'}</h3>
<div style="text-align:center;margin-bottom:14px;">
<div style="width:70px;height:70px;border-radius:50%;background:#f8a5b8;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;font-size:28px;color:#fff;overflow:hidden;" id="editorAvatar">
${role.avatar ? `<img src="${role.avatar}" style="width:100%;height:100%;object-fit:cover;">` : '🐰'}
</div>
<input type="file" id="avatarUpload" accept="image/*" style="display:none;" />
<button class="btn btn-outline btn-sm" id="btnUploadAvatar">上传头像</button>
</div>
<input class="input-field" id="editNickname" placeholder="角色昵称" value="${role.nickname || ''}" />
<input class="input-field" id="editChobun" placeholder="巧司号" value="${role.chobun || ''}" />
<select class="input-field" id="editGender">
<option value="女" ${role.gender === '女' ? 'selected' : ''}>女</option>
<option value="男" ${role.gender === '男' ? 'selected' : ''}>男</option>
</select>
<select class="input-field" id="editRegion">
<optgroup label="国内">
<option value="北京" ${role.region === '北京' ? 'selected' : ''}>北京</option>
<option value="上海" ${role.region === '上海' ? 'selected' : ''}>上海</option>
<option value="广州" ${role.region === '广州' ? 'selected' : ''}>广州</option>
<option value="深圳" ${role.region === '深圳' ? 'selected' : ''}>深圳</option>
<option value="杭州" ${role.region === '杭州' ? 'selected' : ''}>杭州</option>
<option value="成都" ${role.region === '成都' ? 'selected' : ''}>成都</option>
<option value="武汉" ${role.region === '武汉' ? 'selected' : ''}>武汉</option>
<option value="南京" ${role.region === '南京' ? 'selected' : ''}>南京</option>
<option value="西安" ${role.region === '西安' ? 'selected' : ''}>西安</option>
<option value="重庆" ${role.region === '重庆' ? 'selected' : ''}>重庆</option>
</optgroup>
<optgroup label="国外">
<option value="美国" ${role.region === '美国' ? 'selected' : ''}>美国</option>
<option value="英国" ${role.region === '英国' ? 'selected' : ''}>英国</option>
<option value="法国" ${role.region === '法国' ? 'selected' : ''}>法国</option>
<option value="冰岛" ${role.region === '冰岛' ? 'selected' : ''}>冰岛</option>
<option value="菲律宾" ${role.region === '菲律宾' ? 'selected' : ''}>菲律宾</option>
</optgroup>
</select>
<textarea class="input-field" id="editPersona" placeholder="角色性格人设" rows="4">${role.persona || ''}</textarea>
<select class="input-field" id="editWorldbook">
<option value="">不绑定世界书</option>
${wbOptions}
</select>
<input class="input-field" id="editStyle" placeholder="对话说话风格" value="${role.style || ''}" />
<input class="input-field" id="editRemark" placeholder="备注名 (可选)" value="${role.remark || ''}" />
<div style="display:flex;gap:10px;margin-top:10px;">
<button class="btn btn-primary" id="btnSaveRole">保存创建</button>
<button class="btn btn-outline" id="btnCancelRole">取消</button>
</div>
`;
document.getElementById('chatAppContent').innerHTML = html;
// 绑定头像上传
document.getElementById('btnUploadAvatar').addEventListener('click', () => document.getElementById('avatarUpload').click());
document.getElementById('avatarUpload').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
window._tempAvatar = ev.target.result;
document.getElementById('editorAvatar').innerHTML = `<img src="${ev.target.result}" style="width:100%;height:100%;object-fit:cover;">`;
};
reader.readAsDataURL(file);
});
// 保存
document.getElementById('btnSaveRole').addEventListener('click', () => saveRole(role.id));
// 取消
document.getElementById('btnCancelRole').addEventListener('click', () => renderChatApp());
}
function saveRole(roleId) {
const nickname = document.getElementById('editNickname').value.trim();
if (!nickname) { showToast('请输入角色昵称'); return; }
const roles = getRoles();
const idx = roles.findIndex(r => r.id === roleId);
const roleData = {
id: roleId,
nickname: nickname,
chobun: document.getElementById('editChobun').value.trim() || '000000',
gender: document.getElementById('editGender').value,
region: document.getElementById('editRegion').value,
persona: document.getElementById('editPersona').value.trim(),
worldbookId: document.getElementById('editWorldbook').value,
style: document.getElementById('editStyle').value.trim(),
remark: document.getElementById('editRemark').value.trim() || nickname,
avatar: window._tempAvatar || ''
};
if (idx >= 0) roles[idx] = roleData; else roles.push(roleData);
saveRoles(roles);
showToast('创建成功');
setTimeout(() => renderChatApp(), 1200);
}
/* ============================================================
* 通讯录 & 朋友圈
* ============================================================ */
function renderContacts(container) {
container.innerHTML = `
<h2 style="font-size:18px;color:#5d4a4a;margin-bottom:10px;">通讯录</h2>
<p class="text-muted text-center" style="margin-top:40px;">通讯录功能开发中,敬请期待</p>
`;
}
function renderMomentsList(container) {
const roles = getRoles();
let html = `<h2 style="font-size:18px;color:#5d4a4a;margin-bottom:10px;">朋友圈</h2>`;
if (roles.length === 0) {
html += `<p class="text-muted text-center" style="margin-top:40px;">还没有角色,先去创建吧</p>`;
} else {
roles.forEach(r => {
const moments = getMoments(r.id);
html += `
<div class="chat-list-item" data-moment-role="${r.id}">
<div class="avatar">${r.avatar ? `<img src="${r.avatar}">` : '🐰'}</div>
<div class="info">
<div class="name">${r.remark || r.nickname}</div>
<div class="last-msg">${moments.length > 0 ? moments[0].content : '他还没有发布朋友圈'}</div>
</div>
</div>
`;
});
}
html += `<div style="margin-top:16px;"><button class="btn btn-outline" id="btnPublishMyMoment">发布我的朋友圈</button></div>`;
container.innerHTML = html;
container.querySelectorAll('[data-moment-role]').forEach(el => {
el.addEventListener('click', () => {
const rid = el.dataset.momentRole;
openRoleMoments(rid);
});
});
document.getElementById('btnPublishMyMoment').addEventListener('click', publishMyMoment);
}
let currentMomentRoleId = null;
function openRoleMoments(roleId) {
currentMomentRoleId = roleId;
const role = getRoles().find(r => r.id === roleId);
if (!role) return;
const moments = getMoments(roleId);
let html = `
<div class="flex-between mb-10">
<button class="btn btn-outline btn-sm" id="btnBackMoments">← 返回</button>
<span style="font-size:16px;color:#5d4a4a;font-weight:500;">${role.remark || role.nickname} 的朋友圈</span>
<span style="width:60px;"></span>
</div>
<div style="text-align:center;margin-bottom:14px;">
<div style="width:60px;height:60px;border-radius:50%;background:#f8a5b8;margin:0 auto 6px;display:flex;align-items:center;justify-content:center;font-size:24px;color:#fff;overflow:hidden;">
${role.avatar ? `<img src="${role.avatar}" style="width:100%;height:100%;object-fit:cover;">` : '🐰'}
</div>
<p style="font-size:14px;color:#5d4a4a;">${role.remark || role.nickname}</p>
</div>
<div style="height:120px;background:#fce4ec;border-radius:14px;margin-bottom:14px;display:flex;align-items:center;justify-content:center;color:#c4a8ae;font-size:13px;cursor:pointer;" id="momentBg">
点击更换背景图
</div>
`;
if (moments.length === 0) {
html += `<p class="text-muted text-center" style="margin-top:20px;">他还没有发布朋友圈</p>`;
} else {
moments.forEach(m => {
html += `
<div class="card">
<p style="font-size:14px;color:#5d4a4a;">${m.content}</p>
<p style="font-size:11px;color:#c4a8ae;margin-top:6px;">${m.time || ''}</p>
</div>
`;
});
}
document.getElementById('chatAppContent').innerHTML = html;
document.getElementById('btnBackMoments').addEventListener('click', () => {
currentChatTab = 'moments';
renderChatApp();
});
document.getElementById('momentBg').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const roles = getRoles();
const r = roles.find(r => r.id === roleId);
if (r) {
r.momentBg = ev.target.result;
saveRoles(roles);
showToast('背景已更换');
openRoleMoments(roleId);
}
};
reader.readAsDataURL(file);
};
input.click();
});
}
/* ============================================================
* "我" 页面
* ============================================================ */
function renderMe(container) {
const user = getUserProfile();
let html = `
<h2 style="font-size:18px;color:#5d4a4a;margin-bottom:14px;">我</h2>
<div style="text-align:center;margin-bottom:14px;">
<div style="width:80px;height:80px;border-radius:50%;background:#f8a5b8;margin:0 auto 10px;display:flex;align-items:center;justify-content:center;font-size:32px;color:#fff;overflow:hidden;" id="meAvatar">
${user.avatar ? `<img src="${user.avatar}" style="width:100%;height:100%;object-fit:cover;">` : '👤'}
</div>
<input type="file" id="meAvatarUpload" accept="image/*" style="display:none;" />
<button class="btn btn-outline btn-sm" id="btnUploadMeAvatar">更换头像</button>
</div>
<input class="input-field" id="meNickname" placeholder="昵称" value="${user.nickname || ''}" />
<input class="input-field" id="meChobun" placeholder="巧司号" value="${user.chobun || ''}" />
<input class="input-field" id="meRegion" placeholder="地区" value="${user.region || ''}" />
<button class="btn btn-primary" id="btnSaveMe">保存资料</button>
<button class="btn btn-outline" id="btnPublishFromMe">发布朋友圈</button>
`;
container.innerHTML = html;
document.getElementById('btnUploadMeAvatar').addEventListener('click', () => document.getElementById('meAvatarUpload').click());
document.getElementById('meAvatarUpload').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const u = getUserProfile();
u.avatar = ev.target.result;
saveUserProfile(u);
renderMe(container);
showToast('头像已更新');
};
reader.readAsDataURL(file);
});
document.getElementById('btnSaveMe').addEventListener('click', () => {
const u = getUserProfile();
u.nickname = document.getElementById('meNickname').value.trim() || '我';
u.chobun = document.getElementById('meChobun').value.trim() || '000000';
u.region = document.getElementById('meRegion').value.trim() || '国内';
saveUserProfile(u);
showToast('资料已保存');
});
document.getElementById('btnPublishFromMe').addEventListener('click', publishMyMoment);
}
function publishMyMoment() {
const content = prompt('请输入朋友圈内容:');
if (!content) return;
const myId = 'user_me';
const moments = getMoments(myId);
moments.unshift({ content: content, time: new Date().toLocaleString() });
saveMoments(myId, moments);
showToast('发布成功');
}
/* ============================================================
* 角色资料页
* ============================================================ */
function openRoleProfile(roleId) {
currentRoleId = roleId;
goToPage('page-role-profile');
}
function renderRoleProfile() {
const role = getRoles().find(r => r.id === currentRoleId);
if (!role) return;
document.getElementById('profileAvatar').innerHTML = role.avatar ? `<img src="${role.avatar}" style="width:100%;height:100%;object-fit:cover;">` : '🐰';
document.getElementById('profileRemark').textContent = role.remark || role.nickname;
document.getElementById('profileOriginal').textContent = '昵称:' + role.nickname;
document.getElementById('profileChobun').textContent = '巧司号:' + (role.chobun || '000000');
document.getElementById('profileRegion').textContent = '地区:' + (role.region || '未知');
}
/* ============================================================
* 世界书
* ============================================================ */
function renderWorldbookList() {
const list = getWorldbooks();
const container = document.getElementById('worldbookList');
if (list.length === 0) {
container.innerHTML = `<p class="text-muted text-center">暂无世界书,点击新建</p>`;
return;
}
let html = '';
list.forEach(wb => {
html += `
<div class="worldbook-item">
<span class="title">${wb.title}</span>
<div class="actions">
<button class="btn btn-outline btn-sm" data-wb-edit="${wb.id}">编辑</button>
<button class="btn btn-primary btn-sm" data-wb-del="${wb.id}">删除</button>
</div>
</div>
`;
});
container.innerHTML = html;
container.querySelectorAll('[data-wb-edit]').forEach(el => {
el.addEventListener('click', () => openWorldbookEditor(el.dataset.wbEdit));
});
container.querySelectorAll('[data-wb-del]').forEach(el => {
el.addEventListener('click', () => deleteWorldbook(el.dataset.wbDel));
});
}
function openWorldbookEditor(id) {
const list = getWorldbooks();
const wb = id ? list.find(w => w.id === id) : { id: 'wb_' + Date.now(), title: '', content: '' };
const isEdit = !!id;
const container = document.getElementById('page-worldbook');
container.innerHTML = `
<div class="flex-between mb-10">
<h2 style="font-size:18px;color:#5d4a4a;">${isEdit ? '编辑世界书' : '新建世界书'}</h2>
<button class="btn btn-outline btn-sm" id="btnBackWb">返回</button>
</div>
<input class="input-field" id="wbTitle" placeholder="世界书标题" value="${wb.title}" />
<textarea class="input-field" id="wbContent" placeholder="世界书内容" rows="8">${wb.content}</textarea>
<button class="btn btn-primary" id="btnSaveWb">保存</button>
`;
document.getElementById('btnBackWb').addEventListener('click', () => { goToPage('page-worldbook'); renderWorldbookList(); });
document.getElementById('btnSaveWb').addEventListener('click', () => saveWorldbook(wb.id));
}
function saveWorldbook(id) {
const title = document.getElementById('wbTitle').value.trim();
const content = document.getElementById('wbContent').value.trim();
if (!title) { showToast('请输入标题'); return; }
const list = getWorldbooks();
const idx = list.findIndex(w => w.id === id);
const data = { id, title, content };
if (idx >= 0) list[idx] = data; else list.push(data);
saveWorldbooks(list);
showToast('保存成功');
setTimeout(() => { goToPage('page-worldbook'); renderWorldbookList(); }, 800);
}
function deleteWorldbook(id) {
showModal('确定删除这本世界书吗?', [
{ text: '确定', className: 'btn-primary', onClick: () => {
const list = getWorldbooks().filter(w => w.id !== id);
saveWorldbooks(list);
renderWorldbookList();
showToast('已删除');
}},
{ text: '取消', className: 'btn-outline' }
]);
}
/* ============================================================
* 设置
* ============================================================ */
function renderSettings() {
const settings = store.get('chobun_settings', {});
document.getElementById('setApiUrl').value = settings.apiUrl || '';
document.getElementById('setApiKey').value = settings.apiKey || '';
document.getElementById('setModel').value = settings.model || CONFIG.MODEL;
document.getElementById('setTemp').value = settings.temp || CONFIG.TEMPERATURE;
const roles = getRoles();
const container = document.getElementById('settingsRoleList');
if (roles.length === 0) {
container.innerHTML = `<p class="text-muted">暂无角色</p>`;
} else {
let html = '';
roles.forEach(r => {
html += `
<div class="worldbook-item">
<span class="title">${r.nickname}</span>
<div class="actions">
<button class="btn btn-outline btn-sm" data-edit-persona="${r.id}">编辑人设</button>
</div>
</div>
`;
});
container.innerHTML = html;
container.querySelectorAll('[data-edit-persona]').forEach(el => {
el.addEventListener('click', () => editRolePersona(el.dataset.editPersona));
});
}
}
function saveSettings() {
const settings = {
apiUrl: document.getElementById('setApiUrl').value.trim(),
apiKey: document.getElementById('setApiKey').value.trim(),
model: document.getElementById('setModel').value.trim() || CONFIG.MODEL,
temp: document.getElementById('setTemp').value || CONFIG.TEMPERATURE
};
store.set('chobun_settings', settings);
showToast('设置已保存');
}
function editRolePersona(roleId) {
const role = getRoles().find(r => r.id === roleId);
if (!role) return;
const newPersona = prompt('编辑角色人设:', role.persona || '');
if (newPersona !== null) {
role.persona = newPersona;
const roles = getRoles();
const idx = roles.findIndex(r => r.id === roleId);
if (idx >= 0) roles[idx] = role;
saveRoles(roles);
showToast('人设已更新');
}
}
/* ============================================================
* 启动
* ============================================================ */
window.onload = function() {
bindEvents();
const loggedIn = store.get('chobun_logged_in', false);
const savedPwd = getPassword();
if (loggedIn && savedPwd) {
document.getElementById('passwordTitle').textContent = '请输入密码解锁';
goToPage('page-password');
initPasswordDots();
} else if (loggedIn && !savedPwd) {
goToPage('page-desktop');
} else {
goToPage('page-age');
}
renderWorldbookList();
};
console.log('巧司兔Chobun小手机 测试版 已启动');
</script>
</body>
</html>