小程序修正

This commit is contained in:
2025-12-23 13:27:36 +08:00
parent cfb160cf09
commit ce66812c82
48 changed files with 766 additions and 735 deletions

View File

@@ -10,30 +10,30 @@
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
flex-direction: row;
align-items: flex-end;
padding-left: 24rpx;
padding-right: 24rpx;
padding-bottom: 16rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
.nav-back-icon {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
border-left: 4rpx solid #333;
border-bottom: 4rpx solid #333;
transform: rotate(45deg);
}
@@ -43,6 +43,7 @@
font-weight: 600;
color: #222;
margin-left: 16rpx;
line-height: 64rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -6,7 +6,7 @@
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">{{ roomName }}</text>
<view class="nav-actions">

View File

@@ -8,57 +8,55 @@
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
align-items: flex-end;
padding-left: 24rpx;
padding-right: 24rpx;
padding-bottom: 16rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
.nav-back-icon {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
border-left: 4rpx solid #333;
border-bottom: 4rpx solid #333;
transform: rotate(45deg);
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #222;
flex: 1;
font-size: 34rpx;
font-weight: 500;
color: #333;
text-align: center;
padding-right: 174rpx;
line-height: 64rpx;
}
.nav-capsule {
width: 174rpx;
height: 64rpx;
border-radius: 32rpx;
}
.list {
margin-top: 176rpx;
padding: 20rpx 24rpx;
padding-bottom: 60rpx;
}
.room-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx;
background: #fff;
@@ -85,12 +83,14 @@
.room-info {
flex: 1;
flex-direction: column;
margin-left: 20rpx;
overflow: hidden;
}
.room-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
@@ -115,6 +115,7 @@
.room-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}

View File

@@ -4,16 +4,16 @@
<!-- #endif -->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav" :style="{ paddingTop: navPaddingTop + 'px', height: navHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">我的聊天室</text>
<view class="nav-capsule"></view>
</view>
<!-- 聊天室列表 -->
<scroll-view class="list" scroll-y="true" :style="{ marginTop: headerTotalHeight + 'px' }">
<scroll-view class="list" scroll-y="true" :style="{ marginTop: navHeight + 'px' }">
<view class="room-card" v-for="(room, index) in chatRooms" :key="index" @tap="enterRoom(room)">
<view class="room-avatar">
<text class="avatar-text">{{ room.guestName?.charAt(0) || '客' }}</text>
@@ -53,9 +53,10 @@
import { ref, onMounted } from 'vue'
import type { ChatRoomVO } from '@/types/workcase'
// 响应式数据
const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88)
// 导航栏
const navPaddingTop = ref<number>(0)
const navHeight = ref<number>(44)
const capsuleHeight = ref<number>(32)
// 聊天室列表
const chatRooms = ref<ChatRoomVO[]>([
@@ -94,24 +95,23 @@ const chatRooms = ref<ChatRoomVO[]>([
// 生命周期
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
// #endif
}
})
// #ifdef MP-WEIXIN
try {
const menuButton = uni.getMenuButtonBoundingClientRect()
navPaddingTop.value = menuButton.top
capsuleHeight.value = menuButton.height
navHeight.value = menuButton.bottom + 8
} catch (e) {
const sysInfo = uni.getSystemInfoSync()
navPaddingTop.value = sysInfo.statusBarHeight || 20
navHeight.value = navPaddingTop.value + 44
}
// #endif
// #ifndef MP-WEIXIN
const sysInfo = uni.getSystemInfoSync()
navPaddingTop.value = sysInfo.statusBarHeight || 20
navHeight.value = navPaddingTop.value + 44
// #endif
loadChatRooms()
})

View File

