fix: 修复问题

This commit is contained in:
2026-01-18 02:15:08 +08:00
parent aa81a1aebc
commit 961e4bcc77
15 changed files with 1652 additions and 1103 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>AI营销</title>
</head>
<body>
<div id="app"></div>

View File

@@ -72,7 +72,9 @@ onMounted(async () => {})
<template>
<a-config-provider :theme="themeToken">
<SvgSprite />
<RouterView />
<keep-alive>
<RouterView />
</keep-alive>
</a-config-provider>
</template>
@@ -112,5 +114,6 @@ onMounted(async () => {})
}
.ant-modal .ant-modal-footer {
display: flex;
justify-content: end;
}
</style>

View File

@@ -0,0 +1,577 @@
<template>
<div class="prompt-selector">
<!-- 下拉菜单式选择器推荐方案 -->
<div v-if="displayMode === 'select'" class="prompt-select-container">
<a-select
v-model:value="selectedPromptId"
placeholder="选择提示词风格"
style="width: 100%"
@change="handleSelectChange"
:loading="loading"
>
<a-select-option
v-for="prompt in allPrompts"
:key="prompt.id"
:value="prompt.id"
>
<div class="prompt-option">
<span class="prompt-option-name">{{ prompt.name }}</span>
</div>
</a-select-option>
</a-select>
</div>
<!-- 标签式选择器 -->
<div v-else-if="displayMode === 'tags'" class="prompt-tags-container">
<div class="prompt-tags-grid">
<div
v-for="prompt in displayPrompts"
:key="prompt.id"
class="prompt-tag"
:class="{ 'prompt-tag-selected': selectedPromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
<div v-if="hasMore && showMoreButton" class="prompt-more-button">
<a-button size="small" type="link" @click="showAllPromptsModal = true" :disabled="allPrompts.length === 0">
更多 ({{ allPrompts.length - displayCount }})
</a-button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading && allPrompts.length === 0" class="prompt-loading">
<a-spin size="small" />
<span class="loading-text">加载中...</span>
</div>
<!-- 空状态 -->
<div v-else-if="!loading && allPrompts.length === 0" class="prompt-empty-state">
<div class="prompt-empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<p class="prompt-empty-text">没有找到提示词</p>
</div>
<!-- 更多提示词弹窗 -->
<div v-if="showAllPromptsModal" class="prompt-modal-mask" @click.self="showAllPromptsModal = false">
<div class="prompt-modal">
<div class="prompt-modal-header">
<h3 class="prompt-modal-title">选择提示词</h3>
<button class="prompt-modal-close" @click="showAllPromptsModal = false">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<!-- 搜索框 -->
<div class="prompt-modal-search">
<svg class="prompt-search-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
v-model="promptSearchKeyword"
type="text"
placeholder="搜索提示词..."
class="prompt-search-input"
/>
<button class="refresh-button" @click="refreshUserPrompts" :disabled="loading">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
</div>
<!-- 提示词列表 -->
<div class="prompt-modal-content">
<div v-if="filteredPrompts.length > 0" class="all-prompts-grid">
<div
v-for="prompt in filteredPrompts"
:key="prompt.id"
class="all-prompt-tag"
:class="{ 'all-prompt-tag-selected': selectedPromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
<span v-if="prompt.status === 1" class="all-prompt-tag-status">启用</span>
<span v-else class="all-prompt-tag-status all-prompt-tag-status-disabled">禁用</span>
</div>
</div>
<div v-else class="prompt-empty-state">
<div class="prompt-empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<p class="prompt-empty-text">没有找到匹配的提示词</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { usePromptStore } from '@/stores/prompt'
import { message } from 'ant-design-vue'
import { setJSON, getJSON } from '@/utils/storage'
// Props
const props = defineProps({
// 当前选中的提示词ID
modelValue: {
type: Number,
default: null
},
// 展示模式tags标签或 select下拉选择
displayMode: {
type: String,
default: 'select' // 默认为select模式因为用户反馈标签模式不好看
},
// 展示数量(仅标签模式有效)
displayCount: {
type: Number,
default: 6
},
// 是否显示"更多"按钮
showMoreButton: {
type: Boolean,
default: true
},
// 本地存储键名前缀,用于保存和恢复选择
storageKey: {
type: String,
default: 'prompt_selector'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'change'])
// Stores
const userStore = useUserStore()
const promptStore = usePromptStore()
// Refs
const showAllPromptsModal = ref(false)
const promptSearchKeyword = ref('')
const selectedPromptId = ref(props.modelValue)
// 使用 store 中的数据
const allPrompts = computed(() => promptStore.promptList)
const loading = computed(() => promptStore.promptListLoading)
// 计算属性:展示的部分提示词
const displayPrompts = computed(() => {
if (props.displayMode !== 'tags') return []
return allPrompts.value.slice(0, props.displayCount)
})
// 计算属性:是否有更多提示词
const hasMore = computed(() => {
return allPrompts.value.length > props.displayCount
})
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
const filteredPrompts = computed(() => {
if (!promptSearchKeyword.value.trim()) {
return allPrompts.value
}
const keyword = promptSearchKeyword.value.trim().toLowerCase()
return allPrompts.value.filter(p =>
p.name.toLowerCase().includes(keyword) ||
(p.content && p.content.toLowerCase().includes(keyword))
)
})
// 监听 selectedPromptId 变化,同步到父组件
watch(selectedPromptId, (newValue) => {
emit('update:modelValue', newValue)
saveSelectedPromptId(newValue)
})
// 监听 props.modelValue 变化,更新内部状态
watch(() => props.modelValue, (newValue) => {
selectedPromptId.value = newValue
})
// 加载用户提示词
async function loadUserPrompts() {
// 检查用户是否登录
if (!userStore.userId) {
console.warn('用户未登录,无法加载提示词')
return
}
try {
// 使用 store 加载数据
const prompts = await promptStore.loadPromptList(userStore.userId)
// 如果有选中ID但当前选中的提示词不在列表中清空选择
if (selectedPromptId.value && !prompts.find(p => p.id === selectedPromptId.value)) {
selectedPromptId.value = null
}
// 如果没有选中ID且有提示词默认选中第一个
else if (!selectedPromptId.value && prompts.length > 0) {
selectedPromptId.value = prompts[0].id
}
// 尝试从本地存储恢复选中状态
await restoreSelectedPromptId()
} catch (error) {
console.error('加载提示词失败:', error)
message.error('加载提示词失败')
}
}
// 保存选中的提示词ID到本地存储
async function saveSelectedPromptId(promptId) {
if (!promptId) return
try {
await setJSON(`${props.storageKey}_selected`, promptId)
} catch (error) {
console.error('保存提示词选择失败:', error)
}
}
// 从本地存储恢复选中的提示词ID
async function restoreSelectedPromptId() {
try {
const savedId = await getJSON(`${props.storageKey}_selected`, null)
if (savedId) {
// 检查保存的ID是否在当前列表中
const prompt = allPrompts.value.find(p => p.id === savedId)
if (prompt) {
selectedPromptId.value = savedId
}
}
} catch (error) {
console.error('恢复提示词选择失败:', error)
}
}
// 选择提示词
function selectPrompt(prompt) {
if (!prompt || !prompt.content) {
console.warn('提示词内容为空')
return
}
selectedPromptId.value = prompt.id
emit('change', prompt)
showAllPromptsModal.value = false
}
// 处理下拉选择变化
function handleSelectChange(value) {
const prompt = allPrompts.value.find(p => p.id === value)
if (prompt) {
emit('change', prompt)
}
}
// 刷新用户提示词
async function refreshUserPrompts() {
try {
await promptStore.refreshPromptList(userStore.userId)
await restoreSelectedPromptId()
} catch (error) {
console.error('刷新提示词失败:', error)
message.error('刷新提示词失败')
}
}
// 组件挂载时加载提示词
onMounted(() => {
loadUserPrompts()
})
</script>
<style scoped>
/* 基础样式 */
.prompt-selector {
width: 100%;
}
/* 下拉选择器样式 */
.prompt-select-container {
width: 100%;
}
.prompt-option {
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt-option-name {
font-weight: 500;
}
/* 标签模式 */
.prompt-tags-container {
margin-bottom: 12px;
}
.prompt-tags-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.prompt-tag {
display: inline-flex;
align-items: center;
padding: 6px 14px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.prompt-tag:hover {
border-color: var(--color-primary);
background: rgba(24, 144, 255, 0.08);
}
.prompt-tag-selected {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.prompt-tag-name {
white-space: nowrap;
user-select: none;
}
.prompt-more-button {
margin-top: 8px;
text-align: right;
}
/* 加载状态 */
.prompt-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px;
color: var(--color-text-secondary);
}
.loading-text {
font-size: 14px;
}
/* 空状态 */
.prompt-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--color-text-secondary);
}
.prompt-empty-icon {
color: var(--color-text-secondary);
opacity: 0.5;
margin-bottom: 12px;
}
.prompt-empty-text {
font-size: 14px;
margin: 0;
}
/* 弹窗 */
.prompt-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.prompt-modal {
background: var(--color-surface);
border-radius: var(--radius-card);
width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.prompt-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
}
.prompt-modal-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.prompt-modal-close {
background: transparent;
border: none;
cursor: pointer;
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
}
.prompt-modal-close:hover {
background: var(--color-bg);
color: var(--color-text);
}
.prompt-modal-search {
position: relative;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
display: flex;
gap: 12px;
}
.prompt-search-icon {
position: absolute;
left: 36px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-secondary);
pointer-events: none;
}
.prompt-search-input {
flex: 1;
padding: 10px 12px 10px 40px;
font-size: 14px;
color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
}
.prompt-search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
.refresh-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-text);
cursor: pointer;
}
.refresh-button:hover:not(:disabled) {
background: var(--color-primary);
color: #fff;
border-color: var(--color-primary);
}
.refresh-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.prompt-modal-content {
overflow-y: auto;
max-height: calc(80vh - 180px);
padding: 16px 0;
}
/* 提示词列表 */
.all-prompts-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 16px;
}
.all-prompt-tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.all-prompt-tag:hover {
border-color: var(--color-primary);
background: rgba(24, 144, 255, 0.08);
}
.all-prompt-tag-selected {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.all-prompt-tag-name {
white-space: nowrap;
user-select: none;
}
.all-prompt-tag-status {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.all-prompt-tag-status-disabled {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
</style>

View File

@@ -4,7 +4,7 @@
title="素材混剪"
centered
:confirm-loading="loading"
ok-text="提交混剪"
ok-text="确定"
cancel-text="取消"
@ok="handleConfirm"
@cancel="handleCancel"

View File

@@ -25,7 +25,7 @@
</p>
<p class="ant-upload-text">点击或拖拽文件到此处上传</p>
<p class="ant-upload-hint">
支持多文件上传单个文件不超过 100MB
支持多文件上传总大小不超过 300MB
<br />
支持格式视频MP4MOVAVI等图片JPGPNGGIF等音频MP3WAV等
</p>
@@ -34,7 +34,12 @@
<!-- 已选文件列表 -->
<div v-if="fileList.length > 0" class="upload-file-list">
<div class="upload-file-list-title">已选择 {{ fileList.length }} 个文件</div>
<div class="upload-file-list-title">
已选择 {{ fileList.length }} 个文件总大小{{ getTotalSize() }}
<span v-if="getTotalSizeBytes() > MAX_TOTAL_SIZE" style="color: #ff4d4f; margin-left: 8px;">
已超出 300MB 限制
</span>
</div>
<div class="upload-file-items">
<div
v-for="(fileItem, index) in fileList"
@@ -109,7 +114,20 @@ const DEFAULT_FILE_CATEGORY = 'video'
const acceptTypes = 'video/*,image/*,audio/*,.mp4,.mov,.avi,.mkv,.jpg,.jpeg,.png,.gif,.webp,.mp3,.wav,.aac'
// 常量
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
const MAX_TOTAL_SIZE = 300 * 1024 * 1024 // 300MB
const MAX_SINGLE_FILE_SIZE = 300 * 1024 * 1024 // 300MB (单个文件最大300MB)
// 计算总大小(字节)
const getTotalSizeBytes = () => {
return fileList.value.reduce((total, item) => {
return total + getFileSize(item)
}, 0)
}
// 计算总大小(格式化)
const getTotalSize = () => {
return formatFileSize(getTotalSizeBytes())
}
// 监听 visible 变化,重置文件列表
watch(() => props.visible, (newVal) => {
@@ -121,12 +139,22 @@ watch(() => props.visible, (newVal) => {
// 上传前处理
const handleBeforeUpload = (file) => {
// 检查文件大小
if (file.size > MAX_FILE_SIZE) {
message.warning(`文件 ${file.name} 超过 100MB已跳过`)
// 检查单个文件大小
if (file.size > MAX_SINGLE_FILE_SIZE) {
message.warning(`文件 ${file.name} 超过 300MB已跳过`)
return false
}
// 计算当前文件列表的总大小(包括新文件)
const currentTotalSize = getTotalSizeBytes()
const newTotalSize = currentTotalSize + file.size
// 检查总大小
if (newTotalSize > MAX_TOTAL_SIZE) {
message.warning(`文件总大小超过 300MB当前${formatFileSize(currentTotalSize)},新增:${formatFileSize(file.size)}),已跳过`)
return false
}
// 检查是否已存在相同文件
const exists = fileList.value.some(item => {
const itemName = getFileName(item)
@@ -137,7 +165,7 @@ const handleBeforeUpload = (file) => {
message.warning(`文件 ${file.name} 已存在,已跳过`)
return false
}
return false // 阻止自动上传
}
@@ -197,12 +225,18 @@ const handleConfirm = () => {
return
}
// 检查总大小
if (getTotalSizeBytes() > MAX_TOTAL_SIZE) {
message.warning(`文件总大小超过 300MB 限制,请移除部分文件`)
return
}
// 提取文件对象和对应的封面
const filesWithCover = fileList.value
.map(item => {
const fileObj = item.file || item.originFileObj || item
if (!(fileObj instanceof File)) return null
const fileKey = item.uid || fileObj.name
return {
file: fileObj,
@@ -254,6 +288,9 @@ const handleCancel = () => {
font-weight: 500;
margin-bottom: 12px;
color: var(--color-text);
display: flex;
align-items: center;
flex-wrap: wrap;
}
.upload-file-items {
@@ -267,7 +304,6 @@ const handleCancel = () => {
gap: 12px;
padding: 8px;
border-radius: 4px;
transition: background 0.2s;
}
.upload-file-item:hover {

View File

@@ -61,8 +61,8 @@ const routes = [
path: 'system',
name: '系统',
children: [
// { path: '', redirect: '/system/style-settings' },
// { path: 'style-settings', name: '风格设置', component: () => import('../views/system/StyleSettings.vue') },
{ path: '', redirect: '/system/style-settings' },
{ path: 'style-settings', name: '风格设置', component: () => import('../views/system/style-settings/index.vue') },
{
path: 'task-management/:type',
name: '任务中心',

View File

@@ -1,31 +1,116 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import localforage from 'localforage'
import { UserPromptApi } from '@/api/userPrompt'
export const usePromptStore = defineStore('prompt', () => {
// 存储当前选中的提示词
const currentPrompt = ref('')
// 存储提示词相关的视频信息
const currentVideoInfo = ref(null)
// 存储提示词列表
const promptList = ref([])
const promptListLoading = ref(false)
const promptListError = ref(null)
// 设置提示词
function setPrompt(prompt, videoInfo = null) {
currentPrompt.value = prompt
currentVideoInfo.value = videoInfo
}
// 清空提示词
function clearPrompt() {
currentPrompt.value = ''
currentVideoInfo.value = null
}
// 加载提示词列表
async function loadPromptList(userId) {
if (!userId) {
console.warn('用户未登录,无法加载提示词')
return
}
// 如果已有数据且不在加载中,直接返回缓存数据
if (promptList.value.length > 0 && !promptListLoading.value) {
return promptList.value
}
promptListLoading.value = true
promptListError.value = null
try {
const response = await UserPromptApi.getUserPromptPage({
pageNo: 1,
pageSize: 100,
status: undefined
})
if (response?.data?.list) {
promptList.value = response.data.list
} else {
promptList.value = []
}
return promptList.value
} catch (error) {
console.error('加载提示词列表失败:', error)
promptListError.value = error
throw error
} finally {
promptListLoading.value = false
}
}
// 添加提示词到列表
function addPromptToList(prompt) {
const existingIndex = promptList.value.findIndex(p => p.id === prompt.id)
if (existingIndex >= 0) {
// 更新已存在的提示词
promptList.value[existingIndex] = prompt
} else {
// 添加新提示词
promptList.value.unshift(prompt)
}
}
// 从列表中删除提示词
function removePromptFromList(promptId) {
const index = promptList.value.findIndex(p => p.id === promptId)
if (index >= 0) {
promptList.value.splice(index, 1)
}
}
// 更新提示词
function updatePromptInList(updatedPrompt) {
const index = promptList.value.findIndex(p => p.id === updatedPrompt.id)
if (index >= 0) {
promptList.value[index] = updatedPrompt
}
}
// 刷新提示词列表
async function refreshPromptList(userId) {
promptList.value = [] // 清空缓存,强制重新加载
return await loadPromptList(userId)
}
return {
currentPrompt,
currentVideoInfo,
promptList,
promptListLoading,
promptListError,
setPrompt,
clearPrompt
clearPrompt,
loadPromptList,
addPromptToList,
removePromptFromList,
updatePromptInList,
refreshPromptList
}
}, {
persist: {
@@ -35,6 +120,6 @@ export const usePromptStore = defineStore('prompt', () => {
setItem: (key, value) => localforage.setItem(key, value),
removeItem: (key) => localforage.removeItem(key)
},
paths: ['currentPrompt', 'currentVideoInfo']
paths: ['currentPrompt', 'currentVideoInfo', 'promptList']
}
})

View File

@@ -9,6 +9,7 @@ import GmIcon from '@/components/icons/Icon.vue'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import GradientButton from '@/components/GradientButton.vue'
import PromptSelector from '@/components/PromptSelector.vue'
import { setJSON, getJSON } from '@/utils/storage'
import BasicLayout from '@/layouts/components/BasicLayout.vue'
@@ -38,28 +39,10 @@ const { getVoiceText } = useVoiceText()
// 提示词相关状态
const allPrompts = ref([])
const loadingPrompts = ref(false)
const showAllPromptsModal = ref(false)
const selectedPromptId = ref(null)
const promptSearchKeyword = ref('')
const DISPLAY_COUNT = 6 // 展示的提示词数量
// 计算属性:展示的部分提示词
const displayPrompts = computed(() => {
return allPrompts.value.slice(0, DISPLAY_COUNT)
})
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
const filteredPrompts = computed(() => {
if (!promptSearchKeyword.value.trim()) {
return allPrompts.value
}
const keyword = promptSearchKeyword.value.trim().toLowerCase()
return allPrompts.value.filter(p =>
p.name.toLowerCase().includes(keyword) ||
(p.content && p.content.toLowerCase().includes(keyword))
)
})
/**
* 加载用户提示词列表
* 从服务器获取当前用户的提示词,并按创建时间倒序排列
@@ -88,9 +71,6 @@ async function loadUserPrompts() {
const timeB = b.createTime ? new Date(b.createTime).getTime() : 0
return timeB - timeA
})
// 如果用户没有选择提示词,尝试恢复本地存储的选中项或默认选中第一个
await restoreOrSelectPrompt()
} else {
throw new Error(response?.msg || response?.message || '加载失败')
}
@@ -102,97 +82,17 @@ async function loadUserPrompts() {
}
}
/**
* 保存选中的提示词ID到本地存储
*/
async function saveSelectedPromptId(promptId) {
if (promptId) {
await setJSON('copywriting_selected_prompt_id', promptId)
}
}
/**
* 从本地存储恢复选中的提示词ID
*/
async function loadSelectedPromptId() {
try {
const savedId = await getJSON('copywriting_selected_prompt_id', null)
return savedId
} catch (error) {
console.error('加载保存的提示词ID失败:', error)
return null
}
}
/**
* 根据ID选中提示词
*/
async function selectPromptById(promptId) {
if (!promptId) return false
const prompt = allPrompts.value.find(p => p.id === promptId)
if (prompt && prompt.content) {
selectedPromptId.value = prompt.id
form.value.prompt = prompt.content
promptStore.setPrompt(prompt.content, prompt)
await saveSelectedPromptId(promptId)
return true
}
return false
}
/**
* 恢复或选中提示词
* 优先级:本地存储的选中项 > 第一个提示词
*/
async function restoreOrSelectPrompt() {
if (allPrompts.value.length === 0) {
return false
}
// 如果已经有选中项且内容存在,不需要重新选择
if (selectedPromptId.value && form.value.prompt) {
// 验证选中的提示词是否还在列表中
const currentPrompt = allPrompts.value.find(p => p.id === selectedPromptId.value)
if (currentPrompt && currentPrompt.content === form.value.prompt) {
return true // 已经正确选中,无需操作
}
}
// 尝试恢复本地存储的选中项
const savedPromptId = await loadSelectedPromptId()
if (savedPromptId) {
const restored = await selectPromptById(savedPromptId)
if (restored) {
return true // 成功恢复保存的选中项
}
}
// 如果没有保存的选中项或恢复失败,则选中第一个
const firstPrompt = allPrompts.value[0]
if (firstPrompt?.content) {
selectedPromptId.value = firstPrompt.id
form.value.prompt = firstPrompt.content
promptStore.setPrompt(firstPrompt.content, firstPrompt)
await saveSelectedPromptId(firstPrompt.id)
return true
}
return false
}
// 选择提示词
async function selectPrompt(prompt) {
// 处理提示词选择
function handlePromptChange(prompt) {
if (!prompt || !prompt.content) {
message.warning('提示词内容为空')
return
}
selectedPromptId.value = prompt.id
form.value.prompt = prompt.content
promptStore.setPrompt(prompt.content, prompt)
await saveSelectedPromptId(prompt.id)
showAllPromptsModal.value = false
}
@@ -217,7 +117,7 @@ async function waitForUserInfo() {
async function ensureUserInfoLoaded() {
const isLoggedIn = userStore.isLoggedIn
const hasNoUserId = !userStore.userId
if (isLoggedIn && hasNoUserId) {
try {
await userStore.fetchUserInfo()
@@ -239,13 +139,13 @@ async function initializePage() {
if (promptStore.currentPrompt) {
form.value.prompt = promptStore.currentPrompt
}
// 2. 等待用户信息初始化完成
await waitForUserInfo()
// 3. 确保用户信息已加载
await ensureUserInfoLoaded()
// 4. 加载提示词列表
await loadUserPrompts()
}
@@ -255,34 +155,11 @@ onMounted(() => {
initializePage()
})
/**
* keep-alive 激活时的处理
* 1. 如果有提示词列表,尝试恢复或选中提示词
* 2. 如果没有提示词列表但用户已登录,加载提示词列表
*/
onActivated(async () => {
// 如果已经有提示词列表,尝试恢复或选中提示词
if (allPrompts.value.length > 0) {
await restoreOrSelectPrompt()
}
// 如果提示词列表为空,但用户已登录,则尝试加载
else if (userStore.userId) {
await loadUserPrompts()
}
// 如果用户未登录,等待用户信息加载
else if (!userStore.userId && userStore.isLoggedIn) {
await ensureUserInfoLoaded()
if (userStore.userId) {
await loadUserPrompts()
}
}
})
// 监听 userId 变化:如果之前没有 userId现在有了则自动加载提示词
watch(() => userStore.userId, async (newUserId, oldUserId) => {
const userIdChanged = newUserId && !oldUserId
const hasNoPrompts = allPrompts.value.length === 0
if (userIdChanged && hasNoPrompts) {
await loadUserPrompts()
}
@@ -510,47 +387,25 @@ defineOptions({ name: 'ContentStyleCopywriting' })
<a-form :model="form" layout="vertical" class="form-container">
<a-form-item class="form-item">
<template #label>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>
选择提示词风格
<span class="form-tip-inline">从已保存的提示词中选择</span>
</span>
<a-space>
<a-button
size="small"
type="link"
@click="showAllPromptsModal = true"
:disabled="allPrompts.length === 0">
更多 ({{ allPrompts.length }})
</a-button>
</a-space>
</div>
<span>
选择提示词风格
<span class="form-tip-inline">从已保存的提示词中选择</span>
</span>
</template>
<!-- 提示词标签展示区域 -->
<div v-if="displayPrompts.length > 0" class="prompt-tags-container">
<div class="prompt-tags-grid">
<div
v-for="prompt in displayPrompts"
:key="prompt.id"
class="prompt-tag"
:class="{ 'prompt-tag-selected': selectedPromptId === prompt.id }"
@click="selectPrompt(prompt)">
<span class="prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
</div>
<!-- 使用 PromptSelector 组件 -->
<PromptSelector
v-model="selectedPromptId"
:prompts="allPrompts"
:loading="loadingPrompts"
:search-keyword="promptSearchKeyword"
@change="handlePromptChange"
@update:searchKeyword="promptSearchKeyword = $event"
/>
<!-- 空状态 -->
<div v-else-if="!loadingPrompts" class="prompt-empty">
<div style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div>
<!-- 加载状态 -->
<div v-else class="prompt-loading">
<a-spin size="small" />
<!-- 空状态提示 -->
<div v-if="!loadingPrompts && allPrompts.length === 0" class="prompt-empty" style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</a-form-item>
@@ -664,58 +519,6 @@ defineOptions({ name: 'ContentStyleCopywriting' })
</a-row>
</div>
<!-- 更多提示词弹窗 -->
<a-modal
v-model:open="showAllPromptsModal"
title="选择提示词"
:width="800"
:maskClosable="false"
>
<div class="all-prompts-modal">
<!-- 搜索框 -->
<a-input
v-model:value="promptSearchKeyword"
placeholder="搜索提示词名称或内容..."
allow-clear
style="margin-bottom: 16px;"
>
<template #prefix>
<GmIcon name="icon-search" :size="16" />
</template>
</a-input>
<!-- 提示词列表(标签形式) -->
<div v-if="filteredPrompts.length > 0" class="all-prompts-tags">
<div
v-for="prompt in filteredPrompts"
:key="prompt.id"
class="all-prompt-tag"
:class="{ 'all-prompt-tag-selected': selectedPromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-else style="text-align: center; padding: 40px; color: var(--color-text-secondary);">
没有找到匹配的提示词
</div>
</div>
<template #footer>
<a-space>
<a-button @click="showAllPromptsModal = false">取消</a-button>
<a-button
type="primary"
@click="loadUserPrompts"
:loading="loadingPrompts"
>
刷新列表
</a-button>
</a-space>
</template>
</a-modal>
</BasicLayout>
</template>

View File

@@ -57,6 +57,8 @@
v-model:open="modalVisible"
:title="isCreateMode ? '新建配音' : '编辑配音'"
:width="600"
ok-text="确定"
cancel-text="取消"
:confirm-loading="submitting"
@ok="handleSubmit"
@cancel="handleCancel"

View File

@@ -150,6 +150,7 @@
</template>
<template v-else>
<a-input
ref="nameInputRef"
v-model:value="editingDisplayName"
size="small"
@blur="handleSaveName(file)"
@@ -207,6 +208,7 @@
<MaterialUploadModal
v-model:visible="uploadModalVisible"
:group-id="selectedGroupId"
:uploading="uploadLoading"
@confirm="handleFileUpload"
/>
@@ -230,7 +232,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import { ref, reactive, onMounted, watch, nextTick } from 'vue';
import {
UploadOutlined,
SearchOutlined,
@@ -253,6 +255,7 @@ const totalFileCount = ref(0);
const searchKeyword = ref('');
const uploadModalVisible = ref(false);
const uploadLoading = ref(false);
const createGroupModalVisible = ref(false);
// Upload Hook
@@ -270,6 +273,7 @@ const selectedFileIds = ref([]);
// 编辑状态
const editingFileId = ref(null);
const editingDisplayName = ref('');
const nameInputRef = ref(null);
// 分页信息
const pagination = reactive({
@@ -409,6 +413,7 @@ const handleOpenUploadModal = () => {
const handleFileUpload = async (filesWithCover, category, groupId) => {
try {
uploadLoading.value = true;
// 上传每个文件
for (const fileWithCover of filesWithCover) {
await upload(fileWithCover.file, {
@@ -433,6 +438,8 @@ const handleFileUpload = async (filesWithCover, category, groupId) => {
} catch (error) {
console.error("文件上传失败:", error);
message.error("文件上传失败: " + (error.message || "未知错误"));
} finally {
uploadLoading.value = false;
}
};
@@ -525,17 +532,21 @@ const getFileTypeText = (fileName) => {
return ext ? `${ext.toLowerCase()}` : '';
};
const handleEditName = (file) => {
const handleEditName = async (file) => {
editingFileId.value = file.id;
editingDisplayName.value = file.displayName || file.fileName;
// 延迟聚焦,确保输入框已渲染
setTimeout(() => {
const input = document.querySelector('.ant-input:focus');
// 使用 nextTick 确保 DOM 更新后再聚焦
await nextTick();
// 查找当前编辑文件的输入框
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`);
if (nameElement) {
const input = nameElement.querySelector('input');
if (input) {
input.focus();
input.select();
}
}, 50);
}
};
const handleSaveName = async (file) => {
@@ -708,11 +719,6 @@ onMounted(() => {
&--active {
background: #e6f7ff;
color: #1890ff;
// 移除active状态下的hover效果
&:hover {
background: #e6f7ff;
}
}
&__content {
@@ -798,9 +804,9 @@ onMounted(() => {
}
.ant-btn {
transition: all 0.2s ease;
font-weight: 500;
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.2);
@@ -940,15 +946,29 @@ onMounted(() => {
.ant-input {
font-size: 15px;
font-weight: 600;
height: 32px;
line-height: 30px;
border-radius: 6px;
height: 20px; /* 与文字高度一致 */
line-height: 1.4;
border-radius: 4px;
border-color: #3B82F6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
padding: 0 4px; /* 减少内边距 */
margin: 0; /* 移除margin避免布局问题 */
display: block; /* 确保输入框正常显示 */
&:focus {
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
}
/* 移除默认的内边距和高度 */
input {
height: 100%;
padding: 0;
margin: 0;
line-height: inherit;
font-size: inherit;
font-weight: inherit;
color: inherit;
}
}
}
@@ -1014,67 +1034,7 @@ onMounted(() => {
}
}
// 键盘导航支持
.material-item:focus-within {
outline: 2px solid #3B82F6;
outline-offset: 2px;
}
// 响应式设计
@media (max-width: 768px) {
.material-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.material-content__search {
flex-direction: column;
align-items: stretch;
gap: 12px;
.material-content__actions {
margin-left: 0;
flex-wrap: wrap;
}
}
.material-item {
&__preview {
height: 120px;
}
}
}
@media (max-width: 480px) {
.material-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.material-content {
margin: 8px;
border-radius: 8px;
&__search,
&__list,
&__pagination {
padding: 16px;
}
}
}
// 无障碍支持
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
// 批量删除确认弹窗样式
:deep(.batch-delete-modal) {

View File

@@ -1,515 +0,0 @@
<script setup>
import { ref, onMounted, reactive, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { UserPromptApi } from '@/api/userPrompt'
import dayjs from 'dayjs'
// 表格数据
const dataSource = ref([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
})
// 搜索表单
const searchForm = reactive({
name: '',
category: '',
status: undefined,
})
// 编辑弹窗
const editModalVisible = ref(false)
const editForm = reactive({
id: null,
name: '',
content: '',
category: '',
status: 1,
})
const editFormRef = ref(null)
// 表格列定义
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
ellipsis: true,
},
{
title: '内容',
dataIndex: 'content',
key: 'content',
ellipsis: true,
customRender: ({ text }) => {
if (!text) return '-'
const preview = text.length > 100 ? text.substring(0, 100) + '...' : text
return h('span', { title: text }, preview)
},
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
customRender: ({ text }) => {
return h('span', {
style: {
color: text === 1 ? 'var(--color-green-500)' : 'var(--color-red-500)',
},
}, text === 1 ? '启用' : '禁用')
},
},
{
title: '使用次数',
dataIndex: 'useCount',
key: 'useCount',
width: 120,
sorter: (a, b) => (a.useCount || 0) - (b.useCount || 0),
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
customRender: ({ text }) => {
return text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'
},
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
customRender: ({ record }) => {
return h('div', { class: 'action-buttons' }, [
h('a-button', {
type: 'link',
size: 'small',
class: 'action-btn action-btn-edit',
onClick: () => handleEdit(record),
}, [
h(EditOutlined),
'编辑',
]),
h('a-button', {
type: 'link',
size: 'small',
danger: true,
class: 'action-btn action-btn-delete',
onClick: () => handleDelete(record),
}, [
h(DeleteOutlined),
'删除',
]),
])
},
},
]
// 加载数据
async function loadData() {
loading.value = true
try {
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
category: searchForm.category || undefined,
status: searchForm.status,
}
const response = await UserPromptApi.getUserPromptPage(params)
if (response && (response.code === 0 || response.code === 200)) {
dataSource.value = response.data?.list || []
pagination.total = response.data?.total || 0
} else {
throw new Error(response?.msg || response?.message || '加载失败')
}
} catch (error) {
console.error('加载提示词列表失败:', error)
message.error(error?.message || '加载失败,请稍后重试')
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.current = 1
loadData()
}
// 重置搜索
function handleReset() {
searchForm.name = ''
searchForm.category = ''
searchForm.status = undefined
pagination.current = 1
loadData()
}
// 新增
function handleAdd() {
resetEditForm()
editModalVisible.value = true
}
// 编辑
function handleEdit(record) {
editForm.id = record.id
editForm.name = record.name || ''
editForm.content = record.content || ''
editForm.category = record.category || ''
editForm.status = record.status !== null && record.status !== undefined ? record.status : 1
editModalVisible.value = true
}
// 表单重置
function resetEditForm() {
editForm.id = null
editForm.name = ''
editForm.content = ''
editForm.category = ''
editForm.status = 1
}
// 通用API调用
async function apiCall(apiFunc, param, successMessage, isDelete = false) {
loading.value = true
try {
const response = await apiFunc(param)
if (response && (response.code === 0 || response.code === 200)) {
message.success(successMessage)
if (!isDelete) {
editModalVisible.value = false
}
loadData()
return true
} else {
throw new Error(response?.msg || response?.message || '操作失败')
}
} catch (error) {
console.error('API调用失败:', error)
message.error(error?.message || '操作失败,请稍后重试')
return false
} finally {
loading.value = false
}
}
// 保存(新增/编辑)
async function handleSave() {
try {
await editFormRef.value.validate()
} catch (error) {
console.error('表单验证失败:', error)
return
}
const payload = {
name: editForm.name.trim(),
content: editForm.content.trim(),
category: editForm.category.trim() || null,
status: editForm.status,
}
if (editForm.id) {
payload.id = editForm.id
await apiCall(
(data) => UserPromptApi.updateUserPrompt(data),
payload,
'更新成功'
)
} else {
await apiCall(
(data) => UserPromptApi.createUserPrompt(data),
payload,
'创建成功'
)
}
}
// 删除
function handleDelete(record) {
Modal.confirm({
title: '确认删除',
content: `确定要删除提示词"${record.name}"吗?`,
onOk: async () => {
await apiCall(
(id) => UserPromptApi.deleteUserPrompt(id),
record.id,
'删除成功',
true
)
},
})
}
// 分页变化
function handleTableChange(pag) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadData()
}
// 初始化
onMounted(() => {
loadData()
})
</script>
<template>
<div class="style-settings-page">
<div class="page-header">
<h1 class="page-title">风格设置</h1>
<p class="page-description">管理您的提示词模板用于内容创作和风格定制</p>
</div>
<div class="page-content">
<!-- 搜索表单 -->
<div class="search-card card">
<a-form :model="searchForm" layout="inline" class="search-form">
<a-form-item label="名称">
<a-input
v-model:value="searchForm.name"
placeholder="请输入提示词名称"
allow-clear
style="width: 200px"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="分类">
<a-input
v-model:value="searchForm.category"
placeholder="请输入分类"
allow-clear
style="width: 200px"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="1">启用</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">搜索</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<a-button type="primary" @click="handleAdd">
<template #icon>
<PlusOutlined />
</template>
新增提示词
</a-button>
</div>
<!-- 表格 -->
<div class="table-card card">
<a-table
:dataSource="dataSource"
:columns="columns"
:loading="loading"
:pagination="pagination"
rowKey="id"
@change="handleTableChange"
:scroll="{ x: 1200 }"
/>
</div>
</div>
<!-- 编辑弹窗 -->
<a-modal
v-model:visible="editModalVisible"
:title="editForm.id ? '编辑提示词' : '新增提示词'"
width="800px"
@ok="handleSave"
@cancel="editModalVisible = false"
>
<a-form
ref="editFormRef"
:model="editForm"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item
label="名称"
name="name"
:rules="[{ required: true, message: '请输入提示词名称' }]"
>
<a-input v-model:value="editForm.name" placeholder="请输入提示词名称" />
</a-form-item>
<a-form-item
label="内容"
name="content"
:rules="[{ required: true, message: '请输入提示词内容' }]"
>
<a-textarea
v-model:value="editForm.content"
placeholder="请输入提示词内容"
:rows="8"
/>
</a-form-item>
<a-form-item label="分类" name="category">
<a-input v-model:value="editForm.category" placeholder="请输入分类(可选)" />
</a-form-item>
<a-form-item
label="状态"
name="status"
:rules="[{ required: true, message: '请选择状态' }]"
>
<a-radio-group v-model:value="editForm.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.style-settings-page {
padding: 24px;
min-height: calc(100vh - 70px);
background: var(--color-bg);
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: var(--color-text);
margin: 0 0 8px 0;
}
.page-description {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0;
}
.page-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
padding: 16px;
}
.search-card {
padding: 16px;
}
.search-form {
margin: 0;
}
.action-bar {
display: flex;
justify-content: flex-end;
}
.table-card {
padding: 0;
overflow: hidden;
}
:deep(.ant-table) {
background: transparent;
}
:deep(.ant-table-thead > tr > th) {
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
color: var(--color-text);
}
:deep(.ant-table-tbody > tr > td) {
border-bottom: 1px solid var(--color-border);
color: var(--color-text);
}
:deep(.ant-table-tbody > tr:hover > td) {
background: var(--color-bg);
}
:deep(.ant-pagination) {
margin: 16px;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
/* 使用 :deep() 穿透 Ant Design 的样式 */
:deep(.action-btn) {
transition: all 0.2s ease;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
}
:deep(.action-btn-edit:hover),
:deep(.action-btn-edit:hover .anticon) {
background: rgba(59, 130, 246, 0.1) !important;
color: var(--color-primary) !important;
}
:deep(.action-btn-delete:hover),
:deep(.action-btn-delete:hover .anticon) {
background: rgba(59, 130, 246, 0.1) !important;
color: var(--color-primary) !important;
}
:deep(.action-btn:hover) {
transform: translateY(-1px);
}
</style>

View File

@@ -0,0 +1,600 @@
<script setup>
import { ref, onMounted, reactive, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { UserPromptApi } from '@/api/userPrompt'
import { usePromptStore } from '@/stores/prompt'
import dayjs from 'dayjs'
// 表格数据
const dataSource = ref([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
})
// 搜索表单
const searchForm = reactive({
name: '',
status: undefined,
})
// 编辑弹窗
const editModalVisible = ref(false)
const editForm = reactive({
id: null,
name: '',
content: '',
status: 1,
})
const editFormRef = ref(null)
// Store
const promptStore = usePromptStore()
// 表格列定义
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
ellipsis: true,
},
{
title: '内容',
dataIndex: 'content',
key: 'content',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
},
]
// 加载数据
async function loadData() {
loading.value = true
try {
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
status: searchForm.status,
}
const response = await UserPromptApi.getUserPromptPage(params)
if (response && (response.code === 0 || response.code === 200)) {
dataSource.value = response.data?.list || []
pagination.total = response.data?.total || 0
} else {
throw new Error(response?.msg || response?.message || '加载失败')
}
} catch (error) {
console.error('加载提示词列表失败:', error)
message.error(error?.message || '加载失败,请稍后重试')
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.current = 1
loadData()
}
// 重置搜索
function handleReset() {
searchForm.name = ''
searchForm.status = undefined
pagination.current = 1
loadData()
}
// 新增
function handleAdd() {
resetEditForm()
editModalVisible.value = true
}
// 编辑
function handleEdit(record) {
editForm.id = record.id
editForm.name = record.name || ''
editForm.content = record.content || ''
editForm.status = record.status !== null && record.status !== undefined ? record.status : 1
editModalVisible.value = true
}
// 表单重置
function resetEditForm() {
editForm.id = null
editForm.name = ''
editForm.content = ''
editForm.status = 1
}
// 通用API调用
async function apiCall(apiFunc, param, successMessage, isDelete = false) {
loading.value = true
try {
const response = await apiFunc(param)
if (response && (response.code === 0 || response.code === 200)) {
message.success(successMessage)
if (!isDelete) {
editModalVisible.value = false
}
loadData()
return true
} else {
throw new Error(response?.msg || response?.message || '操作失败')
}
} catch (error) {
console.error('API调用失败:', error)
message.error(error?.message || '操作失败,请稍后重试')
return false
} finally {
loading.value = false
}
}
// 保存(新增/编辑)
async function handleSave() {
try {
await editFormRef.value.validate()
} catch (error) {
console.error('表单验证失败:', error)
return
}
const payload = {
name: editForm.name.trim(),
content: editForm.content.trim(),
status: editForm.status,
}
if (editForm.id) {
payload.id = editForm.id
const result = await apiCall(
(data) => UserPromptApi.updateUserPrompt(data),
payload,
'更新成功'
)
// 同步更新 store
if (result && result.data) {
promptStore.updatePromptInList(result.data)
}
} else {
const result = await apiCall(
(data) => UserPromptApi.createUserPrompt(data),
payload,
'创建成功'
)
// 同步更新 store
if (result && result.data) {
promptStore.addPromptToList(result.data)
}
}
}
// 删除
function handleDelete(record) {
Modal.confirm({
title: '确认删除',
content: `确定要删除提示词"${record.name}"吗?`,
onOk: async () => {
const result = await apiCall(
(id) => UserPromptApi.deleteUserPrompt(id),
record.id,
'删除成功',
true
)
// 同步更新 store
if (result) {
promptStore.removePromptFromList(record.id)
}
},
})
}
// 分页变化
function handleTableChange(pag) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadData()
}
// 初始化
onMounted(() => {
loadData()
})
</script>
<template>
<div class="style-settings-page">
<!-- 筛选条件 -->
<div class="style-settings-page__filters">
<a-space :size="16">
<a-input
v-model:value="searchForm.name"
class="filter-input"
placeholder="搜索提示词名称"
allow-clear
@press-enter="handleSearch"
>
<template #prefix>
<EditOutlined />
</template>
</a-input>
<a-select
v-model:value="searchForm.status"
class="filter-select"
placeholder="状态筛选"
allow-clear
@change="handleSearch"
>
<a-select-option :value="1">启用</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
<a-button type="primary" class="filter-button" @click="handleSearch">
搜索
</a-button>
<a-button class="filter-button" @click="handleReset">
重置
</a-button>
<a-button type="primary" class="filter-button add-btn" @click="handleAdd">
<template #icon>
<PlusOutlined />
</template>
新增提示词
</a-button>
</a-space>
</div>
<!-- 任务列表 -->
<div class="style-settings-page__content">
<a-spin :spinning="loading" tip="加载中...">
<a-table
:data-source="dataSource"
:columns="columns"
:row-key="record => record.id"
:pagination="pagination"
@change="handleTableChange"
:scroll="{ x: 1200 }"
>
<!-- 名称列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="name-cell">
<strong>{{ record.name }}</strong>
</div>
</template>
<!-- 内容列 -->
<template v-else-if="column.key === 'content'">
<div class="content-cell" :title="record.content">
{{ record.content ? (record.content.length > 100 ? record.content.substring(0, 100) + '...' : record.content) : '-' }}
</div>
</template>
<!-- 状态列 -->
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<!-- 创建时间列 -->
<template v-else-if="column.key === 'createTime'">
{{ record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
</template>
<!-- 操作列 -->
<template v-else-if="column.key === 'action'">
<a-space>
<a-button
type="link"
size="small"
@click="handleEdit(record)"
class="action-btn-edit"
>
<template #icon>
<EditOutlined />
</template>
编辑
</a-button>
<a-button
type="link"
size="small"
@click="handleDelete(record)"
class="action-btn-delete"
>
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-spin>
</div>
<!-- 编辑弹窗 -->
<a-modal
v-model:visible="editModalVisible"
:title="editForm.id ? '编辑提示词' : '新增提示词'"
width="800px"
:footer="null"
class="edit-modal"
>
<a-form
ref="editFormRef"
:model="editForm"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item
label="名称"
name="name"
:rules="[{ required: true, message: '请输入提示词名称' }]"
>
<a-input v-model:value="editForm.name" placeholder="请输入提示词名称" />
</a-form-item>
<a-form-item
label="内容"
name="content"
:rules="[{ required: true, message: '请输入提示词内容' }]"
>
<a-textarea
v-model:value="editForm.content"
placeholder="请输入提示词内容"
:rows="8"
/>
</a-form-item>
<a-form-item
label="状态"
name="status"
:rules="[{ required: true, message: '请选择状态' }]"
>
<a-radio-group v-model:value="editForm.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
<!-- 弹窗底部按钮 -->
<div class="modal-footer">
<a-space :size="12" class="button-container">
<a-button @click="editModalVisible = false" class="footer-btn">
取消
</a-button>
<a-button type="primary" @click="handleSave" :loading="loading" class="footer-btn">
确定
</a-button>
</a-space>
</div>
</a-modal>
</div>
</template>
<style scoped lang="less">
.style-settings-page {
padding: var(--space-3);
height: 100%;
display: flex;
flex-direction: column;
gap: var(--space-3);
&__filters {
padding: var(--space-3);
background: var(--color-surface);
border-radius: var(--radius-card);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
.filter-input {
width: 200px;
@media (max-width: 1199px) {
width: 160px;
}
@media (max-width: 767px) {
width: 100%;
}
}
.filter-select {
width: 140px;
@media (max-width: 1199px) {
width: 120px;
}
@media (max-width: 767px) {
width: 100%;
}
}
.filter-button {
min-width: 80px;
@media (max-width: 767px) {
min-width: auto;
}
&.add-btn {
margin-left: auto;
@media (max-width: 767px) {
margin-left: 0;
width: 100%;
}
}
}
}
&__content {
flex: 1;
overflow: auto;
background: var(--color-surface);
border-radius: var(--radius-card);
padding: var(--space-3);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
}
/* 表格单元格样式 */
.name-cell {
font-weight: 500;
color: var(--color-text);
}
.content-cell {
color: var(--color-text-secondary);
font-size: 13px;
line-height: 1.5;
}
/* 操作按钮样式 */
.action-btn-edit {
color: var(--color-primary);
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
}
.action-btn-delete {
color: var(--color-error);
&:hover {
color: #dc2626;
}
}
/* 表格样式 */
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-thead > tr > th) {
background: var(--color-bg-2);
font-weight: 600;
}
/* 编辑弹窗样式 */
.edit-modal {
:deep(.ant-modal-body) {
padding: var(--space-3);
}
:deep(.ant-form-item-label > label) {
color: var(--color-text);
font-weight: 500;
}
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-select),
:deep(.ant-radio-group) {
border-radius: var(--radius-tag);
}
:deep(.ant-input:focus),
:deep(.ant-input-focused) {
border-color: var(--color-border-focus);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.modal-footer {
display: flex;
justify-content: flex-end;
padding-top: var(--space-3);
border-top: 1px solid var(--color-border);
margin-top: var(--space-3);
.button-container {
display: flex;
align-items: center;
}
.footer-btn {
min-width: 88px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
}
}
/* 响应式优化 */
@media (max-width: 767px) {
.style-settings-page {
padding: var(--space-2);
&__filters {
padding: var(--space-2);
.ant-space {
width: 100%;
flex-wrap: wrap;
.ant-input,
.ant-select,
.ant-btn {
width: calc(50% - var(--space-1)) !important;
}
}
}
&__content {
padding: var(--space-2);
}
}
:deep(.ant-table-tbody > tr > td) {
padding: 8px 4px;
}
.action-btn-edit,
.action-btn-delete {
font-size: 12px;
padding: 0 4px;
}
}
</style>

View File

@@ -59,16 +59,7 @@
>
<template #action>
<a-space>
<a-button
size="small"
type="primary"
@click="handleBatchDownload"
>
<template #icon>
<DownloadOutlined />
</template>
批量下载
</a-button>
<a-popconfirm
title="确定要删除选中的任务吗?删除后无法恢复。"
@confirm="handleBatchDelete"
@@ -116,7 +107,6 @@
<!-- 进度列 -->
<template v-else-if="column.key === 'progress'">
<div style="min-width: 100px">
<div style="font-size: 12px; margin-bottom: 4px">{{ record.progress }}%</div>
<a-progress
:percent="record.progress"
:status="getProgressStatus(record.status)"
@@ -134,27 +124,33 @@
<!-- 操作列 -->
<template v-else-if="column.key === 'actions'">
<a-space>
<!-- 预览按钮 -->
<a-button
v-if="isStatus(record.status, 'success')"
type="primary"
type="link"
size="small"
@click="handlePreview(record)"
class="action-btn-preview"
>
<template #icon>
<PlayCircleOutlined />
</template>
预览
</a-button>
<!-- 下载按钮 -->
<a-button
v-if="isStatus(record.status, 'success')"
type="link"
size="small"
@click="handleDownload(record)"
class="action-btn-download"
>
<template #icon>
<DownloadOutlined />
</template>
下载
</a-button>
<!-- 取消按钮 -->
<a-button
v-if="isStatus(record.status, 'running')"
size="small"
@@ -167,7 +163,7 @@
title="确定删除这个任务吗删除后无法恢复"
@confirm="() => handleDelete(record.id)"
>
<a-button size="small" danger>删除</a-button>
<a-button size="small" type="link" class="action-btn-delete">删除</a-button>
</a-popconfirm>
</a-space>
</template>
@@ -309,7 +305,7 @@ const columns = [
{
title: '操作',
key: 'actions',
width: 280,
width: 240,
fixed: 'right'
}
]
@@ -443,7 +439,7 @@ onMounted(() => {
<style scoped lang="less">
.digital-human-task-page {
padding: var(--space-3);
padding: 0 var(--space-3);
height: 100%;
display: flex;
flex-direction: column;
@@ -458,35 +454,12 @@ onMounted(() => {
.filter-select,
.filter-input {
width: 200px;
@media (max-width: 1199px) {
width: 160px;
}
@media (max-width: 767px) {
width: 100%;
}
}
.filter-date-picker {
width: 280px;
@media (max-width: 1199px) {
width: 240px;
}
@media (max-width: 767px) {
width: 100%;
}
}
.filter-button {
min-width: 80px;
@media (max-width: 767px) {
min-width: auto;
}
}
}
&__content {
@@ -529,6 +502,31 @@ onMounted(() => {
gap: var(--space-1);
}
/* 操作按钮样式 */
.action-btn-preview {
color: var(--color-primary);
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
}
.action-btn-download {
color: var(--color-success);
&:hover {
color: #059669;
}
}
.action-btn-delete {
color: var(--color-error);
&:hover {
color: #dc2626;
}
}
/* 文本截断 */
.text-ellipsis {
display: inline-block;
@@ -620,34 +618,5 @@ onMounted(() => {
font-weight: 600;
}
/* 响应式优化 */
@media (max-width: 767px) {
.digital-human-task-page {
padding: var(--space-2);
&__filters {
padding: var(--space-2);
.ant-space {
width: 100%;
flex-wrap: wrap;
.ant-select,
.ant-input,
.ant-picker,
.ant-btn {
width: calc(50% - var(--space-1)) !important;
}
}
}
&__content {
padding: var(--space-2);
}
}
.batch-actions {
margin-bottom: var(--space-2);
}
}
/* 桌面端样式优化 */
</style>

View File

@@ -71,24 +71,11 @@
<!-- 状态列 -->
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
<a-tag :color="getStatusColor(record.status)" class="status-tag">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 进度列 -->
<template v-else-if="column.key === 'progress'">
<div style="min-width: 100px">
<div style="font-size: 12px; margin-bottom: 4px">{{ record.progress }}%</div>
<a-progress
:percent="record.progress"
:status="getProgressStatus(record.status)"
size="small"
:show-info="false"
/>
</div>
</template>
<!-- 创建时间列 -->
<template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
@@ -107,9 +94,38 @@
<span v-else>-</span>
</template>
<!-- 操作列 -->
<!-- 操作列 (增强版:预览+下载+其他操作) -->
<template v-else-if="column.key === 'actions'">
<a-space>
<!-- 预览按钮 -->
<a-button
v-if="isStatus(record.status, 'success') && record.outputUrls && record.outputUrls.length > 0"
type="link"
size="small"
@click="openPreviewModal(record)"
class="action-btn-preview"
>
<template #icon>
<PlayCircleOutlined />
</template>
预览
</a-button>
<!-- 下载按钮 -->
<a-button
v-if="isStatus(record.status, 'success') && record.outputUrls && record.outputUrls.length > 0"
type="link"
size="small"
@click="handleDownload(record)"
class="action-btn-download"
>
<template #icon>
<DownloadOutlined />
</template>
下载
</a-button>
<!-- 取消按钮 -->
<a-button
v-if="isStatus(record.status, 'running')"
size="small"
@@ -117,6 +133,8 @@
>
取消
</a-button>
<!-- 重试按钮 -->
<a-button
v-if="isStatus(record.status, 'failed')"
size="small"
@@ -124,17 +142,19 @@
>
重试
</a-button>
<!-- 删除按钮 -->
<a-popconfirm
title="确定删除这个任务吗删除后无法恢复"
@confirm="() => handleDelete(record.id)"
>
<a-button size="small" danger>删除</a-button>
<a-button size="small" type="link" class="action-btn-delete">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<!-- 展开行内容 -->
<!-- 展开行内容 (优化版) -->
<template #expandedRowRender="{ record }">
<div class="expanded-content">
<!-- 任务详情 -->
@@ -143,17 +163,24 @@
<p>{{ record.text }}</p>
</div>
<!-- 生成结果 -->
<div v-if="record.outputUrls && record.outputUrls.length > 0" class="task-results">
<div class="result-header">
<strong>生成结果:</strong>
<span class="result-count">{{ record.outputUrls.length }} 个视频</span>
</div>
<div class="result-list">
<div
v-for="(url, index) in record.outputUrls"
:key="index"
@click="handleDownloadSignedUrl(record.id, index)"
class="result-item"
>
<a-button
v-if="isStatus(record.status, 'success')"
type="link"
size="small"
@click="handlePreviewSingle(record, index)"
class="result-preview-btn"
>
<PlayCircleOutlined />
视频 {{ index + 1 }}
@@ -162,6 +189,8 @@
v-if="isStatus(record.status, 'success')"
type="link"
size="small"
@click="handleDownloadSingle(record.id, index)"
class="result-download-btn"
>
<DownloadOutlined />
</a-button>
@@ -185,6 +214,30 @@
</a-table>
</a-spin>
</div>
<!-- 预览模态框 -->
<a-modal
v-model:open="previewVisible"
:title="previewTitle"
width="800px"
:footer="null"
:centered="true"
class="preview-modal"
>
<div v-if="previewUrl" class="preview-container">
<video
:src="previewUrl"
controls
autoplay
style="width: 100%; max-height: 600px; border-radius: 8px;"
>
您的浏览器不支持视频播放
</video>
</div>
<div v-else class="preview-loading">
<a-spin size="large" tip="正在加载预览..." />
</div>
</a-modal>
</div>
</template>
@@ -219,17 +272,77 @@ const {
handleDelete,
handleCancel,
handleRetry,
handlePreview,
handleBatchDownload
} = useTaskOperations(
{
deleteApi: MixTaskService.deleteTask,
cancelApi: MixTaskService.cancelTask,
retryApi: MixTaskService.retryTask
retryApi: MixTaskService.retryTask,
getSignedUrlsApi: MixTaskService.getSignedUrls
},
fetchList
)
// 预览相关状态
const previewVisible = ref(false)
const previewUrl = ref('')
const previewTitle = ref('')
// 预览单个视频
const handlePreviewSingle = async (record, index) => {
try {
previewTitle.value = `${record.title} - 视频 ${index + 1}`
previewVisible.value = true
previewUrl.value = ''
// 获取签名URL
const res = await MixTaskService.getSignedUrls(record.id)
if (res.code === 0 && res.data && res.data[index]) {
previewUrl.value = res.data[index]
} else {
console.warn('获取预览链接失败')
}
} catch (error) {
console.error('获取预览链接失败:', error)
}
}
// 下载单个视频
const handleDownloadSingle = async (taskId, index) => {
try {
const res = await MixTaskService.getSignedUrls(taskId)
if (res.code === 0 && res.data && res.data[index]) {
const link = document.createElement('a')
link.href = res.data[index]
link.download = `video_${taskId}_${index + 1}.mp4`
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} else {
console.warn('获取下载链接失败')
}
} catch (error) {
console.error('获取下载链接失败:', error)
}
}
// 预览任务(主列表)
const openPreviewModal = async (record) => {
await handlePreviewSingle(record, 0)
}
// 下载任务
const handleDownload = async (record) => {
if (record.outputUrls && record.outputUrls.length > 0) {
await handleBatchDownload(
[],
MixTaskService.getSignedUrls,
record.id
)
}
}
// 使用轮询 Composable
useTaskPolling(MixTaskService.getTaskPage, {
onTaskUpdate: () => {
@@ -267,12 +380,6 @@ const columns = [
key: 'status',
width: 100
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
width: 150
},
{
title: '生成结果',
dataIndex: 'outputUrls',
@@ -340,34 +447,8 @@ const isStatus = (status, targetStatus) => {
return status === targetStatus || status === targetStatus.toUpperCase()
}
// 下载单个视频使用签名URL
const handleDownloadSignedUrl = async (taskId, index) => {
try {
const res = await MixTaskService.getSignedUrls(taskId)
if (res.code === 0 && res.data && res.data[index]) {
const link = document.createElement('a')
link.href = res.data[index]
link.download = `video_${taskId}_${index + 1}`
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} else {
console.warn('获取下载链接失败')
}
} catch (error) {
console.error('获取下载链接失败:', error)
}
}
// 批量下载所有视频
const handleDownloadAll = async (taskId) => {
handleBatchDownload(
[],
MixTaskService.getSignedUrls,
taskId
)
}
// 删除未使用的方法
// handleDownloadSignedUrl 和 handleDownloadAll 已被移除
// 初始化
onMounted(() => {
@@ -377,7 +458,7 @@ onMounted(() => {
<style scoped lang="less">
.mix-task-page {
padding: var(--space-3);
padding: 0 var(--space-3);
height: 100%;
display: flex;
flex-direction: column;
@@ -392,35 +473,12 @@ onMounted(() => {
.filter-select,
.filter-input {
width: 200px;
@media (max-width: 1199px) {
width: 160px;
}
@media (max-width: 767px) {
width: 100%;
}
}
.filter-date-picker {
width: 280px;
@media (max-width: 1199px) {
width: 240px;
}
@media (max-width: 767px) {
width: 100%;
}
}
.filter-button {
min-width: 80px;
@media (max-width: 767px) {
min-width: auto;
}
}
}
&__content {
@@ -440,6 +498,31 @@ onMounted(() => {
gap: var(--space-1);
}
/* 操作按钮样式 */
.action-btn-preview {
color: var(--color-primary);
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
}
.action-btn-download {
color: var(--color-success);
&:hover {
color: #059669;
}
}
.action-btn-delete {
color: var(--color-error);
&:hover {
color: #dc2626;
}
}
/* 展开内容 */
.expanded-content {
padding: var(--space-3);
@@ -463,6 +546,18 @@ onMounted(() => {
.task-results {
margin-bottom: var(--space-3);
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-2);
.result-count {
font-size: 12px;
color: var(--color-text-3, #8c8c8c);
}
}
.result-list {
display: flex;
flex-wrap: wrap;
@@ -482,6 +577,26 @@ onMounted(() => {
&:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.result-preview-btn {
color: var(--color-primary);
padding: 0;
height: auto;
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
}
.result-download-btn {
color: var(--color-success);
padding: 0;
height: auto;
&:hover {
color: #059669;
}
}
}
}
}
@@ -510,30 +625,26 @@ onMounted(() => {
font-weight: 600;
}
/* 响应式优化 */
@media (max-width: 767px) {
.mix-task-page {
padding: var(--space-2);
&__filters {
padding: var(--space-2);
.ant-space {
width: 100%;
flex-wrap: wrap;
.ant-select,
.ant-input,
.ant-picker,
.ant-btn {
width: calc(50% - var(--space-1)) !important;
}
}
}
&__content {
padding: var(--space-2);
}
/* 预览模态框样式 */
.preview-modal {
:deep(.ant-modal-body) {
padding: var(--space-3);
}
}
.preview-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.preview-loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
/* 桌面端样式优化 */
</style>

View File

@@ -6,6 +6,7 @@ import { CommonService } from '@/api/common'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import GradientButton from '@/components/GradientButton.vue'
import PromptSelector from '@/components/PromptSelector.vue'
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
defineOptions({ name: 'ForecastView' })
@@ -42,22 +43,9 @@ const topicDetails = reactive({
// 提示词相关
const allPrompts = ref([])
const loadingPrompts = ref(false)
const showAllPromptsModal = ref(false)
const promptSearchKeyword = ref('')
const DISPLAY_COUNT = 6
// 计算属性
const displayPrompts = computed(() => allPrompts.value.slice(0, DISPLAY_COUNT))
const filteredPrompts = computed(() => {
if (!promptSearchKeyword.value.trim()) return allPrompts.value
const keyword = promptSearchKeyword.value.trim().toLowerCase()
return allPrompts.value.filter(p =>
p.name.toLowerCase().includes(keyword) ||
(p.content && p.content.toLowerCase().includes(keyword))
)
})
// 工具函数
const formatNumber = (num) => {
if (!num) return '0'
@@ -101,12 +89,6 @@ async function loadUserPrompts() {
if (response?.data?.list) {
allPrompts.value = response.data.list
// 自动选中第一个提示词
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
const firstPrompt = allPrompts.value[0]
topicDetails.stylePromptId = firstPrompt.id
topicDetails.stylePrompt = firstPrompt.content || ''
}
}
} catch (error) {
console.error('加载提示词失败:', error)
@@ -116,14 +98,14 @@ async function loadUserPrompts() {
}
}
function selectPrompt(prompt) {
// 处理提示词选择
function handlePromptChange(prompt) {
if (!prompt?.content) {
message.warning('提示词内容为空')
return
}
topicDetails.stylePromptId = prompt.id
topicDetails.stylePrompt = prompt.content
showAllPromptsModal.value = false
}
// 语音分析
@@ -649,44 +631,21 @@ onMounted(async () => {
<!-- 风格提示词 -->
<div>
<div class="form-label-wrapper">
<label class="form-label">风格提示词</label>
<a-button
v-if="allPrompts.length > DISPLAY_COUNT"
size="small"
type="link"
@click="showAllPromptsModal = true"
style="padding: 0; height: auto; font-size: 14px;"
>
更多 ({{ allPrompts.length }})
</a-button>
</div>
<!-- 提示词标签展示区域 -->
<div v-if="displayPrompts.length > 0" class="prompt-tags-container">
<div class="prompt-tags-grid">
<div
v-for="prompt in displayPrompts"
:key="prompt.id"
class="prompt-tag"
:class="{ 'prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="!loadingPrompts" class="prompt-empty">
<div style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div>
<!-- 加载状态 -->
<div v-else class="prompt-loading">
<a-spin size="small" />
<label class="form-label" style="display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: var(--color-text);">风格提示词</label>
<!-- 使用 PromptSelector 组件 -->
<PromptSelector
v-model="topicDetails.stylePromptId"
:prompts="allPrompts"
:loading="loadingPrompts"
:search-keyword="promptSearchKeyword"
@change="handlePromptChange"
@update:searchKeyword="promptSearchKeyword = $event"
/>
<!-- 空状态提示 -->
<div v-if="!loadingPrompts && allPrompts.length === 0" class="prompt-empty" style="color: var(--color-text-secondary); font-size: 14px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div>
@@ -719,47 +678,6 @@ onMounted(async () => {
</div>
</section>
<!-- 更多提示词弹窗 -->
<a-modal
v-model:open="showAllPromptsModal"
title="选择提示词风格"
:width="600"
:footer="null"
>
<div class="prompt-modal-content">
<!-- 搜索框 -->
<a-input
v-model:value="promptSearchKeyword"
placeholder="搜索提示词..."
style="margin-bottom: 16px;"
allow-clear
>
<template #prefix>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</template>
</a-input>
<!-- 提示词列表 -->
<div v-if="filteredPrompts.length > 0" class="all-prompts-grid">
<div
v-for="prompt in filteredPrompts"
:key="prompt.id"
class="all-prompt-tag"
:class="{ 'all-prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-else style="text-align: center; padding: 40px; color: var(--color-text-secondary);">
没有找到匹配的提示词
</div>
</div>
</a-modal>
</div>
</div>
</template>