@@ -494,6 +494,28 @@ function renderChatsPage() {
</div>
</div>
<!-- 操作菜单 -->
<div class="action-menu" id="actionMenu">
<div class="action-menu-content">
<div class="action-menu-item" data-action="rename">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
<span>重命名</span>
</div>
<div class="action-menu-item" data-action="share">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.35 2.92 3 2.92s3-1.31 3-2.92c0-1.61-1.35-2.92-3-2.92z"/></svg>
<span>分享</span>
</div>
<div class="action-menu-item" data-action="pin">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/></svg>
<span id="pinText">置顶</span>
</div>
<div class="action-menu-item delete-action" data-action="delete">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
<span>删除</span>
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar" id="searchBar">
<div class="search-input-wrapper">
@@ -592,6 +614,26 @@ function bindChatsPageEvents() {
// 长按事件
setupLongPressEvents ( conversationList ) ;
}
// 操作菜单事件
const actionMenu = document . getElementById ( 'actionMenu' ) ;
if ( actionMenu ) {
actionMenu . addEventListener ( 'click' , ( e ) => {
const item = e . target . closest ( '.action-menu-item' ) ;
if ( item && currentActionConvId ) {
const action = item . getAttribute ( 'data-action' ) ;
handleActionMenuAction ( action , currentActionConvId ) ;
hideActionMenu ( ) ;
}
} ) ;
// 点击其他地方关闭菜单
document . addEventListener ( 'click' , ( e ) => {
if ( actionMenu . classList . contains ( 'show' ) && ! actionMenu . contains ( e . target ) ) {
hideActionMenu ( ) ;
}
} ) ;
}
}
// ==================== 智能体页面 ====================
@@ -1349,7 +1391,7 @@ function renderProfilePage() {
</div>
<div class="profile-footer">
<p>AI助手 v3.3 .0</p>
<p>AI助手 v3.4 .0</p>
<p>基于智谱 GLM-4.5-Air</p>
</div>
</div>
@@ -1518,6 +1560,21 @@ function showRegisterPage() {
<label>用户名</label>
<input type="text" id="registerUsername" placeholder="请输入用户名( 3-20字符) " autocomplete="username">
</div>
<div class="auth-input-group">
<label>手机号 <span class="required">*</span></label>
<div class="phone-input-wrapper">
<input type="tel" id="registerPhone" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
<button class="send-code-btn" id="sendCodeBtn">获取验证码</button>
</div>
</div>
<div class="auth-input-group">
<label>验证码 <span class="required">*</span></label>
<input type="text" id="registerCode" placeholder="请输入验证码" maxlength="6" autocomplete="one-time-code">
</div>
<div class="auth-input-group">
<label>邮箱 <span class="optional">(可选)</span></label>
<input type="email" id="registerEmail" placeholder="请输入邮箱(可选)" autocomplete="email">
</div>
<div class="auth-input-group">
<label>密码</label>
<input type="password" id="registerPassword" placeholder="请输入密码( 6-20字符) " autocomplete="new-password">
@@ -1558,22 +1615,139 @@ function showRegisterPage() {
} ) ;
}
// 发送验证码按钮
const sendCodeBtn = document . getElementById ( 'sendCodeBtn' ) ;
if ( sendCodeBtn ) {
sendCodeBtn . addEventListener ( 'click' , handleSendCode ) ;
}
// 回车注册
document . getElementById ( 'registerPasswordConfirm' ) ? . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Enter' ) handleRegister ( ) ;
} ) ;
// 手机号输入限制(只允许数字)
const phoneInput = document . getElementById ( 'registerPhone' ) ;
if ( phoneInput ) {
phoneInput . addEventListener ( 'input' , ( e ) => {
e . target . value = e . target . value . replace ( /\D/g , '' ) ;
} ) ;
}
}
// 验证码状态
let verifyCode = null ;
let verifyCodePhone = null ;
let verifyCodeExpire = null ;
// 发送验证码(模拟)
function handleSendCode ( ) {
const phoneInput = document . getElementById ( 'registerPhone' ) ;
const phone = phoneInput ? . value . trim ( ) ;
const sendCodeBtn = document . getElementById ( 'sendCodeBtn' ) ;
// 验证手机号格式
if ( ! validatePhone ( phone ) ) {
showToast ( '请输入正确的手机号' ) ;
return ;
}
// 检查手机号是否已注册
const users = JSON . parse ( localStorage . getItem ( 'registeredUsers' ) || '[]' ) ;
if ( users . find ( u => u . phone === phone ) ) {
showToast ( '该手机号已注册' ) ;
return ;
}
// 生成6位验证码
const code = Math . floor ( 100000 + Math . random ( ) * 900000 ) . toString ( ) ;
verifyCode = code ;
verifyCodePhone = phone ;
verifyCodeExpire = Date . now ( ) + 5 * 60 * 1000 ; // 5分钟有效期
// 显示验证码(模拟,实际应发送短信)
console . log ( '验证码:' , code ) ; // 调试用
showToast ( ` 验证码已发送: ${ code } ` ) ; // 实际项目中应隐藏
// 禁用按钮60秒
if ( sendCodeBtn ) {
sendCodeBtn . disabled = true ;
let countdown = 60 ;
const timer = setInterval ( ( ) => {
countdown -- ;
if ( countdown > 0 ) {
sendCodeBtn . textContent = ` ${ countdown } s ` ;
} else {
clearInterval ( timer ) ;
sendCodeBtn . disabled = false ;
sendCodeBtn . textContent = '获取验证码' ;
}
} , 1000 ) ;
}
}
// 验证手机号格式
function validatePhone ( phone ) {
// 中国手机号: 11位, 以1开头
const phoneRegex = /^1[3-9]\d{9}$/ ;
return phoneRegex . test ( phone ) ;
}
// 验证邮箱格式
function validateEmail ( email ) {
if ( ! email ) return true ; // 可选,空值有效
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ ;
return emailRegex . test ( email ) ;
}
function handleRegister ( ) {
const username = document . getElementById ( 'registerUsername' ) ? . value . trim ( ) ;
const phone = document . getElementById ( 'registerPhone' ) ? . value . trim ( ) ;
const code = document . getElementById ( 'registerCode' ) ? . value . trim ( ) ;
const email = document . getElementById ( 'registerEmail' ) ? . value . trim ( ) ;
const password = document . getElementById ( 'registerPassword' ) ? . value ;
const passwordConfirm = document . getElementById ( 'registerPasswordConfirm' ) ? . value ;
// 验证用户名
if ( ! username || username . length < 3 || username . length > 20 ) {
showToast ( '用户名需要3-20个字符' ) ;
return ;
}
// 验证手机号
if ( ! validatePhone ( phone ) ) {
showToast ( '请输入正确的手机号' ) ;
return ;
}
// 验证验证码
if ( ! code ) {
showToast ( '请输入验证码' ) ;
return ;
}
if ( ! verifyCode || verifyCodePhone !== phone ) {
showToast ( '请先获取验证码' ) ;
return ;
}
if ( code !== verifyCode ) {
showToast ( '验证码错误' ) ;
return ;
}
if ( Date . now ( ) > verifyCodeExpire ) {
showToast ( '验证码已过期,请重新获取' ) ;
return ;
}
// 验证邮箱(可选)
if ( ! validateEmail ( email ) ) {
showToast ( '请输入正确的邮箱格式' ) ;
return ;
}
// 验证密码
if ( ! password || password . length < 6 || password . length > 20 ) {
showToast ( '密码需要6-20个字符' ) ;
return ;
@@ -1591,9 +1765,17 @@ function handleRegister() {
return ;
}
// 检查手机号是否已注册
if ( users . find ( u => u . phone === phone ) ) {
showToast ( '该手机号已注册' ) ;
return ;
}
// 注册新用户
const newUser = {
username ,
phone ,
email : email || '' ,
password ,
registeredAt : Date . now ( )
} ;
@@ -1601,8 +1783,13 @@ function handleRegister() {
users . push ( newUser ) ;
localStorage . setItem ( 'registeredUsers' , JSON . stringify ( users ) ) ;
// 清除验证码
verifyCode = null ;
verifyCodePhone = null ;
verifyCodeExpire = null ;
// 自动登录
currentUser = { username : newUser . username , registeredAt : newUser . registeredAt } ;
currentUser = { username : newUser . username , phone : newUser . phone , registeredAt : newUser . registeredAt } ;
saveCurrentUser ( ) ;
showToast ( '注册成功' ) ;
@@ -1983,9 +2170,11 @@ function showAgentChatPage() {
}
// 设置长按事件
// 全局变量: 当前操作的对话ID
let currentActionConvId = null ;
function setupLongPressEvents ( container ) {
let longPressTimer = null ;
let currentActionConvId = null ;
container . addEventListener ( 'touchstart' , ( e ) => {
const item = e . target . closest ( '.conversation-item' ) ;
@@ -2038,236 +2227,7 @@ function setupLongPressEvents(container) {
function showConversationList ( ) {
currentConversation = null ;
// 渲染对话列表
const listHtml = `
<div class="conversation-list-page">
<header class="list-header">
<div class="header-title">
<span class="logo">🤖</span>
<h1>AI助手</h1>
</div>
<div class="header-actions">
<button class="header-btn search-toggle-btn" id="searchToggleBtn" title="搜索">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
</button>
<button class="header-btn new-chat-btn-header" id="newChatBtn" title="新建对话">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
</div>
</header>
<div class="list-content">
<div class="conversation-list" id="conversationList">
${ conversations . length === 0
? '<div class="empty-list">暂无对话记录</div>'
: sortConversations ( ) . map ( conv => `
<div class="conversation-item ${ conv . is _pinned ? 'pinned' : '' } " data-id=" ${ conv . id } ">
${ conv . is _pinned ? '<span class="pin-icon">📌</span>' : '' }
<div class="conv-title"> ${ escapeHtml ( conv . title ) } </div>
<div class="conv-meta"> ${ conv . messages . length } 条消息 · ${ formatTime ( conv . updatedAt ) } </div>
</div>
` ) . join ( '' )
}
</div>
</div>
<!-- 操作菜单 -->
<div class="action-menu" id="actionMenu">
<div class="action-menu-content">
<div class="action-menu-item" data-action="rename">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
<span>重命名</span>
</div>
<div class="action-menu-item" data-action="share">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.35 2.92 3 2.92s3-1.31 3-2.92c0-1.61-1.35-2.92-3-2.92z"/></svg>
<span>分享</span>
</div>
<div class="action-menu-item" data-action="pin">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/></svg>
<span id="pinText">置顶</span>
</div>
<div class="action-menu-item delete-action" data-action="delete">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
<span>删除</span>
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar" id="searchBar">
<div class="search-input-wrapper">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="searchInput" placeholder="搜索对话标题或内容...">
<button class="search-close-btn" id="searchCloseBtn">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
</button>
</div>
<div class="search-results" id="searchResults"></div>
</div>
</div>
</div>
` ;
appContainer . innerHTML = listHtml ;
// 绑定事件
const newChatBtn = document . getElementById ( 'newChatBtn' ) ;
if ( newChatBtn ) {
newChatBtn . addEventListener ( 'click' , createNewConversation ) ;
}
// 搜索功能
const searchToggleBtn = document . getElementById ( 'searchToggleBtn' ) ;
const searchBar = document . getElementById ( 'searchBar' ) ;
const searchInput = document . getElementById ( 'searchInput' ) ;
const searchCloseBtn = document . getElementById ( 'searchCloseBtn' ) ;
const searchResults = document . getElementById ( 'searchResults' ) ;
if ( searchToggleBtn ) {
searchToggleBtn . addEventListener ( 'click' , ( ) => {
if ( searchBar ) {
searchBar . classList . add ( 'show' ) ;
if ( searchInput ) {
searchInput . focus ( ) ;
}
}
} ) ;
}
if ( searchCloseBtn ) {
searchCloseBtn . addEventListener ( 'click' , ( ) => {
hideSearchBar ( ) ;
} ) ;
}
if ( searchInput ) {
searchInput . addEventListener ( 'input' , ( e ) => {
const keyword = e . target . value . trim ( ) ;
if ( keyword ) {
searchConversations ( keyword ) ;
} else {
if ( searchResults ) searchResults . innerHTML = '' ;
}
} ) ;
searchInput . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Escape' ) {
hideSearchBar ( ) ;
}
} ) ;
}
// 点击搜索结果
if ( searchResults ) {
searchResults . addEventListener ( 'click' , ( e ) => {
const item = e . target . closest ( '.search-result-item' ) ;
if ( item ) {
const id = item . getAttribute ( 'data-id' ) ;
hideSearchBar ( ) ;
openConversation ( id ) ;
}
} ) ;
}
function hideSearchBar ( ) {
if ( searchBar ) {
searchBar . classList . remove ( 'show' ) ;
}
if ( searchInput ) {
searchInput . value = '' ;
}
if ( searchResults ) {
searchResults . innerHTML = '' ;
}
}
const conversationList = document . getElementById ( 'conversationList' ) ;
const actionMenu = document . getElementById ( 'actionMenu' ) ;
let longPressTimer = null ;
let currentActionConvId = null ;
if ( conversationList ) {
// 点击事件
conversationList . addEventListener ( 'click' , ( e ) => {
const item = e . target . closest ( '.conversation-item' ) ;
if ( item ) {
const id = item . getAttribute ( 'data-id' ) ;
openConversation ( id ) ;
}
} ) ;
// 长按事件
conversationList . addEventListener ( 'touchstart' , ( e ) => {
const item = e . target . closest ( '.conversation-item' ) ;
if ( item ) {
longPressTimer = setTimeout ( ( ) => {
currentActionConvId = item . getAttribute ( 'data-id' ) ;
showActionMenu ( currentActionConvId ) ;
} , 500 ) ; // 500ms长按
}
} ) ;
conversationList . addEventListener ( 'touchend' , ( ) => {
if ( longPressTimer ) {
clearTimeout ( longPressTimer ) ;
longPressTimer = null ;
}
} ) ;
conversationList . addEventListener ( 'touchmove' , ( ) => {
if ( longPressTimer ) {
clearTimeout ( longPressTimer ) ;
longPressTimer = null ;
}
} ) ;
// 鼠标长按( PC端)
conversationList . addEventListener ( 'mousedown' , ( e ) => {
const item = e . target . closest ( '.conversation-item' ) ;
if ( item ) {
longPressTimer = setTimeout ( ( ) => {
currentActionConvId = item . getAttribute ( 'data-id' ) ;
showActionMenu ( currentActionConvId ) ;
} , 500 ) ;
}
} ) ;
conversationList . addEventListener ( 'mouseup' , ( ) => {
if ( longPressTimer ) {
clearTimeout ( longPressTimer ) ;
longPressTimer = null ;
}
} ) ;
conversationList . addEventListener ( 'mouseleave' , ( ) => {
if ( longPressTimer ) {
clearTimeout ( longPressTimer ) ;
longPressTimer = null ;
}
} ) ;
}
// 操作菜单事件
if ( actionMenu ) {
actionMenu . addEventListener ( 'click' , ( e ) => {
const item = e . target . closest ( '.action-menu-item' ) ;
if ( item && currentActionConvId ) {
const action = item . getAttribute ( 'data-action' ) ;
handleActionMenuAction ( action , currentActionConvId ) ;
hideActionMenu ( ) ;
}
} ) ;
// 点击其他地方关闭菜单
document . addEventListener ( 'click' , ( e ) => {
if ( actionMenu . classList . contains ( 'show' ) && ! actionMenu . contains ( e . target ) ) {
hideActionMenu ( ) ;
}
} ) ;
}
showMainPage ( ) ; // 返回主页,会正确显示过滤后的对话列表
}
// 排序对话(置顶在前)