Claw 项目完整结构提交
This commit is contained in:
80
Claw/client/wechat_app/app.js
Normal file
80
Claw/client/wechat_app/app.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// 应用入口文件
|
||||
App({
|
||||
onLaunch: function () {
|
||||
console.log('智控未来小程序启动')
|
||||
|
||||
// 初始化系统信息
|
||||
this.globalData.systemInfo = wx.getSystemInfoSync()
|
||||
|
||||
// 检查更新
|
||||
this.checkForUpdate()
|
||||
|
||||
// 初始化网络监听
|
||||
this.initNetworkListener()
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
console.log('小程序显示')
|
||||
},
|
||||
|
||||
onHide: function () {
|
||||
console.log('小程序隐藏')
|
||||
},
|
||||
|
||||
onError: function (msg) {
|
||||
console.error('小程序错误:', msg)
|
||||
},
|
||||
|
||||
// 检查更新
|
||||
checkForUpdate: function() {
|
||||
if (wx.canIUse('getUpdateManager')) {
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate(function (res) {
|
||||
console.log('检查更新结果:', res.hasUpdate)
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(function () {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(function () {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本下载失败',
|
||||
showCancel: false
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化网络监听
|
||||
initNetworkListener: function() {
|
||||
wx.onNetworkStatusChange(function(res) {
|
||||
console.log('网络状态变化:', res)
|
||||
if (!res.isConnected) {
|
||||
wx.showToast({
|
||||
title: '网络已断开',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 全局数据
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
systemInfo: null,
|
||||
apiBase: 'https://pactgo.cn/api/v1',
|
||||
websocketUrl: 'wss://pactgo.cn/ws/task',
|
||||
version: '1.0.0'
|
||||
}
|
||||
})
|
||||
60
Claw/client/wechat_app/app.json
Normal file
60
Claw/client/wechat_app/app.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"desc": "智控未来 - 企业微信智能控制系统",
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/chat/chat",
|
||||
"pages/task/task",
|
||||
"pages/user/user"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "智控未来",
|
||||
"navigationBarTextStyle": "black"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3cc51f",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "assets/icons/home.png",
|
||||
"selectedIconPath": "assets/icons/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/chat/chat",
|
||||
"iconPath": "assets/icons/chat.png",
|
||||
"selectedIconPath": "assets/icons/chat-active.png",
|
||||
"text": "聊天"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/task/task",
|
||||
"iconPath": "assets/icons/task.png",
|
||||
"selectedIconPath": "assets/icons/task-active.png",
|
||||
"text": "任务"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/user/user",
|
||||
"iconPath": "assets/icons/user.png",
|
||||
"selectedIconPath": "assets/icons/user-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 10000,
|
||||
"downloadFile": 10000,
|
||||
"uploadFile": 10000,
|
||||
"websocket": 10000
|
||||
},
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
}
|
||||
},
|
||||
"requiredBackgroundModes": ["audio"],
|
||||
"requiredPrivateInfos": ["getLocation"]
|
||||
}
|
||||
188
Claw/client/wechat_app/app.wxss
Normal file
188
Claw/client/wechat_app/app.wxss
Normal file
@@ -0,0 +1,188 @@
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
|
||||
}
|
||||
|
||||
/* 通用容器 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #06a050;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
background-color: #059040;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background-color: #ccc;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.input {
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 5rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #07c160;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 10rpx;
|
||||
padding: 30rpx;
|
||||
margin: 20rpx 0;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 副标题样式 */
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
/* 文本样式 */
|
||||
.text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 状态颜色 */
|
||||
.status-success {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #dd524d;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
color: #10aeff;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #f3f3f3;
|
||||
border-top: 4rpx solid #07c160;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 消息样式 */
|
||||
.message {
|
||||
padding: 20rpx;
|
||||
margin: 10rpx 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
background-color: #95ec69;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.system {
|
||||
background-color: #e8f5e8;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 任务状态 */
|
||||
.task-status {
|
||||
display: inline-block;
|
||||
padding: 5rpx 15rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-status.pending {
|
||||
background-color: #f0ad4e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.task-status.processing {
|
||||
background-color: #10aeff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.task-status.completed {
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.task-status.failed {
|
||||
background-color: #dd524d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 750rpx) {
|
||||
.container {
|
||||
margin: 10rpx;
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20rpx;
|
||||
margin: 15rpx 0;
|
||||
}
|
||||
}
|
||||
2
Claw/client/wechat_app/assets/icons/README.md
Normal file
2
Claw/client/wechat_app/assets/icons/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 这是一个占位文件,用于创建目录结构 -->
|
||||
<!-- 实际的图标文件需要替换为真实的PNG图标文件 -->
|
||||
2
Claw/client/wechat_app/assets/images/README.md
Normal file
2
Claw/client/wechat_app/assets/images/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 这是一个占位文件,用于创建目录结构 -->
|
||||
<!-- 实际的图片文件需要替换为真实的PNG/JPG图片文件 -->
|
||||
163
Claw/client/wechat_app/components/message/message.js
Normal file
163
Claw/client/wechat_app/components/message/message.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// 消息组件逻辑
|
||||
Component({
|
||||
properties: {
|
||||
// 消息内容
|
||||
content: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 消息类型
|
||||
type: {
|
||||
type: String,
|
||||
value: 'text'
|
||||
},
|
||||
// 发送者头像
|
||||
avatar: {
|
||||
type: String,
|
||||
value: '/assets/images/default-avatar.png'
|
||||
},
|
||||
// 发送者昵称
|
||||
nickname: {
|
||||
type: String,
|
||||
value: '未知用户'
|
||||
},
|
||||
// 发送时间
|
||||
time: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 是否是自己发送的消息
|
||||
isMe: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
// 文件名(文件消息)
|
||||
fileName: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 文件大小(文件消息)
|
||||
fileSize: {
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 预览图片
|
||||
previewImage() {
|
||||
if (this.properties.type === 'image') {
|
||||
wx.previewImage({
|
||||
urls: [this.properties.content],
|
||||
current: this.properties.content
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下载文件
|
||||
downloadFile() {
|
||||
if (this.properties.type === 'file') {
|
||||
wx.showLoading({
|
||||
title: '下载中...'
|
||||
})
|
||||
|
||||
wx.downloadFile({
|
||||
url: this.properties.content,
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存文件到本地
|
||||
wx.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
wx.showToast({
|
||||
title: '文件已保存',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 打开文件
|
||||
wx.openDocument({
|
||||
filePath: saveRes.savedFilePath,
|
||||
showMenu: true
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 长按消息
|
||||
onLongPress() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['复制', '转发', '删除'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0: // 复制
|
||||
this.copyMessage()
|
||||
break
|
||||
case 1: // 转发
|
||||
this.forwardMessage()
|
||||
break
|
||||
case 2: // 删除
|
||||
this.deleteMessage()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 复制消息
|
||||
copyMessage() {
|
||||
if (this.properties.type === 'text') {
|
||||
wx.setClipboardData({
|
||||
data: this.properties.content,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 转发消息
|
||||
forwardMessage() {
|
||||
this.triggerEvent('forward', {
|
||||
content: this.properties.content,
|
||||
type: this.properties.type,
|
||||
fileName: this.properties.fileName,
|
||||
fileSize: this.properties.fileSize
|
||||
})
|
||||
},
|
||||
|
||||
// 删除消息
|
||||
deleteMessage() {
|
||||
this.triggerEvent('delete', {
|
||||
content: this.properties.content,
|
||||
type: this.properties.type
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
3
Claw/client/wechat_app/components/message/message.json
Normal file
3
Claw/client/wechat_app/components/message/message.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
20
Claw/client/wechat_app/components/message/message.wxml
Normal file
20
Claw/client/wechat_app/components/message/message.wxml
Normal file
@@ -0,0 +1,20 @@
|
||||
<!-- 消息组件 -->
|
||||
<view class="message-item {{isMe ? 'message-right' : 'message-left'}}">
|
||||
<view class="message-avatar">
|
||||
<image src="{{avatar}}" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<view class="message-header">
|
||||
<text class="message-nickname">{{nickname}}</text>
|
||||
<text class="message-time">{{time}}</text>
|
||||
</view>
|
||||
<view class="message-body">
|
||||
<text class="message-text" wx:if="{{type === 'text'}}">{{content}}</text>
|
||||
<image class="message-image" wx:if="{{type === 'image'}}" src="{{content}}" mode="widthFix" bindtap="previewImage"></image>
|
||||
<view class="message-file" wx:if="{{type === 'file'}}" bindtap="downloadFile">
|
||||
<text class="file-name">{{fileName}}</text>
|
||||
<text class="file-size">{{fileSize}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
98
Claw/client/wechat_app/components/message/message.wxss
Normal file
98
Claw/client/wechat_app/components/message/message.wxss
Normal file
@@ -0,0 +1,98 @@
|
||||
/* 消息组件样式 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.message-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
143
Claw/client/wechat_app/components/task-card/task-card.js
Normal file
143
Claw/client/wechat_app/components/task-card/task-card.js
Normal file
@@ -0,0 +1,143 @@
|
||||
// 任务卡片组件逻辑
|
||||
const { TASK_STATUS } = require('../../utils/constant.js')
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
// 任务ID
|
||||
taskId: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 任务标题
|
||||
title: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 任务描述
|
||||
description: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 任务类型
|
||||
type: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 任务状态
|
||||
status: {
|
||||
type: String,
|
||||
value: TASK_STATUS.PENDING
|
||||
},
|
||||
// 优先级
|
||||
priority: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 处理结果
|
||||
result: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 进度(0-100)
|
||||
progress: {
|
||||
type: Number,
|
||||
value: 0
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
statusText: ''
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.updateStatusText()
|
||||
}
|
||||
},
|
||||
|
||||
observers: {
|
||||
'status': function(status) {
|
||||
this.updateStatusText()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 更新状态文本
|
||||
updateStatusText() {
|
||||
const statusMap = {
|
||||
[TASK_STATUS.PENDING]: '待处理',
|
||||
[TASK_STATUS.PROCESSING]: '处理中',
|
||||
[TASK_STATUS.COMPLETED]: '已完成',
|
||||
[TASK_STATUS.FAILED]: '处理失败',
|
||||
[TASK_STATUS.CANCELLED]: '已取消'
|
||||
}
|
||||
|
||||
this.setData({
|
||||
statusText: statusMap[this.properties.status] || '未知状态'
|
||||
})
|
||||
},
|
||||
|
||||
// 开始处理任务
|
||||
startTask() {
|
||||
this.triggerEvent('start', {
|
||||
taskId: this.properties.taskId,
|
||||
title: this.properties.title
|
||||
})
|
||||
},
|
||||
|
||||
// 完成任务
|
||||
completeTask() {
|
||||
this.triggerEvent('complete', {
|
||||
taskId: this.properties.taskId,
|
||||
title: this.properties.title
|
||||
})
|
||||
},
|
||||
|
||||
// 重试任务
|
||||
retryTask() {
|
||||
this.triggerEvent('retry', {
|
||||
taskId: this.properties.taskId,
|
||||
title: this.properties.title
|
||||
})
|
||||
},
|
||||
|
||||
// 查看任务详情
|
||||
viewDetails() {
|
||||
this.triggerEvent('detail', {
|
||||
taskId: this.properties.taskId,
|
||||
title: this.properties.title,
|
||||
description: this.properties.description,
|
||||
status: this.properties.status,
|
||||
result: this.properties.result,
|
||||
createdAt: this.properties.createdAt,
|
||||
updatedAt: this.properties.updatedAt
|
||||
})
|
||||
},
|
||||
|
||||
// 取消任务
|
||||
cancelTask() {
|
||||
wx.showModal({
|
||||
title: '确认取消',
|
||||
content: `确定要取消任务"${this.properties.title}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.triggerEvent('cancel', {
|
||||
taskId: this.properties.taskId,
|
||||
title: this.properties.title
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
39
Claw/client/wechat_app/components/task-card/task-card.wxml
Normal file
39
Claw/client/wechat_app/components/task-card/task-card.wxml
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- 任务卡片组件 -->
|
||||
<view class="task-card {{status}}">
|
||||
<view class="task-header">
|
||||
<text class="task-title">{{title}}</text>
|
||||
<text class="task-status {{status}}">{{statusText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="task-content">
|
||||
<text class="task-description">{{description}}</text>
|
||||
|
||||
<view class="task-meta" wx:if="{{type}}">
|
||||
<text class="task-type">类型:{{type}}</text>
|
||||
<text class="task-priority" wx:if="{{priority}}">优先级:{{priority}}</text>
|
||||
</view>
|
||||
|
||||
<view class="task-timeline" wx:if="{{createdAt}}">
|
||||
<text class="task-time">创建时间:{{createdAt}}</text>
|
||||
<text class="task-time" wx:if="{{updatedAt}}">更新时间:{{updatedAt}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="task-actions">
|
||||
<button class="action-btn primary" wx:if="{{status === 'pending'}}" bindtap="startTask">开始处理</button>
|
||||
<button class="action-btn success" wx:if="{{status === 'processing'}}" bindtap="completeTask">标记完成</button>
|
||||
<button class="action-btn warning" wx:if="{{status === 'failed'}}" bindtap="retryTask">重试</button>
|
||||
<button class="action-btn info" bindtap="viewDetails">查看详情</button>
|
||||
<button class="action-btn danger" wx:if="{{status !== 'completed'}}" bindtap="cancelTask">取消</button>
|
||||
</view>
|
||||
|
||||
<view class="task-progress" wx:if="{{status === 'processing' && progress}}">
|
||||
<progress percent="{{progress}}" stroke-width="6" activeColor="#07c160" backgroundColor="#f0f0f0"/>
|
||||
<text class="progress-text">{{progress}}%</text>
|
||||
</view>
|
||||
|
||||
<view class="task-result" wx:if="{{result}}">
|
||||
<text class="result-label">处理结果:</text>
|
||||
<text class="result-content">{{result}}</text>
|
||||
</view>
|
||||
</view>
|
||||
195
Claw/client/wechat_app/components/task-card/task-card.wxss
Normal file
195
Claw/client/wechat_app/components/task-card/task-card.wxss
Normal file
@@ -0,0 +1,195 @@
|
||||
/* 任务卡片组件样式 */
|
||||
.task-card {
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin: 20rpx 0;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
border-left: 8rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.task-card.pending {
|
||||
border-left-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.task-card.processing {
|
||||
border-left-color: #10aeff;
|
||||
}
|
||||
|
||||
.task-card.completed {
|
||||
border-left-color: #07c160;
|
||||
}
|
||||
|
||||
.task-card.failed {
|
||||
border-left-color: #dd524d;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.task-status.pending {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.task-status.processing {
|
||||
background-color: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.task-status.completed {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.task-status.failed {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.task-type,
|
||||
.task-priority {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
background: #f8f8f8;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.task-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15rpx;
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
border: none;
|
||||
border-radius: 25rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background: linear-gradient(135deg, #28a745 0%, #218838 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.action-btn.info {
|
||||
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.95);
|
||||
}
|
||||
|
||||
.task-progress {
|
||||
margin: 20rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
min-width: 60rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.task-result {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
background: white;
|
||||
padding: 15rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 1rpx solid #e9ecef;
|
||||
}
|
||||
117
Claw/client/wechat_app/components/user-avatar/user-avatar.js
Normal file
117
Claw/client/wechat_app/components/user-avatar/user-avatar.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// 用户头像组件逻辑
|
||||
const { DEFAULT_AVATAR } = require('../../utils/constant.js')
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
// 头像图片地址
|
||||
src: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 默认头像
|
||||
defaultAvatar: {
|
||||
type: String,
|
||||
value: DEFAULT_AVATAR
|
||||
},
|
||||
// 尺寸:small, medium, large, xlarge
|
||||
size: {
|
||||
type: String,
|
||||
value: 'medium'
|
||||
},
|
||||
// 形状:circle, rounded, square
|
||||
shape: {
|
||||
type: String,
|
||||
value: 'circle'
|
||||
},
|
||||
// 图片裁剪模式
|
||||
mode: {
|
||||
type: String,
|
||||
value: 'aspectFill'
|
||||
},
|
||||
// 是否懒加载
|
||||
lazyLoad: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
// 是否显示在线状态
|
||||
showStatus: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
// 在线状态:online, offline, busy, away
|
||||
status: {
|
||||
type: String,
|
||||
value: 'offline'
|
||||
},
|
||||
// 徽章数量
|
||||
badge: {
|
||||
type: Number,
|
||||
value: 0
|
||||
},
|
||||
// 是否显示加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
// 自定义样式
|
||||
customStyle: {
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
imageLoaded: false,
|
||||
imageError: false
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 图片加载成功
|
||||
onImageLoad() {
|
||||
this.setData({
|
||||
imageLoaded: true,
|
||||
imageError: false
|
||||
})
|
||||
this.triggerEvent('load')
|
||||
},
|
||||
|
||||
// 图片加载失败
|
||||
onImageError(e) {
|
||||
console.error('头像加载失败:', e)
|
||||
this.setData({
|
||||
imageLoaded: false,
|
||||
imageError: true
|
||||
})
|
||||
this.triggerEvent('error', e)
|
||||
},
|
||||
|
||||
// 点击头像
|
||||
onAvatarTap() {
|
||||
this.triggerEvent('tap', {
|
||||
src: this.properties.src,
|
||||
status: this.properties.status,
|
||||
badge: this.properties.badge
|
||||
})
|
||||
},
|
||||
|
||||
// 长按头像
|
||||
onAvatarLongPress() {
|
||||
this.triggerEvent('longpress', {
|
||||
src: this.properties.src,
|
||||
status: this.properties.status,
|
||||
badge: this.properties.badge
|
||||
})
|
||||
},
|
||||
|
||||
// 获取头像状态文本
|
||||
getStatusText() {
|
||||
const statusMap = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
busy: '忙碌',
|
||||
away: '离开'
|
||||
}
|
||||
return statusMap[this.properties.status] || '未知'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!-- 用户头像组件 -->
|
||||
<view class="user-avatar {{size}} {{shape}}" style="{{customStyle}}">
|
||||
<image
|
||||
class="avatar-image"
|
||||
src="{{src || defaultAvatar}}"
|
||||
mode="{{mode}}"
|
||||
lazy-load="{{lazyLoad}}"
|
||||
bindload="onImageLoad"
|
||||
binderror="onImageError"
|
||||
></image>
|
||||
|
||||
<!-- 在线状态指示器 -->
|
||||
<view class="status-indicator {{status}}" wx:if="{{showStatus}}"></view>
|
||||
|
||||
<!-- 徽章/未读消息数 -->
|
||||
<view class="badge" wx:if="{{badge > 0}}">
|
||||
<text class="badge-text">{{badge > 99 ? '99+' : badge}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-overlay" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
</view>
|
||||
</view>
|
||||
143
Claw/client/wechat_app/components/user-avatar/user-avatar.wxss
Normal file
143
Claw/client/wechat_app/components/user-avatar/user-avatar.wxss
Normal file
@@ -0,0 +1,143 @@
|
||||
/* 用户头像组件样式 */
|
||||
.user-avatar {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 尺寸样式 */
|
||||
.user-avatar.small {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.user-avatar.medium {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.user-avatar.large {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.user-avatar.xlarge {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
/* 形状样式 */
|
||||
.user-avatar.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-avatar.rounded {
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.user-avatar.square {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 头像图片 */
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 在线状态指示器 */
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background-color: #07c160;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.status-indicator.busy {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.status-indicator.away {
|
||||
background-color: #10aeff;
|
||||
}
|
||||
|
||||
/* 徽章 */
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
background-color: #dd524d;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
padding: 0 8rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 加载遮罩 */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #f3f3f3;
|
||||
border-top: 4rpx solid #07c160;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 默认头像样式 */
|
||||
.user-avatar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.user-avatar.error::before {
|
||||
background: linear-gradient(135deg, #dd524d 0%, #c82333 100%);
|
||||
}
|
||||
436
Claw/client/wechat_app/pages/chat/chat.js
Normal file
436
Claw/client/wechat_app/pages/chat/chat.js
Normal 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'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
3
Claw/client/wechat_app/pages/chat/chat.json
Normal file
3
Claw/client/wechat_app/pages/chat/chat.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "智能聊天"
|
||||
}
|
||||
63
Claw/client/wechat_app/pages/chat/chat.wxml
Normal file
63
Claw/client/wechat_app/pages/chat/chat.wxml
Normal 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>
|
||||
167
Claw/client/wechat_app/pages/chat/chat.wxss
Normal file
167
Claw/client/wechat_app/pages/chat/chat.wxss
Normal 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;
|
||||
}
|
||||
224
Claw/client/wechat_app/pages/index/index.js
Normal file
224
Claw/client/wechat_app/pages/index/index.js
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
3
Claw/client/wechat_app/pages/index/index.json
Normal file
3
Claw/client/wechat_app/pages/index/index.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "智控未来"
|
||||
}
|
||||
50
Claw/client/wechat_app/pages/index/index.wxml
Normal file
50
Claw/client/wechat_app/pages/index/index.wxml
Normal 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>
|
||||
125
Claw/client/wechat_app/pages/index/index.wxss
Normal file
125
Claw/client/wechat_app/pages/index/index.wxss
Normal 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;
|
||||
}
|
||||
309
Claw/client/wechat_app/pages/task/task.js
Normal file
309
Claw/client/wechat_app/pages/task/task.js
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
4
Claw/client/wechat_app/pages/task/task.json
Normal file
4
Claw/client/wechat_app/pages/task/task.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务管理",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
128
Claw/client/wechat_app/pages/task/task.wxml
Normal file
128
Claw/client/wechat_app/pages/task/task.wxml
Normal 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>
|
||||
181
Claw/client/wechat_app/pages/task/task.wxss
Normal file
181
Claw/client/wechat_app/pages/task/task.wxss
Normal 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;
|
||||
}
|
||||
243
Claw/client/wechat_app/pages/user/user.js
Normal file
243
Claw/client/wechat_app/pages/user/user.js
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
3
Claw/client/wechat_app/pages/user/user.json
Normal file
3
Claw/client/wechat_app/pages/user/user.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "个人中心"
|
||||
}
|
||||
101
Claw/client/wechat_app/pages/user/user.wxml
Normal file
101
Claw/client/wechat_app/pages/user/user.wxml
Normal 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>
|
||||
223
Claw/client/wechat_app/pages/user/user.wxss
Normal file
223
Claw/client/wechat_app/pages/user/user.wxss
Normal 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);
|
||||
}
|
||||
55
Claw/client/wechat_app/project.config.json
Normal file
55
Claw/client/wechat_app/project.config.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"description": "项目配置文件",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"newFeature": false,
|
||||
"coverView": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"useMultiFrameRuntime": true,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"enableEngineNative": false,
|
||||
"useIsolateContext": true,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"packNpmManually": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"disableUseStrict": false,
|
||||
"minifyWXML": true,
|
||||
"showES6CompileOption": false,
|
||||
"useCompilerPlugins": false,
|
||||
"ignoreUploadUnusedFiles": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "2.19.4",
|
||||
"appid": "wx1234567890abcdef",
|
||||
"projectname": "智控未来",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
||||
32
Claw/client/wechat_app/project.private.config.json
Normal file
32
Claw/client/wechat_app/project.private.config.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"description": "智控未来 - 企业微信智能控制系统",
|
||||
"projectname": "智控未来",
|
||||
"appid": "wx1234567890abcdef",
|
||||
"projectType": "miniprogram",
|
||||
"miniprogramRoot": "./",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"newFeature": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
}
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "2.19.4",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"condition": {}
|
||||
}
|
||||
7
Claw/client/wechat_app/sitemap.json
Normal file
7
Claw/client/wechat_app/sitemap.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "智控未来 - 企业微信智能控制系统站点地图",
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
||||
392
Claw/client/wechat_app/utils/api.js
Normal file
392
Claw/client/wechat_app/utils/api.js
Normal file
@@ -0,0 +1,392 @@
|
||||
// API接口封装
|
||||
const API_BASE = 'https://pactgo.cn/api/v1'
|
||||
|
||||
class API {
|
||||
// 用户登录
|
||||
static async login(code) {
|
||||
return this.request('/wechat/login', 'POST', { code })
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
static async getUserInfo() {
|
||||
return this.request('/wechat/userinfo', 'GET')
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
static async submitTask(taskData) {
|
||||
return this.request('/task/submit', 'POST', taskData)
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
static async getTaskList(page = 1, limit = 20) {
|
||||
return this.request(`/task/list?page=${page}&limit=${limit}`, 'GET')
|
||||
}
|
||||
|
||||
// 获取任务详情
|
||||
static async getTaskDetail(taskId) {
|
||||
return this.request(`/task/${taskId}`, 'GET')
|
||||
}
|
||||
|
||||
// 获取任务状态
|
||||
static async getTaskStatus(taskId) {
|
||||
return this.request(`/task/${taskId}/status`, 'GET')
|
||||
}
|
||||
|
||||
// 发送聊天消息
|
||||
static async sendMessage(content, type = 'text') {
|
||||
return this.request('/chat/message', 'POST', { content, type })
|
||||
}
|
||||
|
||||
// 获取聊天记录
|
||||
static async getChatHistory(page = 1, limit = 50) {
|
||||
return this.request(`/chat/history?page=${page}&limit=${limit}`, 'GET')
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
static async getDeviceInfo() {
|
||||
return this.request('/device/info', 'GET')
|
||||
}
|
||||
|
||||
// 更新设备状态
|
||||
static async updateDeviceStatus(status) {
|
||||
return this.request('/device/status', 'POST', { status })
|
||||
}
|
||||
|
||||
// 获取系统状态
|
||||
static async getSystemStatus() {
|
||||
return this.request('/system/status', 'GET')
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
static async request(url, method = 'GET', data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token') || ''
|
||||
|
||||
wx.request({
|
||||
url: API_BASE + url,
|
||||
method,
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
if (res.data.success !== false) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
reject(new Error(res.data.message || '请求失败'))
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// Token过期,重新登录
|
||||
wx.removeStorageSync('token')
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(new Error('登录已过期'))
|
||||
} else {
|
||||
reject(new Error(`请求失败: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('API请求失败:', err)
|
||||
wx.showToast({
|
||||
title: '网络连接失败',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
static async uploadFile(filePath, formData = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token') || ''
|
||||
|
||||
wx.uploadFile({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: formData,
|
||||
header: {
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
resolve(data)
|
||||
} catch (e) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`上传失败: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('文件上传失败:', err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket管理器
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socket = null
|
||||
this.isConnected = false
|
||||
this.reconnectTimer = null
|
||||
this.messageHandlers = new Map()
|
||||
}
|
||||
|
||||
connect(url) {
|
||||
if (this.socket) {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
this.socket = wx.connectSocket({
|
||||
url: url,
|
||||
header: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.onOpen(() => {
|
||||
console.log('WebSocket连接已打开')
|
||||
this.isConnected = true
|
||||
this.clearReconnectTimer()
|
||||
|
||||
// 发送认证信息
|
||||
this.send({
|
||||
type: 'auth',
|
||||
userId: wx.getStorageSync('userId') || 'anonymous',
|
||||
deviceId: wx.getSystemInfoSync().model,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
this.socket.onMessage((res) => {
|
||||
console.log('收到WebSocket消息:', res.data)
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
this.handleMessage(data)
|
||||
} catch (e) {
|
||||
console.error('解析WebSocket消息失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.onClose(() => {
|
||||
console.log('WebSocket连接已关闭')
|
||||
this.isConnected = false
|
||||
this.socket = null
|
||||
this.scheduleReconnect(url)
|
||||
})
|
||||
|
||||
this.socket.onError((err) => {
|
||||
console.error('WebSocket连接错误:', err)
|
||||
this.isConnected = false
|
||||
this.scheduleReconnect(url)
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.clearReconnectTimer()
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.isConnected && this.socket) {
|
||||
this.socket.send({
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
} else {
|
||||
console.warn('WebSocket未连接,无法发送消息')
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(type, handler) {
|
||||
if (!this.messageHandlers.has(type)) {
|
||||
this.messageHandlers.set(type, [])
|
||||
}
|
||||
this.messageHandlers.get(type).push(handler)
|
||||
}
|
||||
|
||||
offMessage(type, handler) {
|
||||
if (this.messageHandlers.has(type)) {
|
||||
const handlers = this.messageHandlers.get(type)
|
||||
const index = handlers.indexOf(handler)
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
if (data.type && this.messageHandlers.has(data.type)) {
|
||||
const handlers = this.messageHandlers.get(data.type)
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(data)
|
||||
} catch (e) {
|
||||
console.error('消息处理错误:', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
scheduleReconnect(url) {
|
||||
this.clearReconnectTimer()
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('尝试重新连接WebSocket...')
|
||||
this.connect(url)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
clearReconnectTimer() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const util = {
|
||||
// 格式化时间
|
||||
formatTime(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return [year, month, day].map(this.formatNumber).join('/') + ' ' +
|
||||
[hour, minute, second].map(this.formatNumber).join(':')
|
||||
},
|
||||
|
||||
formatNumber(n) {
|
||||
n = n.toString()
|
||||
return n[1] ? n : '0' + n
|
||||
},
|
||||
|
||||
// 显示加载提示
|
||||
showLoading(title = '加载中...') {
|
||||
wx.showLoading({
|
||||
title: title,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
// 隐藏加载提示
|
||||
hideLoading() {
|
||||
wx.hideLoading()
|
||||
},
|
||||
|
||||
// 显示成功提示
|
||||
showSuccess(title = '操作成功') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示错误提示
|
||||
showError(title = '操作失败') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示模态框
|
||||
showModal(title, content) {
|
||||
return new Promise((resolve) => {
|
||||
wx.showModal({
|
||||
title: title,
|
||||
content: content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 防抖函数
|
||||
debounce(func, wait) {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
},
|
||||
|
||||
// 节流函数
|
||||
throttle(func, limit) {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const constants = {
|
||||
// API基础地址
|
||||
API_BASE: 'https://pactgo.cn/api/v1',
|
||||
|
||||
// WebSocket地址
|
||||
WEBSOCKET_URL: 'wss://pactgo.cn/ws/task',
|
||||
|
||||
// 任务状态
|
||||
TASK_STATUS: {
|
||||
PENDING: 'pending',
|
||||
PROCESSING: 'processing',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed'
|
||||
},
|
||||
|
||||
// 消息类型
|
||||
MESSAGE_TYPE: {
|
||||
TEXT: 'text',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
SYSTEM: 'system'
|
||||
},
|
||||
|
||||
// 本地存储键名
|
||||
STORAGE_KEYS: {
|
||||
TOKEN: 'token',
|
||||
USER_INFO: 'userInfo',
|
||||
DEVICE_ID: 'deviceId',
|
||||
TASK_HISTORY: 'taskHistory'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
API,
|
||||
WebSocketManager,
|
||||
util,
|
||||
constants
|
||||
}
|
||||
210
Claw/client/wechat_app/utils/constant.js
Normal file
210
Claw/client/wechat_app/utils/constant.js
Normal file
@@ -0,0 +1,210 @@
|
||||
// 常量定义
|
||||
|
||||
// API基础地址
|
||||
const API_BASE = 'https://pactgo.cn/api/v1'
|
||||
|
||||
// WebSocket地址
|
||||
const WEBSOCKET_URL = 'wss://pactgo.cn/ws/task'
|
||||
|
||||
// 任务状态
|
||||
const TASK_STATUS = {
|
||||
PENDING: 'pending',
|
||||
PROCESSING: 'processing',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
CANCELLED: 'cancelled'
|
||||
}
|
||||
|
||||
// 任务类型
|
||||
const TASK_TYPE = {
|
||||
TEXT: 'text',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
AUDIO: 'audio',
|
||||
VIDEO: 'video'
|
||||
}
|
||||
|
||||
// 消息类型
|
||||
const MESSAGE_TYPE = {
|
||||
TEXT: 'text',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
SYSTEM: 'system',
|
||||
TASK: 'task'
|
||||
}
|
||||
|
||||
// 本地存储键名
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: 'token',
|
||||
USER_INFO: 'userInfo',
|
||||
DEVICE_ID: 'deviceId',
|
||||
TASK_HISTORY: 'taskHistory',
|
||||
CHAT_HISTORY: 'chatHistory',
|
||||
SYSTEM_CONFIG: 'systemConfig'
|
||||
}
|
||||
|
||||
// 网络超时时间(毫秒)
|
||||
const NETWORK_TIMEOUT = {
|
||||
REQUEST: 10000,
|
||||
UPLOAD: 30000,
|
||||
DOWNLOAD: 30000,
|
||||
WEBSOCKET: 10000
|
||||
}
|
||||
|
||||
// 重试配置
|
||||
const RETRY_CONFIG = {
|
||||
MAX_RETRIES: 3,
|
||||
RETRY_DELAY: 1000,
|
||||
BACKOFF_MULTIPLIER: 2
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const PAGINATION = {
|
||||
DEFAULT_PAGE_SIZE: 20,
|
||||
MAX_PAGE_SIZE: 100,
|
||||
PRELOAD_PAGES: 2
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const UPLOAD_LIMITS = {
|
||||
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
MAX_IMAGE_SIZE: 5 * 1024 * 1024, // 5MB
|
||||
ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'text/plain', 'application/pdf']
|
||||
}
|
||||
|
||||
// WebSocket消息类型
|
||||
const WS_MESSAGE_TYPE = {
|
||||
AUTH: 'auth',
|
||||
PING: 'ping',
|
||||
PONG: 'pong',
|
||||
MESSAGE: 'message',
|
||||
TASK_STATUS: 'task_status',
|
||||
SYSTEM: 'system',
|
||||
ERROR: 'error'
|
||||
}
|
||||
|
||||
// 系统配置
|
||||
const SYSTEM_CONFIG = {
|
||||
APP_NAME: '智控未来',
|
||||
APP_VERSION: '1.0.0',
|
||||
API_VERSION: 'v1',
|
||||
SUPPORTED_FEATURES: ['chat', 'task', 'file_upload', 'websocket']
|
||||
}
|
||||
|
||||
// 错误代码
|
||||
const ERROR_CODES = {
|
||||
NETWORK_ERROR: 1000,
|
||||
AUTH_ERROR: 1001,
|
||||
API_ERROR: 1002,
|
||||
VALIDATION_ERROR: 1003,
|
||||
FILE_ERROR: 1004,
|
||||
WEBSOCKET_ERROR: 1005,
|
||||
TASK_ERROR: 1006
|
||||
}
|
||||
|
||||
// 错误消息
|
||||
const ERROR_MESSAGES = {
|
||||
[ERROR_CODES.NETWORK_ERROR]: '网络连接失败',
|
||||
[ERROR_CODES.AUTH_ERROR]: '认证失败',
|
||||
[ERROR_CODES.API_ERROR]: '接口调用失败',
|
||||
[ERROR_CODES.VALIDATION_ERROR]: '参数验证失败',
|
||||
[ERROR_CODES.FILE_ERROR]: '文件处理失败',
|
||||
[ERROR_CODES.WEBSOCKET_ERROR]: 'WebSocket连接失败',
|
||||
[ERROR_CODES.TASK_ERROR]: '任务处理失败'
|
||||
}
|
||||
|
||||
// 默认头像
|
||||
const DEFAULT_AVATAR = '/assets/images/default-avatar.png'
|
||||
|
||||
// 默认图标
|
||||
const DEFAULT_ICONS = {
|
||||
HOME: '/assets/icons/home.png',
|
||||
CHAT: '/assets/icons/chat.png',
|
||||
TASK: '/assets/icons/task.png',
|
||||
USER: '/assets/icons/user.png',
|
||||
SEND: '/assets/icons/send.png',
|
||||
UPLOAD: '/assets/icons/upload.png',
|
||||
SETTINGS: '/assets/icons/settings.png'
|
||||
}
|
||||
|
||||
// 颜色配置
|
||||
const COLORS = {
|
||||
PRIMARY: '#07c160',
|
||||
SECONDARY: '#10aeff',
|
||||
SUCCESS: '#07c160',
|
||||
WARNING: '#f0ad4e',
|
||||
ERROR: '#dd524d',
|
||||
INFO: '#10aeff',
|
||||
TEXT_PRIMARY: '#333333',
|
||||
TEXT_SECONDARY: '#666666',
|
||||
TEXT_HINT: '#999999',
|
||||
BACKGROUND: '#f5f5f5',
|
||||
WHITE: '#ffffff',
|
||||
BORDER: '#e0e0e0'
|
||||
}
|
||||
|
||||
// 字体大小
|
||||
const FONT_SIZES = {
|
||||
SMALL: '24rpx',
|
||||
NORMAL: '28rpx',
|
||||
LARGE: '32rpx',
|
||||
XLARGE: '36rpx',
|
||||
TITLE: '40rpx'
|
||||
}
|
||||
|
||||
// 间距配置
|
||||
const SPACING = {
|
||||
XS: '10rpx',
|
||||
SM: '20rpx',
|
||||
MD: '30rpx',
|
||||
LG: '40rpx',
|
||||
XL: '50rpx'
|
||||
}
|
||||
|
||||
// 圆角配置
|
||||
const BORDER_RADIUS = {
|
||||
SMALL: '5rpx',
|
||||
NORMAL: '10rpx',
|
||||
LARGE: '20rpx',
|
||||
ROUND: '50%'
|
||||
}
|
||||
|
||||
// 阴影配置
|
||||
const SHADOWS = {
|
||||
SMALL: '0 2rpx 10rpx rgba(0, 0, 0, 0.1)',
|
||||
NORMAL: '0 4rpx 20rpx rgba(0, 0, 0, 0.15)',
|
||||
LARGE: '0 8rpx 30rpx rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
|
||||
// 动画配置
|
||||
const ANIMATIONS = {
|
||||
DURATION_SHORT: 200,
|
||||
DURATION_NORMAL: 300,
|
||||
DURATION_LONG: 500,
|
||||
EASING: 'ease-in-out'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
API_BASE,
|
||||
WEBSOCKET_URL,
|
||||
TASK_STATUS,
|
||||
TASK_TYPE,
|
||||
MESSAGE_TYPE,
|
||||
STORAGE_KEYS,
|
||||
NETWORK_TIMEOUT,
|
||||
RETRY_CONFIG,
|
||||
PAGINATION,
|
||||
UPLOAD_LIMITS,
|
||||
WS_MESSAGE_TYPE,
|
||||
SYSTEM_CONFIG,
|
||||
ERROR_CODES,
|
||||
ERROR_MESSAGES,
|
||||
DEFAULT_AVATAR,
|
||||
DEFAULT_ICONS,
|
||||
COLORS,
|
||||
FONT_SIZES,
|
||||
SPACING,
|
||||
BORDER_RADIUS,
|
||||
SHADOWS,
|
||||
ANIMATIONS
|
||||
}
|
||||
310
Claw/client/wechat_app/utils/util.js
Normal file
310
Claw/client/wechat_app/utils/util.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// 通用工具函数
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date} date - 日期对象
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
function formatTime(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return [year, month, day].map(formatNumber).join('/') + ' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字,补零
|
||||
* @param {number} n - 数字
|
||||
* @returns {string} 格式化后的数字字符串
|
||||
*/
|
||||
function formatNumber(n) {
|
||||
n = n.toString()
|
||||
return n[1] ? n : '0' + n
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {number} timestamp - 时间戳
|
||||
* @returns {string} 相对时间描述
|
||||
*/
|
||||
function formatRelativeTime(timestamp) {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
const week = 7 * day
|
||||
const month = 30 * day
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return Math.floor(diff / minute) + '分钟前'
|
||||
} else if (diff < day) {
|
||||
return Math.floor(diff / hour) + '小时前'
|
||||
} else if (diff < week) {
|
||||
return Math.floor(diff / day) + '天前'
|
||||
} else if (diff < month) {
|
||||
return Math.floor(diff / week) + '周前'
|
||||
} else {
|
||||
return formatTime(new Date(timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要执行的函数
|
||||
* @param {number} wait - 等待时间(毫秒)
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 要执行的函数
|
||||
* @param {number} limit - 限制时间(毫秒)
|
||||
* @returns {Function} 节流后的函数
|
||||
*/
|
||||
function throttle(func, limit) {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
* @param {Object} obj - 要拷贝的对象
|
||||
* @returns {Object} 拷贝后的对象
|
||||
*/
|
||||
function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj
|
||||
if (obj instanceof Date) return new Date(obj.getTime())
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item))
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象是否为空
|
||||
* @param {Object} obj - 要判断的对象
|
||||
* @returns {boolean} 是否为空
|
||||
*/
|
||||
function isEmpty(obj) {
|
||||
if (obj == null) return true
|
||||
if (obj.length > 0) return false
|
||||
if (obj.length === 0) return true
|
||||
if (typeof obj !== 'object') return true
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {string} 唯一ID
|
||||
*/
|
||||
function generateUniqueId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validatePhone(phone) {
|
||||
return /^1[3-9]\d{9}$/.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
* @param {string} email - 邮箱
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validateEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证URL
|
||||
* @param {string} url - URL
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validateUrl(url) {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param {string} filename - 文件名
|
||||
* @returns {string} 扩展名
|
||||
*/
|
||||
function getFileExtension(filename) {
|
||||
return filename.split('.').pop().toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes - 字节数
|
||||
* @returns {string} 格式化后的文件大小
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机生成字符串
|
||||
* @param {number} length - 字符串长度
|
||||
* @returns {string} 随机字符串
|
||||
*/
|
||||
function randomString(length) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
* @param {string} str - 字符串
|
||||
* @returns {string} 首字母大写的字符串
|
||||
*/
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰命名转下划线
|
||||
* @param {string} str - 驼峰命名字符串
|
||||
* @returns {string} 下划线命名字符串
|
||||
*/
|
||||
function camelToSnake(str) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线命名转驼峰
|
||||
* @param {string} str - 下划线命名字符串
|
||||
* @returns {string} 驼峰命名字符串
|
||||
*/
|
||||
function snakeToCamel(str) {
|
||||
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组去重
|
||||
* @param {Array} arr - 数组
|
||||
* @returns {Array} 去重后的数组
|
||||
*/
|
||||
function uniqueArray(arr) {
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组分组
|
||||
* @param {Array} arr - 数组
|
||||
* @param {Function} keyFn - 分组键函数
|
||||
* @returns {Object} 分组后的对象
|
||||
*/
|
||||
function groupBy(arr, keyFn) {
|
||||
return arr.reduce((groups, item) => {
|
||||
const key = keyFn(item)
|
||||
if (!groups[key]) {
|
||||
groups[key] = []
|
||||
}
|
||||
groups[key].push(item)
|
||||
return groups
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL参数
|
||||
* @param {string} url - URL字符串
|
||||
* @returns {Object} URL参数对象
|
||||
*/
|
||||
function getUrlParams(url) {
|
||||
const params = {}
|
||||
const urlObj = new URL(url)
|
||||
for (const [key, value] of urlObj.searchParams) {
|
||||
params[key] = value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL参数
|
||||
* @param {Object} params - 参数对象
|
||||
* @returns {string} URL参数字符串
|
||||
*/
|
||||
function buildUrlParams(params) {
|
||||
return Object.keys(params)
|
||||
.filter(key => params[key] !== null && params[key] !== undefined)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime,
|
||||
formatNumber,
|
||||
formatRelativeTime,
|
||||
debounce,
|
||||
throttle,
|
||||
deepClone,
|
||||
isEmpty,
|
||||
generateUniqueId,
|
||||
validatePhone,
|
||||
validateEmail,
|
||||
validateUrl,
|
||||
getFileExtension,
|
||||
formatFileSize,
|
||||
randomString,
|
||||
capitalize,
|
||||
camelToSnake,
|
||||
snakeToCamel,
|
||||
uniqueArray,
|
||||
groupBy,
|
||||
getUrlParams,
|
||||
buildUrlParams
|
||||
}
|
||||
Reference in New Issue
Block a user