vue2-client 1.9.72 → 1.9.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/base-client/components/common/Recording/Recoding.vue +159 -0
- package/src/base-client/components/common/Recording/index.js +3 -0
- package/src/base-client/components/common/XConversation/XConversation.vue +140 -0
- package/src/base-client/components/common/XConversation/XConversationDemo.vue +28 -0
- package/src/base-client/components/common/XForm/XFormItem.vue +14 -0
- package/src/base-client/components/common/XForm/XTreeSelect.vue +9 -14
- package/src/base-client/components/common/XFormTable/demo.vue +1 -1
- package/src/config/CreateQueryConfig.js +3 -0
- package/src/router/async/router.map.js +4 -2
package/package.json
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<button @click="startRecording" :disabled="isRecording">开始录音</button>
|
|
4
|
+
<button @click="stopRecording" :disabled="!isRecording">停止录音</button>
|
|
5
|
+
<!-- <audio v-if="audioUrl" :src="audioUrl" controls></audio>-->
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script>
|
|
10
|
+
import { getConfigByName } from '@vue2-client/services/api/common'
|
|
11
|
+
export default {
|
|
12
|
+
name: 'PersonSetting',
|
|
13
|
+
data () {
|
|
14
|
+
return {
|
|
15
|
+
isRecording: false, // 标识是否正在录音
|
|
16
|
+
mediaRecorder: null, // MediaRecorder 实例
|
|
17
|
+
audioChunks: [], // 存储录音数据块
|
|
18
|
+
audioUrl: null, // 录音生成的 URL
|
|
19
|
+
inputData: '',
|
|
20
|
+
config: {}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
props: {
|
|
24
|
+
},
|
|
25
|
+
watch: {},
|
|
26
|
+
components: {},
|
|
27
|
+
created () {
|
|
28
|
+
getConfigByName('V4RecodingConfig', undefined, (x) => {
|
|
29
|
+
Object.assign(this.config, x)
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
methods: {
|
|
33
|
+
async startRecording () {
|
|
34
|
+
this.inputData = ''
|
|
35
|
+
const url = this.config.webSocketURL // WebSocket地址
|
|
36
|
+
this.setupWebSocket(url)
|
|
37
|
+
|
|
38
|
+
// 等待 WebSocket 连接成功
|
|
39
|
+
await this.waitForWebSocketConnection()
|
|
40
|
+
|
|
41
|
+
this.isRecording = true
|
|
42
|
+
|
|
43
|
+
// 初始化 AudioContext
|
|
44
|
+
this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
|
|
45
|
+
|
|
46
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'录音软件不支持您的浏览器!请使用现代浏览器或确保您正在使用 HTTPS。'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 获取麦克风输入流
|
|
53
|
+
this.audioStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
54
|
+
|
|
55
|
+
// 设置音频处理流程
|
|
56
|
+
this.setupAudioProcessing()
|
|
57
|
+
},
|
|
58
|
+
setupWebSocket (url) {
|
|
59
|
+
this.ws = new WebSocket(url)
|
|
60
|
+
|
|
61
|
+
this.ws.onmessage = (event) => {
|
|
62
|
+
const data = JSON.parse(event.data)
|
|
63
|
+
console.log('Result:', data.result)
|
|
64
|
+
if (data.result !== undefined) {
|
|
65
|
+
this.inputData += data.result
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.ws.onclose = () => console.log('WebSocket closed.')
|
|
70
|
+
this.ws.onerror = (error) => console.error('WebSocket error:', error)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
waitForWebSocketConnection () {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
this.ws.onopen = () => {
|
|
76
|
+
console.log('WebSocket 连接成功')
|
|
77
|
+
resolve()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.ws.onerror = (error) => {
|
|
81
|
+
reject(new Error('WebSocket 连接失败: ' + error.message))
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
setupAudioProcessing () {
|
|
87
|
+
try {
|
|
88
|
+
const source = this.audioContext.createMediaStreamSource(this.audioStream)
|
|
89
|
+
const processor = this.audioContext.createScriptProcessor(4096, 1, 1)
|
|
90
|
+
|
|
91
|
+
processor.onaudioprocess = (e) => {
|
|
92
|
+
if (!this.isRecording) return
|
|
93
|
+
|
|
94
|
+
const inputData = e.inputBuffer.getChannelData(0)
|
|
95
|
+
|
|
96
|
+
// Resample to 16000 Hz if needed
|
|
97
|
+
const resampledData = this.resample(inputData, this.audioContext.sampleRate, 16000)
|
|
98
|
+
|
|
99
|
+
// Convert to 16-bit PCM and send via WebSocket
|
|
100
|
+
const byteArray = this.convertTo16BitPCM(resampledData)
|
|
101
|
+
this.ws.send(byteArray)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
source.connect(processor)
|
|
105
|
+
processor.connect(this.audioContext.destination)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error setting up audio processing:', error)
|
|
108
|
+
throw error
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
stopRecording () {
|
|
112
|
+
this.isRecording = false
|
|
113
|
+
if (this.ws) {
|
|
114
|
+
// 可以添加一个事件处理器来处理WebSocket关闭
|
|
115
|
+
this.ws.onclose = () => {
|
|
116
|
+
console.log('WebSocket closed.')
|
|
117
|
+
// 这里可以添加WebSocket关闭后的清理工作
|
|
118
|
+
}
|
|
119
|
+
// 关闭WebSocket连接
|
|
120
|
+
this.ws.close()
|
|
121
|
+
}
|
|
122
|
+
if (this.audioStream) {
|
|
123
|
+
this.audioStream.getTracks().forEach(track => track.stop())
|
|
124
|
+
}
|
|
125
|
+
if (this.audioContext) {
|
|
126
|
+
this.audioContext.close()
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
resample (data, inputSampleRate, outputSampleRate) {
|
|
130
|
+
if (inputSampleRate === outputSampleRate) {
|
|
131
|
+
return data
|
|
132
|
+
}
|
|
133
|
+
const sampleRateRatio = inputSampleRate / outputSampleRate
|
|
134
|
+
const newLength = Math.round(data.length / sampleRateRatio)
|
|
135
|
+
const resampledData = new Float32Array(newLength)
|
|
136
|
+
for (let i = 0; i < newLength; i++) {
|
|
137
|
+
const index = Math.round(i * sampleRateRatio)
|
|
138
|
+
resampledData[i] = data[index]
|
|
139
|
+
}
|
|
140
|
+
return resampledData
|
|
141
|
+
},
|
|
142
|
+
convertTo16BitPCM (data) {
|
|
143
|
+
const output = new Int16Array(data.length)
|
|
144
|
+
for (let i = 0; i < data.length; i++) {
|
|
145
|
+
output[i] = Math.max(-1, Math.min(1, data[i])) * 0x7FFF
|
|
146
|
+
}
|
|
147
|
+
return new Uint8Array(output.buffer)
|
|
148
|
+
},
|
|
149
|
+
getRecordingData () {
|
|
150
|
+
return this.inputData
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<style lang="less" scoped>
|
|
158
|
+
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="chat-container" v-if="loading">
|
|
4
|
+
<!-- 对话展示区域 -->
|
|
5
|
+
<div class="chat-content">
|
|
6
|
+
<div
|
|
7
|
+
v-for="(message, index) in messages"
|
|
8
|
+
:key="index"
|
|
9
|
+
:class="['chat-message', message.type]"
|
|
10
|
+
>
|
|
11
|
+
<span class="chat-avatar">{{ message.type === 'user' ? '👤' : '🤖' }}</span>
|
|
12
|
+
<div class="chat-text">{{ message.text }}</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- 输入框和发送按钮 -->
|
|
17
|
+
<div class="chat-input">
|
|
18
|
+
<a-input
|
|
19
|
+
v-model="inputMessage"
|
|
20
|
+
placeholder="Type your message..."
|
|
21
|
+
@pressEnter="sendMessage"
|
|
22
|
+
/>
|
|
23
|
+
<a-button type="primary" @click="sendMessage">Send</a-button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<a-spin :spinning="!loading" />
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import { runLogic } from '@vue2-client/services/api/common'
|
|
32
|
+
export default {
|
|
33
|
+
data () {
|
|
34
|
+
return {
|
|
35
|
+
serviceName: undefined,
|
|
36
|
+
// 组件加载状态
|
|
37
|
+
loading: false,
|
|
38
|
+
// 配置内容
|
|
39
|
+
renderConfig: undefined,
|
|
40
|
+
inputMessage: '', // 用户输入的内容
|
|
41
|
+
additionalInfo: {},
|
|
42
|
+
messages: [ // 消息列表
|
|
43
|
+
{ type: 'bot', text: 'Hello! How can I assist you today?' },
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
methods: {
|
|
48
|
+
setAddtionalInfo (params) {
|
|
49
|
+
this.additionalInfo = params
|
|
50
|
+
},
|
|
51
|
+
init (params) {
|
|
52
|
+
const {
|
|
53
|
+
serviceName,
|
|
54
|
+
// 配置内容
|
|
55
|
+
value
|
|
56
|
+
} = params
|
|
57
|
+
this.renderConfig = value
|
|
58
|
+
this.loading = true
|
|
59
|
+
this.serviceName = serviceName
|
|
60
|
+
},
|
|
61
|
+
async sendMessage () {
|
|
62
|
+
if (!this.inputMessage.trim()) return
|
|
63
|
+
|
|
64
|
+
// 添加用户消息
|
|
65
|
+
this.messages.push({ type: 'user', text: this.inputMessage })
|
|
66
|
+
|
|
67
|
+
// 清空输入框
|
|
68
|
+
const userMessage = this.inputMessage
|
|
69
|
+
this.inputMessage = ''
|
|
70
|
+
|
|
71
|
+
// 模拟机器人的回复
|
|
72
|
+
const response = await runLogic(this.renderConfig.logicName, {
|
|
73
|
+
question: userMessage,
|
|
74
|
+
additionalInfo: this.additionalInfo
|
|
75
|
+
}, this.serviceName)
|
|
76
|
+
this.messages.push({ type: 'bot', text: response })
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
.chat-container {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
height: 500px;
|
|
87
|
+
border: 1px solid #d9d9d9;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.chat-content {
|
|
93
|
+
flex: 1;
|
|
94
|
+
padding: 16px;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
background: #f5f5f5;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.chat-message {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: flex-start;
|
|
102
|
+
margin-bottom: 10px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.chat-message.user {
|
|
106
|
+
justify-content: flex-end;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.chat-message.bot {
|
|
110
|
+
justify-content: flex-start;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.chat-avatar {
|
|
114
|
+
margin: 0 8px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.chat-text {
|
|
118
|
+
max-width: 70%;
|
|
119
|
+
padding: 8px 12px;
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
background-color: #fff;
|
|
122
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.chat-message.user .chat-text {
|
|
126
|
+
background-color: #e6f7ff;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.chat-input {
|
|
130
|
+
display: flex;
|
|
131
|
+
padding: 8px;
|
|
132
|
+
background: #fff;
|
|
133
|
+
border-top: 1px solid #d9d9d9;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.chat-input a-input {
|
|
137
|
+
flex: 1;
|
|
138
|
+
margin-right: 8px;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<h1>对话示例</h1>
|
|
4
|
+
<XConversation ref="XConversation"/>
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
import XConversation from './XConversation.vue' // 请根据实际路径调整
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
name: 'ConversationDemo',
|
|
13
|
+
components: {
|
|
14
|
+
XConversation
|
|
15
|
+
},
|
|
16
|
+
mounted () {
|
|
17
|
+
this.$refs.XConversation.init({
|
|
18
|
+
value: { logicName: 'submitMessage' },
|
|
19
|
+
serviceName: 'af-his'
|
|
20
|
+
})
|
|
21
|
+
this.$refs.XConversation.setAddtionalInfo({ patient_id: 37 })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style scoped>
|
|
27
|
+
/* 这里可以添加你的样式 */
|
|
28
|
+
</style>
|
|
@@ -611,6 +611,15 @@
|
|
|
611
611
|
></x-license-plate>
|
|
612
612
|
</a-form-model-item>
|
|
613
613
|
</x-form-col>
|
|
614
|
+
<!-- 录音 -->
|
|
615
|
+
<x-form-col
|
|
616
|
+
v-else-if="attr.type === 'recording' && show"
|
|
617
|
+
:flex="attr.flex">
|
|
618
|
+
<recoding
|
|
619
|
+
ref="recording"
|
|
620
|
+
>
|
|
621
|
+
</recoding>
|
|
622
|
+
</x-form-col>
|
|
614
623
|
</template>
|
|
615
624
|
<script>
|
|
616
625
|
import { debounce } from 'ant-design-vue/lib/vc-table/src/utils'
|
|
@@ -633,6 +642,7 @@ export default {
|
|
|
633
642
|
name: 'XFormItem',
|
|
634
643
|
components: {
|
|
635
644
|
XFormTable: () => import('@vue2-client/base-client/components/common/XFormTable/XFormTable.vue'),
|
|
645
|
+
Recoding: () => import('@vue2-client/base-client/components/common/Recording/Recoding.vue'),
|
|
636
646
|
XLicensePlate,
|
|
637
647
|
XTreeSelect,
|
|
638
648
|
XFormCol,
|
|
@@ -1136,6 +1146,10 @@ export default {
|
|
|
1136
1146
|
},
|
|
1137
1147
|
rowChooseSearchAfterQuery () {
|
|
1138
1148
|
this.searching = false
|
|
1149
|
+
},
|
|
1150
|
+
// 获取 recording 转换后的数据
|
|
1151
|
+
getRecodingData () {
|
|
1152
|
+
return this.$refs.recording.getRecordingData()
|
|
1139
1153
|
}
|
|
1140
1154
|
}
|
|
1141
1155
|
}
|
|
@@ -88,15 +88,7 @@ export default {
|
|
|
88
88
|
} else {
|
|
89
89
|
if (this.mode === '查询') {
|
|
90
90
|
const values = []
|
|
91
|
-
|
|
92
|
-
if (value.includes(item.value)) {
|
|
93
|
-
values.push(item.value)
|
|
94
|
-
if (item.children && item.children.length) {
|
|
95
|
-
this.getValues(item.children, value, values)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
this.$emit('onChange', values)
|
|
91
|
+
this.getValues(option, value, values)
|
|
100
92
|
} else {
|
|
101
93
|
this.value = value
|
|
102
94
|
}
|
|
@@ -109,11 +101,14 @@ export default {
|
|
|
109
101
|
this.$emit('mounted', this)
|
|
110
102
|
this.loaded = true
|
|
111
103
|
},
|
|
112
|
-
getValues (
|
|
113
|
-
for (const item of
|
|
114
|
-
|
|
104
|
+
getValues (option, value, values, flag) {
|
|
105
|
+
for (const item of option) {
|
|
106
|
+
if (value.includes(item.value)) {
|
|
107
|
+
values.push(item.value)
|
|
108
|
+
flag = true
|
|
109
|
+
}
|
|
115
110
|
if (item.children && item.children.length) {
|
|
116
|
-
this.getValues(item.children, value, values)
|
|
111
|
+
this.getValues(item.children, value, values, flag)
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
114
|
},
|
|
@@ -205,7 +200,7 @@ export default {
|
|
|
205
200
|
for (const item of data) {
|
|
206
201
|
values.push(item.node.key)
|
|
207
202
|
if (item.children && item.children.length) {
|
|
208
|
-
this.
|
|
203
|
+
this.getNodeValues(item.children, value, values)
|
|
209
204
|
}
|
|
210
205
|
}
|
|
211
206
|
},
|
|
@@ -84,13 +84,15 @@ routerResource.example = {
|
|
|
84
84
|
path: 'default',
|
|
85
85
|
name: '示例页面',
|
|
86
86
|
// component: () => import('@vue2-client/base-client/components/common/XAddNativeForm/demo.vue'),
|
|
87
|
-
component: () => import('@vue2-client/base-client/components/common/XFormGroup/demo.vue'),
|
|
87
|
+
// component: () => import('@vue2-client/base-client/components/common/XFormGroup/demo.vue'),
|
|
88
|
+
// component: () => import('@vue2-client/base-client/components/common/XFormGroup/demo.vue'),
|
|
88
89
|
// component: () => import('@vue2-client/base-client/components/common/XReport/XReportDemo.vue'),
|
|
89
90
|
// component: () => import('@vue2-client/base-client/components/common/XFormTable/demo.vue'),
|
|
90
91
|
// component: () => import('@vue2-client/base-client/components/common/XTab/XTabDemo.vue'),
|
|
91
92
|
// component: () => import('@vue2-client/base-client/components/common/XReportGrid/XReportDemo.vue'),
|
|
92
93
|
// component: () => import('@vue2-client/pages/WorkflowDetail/WorkFlowDemo.vue'),
|
|
93
|
-
component: () => import('@vue2-client/base-client/components/common/
|
|
94
|
+
component: () => import('@vue2-client/base-client/components/common/XConversation/XConversationDemo.vue'),
|
|
95
|
+
// component: () => import('@vue2-client/base-client/components/common/XButtons/XButtonDemo.vue'),
|
|
94
96
|
meta: {
|
|
95
97
|
// 菜单中不显示
|
|
96
98
|
invisible: true,
|