Claw 项目完整结构提交

This commit is contained in:
zqm
2026-03-16 15:47:55 +08:00
parent ca4970bcbf
commit fb0aeb6ca2
118 changed files with 28648 additions and 281 deletions

View File

@@ -0,0 +1,436 @@
// 聊天页面逻辑
const { API, WebSocketManager, util, constants } = require('../../utils/api.js')
const { formatRelativeTime } = util
const { MESSAGE_TYPE, WS_MESSAGE_TYPE } = constants
Page({
data: {
messages: [],
inputValue: '',
sending: false,
websocketManager: null,
lastMessageId: '',
isConnected: false
},
onLoad() {
this.initWebSocket()
this.loadChatHistory()
},
onUnload() {
if (this.data.websocketManager) {
this.data.websocketManager.disconnect()
}
},
// 初始化WebSocket连接
initWebSocket() {
const manager = new WebSocketManager()
const app = getApp()
manager.onMessage(WS_MESSAGE_TYPE.MESSAGE, (data) => {
this.handleNewMessage(data)
})
manager.onMessage(WS_MESSAGE_TYPE.SYSTEM, (data) => {
this.handleSystemMessage(data)
})
manager.connect(app.globalData.websocketUrl)
this.setData({
websocketManager: manager,
isConnected: true
})
},
// 加载聊天记录
async loadChatHistory() {
try {
const result = await API.getChatHistory(1, 50)
if (result.success && result.data) {
const messages = result.data.messages.map(msg => ({
id: msg.id,
content: msg.content,
type: msg.type || MESSAGE_TYPE.TEXT,
isMe: msg.isMe || false,
nickname: msg.nickname || 'AI助手',
avatar: msg.avatar || '/assets/images/ai-avatar.png',
time: formatRelativeTime(new Date(msg.timestamp))
}))
this.setData({
messages: messages.reverse()
})
this.scrollToBottom()
}
} catch (error) {
console.error('加载聊天记录失败:', error)
}
},
// 处理新消息
handleNewMessage(data) {
const message = {
id: data.id || util.generateUniqueId(),
content: data.content,
type: data.type || MESSAGE_TYPE.TEXT,
isMe: false,
nickname: data.nickname || 'AI助手',
avatar: data.avatar || '/assets/images/ai-avatar.png',
time: formatRelativeTime(new Date())
}
this.addMessage(message)
},
// 处理系统消息
handleSystemMessage(data) {
const message = {
id: data.id || util.generateUniqueId(),
content: data.content,
type: MESSAGE_TYPE.SYSTEM,
isMe: false,
nickname: '系统',
avatar: '/assets/images/system-avatar.png',
time: formatRelativeTime(new Date())
}
this.addMessage(message)
},
// 添加消息
addMessage(message) {
const messages = [...this.data.messages, message]
this.setData({
messages: messages,
lastMessageId: message.id
})
this.scrollToBottom()
},
// 滚动到底部
scrollToBottom() {
if (this.data.messages.length > 0) {
const lastMessage = this.data.messages[this.data.messages.length - 1]
this.setData({
lastMessageId: lastMessage.id
})
}
},
// 输入变化
onInputChange(e) {
this.setData({
inputValue: e.detail.value
})
},
// 发送消息
async sendMessage() {
const content = this.data.inputValue.trim()
if (!content || this.data.sending) {
return
}
this.setData({
sending: true
})
// 添加用户消息到界面
const userMessage = {
id: util.generateUniqueId(),
content: content,
type: MESSAGE_TYPE.TEXT,
isMe: true,
nickname: '我',
avatar: '/assets/images/user-avatar.png',
time: formatRelativeTime(new Date())
}
this.addMessage(userMessage)
try {
// 通过WebSocket发送消息
if (this.data.websocketManager && this.data.isConnected) {
this.data.websocketManager.send({
type: WS_MESSAGE_TYPE.MESSAGE,
content: content,
messageType: MESSAGE_TYPE.TEXT,
timestamp: Date.now()
})
} else {
// 通过HTTP API发送消息
await API.sendMessage(content, MESSAGE_TYPE.TEXT)
}
this.setData({
inputValue: '',
sending: false
})
} catch (error) {
console.error('发送消息失败:', error)
util.showError('发送失败,请重试')
this.setData({
sending: false
})
}
},
// 选择图片
chooseImage() {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.uploadImage(res.tempFilePaths[0])
},
fail: (error) => {
console.error('选择图片失败:', error)
}
})
},
// 上传图片
async uploadImage(filePath) {
try {
util.showLoading('上传中...')
const result = await API.uploadFile(filePath, {
type: 'image',
messageType: MESSAGE_TYPE.IMAGE
})
util.hideLoading()
if (result.success) {
// 发送图片消息
const imageMessage = {
id: util.generateUniqueId(),
content: result.data.url,
type: MESSAGE_TYPE.IMAGE,
isMe: true,
nickname: '我',
avatar: '/assets/images/user-avatar.png',
time: formatRelativeTime(new Date())
}
this.addMessage(imageMessage)
// 通过WebSocket发送图片消息
if (this.data.websocketManager && this.data.isConnected) {
this.data.websocketManager.send({
type: WS_MESSAGE_TYPE.MESSAGE,
content: result.data.url,
messageType: MESSAGE_TYPE.IMAGE,
timestamp: Date.now()
})
}
}
} catch (error) {
util.hideLoading()
console.error('上传图片失败:', error)
util.showError('上传失败')
}
},
// 显示更多操作
showMoreActions() {
wx.showActionSheet({
itemList: ['发送文件', '语音输入', '清空聊天记录'],
success: (res) => {
switch (res.tapIndex) {
case 0:
this.chooseFile()
break
case 1:
this.startVoiceInput()
break
case 2:
this.clearChatHistory()
break
}
}
})
},
// 选择文件
chooseFile() {
wx.chooseMessageFile({
count: 1,
type: 'file',
success: (res) => {
this.uploadFile(res.tempFiles[0])
},
fail: (error) => {
console.error('选择文件失败:', error)
}
})
},
// 上传文件
async uploadFile(file) {
try {
util.showLoading('上传中...')
const result = await API.uploadFile(file.path, {
type: 'file',
messageType: MESSAGE_TYPE.FILE,
fileName: file.name,
fileSize: file.size
})
util.hideLoading()
if (result.success) {
// 发送文件消息
const fileMessage = {
id: util.generateUniqueId(),
content: result.data.url,
type: MESSAGE_TYPE.FILE,
isMe: true,
nickname: '我',
avatar: '/assets/images/user-avatar.png',
time: formatRelativeTime(new Date()),
fileName: file.name,
fileSize: util.formatFileSize(file.size)
}
this.addMessage(fileMessage)
// 通过WebSocket发送文件消息
if (this.data.websocketManager && this.data.isConnected) {
this.data.websocketManager.send({
type: WS_MESSAGE_TYPE.MESSAGE,
content: result.data.url,
messageType: MESSAGE_TYPE.FILE,
fileName: file.name,
fileSize: file.size,
timestamp: Date.now()
})
}
}
} catch (error) {
util.hideLoading()
console.error('上传文件失败:', error)
util.showError('上传失败')
}
},
// 开始语音输入
startVoiceInput() {
wx.showModal({
title: '语音输入',
content: '按住录音按钮开始录音',
showCancel: true,
confirmText: '开始录音',
success: (res) => {
if (res.confirm) {
this.startRecording()
}
}
})
},
// 开始录音
startRecording() {
const recorderManager = wx.getRecorderManager()
recorderManager.onStart(() => {
console.log('录音开始')
})
recorderManager.onStop((res) => {
console.log('录音结束', res)
if (res.tempFilePath) {
this.uploadAudio(res.tempFilePath)
}
})
recorderManager.start({
duration: 60000, // 最长60秒
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3'
})
// 5秒后自动停止录音
setTimeout(() => {
recorderManager.stop()
}, 5000)
},
// 上传音频文件
async uploadAudio(filePath) {
try {
util.showLoading('处理中...')
const result = await API.uploadFile(filePath, {
type: 'audio',
messageType: MESSAGE_TYPE.AUDIO
})
util.hideLoading()
if (result.success) {
// 发送音频消息
const audioMessage = {
id: util.generateUniqueId(),
content: result.data.url,
type: MESSAGE_TYPE.AUDIO,
isMe: true,
nickname: '我',
avatar: '/assets/images/user-avatar.png',
time: formatRelativeTime(new Date())
}
this.addMessage(audioMessage)
// 通过WebSocket发送音频消息
if (this.data.websocketManager && this.data.isConnected) {
this.data.websocketManager.send({
type: WS_MESSAGE_TYPE.MESSAGE,
content: result.data.url,
messageType: MESSAGE_TYPE.AUDIO,
timestamp: Date.now()
})
}
}
} catch (error) {
util.hideLoading()
console.error('上传音频失败:', error)
util.showError('处理失败')
}
},
// 清空聊天记录
clearChatHistory() {
wx.showModal({
title: '清空聊天记录',
content: '确定要清空所有聊天记录吗?此操作不可恢复。',
confirmText: '清空',
confirmColor: '#dd524d',
success: (res) => {
if (res.confirm) {
this.setData({
messages: [],
lastMessageId: ''
})
// 清空本地存储
wx.removeStorageSync('chatHistory')
wx.showToast({
title: '已清空',
icon: 'success'
})
}
}
})
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "智能聊天"
}

