Unverified Commit e6868185 authored by LiangLiu's avatar LiangLiu Committed by GitHub
Browse files

Update frontend (#436)



修复了一些问题、流程优化以及页面设计

---------
Co-authored-by: default avatarXHPlus <xhplus@163.com>
Co-authored-by: default avatarqinxinyi <qxy118045534@163.com>
parent 837feba7
This diff is collapsed.
...@@ -15,7 +15,7 @@ class VolcEngineTTSClient: ...@@ -15,7 +15,7 @@ class VolcEngineTTSClient:
VolcEngine TTS客户端 VolcEngine TTS客户端
参数范围说明: 参数范围说明:
- speed_rate: -50~100 (100代表2倍速, -50代表0.5倍速, 0为正常语速) - speech_rate: -50~100 (100代表2倍速, -50代表0.5倍速, 0为正常语速)
- loudness_rate: -50~100 (100代表2倍音量, -50代表0.5倍音量, 0为正常音量) - loudness_rate: -50~100 (100代表2倍音量, -50代表0.5倍音量, 0为正常音量)
- emotion_scale: 1-5 - emotion_scale: 1-5
""" """
...@@ -86,12 +86,13 @@ class VolcEngineTTSClient: ...@@ -86,12 +86,13 @@ class VolcEngineTTSClient:
async def tts_request( async def tts_request(
self, self,
text, text,
voice_type, voice_type="zh_female_vv_uranus_bigtts",
context_texts="", context_texts="",
emotion="", emotion="",
emotion_scale=4, emotion_scale=4,
speed_rate=0, speech_rate=0,
loudness_rate=0, loudness_rate=0,
pitch=0,
output="tts_output.mp3", output="tts_output.mp3",
resource_id="seed-tts-2.0", resource_id="seed-tts-2.0",
app_key="aGjiRDfUWi", app_key="aGjiRDfUWi",
...@@ -108,8 +109,9 @@ class VolcEngineTTSClient: ...@@ -108,8 +109,9 @@ class VolcEngineTTSClient:
voice_type: 声音类型 voice_type: 声音类型
emotion: 情感类型 emotion: 情感类型
emotion_scale: 情感强度 (1-5) emotion_scale: 情感强度 (1-5)
speed_rate: 语速调节 (-50~100, 100代表2倍速, -50代表0.5倍速, 0为正常语速) speech_rate: 语速调节 (-50~100, 100代表2倍速, -50代表0.5倍速, 0为正常语速)
loudness_rate: 音量调节 (-50~100, 100代表2倍音量, -50代表0.5倍音量, 0为正常音量) loudness_rate: 音量调节 (-50~100, 100代表2倍音量, -50代表0.5倍音量, 0为正常音量)
pitch: 音调调节 (-12~12, 12代表高音调, -12代表低音调, 0为正常音调)
output: 输出文件路径 output: 输出文件路径
resource_id: 资源ID resource_id: 资源ID
app_key: 应用密钥 app_key: 应用密钥
...@@ -119,9 +121,9 @@ class VolcEngineTTSClient: ...@@ -119,9 +121,9 @@ class VolcEngineTTSClient:
enable_timestamp: 是否启用时间戳 enable_timestamp: 是否启用时间戳
""" """
# 验证参数范围 # 验证参数范围
if not (-50 <= speed_rate <= 100): if not (-50 <= speech_rate <= 100):
logger.warning(f"speed_rate {speed_rate} 超出有效范围 [-50, 100],将使用默认值 0") logger.warning(f"speech_rate {speech_rate} 超出有效范围 [-50, 100],将使用默认值 0")
speed_rate = 0 speech_rate = 0
if not (-50 <= loudness_rate <= 100): if not (-50 <= loudness_rate <= 100):
logger.warning(f"loudness_rate {loudness_rate} 超出有效范围 [-50, 100],将使用默认值 0") logger.warning(f"loudness_rate {loudness_rate} 超出有效范围 [-50, 100],将使用默认值 0")
...@@ -131,6 +133,10 @@ class VolcEngineTTSClient: ...@@ -131,6 +133,10 @@ class VolcEngineTTSClient:
logger.warning(f"emotion_scale {emotion_scale} 超出有效范围 [1, 5],将使用默认值 3") logger.warning(f"emotion_scale {emotion_scale} 超出有效范围 [1, 5],将使用默认值 3")
emotion_scale = 3 emotion_scale = 3
if not (-12 <= pitch <= 12):
logger.warning(f"pitch {pitch} 超出有效范围 [-12, 12],将使用默认值 0")
pitch = 0
headers = { headers = {
"X-Api-App-Id": self.appid, "X-Api-App-Id": self.appid,
"X-Api-Access-Key": self.access_token, "X-Api-Access-Key": self.access_token,
...@@ -139,7 +145,9 @@ class VolcEngineTTSClient: ...@@ -139,7 +145,9 @@ class VolcEngineTTSClient:
"Content-Type": "application/json", "Content-Type": "application/json",
"Connection": "keep-alive", "Connection": "keep-alive",
} }
additions = json.dumps({"explicit_language": "zh", "disable_markdown_filter": True, "enable_timestamp": True, "context_texts": [context_texts] if context_texts else None}) additions = json.dumps(
{"explicit_language": "zh", "disable_markdown_filter": True, "enable_timestamp": True, "context_texts": [context_texts] if context_texts else None, "post_process": {"pitch": pitch}}
)
payload = { payload = {
"user": {"uid": uid}, "user": {"uid": uid},
"req_params": { "req_params": {
...@@ -151,7 +159,7 @@ class VolcEngineTTSClient: ...@@ -151,7 +159,7 @@ class VolcEngineTTSClient:
"enable_timestamp": enable_timestamp, "enable_timestamp": enable_timestamp,
"emotion": emotion, "emotion": emotion,
"emotion_scale": emotion_scale, "emotion_scale": emotion_scale,
"speed_rate": speed_rate, "speech_rate": speech_rate,
"loudness_rate": loudness_rate, "loudness_rate": loudness_rate,
}, },
"additions": additions, "additions": additions,
...@@ -170,24 +178,26 @@ async def test(args): ...@@ -170,24 +178,26 @@ async def test(args):
TTS测试函数 TTS测试函数
Args: Args:
args: list, e.g. [text, voice_type, emotion, emotion_scale, speed_rate, loudness_rate, output, resource_id, app_key, uid, format, sample_rate, enable_timestamp] args: list, e.g. [text, voice_type, emotion, emotion_scale, speech_rate, loudness_rate, output, resource_id, app_key, uid, format, sample_rate, enable_timestamp]
Provide as many as needed, from left to right. Provide as many as needed, from left to right.
Parameter ranges: Parameter ranges:
- speed_rate: -50~100 (100代表2倍速, -50代表0.5倍速, 0为正常语速) - speech_rate: -50~100 (100代表2倍速, -50代表0.5倍速, 0为正常语速)
- loudness_rate: -50~100 (100代表2倍音量, -50代表0.5倍音量, 0为正常音量) - loudness_rate: -50~100 (100代表2倍音量, -50代表0.5倍音量, 0为正常音量)
- emotion_scale: 1-5 - emotion_scale: 1-5
- pitch: -12~12 (12代表高音调, -12代表低音调, 0为正常音调)
""" """
client = VolcEngineTTSClient() client = VolcEngineTTSClient()
# 设置默认参数 # 设置默认参数
params = { params = {
"text": "", "text": "",
"voice_type": "", "voice_type": "zh_female_vv_uranus_bigtts",
"context_texts": "", "context_texts": "",
"emotion": "", "emotion": "",
"emotion_scale": 4, "emotion_scale": 4,
"speed_rate": 0, "speech_rate": 0,
"loudness_rate": 0, "loudness_rate": 0,
"pitch": 12,
"output": "tts_output.mp3", "output": "tts_output.mp3",
"resource_id": "seed-tts-2.0", "resource_id": "seed-tts-2.0",
"app_key": "aGjiRDfUWi", "app_key": "aGjiRDfUWi",
...@@ -214,8 +224,9 @@ async def test(args): ...@@ -214,8 +224,9 @@ async def test(args):
params["context_texts"], params["context_texts"],
params["emotion"], params["emotion"],
params["emotion_scale"], params["emotion_scale"],
params["speed_rate"], params["speech_rate"],
params["loudness_rate"], params["loudness_rate"],
params["pitch"],
params["output"], params["output"],
params["resource_id"], params["resource_id"],
params["app_key"], params["app_key"],
......
...@@ -40,7 +40,8 @@ class TTSRequest(BaseModel): ...@@ -40,7 +40,8 @@ class TTSRequest(BaseModel):
context_texts: str = "" context_texts: str = ""
emotion: str = "" emotion: str = ""
emotion_scale: int = 3 emotion_scale: int = 3
speed_rate: int = 0 speech_rate: int = 0
pitch: int = 0
loudness_rate: int = 0 loudness_rate: int = 0
resource_id: str = "seed-tts-1.0" resource_id: str = "seed-tts-1.0"
...@@ -963,8 +964,9 @@ async def api_v1_tts_generate(request: TTSRequest): ...@@ -963,8 +964,9 @@ async def api_v1_tts_generate(request: TTSRequest):
context_texts=request.context_texts, context_texts=request.context_texts,
emotion=request.emotion, emotion=request.emotion,
emotion_scale=request.emotion_scale, emotion_scale=request.emotion_scale,
speed_rate=request.speed_rate, speech_rate=request.speech_rate,
loudness_rate=request.loudness_rate, loudness_rate=request.loudness_rate,
pitch=request.pitch,
output=output_path, output=output_path,
resource_id=request.resource_id, resource_id=request.resource_id,
) )
......
...@@ -5,6 +5,14 @@ import { ref, onMounted, onUnmounted } from 'vue' ...@@ -5,6 +5,14 @@ import { ref, onMounted, onUnmounted } from 'vue'
const { t, locale } = useI18n() const { t, locale } = useI18n()
// 处理操作按钮点击
const handleActionClick = () => {
if (alert.value.action && alert.value.action.onClick) {
alert.value.action.onClick()
alert.value.show = false
}
}
// 响应式变量控制Alert位置 // 响应式变量控制Alert位置
const alertPosition = ref({ top: '1rem' }) const alertPosition = ref({ top: '1rem' })
...@@ -50,34 +58,214 @@ onUnmounted(() => { ...@@ -50,34 +58,214 @@ onUnmounted(() => {
}) })
</script> </script>
<template> <template>
<!-- 增强的提示消息系统 --> <!-- Apple 风格极简提示消息 -->
<div v-cloak> <div v-cloak>
<div v-if="alert.show" <transition
class="fixed left-1/2 transform -translate-x-1/2 z-[9999] max-w-xs w-full px-4 transition-all duration-300 ease-out" enter-active-class="alert-enter-active"
:style="alertPosition" leave-active-class="alert-leave-active"
:class="getAlertClass(alert.type)"> enter-from-class="alert-enter-from"
<div leave-to-class="alert-leave-to">
class="bg-gray-800/95 backdrop-blur-sm border border-gray-700/50 rounded-lg shadow-lg transition-all duration-300 ease-out"> <div v-if="alert.show"
<div class="flex items-center p-3"> class="fixed left-1/2 transform -translate-x-1/2 z-[9999] w-auto min-w-[280px] sm:min-w-[320px] max-w-[calc(100vw-3rem)] sm:max-w-xl px-6 sm:px-6 transition-all duration-500 ease-out"
<div class="flex-shrink-0 mr-3"> :style="alertPosition">
<div class="w-6 h-6 rounded-full flex items-center justify-center" <div class="alert-container">
:class="getAlertIconBgClass(alert.type)"> <div class="alert-content">
<i :class="getAlertIcon(alert.type)" class="text-xs"></i> <!-- 图标 -->
<div class="alert-icon-wrapper">
<i :class="getAlertIcon(alert.type)" class="alert-icon"></i>
</div> </div>
</div> <!-- 消息文本和操作按钮(一行显示) -->
<div class="flex-1"> <div class="alert-message">
<p class="text-xs font-medium text-gray-200"> <span>{{ alert.message }}</span>
{{ alert.message }} <!-- 操作链接 - Apple 风格 -->
</p> <button v-if="alert.action" @click="handleActionClick" class="alert-action-link">
</div> {{ alert.action.label }}
<div class="flex-shrink-0 ml-2"> </button>
<button @click="alert.show = false" </div>
class="w-5 h-5 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-200 hover:bg-gray-700/50 transition-all duration-200"> <!-- 关闭按钮 -->
<i class="fas fa-times text-xs"></i> <button @click="alert.show = false" class="alert-close-btn" aria-label="Close">
<i class="fas fa-times"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </transition>
</div> </div>
</template> </template>
<style scoped>
/* Apple 风格 Alert 容器 */
.alert-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-radius: 16px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 0 0 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
/* 深色模式下的容器样式 */
:global(.dark) .alert-container {
background: rgba(30, 30, 30, 0.95);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -1px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.08);
}
/* Alert 内容 */
.alert-content {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
}
/* 图标包装器 */
.alert-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 图标样式 */
.alert-icon {
font-size: 18px;
color: #1d1d1f;
}
:global(.dark) .alert-icon {
color: #f5f5f7;
}
/* 消息文本 */
.alert-message {
flex: 1;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
color: #1d1d1f;
letter-spacing: -0.01em;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
:global(.dark) .alert-message {
color: #f5f5f7;
}
/* 关闭按钮 */
.alert-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
border: none;
background: transparent;
color: #86868b;
cursor: pointer;
flex-shrink: 0;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-close-btn:hover {
background: rgba(0, 0, 0, 0.05);
color: #1d1d1f;
transform: scale(1.05);
}
.alert-close-btn:active {
transform: scale(0.95);
}
:global(.dark) .alert-close-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: #f5f5f7;
}
.alert-close-btn i {
font-size: 12px;
}
/* 操作链接 - Apple 风格下划线文本 */
.alert-action-link {
display: inline;
padding: 0;
border: none;
background: transparent;
color: var(--brand-primary);
font-size: inherit;
font-weight: 600;
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
}
.alert-action-link:hover {
color: var(--brand-primary);
opacity: 0.8;
text-decoration-thickness: 2px;
}
.alert-action-link:active {
opacity: 0.6;
}
:global(.dark) .alert-action-link {
color: var(--brand-primary-light);
}
:global(.dark) .alert-action-link:hover {
color: var(--brand-primary-light);
}
/* 进入动画 */
.alert-enter-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.alert-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 1, 1);
}
.alert-enter-from {
opacity: 0;
transform: translate(-50%, -20px) scale(0.95);
}
.alert-leave-to {
opacity: 0;
transform: translate(-50%, -10px) scale(0.98);
}
/* 响应式设计 */
@media (max-width: 640px) {
.alert-content {
padding: 12px 16px;
gap: 8px;
}
.alert-message {
font-size: 13px;
}
.alert-icon {
font-size: 16px;
}
.alert-action-link {
font-size: 13px;
}
}
</style>
...@@ -5,66 +5,66 @@ import { useI18n } from 'vue-i18n' ...@@ -5,66 +5,66 @@ import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n() const { t, locale } = useI18n()
</script> </script>
<template> <template>
<!-- 自定义确认对话框 --> <!-- 自定义确认对话框 - Apple 极简风格 -->
<div v-cloak> <div v-cloak>
<div v-if="confirmDialog.show" class="fixed inset-0 z-50 flex items-center justify-center p-4"> <div v-if="confirmDialog.show" class="fixed inset-0 z-[70] flex items-center justify-center p-4">
<!-- 背景遮罩 --> <!-- 背景遮罩 - Apple 风格 -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="confirmDialog.show = false"> <div class="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm" @click="confirmDialog.cancel()">
</div> </div>
<!-- 对话框内容 --> <!-- 对话框内容 - Apple 风格 -->
<div class="relative bg-dark-light border border-laser-purple/30 rounded-xl shadow-2xl max-w-md w-full mx-4 transform transition-all duration-300 ease-out" <div class="relative bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] max-w-md w-full mx-4 overflow-hidden transform transition-all duration-300 ease-out"
:class="confirmDialog.show ? 'scale-100 opacity-100' : 'scale-95 opacity-0'"> :class="confirmDialog.show ? 'scale-100 opacity-100' : 'scale-95 opacity-0'">
<!-- 头部 --> <!-- 头部 - Apple 风格 -->
<div class="flex items-center justify-between p-6 border-b border-gray-700"> <div class="flex items-center justify-between px-6 py-5 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 bg-red-500/20 rounded-full flex items-center justify-center"> <div class="w-9 h-9 bg-red-500/10 dark:bg-red-400/10 rounded-full flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-red-400 text-lg"></i> <i class="fas fa-exclamation-triangle text-red-500 dark:text-red-400 text-base"></i>
</div> </div>
<h3 class="text-lg font-semibold text-white">{{ confirmDialog.title }}</h3> <h3 class="text-lg font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ confirmDialog.title }}</h3>
</div>
<button @click="confirmDialog.cancel()"
class="w-8 h-8 flex items-center justify-center bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] rounded-full transition-all duration-200 hover:scale-110 active:scale-100">
<i class="fas fa-times text-xs"></i>
</button>
</div> </div>
<button @click="confirmDialog.show = false"
class="text-gray-400 hover:text-gray-300 transition-colors">
<i class="fas fa-times text-lg"></i>
</button>
</div>
<!-- 内容 --> <!-- 内容 - Apple 风格 -->
<div class="p-6"> <div class="px-6 py-5">
<p class="text-gray-300 leading-relaxed mb-6">{{ confirmDialog.message }}</p> <p class="text-[15px] text-[#1d1d1f] dark:text-[#f5f5f7] leading-relaxed tracking-tight">{{ confirmDialog.message }}</p>
<!-- 警告信息 --> <!-- 警告信息 - Apple 风格 -->
<div v-if="confirmDialog.warning" <div v-if="confirmDialog.warning"
class="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-6"> class="mt-4 bg-red-500/5 dark:bg-red-400/5 border border-red-500/20 dark:border-red-400/20 rounded-2xl p-4">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<i class="fas fa-info-circle text-red-400 mt-0.5"></i> <i class="fas fa-info-circle text-red-500 dark:text-red-400 text-sm mt-0.5"></i>
<div class="text-sm text-red-300"> <div class="flex-1">
<p class="font-medium mb-2">{{ confirmDialog.warning.title }}</p> <p class="text-sm font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-2 tracking-tight">{{ confirmDialog.warning.title }}</p>
<ul class="space-y-1 text-xs"> <ul class="space-y-1.5 text-xs text-[#86868b] dark:text-[#98989d]">
<li v-for="item in confirmDialog.warning.items" :key="item" <li v-for="item in confirmDialog.warning.items" :key="item"
class="flex items-center gap-2"> class="flex items-start gap-2 tracking-tight">
<i class="fas fa-minus text-red-400 text-xs"></i> <i class="fas fa-circle text-[6px] text-red-500 dark:text-red-400 mt-1"></i>
{{ item }} <span class="flex-1">{{ item }}</span>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 底部按钮 --> <!-- 底部按钮 - Apple 风格 -->
<div class="flex gap-3 p-6 pt-0"> <div class="flex gap-3 px-6 pb-6">
<button @click="confirmDialog.cancel()" <button @click="confirmDialog.cancel()"
class="flex-1 px-4 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-all duration-200 font-medium"> class="flex-1 px-5 py-3 bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 text-[#1d1d1f] dark:text-[#f5f5f7] rounded-full transition-all duration-200 font-medium text-[15px] tracking-tight hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98]">
{{ t('cancel') }} {{ t('cancel') }}
</button> </button>
<button @click="confirmDialog.confirm()" <button @click="confirmDialog.confirm()"
class="flex-1 px-4 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-lg transition-all duration-200 font-medium flex items-center justify-center gap-2"> class="flex-1 px-5 py-3 bg-red-500 dark:bg-red-500 text-white rounded-full transition-all duration-200 font-semibold text-[15px] tracking-tight hover:bg-red-600 dark:hover:bg-red-600 hover:shadow-[0_8px_24px_rgba(239,68,68,0.35)] dark:hover:shadow-[0_8px_24px_rgba(239,68,68,0.4)] active:scale-[0.98] flex items-center justify-center gap-2">
<i class="fas fa-trash text-sm"></i> <i class="fas fa-check text-sm"></i>
{{ confirmDialog.confirmText }} {{ confirmDialog.confirmText }}
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
...@@ -2,46 +2,42 @@ ...@@ -2,46 +2,42 @@
<div class="relative inline-block text-left"> <div class="relative inline-block text-left">
<Menu as="div"> <Menu as="div">
<div> <div>
<MenuButton <!-- Apple 风格菜单按钮 -->
class="inline-flex w-full justify-center rounded-md bg-dark-light border border-laser-purple/30 px-4 py-3 text-sm font-medium text-white hover:bg-dark-light/80 hover:border-laser-purple/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-laser-purple/50 transition-all duration-200" <MenuButton class="inline-flex w-full sm:min-w-[120px] md:min-w-[160px] lg:min-w-[200px] justify-between items-center px-4 py-2.5 sm:px-3.5 sm:py-2 bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] backdrop-saturate-[180%] border border-black/8 dark:border-white/8 rounded-xl text-sm sm:text-[13px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] cursor-pointer transition-all duration-200 ease-out shadow-[0_1px_3px_0_rgba(0,0,0,0.08),0_0_0_1px_rgba(0,0,0,0.02)] dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.3),0_0_0_1px_rgba(255,255,255,0.04)] tracking-tight hover:bg-white dark:hover:bg-[#282828]/98 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_2px_6px_0_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.04)] dark:hover:shadow-[0_2px_6px_0_rgba(0,0,0,0.4),0_0_0_1px_rgba(255,255,255,0.06)] focus:outline-none focus:border-[color:var(--brand-primary)]/50 dark:focus:border-[color:var(--brand-primary-light)]/60 focus:shadow-[0_2px_6px_0_rgba(var(--brand-primary-rgb),0.15),0_0_0_3px_rgba(var(--brand-primary-rgb),0.1)] dark:focus:shadow-[0_2px_6px_0_rgba(var(--brand-primary-light-rgb),0.3),0_0_0_3px_rgba(var(--brand-primary-light-rgb),0.2)]">
> <span class="flex-1 text-left">{{ selectedLabel || placeholder }}</span>
{{ selectedLabel || placeholder }} <ChevronDownIcon class="w-4 h-4 ml-2 flex-shrink-0 text-[#86868b] dark:text-[#98989d] transition-all duration-200" aria-hidden="true" />
<ChevronDownIcon
class="-mr-1 ml-2 h-5 w-5 text-laser-purple hover:text-gradient-primary transition-colors"
aria-hidden="true"
/>
</MenuButton> </MenuButton>
</div> </div>
<transition <transition
enter-active-class="transition duration-100 ease-out" enter-active-class="transition-all duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)]"
enter-from-class="transform scale-95 opacity-0" enter-from-class="opacity-0 scale-95 -translate-y-2"
enter-to-class="transform scale-100 opacity-100" enter-to-class="opacity-100 scale-100 translate-y-0"
leave-active-class="transition duration-75 ease-in" leave-active-class="transition-all duration-150 ease-[cubic-bezier(0.4,0,1,1)]"
leave-from-class="transform scale-100 opacity-100" leave-from-class="opacity-100 scale-100 translate-y-0"
leave-to-class="transform scale-95 opacity-0" leave-to-class="opacity-0 scale-95 -translate-y-1"
> >
<MenuItems <!-- Apple 风格下拉菜单 -->
class="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-700/50 rounded-md bg-dark-light border border-laser-purple/30 shadow-lg ring-1 ring-black/5 focus:outline-none z-50" <MenuItems class="absolute right-0 mt-2 min-w-[200px] w-max max-w-[320px] bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] backdrop-saturate-[180%] border border-black/8 dark:border-white/8 rounded-xl shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_10px_15px_-3px_rgba(0,0,0,0.1),0_0_0_1px_rgba(0,0,0,0.05)] dark:shadow-[0_4px_6px_-1px_rgba(0,0,0,0.3),0_10px_15px_-3px_rgba(0,0,0,0.2),0_0_0_1px_rgba(255,255,255,0.06)] overflow-hidden z-50 outline-none">
> <div class="p-1.5">
<div class="px-1 py-1">
<MenuItem v-for="item in items" :key="item.value" v-slot="{ active }"> <MenuItem v-for="item in items" :key="item.value" v-slot="{ active }">
<button <button
@click="selectItem(item)" @click="selectItem(item)"
:class="[ :class="[
active ? 'bg-laser-purple/20 text-white' : 'text-gray-300', 'flex w-full items-center px-3 py-2 sm:px-2.5 sm:py-1.5 border-0 bg-transparent rounded-lg text-sm sm:text-[13px] text-[#1d1d1f] dark:text-[#f5f5f7] text-left cursor-pointer transition-all duration-150 ease-out tracking-tight',
'group flex w-full items-center rounded-md px-3 py-2 text-sm transition-colors duration-200', active ? 'bg-black/4 dark:bg-white/8' : '',
selectedValue === item.value ? 'font-medium bg-black/6 dark:bg-white/12' : 'font-normal'
]" ]"
> >
<i v-if="item.icon" :class="[item.icon, 'mr-3 h-4 w-4 text-laser-purple']" aria-hidden="true"></i> <i v-if="item.icon" :class="[item.icon, 'w-4 h-4 mr-2.5 sm:mr-2 flex-shrink-0 text-[#86868b] dark:text-[#98989d] transition-colors', active ? 'text-[#1d1d1f] dark:text-[#f5f5f7]' : '']" aria-hidden="true"></i>
{{ item.label }} <span class="flex-1">{{ item.label }}</span>
<i v-if="selectedValue === item.value" class="fas fa-check ml-auto text-laser-purple"></i> <i v-if="selectedValue === item.value" class="fas fa-check w-3.5 h-3.5 ml-3 flex-shrink-0 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</button> </button>
</MenuItem> </MenuItem>
</div> </div>
<div v-if="items.length === 0" class="px-1 py-1"> <div v-if="items.length === 0" class="p-1.5">
<div class="px-3 py-2 text-sm text-gray-500 text-center"> <div class="px-3 py-3 text-center text-[13px] text-[#86868b] dark:text-[#98989d]">
{{ emptyMessage }} {{ emptyMessage }}
</div> </div>
</div> </div>
...@@ -93,3 +89,8 @@ const selectItem = (item) => { ...@@ -93,3 +89,8 @@ const selectItem = (item) => {
emit('select-item', item) emit('select-item', item)
} }
</script> </script>
<style scoped>
/* 所有样式已通过 Tailwind CSS 的 dark: 前缀在 template 中定义 */
/* Apple 风格的极简黑白设计 */
</style>
<script setup>
import { showImageZoomModal, closeImageZoomModal, zoomedImageUrl } from '../utils/other'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<!-- 图片放大弹窗 -->
<div v-cloak>
<div v-if="showImageZoomModal" class="fixed inset-0 bg-black/80 z-60 flex items-center justify-center p-4"
@click="closeImageZoomModal">
<div class="relative max-w-4xl max-h-[90vh] bg-secondary rounded-xl overflow-hidden" @click.stop>
<div class="flex items-center justify-between p-4 border-b border-gray-700">
<h3 class="text-lg font-medium text-white">{{ t('imagePreview') }}</h3>
<button @click="closeImageZoomModal"
class="text-gray-400 hover:text-white transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="p-4">
<img :src="zoomedImageUrl" :alt="'图片预览'"
class="w-full h-auto max-h-[70vh] object-contain rounded-lg" />
</div>
</div>
</div>
</div>
</template>
...@@ -4,51 +4,52 @@ import { useI18n } from 'vue-i18n' ...@@ -4,51 +4,52 @@ import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n() const { t, locale } = useI18n()
</script> </script>
<template> <template>
<!-- 左侧功能区 --> <!-- 左侧功能区 - Apple 极简风格 - 响应式布局 -->
<div class="relative w-20 pl-5 flex flex-col z-10"> <div class="relative flex flex-col z-10 pl-0 sm:pl-5 w-full sm:w-24">
<!-- 功能导航 --> <!-- 功能导航 - Apple 风格统一容器 -->
<div class="p-2 flex flex-col justify-center h-full mobile-nav-buttons" style="margin-top: -10vh;"> <div class="p-2 flex flex-col justify-center h-full mobile-nav-buttons sm:mt-[-10vh]">
<nav class="lg:space-y-3 md:space-y-3 sm:space-x-3 flex flex-col"> <!-- 统一的圆角矩形容器 - Apple 风格 - 响应式方向 -->
<nav class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] border border-black/10 dark:border-white/10 rounded-3xl shadow-[0_4px_16px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] overflow-hidden flex flex-row sm:flex-col w-full sm:w-16">
<!-- 生成视频功能 --> <!-- 生成视频功能 -->
<div <div
@click="switchToCreateView" @click="switchToCreateView"
class="w-18 h-18 flex items-center justify-center rounded-lg transition-all duration-300 class="flex items-center justify-center flex-1 sm:flex-none h-14 sm:h-16 cursor-pointer transition-all duration-200 ease-out mobile-nav-btn group"
font-medium text-sm border border-transparent hover:scale-105 hover:shadow-lg
active:scale-105 active:shadow-lg active:shadow-laser-purple/20 mobile-nav-btn"
:class="$route.path === '/generate' :class="$route.path === '/generate'
? 'bg-laser-purple/40 text-white border-laser-purple/40 shadow-lg shadow-laser-purple/20' ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white'
: 'text-gray-400 hover:text-laser-purple active:text-white active:bg-laser-purple active:border-laser-purple/30'" : 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'"
:title="t('generateVideo')"> :title="t('generateVideo')">
<i class="fi fi-sr-add text-3xl transition-transform duration-300 group-hover:animate-pulse"></i> <i class="fas fa-plus text-xl transition-all duration-200 group-hover:scale-110"></i>
</div> </div>
<!-- 分割线 - Apple 风格 - 响应式方向 -->
<div class="w-px sm:w-auto sm:h-px bg-black/8 dark:bg-white/8 my-3 sm:my-0 sm:mx-3"></div>
<!-- 我的项目功能 --> <!-- 我的项目功能 -->
<div <div
@click="switchToProjectsView" @click="switchToProjectsView"
class="w-18 h-18 flex items-center justify-center rounded-lg transition-all duration-300 class="flex items-center justify-center flex-1 sm:flex-none h-14 sm:h-16 cursor-pointer transition-all duration-200 ease-out mobile-nav-btn group"
font-medium text-sm border border-transparent hover:scale-105 hover:shadow-lg
mobile-nav-btn"
:class="$route.path === '/projects' :class="$route.path === '/projects'
? 'bg-laser-purple/40 text-white border-laser-purple/40 shadow-lg shadow-laser-purple/20' ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white'
: 'text-gray-400 hover:text-laser-purple active:text-white active:bg-laser-purple active:border-laser-purple/30'" : 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'"
:title="t('myProjects')"> :title="t('myProjects')">
<i class="fi fi-br-house-chimney-heart text-3xl transition-transform duration-300 group-hover:animate-pulse"></i> <i class="fas fa-folder-open text-lg transition-all duration-200 group-hover:scale-110"></i>
</div> </div>
<!-- 分割线 - Apple 风格 - 响应式方向 -->
<div class="w-px sm:w-auto sm:h-px bg-black/8 dark:bg-white/8 my-3 sm:my-0 sm:mx-3"></div>
<!-- 灵感广场功能 --> <!-- 灵感广场功能 -->
<div <div
@click="switchToInspirationView" @click="switchToInspirationView"
class="w-18 h-18 flex items-center justify-center rounded-lg transition-all duration-300 class="flex items-center justify-center flex-1 sm:flex-none h-14 sm:h-16 cursor-pointer transition-all duration-200 ease-out mobile-nav-btn group"
font-medium text-sm border border-transparent hover:scale-105 hover:shadow-lg
active:scale-105 active:shadow-lg active:shadow-laser-purple/20 mobile-nav-btn"
:class="$route.path === '/inspirations' :class="$route.path === '/inspirations'
? 'bg-laser-purple/40 text-white border-laser-purple/40 shadow-lg shadow-laser-purple/20' ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white'
: 'text-gray-400 hover:text-laser-purple active:text-white active:bg-laser-purple active:border-laser-purple/30'" : 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'"
:title="t('inspiration')"> :title="t('inspiration')">
<i class="fas fa-lightbulb text-lg transition-all duration-200 group-hover:scale-110"></i>
<i class="fi fi-sr-sparkles text-3xl transition-transform duration-300 group-hover:animate-pulse"></i>
</div> </div>
</nav> </nav>
</div> </div>
</div> </div>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment