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

@@ -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>