View File

@@ -0,0 +1,63 @@
<!-- 聊天页面 -->
<view class="chat-container">
<!-- 消息列表 -->
<scroll-view
class="message-list"
scroll-y="{{true}}"
scroll-into-view="{{lastMessageId}}"
enable-back-to-top="{{true}}"
>
<view
wx:for="{{messages}}"
wx:key="id"
id="{{item.id}}"
class="message-item {{item.isMe ? 'message-right' : 'message-left'}}"
>
<view class="message-avatar">
<image src="{{item.avatar}}" mode="aspectFill"></image>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-nickname">{{item.nickname}}</text>
<text class="message-time">{{item.time}}</text>
</view>
<view class="message-body">
<text class="message-text" wx:if="{{item.type === 'text'}}">{{item.content}}</text>
<image class="message-image" wx:if="{{item.type === 'image'}}" src="{{item.content}}" mode="widthFix"></image>
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="input-container">
<view class="input-wrapper">
<input
class="message-input"
placeholder="请输入消息..."
value="{{inputValue}}"
bindinput="onInputChange"
bindconfirm="sendMessage"
confirm-type="send"
adjust-position="{{true}}"
/>
<button
class="send-btn"
bindtap="sendMessage"
disabled="{{!inputValue.trim()}}"
loading="{{sending}}"
>
发送
</button>
</view>
<view class="action-buttons">
<button class="action-btn" bindtap="chooseImage">
<image src="/assets/icons/image.png" mode="aspectFit"></image>
</button>
<button class="action-btn" bindtap="showMoreActions">
<image src="/assets/icons/more.png" mode="aspectFit"></image>
</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,167 @@
/* 聊天页面样式 */
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.message-list {
flex: 1;
padding: 20rpx;
overflow-y: auto;
padding-bottom: 120rpx; /* 为输入区域预留空间 */
}
.message-item {
display: flex;
margin: 20rpx 0;
align-items: flex-start;
}
.message-left {
flex-direction: row;
}
.message-right {
flex-direction: row-reverse;
}
.message-avatar {
width: 80rpx;
height: 80rpx;
margin: 0 20rpx;
flex-shrink: 0;
}
.message-avatar image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.message-content {
max-width: 70%;
display: flex;
flex-direction: column;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.message-nickname {
font-size: 24rpx;
color: #999;
margin-right: 10rpx;
}
.message-time {
font-size: 20rpx;
color: #ccc;
}
.message-body {
background-color: #f0f0f0;
border-radius: 10rpx;
padding: 20rpx;
word-wrap: break-word;
}
.message-right .message-body {
background-color: #95ec69;
}
.message-text {
font-size: 28rpx;
color: #333;
line-height: 1.5;
}
.message-right .message-text {
color: #000;
}
.message-image {
max-width: 300rpx;
max-height: 300rpx;
border-radius: 10rpx;
}
.input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1rpx solid #e0e0e0;
padding: 20rpx;
z-index: 1000;
}
.input-wrapper {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.message-input {
flex: 1;
border: 1rpx solid #e0e0e0;
border-radius: 50rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
background-color: #f8f8f8;
margin-right: 20rpx;
}
.message-input:focus {
border-color: #07c160;
background-color: white;
}
.send-btn {
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
color: white;
border: none;
border-radius: 50rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
font-weight: 500;
transition: all 0.3s ease;
min-width: 120rpx;
}
.send-btn:active {
transform: scale(0.95);
}
.send-btn:disabled {
background: #ccc;
color: #999;
transform: none;
}
.action-buttons {
display: flex;
justify-content: space-around;
}
.action-btn {
background: none;
border: none;
padding: 20rpx;
border-radius: 10rpx;
transition: background-color 0.3s;
}
.action-btn:active {
background-color: #f0f0f0;
}
.action-btn image {
width: 60rpx;
height: 60rpx;
}

View File

@@ -0,0 +1,224 @@
// 首页逻辑
const app = getApp()
Page({
data: {
userInfo: null,
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo'),
websocketConnected: false,
version: app.globalData.version
},
onLoad() {
if (app.globalData.userInfo) {
this.setData({
userInfo: app.globalData.userInfo,
hasUserInfo: true
})
} else if (this.data.canIUse) {
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
app.userInfoReadyCallback = res => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
} else {
// 在没有 open-type=getUserInfo 版本的兼容处理
wx.getUserInfo({
success: res => {
app.globalData.userInfo = res.userInfo
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
}
// 初始化WebSocket连接
this.initWebSocket()
},
getUserProfile(e) {
// 推荐使用wx.getUserProfile获取用户信息开发者每次通过该接口获取用户个人信息均需用户确认
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
app.globalData.userInfo = res.userInfo
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
// 发送用户信息到服务器
this.sendUserInfoToServer(res.userInfo)
}
})
},
getUserInfo(e) {
// 不推荐使用getUserInfo获取用户信息预计自2021年4月13日起getUserInfo将不再弹出弹窗并直接返回匿名的用户个人信息
app.globalData.userInfo = e.detail.userInfo
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
})
// 发送用户信息到服务器
this.sendUserInfoToServer(e.detail.userInfo)
},
// 发送用户信息到服务器
sendUserInfoToServer(userInfo) {
wx.request({
url: `${app.globalData.apiBase}/user/info`,
method: 'POST',
data: {
userInfo: userInfo,
deviceId: app.globalData.systemInfo.model
},
header: {
'content-type': 'application/json'
},
success: (res) => {
console.log('用户信息上传成功', res.data)
},
fail: (err) => {
console.error('用户信息上传失败', err)
}
})
},
// 初始化WebSocket连接
initWebSocket() {
const socket = wx.connectSocket({
url: app.globalData.websocketUrl,
header: {
'content-type': 'application/json'
}
})
socket.onOpen(() => {
console.log('WebSocket连接已打开')
this.setData({
websocketConnected: true
})
// 发送认证信息
socket.send({
data: JSON.stringify({
type: 'auth',
userId: app.globalData.userInfo ? app.globalData.userInfo.nickName : 'anonymous',
deviceId: app.globalData.systemInfo.model,
timestamp: Date.now()
})
})
})
socket.onMessage((res) => {
console.log('收到WebSocket消息', res.data)
try {
const data = JSON.parse(res.data)
this.handleWebSocketMessage(data)
} catch (e) {
console.error('解析WebSocket消息失败', e)
}
})
socket.onClose(() => {
console.log('WebSocket连接已关闭')
this.setData({
websocketConnected: false
})
// 3秒后尝试重连
setTimeout(() => {
this.initWebSocket()
}, 3000)
})
socket.onError((err) => {
console.error('WebSocket连接错误', err)
this.setData({
websocketConnected: false
})
})
},
// 处理WebSocket消息
handleWebSocketMessage(data) {
switch (data.type) {
case 'task_status':
// 处理任务状态更新
this.handleTaskStatusUpdate(data)
break
case 'message':
// 处理聊天消息
this.handleChatMessage(data)
break
default:
console.log('未知消息类型', data.type)
}
},
// 处理任务状态更新
handleTaskStatusUpdate(data) {
// 可以在这里更新任务列表或显示通知
if (data.status === 'completed') {
wx.showToast({
title: '任务完成',
icon: 'success'
})
} else if (data.status === 'failed') {
wx.showToast({
title: '任务失败',
icon: 'error'
})
}
},
// 处理聊天消息
handleChatMessage(data) {
// 可以在这里显示新消息通知
if (data.message) {
wx.showToast({
title: '新消息',
icon: 'none'
})
}
},
// 跳转到聊天页面
goToChat() {
wx.navigateTo({
url: '/pages/chat/chat'
})
},
// 跳转到任务页面
goToTask() {
wx.navigateTo({
url: '/pages/task/task'
})
},
// 显示设备信息
showDeviceInfo() {
const systemInfo = app.globalData.systemInfo
wx.showModal({
title: '设备信息',
content: `设备型号:${systemInfo.model}\n系统版本:${systemInfo.system}\n微信版本:${systemInfo.version}\n屏幕尺寸:${systemInfo.screenWidth}x${systemInfo.screenHeight}`,
showCancel: false
})
},
onShareAppMessage() {
return {
title: '智控未来 - 企业微信智能控制系统',
path: '/pages/index/index'
}
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "智控未来"
}

View File

@@ -0,0 +1,50 @@
<!-- 首页 - 用户登录和主界面 -->
<view class="container">
<!-- 用户信息区域 -->
<view class="user-info" wx:if="{{userInfo}}">
<image class="avatar" src="{{userInfo.avatarUrl}}" mode="aspectFill"></image>
<text class="nickname">{{userInfo.nickName}}</text>
</view>
<!-- 登录按钮 -->
<view class="login-section" wx:else>
<button class="login-btn" bindtap="getUserProfile">授权登录</button>
<text class="login-tip">请先授权登录以使用完整功能</text>
</view>
<!-- 功能菜单 -->
<view class="feature-menu" wx:if="{{userInfo}}">
<view class="menu-item" bindtap="goToChat">
<image class="menu-icon" src="/assets/icons/chat.png"></image>
<text class="menu-title">智能聊天</text>
<text class="menu-desc">与AI助手对话</text>
</view>
<view class="menu-item" bindtap="goToTask">
<image class="menu-icon" src="/assets/icons/task.png"></image>
<text class="menu-title">任务管理</text>
<text class="menu-desc">创建和管理任务</text>
</view>
<view class="menu-item" bindtap="showDeviceInfo">
<image class="menu-icon" src="/assets/icons/device.png"></image>
<text class="menu-title">设备信息</text>
<text class="menu-desc">查看设备状态</text>
</view>
</view>
<!-- 系统状态 -->
<view class="system-status">
<view class="status-item">
<text class="status-label">连接状态:</text>
<text class="status-value {{websocketConnected ? 'status-success' : 'status-error'}}">
{{websocketConnected ? '已连接' : '未连接'}}
</text>
</view>
<view class="status-item">
<text class="status-label">系统版本:</text>
<text class="status-value">{{version}}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,125 @@
/* 首页样式 */
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
/* 用户信息区域 */
.user-info {
display: flex;
align-items: center;
padding: 40rpx;
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
border-radius: 20rpx;
margin-bottom: 30rpx;
color: white;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-right: 30rpx;
border: 4rpx solid white;
}
.nickname {
font-size: 36rpx;
font-weight: bold;
}
/* 登录区域 */
.login-section {
padding: 60rpx 40rpx;
background: white;
border-radius: 20rpx;
margin-bottom: 30rpx;
text-align: center;
}
.login-btn {
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
color: white;
border: none;
border-radius: 50rpx;
padding: 30rpx 80rpx;
font-size: 32rpx;
margin-bottom: 20rpx;
}
.login-tip {
color: #999;
font-size: 28rpx;
}
/* 功能菜单 */
.feature-menu {
background: white;
border-radius: 20rpx;
padding: 20rpx;
margin-bottom: 30rpx;
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.3s;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: #f8f8f8;
}
.menu-icon {
width: 60rpx;
height: 60rpx;
margin-right: 30rpx;
}
.menu-title {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
flex: 1;
}
.menu-desc {
font-size: 24rpx;
color: #999;
}
/* 系统状态 */
.system-status {
background: white;
border-radius: 20rpx;
padding: 30rpx;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
font-size: 28rpx;
color: #666;
}
.status-value {
font-size: 28rpx;
font-weight: bold;
}

View File

@@ -0,0 +1,309 @@
// 任务页面逻辑
const { API, util, constants } = require('../../utils/api.js')
const { TASK_STATUS, TASK_TYPE } = constants
Page({
data: {
taskTitle: '',
taskDescription: '',
taskTypeIndex: 0,
priorityIndex: 0,
tasks: [],
filteredTasks: [],
filterStatus: 'all',
canSubmit: false,
taskTypes: [
{ name: '文本处理', value: 'text' },
{ name: '图像识别', value: 'image' },
{ name: '文件分析', value: 'file' },
{ name: '数据查询', value: 'data' },
{ name: '其他', value: 'other' }
],
priorities: [
{ name: '低', value: 'low' },
{ name: '中', value: 'medium' },
{ name: '高', value: 'high' },
{ name: '紧急', value: 'urgent' }
]
},
onLoad() {
this.loadTasks()
},
onShow() {
this.loadTasks()
},
// 监听输入变化
observers: {
'taskTitle, taskDescription': function(title, description) {
this.setData({
canSubmit: title.trim().length > 0 && description.trim().length > 0
})
}
},
// 标题输入
onTitleInput(e) {
this.setData({
taskTitle: e.detail.value
})
},
// 描述输入
onDescriptionInput(e) {
this.setData({
taskDescription: e.detail.value
})
},
// 类型选择
onTypeChange(e) {
this.setData({
taskTypeIndex: parseInt(e.detail.value)
})
},
// 优先级选择
onPriorityChange(e) {
this.setData({
priorityIndex: parseInt(e.detail.value)
})
},
// 提交任务
async submitTask() {
if (!this.data.canSubmit) {
return
}
util.showLoading('提交中...')
try {
const taskData = {
title: this.data.taskTitle.trim(),
description: this.data.taskDescription.trim(),
type: this.data.taskTypes[this.data.taskTypeIndex].value,
priority: this.data.priorities[this.data.priorityIndex].value,
timestamp: Date.now()
}
const result = await API.submitTask(taskData)
util.hideLoading()
if (result.success) {
util.showSuccess('任务提交成功')
// 清空表单
this.setData({
taskTitle: '',
taskDescription: '',
taskTypeIndex: 0,
priorityIndex: 0
})
// 重新加载任务列表
this.loadTasks()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
} else {
util.showError(result.message || '任务提交失败')
}
} catch (error) {
util.hideLoading()
console.error('提交任务失败:', error)
util.showError('提交失败,请重试')
}
},
// 加载任务列表
async loadTasks() {
try {
const result = await API.getTaskList(1, 50)
if (result.success && result.data) {
const tasks = result.data.tasks.map(task => ({
id: task.id,
title: task.title,
description: task.description,
type: task.type,
status: task.status,
priority: task.priority,
createdAt: util.formatRelativeTime(new Date(task.createdAt)),
updatedAt: task.updatedAt ? util.formatRelativeTime(new Date(task.updatedAt)) : '',
result: task.result || '',
progress: task.progress || 0
}))
this.setData({
tasks: tasks
})
this.filterTasks()
}
} catch (error) {
console.error('加载任务列表失败:', error)
util.showError('加载失败')
}
},
// 设置过滤器
setFilter(e) {
const status = e.currentTarget.dataset.status
this.setData({
filterStatus: status
})
this.filterTasks()
},
// 过滤任务
filterTasks() {
const { tasks, filterStatus } = this.data
if (filterStatus === 'all') {
this.setData({
filteredTasks: tasks
})
} else {
const filtered = tasks.filter(task => task.status === filterStatus)
this.setData({
filteredTasks: filtered
})
}
},
// 开始任务
async onStartTask(e) {
const { taskId, title } = e.detail
try {
const confirmed = await util.showModal('确认开始', `确定要开始处理任务"${title}"吗?`)
if (confirmed) {
util.showLoading('处理中...')
// 这里可以调用开始任务的API
// await API.startTask(taskId)
util.hideLoading()
util.showSuccess('任务已开始')
// 重新加载任务列表
this.loadTasks()
}
} catch (error) {
util.hideLoading()
console.error('开始任务失败:', error)
util.showError('操作失败')
}
},
// 完成任务
async onCompleteTask(e) {
const { taskId, title } = e.detail
try {
const confirmed = await util.showModal('确认完成', `确定要标记任务"${title}"为已完成吗?`)
if (confirmed) {
util.showLoading('处理中...')
// 这里可以调用完成任务的API
// await API.completeTask(taskId)
util.hideLoading()
util.showSuccess('任务已完成')
// 重新加载任务列表
this.loadTasks()
}
} catch (error) {
util.hideLoading()
console.error('完成任务失败:', error)
util.showError('操作失败')
}
},
// 重试任务
async onRetryTask(e) {
const { taskId, title } = e.detail
try {
const confirmed = await util.showModal('确认重试', `确定要重试任务"${title}"吗?`)
if (confirmed) {
util.showLoading('处理中...')
// 这里可以调用重试任务的API
// await API.retryTask(taskId)
util.hideLoading()
util.showSuccess('任务已重试')
// 重新加载任务列表
this.loadTasks()
}
} catch (error) {
util.hideLoading()
console.error('重试任务失败:', error)
util.showError('操作失败')
}
},
// 取消任务
async onCancelTask(e) {
const { taskId, title } = e.detail
try {
const confirmed = await util.showModal('确认取消', `确定要取消任务"${title}"吗?`)
if (confirmed) {
util.showLoading('处理中...')
// 这里可以调用取消任务的API
// await API.cancelTask(taskId)
util.hideLoading()
util.showSuccess('任务已取消')
// 重新加载任务列表
this.loadTasks()
}
} catch (error) {
util.hideLoading()
console.error('取消任务失败:', error)
util.showError('操作失败')
}
},
// 查看任务详情
onTaskDetail(e) {
const { taskId, title, description, status, result, createdAt, updatedAt } = e.detail
wx.navigateTo({
url: `/pages/task-detail/task-detail?taskId=${taskId}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&status=${status}&result=${encodeURIComponent(result)}&createdAt=${encodeURIComponent(createdAt)}&updatedAt=${encodeURIComponent(updatedAt)}`
})
},
// 下拉刷新
async onPullDownRefresh() {
await this.loadTasks()
wx.stopPullDownRefresh()
},
// 分享
onShareAppMessage() {
return {
title: '智控未来 - 任务管理',
path: '/pages/task/task'
}
}
})

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "任务管理",
"enablePullDownRefresh": true
}

