feat: 设置麦克风音量
This commit is contained in:
parent
96ba1adb73
commit
974b4ed736
@ -4,7 +4,12 @@ import cors from 'cors'
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { createAGVGrpcClient, createARMGrpcClient, __dirname } from './grpcClient.js';
|
import {
|
||||||
|
createAGVGrpcClient,
|
||||||
|
createARMGrpcClient,
|
||||||
|
createMicrophoneGrpcClient,
|
||||||
|
__dirname
|
||||||
|
} from './grpcClient.js';
|
||||||
import { attachAudioWebSocket } from './audioWebSocket.js';
|
import { attachAudioWebSocket } from './audioWebSocket.js';
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
@ -12,6 +17,72 @@ app.use(express.static(path.join(__dirname, '../dist')));
|
|||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
|
|
||||||
|
// 获取机器人麦克风音量(0-100)。
|
||||||
|
app.post('/api/audio/microphone/volume/get', (req, res) => {
|
||||||
|
const { ip, deviceId } = req.body
|
||||||
|
if (!ip || !deviceId) {
|
||||||
|
res.status(400).json({ code: 400, message: '机器人地址和麦克风设备 ID 不能为空' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const grpcClient = createMicrophoneGrpcClient(ip)
|
||||||
|
grpcClient.getVolume(
|
||||||
|
{ header: { device_id: deviceId } },
|
||||||
|
{ deadline: Date.now() + 5000 },
|
||||||
|
(error, feedback) => {
|
||||||
|
grpcClient.close()
|
||||||
|
if (error) {
|
||||||
|
console.error('[audio] get microphone volume failed:', error)
|
||||||
|
res.status(502).json({ code: 502, message: error.details || error.message || '获取机器人音量失败' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (feedback?.header?.success === false) {
|
||||||
|
res.status(502).json({
|
||||||
|
code: 502,
|
||||||
|
message: feedback.header.error_message || '机器人拒绝获取音量'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.json({ code: 200, data: { volume: feedback?.volume ?? 0 } })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置机器人麦克风音量(0-100)。
|
||||||
|
app.post('/api/audio/microphone/volume/set', (req, res) => {
|
||||||
|
const { ip, deviceId } = req.body
|
||||||
|
const volume = Number(req.body.volume)
|
||||||
|
if (!ip || !deviceId) {
|
||||||
|
res.status(400).json({ code: 400, message: '机器人地址和麦克风设备 ID 不能为空' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(volume) || volume < 0 || volume > 100) {
|
||||||
|
res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const grpcClient = createMicrophoneGrpcClient(ip)
|
||||||
|
grpcClient.setVolume({
|
||||||
|
header: { device_id: deviceId },
|
||||||
|
volume
|
||||||
|
}, { deadline: Date.now() + 5000 }, (error, feedback) => {
|
||||||
|
grpcClient.close()
|
||||||
|
if (error) {
|
||||||
|
console.error('[audio] set microphone volume failed:', error)
|
||||||
|
res.status(502).json({ code: 502, message: error.details || error.message || '设置机器人音量失败' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (feedback?.header?.success === false) {
|
||||||
|
res.status(502).json({
|
||||||
|
code: 502,
|
||||||
|
message: feedback.header.error_message || '机器人拒绝设置音量'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.json({ code: 200, data: { volume } })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 获取agv运行时状态
|
// 获取agv运行时状态
|
||||||
app.post('/api/agv/getRuntimeState', async (req, res) => {
|
app.post('/api/agv/getRuntimeState', async (req, res) => {
|
||||||
const request = {
|
const request = {
|
||||||
|
|||||||
9
src/api/audio.js
Normal file
9
src/api/audio.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getMicrophoneVolume(params) {
|
||||||
|
return request('/api/audio/microphone/volume/get', params, 'POST')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMicrophoneVolume(params) {
|
||||||
|
return request('/api/audio/microphone/volume/set', params, 'POST')
|
||||||
|
}
|
||||||
@ -94,3 +94,75 @@ $font-family-body: 'Noto Sans SC', 'PingFang SC', sans-serif;
|
|||||||
$font-family-monospace: 'JetBrains Mono', monospace;
|
$font-family-monospace: 'JetBrains Mono', monospace;
|
||||||
|
|
||||||
$shadow-input: 0 2px 12px $color-shadow-soft;
|
$shadow-input: 0 2px 12px $color-shadow-soft;
|
||||||
|
|
||||||
|
// Real-time voice conversation
|
||||||
|
$color-voice-white: #fff;
|
||||||
|
$color-voice-mask-black: #000;
|
||||||
|
$color-voice-transparent: transparent;
|
||||||
|
$color-voice-cyan: #46c8ff;
|
||||||
|
$color-voice-purple: #936cff;
|
||||||
|
$color-voice-background-start: #071a3e;
|
||||||
|
$color-voice-background-middle: #08152f;
|
||||||
|
$color-voice-background-end: #050c21;
|
||||||
|
$color-voice-background-blue-glow: rgb(44 90 198 / 34%);
|
||||||
|
$color-voice-background-purple-glow: rgb(81 49 172 / 18%);
|
||||||
|
$color-voice-grid-line: rgb(32 143 255 / 7%);
|
||||||
|
$color-voice-grid-diagonal: rgb(89 138 255 / 3%);
|
||||||
|
$color-voice-outer-ring: rgb(70 200 255 / 7%);
|
||||||
|
$color-voice-outer-ring-shadow: rgb(36 120 255 / 8%);
|
||||||
|
$color-voice-title-shadow: rgb(74 168 255 / 30%);
|
||||||
|
$color-voice-heading-shadow: rgb(79 159 255 / 25%);
|
||||||
|
$color-voice-subtitle: #86c6f4;
|
||||||
|
$color-voice-orbit-border: rgb(117 210 255 / 45%);
|
||||||
|
$color-voice-orbit-blue-fill: rgb(41 146 255 / 20%);
|
||||||
|
$color-voice-orbit-purple-fill: rgb(120 73 255 / 16%);
|
||||||
|
$color-voice-orbit-blue-shadow: rgb(35 158 255 / 35%);
|
||||||
|
$color-voice-orbit-purple-shadow: rgb(117 83 255 / 24%);
|
||||||
|
$color-voice-orbit-inner-border: rgb(160 124 255 / 55%);
|
||||||
|
$color-voice-orbit-inner-shadow: rgb(55 186 255 / 22%);
|
||||||
|
$color-voice-orbit-conic-blue: rgb(58 190 255 / 35%);
|
||||||
|
$color-voice-orbit-conic-purple: rgb(146 94 255 / 42%);
|
||||||
|
$color-voice-microphone-blue: #237bfa;
|
||||||
|
$color-voice-microphone-purple: #794be1;
|
||||||
|
$color-voice-microphone-shadow: rgb(34 111 255 / 48%);
|
||||||
|
$color-voice-highlight-strong: rgb(255 255 255 / 35%);
|
||||||
|
$color-voice-wave-shadow: rgb(72 190 255 / 46%);
|
||||||
|
$color-voice-status-border: rgb(255 255 255 / 9%);
|
||||||
|
$color-voice-status-text: #d5d9e4;
|
||||||
|
$color-voice-status-background: rgb(31 38 58 / 86%);
|
||||||
|
$color-voice-status-shadow: rgb(0 0 0 / 24%);
|
||||||
|
$color-voice-status-idle: #8791a7;
|
||||||
|
$color-voice-status-connecting: #ffd166;
|
||||||
|
$color-voice-status-connected: #4ef5ae;
|
||||||
|
$color-voice-status-error: #ff6482;
|
||||||
|
$color-voice-control-text: #badeff;
|
||||||
|
$color-voice-slider-track: rgb(106 170 226 / 24%);
|
||||||
|
$color-voice-slider-thumb: #dff7ff;
|
||||||
|
$color-voice-slider-glow: rgb(70 200 255 / 65%);
|
||||||
|
$color-voice-panel-border: rgb(112 192 255 / 24%);
|
||||||
|
$color-voice-call-border: rgb(125 207 255 / 52%);
|
||||||
|
$color-voice-call-start: #176ee9;
|
||||||
|
$color-voice-call-middle: #448eff;
|
||||||
|
$color-voice-call-end: #7657e9;
|
||||||
|
$color-voice-call-shadow: rgb(24 105 235 / 35%);
|
||||||
|
$color-voice-highlight-soft: rgb(255 255 255 / 18%);
|
||||||
|
$color-voice-call-hover-border: #86d5ff;
|
||||||
|
$color-voice-call-hover-start: #2280ff;
|
||||||
|
$color-voice-call-hover-middle: #54a0ff;
|
||||||
|
$color-voice-call-hover-end: #896afa;
|
||||||
|
$color-voice-danger-border: rgb(255 126 157 / 55%);
|
||||||
|
$color-voice-danger-start: #cf3e67;
|
||||||
|
$color-voice-danger-middle: #e4597e;
|
||||||
|
$color-voice-danger-end: #8554d9;
|
||||||
|
$color-voice-muted-background: rgb(16 45 85 / 72%);
|
||||||
|
$color-voice-muted-hover-background: rgb(23 76 128 / 82%);
|
||||||
|
$color-voice-card-start: rgb(14 49 91 / 67%);
|
||||||
|
$color-voice-card-end: rgb(20 30 74 / 55%);
|
||||||
|
$color-voice-card-shadow: rgb(0 4 24 / 28%);
|
||||||
|
$color-voice-card-highlight: rgb(255 255 255 / 5%);
|
||||||
|
$color-voice-card-primary-text: #edf7ff;
|
||||||
|
$color-voice-card-secondary-text: #8eafd0;
|
||||||
|
$color-voice-tag-divider: rgb(116 186 255 / 13%);
|
||||||
|
$color-voice-tag-border: rgb(87 183 255 / 35%);
|
||||||
|
$color-voice-tag-text: #a9d6f8;
|
||||||
|
$color-voice-tag-background: rgb(31 104 166 / 13%);
|
||||||
|
|||||||
@ -52,6 +52,20 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="robot-volume">
|
||||||
|
<span class="robot-volume__label">机器人麦克风音量</span>
|
||||||
|
<el-slider
|
||||||
|
v-model="robotVolume"
|
||||||
|
class="robot-volume__slider"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:disabled="isVolumeLoading"
|
||||||
|
:show-tooltip="true"
|
||||||
|
@change="updateRobotVolume"
|
||||||
|
/>
|
||||||
|
<span class="robot-volume__value">{{ robotVolume }}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="status-card">
|
<section class="status-card">
|
||||||
<div class="status-copy">
|
<div class="status-copy">
|
||||||
<p>当前状态:<strong>{{ statusMeta.label }}</strong></p>
|
<p>当前状态:<strong>{{ statusMeta.label }}</strong></p>
|
||||||
@ -70,9 +84,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { ElMessage, ElNotification } from 'element-plus'
|
import { ElMessage, ElNotification } from 'element-plus'
|
||||||
import { useRobotStore } from '@/stores/robot'
|
import { useRobotStore } from '@/stores/robot'
|
||||||
|
import { getMicrophoneVolume, setMicrophoneVolume } from '@/api/audio'
|
||||||
|
|
||||||
// 机器人实际音频流为 48kHz,因此浏览器上传也统一为 48kHz。
|
// 机器人实际音频流为 48kHz,因此浏览器上传也统一为 48kHz。
|
||||||
const TARGET_SAMPLE_RATE = 48000
|
const TARGET_SAMPLE_RATE = 48000
|
||||||
@ -86,6 +101,9 @@ const isStarting = ref(false)
|
|||||||
const isMuted = ref(false)
|
const isMuted = ref(false)
|
||||||
const connectionStatus = ref('idle')
|
const connectionStatus = ref('idle')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
const robotVolume = ref(50)
|
||||||
|
const isVolumeLoading = ref(false)
|
||||||
|
let confirmedRobotVolume = 50
|
||||||
|
|
||||||
// 本次通话占用的浏览器资源。
|
// 本次通话占用的浏览器资源。
|
||||||
const localStream = ref(null)
|
const localStream = ref(null)
|
||||||
@ -110,6 +128,49 @@ const statusMeta = computed(() => {
|
|||||||
return statusMap[connectionStatus.value] || statusMap.idle
|
return statusMap[connectionStatus.value] || statusMap.idle
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function volumeRequestParams() {
|
||||||
|
return {
|
||||||
|
ip: robotStore.ip,
|
||||||
|
deviceId: robotStore.micDeviceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRobotVolume({ silent = false } = {}) {
|
||||||
|
if (!robotStore.ip || !robotStore.micDeviceId) return
|
||||||
|
isVolumeLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await getMicrophoneVolume(volumeRequestParams())
|
||||||
|
if (response.code !== 200) throw new Error(response.message || '获取机器人音量失败')
|
||||||
|
const volume = Math.max(0, Math.min(100, Number(response.data?.volume) || 0))
|
||||||
|
robotVolume.value = volume
|
||||||
|
confirmedRobotVolume = volume
|
||||||
|
} catch (error) {
|
||||||
|
if (!silent) ElMessage.error(error.message || '获取机器人音量失败')
|
||||||
|
} finally {
|
||||||
|
isVolumeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRobotVolume(volume) {
|
||||||
|
const previousVolume = confirmedRobotVolume
|
||||||
|
isVolumeLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await setMicrophoneVolume({
|
||||||
|
...volumeRequestParams(),
|
||||||
|
volume: Math.round(volume)
|
||||||
|
})
|
||||||
|
if (response.code !== 200) throw new Error(response.message || '设置机器人音量失败')
|
||||||
|
robotVolume.value = response.data.volume
|
||||||
|
confirmedRobotVolume = response.data.volume
|
||||||
|
ElMessage.success(`机器人麦克风音量已设置为 ${response.data.volume}%`)
|
||||||
|
} catch (error) {
|
||||||
|
robotVolume.value = previousVolume
|
||||||
|
ElMessage.error(error.message || '设置机器人音量失败')
|
||||||
|
} finally {
|
||||||
|
isVolumeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一显示通话异常。
|
* 统一显示通话异常。
|
||||||
* @param {string} message 用户可读的错误说明
|
* @param {string} message 用户可读的错误说明
|
||||||
@ -411,32 +472,28 @@ function endCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 离开页面时释放麦克风、AudioWorklet 和 WebSocket。
|
// 离开页面时释放麦克风、AudioWorklet 和 WebSocket。
|
||||||
|
onMounted(() => loadRobotVolume())
|
||||||
onBeforeUnmount(cleanupResources)
|
onBeforeUnmount(cleanupResources)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$cyan: #46c8ff;
|
|
||||||
$blue: #2478ff;
|
|
||||||
$purple: #936cff;
|
|
||||||
$panel-border: rgb(112 192 255 / 24%);
|
|
||||||
|
|
||||||
.voice-conversation {
|
.voice-conversation {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 500px;
|
||||||
max-height: 400px;
|
max-height: 500px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 30px 28px 16px;
|
padding: 30px 28px 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: #fff;
|
color: $color-voice-white;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 50% 38%, rgb(44 90 198 / 34%) 0%, transparent 33%),
|
radial-gradient(circle at 50% 38%, $color-voice-background-blue-glow 0%, $color-voice-transparent 33%),
|
||||||
radial-gradient(circle at 75% 20%, rgb(81 49 172 / 18%) 0%, transparent 30%),
|
radial-gradient(circle at 75% 20%, $color-voice-background-purple-glow 0%, $color-voice-transparent 30%),
|
||||||
linear-gradient(145deg, #071a3e 0%, #08152f 45%, #050c21 100%);
|
linear-gradient(145deg, $color-voice-background-start 0%, $color-voice-background-middle 45%, $color-voice-background-end 100%);
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
@ -447,24 +504,24 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(90deg, rgb(32 143 255 / 7%) 1px, transparent 1px),
|
background: linear-gradient(90deg, $color-voice-grid-line 1px, $color-voice-transparent 1px),
|
||||||
linear-gradient(rgb(32 143 255 / 7%) 1px, transparent 1px);
|
linear-gradient($color-voice-grid-line 1px, $color-voice-transparent 1px);
|
||||||
background-size: 42px 42px;
|
background-size: 42px 42px;
|
||||||
mask-image: radial-gradient(circle at center, #000 12%, transparent 82%);
|
mask-image: radial-gradient(circle at center, $color-voice-mask-black 12%, $color-voice-transparent 82%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
height: 360px;
|
height: 360px;
|
||||||
border: 1px solid rgb(70 200 255 / 7%);
|
border: 1px solid $color-voice-outer-ring;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 100px rgb(36 120 255 / 8%);
|
box-shadow: 0 0 100px $color-voice-outer-ring-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-grid {
|
.tech-grid {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: repeating-linear-gradient(135deg, transparent 0 118px, rgb(89 138 255 / 3%) 119px 120px);
|
background: repeating-linear-gradient(135deg, $color-voice-transparent 0 118px, $color-voice-grid-diagonal 119px 120px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,7 +533,7 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-shadow: 0 0 18px rgb(74 168 255 / 30%);
|
text-shadow: 0 0 18px $color-voice-title-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voice-content {
|
.voice-content {
|
||||||
@ -496,12 +553,12 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
font-size: clamp(23px, 3vw, 30px);
|
font-size: clamp(23px, 3vw, 30px);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
letter-spacing: 5px;
|
letter-spacing: 5px;
|
||||||
text-shadow: 0 0 25px rgb(79 159 255 / 25%);
|
text-shadow: 0 0 25px $color-voice-heading-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
color: #86c6f4;
|
color: $color-voice-subtitle;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
}
|
}
|
||||||
@ -528,17 +585,17 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border: 1px solid rgb(117 210 255 / 45%);
|
border: 1px solid $color-voice-orbit-border;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(145deg, rgb(41 146 255 / 20%), rgb(120 73 255 / 16%));
|
background: linear-gradient(145deg, $color-voice-orbit-blue-fill, $color-voice-orbit-purple-fill);
|
||||||
box-shadow: 0 0 26px rgb(35 158 255 / 35%), inset 0 0 28px rgb(117 83 255 / 24%);
|
box-shadow: 0 0 26px $color-voice-orbit-blue-shadow, inset 0 0 28px $color-voice-orbit-purple-shadow;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 7px;
|
inset: 7px;
|
||||||
border: 1px solid rgb(160 124 255 / 55%);
|
border: 1px solid $color-voice-orbit-inner-border;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
box-shadow: inset 0 0 16px rgb(55 186 255 / 22%);
|
box-shadow: inset 0 0 16px $color-voice-orbit-inner-shadow;
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,7 +603,7 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -13px;
|
inset: -13px;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background: conic-gradient(from 90deg, transparent, rgb(58 190 255 / 35%), transparent, rgb(146 94 255 / 42%), transparent);
|
background: conic-gradient(from 90deg, $color-voice-transparent, $color-voice-orbit-conic-blue, $color-voice-transparent, $color-voice-orbit-conic-purple, $color-voice-transparent);
|
||||||
filter: blur(12px);
|
filter: blur(12px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -558,15 +615,15 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
width: 62px;
|
width: 62px;
|
||||||
height: 62px;
|
height: 62px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(145deg, #237bfa, #794be1);
|
background: linear-gradient(145deg, $color-voice-microphone-blue, $color-voice-microphone-purple);
|
||||||
box-shadow: 0 8px 30px rgb(34 111 255 / 48%), inset 0 1px 1px rgb(255 255 255 / 35%);
|
box-shadow: 0 8px 30px $color-voice-microphone-shadow, inset 0 1px 1px $color-voice-highlight-strong;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 31px;
|
width: 31px;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
fill: #fff;
|
fill: $color-voice-white;
|
||||||
stroke: #fff;
|
stroke: $color-voice-white;
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
@ -587,8 +644,8 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
width: 4px;
|
width: 4px;
|
||||||
height: var(--wave-height, 30px);
|
height: var(--wave-height, 30px);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(to bottom, $cyan, $purple);
|
background: linear-gradient(to bottom, $color-voice-cyan, $color-voice-purple);
|
||||||
box-shadow: 0 0 10px rgb(72 190 255 / 46%);
|
box-shadow: 0 0 10px $color-voice-wave-shadow;
|
||||||
opacity: 0.74;
|
opacity: 0.74;
|
||||||
animation: wave-pulse 1.15s ease-in-out infinite alternate;
|
animation: wave-pulse 1.15s ease-in-out infinite alternate;
|
||||||
animation-delay: calc(var(--index) * -0.09s);
|
animation-delay: calc(var(--index) * -0.09s);
|
||||||
@ -610,22 +667,22 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
padding: 4px 13px;
|
padding: 4px 13px;
|
||||||
border: 1px solid rgb(255 255 255 / 9%);
|
border: 1px solid $color-voice-status-border;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
color: #d5d9e4;
|
color: $color-voice-status-text;
|
||||||
background: rgb(31 38 58 / 86%);
|
background: $color-voice-status-background;
|
||||||
box-shadow: 0 7px 18px rgb(0 0 0 / 24%);
|
box-shadow: 0 7px 18px $color-voice-status-shadow;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
||||||
.status-light {
|
.status-light {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #8791a7;
|
background: $color-voice-status-idle;
|
||||||
|
|
||||||
&.connecting { background: #ffd166; box-shadow: 0 0 8px #ffd166; }
|
&.connecting { background: $color-voice-status-connecting; box-shadow: 0 0 8px $color-voice-status-connecting; }
|
||||||
&.connected { background: #4ef5ae; box-shadow: 0 0 8px #4ef5ae; }
|
&.connected { background: $color-voice-status-connected; box-shadow: 0 0 8px $color-voice-status-connected; }
|
||||||
&.error { background: #ff6482; box-shadow: 0 0 8px #ff6482; }
|
&.error { background: $color-voice-status-error; box-shadow: 0 0 8px $color-voice-status-error; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,50 +697,74 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
:deep(.el-button + .el-button) { margin-left: 0; }
|
:deep(.el-button + .el-button) { margin-left: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.robot-volume {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(120px, 210px) 42px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: min(100%, 430px);
|
||||||
|
margin-top: 10px;
|
||||||
|
color: $color-voice-control-text;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
&__label { white-space: nowrap; }
|
||||||
|
&__value { color: $color-voice-white; text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
|
:deep(.el-slider__runway) { background-color: $color-voice-slider-track; }
|
||||||
|
:deep(.el-slider__bar) { background: linear-gradient(90deg, $color-voice-cyan, $color-voice-purple); }
|
||||||
|
:deep(.el-slider__button) {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-color: $color-voice-cyan;
|
||||||
|
background: $color-voice-slider-thumb;
|
||||||
|
box-shadow: 0 0 10px $color-voice-slider-glow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.call-button) {
|
:deep(.call-button) {
|
||||||
width: 178px;
|
width: 178px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border: 1px solid rgb(125 207 255 / 52%);
|
border: 1px solid $color-voice-call-border;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: #fff;
|
color: $color-voice-white;
|
||||||
background: linear-gradient(100deg, #176ee9, #448eff 56%, #7657e9);
|
background: linear-gradient(100deg, $color-voice-call-start, $color-voice-call-middle 56%, $color-voice-call-end);
|
||||||
box-shadow: 0 10px 28px rgb(24 105 235 / 35%), inset 0 1px rgb(255 255 255 / 18%);
|
box-shadow: 0 10px 28px $color-voice-call-shadow, inset 0 1px $color-voice-highlight-soft;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: $color-voice-white;
|
||||||
border-color: #86d5ff;
|
border-color: $color-voice-call-hover-border;
|
||||||
background: linear-gradient(100deg, #2280ff, #54a0ff 56%, #896afa);
|
background: linear-gradient(100deg, $color-voice-call-hover-start, $color-voice-call-hover-middle 56%, $color-voice-call-hover-end);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.call-button--danger {
|
&.call-button--danger {
|
||||||
border-color: rgb(255 126 157 / 55%);
|
border-color: $color-voice-danger-border;
|
||||||
background: linear-gradient(100deg, #cf3e67, #e4597e, #8554d9);
|
background: linear-gradient(100deg, $color-voice-danger-start, $color-voice-danger-middle, $color-voice-danger-end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.mute-button) {
|
:deep(.mute-button) {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-color: $panel-border;
|
border-color: $color-voice-panel-border;
|
||||||
color: #badeff;
|
color: $color-voice-control-text;
|
||||||
background: rgb(16 45 85 / 72%);
|
background: $color-voice-muted-background;
|
||||||
|
|
||||||
&:hover { color: #fff; border-color: $cyan; background: rgb(23 76 128 / 82%); }
|
&:hover { color: $color-voice-white; border-color: $color-voice-cyan; background: $color-voice-muted-hover-background; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
width: min(100%, 550px);
|
width: min(100%, 550px);
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 8px 18px 8px;
|
padding: 8px 18px 8px;
|
||||||
border: 1px solid $panel-border;
|
border: 1px solid $color-voice-panel-border;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(135deg, rgb(14 49 91 / 67%), rgb(20 30 74 / 55%));
|
background: linear-gradient(135deg, $color-voice-card-start, $color-voice-card-end);
|
||||||
box-shadow: 0 16px 40px rgb(0 4 24 / 28%), inset 0 1px rgb(255 255 255 / 5%);
|
box-shadow: 0 16px 40px $color-voice-card-shadow, inset 0 1px $color-voice-card-highlight;
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
|
|
||||||
.status-copy {
|
.status-copy {
|
||||||
@ -691,16 +772,16 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #edf7ff;
|
color: $color-voice-card-primary-text;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
strong { color: #fff; font-weight: 600; }
|
strong { color: $color-voice-white; font-weight: 600; }
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: #8eafd0;
|
color: $color-voice-card-secondary-text;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -714,14 +795,14 @@ $panel-border: rgb(112 192 255 / 24%);
|
|||||||
gap: 9px;
|
gap: 9px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
border-top: 1px solid rgb(116 186 255 / 13%);
|
border-top: 1px solid $color-voice-tag-divider;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border: 1px solid rgb(87 183 255 / 35%);
|
border: 1px solid $color-voice-tag-border;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
color: #a9d6f8;
|
color: $color-voice-tag-text;
|
||||||
background: rgb(31 104 166 / 13%);
|
background: $color-voice-tag-background;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user