feat: 配额优化
This commit is contained in:
61
frontend/app/web-gold/src/api/modelConfig.js
Normal file
61
frontend/app/web-gold/src/api/modelConfig.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* AI模型配置 API 服务
|
||||
* 用于获取模型积分消耗配置
|
||||
*/
|
||||
|
||||
import http from './http'
|
||||
|
||||
const BASE_URL = '/webApi/api/tik/ai-model-config'
|
||||
|
||||
/**
|
||||
* 模型配置 API 服务
|
||||
*/
|
||||
export const ModelConfigService = {
|
||||
/**
|
||||
* 获取所有启用的模型配置列表(按平台分组)
|
||||
* @returns {Promise<Object>} 按平台分组的模型配置
|
||||
* 格式: { platform: [{ modelCode, modelName, consumePoints }] }
|
||||
*/
|
||||
async getEnabledModelConfigList() {
|
||||
const { data } = await http.get(`${BASE_URL}/list-enabled`)
|
||||
return data || {}
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据模型代码获取积分消耗
|
||||
* @param {Object} configMap - 配置映射(从 getEnabledModelConfigList 获取)
|
||||
* @param {string} modelCode - 模型代码
|
||||
* @returns {number|null} 积分消耗,未找到返回 null
|
||||
*/
|
||||
getConsumePoints(configMap, modelCode) {
|
||||
if (!configMap || !modelCode) return null
|
||||
|
||||
for (const platform of Object.values(configMap)) {
|
||||
const model = platform?.find(m => m.modelCode === modelCode)
|
||||
if (model) {
|
||||
return model.consumePoints
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据模型代码获取模型名称
|
||||
* @param {Object} configMap - 配置映射
|
||||
* @param {string} modelCode - 模型代码
|
||||
* @returns {string|null} 模型名称,未找到返回 null
|
||||
*/
|
||||
getModelName(configMap, modelCode) {
|
||||
if (!configMap || !modelCode) return null
|
||||
|
||||
for (const platform of Object.values(configMap)) {
|
||||
const model = platform?.find(m => m.modelCode === modelCode)
|
||||
if (model) {
|
||||
return model.modelName
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default ModelConfigService
|
||||
100
frontend/app/web-gold/src/components/PointsTag.vue
Normal file
100
frontend/app/web-gold/src/components/PointsTag.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<span v-if="displayText" :class="['points-tag', `points-tag--${size}`, { 'points-tag--free': isFree }]">
|
||||
<span v-if="showIcon" class="points-tag__icon">⚡</span>
|
||||
<span class="points-tag__text">{{ displayText }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||
|
||||
const props = defineProps({
|
||||
// 模型代码
|
||||
modelCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 直接传入积分数(优先级高于 modelCode)
|
||||
points: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
// 尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default', // small, default, large
|
||||
validator: (value) => ['small', 'default', 'large'].includes(value)
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const pointsConfigStore = usePointsConfigStore()
|
||||
|
||||
// 获取积分数
|
||||
const consumePoints = computed(() => {
|
||||
if (props.points !== null) {
|
||||
return props.points
|
||||
}
|
||||
if (props.modelCode) {
|
||||
return pointsConfigStore.getConsumePoints(props.modelCode)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 是否免费
|
||||
const isFree = computed(() => consumePoints.value === 0)
|
||||
|
||||
// 显示文本
|
||||
const displayText = computed(() => {
|
||||
if (consumePoints.value === null) {
|
||||
return ''
|
||||
}
|
||||
return pointsConfigStore.formatPoints(consumePoints.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.points-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
|
||||
&--small {
|
||||
padding: 1px 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&--default {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--large {
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&--free {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&__text {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<transition name="drawer-fade">
|
||||
<div v-if="visible" class="chat-overlay" @click.self="handleOverlayClick">
|
||||
<div v-if="visible" class="chat-overlay" @click.self="handleClose">
|
||||
<div class="chat-drawer" :class="{ 'chat-drawer--visible': visible }" @click.stop>
|
||||
<!-- Background Effect -->
|
||||
<div class="drawer-bg-pattern"></div>
|
||||
@@ -104,7 +104,7 @@
|
||||
@click="modelMode = 'standard'"
|
||||
>
|
||||
标准
|
||||
<span class="model-cost">-10 积分</span>
|
||||
<PointsTag :points="10" size="small" />
|
||||
</button>
|
||||
<button
|
||||
class="model-tab pro"
|
||||
@@ -113,7 +113,7 @@
|
||||
>
|
||||
<ThunderboltFilled />
|
||||
深度
|
||||
<span class="model-cost">-50 积分</span>
|
||||
<PointsTag :points="50" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||
import {
|
||||
CloseOutlined,
|
||||
RobotOutlined,
|
||||
@@ -173,6 +173,15 @@ import { message, Modal } from 'ant-design-vue'
|
||||
import { sendChatStream } from '@/api/agent'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import HistoryPanel from './HistoryPanel.vue'
|
||||
import PointsTag from '@/components/PointsTag.vue'
|
||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||
|
||||
const pointsConfigStore = usePointsConfigStore()
|
||||
|
||||
// 在组件挂载时加载积分配置
|
||||
onMounted(() => {
|
||||
pointsConfigStore.loadConfig()
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
@@ -232,8 +241,6 @@ const doClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleOverlayClick = () => handleClose()
|
||||
|
||||
const adjustTextareaHeight = () => {
|
||||
const el = textareaRef.value
|
||||
if (el) {
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { MaterialService } from '@/api/material'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// GB转字节常量
|
||||
const GB_TO_BYTES = 1073741824
|
||||
|
||||
/**
|
||||
* 获取视频时长(秒)
|
||||
@@ -140,6 +144,20 @@ export function useUpload() {
|
||||
state.error = null
|
||||
state.progress = 0
|
||||
|
||||
// 存储空间校验
|
||||
const userStore = useUserStore()
|
||||
const fileSizeGB = file.size / GB_TO_BYTES
|
||||
const remainingStorage = userStore.remainingStorage || 0
|
||||
|
||||
if (fileSizeGB > remainingStorage) {
|
||||
const error = new Error(`存储空间不足!需要 ${fileSizeGB.toFixed(2)} GB,剩余 ${remainingStorage.toFixed(2)} GB`)
|
||||
state.uploading = false
|
||||
state.status = 'error'
|
||||
state.error = error.message
|
||||
onError?.(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// 通知开始
|
||||
onStart?.()
|
||||
|
||||
|
||||
162
frontend/app/web-gold/src/stores/pointsConfig.js
Normal file
162
frontend/app/web-gold/src/stores/pointsConfig.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 积分配置 Store
|
||||
* 管理AI模型积分消耗配置
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getJSON, setJSON } from '@/utils/storage'
|
||||
import ModelConfigService from '@/api/modelConfig'
|
||||
|
||||
const STORAGE_KEY = 'points_config_v1'
|
||||
const CACHE_DURATION = 30 * 60 * 1000 // 30分钟缓存
|
||||
|
||||
export const usePointsConfigStore = defineStore('pointsConfig', () => {
|
||||
// 模型配置映射(按平台分组)
|
||||
const configMap = ref({})
|
||||
// 是否已加载
|
||||
const isLoaded = ref(false)
|
||||
// 上次加载时间
|
||||
const lastLoadTime = ref(0)
|
||||
// 是否正在加载
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 所有平台列表
|
||||
const platforms = computed(() => Object.keys(configMap.value))
|
||||
|
||||
// 获取所有模型列表(扁平化)
|
||||
const allModels = computed(() => {
|
||||
const models = []
|
||||
for (const [platform, list] of Object.entries(configMap.value)) {
|
||||
if (Array.isArray(list)) {
|
||||
list.forEach(model => {
|
||||
models.push({ ...model, platform })
|
||||
})
|
||||
}
|
||||
}
|
||||
return models
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据模型代码获取积分消耗
|
||||
* @param {string} modelCode - 模型代码
|
||||
* @returns {number|null} 积分消耗
|
||||
*/
|
||||
const getConsumePoints = (modelCode) => {
|
||||
if (!modelCode) return null
|
||||
return ModelConfigService.getConsumePoints(configMap.value, modelCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型代码获取模型名称
|
||||
* @param {string} modelCode - 模型代码
|
||||
* @returns {string|null} 模型名称
|
||||
*/
|
||||
const getModelName = (modelCode) => {
|
||||
if (!modelCode) return null
|
||||
return ModelConfigService.getModelName(configMap.value, modelCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型完整信息
|
||||
* @param {string} modelCode - 模型代码
|
||||
* @returns {Object|null} 模型信息 { modelCode, modelName, consumePoints, platform }
|
||||
*/
|
||||
const getModelInfo = (modelCode) => {
|
||||
if (!modelCode) return null
|
||||
for (const [platform, list] of Object.entries(configMap.value)) {
|
||||
if (Array.isArray(list)) {
|
||||
const model = list.find(m => m.modelCode === modelCode)
|
||||
if (model) {
|
||||
return { ...model, platform }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储恢复
|
||||
*/
|
||||
const hydrateFromStorage = async () => {
|
||||
const saved = await getJSON(STORAGE_KEY)
|
||||
if (saved && saved.configMap) {
|
||||
configMap.value = saved.configMap
|
||||
lastLoadTime.value = saved.lastLoadTime || 0
|
||||
isLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化到本地存储
|
||||
*/
|
||||
const persistToStorage = async () => {
|
||||
await setJSON(STORAGE_KEY, {
|
||||
configMap: configMap.value,
|
||||
lastLoadTime: lastLoadTime.value,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模型配置(从服务器)
|
||||
* @param {boolean} force - 是否强制刷新
|
||||
*/
|
||||
const loadConfig = async (force = false) => {
|
||||
// 检查缓存是否有效
|
||||
const now = Date.now()
|
||||
if (!force && isLoaded.value && (now - lastLoadTime.value) < CACHE_DURATION) {
|
||||
return configMap.value
|
||||
}
|
||||
|
||||
// 防止重复加载
|
||||
if (isLoading.value) {
|
||||
return configMap.value
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await ModelConfigService.getEnabledModelConfigList()
|
||||
configMap.value = data || {}
|
||||
lastLoadTime.value = now
|
||||
isLoaded.value = true
|
||||
await persistToStorage()
|
||||
return configMap.value
|
||||
} catch (error) {
|
||||
console.error('[pointsConfig] 加载模型配置失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化积分显示
|
||||
* @param {number} points - 积分数
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
const formatPoints = (points) => {
|
||||
if (points === null || points === undefined) return ''
|
||||
if (points === 0) return '免费'
|
||||
return `${points}积分`
|
||||
}
|
||||
|
||||
// 初始化时从本地恢复
|
||||
hydrateFromStorage()
|
||||
|
||||
return {
|
||||
// 状态
|
||||
configMap,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
platforms,
|
||||
allModels,
|
||||
// 方法
|
||||
getConsumePoints,
|
||||
getModelName,
|
||||
getModelInfo,
|
||||
loadConfig,
|
||||
formatPoints,
|
||||
}
|
||||
})
|
||||
|
||||
export default usePointsConfigStore
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import GradientButton from '@/components/GradientButton.vue'
|
||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||
|
||||
const pointsConfigStore = usePointsConfigStore()
|
||||
|
||||
// 加载积分配置
|
||||
onMounted(() => {
|
||||
pointsConfigStore.loadConfig()
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -65,6 +73,7 @@ function handleReset() {
|
||||
/>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
<p class="points-hint">每次分析将消耗积分,消耗量与分析数量相关</p>
|
||||
</a-form>
|
||||
</section>
|
||||
</template>
|
||||
@@ -88,6 +97,13 @@ function handleReset() {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.points-hint {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.ant-slider) {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 试听优化:添加缓存机制,同一参数下第二次试听直接播放缓存,无需重复调用API
|
||||
*/
|
||||
defineOptions({ name: 'DigitalVideoPage' })
|
||||
import { ref, computed, onMounted, watch, onUnmounted, onActivated } from 'vue'
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { InboxOutlined, SoundOutlined, LoadingOutlined } from '@ant-design/icons-vue'
|
||||
import { VoiceService } from '@/api/voice'
|
||||
@@ -18,7 +18,9 @@ import { DEFAULT_VOICE_PROVIDER } from '@/config/voiceConfig'
|
||||
|
||||
// 导入 voiceStore 用于获取用户音色
|
||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||
const voiceStore = useVoiceCopyStore()
|
||||
const pointsConfigStore = usePointsConfigStore()
|
||||
|
||||
// 状态管理
|
||||
const uploadedVideo = ref('')
|
||||
@@ -36,6 +38,10 @@ const playingPreviewVoiceId = ref('') // 当前正在试听的音色ID
|
||||
const isPlayingSynthesized = ref(false) // 是否正在播放已合成的音频
|
||||
const pollingInterval = ref(null) // 轮询间隔ID
|
||||
|
||||
// 音频播放实例
|
||||
let previewAudio = null
|
||||
let previewObjectUrl = ''
|
||||
|
||||
// Upload Hook
|
||||
const { upload } = useUpload()
|
||||
|
||||
@@ -760,6 +766,8 @@ const playAudioFromBase64 = (audioBase64, format = 'mp3', onEnded = null) => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 加载积分配置
|
||||
pointsConfigStore.loadConfig()
|
||||
|
||||
await voiceStore.refresh()
|
||||
// 默认选择第一个音色
|
||||
@@ -830,9 +838,6 @@ watch([ttsText, speechRate, instruction, emotion], () => {
|
||||
console.log('试听参数已变化,清除缓存')
|
||||
})
|
||||
|
||||
// 音频实例
|
||||
let previewAudio = null
|
||||
let previewObjectUrl = ''
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1032,6 +1037,7 @@ let previewObjectUrl = ''
|
||||
>
|
||||
{{ isGenerating ? '生成中...' : '生成视频' }}
|
||||
</a-button>
|
||||
<p class="points-hint">生成视频将消耗积分,消耗量与视频时长相关</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-actions">
|
||||
@@ -1440,6 +1446,13 @@ let previewObjectUrl = ''
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.points-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -82,6 +82,14 @@
|
||||
|
||||
<!-- 搜索和操作区 -->
|
||||
<div class="toolbar-actions">
|
||||
<!-- 存储配额显示 -->
|
||||
<div class="storage-quota">
|
||||
<span class="storage-quota__label">存储空间</span>
|
||||
<span class="storage-quota__value">{{ userStore.usedStorage.toFixed(2) }} / {{ userStore.totalStorage }} GB</span>
|
||||
<div class="storage-quota__bar">
|
||||
<div class="storage-quota__used" :style="{ width: `${Math.min((userStore.usedStorage / userStore.totalStorage) * 100, 100)}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名..."
|
||||
@@ -273,6 +281,10 @@ import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
|
||||
import MaterialService, { MaterialGroupService } from '@/api/material';
|
||||
import { useUpload } from '@/composables/useUpload';
|
||||
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
// 用户状态(获取存储配额)
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
@@ -530,15 +542,6 @@ const handleFileClick = (file) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelectChange = (fileId, checked) => {
|
||||
const index = selectedFileIds.value.indexOf(fileId)
|
||||
if (checked && index === -1) {
|
||||
selectedFileIds.value.push(fileId)
|
||||
} else if (!checked && index > -1) {
|
||||
selectedFileIds.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenUploadModal = () => {
|
||||
uploadModalVisible.value = true
|
||||
}
|
||||
@@ -572,6 +575,8 @@ const handleFileUpload = async (filesWithCover, category, groupId) => {
|
||||
message.success(`成功上传 ${filesWithCover.length} 个文件`)
|
||||
uploadModalVisible.value = false
|
||||
await loadFileList()
|
||||
// 刷新存储配额
|
||||
await userStore.fetchUserProfile()
|
||||
// 混剪素材才刷新分组列表
|
||||
if (activeCategory.value === 'MIX') {
|
||||
await loadGroupList()
|
||||
@@ -633,6 +638,9 @@ const handleBatchDelete = async () => {
|
||||
totalFileCount.value = Math.max(0, totalFileCount.value - count)
|
||||
selectedFileIds.value = []
|
||||
|
||||
// 刷新存储配额
|
||||
await userStore.fetchUserProfile()
|
||||
|
||||
// 如果删除后当前页没有数据了,则加载上一页
|
||||
if (fileList.value.length === 0 && pagination.current > 1) {
|
||||
pagination.current = pagination.current - 1
|
||||
@@ -727,8 +735,10 @@ watch(activeCategory, () => {
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadGroupList()
|
||||
onMounted(async () => {
|
||||
// 刷新用户档案获取最新存储配额
|
||||
await userStore.fetchUserProfile()
|
||||
await loadGroupList()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -987,6 +997,43 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 存储配额显示
|
||||
.storage-quota {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: @bg-page;
|
||||
border-radius: @radius-sm;
|
||||
border: 1px solid @border-color;
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
color: @text-muted;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: @text-primary;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: @border-color;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__used {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, @primary-color, #818cf8);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏操作区
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
|
||||
@@ -6,6 +6,7 @@ import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhu
|
||||
import { rewriteStream } from '@/api/forecast'
|
||||
import { getAgentList } from '@/api/agent'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
|
||||
@@ -13,6 +14,7 @@ defineOptions({ name: 'ForecastView' })
|
||||
|
||||
// 状态管理
|
||||
const userStore = useUserStore()
|
||||
const pointsConfigStore = usePointsConfigStore()
|
||||
const searchKeyword = ref('')
|
||||
const isLoading = ref(false)
|
||||
const isGenerating = ref(false)
|
||||
@@ -368,6 +370,7 @@ async function handleSearch() {
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadAgentList()
|
||||
pointsConfigStore.loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -604,6 +607,7 @@ onMounted(() => {
|
||||
<span>生成爆款</span>
|
||||
</template>
|
||||
</button>
|
||||
<p class="points-hint">生成爆款文案将消耗积分</p>
|
||||
|
||||
<!-- 生成结果 -->
|
||||
<Transition name="slide-up">
|
||||
@@ -848,10 +852,6 @@ onMounted(() => {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 热点卡片
|
||||
.topic-list {
|
||||
display: flex;
|
||||
@@ -1205,6 +1205,13 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.points-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
// 结果区域
|
||||
.result-block {
|
||||
padding-top: 16px;
|
||||
|
||||
Reference in New Issue
Block a user