View File

@@ -0,0 +1,128 @@
<!-- 任务页面 -->
<view class="task-container">
<!-- 任务创建区域 -->
<view class="task-create">
<view class="input-group">
<text class="label">任务标题:</text>
<input
class="task-input"
placeholder="请输入任务标题"
value="{{taskTitle}}"
bindinput="onTitleInput"
maxlength="100"
/>
</view>
<view class="input-group">
<text class="label">任务描述:</text>
<textarea
class="task-textarea"
placeholder="请输入任务描述"
value="{{taskDescription}}"
bindinput="onDescriptionInput"
maxlength="500"
auto-height
/>
</view>
<view class="input-group">
<text class="label">任务类型:</text>
<picker
bindchange="onTypeChange"
value="{{taskTypeIndex}}"
range="{{taskTypes}}"
range-key="{{'name'}}"
>
<view class="picker">
{{taskTypes[taskTypeIndex].name}}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="input-group">
<text class="label">优先级:</text>
<picker
bindchange="onPriorityChange"
value="{{priorityIndex}}"
range="{{priorities}}"
range-key="{{'name'}}"
>
<view class="picker">
{{priorities[priorityIndex].name}}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<button class="submit-btn" bindtap="submitTask" disabled="{{!canSubmit}}">
提交任务
</button>
</view>
<!-- 任务列表 -->
<view class="task-list">
<view class="list-header">
<text class="list-title">任务列表</text>
<view class="filter-buttons">
<button
class="filter-btn {{filterStatus === 'all' ? 'active' : ''}}"
bindtap="setFilter"
data-status="all"
>
全部
</button>
<button
class="filter-btn {{filterStatus === 'pending' ? 'active' : ''}}"
bindtap="setFilter"
data-status="pending"
>
待处理
</button>
<button
class="filter-btn {{filterStatus === 'processing' ? 'active' : ''}}"
bindtap="setFilter"
data-status="processing"
>
处理中
</button>
<button
class="filter-btn {{filterStatus === 'completed' ? 'active' : ''}}"
bindtap="setFilter"
data-status="completed"
>
已完成
</button>
</view>
</view>
<scroll-view class="task-scroll" scroll-y="{{true}}">
<view wx:if="{{tasks.length === 0}}" class="empty-state">
<image src="/assets/images/empty-task.png" mode="aspectFit"></image>
<text class="empty-text">暂无任务</text>
</view>
<view wx:else>
<task-card
wx:for="{{filteredTasks}}"
wx:key="id"
taskId="{{item.id}}"
title="{{item.title}}"
description="{{item.description}}"
type="{{item.type}}"
status="{{item.status}}"
priority="{{item.priority}}"
createdAt="{{item.createdAt}}"
updatedAt="{{item.updatedAt}}"
result="{{item.result}}"
progress="{{item.progress}}"
bind:start="onStartTask"
bind:complete="onCompleteTask"
bind:retry="onRetryTask"
bind:detail="onTaskDetail"
bind:cancel="onCancelTask"
/>
</view>
</scroll-view>
</view>
</view>

