优化会议室预约
This commit is contained in:
parent
98e9cf72e7
commit
f735571c56
236
components/gx-upload.vue
Normal file
236
components/gx-upload.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="upload-area">
|
||||
<view class="upload-list">
|
||||
<view class="upload-item" v-for="(item, index) in fileList" :key="index">
|
||||
<image :src="item.url" mode="aspectFill" @click="previewImage(index)"></image>
|
||||
<view class="delete-btn" @click="handleDelete(index)">
|
||||
<u-icon name="close" color="#fff" size="24"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="upload-btn" @click="chooseMedia('album');" v-if="fileList.length < 1">
|
||||
<u-icon name="plus" size="40" color="#c0c4cc"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="note">注:电子印章必须为透明底的PNG格式,其他格式将不被接受</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BASE_URL } from '@/utils/config';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showUploadAction() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['拍照', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.chooseMedia('camera');
|
||||
} else {
|
||||
this.chooseMedia('album');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async chooseMedia(sourceType) {
|
||||
try {
|
||||
// 1. 选择图片
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: [sourceType === 'camera' ? 'camera' : 'album'],
|
||||
extension: ['png'],
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 验证文件格式
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
const isValid = await this.checkPngFormat(tempFilePath);
|
||||
|
||||
if (!isValid) {
|
||||
uni.showToast({ title: '请上传PNG格式图片', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 添加到预览列表
|
||||
this.fileList = [{
|
||||
url: tempFilePath,
|
||||
status: 'uploading'
|
||||
}];
|
||||
|
||||
// 4. 上传文件
|
||||
await this.uploadFile(this.fileList[0]);
|
||||
|
||||
} catch (err) {
|
||||
console.error('选择图片失败:', err);
|
||||
uni.showToast({
|
||||
title: err.errMsg.includes('cancel') ? '已取消选择' : '请选择PNG格式图片',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async checkPngFormat(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
// 获取文件后缀名
|
||||
const ext = filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase();
|
||||
if (ext !== 'png') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件头验证
|
||||
const fs = uni.getFileSystemManager();
|
||||
fs.readFile({
|
||||
filePath,
|
||||
encoding: 'binary',
|
||||
success: (readRes) => {
|
||||
// PNG文件头应该是 '\x89PNG\r\n\x1a\n'
|
||||
const isRealPng = readRes.data.startsWith('\x89PNG\r\n\x1a\n');
|
||||
resolve(isRealPng);
|
||||
},
|
||||
fail: () => resolve(false)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async uploadFile(file) {
|
||||
// 上传前再次验证
|
||||
const isValid = await this.checkPngFormat(file.url);
|
||||
if (!isValid) {
|
||||
uni.showToast({ title: '文件格式不符合要求', icon: 'none' });
|
||||
return Promise.reject('Invalid file format');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/api/v1/upload`,
|
||||
filePath: file.url,
|
||||
name: 'file',
|
||||
header: {
|
||||
'Authorization': `Bearer ${uni.getStorageSync('token')}`,
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
if (data.success) {
|
||||
resolve(data.data);
|
||||
} else {
|
||||
reject(data.message || '上传失败');
|
||||
}
|
||||
} catch (e) {
|
||||
reject('解析响应失败');
|
||||
}
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
});
|
||||
// 更新文件状态
|
||||
const index = this.fileList.findIndex(f => f.url === file.url);
|
||||
if (index !== -1) {
|
||||
this.$set(this.fileList, index, {
|
||||
...file,
|
||||
status: 'success',
|
||||
serverUrl: res
|
||||
});
|
||||
}
|
||||
this.$emit('upload-success',res)
|
||||
} catch (err) {
|
||||
console.error('上传失败:', err);
|
||||
const index = this.fileList.findIndex(f => f.url === file.url);
|
||||
if (index !== -1) {
|
||||
this.$set(this.fileList, index, {
|
||||
...file,
|
||||
status: 'failed',
|
||||
error: err.message || err
|
||||
});
|
||||
}
|
||||
uni.showToast({
|
||||
title: `上传失败: ${err.message || err}`,
|
||||
icon: 'none'
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
previewImage(index) {
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: [this.fileList[index].url]
|
||||
});
|
||||
},
|
||||
|
||||
handleDelete(index) {
|
||||
this.fileList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-area {
|
||||
margin-top: 16rpx;
|
||||
|
||||
.upload-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -5rpx;
|
||||
|
||||
.upload-item, .upload-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin: 5rpx;
|
||||
position: relative;
|
||||
background: #f8f8f8;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
image, video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0 0 0 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
border: 1rpx dashed #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 24rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
145
components/sign/pickerColor.vue
Normal file
145
components/sign/pickerColor.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<view v-show="isShow">
|
||||
<view class="shade" @tap="hide">
|
||||
<view class="pop">
|
||||
<view class="list flex_col" v-for="(item,index) in colorArr" :key="index">
|
||||
<view v-for="(v,i) in item" :key="i" :style="{'backgroundColor':v}" :data-color="v"
|
||||
:data-index="index" :data-i="i" :class="{'active':(index==pickerArr[0] && i==pickerArr[1])}"
|
||||
@tap.stop="picker"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'picker-color',
|
||||
props: {
|
||||
isShow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
colorArr: [
|
||||
['#000000', '#111111', '#222222', '#333333', '#444444', '#666666', '#999999', '#CCCCCC', '#EEEEEE',
|
||||
'#FFFFFF'
|
||||
],
|
||||
['#ff0000', '#ff0033', '#ff3399', '#ff33cc', '#cc00ff', '#9900ff', '#cc00cc', '#cc0099', '#cc3399',
|
||||
'#cc0066'
|
||||
],
|
||||
['#cc3300', '#cc6600', '#ff9933', '#ff9966', '#ff9999', '#ff99cc', '#ff99ff', '#cc66ff', '#9966ff',
|
||||
'#cc33ff'
|
||||
],
|
||||
['#663300', '#996600', '#996633', '#cc9900', '#a58800', '#cccc00', '#ffff66', '#ffff99', '#ffffcc',
|
||||
'#ffcccc'
|
||||
],
|
||||
['#336600', '#669900', '#009900', '#009933', '#00cc00', '#66ff66', '#339933', '#339966', '#009999',
|
||||
'#33cccc'
|
||||
],
|
||||
['#003366', '#336699', '#3366cc', '#0099ff', '#000099', '#0000cc', '#660066', '#993366', '#993333',
|
||||
'#800000'
|
||||
]
|
||||
],
|
||||
pickerColor: '',
|
||||
pickerArr: [-1, -1]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
picker(e) {
|
||||
let data = e.currentTarget.dataset;
|
||||
this.pickerColor = data.color;
|
||||
this.pickerArr = [data.index, data.i];
|
||||
this.$emit("callback", this.pickerColor);
|
||||
},
|
||||
hide() {
|
||||
this.$emit("callback", '');
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.pop {
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
z-index: 100;
|
||||
padding: 12upx;
|
||||
font-size: 32upx;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.flex_col {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.list {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.list>view {
|
||||
width: 60upx;
|
||||
height: 60upx;
|
||||
margin: 5upx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 2px #ccc;
|
||||
}
|
||||
|
||||
.list .active {
|
||||
box-shadow: 0 0 2px #09f;
|
||||
transform: scale(1.05, 1.05);
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 180upx;
|
||||
height: 60upx;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin: 0 40upx;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.ok {
|
||||
width: 160upx;
|
||||
height: 60upx;
|
||||
line-height: 60upx;
|
||||
text-align: center;
|
||||
background-color: #ff9933;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 3px;
|
||||
font-size: 32upx;
|
||||
}
|
||||
|
||||
.ok:active {
|
||||
background-color: rgb(255, 107, 34);
|
||||
}
|
||||
</style>
|
||||
724
components/sign/sign.vue
Normal file
724
components/sign/sign.vue
Normal file
@ -0,0 +1,724 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="wrapper">
|
||||
<!-- <view class="handRight">
|
||||
<view class="handTitle">请签名</view>
|
||||
</view> -->
|
||||
<view class="handCenter">
|
||||
<canvas class="handWriting" :disable-scroll="true" @touchstart="uploadScaleStart"
|
||||
@touchmove="uploadScaleMove" @touchend="uploadScaleEnd" canvas-id="handWriting"></canvas>
|
||||
</view>
|
||||
<view class="handBtn">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- <image @click="selectColorEvent('black','#1A1A1A')"
|
||||
:src="selectColor === 'black' ? '/static/other/color_black_selected.png' : '/static/other/color_black.png'"
|
||||
class="black-select"></image>
|
||||
<image @click="selectColorEvent('red','#ca262a')"
|
||||
:src="selectColor === 'red' ? '/static/other/color_red_selected.png' : '/static/other/color_red.png'"
|
||||
class="red-select"></image> -->
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<div class="color_pic" :style="{background:lineColor}" @click="showPickerColor=true"></div>
|
||||
<!-- #endif -->
|
||||
<button @click="clear" class="delBtn">清空</button>
|
||||
<!-- <button @click="saveCanvasAsImg" class="saveBtn">保存</button> -->
|
||||
<!-- <button @click="previewCanvasImg" class="previewBtn">预览</button> -->
|
||||
<button @click="undo" class="undoBtn">撤销</button>
|
||||
<button @click="subCanvas" class="subBtn">完成</button>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
|
||||
<pickerColor :isShow="showPickerColor" :bottom="0" @callback='getPickerColor' />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { post } from "../../utils/request";
|
||||
import pickerColor from "./pickerColor.vue"
|
||||
import {IMAGE_BASE_URL,BASE_URL} from '@/utils/config';
|
||||
import {downloadPdfFiles} from '@/utils/download.js'
|
||||
export default {
|
||||
components: {
|
||||
pickerColor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPickerColor: false,
|
||||
ctx: '',
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
selectColor: 'black',
|
||||
lineColor: '#1A1A1A',
|
||||
points: [],
|
||||
historyList: [],
|
||||
canAddHistory: true,
|
||||
getImagePath: () => {
|
||||
return new Promise((resolve) => {
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId: 'handWriting',
|
||||
fileType: 'png',
|
||||
quality: 1, //图片质量
|
||||
success: res => resolve(res.tempFilePath),
|
||||
})
|
||||
})
|
||||
},
|
||||
toDataURL: void 0,
|
||||
requestAnimationFrame: void 0,
|
||||
applyInfo:null,
|
||||
isSelfStudy:true,
|
||||
};
|
||||
},
|
||||
props: { //可用于修改的参数放在props里 也可单独放在外面做成组件调用 传值
|
||||
minSpeed: { //画笔最小速度
|
||||
type: Number,
|
||||
default: 1.5
|
||||
},
|
||||
minWidth: { //线条最小粗度
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
maxWidth: { //线条最大粗度
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
openSmooth: { //开启平滑线条(笔锋)
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxHistoryLength: { //历史最大长度
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
maxWidthDiffRate: { //最大差异率
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
bgColor: { //背景色
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
onLoad(option) {
|
||||
this.isSelfStudy=option.isSelfStudy
|
||||
// 页面A设置参数
|
||||
const app = getApp()
|
||||
if(app.globalData.applyInfo){
|
||||
this.applyInfo=app.globalData.applyInfo
|
||||
}else{
|
||||
uni.navigateTo({
|
||||
url:'/pages/meetingList/index'
|
||||
})
|
||||
}
|
||||
this.ctx = uni.createCanvasContext("handWriting");
|
||||
this.$nextTick(() => {
|
||||
uni.createSelectorQuery().select('.handCenter').boundingClientRect(rect => {
|
||||
this.canvasWidth = rect.width;
|
||||
this.canvasHeight = rect.height;
|
||||
this.drawBgColor()
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getPickerColor(color) {
|
||||
this.showPickerColor = false;
|
||||
if (color) {
|
||||
this.lineColor = color;
|
||||
}
|
||||
},
|
||||
// 笔迹开始
|
||||
uploadScaleStart(e) {
|
||||
this.canAddHistory = true
|
||||
this.ctx.setStrokeStyle(this.lineColor)
|
||||
this.ctx.setLineCap("round") //'butt'、'round'、'square'
|
||||
},
|
||||
// 笔迹移动
|
||||
uploadScaleMove(e) {
|
||||
let temX = e.changedTouches[0].x
|
||||
let temY = e.changedTouches[0].y
|
||||
this.initPoint(temX, temY)
|
||||
this.onDraw()
|
||||
},
|
||||
/**
|
||||
* 触摸结束
|
||||
*/
|
||||
uploadScaleEnd() {
|
||||
this.canAddHistory = true;
|
||||
this.points = [];
|
||||
},
|
||||
/**
|
||||
* 记录点属性
|
||||
*/
|
||||
initPoint(x, y) {
|
||||
var point = {
|
||||
x: x,
|
||||
y: y,
|
||||
t: Date.now()
|
||||
};
|
||||
var prePoint = this.points.slice(-1)[0];
|
||||
if (prePoint && (prePoint.t === point.t || prePoint.x === x && prePoint.y === y)) {
|
||||
return;
|
||||
}
|
||||
if (prePoint && this.openSmooth) {
|
||||
var prePoint2 = this.points.slice(-2, -1)[0];
|
||||
point.distance = Math.sqrt(Math.pow(point.x - prePoint.x, 2) + Math.pow(point.y - prePoint.y, 2));
|
||||
point.speed = point.distance / (point.t - prePoint.t || 0.1);
|
||||
point.lineWidth = this.getLineWidth(point.speed);
|
||||
if (prePoint2 && prePoint2.lineWidth && prePoint.lineWidth) {
|
||||
var rate = (point.lineWidth - prePoint.lineWidth) / prePoint.lineWidth;
|
||||
var maxRate = this.maxWidthDiffRate / 100;
|
||||
maxRate = maxRate > 1 ? 1 : maxRate < 0.01 ? 0.01 : maxRate;
|
||||
if (Math.abs(rate) > maxRate) {
|
||||
var per = rate > 0 ? maxRate : -maxRate;
|
||||
point.lineWidth = prePoint.lineWidth * (1 + per);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.points.push(point);
|
||||
this.points = this.points.slice(-3);
|
||||
},
|
||||
/**
|
||||
* @param {Object}
|
||||
* 线宽
|
||||
*/
|
||||
getLineWidth(speed) {
|
||||
var minSpeed = this.minSpeed > 10 ? 10 : this.minSpeed < 1 ? 1 : this.minSpeed; //1.5
|
||||
var addWidth = (this.maxWidth - this.minWidth) * speed / minSpeed;
|
||||
var lineWidth = Math.max(this.maxWidth - addWidth, this.minWidth);
|
||||
return Math.min(lineWidth, this.maxWidth);
|
||||
},
|
||||
/**
|
||||
* 绘画逻辑
|
||||
*/
|
||||
onDraw() {
|
||||
if (this.points.length < 2) return;
|
||||
this.addHistory();
|
||||
var point = this.points.slice(-1)[0];
|
||||
var prePoint = this.points.slice(-2, -1)[0];
|
||||
let that = this
|
||||
var onDraw = function onDraw() {
|
||||
if (that.openSmooth) {
|
||||
that.drawSmoothLine(prePoint, point);
|
||||
} else {
|
||||
that.drawNoSmoothLine(prePoint, point);
|
||||
}
|
||||
};
|
||||
if (typeof this.requestAnimationFrame === 'function') {
|
||||
this.requestAnimationFrame(function() {
|
||||
return onDraw();
|
||||
});
|
||||
} else {
|
||||
onDraw();
|
||||
}
|
||||
},
|
||||
//添加历史图片地址
|
||||
addHistory() {
|
||||
if (!this.maxHistoryLength || !this.canAddHistory) return;
|
||||
this.canAddHistory = false;
|
||||
if (!this.getImagePath) {
|
||||
this.historyList.length++;
|
||||
return;
|
||||
}
|
||||
//历史地址 (暂时无用)
|
||||
let that = this
|
||||
that.getImagePath().then(function(url) {
|
||||
if (url) {
|
||||
that.historyList.push(url)
|
||||
that.historyList = that.historyList.slice(-that.maxHistoryLength);
|
||||
}
|
||||
});
|
||||
},
|
||||
//画平滑线
|
||||
drawSmoothLine(prePoint, point) {
|
||||
var dis_x = point.x - prePoint.x;
|
||||
var dis_y = point.y - prePoint.y;
|
||||
|
||||
if (Math.abs(dis_x) + Math.abs(dis_y) <= 2) {
|
||||
point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5;
|
||||
point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5;
|
||||
} else {
|
||||
point.lastX1 = prePoint.x + dis_x * 0.3;
|
||||
point.lastY1 = prePoint.y + dis_y * 0.3;
|
||||
point.lastX2 = prePoint.x + dis_x * 0.7;
|
||||
point.lastY2 = prePoint.y + dis_y * 0.7;
|
||||
}
|
||||
point.perLineWidth = (prePoint.lineWidth + point.lineWidth) / 2;
|
||||
if (typeof prePoint.lastX1 === 'number') {
|
||||
this.drawCurveLine(prePoint.lastX2, prePoint.lastY2, prePoint.x, prePoint.y, point.lastX1, point
|
||||
.lastY1, point.perLineWidth);
|
||||
if (prePoint.isFirstPoint) return;
|
||||
if (prePoint.lastX1 === prePoint.lastX2 && prePoint.lastY1 === prePoint.lastY2) return;
|
||||
var data = this.getRadianData(prePoint.lastX1, prePoint.lastY1, prePoint.lastX2, prePoint.lastY2);
|
||||
var points1 = this.getRadianPoints(data, prePoint.lastX1, prePoint.lastY1, prePoint.perLineWidth / 2);
|
||||
var points2 = this.getRadianPoints(data, prePoint.lastX2, prePoint.lastY2, point.perLineWidth / 2);
|
||||
this.drawTrapezoid(points1[0], points2[0], points2[1], points1[1]);
|
||||
} else {
|
||||
point.isFirstPoint = true;
|
||||
}
|
||||
},
|
||||
//画不平滑线
|
||||
drawNoSmoothLine(prePoint, point) {
|
||||
point.lastX = prePoint.x + (point.x - prePoint.x) * 0.5;
|
||||
point.lastY = prePoint.y + (point.y - prePoint.y) * 0.5;
|
||||
if (typeof prePoint.lastX === 'number') {
|
||||
this.drawCurveLine(prePoint.lastX, prePoint.lastY, prePoint.x, prePoint.y, point.lastX, point.lastY,
|
||||
this.maxWidth);
|
||||
}
|
||||
},
|
||||
//画线
|
||||
drawCurveLine(x1, y1, x2, y2, x3, y3, lineWidth) {
|
||||
lineWidth = Number(lineWidth.toFixed(1));
|
||||
this.ctx.setLineWidth && this.ctx.setLineWidth(lineWidth);
|
||||
this.ctx.lineWidth = lineWidth;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(Number(x1.toFixed(1)), Number(y1.toFixed(1)));
|
||||
this.ctx.quadraticCurveTo(Number(x2.toFixed(1)), Number(y2.toFixed(1)), Number(x3.toFixed(1)), Number(y3
|
||||
.toFixed(1)));
|
||||
this.ctx.stroke();
|
||||
this.ctx.draw && this.ctx.draw(true);
|
||||
},
|
||||
//画梯形
|
||||
drawTrapezoid(point1, point2, point3, point4) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(Number(point1.x.toFixed(1)), Number(point1.y.toFixed(1)));
|
||||
this.ctx.lineTo(Number(point2.x.toFixed(1)), Number(point2.y.toFixed(1)));
|
||||
this.ctx.lineTo(Number(point3.x.toFixed(1)), Number(point3.y.toFixed(1)));
|
||||
this.ctx.lineTo(Number(point4.x.toFixed(1)), Number(point4.y.toFixed(1)));
|
||||
this.ctx.setFillStyle && this.ctx.setFillStyle(this.lineColor);
|
||||
this.ctx.fillStyle = this.lineColor;
|
||||
this.ctx.fill();
|
||||
this.ctx.draw && this.ctx.draw(true);
|
||||
},
|
||||
//获取弧度
|
||||
getRadianData(x1, y1, x2, y2) {
|
||||
var dis_x = x2 - x1;
|
||||
var dis_y = y2 - y1;
|
||||
if (dis_x === 0) {
|
||||
return {
|
||||
val: 0,
|
||||
pos: -1
|
||||
};
|
||||
}
|
||||
if (dis_y === 0) {
|
||||
return {
|
||||
val: 0,
|
||||
pos: 1
|
||||
};
|
||||
}
|
||||
var val = Math.abs(Math.atan(dis_y / dis_x));
|
||||
if (x2 > x1 && y2 < y1 || x2 < x1 && y2 > y1) {
|
||||
return {
|
||||
val: val,
|
||||
pos: 1
|
||||
};
|
||||
}
|
||||
return {
|
||||
val: val,
|
||||
pos: -1
|
||||
};
|
||||
},
|
||||
//获取弧度点
|
||||
getRadianPoints(radianData, x, y, halfLineWidth) {
|
||||
if (radianData.val === 0) {
|
||||
if (radianData.pos === 1) {
|
||||
return [{
|
||||
x: x,
|
||||
y: y + halfLineWidth
|
||||
}, {
|
||||
x: x,
|
||||
y: y - halfLineWidth
|
||||
}];
|
||||
}
|
||||
return [{
|
||||
y: y,
|
||||
x: x + halfLineWidth
|
||||
}, {
|
||||
y: y,
|
||||
x: x - halfLineWidth
|
||||
}];
|
||||
}
|
||||
var dis_x = Math.sin(radianData.val) * halfLineWidth;
|
||||
var dis_y = Math.cos(radianData.val) * halfLineWidth;
|
||||
if (radianData.pos === 1) {
|
||||
return [{
|
||||
x: x + dis_x,
|
||||
y: y + dis_y
|
||||
}, {
|
||||
x: x - dis_x,
|
||||
y: y - dis_y
|
||||
}];
|
||||
}
|
||||
return [{
|
||||
x: x + dis_x,
|
||||
y: y - dis_y
|
||||
}, {
|
||||
x: x - dis_x,
|
||||
y: y + dis_y
|
||||
}];
|
||||
},
|
||||
/**
|
||||
* 背景色
|
||||
*/
|
||||
drawBgColor() {
|
||||
if (!this.bgColor) return;
|
||||
this.ctx.setFillStyle && this.ctx.setFillStyle(this.bgColor);
|
||||
this.ctx.fillStyle = this.bgColor;
|
||||
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
|
||||
this.ctx.draw && this.ctx.draw(true);
|
||||
},
|
||||
//图片绘制
|
||||
drawByImage(url) {
|
||||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
|
||||
try {
|
||||
this.ctx.drawImage(url, 0, 0, this.canvasWidth, this.canvasHeight);
|
||||
this.ctx.draw && this.ctx.draw(true);
|
||||
} catch (e) {
|
||||
this.historyList.length = 0;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 清空
|
||||
*/
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
|
||||
this.ctx.draw && this.ctx.draw();
|
||||
this.drawBgColor();
|
||||
this.historyList.length = 0;
|
||||
},
|
||||
//撤消
|
||||
undo() {
|
||||
if (!this.getImagePath || !this.historyList.length) return;
|
||||
var pngURL = this.historyList.splice(-1)[0];
|
||||
this.drawByImage(pngURL);
|
||||
if (this.historyList.length === 0) {
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
//是否为空
|
||||
isEmpty() {
|
||||
return this.historyList.length === 0;
|
||||
},
|
||||
/**
|
||||
* @param {Object} str
|
||||
* @param {Object} color
|
||||
* 选择颜色
|
||||
*/
|
||||
selectColorEvent(str, color) {
|
||||
this.selectColor = str;
|
||||
this.lineColor = color;
|
||||
this.ctx.setStrokeStyle(this.lineColor)
|
||||
},
|
||||
//完成
|
||||
// 完成
|
||||
subCanvas() {
|
||||
if (this.isEmpty()) {
|
||||
uni.showToast({
|
||||
title: '没有任何绘制内容哦',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showLoading({
|
||||
title: '正在生成图片...'
|
||||
});
|
||||
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId: 'handWriting',
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
success: async (res) => {
|
||||
uni.hideLoading();
|
||||
|
||||
// 1. 保存到本地相册
|
||||
try {
|
||||
// await this.saveToAlbum(res.tempFilePath);
|
||||
|
||||
// 2. 上传到服务器
|
||||
uni.showLoading({
|
||||
title: '正在上传签名...'
|
||||
});
|
||||
|
||||
const uploadRes = await this.uploadSignature(res.tempFilePath);
|
||||
|
||||
uni.hideLoading();
|
||||
this.applyInfo.applySign=uploadRes
|
||||
const response=await post('/api/v1/app_auth/metting-room/order/register', this.applyInfo)
|
||||
console.log("===response", response)
|
||||
if (!response || !response.success) {
|
||||
throw new Error('会议室预定失败');
|
||||
}
|
||||
if(response.success==true){
|
||||
uni.navigateTo({
|
||||
url:`/pages/docList/index?files=${JSON.stringify(response.data)}&&isSelfStudy=${this.isSelfStudy}`
|
||||
})
|
||||
}
|
||||
uni.showToast({
|
||||
title: '会议室预定成功!',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// const result = await downloadPdfFiles(response);
|
||||
// console.log('下载结果:', result);
|
||||
// if(result.success==true){
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/meetingList/index`
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// 3. 可以通过回调或者事件将上传结果返回给父组件
|
||||
// this.$emit('upload-success', {
|
||||
// tempFilePath: res.tempFilePath,
|
||||
// serverPath: uploadRes // 假设服务器返回了图片地址
|
||||
// });
|
||||
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || '上传失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '生成图片失败',
|
||||
icon: 'none'
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 保存到相册的方法
|
||||
saveToAlbum(tempFilePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: tempFilePath,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '已保存到相册',
|
||||
duration: 2000
|
||||
});
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('保存到相册失败'));
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 上传签名到服务器的方法
|
||||
uploadSignature(tempFilePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 这里替换为你自己的上传接口
|
||||
const uploadUrl = IMAGE_BASE_URL+'/api/v1/upload';
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
// formData: {
|
||||
// // 可以添加其他表单数据
|
||||
// userId: '123', // 示例用户ID
|
||||
// type: 'signature'
|
||||
// },
|
||||
success: (uploadRes) => {
|
||||
console.log(uploadRes);
|
||||
console.log(JSON.parse(uploadRes.data))
|
||||
try {
|
||||
const {success,data} = JSON.parse(uploadRes.data);
|
||||
if (success) {
|
||||
resolve(data); // 假设服务器返回了图片URL
|
||||
} else {
|
||||
reject(new Error(success || '上传失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('解析服务器响应失败'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('上传请求失败'));
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
//保存到相册
|
||||
saveCanvasAsImg() {
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId: 'handWriting',
|
||||
fileType: 'png',
|
||||
quality: 1, //图片质量
|
||||
success(res) {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success(res) {
|
||||
uni.showToast({
|
||||
title: '已保存到相册',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
//预览
|
||||
previewCanvasImg() {
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId: 'handWriting',
|
||||
fileType: 'jpg',
|
||||
quality: 1, //图片质量
|
||||
success(res) {
|
||||
uni.previewImage({
|
||||
urls: [res.tempFilePath] //预览图片 数组
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
/* margin: 40rpx 0; */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
flex-direction:column;
|
||||
/* justify-content: center; */
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.handWriting {
|
||||
background: #fff;
|
||||
width: 95vw;
|
||||
height: 240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.handRight {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.handCenter {
|
||||
border: 4rpx dashed #e9e9e9;
|
||||
height: 240px;
|
||||
overflow: hidden;
|
||||
width: 95vw;
|
||||
box-sizing: border-box;
|
||||
margin: 10px auto 0px auto;
|
||||
}
|
||||
|
||||
.handTitle {
|
||||
transform: rotate(90deg);
|
||||
flex: 1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.handBtn button {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.handBtn {
|
||||
height: 10vh;
|
||||
display: flex;
|
||||
/* flex-direction: column; */
|
||||
justify-content: space-between;
|
||||
align-content:center;
|
||||
align-items: center;
|
||||
/* flex: 1; */
|
||||
}
|
||||
|
||||
.delBtn {
|
||||
/* position: absolute;
|
||||
top: 250rpx;
|
||||
left: 0rpx;
|
||||
transform: rotate(90deg); */
|
||||
height: 35px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.delBtn image {
|
||||
position: absolute;
|
||||
top: 13rpx;
|
||||
left: 25rpx;
|
||||
}
|
||||
|
||||
.subBtn {
|
||||
/* position: absolute;
|
||||
bottom: 52rpx;
|
||||
left: -3rpx;
|
||||
display: inline-flex;
|
||||
transform: rotate(90deg); */
|
||||
background: #008ef6;
|
||||
color: #fff;
|
||||
/* margin-bottom: 30rpx; */
|
||||
text-align: center;
|
||||
height: 35px;
|
||||
/* justify-content: center; */
|
||||
}
|
||||
|
||||
/*Peach - 新增 - 保存*/
|
||||
|
||||
.saveBtn {
|
||||
position: absolute;
|
||||
top: 375rpx;
|
||||
left: 0rpx;
|
||||
transform: rotate(90deg);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.previewBtn {
|
||||
position: absolute;
|
||||
top: 500rpx;
|
||||
left: 0rpx;
|
||||
transform: rotate(90deg);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.undoBtn {
|
||||
/* position: absolute;
|
||||
top: 625rpx;
|
||||
left: 0rpx;
|
||||
transform: rotate(90deg); */
|
||||
height: 35px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.black-select {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
position: absolute;
|
||||
top: 30rpx;
|
||||
left: 25rpx;
|
||||
}
|
||||
|
||||
.red-select {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
position: absolute;
|
||||
top: 140rpx;
|
||||
left: 25rpx;
|
||||
}
|
||||
|
||||
.color_pic {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 25px;
|
||||
position: absolute;
|
||||
top: 60rpx;
|
||||
left: 18rpx;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
@ -1,353 +0,0 @@
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
|
||||
@scroll="onScroll">
|
||||
<view v-for="(msg, index) in messages" :key="index"
|
||||
:class="['message', msg.sender === 'me' ? 'sent' : 'received']">
|
||||
<image v-if="msg.sender !== 'me'" :src="botAvatar" class="avatar"></image>
|
||||
|
||||
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '']">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
<view class="typing-indicator" v-if="msg.isTyping">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<text class="typing-text">对方正在输入...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image v-if="msg.sender === 'me'" :src="userAvatar" class="avatar"></image>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input">
|
||||
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage"
|
||||
confirm-type="send" />
|
||||
<button class="send-btn" :disabled="!inputMsg.trim()" @click="sendMessage">
|
||||
发送
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
BASE_URL
|
||||
} from '@/utils/config';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
inputMsg: '',
|
||||
scrollTop: 0,
|
||||
userAvatar: require('../../static/imgs/index/nav.png'),
|
||||
botAvatar: require('../../static/imgs/ai/chuandaguwen.png'),
|
||||
messages: [],
|
||||
socketTask: null, // 微信小程序的 WebSocket 任务对象
|
||||
isConnected: false,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 3000,
|
||||
userId: null,
|
||||
userName: '默认用户',
|
||||
apiKey: '',
|
||||
serviceUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.serviceUrl = options.serviceUrl || '';
|
||||
this.apiKey = options.apiKey || '';
|
||||
this.botAvatar = options.icon || this.botAvatar;
|
||||
|
||||
const userInfo = wx.getStorageSync('userInfo') || {};
|
||||
this.userId = userInfo.id || Date.now().toString();
|
||||
this.userName = userInfo.name || '默认用户';
|
||||
this.userAvatar = userInfo.avatar || this.userAvatar;
|
||||
|
||||
uni.setNavigationBarTitle({
|
||||
title: options.name || 'AI助手'
|
||||
});
|
||||
|
||||
this.initWebSocket();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.closeWebSocket();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化WebSocket连接
|
||||
initWebSocket() {
|
||||
if (this.isConnected) return;
|
||||
|
||||
// 微信小程序中使用 wx.connectSocket
|
||||
this.socketTask = wx.connectSocket({
|
||||
url: `ws://10.10.1.6:8071/api/v1/ws/ai?apiUrl=${encodeURIComponent(this.serviceUrl)}&apiToken=${this.apiKey}&userName=${this.userName}`,
|
||||
success: () => {
|
||||
console.log('WebSocket连接创建成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('WebSocket连接创建失败', err);
|
||||
this.handleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 WebSocket 事件
|
||||
this.socketTask.onOpen(() => {
|
||||
console.log('WebSocket连接已打开');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.addMessage('bot', '连接已建立,请问有什么可以帮您?');
|
||||
});
|
||||
|
||||
this.socketTask.onMessage((res) => {
|
||||
console.log('收到WebSocket消息:', res.data);
|
||||
this.processBotMessage(res.data);
|
||||
});
|
||||
|
||||
this.socketTask.onError((err) => {
|
||||
console.error('WebSocket发生错误:', err);
|
||||
this.isConnected = false;
|
||||
this.addMessage('bot', '连接出错,请稍后再试');
|
||||
this.handleReconnect();
|
||||
});
|
||||
|
||||
this.socketTask.onClose(() => {
|
||||
console.log('WebSocket连接已关闭');
|
||||
this.isConnected = false;
|
||||
this.handleReconnect();
|
||||
});
|
||||
},
|
||||
|
||||
// 处理机器人消息
|
||||
processBotMessage(data) {
|
||||
// 移除思考状态的消息
|
||||
this.messages = this.messages.filter(msg => !msg.isTyping);
|
||||
|
||||
// 处理不同类型的消息
|
||||
if (data.startsWith('<think>')) {
|
||||
// 机器人正在思考
|
||||
this.addMessage('bot', '', true);
|
||||
} else if (data.startsWith('<answer>')) {
|
||||
// 正式回答
|
||||
const content = data.replace(/<answer>/g, '').replace(/<\/answer>/g, '');
|
||||
this.addMessage('bot', content);
|
||||
} else if (data.includes('</think>')) {
|
||||
// 思考结束,不做特殊处理
|
||||
} else {
|
||||
// 普通消息
|
||||
this.addMessage('bot', data);
|
||||
}
|
||||
},
|
||||
|
||||
// 添加消息到聊天列表
|
||||
addMessage(sender, content, isTyping = false) {
|
||||
const message = {
|
||||
sender: sender === 'bot' ? 'other' : 'me',
|
||||
content,
|
||||
isTyping,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.messages.push(message);
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage() {
|
||||
const content = this.inputMsg.trim();
|
||||
if (!content || !this.isConnected) return;
|
||||
|
||||
this.addMessage('me', content);
|
||||
|
||||
// 微信小程序中使用 socketTask.send
|
||||
this.socketTask.send({
|
||||
data: content,
|
||||
success: () => {
|
||||
console.log('消息发送成功');
|
||||
// 添加机器人正在输入的提示
|
||||
this.addMessage('bot', '', true);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('消息发送失败', err);
|
||||
this.addMessage('bot', '消息发送失败,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
this.inputMsg = '';
|
||||
},
|
||||
|
||||
// 处理重连逻辑
|
||||
handleReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log('已达到最大重连次数');
|
||||
this.addMessage('bot', '连接已断开,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
console.log(`尝试重新连接,第${this.reconnectAttempts}次`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.initWebSocket();
|
||||
}, this.reconnectDelay);
|
||||
},
|
||||
|
||||
// 关闭WebSocket连接
|
||||
closeWebSocket() {
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close({
|
||||
success: () => {
|
||||
console.log('WebSocket已主动关闭');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('关闭WebSocket失败', err);
|
||||
}
|
||||
});
|
||||
this.socketTask = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
},
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = 99999; // 足够大的值确保滚动到底部
|
||||
});
|
||||
},
|
||||
|
||||
onScroll(e) {
|
||||
// 可以在这里实现加载历史消息
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
background-color: #e5ddd5;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sent {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.received {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 70%;
|
||||
padding: 10px 15px;
|
||||
border-radius: 18px;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.me {
|
||||
background-color: #dcf8c6;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
background-color: #f0f0f0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0 20px;
|
||||
border-radius: 20px;
|
||||
background-color: #07C160;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 对方正在输入提示 */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #7f8c8d;
|
||||
margin: 0 3px;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.typing-text {
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -3,15 +3,72 @@
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
|
||||
@scroll="onScroll">
|
||||
<view class="" style="padding: 10px 0;">
|
||||
|
||||
<view v-for="(msg, index) in messages" :key="index"
|
||||
:class="['message', msg.sender === 'me' ? 'sent' : 'received']">
|
||||
<image v-if="msg.sender !== 'me'" :src="botAvatar" class="avatar"></image>
|
||||
|
||||
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '', msg.type]">
|
||||
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '']">
|
||||
<!-- <text class="message-type-tag" v-if="msg.type !== 'normal'">
|
||||
{{ msg.type === 'thinking' ? '思考中' : msg.type === 'answer' ? '回答' : '' }}
|
||||
</text> -->
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
<view class="message-text" style="color: #999999;">{{ msg.thinkContent }}</view>
|
||||
<view class="message-text" v-if="msg.answerType===2">{{ msg.showAnswer}}</view>
|
||||
<view class="message-text" v-if="msg.answerType===1">
|
||||
<view>
|
||||
【穿搭建议】:
|
||||
<view>
|
||||
🧥上身:
|
||||
<view>
|
||||
{{`${msg.showAnswer.outfit.top.color+msg.showAnswer.outfit.top.style}👔,${msg.showAnswer.outfit.top.detail_description},${msg.showAnswer.outfit.top.reason}`}}
|
||||
</view>
|
||||
</view>
|
||||
<view style="margin-top: 10px;">
|
||||
🩳下身:
|
||||
<view>
|
||||
{{`${msg.showAnswer.outfit.bottom.color+msg.showAnswer.outfit.bottom.style},${msg.showAnswer.outfit.bottom.detail_description},${msg.showAnswer.outfit.bottom.reason}😎`}}
|
||||
</view>
|
||||
</view>
|
||||
<view style="margin-top: 10px;">
|
||||
🥾鞋子:
|
||||
<view>
|
||||
{{`${msg.showAnswer.outfit.shoes.color+msg.showAnswer.outfit.shoes.style},${msg.showAnswer.outfit.shoes.detail_description},${msg.showAnswer.outfit.shoes.reason}🏃🏻`}}
|
||||
</view>
|
||||
</view>
|
||||
<view style="margin-top: 10px;">👒配饰:
|
||||
<view v-for="(item,index) of msg.showAnswer.outfit.accessories">
|
||||
<view>
|
||||
{{`${index+1}.`+item.color+item.type+','+item.detail_description+','+item.reason}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="margin-top: 10px;">
|
||||
💅化妆建议:
|
||||
<view>
|
||||
<view class="">
|
||||
{{msg.showAnswer.makeup_suggestion.style+','+msg.showAnswer.makeup_suggestion.description}}
|
||||
</view>
|
||||
<view style="margin-top: 10px;">
|
||||
🧩产品推荐:
|
||||
<view v-for="(item,index) of msg.showAnswer.makeup_suggestion.products">
|
||||
<view class="">
|
||||
{{`${index+1}.`+item.name+','+item.detail_description+','+item.reason}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="margin-top: 10px;">
|
||||
📝风格推荐:
|
||||
<view class="">{{msg.showAnswer.styling_tips.color_coordination}}</view>
|
||||
<view class="">{{msg.showAnswer.styling_tips.occasion_adaptation}}</view>
|
||||
<view v-for="(item,index) of msg.showAnswer.styling_tips.detail_enhancements">
|
||||
<view class="">{{`${index+1}.${item}`}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="typing-indicator" v-if="msg.isTyping">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
@ -22,22 +79,29 @@
|
||||
|
||||
<image v-if="msg.sender === 'me'" :src="userAvatar" class="avatar"></image>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input">
|
||||
<view class="" style="flex: 1;">
|
||||
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage"
|
||||
confirm-type="send" />
|
||||
<button class="send-btn" :disabled="!inputMsg.trim()" @click="sendMessage">
|
||||
</view>
|
||||
<view class="" style="width: 100px;">
|
||||
<u-button type="primary" :disabled="!inputMsg.trim()||isAnswering" @click="sendMessage" shape="circle">
|
||||
发送
|
||||
</button>
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
BASE_URL
|
||||
WS_BASE_URL
|
||||
} from '@/utils/config';
|
||||
|
||||
export default {
|
||||
@ -56,7 +120,10 @@
|
||||
userId: null,
|
||||
userName: '默认用户',
|
||||
apiKey: '',
|
||||
serviceUrl: ''
|
||||
serviceUrl: '',
|
||||
currentAnswer: '',
|
||||
isAnswering: false,
|
||||
isHandleClose:false
|
||||
}
|
||||
},
|
||||
|
||||
@ -88,7 +155,7 @@
|
||||
|
||||
// 微信小程序中使用 wx.connectSocket
|
||||
this.socketTask = wx.connectSocket({
|
||||
url: `ws://10.10.1.6:8071/api/v1/ws/ai?apiUrl=${encodeURIComponent(this.serviceUrl)}&apiToken=${this.apiKey}&userName=${this.userName}`,
|
||||
url: `${WS_BASE_URL}/api/v1/ws/ai?apiUrl=${encodeURIComponent(this.serviceUrl)}&apiToken=${this.apiKey}&userName=${this.userName}`,
|
||||
success: () => {
|
||||
console.log('WebSocket连接创建成功');
|
||||
},
|
||||
@ -103,7 +170,16 @@
|
||||
console.log('WebSocket连接已打开');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.addMessage('bot', '连接已建立,请问有什么可以帮您?');
|
||||
const params = {
|
||||
sender: 'bot',
|
||||
thinkContent: '',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: false,
|
||||
showAnswer: '已连接,请输入您的问题',
|
||||
answerType: 2
|
||||
}
|
||||
this.addMessage(params);
|
||||
});
|
||||
|
||||
this.socketTask.onMessage((res) => {
|
||||
@ -114,14 +190,25 @@
|
||||
this.socketTask.onError((err) => {
|
||||
console.error('WebSocket发生错误:', err);
|
||||
this.isConnected = false;
|
||||
this.addMessage('bot', '连接出错,请稍后再试');
|
||||
const params = {
|
||||
sender: 'bot',
|
||||
thinkContent: '',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: false,
|
||||
showAnswer: '连接出错,请稍后再试',
|
||||
answerType: 2
|
||||
}
|
||||
this.addMessage(params);
|
||||
this.handleReconnect();
|
||||
});
|
||||
|
||||
this.socketTask.onClose(() => {
|
||||
console.log('WebSocket连接已关闭');
|
||||
this.isConnected = false;
|
||||
if(!this.isHandleClose){
|
||||
this.handleReconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -134,12 +221,22 @@
|
||||
// 处理分块文本
|
||||
this.handleTextChunk(messageData);
|
||||
} else if (messageData.event === 'workflow_finished') {
|
||||
this.currentAnswer = ''
|
||||
// 处理完整节点完成
|
||||
this.handleNodeFinished(messageData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理机器人消息出错:', error);
|
||||
this.addMessage('bot', '处理消息时出错: ' + error.message, 'error');
|
||||
const params = {
|
||||
sender: 'bot',
|
||||
thinkContent: '',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: false,
|
||||
showAnswer: '处理消息时出错: ' + error.message,
|
||||
answerType: 2
|
||||
}
|
||||
this.addMessage(params);
|
||||
}
|
||||
},
|
||||
|
||||
@ -147,16 +244,25 @@
|
||||
handleTextChunk(chunkData) {
|
||||
const text = chunkData.data.text;
|
||||
if (!text || text.trim() === '') return;
|
||||
let isThinking = true
|
||||
if (text.includes('<think>')) {
|
||||
this.messages[this.messages.length - 1].type = 'thinking'
|
||||
}
|
||||
if (text.includes('</think>')) {
|
||||
this.messages[this.messages.length - 1].type = 'answer'
|
||||
}
|
||||
|
||||
// 判断是否是思考内容
|
||||
const isThinking = text.includes('<think>') ||(this.messages[this.messages.length - 1]?.type === 'thinking');
|
||||
if (isThinking) {
|
||||
// const isThinking = text.includes('<think>') ||(this.messages[this.messages.length - 1]?.type === 'thinking');
|
||||
console.log(text, this.messages[this.messages.length - 1].type);
|
||||
if (this.messages[this.messages.length - 1].type == 'thinking') {
|
||||
this.handleThinkingContent(text);
|
||||
} else {
|
||||
}
|
||||
if (this.messages[this.messages.length - 1].type == 'answer') {
|
||||
this.handleAnswerContent(text);
|
||||
}
|
||||
|
||||
this.scrollToBottom();
|
||||
|
||||
},
|
||||
|
||||
// 处理思考内容
|
||||
@ -166,39 +272,46 @@
|
||||
if (!thinkContent) return;
|
||||
|
||||
// 查找最后一条思考消息
|
||||
const lastThinkMsgIndex = this.messages.findLastIndex(
|
||||
msg => msg.type === 'thinking'
|
||||
);
|
||||
// const lastThinkMsgIndex = this.messages.findLastIndex(
|
||||
// msg => msg.type === 'thinking'
|
||||
// );
|
||||
|
||||
if (lastThinkMsgIndex >= 0) {
|
||||
// 追加到现有思考消息
|
||||
this.messages[lastThinkMsgIndex].content += thinkContent;
|
||||
// if (lastThinkMsgIndex >= 0) {
|
||||
// // 追加到现有思考消息
|
||||
// this.messages[lastThinkMsgIndex].thinkContent += thinkContent;
|
||||
// this.$forceUpdate();
|
||||
// } else {
|
||||
// // 创建新的思考消息
|
||||
// this.addMessage('bot', thinkContent, 'thinking');
|
||||
// }
|
||||
this.messages[this.messages.length - 1].thinkContent += thinkContent
|
||||
this.$forceUpdate();
|
||||
} else {
|
||||
// 创建新的思考消息
|
||||
this.addMessage('bot', thinkContent, 'thinking');
|
||||
}
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
// 处理回答内容
|
||||
handleAnswerContent(text) {
|
||||
// 提取回答内容,去除标签
|
||||
const answerContent = text.replace(/<\/?answer>/g, '').trim();
|
||||
const answerContent = text.replace(/<\/?think>/g, '');
|
||||
if (!answerContent) return;
|
||||
|
||||
// 查找最后一条回答消息
|
||||
const lastAnswerMsgIndex = this.messages.findLastIndex(
|
||||
msg => msg.type === 'answer' || msg.type === 'normal'
|
||||
);
|
||||
// const lastAnswerMsgIndex = this.messages.findLastIndex(
|
||||
// msg => msg.type === 'answer' || msg.type === 'normal'
|
||||
// );
|
||||
|
||||
if (lastAnswerMsgIndex >= 0 && this.messages[lastAnswerMsgIndex].type === 'answer') {
|
||||
// 追加到现有回答消息
|
||||
this.messages[lastAnswerMsgIndex].content += answerContent;
|
||||
this.$forceUpdate();
|
||||
} else {
|
||||
// 创建新的回答消息
|
||||
this.addMessage('bot', answerContent, 'answer');
|
||||
}
|
||||
// if (lastAnswerMsgIndex >= 0 && this.messages[lastAnswerMsgIndex].type === 'answer') {
|
||||
// // 追加到现有回答消息
|
||||
// this.messages[lastAnswerMsgIndex].answerContent += answerContent;
|
||||
// this.$forceUpdate();
|
||||
|
||||
// } else {
|
||||
// // 创建新的回答消息
|
||||
// this.addMessage('bot', answerContent, 'answer');
|
||||
// }
|
||||
this.messages[this.messages.length - 1].answerContent += answerContent
|
||||
// this.$forceUpdate();
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
// 处理节点完成
|
||||
@ -207,45 +320,67 @@
|
||||
if (this.messages.length > 0) {
|
||||
const lastMsg = this.messages[this.messages.length - 1];
|
||||
lastMsg.isComplete = true;
|
||||
lastMsg.isTyping=false
|
||||
lastMsg.isTyping = false
|
||||
// 如果最后一条是思考消息,我们不需要额外处理
|
||||
if (lastMsg.type === 'thinking') {
|
||||
return;
|
||||
}
|
||||
if (lastMsg.answerContent.includes('outfit')) {
|
||||
const ls = lastMsg.answerContent.replace(/\n/g, '')
|
||||
try {
|
||||
const obj = JSON.parse(lastMsg.answerContent)
|
||||
lastMsg.answerContent = obj
|
||||
} catch (error) {
|
||||
lastMsg.answerContent = '哎呀,AI开小差了,请重新提问'
|
||||
}
|
||||
|
||||
lastMsg.answerType = 1 //1代表返回了正式的穿搭建议,2代表非正式的问答
|
||||
}
|
||||
lastMsg.showAnswer = lastMsg.answerContent
|
||||
this.isAnswering = false
|
||||
// console.log(JSON.parse(lastMsg.answerContent));
|
||||
}
|
||||
|
||||
// 只有当没有找到回答内容时才添加完整输出
|
||||
if (nodeData.data.outputs?.text) {
|
||||
const fullText = nodeData.data.outputs.text;
|
||||
const answerContent = fullText.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
// if (nodeData.data.outputs?.text) {
|
||||
// const fullText = nodeData.data.outputs.text;
|
||||
// const answerContent = fullText.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
|
||||
// 检查是否已经有相同内容的消息
|
||||
const hasSameContent = this.messages.some(
|
||||
msg => msg.content.includes(answerContent)
|
||||
);
|
||||
// // 检查是否已经有相同内容的消息
|
||||
// const hasSameContent = this.messages.some(
|
||||
// msg => msg.content.includes(answerContent)
|
||||
// );
|
||||
|
||||
if (answerContent && !hasSameContent) {
|
||||
this.addMessage('bot', answerContent, 'answer');
|
||||
}
|
||||
}
|
||||
// if (answerContent && !hasSameContent) {
|
||||
// this.addMessage('bot', answerContent, 'answer');
|
||||
// }
|
||||
// }
|
||||
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
|
||||
// 修改后的 addMessage 方法
|
||||
addMessage(sender, content, type = 'normal', isTyping = false) {
|
||||
addMessage(params) {
|
||||
// const message = {
|
||||
// sender: sender === 'bot' ? 'other' : 'me',
|
||||
// thinkContent,
|
||||
// answerContent,
|
||||
// showAnswer,
|
||||
// type, // 'thinking', 'answer', 或 'normal'
|
||||
// isTyping,
|
||||
// isComplete: true, // 默认完整,流式消息会设为false
|
||||
// timestamp: Date.now(),
|
||||
// answerType,
|
||||
// };
|
||||
const message = {
|
||||
sender: sender === 'bot' ? 'other' : 'me',
|
||||
content,
|
||||
type, // 'thinking', 'answer', 或 'normal'
|
||||
isTyping,
|
||||
...params,
|
||||
sender: params.sender === 'bot' ? 'other' : 'me',
|
||||
isComplete: true, // 默认完整,流式消息会设为false
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
// 如果是流式消息,标记为未完成
|
||||
if (type === 'thinking' || type === 'answer') {
|
||||
if (message.type === 'thinking' || message.type === 'answer') {
|
||||
message.isComplete = false;
|
||||
}
|
||||
|
||||
@ -256,20 +391,46 @@
|
||||
sendMessage() {
|
||||
const content = this.inputMsg.trim();
|
||||
if (!content || !this.isConnected) return;
|
||||
|
||||
this.addMessage('me', content);
|
||||
|
||||
const params = {
|
||||
sender: 'me',
|
||||
thinkContent: '',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: false,
|
||||
showAnswer: content,
|
||||
answerType: 2
|
||||
}
|
||||
this.addMessage(params);
|
||||
this.isAnswering = true
|
||||
// 微信小程序中使用 socketTask.send
|
||||
this.socketTask.send({
|
||||
data: content,
|
||||
success: () => {
|
||||
console.log('消息发送成功');
|
||||
const params = {
|
||||
sender: 'bot',
|
||||
thinkContent: '思考中...',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: true,
|
||||
showAnswer: '',
|
||||
answerType: 2
|
||||
}
|
||||
// 添加机器人正在输入的提示
|
||||
this.addMessage('bot', '思考中...','thinking',true);
|
||||
this.addMessage(params);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('消息发送失败', err);
|
||||
this.addMessage('bot', '消息发送失败,请重试');
|
||||
const params = {
|
||||
sender: 'bot',
|
||||
thinkContent: '',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: true,
|
||||
showAnswer: '消息发送失败,请重试',
|
||||
answerType: 2
|
||||
}
|
||||
this.addMessage(params);
|
||||
}
|
||||
});
|
||||
|
||||
@ -280,7 +441,16 @@
|
||||
handleReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log('已达到最大重连次数');
|
||||
this.addMessage('bot', '连接已断开,请刷新页面重试');
|
||||
const params = {
|
||||
sender: 'bot',
|
||||
thinkContent: '',
|
||||
answerContent: '',
|
||||
type: 'normal',
|
||||
isTyping: false,
|
||||
showAnswer: '连接已断开,请刷新页面重试',
|
||||
answerType: 2
|
||||
}
|
||||
this.addMessage(params);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -295,15 +465,13 @@
|
||||
// 关闭WebSocket连接
|
||||
closeWebSocket() {
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close({
|
||||
success: () => {
|
||||
console.log('WebSocket已主动关闭');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('关闭WebSocket失败', err);
|
||||
}
|
||||
});
|
||||
// 移除所有回调引用
|
||||
this.socketTask.onClose = null;
|
||||
this.socketTask.onError = null;
|
||||
this.socketTask.onMessage = null;
|
||||
this.socketTask.close();
|
||||
this.socketTask = null;
|
||||
this.isHandleClose=true
|
||||
}
|
||||
this.isConnected = false;
|
||||
},
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
<image src="/static/imgs/word.png" alt="" srcset="" class="doc-img"/>
|
||||
<view class="doc-name">{{item.name}}</view>
|
||||
</view>
|
||||
<view style="margin-top: 20px;">
|
||||
<u-button shape="circle" type="primary" @click="reback()" >返回</u-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@ -41,6 +44,11 @@
|
||||
// url: `/pages/meetingList/index`
|
||||
// });
|
||||
}
|
||||
},
|
||||
reback(){
|
||||
uni.redirectTo({
|
||||
url: `/pages/meetingList/index`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
<u-swiper class="room-swiper" :list="list1" height="420" indicator indicatorMode="dot"
|
||||
indicatorActiveColor="#2979ff" bgColor="#f8faff">
|
||||
</u-swiper>
|
||||
|
||||
<!-- 会议室基本信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="room-info">
|
||||
@ -146,11 +145,35 @@
|
||||
<u--input placeholder="请输入申请理由" border="null" v-model="reason"></u--input>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <view class="date-section" v-show="applyName=='个人申请'">
|
||||
<view class="section-header">
|
||||
<u-icon name="plus-circle" size="38" color="#2979ff"></u-icon>
|
||||
<text class="section-title">签字</text>
|
||||
</view>
|
||||
<view class="upload-item" v-if="signUrl">
|
||||
<image :src="signUrl" mode="aspectFill" @click="previewSign()"></image>
|
||||
<view class="delete-btn" @click="handleDeleteSign()">
|
||||
<u-icon name="close" color="#fff" size="24"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="upload-btn" @click="signShow=true" v-if="!signUrl">
|
||||
<u-icon name="plus" size="40" color="#c0c4cc"></u-icon>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="date-section" v-show="applyName=='单位申请'">
|
||||
<view class="section-header">
|
||||
<u-icon name="plus-circle" size="38" color="#2979ff"></u-icon>
|
||||
<text class="section-title">上传印章</text>
|
||||
</view>
|
||||
<!-- 上传照片/视频 - 完全重写的上传组件 -->
|
||||
<gx-upload @upload-success="handleUploadSuccess" />
|
||||
</view>
|
||||
<view >
|
||||
<u-button type="primary" shape="circle" @click="handleBook()">立即预订</u-button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 底部固定操作栏 -->
|
||||
<view class="action-bar">
|
||||
<u-button type="primary" shape="circle" @click="handleBook">立即预订</u-button>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<u-calendar :key="calendarKey" :show="showCalendar" mode="single" :min-date="minDate" :max-date="maxDate"
|
||||
@ -165,7 +188,13 @@
|
||||
@confirm="handleEndTimeConfirm" @cancel="showEndTimePicker = false" ref="endTimePicker"></u-datetime-picker>
|
||||
<u-picker :show="showApplyType" :columns="applyTypeColumns" @confirm="applyConfirm"
|
||||
@cancel="showApplyType=false"></u-picker>
|
||||
|
||||
<u-popup :show="readShow" mode="center" :round="10">
|
||||
<instructionVue @change='readChange' />
|
||||
<u-button type="primary" text="我已知晓" :disabled="hasReaded" @click="postApply"></u-button>
|
||||
</u-popup>
|
||||
<!-- <u-popup :show="signShow" mode="center" :round="10">
|
||||
<gxsign />
|
||||
</u-popup> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -187,9 +216,15 @@
|
||||
usages,
|
||||
thingThemes
|
||||
} from '@/utils/dict.js'
|
||||
// import signature from '../sign/sign.vue';
|
||||
import gxsign from '../../components/sign/sign.vue';
|
||||
import gxUpload from '../../components/gx-upload.vue';
|
||||
import instructionVue from '../../components/instruction.vue';
|
||||
export default {
|
||||
|
||||
components: {
|
||||
instructionVue,
|
||||
gxUpload,
|
||||
gxsign
|
||||
},
|
||||
data() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
@ -208,6 +243,10 @@
|
||||
|
||||
return {
|
||||
companyName: '',
|
||||
readShow:false,
|
||||
hasReaded:true,
|
||||
signUrl:'',
|
||||
signShow:false,
|
||||
applyRules: {
|
||||
userPhone: '申请人手机号',
|
||||
userName: '申请人姓名',
|
||||
@ -227,7 +266,7 @@
|
||||
num: '',
|
||||
// 会议室图片数组
|
||||
applyTheme: 1,
|
||||
applyArea:1,
|
||||
applyArea: 1,
|
||||
thingThemes: thingThemes.getAll(),
|
||||
usage: [],
|
||||
usages: usages.getAll(),
|
||||
@ -254,11 +293,11 @@
|
||||
// 时间选择器控制
|
||||
showStartTimePicker: false,
|
||||
showEndTimePicker: false,
|
||||
|
||||
stampUrl: '',
|
||||
// 使用字符串格式的时间
|
||||
startTimePickerValue: `${year}-${month}-${day}`,
|
||||
endTimePickerValue: ``,
|
||||
|
||||
applyInfo:{},
|
||||
// 日期相关
|
||||
selectedDate: `${year}-${month}-${day}`,
|
||||
minDate: `${year}-${month}-${day}`,
|
||||
@ -286,7 +325,7 @@
|
||||
onLoad(options) {
|
||||
if (options && options.Id) {
|
||||
this.Id = options.Id;
|
||||
this.isSelfStudy=JSON.parse(options.isSelfStudy)
|
||||
this.isSelfStudy = JSON.parse(options.isSelfStudy)
|
||||
this.mode = options.isSelfStudy === 'true' ? 'year-month' : 'datetime'
|
||||
}
|
||||
},
|
||||
@ -308,9 +347,16 @@
|
||||
this.getDetail();
|
||||
},
|
||||
methods: {
|
||||
showApplyTypeClick(){
|
||||
if(this.isSelfStudy) return
|
||||
this.showApplyType=true
|
||||
readChange(flag){
|
||||
this.hasReaded=flag
|
||||
|
||||
},
|
||||
showApplyTypeClick() {
|
||||
if (this.isSelfStudy) return
|
||||
this.showApplyType = true
|
||||
},
|
||||
handleUploadSuccess(e) {
|
||||
this.stampUrl = e
|
||||
},
|
||||
// 获取详情页面
|
||||
async getDetail() {
|
||||
@ -344,14 +390,14 @@
|
||||
},
|
||||
applyConfirm(e) {
|
||||
console.log(e.value[0])
|
||||
const list=['个人申请','单位申请']
|
||||
const list = ['个人申请', '单位申请']
|
||||
this.applyName = e.value[0]
|
||||
const index = list.findIndex((item) => item === e.value[0])
|
||||
console.log(index)
|
||||
if(index!=-1){
|
||||
if (index != -1) {
|
||||
this.applyType = parseInt(index + 1)
|
||||
this.showApplyType = false
|
||||
}else{
|
||||
} else {
|
||||
console.log('未找到');
|
||||
}
|
||||
|
||||
@ -494,6 +540,7 @@
|
||||
},
|
||||
async handleBook() {
|
||||
try {
|
||||
this.readShow=false
|
||||
console.log(this.applyType);
|
||||
let bookingInfo = {
|
||||
roomId: this.Id,
|
||||
@ -522,6 +569,9 @@
|
||||
})
|
||||
}
|
||||
if (this.applyType === 2) {
|
||||
if (!this.stampUrl) return uni.showToast({
|
||||
title: '请上传印章'
|
||||
})
|
||||
const userApplyInfo = {
|
||||
concatName: this.concatName,
|
||||
concatPhone: this.concatPhone,
|
||||
@ -529,24 +579,12 @@
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime,
|
||||
counter: this.counter,
|
||||
num: this.num
|
||||
num: this.num,
|
||||
stampUrl: this.stampUrl
|
||||
}
|
||||
await this.validateObject(userApplyInfo, this.applyRules)
|
||||
const applyInfo = Object.assign(bookingInfo, userApplyInfo)
|
||||
// 这里可以添加实际的API调用
|
||||
const res = await post('/api/v1/app_auth/metting-room/order/register', applyInfo)
|
||||
if (!res || !res.success) {
|
||||
throw new Error('会议室预定失败');
|
||||
}
|
||||
uni.showToast({
|
||||
title: '会议室预定成功!',
|
||||
icon: 'success'
|
||||
});
|
||||
if(res.success==true){
|
||||
uni.navigateTo({
|
||||
url:`/pages/docList/index?files=${JSON.stringify(res.data)}&&isSelfStudy=${this.isSelfStudy}`
|
||||
})
|
||||
}
|
||||
this.applyInfo = Object.assign(bookingInfo, userApplyInfo)
|
||||
this.readShow=true
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
@ -556,6 +594,23 @@
|
||||
}
|
||||
|
||||
},
|
||||
async postApply(){
|
||||
this.readShow=false
|
||||
// 这里可以添加实际的API调用
|
||||
const res = await post('/api/v1/app_auth/metting-room/order/register', this.applyInfo)
|
||||
if (!res || !res.success) {
|
||||
throw new Error('会议室预定失败');
|
||||
}
|
||||
uni.showToast({
|
||||
title: '会议室预定成功!',
|
||||
icon: 'success'
|
||||
});
|
||||
if (res.success == true) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/docList/index?files=${JSON.stringify(res.data)}&&isSelfStudy=${this.isSelfStudy}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@ -564,7 +619,7 @@
|
||||
<style lang="scss" scoped>
|
||||
.meeting-room-detail {
|
||||
padding: 20rpx;
|
||||
padding-bottom: 140rpx;
|
||||
// padding-bottom: 140rpx;
|
||||
/* 增大底部空间 */
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
@ -572,7 +627,7 @@
|
||||
/* 轮播图优化 */
|
||||
.room-swiper {
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
// height: 420rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
@ -719,7 +774,40 @@
|
||||
|
||||
}
|
||||
|
||||
.upload-item, .upload-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin: 5rpx;
|
||||
position: relative;
|
||||
background: #f8f8f8;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
image, video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0 0 0 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
border: 1rpx dashed #c0c4cc;
|
||||
}
|
||||
|
||||
/* 新增备注区域 */
|
||||
.remark-section {
|
||||
@ -763,10 +851,10 @@
|
||||
|
||||
/* 底部操作栏 */
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
// position: fixed;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
<noData />
|
||||
</view>
|
||||
<view class="order-item" v-for="(item, index) in orderList" :key="index">
|
||||
<view class="order-header" @click="goDetail(item.roomInfo || item)">
|
||||
<view class="order-header">
|
||||
<view class="order-name">{{ item.roomInfo ? item.roomInfo.title : '会议室' }}</view>
|
||||
<view class="order-status" :class="item.status == 0 ? 'available':''">
|
||||
{{ getStatusText(item.status) }}
|
||||
@ -47,19 +47,27 @@
|
||||
<view class="order-details">
|
||||
<view class="detail-item">
|
||||
<u-icon name="account" size="36" color="#666" class="detail-icon"></u-icon>
|
||||
<text>联系人: {{ item.concatName }} ({{ item.concatPhone }})</text>
|
||||
<text v-if="item.companyName==''">联系人: {{ item.userName }} ({{ item.userPhone }})</text>
|
||||
<text v-else>负责人: {{ item.concatName }} ({{ item.concatPhone }})</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-item">
|
||||
<u-icon name="calendar" size="36" color="#666" class="detail-icon"></u-icon>
|
||||
<text>预约日期: {{ formatDate(item.startAt) }}</text>
|
||||
<text>预约日期: {{ formatDate(item.createdAt) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-item">
|
||||
<u-icon name="clock" size="30" color="#666" class="detail-icon"></u-icon>
|
||||
<text>时间段: {{ formatTimeRange(item.startAt, item.endAt) }}</text>
|
||||
<text>时间段: {{`${item.startTime}~${item.endTime}`}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<u-icon name="tags" size="30" color="#666" class="detail-icon"></u-icon>
|
||||
<text>事由主题: {{formatName(item.applyTheme)}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<u-icon name="pushpin" size="30" color="#666" class="detail-icon"></u-icon>
|
||||
<text>申请场次及人数: {{`${item.counter}/${item.num}`}}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-item" v-if="item.remark">
|
||||
<u-icon name="edit-pen" size="36" color="#666" class="detail-icon"></u-icon>
|
||||
<text>备注: {{ item.remark }}</text>
|
||||
@ -68,8 +76,8 @@
|
||||
</view>
|
||||
|
||||
<view class="order-actions">
|
||||
<u-button type="primary" plain @click="goDetail(item.roomInfo || item)">查看详情</u-button>
|
||||
<u-button type="error" plain v-if="item.status === 0" @click="cancelOrder(item.id)">
|
||||
<!-- <u-button type="primary" plain @click="goDetail(item.roomInfo || item)">查看详情</u-button> -->
|
||||
<u-button type="error" plain @click="cancelOrder(item.id)">
|
||||
取消预约
|
||||
</u-button>
|
||||
</view>
|
||||
@ -83,10 +91,10 @@
|
||||
<u-loading-icon mode="circle" size="36"></u-loading-icon>
|
||||
<text class="loading-text">加载会议室信息中...</text>
|
||||
</view>
|
||||
<u-popup :show="show" mode="center" :round="10">
|
||||
<!-- <u-popup :show="show" mode="center" :round="10">
|
||||
<instructionVue @change='readChange'/>
|
||||
<u-button type="primary" text="我已知晓" :disabled="hasReaded" @click="show=false"></u-button>
|
||||
</u-popup>
|
||||
</u-popup> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -105,6 +113,11 @@
|
||||
} from '@/utils/timeFormat';
|
||||
import instructionVue from '../../components/instruction.vue';
|
||||
import noData from '../../components/noData.vue'
|
||||
import {
|
||||
applyType,
|
||||
usages,
|
||||
thingThemes
|
||||
} from '@/utils/dict.js'
|
||||
export default {
|
||||
components: {
|
||||
instructionVue,
|
||||
@ -112,7 +125,7 @@
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasReaded:true,
|
||||
hasReaded: true,
|
||||
checkboxValue1: [],
|
||||
// 基本案列数据
|
||||
checkboxList1: [{
|
||||
@ -168,8 +181,8 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readChange(flag){
|
||||
this.hasReaded=flag
|
||||
formatName(value){
|
||||
return thingThemes.getName(value)
|
||||
},
|
||||
handleTabChange(index) {
|
||||
this.currentTab = index.index;
|
||||
@ -196,11 +209,10 @@
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
0: '待确认',
|
||||
1: '已确认',
|
||||
2: '已取消',
|
||||
3: '已拒绝',
|
||||
4: '已完成'
|
||||
1: '待提交',
|
||||
2: '待审核',
|
||||
3: '审核通过',
|
||||
99: '审核不通过'
|
||||
};
|
||||
return statusMap[status] || '未知状态';
|
||||
},
|
||||
@ -254,7 +266,7 @@
|
||||
|
||||
// 安全获取ID(兼容各种可能的字段名)
|
||||
const id = item.id;
|
||||
const isSelfStudy=item.title==='自习室'?true:false
|
||||
const isSelfStudy = item.title === '自习室' ? true : false
|
||||
if (!id) {
|
||||
uni.showToast({
|
||||
title: '会议室信息异常',
|
||||
|
||||
@ -24,13 +24,17 @@
|
||||
<!-- <button @click="saveCanvasAsImg" class="saveBtn">保存</button> -->
|
||||
<!-- <button @click="previewCanvasImg" class="previewBtn">预览</button> -->
|
||||
<button @click="undo" class="undoBtn">撤销</button>
|
||||
<button @click="subCanvas" class="subBtn">完成</button>
|
||||
<button @click="readShow=true" class="subBtn">完成</button>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
|
||||
<pickerColor :isShow="showPickerColor" :bottom="0" @callback='getPickerColor' />
|
||||
<u-popup :show="readShow" mode="center" :round="10">
|
||||
<instructionVue @change='readChange' />
|
||||
<u-button type="primary" text="我已知晓" :disabled="hasReaded" @click="postApply"></u-button>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -39,12 +43,16 @@
|
||||
import pickerColor from "./pickerColor.vue"
|
||||
import {IMAGE_BASE_URL,BASE_URL} from '@/utils/config';
|
||||
import {downloadPdfFiles} from '@/utils/download.js'
|
||||
import instructionVue from '../../components/instruction.vue';
|
||||
export default {
|
||||
components: {
|
||||
pickerColor
|
||||
pickerColor,
|
||||
instructionVue
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
readShow:false,
|
||||
hasReaded:true,
|
||||
showPickerColor: false,
|
||||
ctx: '',
|
||||
canvasWidth: 0,
|
||||
@ -122,6 +130,15 @@ import pickerColor from "./pickerColor.vue"
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
readChange(flag){
|
||||
this.hasReaded=flag
|
||||
|
||||
},
|
||||
postApply(){
|
||||
this.readShow=false
|
||||
// 这里可以添加实际的API调用
|
||||
this.subCanvas()
|
||||
},
|
||||
getPickerColor(color) {
|
||||
this.showPickerColor = false;
|
||||
if (color) {
|
||||
@ -585,7 +602,7 @@ import pickerColor from "./pickerColor.vue"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
page {
|
||||
background: #fbfbfb;
|
||||
height: auto;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export const BASE_URL = 'http://10.10.1.6:8071';
|
||||
export const IMAGE_BASE_URL = `http://10.10.1.6:8071`;
|
||||
export const BASE_URL = 'https://jinshan.nantong.info';
|
||||
export const IMAGE_BASE_URL = `https://jinshan.nantong.info`
|
||||
export const WS_BASE_URL = `wss://jinshan.nantong.info`
|
||||
|
||||
|
||||
// http://36.212.197.253:8071
|
||||
|
||||
@ -13,9 +13,16 @@ export const navigateTo = (options,isNavigate=true) => {
|
||||
uni.navigateTo(options);
|
||||
} else {
|
||||
if(isNavigate){
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon:"none"
|
||||
})
|
||||
setTimeout(()=>{
|
||||
uni.redirectTo({
|
||||
url: `/pages/mine/index?redirect=${encodeURIComponent(options.url)}`
|
||||
});
|
||||
},2000)
|
||||
|
||||
}else{
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user