|
@@ -1,21 +1,88 @@
|
|
<template>
|
|
<template>
|
|
<view class="container">
|
|
<view class="container">
|
|
- <view class="header">
|
|
|
|
- <text class="title">语音对话</text>
|
|
|
|
- </view>
|
|
|
|
|
|
+ <!-- <view class="header">
|
|
|
|
+ <text class="title">手部照片采集</text>
|
|
|
|
+ </view> -->
|
|
|
|
|
|
- <view class="chat-container">
|
|
|
|
- <scroll-view scroll-y class="chat-messages" :scroll-top="scrollTop">
|
|
|
|
- <view v-for="(message, index) in messages" :key="index" class="message" :class="message.role">
|
|
|
|
- <text>{{ message.content }}</text>
|
|
|
|
|
|
+ <view class="photo-container">
|
|
|
|
+ <view class="instruction">
|
|
|
|
+ <text>{{ instructions[currentStep] }}</text>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <view class="preview-container">
|
|
|
|
+ <image v-if="photos[currentStep]" :src="photos[currentStep]" mode="aspectFit" class="preview-image"></image>
|
|
|
|
+ <view v-else class="empty-preview">
|
|
|
|
+ <text>{{ previewTexts[currentStep] }}</text>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <view class="thumbnails">
|
|
|
|
+ <view v-for="(photo, index) in photos" :key="index"
|
|
|
|
+ class="thumbnail-item"
|
|
|
|
+ :class="{ active: currentStep === index, completed: photo }">
|
|
|
|
+ <image v-if="photo" :src="photo" mode="aspectFill" class="thumbnail-image" @click="previewPhoto(index)"></image>
|
|
|
|
+ <view v-else class="thumbnail-placeholder">{{ index + 1 }}</view>
|
|
</view>
|
|
</view>
|
|
- </scroll-view>
|
|
|
|
|
|
+ </view>
|
|
</view>
|
|
</view>
|
|
|
|
|
|
<view class="controls">
|
|
<view class="controls">
|
|
- <button class="voice-btn" :class="{ recording: isRecording }" @touchstart="startRecording" @touchend="stopRecording">
|
|
|
|
- <text>{{ isRecording ? '松开结束' : '按住说话' }}</text>
|
|
|
|
- </button>
|
|
|
|
|
|
+ <button class="camera-btn" @click="takePhoto">{{ photos[currentStep] ? '重新拍摄' : '拍摄照片' }}</button>
|
|
|
|
+
|
|
|
|
+ <view class="navigation-btns">
|
|
|
|
+ <button class="nav-btn prev" :disabled="currentStep === 0" @click="prevStep">上一步</button>
|
|
|
|
+ <button class="nav-btn next" :disabled="!canGoNext" @click="nextStep">
|
|
|
|
+ {{ currentStep === 5 && photos[5] ? '完成' : '下一步' }}
|
|
|
|
+ </button>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 为小程序环境添加camera组件 -->
|
|
|
|
+ <view v-if="!isH5 && showCamera" class="camera-container">
|
|
|
|
+ <camera
|
|
|
|
+ device-position="back"
|
|
|
|
+ flash="auto"
|
|
|
|
+ @error="handleCameraError"
|
|
|
|
+ class="camera-component"
|
|
|
|
+ :mode="cameraMode"
|
|
|
|
+ frame-size="medium"
|
|
|
|
+ ></camera>
|
|
|
|
+
|
|
|
|
+ <!-- 添加手势引导蒙层 -->
|
|
|
|
+ <cover-view class="gesture-overlay">
|
|
|
|
+ <cover-image
|
|
|
|
+ :src="gestureOverlays[currentStep]"
|
|
|
|
+ class="gesture-image"
|
|
|
|
+ ></cover-image>
|
|
|
|
+ <cover-view class="gesture-hint">
|
|
|
|
+ <cover-view class="gesture-hint-text">{{ gestureHints[currentStep] }}</cover-view>
|
|
|
|
+ </cover-view>
|
|
|
|
+ </cover-view>
|
|
|
|
+
|
|
|
|
+ <cover-view class="camera-controls">
|
|
|
|
+ <cover-view class="capture-btn" @tap="capturePhotoMP"></cover-view>
|
|
|
|
+ <cover-view class="cancel-btn" @tap="cancelCamera">取消</cover-view>
|
|
|
|
+ </cover-view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <!-- 为浏览器环境添加视频元素 -->
|
|
|
|
+ <view v-if="isH5" class="camera-container" v-show="showCamera">
|
|
|
|
+ <video ref="videoElement" class="camera-video" autoplay></video>
|
|
|
|
+ <canvas ref="canvasElement" class="camera-canvas" style="display: none;"></canvas>
|
|
|
|
+
|
|
|
|
+ <!-- 添加H5环境的手势引导蒙层 -->
|
|
|
|
+ <view class="gesture-overlay">
|
|
|
|
+ <image
|
|
|
|
+ :src="gestureOverlays[currentStep]"
|
|
|
|
+ class="gesture-image"
|
|
|
|
+ ></image>
|
|
|
|
+ <view class="gesture-hint">
|
|
|
|
+ <text class="gesture-hint-text">{{ gestureHints[currentStep] }}</text>
|
|
|
|
+ </view>
|
|
|
|
+ </view>
|
|
|
|
+
|
|
|
|
+ <button class="capture-btn" @click="capturePhoto">拍照</button>
|
|
|
|
+ <button class="cancel-btn" @click="cancelCamera">取消</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
</template>
|
|
@@ -24,167 +91,512 @@
|
|
export default {
|
|
export default {
|
|
data() {
|
|
data() {
|
|
return {
|
|
return {
|
|
- messages: [],
|
|
|
|
- isRecording: false,
|
|
|
|
- recorderManager: null,
|
|
|
|
- innerAudioContext: null,
|
|
|
|
- scrollTop: 0,
|
|
|
|
- apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
|
|
|
|
|
|
+ currentStep: 0,
|
|
|
|
+ photos: [null, null, null, null, null, null], // 存储6张照片:左手正面、左手反面、左手握拳、右手正面、右手反面、右手握拳
|
|
|
|
+ instructions: [
|
|
|
|
+ '请将左手掌正面朝上,保持手指自然伸展,确保整个手掌清晰可见',
|
|
|
|
+ '请将左手掌反面朝上,保持手指自然伸展,确保整个手背清晰可见',
|
|
|
|
+ '请握紧左手拳头,使拳面朝向相机,确保整个拳头清晰可见',
|
|
|
|
+ '请将右手掌正面朝上,保持手指自然伸展,确保整个手掌清晰可见',
|
|
|
|
+ '请将右手掌反面朝上,保持手指自然伸展,确保整个手背清晰可见',
|
|
|
|
+ '请握紧右手拳头,使拳面朝向相机,确保整个拳头清晰可见'
|
|
|
|
+ ],
|
|
|
|
+ previewTexts: [
|
|
|
|
+ '左手掌正面照片预览区',
|
|
|
|
+ '左手掌反面照片预览区',
|
|
|
|
+ '左手握拳照片预览区',
|
|
|
|
+ '右手掌正面照片预览区',
|
|
|
|
+ '右手掌反面照片预览区',
|
|
|
|
+ '右手握拳照片预览区'
|
|
|
|
+ ],
|
|
|
|
+ photoTypes: ['left_palm', 'left_back', 'left_fist', 'right_palm', 'right_back', 'right_fist'], // 照片类型标识
|
|
|
|
+ isH5: false,
|
|
|
|
+ showCamera: false,
|
|
|
|
+ mediaStream: null,
|
|
|
|
+ cameraContext: null,
|
|
|
|
+ cameraMode: 'normal', // 添加相机模式
|
|
|
|
+ cameraInitRetries: 0, // 添加重试计数器
|
|
|
|
+ gestureOverlays: [
|
|
|
|
+ /* '/static/images/palm_overlay.png', // 手掌正面引导图
|
|
|
|
+ '/static/images/back_overlay.png', // 手掌反面引导图
|
|
|
|
+ '/static/images/fist_overlay.png' // 握拳引导图 */
|
|
|
|
+ ],
|
|
|
|
+ gestureHints: [
|
|
|
|
+ '请将左手掌对准轮廓线',
|
|
|
|
+ '请将左手背对准轮廓线',
|
|
|
|
+ '请将左手拳头对准轮廓线',
|
|
|
|
+ '请将右手掌对准轮廓线',
|
|
|
|
+ '请将右手背对准轮廓线',
|
|
|
|
+ '请将右手拳头对准轮廓线'
|
|
|
|
+ ],
|
|
|
|
+ photoLinks: [null, null, null, null, null, null], // 存储上传后的图片链接
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ computed: {
|
|
|
|
+ canGoNext() {
|
|
|
|
+ // 只有当前步骤的照片已拍摄才能进入下一步
|
|
|
|
+ return this.photos[this.currentStep] !== null;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
onLoad() {
|
|
onLoad() {
|
|
- // 初始化录音管理器
|
|
|
|
- this.recorderManager = uni.getRecorderManager();
|
|
|
|
- this.innerAudioContext = uni.createInnerAudioContext();
|
|
|
|
|
|
+ // 判断当前平台
|
|
|
|
+ // #ifdef H5
|
|
|
|
+ this.isH5 = true;
|
|
|
|
+ // #endif
|
|
|
|
|
|
- // 监听录音结束事件
|
|
|
|
- this.recorderManager.onStop((res) => {
|
|
|
|
- this.processAudio(res.tempFilePath);
|
|
|
|
|
|
+ // 检查相机权限
|
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
|
+ uni.authorize({
|
|
|
|
+ scope: 'scope.camera',
|
|
|
|
+ success: () => {
|
|
|
|
+ console.log('相机权限已授权');
|
|
|
|
+ },
|
|
|
|
+ fail: () => {
|
|
|
|
+ uni.showModal({
|
|
|
|
+ title: '提示',
|
|
|
|
+ content: '请授权相机权限以完成手部照片采集',
|
|
|
|
+ success: (res) => {
|
|
|
|
+ if (res.confirm) {
|
|
|
|
+ uni.openSetting();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
});
|
|
});
|
|
|
|
+ // #endif
|
|
|
|
+ },
|
|
|
|
+ beforeDestroy() {
|
|
|
|
+ // 在组件销毁前停止摄像头流
|
|
|
|
+ this.stopMediaStream();
|
|
},
|
|
},
|
|
methods: {
|
|
methods: {
|
|
- startRecording() {
|
|
|
|
- this.isRecording = true;
|
|
|
|
- // 开始录音
|
|
|
|
- this.recorderManager.start({
|
|
|
|
- format: 'mp3',
|
|
|
|
- sampleRate: 16000,
|
|
|
|
- numberOfChannels: 1
|
|
|
|
- });
|
|
|
|
- },
|
|
|
|
- stopRecording() {
|
|
|
|
- this.isRecording = false;
|
|
|
|
- // 停止录音
|
|
|
|
- this.recorderManager.stop();
|
|
|
|
|
|
+ takePhoto() {
|
|
|
|
+ // 根据平台选择不同的拍照方法
|
|
|
|
+ if (this.isH5) {
|
|
|
|
+ this.startCamera();
|
|
|
|
+ } else {
|
|
|
|
+ // 小程序环境使用camera组件
|
|
|
|
+ this.showCamera = true;
|
|
|
|
+ this.cameraInitRetries = 0; // 重置重试计数器
|
|
|
|
+
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
+ // 创建相机上下文
|
|
|
|
+ // #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
|
|
|
|
+ try {
|
|
|
|
+ this.cameraContext = uni.createCameraContext();
|
|
|
|
+ console.log('相机上下文创建成功');
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.error('创建相机上下文失败:', err);
|
|
|
|
+ this.handleCameraInitError();
|
|
|
|
+ }
|
|
|
|
+ // #endif
|
|
|
|
+ });
|
|
|
|
+ }
|
|
},
|
|
},
|
|
- processAudio(filePath) {
|
|
|
|
- // 显示加载状态
|
|
|
|
|
|
+ uploadPhoto(filePathOrData, type) {
|
|
|
|
+ console.log(`准备上传${type}类型的手部照片`);
|
|
|
|
+
|
|
uni.showLoading({
|
|
uni.showLoading({
|
|
- title: '处理中...'
|
|
|
|
|
|
+ title: '上传中...'
|
|
});
|
|
});
|
|
|
|
|
|
- // 将录音文件转换为文本(语音识别)
|
|
|
|
- // 这里需要接入语音识别服务,以下为示例
|
|
|
|
- this.speechToText(filePath).then(text => {
|
|
|
|
- // 添加用户消息
|
|
|
|
- this.addMessage('user', text);
|
|
|
|
-
|
|
|
|
- // 调用DashScope API
|
|
|
|
- this.callDashScopeAPI(text);
|
|
|
|
- }).catch(err => {
|
|
|
|
|
|
+ // 获取用户openid
|
|
|
|
+ const userInfo = uni.getStorageSync('userInfo');
|
|
|
|
+ const openid = userInfo ? JSON.parse(userInfo).openid || '' : '';
|
|
|
|
+ const tenant_id = uni.getStorageSync('tenant_id') || '1';
|
|
|
|
+
|
|
|
|
+ // 检查用户信息是否完整
|
|
|
|
+ if (!tenant_id) {
|
|
uni.hideLoading();
|
|
uni.hideLoading();
|
|
uni.showToast({
|
|
uni.showToast({
|
|
- title: '语音识别失败',
|
|
|
|
|
|
+ title: '用户信息不完整,请重新登录',
|
|
icon: 'none'
|
|
icon: 'none'
|
|
});
|
|
});
|
|
- console.error(err);
|
|
|
|
- });
|
|
|
|
- },
|
|
|
|
- speechToText(filePath) {
|
|
|
|
- // 临时解决方案:直接返回模拟的语音识别结果
|
|
|
|
- return new Promise((resolve) => {
|
|
|
|
- console.log('录音文件路径:', filePath);
|
|
|
|
- // 模拟语音识别结果
|
|
|
|
- setTimeout(() => {
|
|
|
|
- resolve('你好,我需要帮助');
|
|
|
|
- }, 500);
|
|
|
|
- });
|
|
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
|
|
- // 注释掉原来的实现,等待后续接入真实的语音识别服务
|
|
|
|
- /*
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
|
+ // 如果是H5环境且是base64数据
|
|
|
|
+ if (this.isH5 && filePathOrData.startsWith('data:image')) {
|
|
|
|
+ // 将base64转为blob对象
|
|
|
|
+ const base64Data = filePathOrData.split(',')[1];
|
|
|
|
+ const blob = this.base64ToBlob(base64Data, 'image/jpeg');
|
|
|
|
+
|
|
|
|
+ const formData = new FormData();
|
|
|
|
+ // 确保文件参数名称正确
|
|
|
|
+ formData.append('photo_file', blob, `${type}.jpg`);
|
|
|
|
+ formData.append('application_id', '1');
|
|
|
|
+ formData.append('tenant_id', tenant_id);
|
|
|
|
+ formData.append('description', `手部照片-${type}`);
|
|
|
|
+
|
|
|
|
+ console.log('H5上传参数:', {
|
|
|
|
+ application_id: '1',
|
|
|
|
+ tenant_id: tenant_id,
|
|
|
|
+ description: `手部照片-${type}`
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 使用fetch直接上传到 job/upload_posture_photo 接口
|
|
|
|
+ fetch('http://192.168.66.187:8083/job/upload_posture_photo', {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ body: formData
|
|
|
|
+ })
|
|
|
|
+ .then(response => response.json())
|
|
|
|
+ .then(result => {
|
|
|
|
+ console.log('照片上传成功:', result);
|
|
|
|
+
|
|
|
|
+ // 保存图片链接到对应类型的索引位置
|
|
|
|
+ const index = this.photoTypes.indexOf(type);
|
|
|
|
+ if (index !== -1 && result.data && result.data.url) {
|
|
|
|
+ this.$set(this.photoLinks, index, result.data.url);
|
|
|
|
+ console.log(`已保存${type}类型照片链接:`, result.data.url);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ uni.hideLoading();
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '照片上传成功',
|
|
|
|
+ icon: 'success'
|
|
|
|
+ });
|
|
|
|
+ })
|
|
|
|
+ .catch(error => {
|
|
|
|
+ console.error('照片上传失败:', error);
|
|
|
|
+ uni.hideLoading();
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '照片上传失败,请重试',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ // 小程序环境的上传逻辑
|
|
|
|
+ console.log(`小程序环境上传文件路径:`, filePathOrData);
|
|
|
|
+ console.log('小程序上传参数:', {
|
|
|
|
+ application_id: '1',
|
|
|
|
+ tenant_id: tenant_id,
|
|
|
|
+ description: `手部照片-${type}`
|
|
|
|
+ });
|
|
|
|
+
|
|
uni.uploadFile({
|
|
uni.uploadFile({
|
|
- url: '您的语音识别服务URL',
|
|
|
|
- filePath: filePath,
|
|
|
|
- name: 'file',
|
|
|
|
- success: (res) => {
|
|
|
|
- const data = JSON.parse(res.data);
|
|
|
|
- if (data && data.result) {
|
|
|
|
- resolve(data.result);
|
|
|
|
- } else {
|
|
|
|
- // 模拟语音识别结果,实际项目中请删除此行
|
|
|
|
- resolve('你好,我需要帮助');
|
|
|
|
- // reject(new Error('语音识别失败'));
|
|
|
|
|
|
+ url: 'http://192.168.66.187:8083/job/upload_posture_photo',
|
|
|
|
+ filePath: filePathOrData,
|
|
|
|
+ name: 'photo_file', // 确保文件参数名称正确
|
|
|
|
+ formData: { // 使用formData替代data以确保参数正确传递
|
|
|
|
+ 'application_id': '1',
|
|
|
|
+ 'tenant_id': tenant_id,
|
|
|
|
+ 'description': `手部照片-${type}`
|
|
|
|
+ },
|
|
|
|
+ success: (uploadRes) => {
|
|
|
|
+ console.log('照片上传成功:', uploadRes);
|
|
|
|
+
|
|
|
|
+ // 解析返回的JSON字符串
|
|
|
|
+ let result;
|
|
|
|
+ try {
|
|
|
|
+ if (typeof uploadRes.data === 'string') {
|
|
|
|
+ result = JSON.parse(uploadRes.data);
|
|
|
|
+ } else {
|
|
|
|
+ result = uploadRes.data;
|
|
|
|
+ }
|
|
|
|
+ console.log('解析后的上传结果:', result);
|
|
|
|
+
|
|
|
|
+ // 保存图片链接到对应类型的索引位置
|
|
|
|
+ const index = this.photoTypes.indexOf(type);
|
|
|
|
+ if (index !== -1 && result.data && result.data.url) {
|
|
|
|
+ this.$set(this.photoLinks, index, result.data.url);
|
|
|
|
+ console.log(`已保存${type}类型照片链接:`, result.data.url);
|
|
|
|
+ }
|
|
|
|
+ } catch (e) {
|
|
|
|
+ console.error('解析上传结果失败:', e);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ uni.hideLoading();
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '照片上传成功',
|
|
|
|
+ icon: 'success'
|
|
|
|
+ });
|
|
},
|
|
},
|
|
- fail: reject
|
|
|
|
- });
|
|
|
|
- });
|
|
|
|
- */
|
|
|
|
- },
|
|
|
|
- callDashScopeAPI(userInput) {
|
|
|
|
- // 构建请求体
|
|
|
|
- const requestBody = {
|
|
|
|
- model: "qwen-plus",
|
|
|
|
- messages: [
|
|
|
|
- {
|
|
|
|
- role: "system",
|
|
|
|
- content: "You are a helpful assistant."
|
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
+ console.error('照片上传失败:', err);
|
|
|
|
+ uni.hideLoading();
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '照片上传失败,请重试',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
},
|
|
},
|
|
- {
|
|
|
|
- role: "user",
|
|
|
|
- content: userInput
|
|
|
|
|
|
+ complete: () => {
|
|
|
|
+ uni.hideLoading();
|
|
}
|
|
}
|
|
- ]
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- // 发送请求到DashScope API
|
|
|
|
- uni.request({
|
|
|
|
- url: this.apiUrl,
|
|
|
|
- method: 'POST',
|
|
|
|
- header: {
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
- 'Authorization': 'Bearer YOUR_API_KEY' // 替换为您的API密钥
|
|
|
|
- },
|
|
|
|
- data: requestBody,
|
|
|
|
- success: (res) => {
|
|
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ prevStep() {
|
|
|
|
+ if (this.currentStep > 0) {
|
|
|
|
+ this.currentStep--;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ nextStep() {
|
|
|
|
+ if (this.currentStep < 5) {
|
|
|
|
+ this.currentStep++;
|
|
|
|
+ } else if (this.currentStep === 5 && this.photos[5]) {
|
|
|
|
+ // 所有照片都已拍摄完成,可以进行提交或跳转
|
|
|
|
+ this.submitAllPhotos();
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ previewPhoto(index) {
|
|
|
|
+ // 如果照片存在,则预览
|
|
|
|
+ if (this.photos[index]) {
|
|
|
|
+ uni.previewImage({
|
|
|
|
+ urls: [this.photos[index]],
|
|
|
|
+ current: this.photos[index]
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ submitAllPhotos() {
|
|
|
|
+ // 检查是否所有照片都已拍摄
|
|
|
|
+ if (this.photos.every(photo => photo !== null)) {
|
|
|
|
+ uni.showLoading({
|
|
|
|
+ title: '处理中...'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 所有照片已经单独上传,这里只需要处理跳转
|
|
|
|
+ setTimeout(() => {
|
|
uni.hideLoading();
|
|
uni.hideLoading();
|
|
- if (res.statusCode === 200 && res.data && res.data.choices && res.data.choices.length > 0) {
|
|
|
|
- const assistantMessage = res.data.choices[0].message.content;
|
|
|
|
- this.addMessage('assistant', assistantMessage);
|
|
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '手部照片采集完成',
|
|
|
|
+ icon: 'success',
|
|
|
|
+ duration: 2000,
|
|
|
|
+ success: () => {
|
|
|
|
+ // 延迟跳转,让用户看到成功提示
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ //uni.navigateBack();
|
|
|
|
+ // 或者跳转到下一个页面
|
|
|
|
+ uni.navigateTo({
|
|
|
|
+ url: '/pages/ResumeEvaluation/ResumeEvaluation'
|
|
|
|
+ });
|
|
|
|
+ }, 2000);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }, 500);
|
|
|
|
+ } else {
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '请完成所有照片拍摄',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ // H5环境下启动摄像头
|
|
|
|
+ startCamera() {
|
|
|
|
+ this.showCamera = true;
|
|
|
|
+
|
|
|
|
+ // 确保浏览器支持getUserMedia
|
|
|
|
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
|
|
+ navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
|
|
|
+ .then(stream => {
|
|
|
|
+ this.mediaStream = stream;
|
|
|
|
+ // 等待DOM更新后再设置视频源
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
+ const videoElement = this.$refs.videoElement;
|
|
|
|
+ if (videoElement) {
|
|
|
|
+ videoElement.srcObject = stream;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ })
|
|
|
|
+ .catch(error => {
|
|
|
|
+ console.error('获取摄像头失败:', error);
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '无法访问摄像头,请检查权限设置',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '您的浏览器不支持摄像头功能',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // H5环境下拍照
|
|
|
|
+ capturePhoto() {
|
|
|
|
+ const videoElement = this.$refs.videoElement;
|
|
|
|
+ const canvasElement = this.$refs.canvasElement;
|
|
|
|
+
|
|
|
|
+ if (videoElement && canvasElement) {
|
|
|
|
+ const context = canvasElement.getContext('2d');
|
|
|
|
+
|
|
|
|
+ // 设置canvas尺寸与视频一致
|
|
|
|
+ canvasElement.width = videoElement.videoWidth;
|
|
|
|
+ canvasElement.height = videoElement.videoHeight;
|
|
|
|
+
|
|
|
|
+ // 在canvas上绘制当前视频帧
|
|
|
|
+ context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
|
|
|
|
+
|
|
|
|
+ // 将canvas内容转为base64图片
|
|
|
|
+ const photoData = canvasElement.toDataURL('image/jpeg');
|
|
|
|
+
|
|
|
|
+ // 更新照片数据
|
|
|
|
+ this.$set(this.photos, this.currentStep, photoData);
|
|
|
|
+
|
|
|
|
+ // 上传照片
|
|
|
|
+ this.uploadPhoto(photoData, this.photoTypes[this.currentStep]);
|
|
|
|
+
|
|
|
|
+ // 关闭摄像头
|
|
|
|
+ this.stopMediaStream();
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 小程序环境下拍照
|
|
|
|
+ capturePhotoMP() {
|
|
|
|
+ if (!this.cameraContext) {
|
|
|
|
+ console.error('相机上下文不存在');
|
|
|
|
+ this.handleCameraInitError();
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ this.cameraContext.takePhoto({
|
|
|
|
+ quality: 'high',
|
|
|
|
+ success: (res) => {
|
|
|
|
+ console.log('拍照成功:', res);
|
|
|
|
+ // 更新当前步骤的照片
|
|
|
|
+ this.$set(this.photos, this.currentStep, res.tempImagePath);
|
|
|
|
+
|
|
|
|
+ // 上传照片
|
|
|
|
+ this.uploadPhoto(res.tempImagePath, this.photoTypes[this.currentStep]);
|
|
|
|
|
|
- // 可选:将回复转为语音(文本转语音)
|
|
|
|
- this.textToSpeech(assistantMessage);
|
|
|
|
- } else {
|
|
|
|
|
|
+ // 关闭相机
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ },
|
|
|
|
+ fail: (err) => {
|
|
|
|
+ console.error('拍照失败:', err);
|
|
uni.showToast({
|
|
uni.showToast({
|
|
- title: '获取回复失败',
|
|
|
|
|
|
+ title: '拍照失败,请重试',
|
|
icon: 'none'
|
|
icon: 'none'
|
|
});
|
|
});
|
|
- console.error('API响应错误:', res);
|
|
|
|
|
|
+ // 尝试使用替代方法
|
|
|
|
+ this.fallbackToChooseImage();
|
|
}
|
|
}
|
|
|
|
+ });
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.error('takePhoto方法执行失败:', err);
|
|
|
|
+ this.fallbackToChooseImage();
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 添加相机初始化错误处理
|
|
|
|
+ handleCameraInitError() {
|
|
|
|
+ if (this.cameraInitRetries < 3) {
|
|
|
|
+ this.cameraInitRetries++;
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: `相机初始化失败,正在重试(${this.cameraInitRetries}/3)`,
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ this.takePhoto();
|
|
|
|
+ }, 500);
|
|
|
|
+ }, 1000);
|
|
|
|
+ } else {
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '相机初始化失败,将使用系统相机',
|
|
|
|
+ icon: 'none'
|
|
|
|
+ });
|
|
|
|
+ this.fallbackToChooseImage();
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 添加备用的系统相机方法
|
|
|
|
+ fallbackToChooseImage() {
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+
|
|
|
|
+ // 使用系统相机作为备选方案
|
|
|
|
+ uni.chooseImage({
|
|
|
|
+ count: 1,
|
|
|
|
+ sourceType: ['camera'],
|
|
|
|
+ success: (res) => {
|
|
|
|
+ // 更新当前步骤的照片
|
|
|
|
+ this.$set(this.photos, this.currentStep, res.tempFilePaths[0]);
|
|
|
|
+
|
|
|
|
+ // 上传照片
|
|
|
|
+ this.uploadPhoto(res.tempFilePaths[0], this.photoTypes[this.currentStep]);
|
|
},
|
|
},
|
|
fail: (err) => {
|
|
fail: (err) => {
|
|
- uni.hideLoading();
|
|
|
|
|
|
+ console.error('选择图片失败:', err);
|
|
uni.showToast({
|
|
uni.showToast({
|
|
- title: '网络请求失败',
|
|
|
|
|
|
+ title: '拍照失败,请重试',
|
|
icon: 'none'
|
|
icon: 'none'
|
|
});
|
|
});
|
|
- console.error(err);
|
|
|
|
}
|
|
}
|
|
});
|
|
});
|
|
},
|
|
},
|
|
- addMessage(role, content) {
|
|
|
|
- this.messages.push({ role, content });
|
|
|
|
- // 滚动到底部
|
|
|
|
- this.$nextTick(() => {
|
|
|
|
- this.scrollTop = 9999;
|
|
|
|
|
|
+
|
|
|
|
+ // 修改相机错误处理方法
|
|
|
|
+ handleCameraError(e) {
|
|
|
|
+ console.error('相机错误:', e.detail);
|
|
|
|
+
|
|
|
|
+ // 记录更详细的错误信息
|
|
|
|
+ const errorInfo = JSON.stringify(e.detail);
|
|
|
|
+ console.log('详细错误信息:', errorInfo);
|
|
|
|
+
|
|
|
|
+ uni.showToast({
|
|
|
|
+ title: '相机启动失败,将使用系统相机',
|
|
|
|
+ icon: 'none'
|
|
});
|
|
});
|
|
|
|
+
|
|
|
|
+ // 使用备用方法
|
|
|
|
+ this.fallbackToChooseImage();
|
|
},
|
|
},
|
|
- textToSpeech(text) {
|
|
|
|
- // 这里需要接入文本转语音服务
|
|
|
|
- // 以下为示例,实际实现需要根据您使用的文本转语音服务调整
|
|
|
|
- uni.request({
|
|
|
|
- url: '您的文本转语音服务URL',
|
|
|
|
- method: 'POST',
|
|
|
|
- data: { text },
|
|
|
|
- success: (res) => {
|
|
|
|
- if (res.statusCode === 200 && res.data && res.data.audio_url) {
|
|
|
|
- // 播放语音
|
|
|
|
- this.innerAudioContext.src = res.data.audio_url;
|
|
|
|
- this.innerAudioContext.play();
|
|
|
|
- }
|
|
|
|
|
|
+
|
|
|
|
+ // 取消拍照 - 更新为同时支持H5和小程序
|
|
|
|
+ cancelCamera() {
|
|
|
|
+ if (this.isH5) {
|
|
|
|
+ this.stopMediaStream();
|
|
|
|
+ }
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 停止摄像头流
|
|
|
|
+ stopMediaStream() {
|
|
|
|
+ if (this.mediaStream) {
|
|
|
|
+ this.mediaStream.getTracks().forEach(track => {
|
|
|
|
+ track.stop();
|
|
|
|
+ });
|
|
|
|
+ this.mediaStream = null;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // base64转blob工具方法
|
|
|
|
+ base64ToBlob(base64, mimeType) {
|
|
|
|
+ const byteCharacters = atob(base64);
|
|
|
|
+ const byteArrays = [];
|
|
|
|
+
|
|
|
|
+ for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
|
|
|
+ const slice = byteCharacters.slice(offset, offset + 512);
|
|
|
|
+
|
|
|
|
+ const byteNumbers = new Array(slice.length);
|
|
|
|
+ for (let i = 0; i < slice.length; i++) {
|
|
|
|
+ byteNumbers[i] = slice.charCodeAt(i);
|
|
}
|
|
}
|
|
- });
|
|
|
|
|
|
+
|
|
|
|
+ const byteArray = new Uint8Array(byteNumbers);
|
|
|
|
+ byteArrays.push(byteArray);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return new Blob(byteArrays, { type: mimeType });
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ retryCamera() {
|
|
|
|
+ this.showCamera = false;
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ this.takePhoto();
|
|
|
|
+ }, 500);
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 可以添加一个方法来切换蒙层显示
|
|
|
|
+ toggleGestureOverlay() {
|
|
|
|
+ // 实现蒙层显示/隐藏的逻辑
|
|
|
|
+ // 如果需要的话
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -194,7 +606,7 @@
|
|
.container {
|
|
.container {
|
|
display: flex;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
- height: 100vh;
|
|
|
|
|
|
+ height: 94vh;
|
|
background-color: #f5f5f5;
|
|
background-color: #f5f5f5;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -210,54 +622,217 @@
|
|
font-weight: bold;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
|
|
|
|
-.chat-container {
|
|
|
|
|
|
+.photo-container {
|
|
flex: 1;
|
|
flex: 1;
|
|
padding: 20rpx;
|
|
padding: 20rpx;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.instruction {
|
|
|
|
+ padding: 20rpx;
|
|
|
|
+ background-color: #E5E5EA;
|
|
|
|
+ border-radius: 10rpx;
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-container {
|
|
|
|
+ flex: 1;
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ align-items: center;
|
|
|
|
+ background-color: #E5E5EA;
|
|
|
|
+ border-radius: 10rpx;
|
|
|
|
+ margin-bottom: 20rpx;
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
}
|
|
}
|
|
|
|
|
|
-.chat-messages {
|
|
|
|
|
|
+.preview-image {
|
|
|
|
+ width: 100%;
|
|
height: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
|
|
-.message {
|
|
|
|
|
|
+.empty-preview {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ align-items: center;
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ color: #999;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.thumbnails {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-around;
|
|
margin-bottom: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
- padding: 20rpx;
|
|
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.thumbnail-item {
|
|
|
|
+ width: 150rpx;
|
|
|
|
+ height: 150rpx;
|
|
border-radius: 10rpx;
|
|
border-radius: 10rpx;
|
|
- max-width: 80%;
|
|
|
|
- word-break: break-word;
|
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ background-color: #E5E5EA;
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ align-items: center;
|
|
|
|
+ border: 4rpx solid transparent;
|
|
}
|
|
}
|
|
|
|
|
|
-.message.user {
|
|
|
|
- align-self: flex-end;
|
|
|
|
- background-color: #007AFF;
|
|
|
|
- color: white;
|
|
|
|
- margin-left: auto;
|
|
|
|
|
|
+.thumbnail-item.active {
|
|
|
|
+ border-color: #007AFF;
|
|
}
|
|
}
|
|
|
|
|
|
-.message.assistant {
|
|
|
|
- align-self: flex-start;
|
|
|
|
- background-color: #E5E5EA;
|
|
|
|
- color: #333;
|
|
|
|
|
|
+.thumbnail-item.completed {
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.thumbnail-image {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.thumbnail-placeholder {
|
|
|
|
+ font-size: 40rpx;
|
|
|
|
+ color: #999;
|
|
}
|
|
}
|
|
|
|
|
|
.controls {
|
|
.controls {
|
|
padding: 20rpx;
|
|
padding: 20rpx;
|
|
background-color: #ffffff;
|
|
background-color: #ffffff;
|
|
- border-top: 1px solid #e0e0e0;
|
|
|
|
|
|
+ border-top: 1px solid #6c5ce7;
|
|
}
|
|
}
|
|
|
|
|
|
-.voice-btn {
|
|
|
|
|
|
+.camera-btn {
|
|
width: 100%;
|
|
width: 100%;
|
|
height: 80rpx;
|
|
height: 80rpx;
|
|
line-height: 80rpx;
|
|
line-height: 80rpx;
|
|
text-align: center;
|
|
text-align: center;
|
|
- background-color: #007AFF;
|
|
|
|
|
|
+ background-color: #6c5ce7;
|
|
color: white;
|
|
color: white;
|
|
border-radius: 40rpx;
|
|
border-radius: 40rpx;
|
|
|
|
+ margin-bottom: 20rpx;
|
|
}
|
|
}
|
|
|
|
|
|
-.voice-btn.recording {
|
|
|
|
- background-color: #FF3B30;
|
|
|
|
|
|
+.navigation-btns {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.nav-btn {
|
|
|
|
+ width: 48%;
|
|
|
|
+ height: 80rpx;
|
|
|
|
+ line-height: 80rpx;
|
|
|
|
+ text-align: center;
|
|
|
|
+ border-radius: 40rpx;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.nav-btn.prev {
|
|
|
|
+ background-color: #E5E5EA;
|
|
|
|
+ color: #333;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.nav-btn.next {
|
|
|
|
+ background-color: #34C759;
|
|
|
|
+ color: white;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.nav-btn[disabled] {
|
|
|
|
+ opacity: 0.5;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 浏览器摄像头相关样式 */
|
|
|
|
+.camera-container {
|
|
|
|
+ position: fixed;
|
|
|
|
+ top: 0;
|
|
|
|
+ left: 0;
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ background-color: #000;
|
|
|
|
+ z-index: 999;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ align-items: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.camera-video {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 70%;
|
|
|
|
+ object-fit: cover;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.camera-canvas {
|
|
|
|
+ display: none;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.capture-btn {
|
|
|
|
+ width: 200rpx;
|
|
|
|
+ height: 200rpx;
|
|
|
|
+ border-radius: 50%;
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
+ border: 10rpx solid #6c5ce7;
|
|
|
|
+ margin: 30rpx 0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.cancel-btn {
|
|
|
|
+ width: 200rpx;
|
|
|
|
+ height: 80rpx;
|
|
|
|
+ line-height: 80rpx;
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.3);
|
|
|
|
+ color: #ffffff;
|
|
|
|
+ text-align: center;
|
|
|
|
+ border-radius: 40rpx;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 小程序相机组件样式 */
|
|
|
|
+.camera-component {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 80%;
|
|
|
|
+ display: block;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.camera-controls {
|
|
|
|
+ width: 100%;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ align-items: center;
|
|
|
|
+ position: absolute;
|
|
|
|
+ bottom: 50rpx;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 添加手势引导蒙层样式 */
|
|
|
|
+.gesture-overlay {
|
|
|
|
+ position: absolute;
|
|
|
|
+ top: 0;
|
|
|
|
+ left: 0;
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 80%;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ align-items: center;
|
|
|
|
+ pointer-events: none; /* 允许点击穿透 */
|
|
|
|
+ z-index: 10;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.gesture-image {
|
|
|
|
+ width: 70%;
|
|
|
|
+ height: 70%;
|
|
|
|
+ opacity: 0.6;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.gesture-hint {
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
|
+ border-radius: 10rpx;
|
|
|
|
+ margin-top: 20rpx;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.gesture-hint-text {
|
|
|
|
+ color: white;
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
+ text-align: center;
|
|
}
|
|
}
|
|
</style>
|
|
</style>
|
|
|
|
+
|