View File

@@ -0,0 +1,181 @@
/* 任务页面样式 */
.task-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.task-create {
background: white;
padding: 30rpx;
margin-bottom: 20rpx;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.input-group {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
.task-input {
width: 100%;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
background-color: #f8f8f8;
transition: border-color 0.3s;
}
.task-input:focus {
border-color: #07c160;
background-color: white;
outline: none;
}
.task-textarea {
width: 100%;
min-height: 120rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
background-color: #f8f8f8;
transition: border-color 0.3s;
resize: vertical;
}
.task-textarea:focus {
border-color: #07c160;
background-color: white;
outline: none;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
background-color: #f8f8f8;
transition: border-color 0.3s;
}
.picker:focus {
border-color: #07c160;
background-color: white;
}
.picker-arrow {
color: #999;
font-size: 24rpx;
}
.submit-btn {
width: 100%;
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
color: white;
border: none;
border-radius: 50rpx;
padding: 30rpx;
font-size: 32rpx;
font-weight: 500;
transition: all 0.3s ease;
margin-top: 20rpx;
}
.submit-btn:active {
transform: scale(0.98);
}
.submit-btn:disabled {
background: #ccc;
color: #999;
transform: none;
}
.task-list {
flex: 1;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.list-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.filter-buttons {
display: flex;
gap: 10rpx;
}
.filter-btn {
padding: 10rpx 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 20rpx;
font-size: 24rpx;
background: white;
color: #666;
transition: all 0.3s ease;
}
.filter-btn.active {
background: #07c160;
color: white;
border-color: #07c160;
}
.filter-btn:active {
transform: scale(0.95);
}
.task-scroll {
flex: 1;
padding: 20rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
text-align: center;
}
.empty-state image {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}

View File

@@ -0,0 +1,243 @@
// 用户中心页面逻辑
const { API, util, constants } = require('../../utils/api.js')
Page({
data: {
userInfo: null,
userId: '',
userStatus: 'online',
statusText: '在线',
taskStats: {
total: 0,
completed: 0,
processing: 0
},
deviceInfo: {},
appVersion: ''
},
onLoad() {
const app = getApp()
this.setData({
appVersion: app.globalData.version,
deviceInfo: app.globalData.systemInfo
})
this.loadUserData()
},
onShow() {
this.loadUserData()
this.loadTaskStats()
},
// 加载用户数据
loadUserData() {
const userInfo = wx.getStorageSync('userInfo')
const userId = wx.getStorageSync('userId') || 'anonymous'
if (userInfo) {
this.setData({
userInfo: userInfo,
userId: userId
})
} else {
// 如果没有用户信息,尝试获取
this.getUserProfile()
}
},
// 获取用户资料
getUserProfile() {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
const userInfo = res.userInfo
wx.setStorageSync('userInfo', userInfo)
this.setData({
userInfo: userInfo
})
// 发送用户信息到服务器
this.sendUserInfoToServer(userInfo)
},
fail: () => {
// 使用默认信息
this.setData({
userInfo: {
nickName: '访客用户',
avatarUrl: '/assets/images/default-avatar.png'
}
})
}
})
},
// 发送用户信息到服务器
async sendUserInfoToServer(userInfo) {
try {
await API.request('/user/info', 'POST', {
userInfo: userInfo,
deviceId: this.data.deviceInfo.model
})
} catch (error) {
console.error('发送用户信息失败:', error)
}
},
// 加载任务统计
async loadTaskStats() {
try {
const result = await API.getTaskList(1, 100) // 获取所有任务进行统计
if (result.success && result.data) {
const tasks = result.data.tasks
const stats = {
total: tasks.length,
completed: tasks.filter(task => task.status === 'completed').length,
processing: tasks.filter(task => task.status === 'processing').length
}
this.setData({
taskStats: stats
})
}
} catch (error) {
console.error('加载任务统计失败:', error)
}
},
// 显示任务历史
showTaskHistory() {
wx.navigateTo({
url: '/pages/task-history/task-history'
})
},
// 显示设置
showSettings() {
wx.navigateTo({
url: '/pages/settings/settings'
})
},
// 显示关于
showAbout() {
wx.showModal({
title: '关于智控未来',
content: `智控未来 v${this.data.appVersion}\n\n企业微信智能控制系统\n基于微信小程序原生技术栈开发\n\n功能特点:\n• 智能聊天对话\n• 任务管理处理\n• 多设备状态同步\n• WebSocket实时通信\n\n技术支持:\n• Rust后端服务\n• Embedded-Redis状态管理\n• HeedDB数据存储\n• LMStudio AI集成`,
showCancel: false
})
},
// 显示帮助
showHelp() {
wx.showModal({
title: '使用帮助',
content: `快速上手:\n\n1. 智能聊天\n • 在聊天页面输入问题\n • 支持文本、图片、文件消息\n • 实时接收AI回复\n\n2. 任务管理\n • 创建新任务并设置类型\n • 查看任务处理进度\n • 管理任务状态\n\n3. 用户中心\n • 查看个人信息\n • 查看任务统计\n • 系统设置\n\n常见问题:\n• 网络连接失败请检查网络\n• 任务长时间未响应可重试\n• 如有问题请联系技术支持`,
showCancel: false
})
},
// 刷新数据
async refreshData() {
util.showLoading('刷新中...')
try {
// 重新加载用户数据
this.loadUserData()
// 重新加载任务统计
await this.loadTaskStats()
util.hideLoading()
util.showSuccess('数据已刷新')
} catch (error) {
util.hideLoading()
console.error('刷新数据失败:', error)
util.showError('刷新失败')
}
},
// 清除缓存
async clearCache() {
const confirmed = await util.showModal('确认清除', '确定要清除本地缓存吗?这将删除聊天记录和临时文件。')
if (confirmed) {
util.showLoading('清除中...')
try {
// 清除本地存储
wx.removeStorageSync('chatHistory')
wx.removeStorageSync('taskHistory')
// 清除临时文件
const fileSystemManager = wx.getFileSystemManager()
try {
fileSystemManager.readdir({
dirPath: `${wx.env.USER_DATA_PATH}/`,
success: (res) => {
res.files.forEach(file => {
if (file.startsWith('tmp_')) {
fileSystemManager.unlink({
filePath: `${wx.env.USER_DATA_PATH}/${file}`
})
}
})
}
})
} catch (e) {
console.log('清除临时文件失败:', e)
}
util.hideLoading()
util.showSuccess('缓存已清除')
} catch (error) {
util.hideLoading()
console.error('清除缓存失败:', error)
util.showError('清除失败')
}
}
},
// 退出登录
async logout() {
const confirmed = await util.showModal('确认退出', '确定要退出登录吗?')
if (confirmed) {
util.showLoading('退出中...')
try {
// 通知服务器退出
await API.request('/user/logout', 'POST')
// 清除本地数据
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
wx.removeStorageSync('userId')
wx.removeStorageSync('chatHistory')
util.hideLoading()
// 返回首页
wx.reLaunch({
url: '/pages/index/index'
})
} catch (error) {
util.hideLoading()
console.error('退出登录失败:', error)
util.showError('退出失败')
}
}
},
// 分享
onShareAppMessage() {
return {
title: '智控未来 - 我的个人中心',
path: '/pages/user/user',
imageUrl: '/assets/images/share-user.png'
}
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "个人中心"
}

View File

@@ -0,0 +1,101 @@
<!-- 用户中心页面 -->
<view class="user-container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="avatar-section">
<user-avatar
src="{{userInfo.avatarUrl}}"
size="large"
shape="circle"
showStatus="{{true}}"
status="{{userStatus}}"
/>
<view class="user-info">
<text class="nickname">{{userInfo.nickName}}</text>
<text class="user-id">ID: {{userId}}</text>
<text class="status-text">{{statusText}}</text>
</view>
</view>
<view class="user-stats">
<view class="stat-item">
<text class="stat-number">{{taskStats.total}}</text>
<text class="stat-label">总任务</text>
</view>
<view class="stat-item">
<text class="stat-number">{{taskStats.completed}}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-item">
<text class="stat-number">{{taskStats.processing}}</text>
<text class="stat-label">处理中</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-title">功能设置</view>
<view class="menu-list">
<view class="menu-item" bindtap="showTaskHistory">
<image class="menu-icon" src="/assets/icons/history.png"></image>
<text class="menu-text">任务历史</text>
<text class="menu-badge">{{taskStats.total}}</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="showSettings">
<image class="menu-icon" src="/assets/icons/settings.png"></image>
<text class="menu-text">设置</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="showAbout">
<image class="menu-icon" src="/assets/icons/about.png"></image>
<text class="menu-text">关于</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" bindtap="showHelp">
<image class="menu-icon" src="/assets/icons/help.png"></image>
<text class="menu-text">帮助</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- 系统信息 -->
<view class="system-info">
<view class="info-item">
<text class="info-label">版本号:</text>
<text class="info-value">{{appVersion}}</text>
</view>
<view class="info-item">
<text class="info-label">设备型号:</text>
<text class="info-value">{{deviceInfo.model}}</text>
</view>
<view class="info-item">
<text class="info-label">系统版本:</text>
<text class="info-value">{{deviceInfo.system}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="action-btn primary" bindtap="refreshData">
<image src="/assets/icons/refresh.png"></image>
刷新数据
</button>
<button class="action-btn secondary" bindtap="clearCache">
<image src="/assets/icons/clear.png"></image>
清除缓存
</button>
<button class="action-btn danger" bindtap="logout">
<image src="/assets/icons/logout.png"></image>
退出登录
</button>
</view>
</view>

View File

@@ -0,0 +1,223 @@
/* 用户中心页面样式 */
.user-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
/* 用户信息卡片 */
.user-card {
background: white;
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
text-align: center;
}
.avatar-section {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.user-info {
margin-left: 30rpx;
text-align: left;
}
.nickname {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.user-id {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
}
.status-text {
display: block;
font-size: 24rpx;
color: #07c160;
font-weight: 500;
}
/* 用户统计 */
.user-stats {
display: flex;
justify-content: space-around;
padding-top: 30rpx;
border-top: 1rpx solid #f0f0f0;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-number {
font-size: 40rpx;
font-weight: bold;
color: #07c160;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
/* 菜单区域 */
.menu-section {
background: white;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.menu-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
padding: 30rpx 30rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.menu-list {
padding: 0;
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.3s;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: #f8f8f8;
}
.menu-icon {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
opacity: 0.7;
}
.menu-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.menu-badge {
background: #dd524d;
color: white;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
margin-right: 20rpx;
min-width: 32rpx;
text-align: center;
}
.menu-arrow {
font-size: 32rpx;
color: #999;
}
/* 系统信息 */
.system-info {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 28rpx;
color: #666;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 20rpx;
margin-top: auto;
padding-top: 40rpx;
}
.action-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
border: none;
border-radius: 20rpx;
font-size: 24rpx;
transition: all 0.3s ease;
min-height: 120rpx;
}
.action-btn.primary {
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
color: white;
}
.action-btn.secondary {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
color: white;
}
.action-btn.danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
}
.action-btn:active {
transform: scale(0.98);
}
.action-btn image {
width: 40rpx;
height: 40rpx;
margin-bottom: 10rpx;
filter: brightness(0) invert(1);
}