智能安全检查移动端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

896 lines
28 KiB

<template>
<view class="container">
<uni-nav-bar :fontSizes="17" dark left-icon="left" @clickLeft="back" :fixed="true" :border="false"
background-color="#3E73F5" status-bar title="智能安全检查" />
<!-- <image class="bg-img" src="https://mp-df79fe8b-b924-41b0-bcb1-960be6b4a619.cdn.bspapp.com/images/ask/content-bg@2x.png" /> -->
<image class="bg-img" src="https://i.postimg.cc/PxcQrX3C/content-bg-2x.png" />
<scroll-view class="main-scroll" scroll-y scroll-with-animation lower-threshold="150" ref="chatArea"
@scroll="handleScroll" @scrolltolower="lower">
<!-- 猜你想问 -->
<view class="guess-section">
<view class="guess-title">常见问题</view>
<view class="guess-item" v-for="(item, index) in guessList" :key="index" @click="handleGuessClick(item)">
<text class="question">{{ item.question }}</text>
<!-- <uni-icons type="right" size="18" color="#999"></uni-icons> -->
</view>
</view>
<!-- 聊天内容区 -->
<view class="chat-area">
<view v-for="(msg, idx) in messages" :key="idx">
<view :class="[msg.role + 's']">
<view :class="['msg', msg.role]" v-if="msg.role === 'user'">{{ msg.content }}</view>
<view :class="['msg', msg.role]" v-else v-html="parseMarkdown(msg.content)"></view>
<!-- 添加按钮,仅对 AI 回答显示 -->
<view v-if="msg.role === 'assistant' && userType == 1 || msg.role === 'assistant' && userType == 2"
class="add-btn-box">
<button v-if="!streaming" class="add-btn" @click="toggleAnswer(msg.content, idx)"
:class="{ 'added': selectedAnswerIds.has(idx) }">
<!-- <span class="iconfont" :class="selectedAnswerIds.has(index) ? 'icon-jian' : 'icon-jia'"></span> -->
<uni-icons :type="selectedAnswerIds.has(idx) ? 'trash' : 'plusempty'" size="22"
color="#fff"></uni-icons>
</button>
</view>
</view>
<view v-if="streaming && idx == messages.length - 1 && msg.role == 'assistant'" class="dots">
<view class="dotss"></view>
<view class="dotss"></view>
<view class="dotss"></view>
</view>
</view>
<!-- <view v-if="streaming" class="msg assistant">
<view v-html="parseMarkdown(streamingContent)"></view>
</view> -->
</view>
<view ref="scrollIntoViewId" :id="scrollIntoViewId" style="height: 1px;"></view>
</scroll-view>
<view v-if="selectedAnswers.length > 0" class="save-bar">
<!-- <div class="save-bar"> -->
<button class="save-btn" @click="saveAnswers">保存所选回答</button>
</view>
<!-- 底部输入框 -->
<view class="input-area">
<input class="chat-input" type="text" v-model="inputValue" :disabled="streaming"
:placeholder="activeTab === 'suggest' ? '请输入检查内容..' : '请输入你的问题或需求...'" @confirm="sendMessage" />
<button class="send-btn" @click="sendMessage" :disabled="!inputValue || streaming">
<!-- <image src="https://mp-df79fe8b-b924-41b0-bcb1-960be6b4a619.cdn.bspapp.com/images/ask/btn@2x.png" /> -->
<image src="https://i.postimg.cc/RqxF0KQW/btn-2x.png" />
</button>
</view>
</view>
</template>
<script>
import { marked } from 'marked';
import * as Api from '@/api/index/index'
import * as TextEncoding from "text-encoding-shim";
import store from '@/store'
let buffer = ''; //定义在页面的最外面。
let encoder = new TextEncoding.TextDecoder("utf-8");//定义在页面的最外面。
export default {
data() {
return {
activeTab: 'suggest',
random: '',
questId: 0,
messages: [],
inputValue: '',
selectedAnswers: [],
selectedAnswerIds: new Set(), // 使用 Set 存储已选答案的 id
streaming: false,
streamingContent: '',
scrollIntoViewId: '',
autoScroll: true,
userType: this.$store.state.user.userType,
tools: [
{ label: '客流分析', value: 'suggest', icon: '/static/icon_passenger-flow.svg', text: '景区舒适度实时知晓' },
{ label: '智能助手', value: 'assistant', icon: '/static/icon_assistant.svg', text: '行程规划与导览' },
],
guessList: [
{
type: 'suggest',
id: 0,
question: '游客休息区附近的灭火器随意摆放在地面,多个灭火器铭牌面朝墙壁,其中1具灭火器顶部高于1.6米。'
}, {
type: 'suggest',
id: 1,
question: '电脑桌下插座超负荷使用,存在多个插排串联现象,电缆未做穿管保护,线路裸露。'
}, {
type: 'suggest',
id: 2,
question: '安全出口被杂物堵塞,部分疏散指示灯不亮,房间内无疏散图,未张贴疏散方向指示。'
}, {
type: 'suggest',
id: 3,
question: '场馆未设置专职消防安全管理人员,无消防巡查记录,消火栓箱内设备锈蚀,疏散通道部分设有铁门限制通行。'
}
]
};
},
mounted() {
this.random = Array.from({ length: 12 }, () =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.charAt(
Math.floor(Math.random() * 62)
)
).join('');
//获取常见问题
this.getNoticeList()
},
methods: {
//常见问题
getNoticeList() {
Api.getNoticeList().then(res => {
const arr = res.data.filter(item => item.status === true).slice(0, 5);
console.log(res, arr);
if (arr.length > 0) {
this.guessList = arr;
}
}).catch(err => {
console.log(err);
})
}
,
back() {
uni.navigateBack()
},
parseMarkdown(content) {
return marked.parse(content || '');
},
handleScroll(e) {
const { scrollTop, scrollHeight, clientHeight } = e.detail;
this.autoScroll = false;
console.log(this.autoScroll, 'this.autoScroll')
},
lower: function (e) {
console.log(e)
},
scrollToBottom() {
let this_ = this;
// this.scrollIntoViewId = ''
if (!this.autoScroll) return;
this.scrollIntoViewId = 'bottom-anchor-' + Date.now();
// console.log(this.scrollIntoViewId, 'this.scrollIntoViewId', this.$refs.scrollIntoViewId)
this.$nextTick(() => {
uni.pageScrollTo({
selector: '#' + this.scrollIntoViewId,
duration: 10,
complete: () => {
console.log(this.autoScroll, 'this.autoScroll')
}
})
})
// setTimeout(() => {
// uni.pageScrollTo({
// selector: '#' + this.scrollIntoViewId,
// duration: 1000,
// })
// }, 1000)
},
switchTab(tab) {
this.activeTab = tab;
this.messages = [];
this.inputValue = '';
this.streaming = false;
this.streamingContent = '';
this.scrollToBottom()
// this.$nextTick(this.scrollToBottom);
},
sendMessage() {
this.autoScroll = true;
const question = this.inputValue.trim();
if (!question || this.streaming) return;
this.messages.push({ role: 'user', content: question });
this.inputValue = '';
this.scrollToBottom()
// this.$nextTick(this.scrollToBottom);
this.streamAnswer(question);
},
parseSSEEvent(rawData) {
const lines = rawData.split('\n');
let event = { data: '' };
lines.forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const field = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
if (field === 'data') {
event.data += value + '\n';
} else if (field === 'event') {
event.type = value;
} else if (field === 'id') {
event.id = value;
} else if (field === 'retry') {
event.retry = parseInt(value, 10);
}
}
});
event.data = event.data.trimEnd(); // 移除末尾换行
return event.data ? event : null;
},
extractAllMarkdownContents(visData) {
const markdownContents = [];
try {
// 使用正则表达式匹配所有 agent-messages 块
const agentMessagesRegex = /```agent-messages\n\[(.*?)\]\n```/gs;
let match;
while ((match = agentMessagesRegex.exec(visData)) !== null) {
try {
// 解析匹配到的 JSON 字符串
const messageJson = match[1].replace(/\\"/g, '"');
const messageData = JSON.parse(messageJson);
// 只提取 markdown 字段,并在前面加上【隐患描述】标题
if (messageData.markdown) {
// 在原始内容前添加【隐患描述】标题
// const contentWithTitle = `【隐患描述】\n\n${messageData.markdown}`;
markdownContents.push(messageData.markdown);
}
} catch (e) {
console.log('解析单个 agent-message 失败:', e);
}
}
} catch (e) {
console.log('解析 agent-messages 失败:', e);
}
return markdownContents;
},
async streamAnswer(question) {
this.streaming = true;
// this.streamingContent = '正在思考中...';
const msg = { role: 'assistant', content: '正在思考中...' };
this.messages.push(msg);
this.scrollToBottom();
let result = '';
// 缓存池(半截事件用)
this.sseBuffer = "";
const url = 'https://e629859.r39.cpolar.top/agent-api/api/v3/user_share_chat_completions'
+ '?api_key=cjy-c8c36aa2ffa64aa5b4b1e89b14166a4c'
+ '&app_code=ebccfa05-78ab-11f0-9afa-00e04f3085ba'
+ '&random=' + this.random + '&user_input=' + encodeURIComponent(question);
const requestTask = wx.request({
url: url,
method: "GET",
header: {
Accept: "text/event-stream",
},
responseType: "arraybuffer",
enableChunked: true,
success(res) {
console.log("连接成功")
},
fail(err) {
console.error("请求失败:", err)
},
})
requestTask.onChunkReceived(async (res) => {
try {
// 将ArrayBuffer转为字符串并追加到缓冲区
let arrayBuffer = new Uint8Array(res.data)
let chunkStr = encoder.decode(arrayBuffer);
buffer += chunkStr;
// 分割完整事件(以\n\n分隔)
let eventEndIndex;
while ((eventEndIndex = buffer.indexOf('\n\n')) >= 0) {
const eventData = buffer.slice(0, eventEndIndex);
buffer = buffer.slice(eventEndIndex + 2);
// 解析SSE事件内容
const message = this.parseSSEEvent(eventData);
if (message) {
console.log('收到事件:', message);
// 触发自定义事件或更新数据
//数据拿到后,做自己的业务处理
const data = message.data.trim(); // 去掉 'data:' 前缀
// 检查是否为结束标记
if (data === '[DONE]') {
continue;
}
try {
const jsonData = JSON.parse(data);
// 提取 vis 字段内容
if (jsonData.vis && jsonData.vis !== '[DONE]') {
// 提取 agent-messages 中的 markdown 内容
const markdownContents = this.extractAllMarkdownContents(jsonData.vis);
if (markdownContents.length > 0) {
markdownContents[0] = `【隐患描述】\n\n ${markdownContents[0]} `;
console.log('markdownContents:', markdownContents);
result = markdownContents.join('\n\n'); // 合并所有markdown内容
msg.content = result;
this.scrollToBottom();
}
} else {
this.streaming = false;
}
} catch (e) {
console.log('解析JSON失败:', e);
} finally {
// this.streaming = false;
}
}
}
} catch (e) {
console.error('数据处理异常:', e);
} finally {
// this.streaming = false;
}
})
},
reBack(questId) {
console.log(questId, 'questId')
switch (questId) {
case 0:
return `
**【隐患描述】**:
游客休息区附近的灭火器随意摆放在地面,多个灭火器铭牌面朝墙壁,其中1具灭火器顶部高于1.6米。
**风险判定**:
存在中度消防安全隐患。
**【整改依据】**:
1. 《建筑灭火器配置设计规范》GB50140-2005 第5.1.3条:
> 灭火器的摆放应稳固,其铭牌应朝外。手提式灭火器宜设置在灭火器箱内或挂钩、托架上,其顶部离地面高度不应大于1.50m;底部离地面高度不宜小于0.08m。灭火器箱不得上锁。
2. 《中华人民共和国消防法》(2021年修订)第二十八条:
> 任何单位、个人不得损坏、挪用或者擅自拆除、停用消防设施、器材;不得埋压、圈占、遮挡消火栓或者占用防火间距;不得占用、堵塞、封闭疏散通道、安全出口、消防车通道。
**【整改建议】**:
- 所有灭火器应重新安装在灭火器箱或墙壁挂钩/托架上;
- 调整高度符合规范要求(顶部不高于1.50m);
- 保证铭牌朝外,方便检查与识别。
**【隐患等级】**:
中度风险。
`;
case 1:
return `
**问题描述**:
电脑桌下插座超负荷使用,存在多个插排串联现象,电缆未做穿管保护,线路裸露。
**风险判定**:
重大用电安全隐患。
**整改依据**:
1. 《用电安全导则》GB/T 13869-2017 第5.1.1条:
> 应禁止电气设备使用中串接插座(插排)进行多台设备连接,防止过载引发火灾。
2. 《建筑电气工程施工质量验收规范》GB50303-2015 第5.1.1:
> 导线敷设应平整,无扭结、无机械损伤,穿越楼板、墙体等处应加套管保护。
3. 《中华人民共和国安全生产法》第三十五条:
> 生产经营单位应当在有较大危险因素的生产经营场所和有关设施、设备上,设置明显的安全警示标志。
**整改建议**:
- 拆除串联插排,使用定制排插或分路供电;
- 所有电缆线路穿线管加装防护,避免裸露;
- 在高风险位置张贴电气警示标志。
**风险评级**:
重大风险。
`;
case 2:
return `
**问题描述**:
安全出口被杂物堵塞,部分疏散指示灯不亮,房间内无疏散图,未张贴疏散方向指示。
**风险判定**:
重大人员疏散安全隐患。
**整改依据**:
1. 《消防应急照明和疏散指示系统技术标准》GB51309-2018 第4.5.10条:
> 疏散指示标志应设置在通道明显位置,指明疏散方向,应能在火灾断电时自动点亮。
2. 《人员密集场所消防安全管理》GB/T 40248-2021 第7.5.2.j条:
> 宾馆、商场、医院、公共娱乐场所等场所各楼层的明显位置应设置安全疏散指示图,标明疏散路线、安全出口、人员所在位置等。
3. 《中华人民共和国消防法》(2021年修订)第二十八条:
> 不得占用、堵塞、封闭疏散通道、安全出口。
**整改建议**:
- 清除安全出口周边杂物,保持通畅;
- 检查疏散照明电源,维修损坏灯具,确保断电可自动亮起;
- 制作疏散图并张贴在包间门背面及走道显著位置。
**风险评级**:
重大风险。
`;
case 3:
return `
**问题描述**:
场馆未设置专职消防安全管理人员,无消防巡查记录,消火栓箱内设备锈蚀,疏散通道部分设有铁门限制通行。
**风险判定**:
制度缺失 + 消防设施老化 + 疏散障碍,构成重大管理风险。
**整改依据**:
1. 《中华人民共和国消防法》第十六条第二款:
> 机关、团体、企业、事业等单位应当按照国家标准、行业标准配置消防设施、器材,设置消防安全标志,并定期组织检验、维修,确保完好有效。
2. 《河北省安全生产风险管控与隐患治理规定》(省政府2号令)第十五条:
> 在有较大及以上等级风险的生产经营场所显著位置、关键部位和有关设施设备上应当设置明显警示标志、标识。
3. 《消防设施的维护管理》GB25201-2010 第5.2条:
> 消防设施应每月检查1次,每季度维护保养1次,发现损坏应及时修复。
4. 《建筑设计防火规范》GB50016-2014 第6.4.1条:
> 建筑的疏散通道、安全出口不得设置影响人员疏散的障碍物。
**整改建议**:
- 指定专人负责消防巡查,建立消防巡检制度并执行记录;
- 更换损坏消防器材,清理锈蚀水带;
- 疏散通道去除铁门等障碍物,保持畅通;
- 在关键部位张贴明显警示标志。
**风险评级**:
重大风险。
`;
default:
return '';
}
},
mockStreamAnswer(question) {
this.streaming = true;
this.streamingContent = '';
const answer = this.activeTab === 'suggest'
?
this.reBack(this.questId)
: '这是针对“游玩助手”的流式回复内容,模拟逐字输出效果。';
let i = 0;
const stream = () => {
if (i < answer.length) {
this.streamingContent += answer[i++];
// this.scrollToBottom()
this.$nextTick(this.scrollToBottom());
setTimeout(stream, 10);
} else {
this.messages.push({ role: 'assistant', content: this.streamingContent });
this.streaming = false;
this.streamingContent = '';
// this.scrollToBottom()
this.$nextTick(this.scrollToBottom());
}
};
stream();
},
handleGuessClick(question) {
this.activeTab = question.type;
this.questId = question.id;
if (this.streaming) return;
this.inputValue = '';
this.messages.push({ role: 'user', content: question.question });
this.scrollToBottom()
// this.$nextTick(this.scrollToBottom);
// this.mockStreamAnswer(question.question);
this.streamAnswer(question.question);
},
toggleAnswer(content, index) {
console.log('selectedAnswers:', this.selectedAnswers);
if (this.selectedAnswerIds.has(index)) {
// 如果已添加,则移除
this.selectedAnswerIds.delete(index);
const removeIndex = this.selectedAnswers.indexOf(content);
if (removeIndex > -1) {
this.selectedAnswers.splice(removeIndex, 1);
}
} else {
// 如果未添加,则添加
this.selectedAnswerIds.add(index);
// if (!this.selectedAnswers.includes(content)) {
this.selectedAnswers.push(content);
// }
}
},
saveAnswers() {
const data = this.selectedAnswers;
// const data = ['222', '111'] ;
// debugger;
const jsonData = JSON.stringify(data);
uni.navigateTo({
url: '/pageIndex/addSafeCheck/addSafeCheck?data=' + encodeURIComponent(jsonData),
})
this.selectedAnswers = [];
this.selectedAnswerIds = new Set();
},
}
}
</script>
<style lang="scss">
page {
background-color: #F5F6FB;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
background: #F5F6FB;
.bg-img {
width: 100%;
height: 700rpx;
position: fixed;
top: 0;
left: 0;
}
}
.main-scroll {
width: 100%;
// height: calc(100vh - var(--window-top) - 200rpx);
flex: 1;
overflow-y: auto;
position: relative;
// z-index: 999;
margin-bottom: 100rpx;
}
/* 顶部渐变区 */
.header {
text-align: center;
// background: linear-gradient(to bottom, #d8f1ff, #ffffff);
padding: 60rpx 40rpx 40rpx;
display: flex;
flex-direction: column;
}
.avatar {
width: 100rpx;
height: 100rpx;
margin: 0 auto 20rpx;
border-radius: 50%;
background: #a0d8ff;
image {
width: 100%;
height: 100%;
border-radius: 50%;
margin-top: 5px;
}
}
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.subtitle {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.content {
display: flex;
flex-direction: column;
align-items: baseline;
}
.tool-label {
font-weight: 500;
font-size: 28rpx;
color: #1B1B1B;
}
.tool-label-s {
font-weight: 400;
font-size: 22rpx;
color: #9DA9C2;
}
/* 猜你想问 */
.guess-section {
background: #fff;
border-radius: 20rpx;
margin: 20rpx;
padding: 0 20rpx 20rpx;
}
.guess-title {
font-size: 32rpx;
color: #007aff;
width: 200rpx;
height: 68rpx;
display: flex;
align-items: center;
justify-content: start;
padding-left: 5rpx;
// background: url('/static/gues.png');
background-position: center;
background-size: contain;
background-repeat: no-repeat;
padding-top: 20rpx;
}
.guess-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
font-size: 28rpx;
color: #333;
// padding-left: 40rpx;
position: relative;
border-bottom: 1rpx solid #eee;
margin-top: 10rpx;
.question {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.guess-item::before {
content: '';
width: 24rpx;
height: 24rpx;
// background-color: #eee;
position: absolute;
left: 10rpx;
top: 50%;
transform: translateY(-50%);
// background-image: url('/static/start@2x.png');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.guess-item:last-child {
border-bottom: none;
}
.arrow {
color: #ccc;
font-size: 28rpx;
}
/* 聊天气泡 */
.chat-area {
flex: 1;
padding: 20rpx;
overflow-y: auto;
// background: #f7f7f7;
width: unset !important;
}
.msg {
max-width: 70%;
padding: 20rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
word-break: break-word;
}
.msg.user {
align-self: flex-end;
background: #007aff;
color: white;
}
.users {
display: flex;
flex-direction: row-reverse;
}
.msg.assistant {
align-self: flex-start;
background: #ffffff;
color: #333;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
.assistants {
display: flex;
align-items: flex-start;
}
.add-btn-box {
margin-top: 6px;
margin-left: 6px;
}
.add-btn {
background-color: #3e73f5;
color: #fff;
border: none;
padding: 4px 4px;
border-radius: 6px;
font-size: 14px;
display: flex;
width: 50rpx;
height: 50rpx;
align-items: center;
justify-content: center;
}
.add-btn.added {
background-color: #ff4d4f;
}
.save-bar {
position: sticky;
padding: 8px;
text-align: center;
z-index: 10;
bottom: 100rpx;
width: 50%;
margin: 0 auto;
}
.save-btn {
background-color: #00c092;
color: #fff;
border: none;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 28rpx;
font-weight: bold;
}
/* 底部输入栏 */
.input-area {
display: flex;
align-items: center;
padding: 0 5%;
margin-bottom: 11px;
position: fixed;
bottom: 10px;
z-index: 999;
width: -webkit-fill-available;
}
.chat-input {
flex: 1;
height: 40px;
border: 1px solid #B0B6C2;
border-radius: 20px;
padding: 0 20rpx;
font-size: 28rpx;
background: #fafafa;
}
.send-btn {
width: 56rpx;
height: 56rpx;
flex: none;
margin-left: 16rpx;
padding: 4rpx;
line-height: 40px;
// background: #007aff;
color: #fff;
border: none;
border-radius: 20rpx;
font-size: 28rpx;
// background-image: url('/static/icon_AI_bg.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
display: flex;
align-items: center;
justify-content: space-evenly;
padding-top: 10rpx;
background-image: linear-gradient(to bottom, #a06bd1, #9779d9, #8f85df, #8990e4, #869be6, #7aa8ee, #70b4f4, #69bff8, #52cffe, #41dfff, #46eefa, #5ffbf1);
image {
width: 32rpx;
height: 32rpx;
}
&::after {
border: none !important;
}
}
.send-btn:disabled {
// background-image: url(/static/btn-bg@2x.png);
// background-repeat: no-repeat;
// background-position: center;
// background-size: contain;
opacity: 0.5;
}
::v-deep .chat-area {
display: flex;
flex-direction: column;
}
.dots {
width: 3.5em;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
margin: 0.5em 1em;
}
.dots .dotss {
width: 0.8em;
height: 0.8em;
border-radius: 50%;
background-color: #007aff50;
animation: fade 0.8s ease-in-out alternate infinite;
}
.dots .dotss:nth-child(2) {
animation-delay: 0.2s;
}
.dots .dotss:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes fade {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
</style>