@@ -94,6 +94,8 @@
position: absolute;
width: 100%;
height: 100%;
background: none !important;
border: none !important;
}
.ring {
@@ -102,17 +104,29 @@
top: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 1px solid rgba(100, 180, 255, 0.15);
background: none !important;
border: none !important;
}
.r1 { width: 260px; height: 260px; }
.r2 { width: 200px; height: 200px; border-color: rgba(100, 180, 255, 0.2); }
.r3 { width: 150px; height: 150px; border-color: rgba(100, 180, 255, 0.25); }
.r4 { width: 110px; height: 110px; border-color: rgba(100, 180, 255, 0.35); }
.r2 { width: 200px; height: 200px; }
.r3 { width: 150px; height: 150px; }
.r4 { width: 110px; height: 110px; }
.robot {
position: relative;
z-index: 2;
width: 140px;
height: 140px;
/* 父容器加一层径向渐变背景,模拟外层模糊光晕 */
background: radial-gradient(circle at center,
rgba(180, 220, 255, 0.5) 0%,
rgba(180, 220, 255, 0.25) 50%,
transparent 75%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.robot-face {
@@ -125,7 +139,10 @@
align-items: center;
justify-content: center;
gap: 16px;
box-shadow: 0 10px 40px rgba(180, 220, 255, 0.5), inset 0 0 20px rgba(255, 255, 255, 0.8);
/* 只保留内阴影,外模糊交给父容器的径向渐变 */
box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.8);
/* 取消overflow:hidden避免裁切父容器的渐变光晕 */
overflow: visible;
}
.eye {
@@ -138,9 +155,9 @@
.float-tag {
position: absolute;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(200, 220, 255, 0.5);
border-radius: 16px;
background: transparent; /* 去掉背景色,和左侧一致 */
border: none; /* 去掉边框 */
border-radius: 0; /* 保持直角如果需要圆角也可以改回16px */
font-size: 12px;
color: #666;
}
@@ -192,7 +209,7 @@
padding-bottom: 120px;
position: relative;
// 为固定定位的header留出空间
margin-top: 76px; // 默认header高度
margin-top: 50px; // 默认header高度
}
// 欢迎界面

View File

@@ -112,26 +112,19 @@
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import { guestAPI, workcaseChatAPI } from '@/api'
import type { TbWorkcaseDTO } from '@/types'
// 接口定义
interface Message {
type : 'user' | 'bot'
content : string
time : string
actions ?: string[] | null
}
interface WorkcaseData {
title : string
category : string
priority : string
description : string
contact : string
images : string[]
// 前端消息展示类型
interface ChatMessageItem {
type: 'user' | 'bot'
content: string
time: string
actions?: string[] | null
}
// 响应式数据
const messages = ref<Message[]>([])
const messages = ref<ChatMessageItem[]>([])
const inputText = ref<string>('')
const isTyping = ref<boolean>(false)
const scrollTop = ref<number>(0)
@@ -144,12 +137,17 @@
const userInfo = ref({
wechatId: '',
username: '',
phone: ''
phone: '',
userId: ''
})
const isMockMode = ref(true) // 开发环境mock模式
// AI 对话相关
const chatId = ref<string>('') // 当前会话ID
const currentTaskId = ref<string>('') // 当前任务ID用于停止
// 初始化用户信息
function initUserInfo() {
async function initUserInfo() {
// #ifdef MP-WEIXIN
// 正式环境:从微信获取用户信息
// wx.login({
@@ -165,9 +163,10 @@
userInfo.value = {
wechatId: '17857100375',
username: '测试用户',
phone: '17857100375'
phone: '17857100375',
userId: ''
}
doIdentify()
await doIdentify()
}
}
@@ -177,9 +176,9 @@
itemList: ['员工 (17857100375)', '访客 (17857100376)'],
success: (res) => {
if (res.tapIndex === 0) {
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375' }
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
} else {
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376' }
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376', userId: '' }
}
doIdentify()
}
@@ -187,31 +186,29 @@
}
// 调用identify接口
function doIdentify() {
async function doIdentify() {
uni.showLoading({ title: '登录中...' })
uni.request({
url: 'http://localhost:8180/urban-lifeline/system/guest/identify',
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { wechatId: userInfo.value.wechatId, phone: userInfo.value.phone },
success: (res : any) => {
uni.hideLoading()
if (res.statusCode === 200 && res.data?.success) {
const loginDomain = res.data.data
uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('wechatId', userInfo.value.wechatId)
console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' })
} else {
console.error('identify失败:', res.data?.message)
}
},
fail: (err) => {
uni.hideLoading()
console.error('identify请求失败:', err)
try {
const res = await guestAPI.identify({
wechatId: userInfo.value.wechatId,
phone: userInfo.value.phone
})
uni.hideLoading()
if (res.success && res.data) {
const loginDomain = res.data
uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('wechatId', userInfo.value.wechatId)
userInfo.value.userId = loginDomain.user?.userId || ''
console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' })
} else {
console.error('identify失败:', res.message)
}
})
} catch (err) {
uni.hideLoading()
console.error('identify请求失败:', err)
}
}
// 生命周期
@@ -259,7 +256,7 @@
})
// 发送消息
function sendMessage() {
async function sendMessage() {
const text = inputText.value.trim()
if (!text || isTyping.value) return
@@ -267,8 +264,115 @@
addMessage('user', text)
inputText.value = ''
// 模拟AI回复
simulateAIResponse(text)
// 调用AI聊天接口
await callAIChat(text)
}
// 调用AI聊天接口
async function callAIChat(query : string) {
isTyping.value = true
try {
// 如果没有会话ID先创建会话
if (!chatId.value) {
const createRes = await workcaseChatAPI.createChat({
title: '智能助手对话',
userId: userInfo.value.userId || userInfo.value.wechatId
})
if (createRes.success && createRes.data) {
chatId.value = createRes.data.chatId || ''
console.log('创建会话成功:', chatId.value)
} else {
throw new Error(createRes.message || '创建会话失败')
}
}
// 准备流式对话
const prepareRes = await workcaseChatAPI.prepareChatMessageSession({
chatId: chatId.value,
message: query
})
if (!prepareRes.success || !prepareRes.data) {
throw new Error(prepareRes.message || '准备对话失败')
}
const sessionId = prepareRes.data
console.log('准备流式对话成功:', sessionId)
// 添加空的AI消息占位
const messageIndex = messages.value.length
addMessage('bot', '')
// 建立SSE连接
streamChat(sessionId, messageIndex)
} catch (error : any) {
console.error('AI聊天失败:', error)
isTyping.value = false
addMessage('bot', '抱歉AI服务暂时不可用请稍后重试。')
}
}
// SSE 流式对话
function streamChat(sessionId : string, messageIndex : number) {
const url = `http://localhost:8180${workcaseChatAPI.getStreamUrl(sessionId)}`
console.log('建立SSE连接:', url)
const requestTask = uni.request({
url: url,
method: 'GET',
header: { 'Accept': 'text/event-stream' },
enableChunked: true,
success: (res : any) => {
console.log('SSE请求完成:', res)
isTyping.value = false
},
fail: (err) => {
console.error('SSE请求失败:', err)
isTyping.value = false
messages.value[messageIndex].content = '抱歉,网络连接失败,请稍后重试。'
}
})
// 监听分块数据
requestTask.onChunkReceived((res : any) => {
try {
const decoder = new TextDecoder('utf-8')
const text = decoder.decode(new Uint8Array(res.data))
console.log('收到分块数据:', text)
const lines = text.split('\n')
for (const line of lines) {
if (line.startsWith('data:')) {
const dataStr = line.substring(5).trim()
if (dataStr && dataStr !== '[DONE]') {
try {
const data = JSON.parse(dataStr)
const event = data.event
if (event === 'message' || event === 'agent_message') {
if (data.answer) {
messages.value[messageIndex].content += data.answer
}
} else if (event === 'message_end') {
isTyping.value = false
if (data.task_id) {
currentTaskId.value = data.task_id
}
} else if (event === 'error') {
console.error('SSE错误:', data.message)
isTyping.value = false
messages.value[messageIndex].content = data.message || '抱歉,发生错误,请稍后重试。'
}
} catch (e) {
console.log('解析SSE数据失败:', dataStr)
}
}
}
}
} catch (e) {
console.error('处理分块数据失败:', e)
}
nextTick(() => scrollToBottom())
})
}
// 添加消息
@@ -359,7 +463,7 @@
}
// 工单创建成功
function onWorkcaseCreated(workcaseData : WorkcaseData) {
function onWorkcaseCreated(workcaseData : TbWorkcaseDTO) {
hideCreator()
uni.showToast({
@@ -368,7 +472,7 @@
})
// 添加成功消息
addMessage('bot', `工单创建成功!\n标题${workcaseData.title}\n分类${workcaseData.category}\n我们会尽快处理您的问题。`, ['查看工单', '创建新工单'])
addMessage('bot', `工单创建成功!\n类型${workcaseData.type || ''}\n设备${workcaseData.device || ''}\n我们会尽快处理您的问题。`, ['查看工单', '创建新工单'])
}
// 跳转到工单列表
@@ -408,9 +512,9 @@
}
// 处理快速问题
function handleQuickQuestion(question : string) {
async function handleQuickQuestion(question : string) {
addMessage('user', question)
simulateAIResponse(question)
await callAIChat(question)
}
// 显示上传选项

View File

@@ -8,44 +8,45 @@
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
align-items: flex-end;
padding-left: 24rpx;
padding-right: 24rpx;
padding-bottom: 16rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
.nav-back-icon {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
border-left: 4rpx solid #333;
border-bottom: 4rpx solid #333;
transform: rotate(45deg);
}
.nav-title {
flex: 1;
font-size: 34rpx;
font-weight: 600;
color: #222;
font-weight: 500;
color: #333;
text-align: center;
line-height: 64rpx;
}
.nav-capsule {
width: 174rpx;
height: 64rpx;
border-radius: 32rpx;
}
.meeting-container {

View File

@@ -6,7 +6,7 @@
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">视频会议</text>
<view class="nav-capsule"></view>

View File

@@ -8,45 +8,45 @@
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
align-items: flex-end;
padding-left: 24rpx;
padding-right: 24rpx;
padding-bottom: 16rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
.nav-back-icon {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
border-left: 4rpx solid #333;
border-bottom: 4rpx solid #333;
transform: rotate(45deg);
}
.nav-title {
flex: 1;
font-size: 34rpx;
font-weight: 600;
color: #222;
font-weight: 500;
color: #333;
text-align: center;
line-height: 64rpx;
}
.nav-capsule {
width: 174rpx;
height: 64rpx;
background: #f5f5f5;
border-radius: 32rpx;
}
.content {
@@ -66,6 +66,7 @@
.section-title {
display: flex;
flex-direction: row;
align-items: center;
font-size: 30rpx;
font-weight: 600;
@@ -94,6 +95,7 @@
.info-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 0;
}
@@ -215,6 +217,7 @@
.timeline-item {
display: flex;
flex-direction: row;
position: relative;
padding-bottom: 32rpx;
}
@@ -248,10 +251,12 @@
.timeline-content {
flex: 1;
flex-direction: column;
}
.timeline-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8rpx;
}

View File

@@ -6,7 +6,7 @@
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">工单详情</text>
<view class="nav-capsule"></view>

View File

@@ -8,49 +8,45 @@
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
align-items: flex-end;
padding-left: 24rpx;
padding-right: 24rpx;
padding-bottom: 16rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
.nav-back-icon {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
border-left: 4rpx solid #333;
border-bottom: 4rpx solid #333;
transform: rotate(45deg);
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #222;
flex: 1;
font-size: 34rpx;
font-weight: 500;
color: #333;
text-align: center;
padding-right: 174rpx;
box-sizing: border-box;
line-height: 64rpx;
}
.nav-capsule {
width: 174rpx;
height: 64rpx;
border-radius: 32rpx;
margin-left: auto;
}
.tabs {
@@ -61,6 +57,7 @@
height: 100rpx;
background: #fff;
display: flex;
flex-direction: row;
align-items: center;
z-index: 100;
}
@@ -115,6 +112,7 @@
.card-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 24rpx;
@@ -123,6 +121,7 @@
.card-title {
display: flex;
flex-direction: row;
align-items: center;
font-size: 30rpx;
font-weight: 600;
@@ -184,10 +183,13 @@
.card-body {
padding: 24rpx;
display: flex;
flex-direction: column;
}
.fault-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
@@ -231,6 +233,7 @@
.info-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 16rpx;
}
@@ -249,6 +252,7 @@
.card-footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;

View File

@@ -6,7 +6,7 @@
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">我的工单</text>
<view class="nav-capsule"></view>