mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-22 11:04:26 +08:00
完成移动端Suno页面功能
This commit is contained in:
@@ -112,6 +112,10 @@ type RespVo struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
Channel string `json:"channel,omitempty"`
|
Channel string `json:"channel,omitempty"`
|
||||||
|
Error struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||||
@@ -154,13 +158,14 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
logger.Debugf("API response: %s", string(body))
|
||||||
err = json.Unmarshal(body, &res)
|
err = json.Unmarshal(body, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Code != "success" {
|
if res.Code != "success" {
|
||||||
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
|
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Error.Message)
|
||||||
}
|
}
|
||||||
// update the last_use_at for api key
|
// update the last_use_at for api key
|
||||||
apiKey.LastUsedAt = time.Now().Unix()
|
apiKey.LastUsedAt = time.Now().Unix()
|
||||||
|
|||||||
@@ -56,3 +56,24 @@ export function showLoading(message = '正在处理...') {
|
|||||||
export function closeLoading() {
|
export function closeLoading() {
|
||||||
closeToast()
|
closeToast()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义 Toast 消息系统
|
||||||
|
export function showToastMessage(message, type = 'info', duration = 3000) {
|
||||||
|
const toast = document.createElement('div')
|
||||||
|
toast.className = `fixed top-20 left-1/2 transform -translate-x-1/2 z-50 px-4 py-2 rounded-lg text-white font-medium ${
|
||||||
|
type === 'error' ? 'bg-red-500' : type === 'success' ? 'bg-green-500' : 'bg-blue-500'
|
||||||
|
} animate-fade-in`
|
||||||
|
toast.textContent = message
|
||||||
|
document.body.appendChild(toast)
|
||||||
|
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('animate-fade-out')
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(toast)) {
|
||||||
|
document.body.removeChild(toast)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
116
web/src/views/mobile/components/CustomSelect.vue
Normal file
116
web/src/views/mobile/components/CustomSelect.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="custom-select">
|
||||||
|
<label v-if="label" class="block text-gray-700 font-medium mb-2">{{ label }}</label>
|
||||||
|
<button
|
||||||
|
@click="showPicker = true"
|
||||||
|
class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors"
|
||||||
|
>
|
||||||
|
<span class="text-gray-900">{{ selectedLabel || placeholder || '请选择' }}</span>
|
||||||
|
<i class="iconfont icon-down text-gray-400"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 选择器弹窗 -->
|
||||||
|
<div
|
||||||
|
v-if="showPicker"
|
||||||
|
class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-50"
|
||||||
|
@click="showPicker = false"
|
||||||
|
>
|
||||||
|
<div @click.stop class="bg-white rounded-t-2xl w-full max-w-md animate-slide-up">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">{{ title || '请选择' }}</h3>
|
||||||
|
<button @click="showPicker = false" class="p-2 hover:bg-gray-100 rounded-full">
|
||||||
|
<i class="iconfont icon-close text-gray-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-80 overflow-y-auto">
|
||||||
|
<CustomSelectOption
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
:option="option"
|
||||||
|
:selected="modelValue === option.value"
|
||||||
|
@select="onSelect"
|
||||||
|
>
|
||||||
|
<template #default="slotProps">
|
||||||
|
<slot name="option" v-bind="slotProps">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-900 font-medium">{{ slotProps.option.label }}</span>
|
||||||
|
<p v-if="slotProps.option.desc" class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ slotProps.option.desc }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="slotProps.selected" class="text-blue-600">
|
||||||
|
<i class="iconfont icon-success"></i>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</CustomSelectOption>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import CustomSelectOption from './CustomSelectOption.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请选择',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '请选择',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const showPicker = ref(false)
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const selectedLabel = computed(() => {
|
||||||
|
const selected = props.options.find((option) => option.value === props.modelValue)
|
||||||
|
return selected ? selected.label : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const onSelect = (option) => {
|
||||||
|
emit('update:modelValue', option.value)
|
||||||
|
emit('change', option)
|
||||||
|
showPicker.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slide-up 0.3s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
web/src/views/mobile/components/CustomSelectOption.vue
Normal file
50
web/src/views/mobile/components/CustomSelectOption.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
CustomSelectOption 组件
|
||||||
|
Props:
|
||||||
|
- option: 选项对象,必需,包含 label/desc/value 等属性
|
||||||
|
- selected: 是否为当前选中项
|
||||||
|
Emits:
|
||||||
|
- select(option): 选中该项时触发
|
||||||
|
Slots:
|
||||||
|
- 默认插槽(default):用于自定义 option 内容,slotProps: { option, selected }
|
||||||
|
示例:
|
||||||
|
<template #option="{ option, selected }">
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div v-if="selected">✔</div>
|
||||||
|
</template>
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors border-b last:border-b-0"
|
||||||
|
@click="$emit('select', option)"
|
||||||
|
>
|
||||||
|
<slot :option="option" :selected="selected">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-900 font-medium">{{ option.label }}</span>
|
||||||
|
<p v-if="option.desc" class="text-sm text-gray-500 mt-1">{{ option.desc }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="selected" class="text-blue-600">
|
||||||
|
<i class="iconfont icon-success"></i>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
option: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['select'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user