update: mobile 编辑源和语速音调页面样式调整

This commit is contained in:
zhuoqinglian
2026-03-11 17:10:18 +08:00
parent 6a14d694b0
commit 48087582c2
13 changed files with 487 additions and 819 deletions
@@ -1,467 +0,0 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { t } from '@/i18n'
interface Props {
visible?: boolean
providers?: Array<{
url: string
headers: Record<string, string>
}>
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
providers: () => [],
})
const emit = defineEmits<{
'update:visible': [value: boolean]
'confirm': [value: Array<{
url: string
headers: Record<string, string>
}>]
}>()
const localProviders = ref<Array<{
url: string
headers: Array<{
key: string
value: string
}>
}>>([])
function initLocalData() {
localProviders.value = props.providers.map((p) => {
const headers = p.headers || {}
return {
url: p.url || '',
headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
}
})
if (localProviders.value.length === 0) {
localProviders.value.push({ url: '', headers: [{ key: '', value: '' }] })
}
}
function addProvider(index: number) {
localProviders.value.splice(index, 0, {
url: '',
headers: [{ key: '', value: '' }],
})
}
function removeProvider(index: number) {
localProviders.value.splice(index, 1)
}
function addHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 0, { key: '', value: '' })
}
function removeHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 1)
}
function handleConfirm() {
const result = localProviders.value
.filter(p => p.url.trim() !== '')
.map((p) => {
const headersObj: Record<string, string> = {}
p.headers.forEach((h) => {
if (h.key.trim()) {
headersObj[h.key.trim()] = h.value
}
})
return {
url: p.url.trim(),
headers: headersObj,
}
})
emit('confirm', result)
emit('update:visible', false)
}
function handleClose() {
emit('update:visible', false)
}
watch(() => props.visible, (val) => {
if (val) {
initLocalData()
}
})
</script>
<template>
<view v-if="visible" class="context-provider-dialog-mask" @click="handleClose">
<view class="context-provider-dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">
{{ t('contextProviderDialog.title') }}
</text>
</view>
<view class="dialog-content">
<view v-if="localProviders.length === 0" class="empty-container">
<text class="empty-text">
{{ t('contextProviderDialog.noContextApi') }}
</text>
<wd-button type="primary" size="small" @click="addProvider(0)">
{{ t('contextProviderDialog.add') }}
</wd-button>
</view>
<view v-else class="providers-list">
<view v-for="(provider, pIndex) in localProviders" :key="pIndex" class="provider-item">
<view class="provider-card">
<view class="card-header">
<view class="card-controls">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#66b1ff] !text-[#fff]"
@click="addProvider(pIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#F56C6C] !text-[#fff]"
@click="removeProvider(pIndex)"
/>
</view>
</view>
<view class="input-row">
<text class="label-text">
{{ t('contextProviderDialog.apiUrl') }}
</text>
<wd-input
v-model="provider.url"
class="flex-1"
:placeholder="t('contextProviderDialog.apiUrlPlaceholder')"
/>
</view>
<view class="headers-section">
<view class="label-text" style="margin-top: 6px;">
{{ t('contextProviderDialog.requestHeaders') }}
</view>
<view class="headers-list">
<view
v-for="(header, hIndex) in provider.headers"
:key="hIndex"
class="header-row-vertical"
>
<view class="header-input-group">
<wd-input
v-model="header.key"
placeholder="key"
class="header-input-full"
/>
</view>
<view class="header-input-group">
<wd-input
v-model="header.value"
placeholder="value"
class="header-input-full"
/>
</view>
<view class="header-controls">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#ecf5ff] !text-[#b3d8ff]"
@click="addHeader(pIndex, hIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#fef0f0] !text-[#F56C6C]"
@click="removeHeader(pIndex, hIndex)"
/>
</view>
</view>
<view v-if="provider.headers.length === 0" class="header-row empty-header">
<text class="no-header-text">
{{ t('contextProviderDialog.noHeaders') }}
</text>
<wd-button type="text" size="small" @click="addHeader(pIndex, 0)">
{{ t('contextProviderDialog.addHeader') }}
</wd-button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="dialog-footer">
<wd-button class="cancel-btn" @click="handleClose">
{{ t('contextProviderDialog.cancel') }}
</wd-button>
<wd-button type="primary" class="confirm-btn" @click="handleConfirm">
{{ t('contextProviderDialog.confirm') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.context-provider-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.context-provider-dialog {
background: #fff;
// border-radius: 20rpx;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #232338;
}
.close-icon {
font-size: 40rpx;
color: #9d9ea3;
cursor: pointer;
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 20rpx;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
gap: 30rpx;
}
.empty-text {
font-size: 28rpx;
color: #9d9ea3;
}
.providers-list {
display: flex;
flex-direction: column;
}
.provider-item {
margin-bottom: 30rpx;
}
.provider-card {
flex: 1;
background: #fff;
border-radius: 16rpx;
border: 1px solid #eee;
border-left: 6rpx solid #336cff;
padding: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #232338;
}
.card-controls {
display: flex;
gap: 16rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 40rpx;
}
.headers-section {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.headers-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
background: #fcfcfc;
padding: 4rpx;
border-radius: 12rpx;
border: 1px dashed #dcdfe6;
}
.header-row-vertical {
display: flex;
flex-direction: column;
gap: 16rpx;
background: #fff;
// padding: 20rpx;
border-radius: 12rpx;
// border: 1px solid #e8e8e8;
}
.header-input-group {
display: flex;
align-items: center;
gap: 8rpx;
}
.header-key-label,
.header-value-label {
font-size: 24rpx;
font-weight: 500;
color: #606266;
}
.header-input-full {
width: 100%;
}
.header-controls {
display: flex;
gap: 12rpx;
// margin-top: 8rpx;
align-self: flex-start;
}
.block-controls {
display: flex;
flex-direction: column;
gap: 16rpx;
padding-top: 10rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 20rpx;
}
.label-text {
font-size: 28rpx;
font-weight: 600;
color: #606266;
width: 140rpx;
}
.headers-section {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.headers-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
background: #fcfcfc;
padding: 16rpx;
border-radius: 12rpx;
border: 1px dashed #dcdfe6;
}
.header-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.header-input {
width: 200rpx;
}
.separator {
color: #909399;
font-weight: bold;
font-size: 32rpx;
}
.row-controls {
display: flex;
gap: 12rpx;
margin-left: 8rpx;
flex-shrink: 0;
}
.empty-header {
justify-content: center;
padding: 20rpx;
color: #909399;
font-size: 26rpx;
}
.no-header-text {
margin-right: 16rpx;
}
.flex-1 {
flex: 1;
}
.dialog-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #eee;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
}
</style>
@@ -1,270 +0,0 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { t } from '@/i18n'
interface Props {
visible?: boolean
settings?: {
volume: number
speed: number
pitch: number
}
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
settings: () => ({
volume: 0,
speed: 0,
pitch: 0,
}),
})
const emit = defineEmits<{
'update:visible': [value: boolean]
'confirm': [value: {
volume: number
speed: number
pitch: number
}]
}>()
const localSettings = ref({
volume: 0,
speed: 0,
pitch: 0,
})
function initLocalData() {
localSettings.value = {
volume: props.settings.volume || 0,
speed: props.settings.speed || 0,
pitch: props.settings.pitch || 0,
}
}
function handleConfirm() {
emit('confirm', localSettings.value)
emit('update:visible', false)
}
function handleClose() {
emit('update:visible', false)
}
watch(() => props.visible, (val) => {
if (val) {
initLocalData()
}
})
</script>
<template>
<view v-if="visible" class="voice-settings-dialog-mask" @click="handleClose">
<view class="voice-settings-dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">
{{ t('agent.languageConfig') }}
</text>
</view>
<view class="dialog-content">
<!-- 音量调节 -->
<view class="setting-item">
<text class="setting-label">
{{ t('agent.ttsVolume') }}
</text>
<view class="slider-container">
<wd-slider
v-model="localSettings.volume"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
/>
<text class="slider-value">
{{ localSettings.volume }}
</text>
</view>
<text class="setting-desc">
{{ t('agent.volumeHint') }}
</text>
</view>
<!-- 语速调节 -->
<view class="setting-item">
<text class="setting-label">
{{ t('agent.ttsRate') }}
</text>
<view class="slider-container">
<wd-slider
v-model="localSettings.speed"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
/>
<text class="slider-value">
{{ localSettings.speed }}
</text>
</view>
<text class="setting-desc">
{{ t('agent.speedHint') }}
</text>
</view>
<!-- 音调调节 -->
<view class="setting-item">
<text class="setting-label">
{{ t('agent.ttsPitch') }}
</text>
<view class="slider-container">
<wd-slider
v-model="localSettings.pitch"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
/>
<text class="slider-value">
{{ localSettings.pitch }}
</text>
</view>
<text class="setting-desc">
{{ t('agent.pitchHint') }}
</text>
</view>
</view>
<view class="dialog-footer">
<wd-button class="cancel-btn" @click="handleClose">
{{ t('agent.cancel') }}
</wd-button>
<wd-button type="primary" class="confirm-btn" @click="handleConfirm">
{{ t('agent.save') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.voice-settings-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.voice-settings-dialog {
background: #fff;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #eee;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #232338;
}
.close-icon {
font-size: 40rpx;
color: #9d9ea3;
cursor: pointer;
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 40rpx;
display: flex;
flex-direction: column;
gap: 50rpx;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.setting-label {
font-size: 30rpx;
font-weight: 600;
color: #232338;
}
.slider-container {
display: flex;
align-items: center;
gap: 20rpx;
}
.voice-slider {
flex: 1;
}
.slider-value {
font-size: 28rpx;
color: #336cff;
font-weight: 500;
min-width: 80rpx;
text-align: right;
}
.setting-desc {
font-size: 24rpx;
color: #9d9ea3;
margin-top: 10rpx;
}
.dialog-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #eee;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
}
.confirm-btn {
background-color: #336cff !important;
}
/* 自定义滑块样式 */
:deep(.wd-slider) {
--wd-slider-bar-background: #e6ebff;
--wd-slider-bar-active-background: #336cff;
--wd-slider-thumb-border-color: #336cff;
--wd-slider-thumb-background: #336cff;
--wd-slider-thumb-size: 32rpx;
}
</style>