refactor(StyleSelector): simplify component by removing agent integration and using unified prompt list

Removed separate user prompts and favorite agents groups, replaced with a single unified list from /ai/user-prompt/my-list endpoint. Removed agent-related functionality including API calls, filtering logic, and type handling since the component now only works with user prompts. Simplified the UI structure and filtering mechanism to work with the new data model.
This commit is contained in:
2026-03-15 21:47:33 +08:00
parent 1da5d283a9
commit 711e412afb
5 changed files with 33 additions and 1694 deletions

View File

@@ -1,604 +0,0 @@
<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">
<div class="prompt-option-left">
<span class="prompt-option-name">{{ prompt.name }}</span>
<span v-if="prompt.category" class="prompt-option-tag category">{{ prompt.category }}</span>
</div>
<span class="prompt-option-tag" :class="prompt.source === 'created' ? 'created' : 'favorite'">
{{ prompt.source === 'created' ? '自建' : '收藏' }}
</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-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'
},
// 展示数量(仅标签模式有效)
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
})
// 加载用户提示词(通过 Store
async function loadUserPrompts() {
if (!userStore.userId) {
console.warn('用户未登录,无法加载提示词')
return
}
try {
// 使用 store 加载(自建 + 收藏的智能体)
await promptStore.loadPromptList()
// 如果有选中ID验证是否在列表中
if (selectedPromptId.value) {
const exists = allPrompts.value.find(p => p.id === selectedPromptId.value)
if (!exists) {
selectedPromptId.value = null
}
}
// 如果没有选中且有提示词,默认选中第一个
if (!selectedPromptId.value && allPrompts.value.length > 0) {
selectedPromptId.value = allPrompts.value[0].id
}
// 恢复本地存储的选择
await restoreSelectedPromptId()
} catch (error) {
console.error('加载提示词失败:', 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()
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-option-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.prompt-option-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
flex-shrink: 0;
}
.prompt-option-tag.category {
background: rgba(24, 144, 255, 0.1);
color: var(--color-primary);
}
.prompt-option-tag.created {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.prompt-option-tag.favorite {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
/* 标签模式 */
.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

@@ -8,43 +8,20 @@
:filter-option="filterOption"
@change="handleChange"
>
<!-- 用户风格分组 -->
<a-select-opt-group v-if="userPrompts.length > 0">
<template #label>
<span class="group-label">用户风格</span>
</template>
<a-select-option
v-for="item in userPrompts"
:key="`prompt-${item.id}`"
:value="`prompt-${item.id}`"
>
<div class="option-content">
<span class="option-name">{{ item.name }}</span>
<span class="option-tag style-tag">风格</span>
</div>
</a-select-option>
</a-select-opt-group>
<!-- 收藏的智能体分组 -->
<a-select-opt-group v-if="favoriteAgents.length > 0">
<template #label>
<span class="group-label">收藏的智能体</span>
</template>
<a-select-option
v-for="item in favoriteAgents"
:key="`agent-${item.id}`"
:value="`agent-${item.id}`"
>
<div class="option-content">
<img v-if="item.avatar" :src="item.avatar" class="option-avatar" />
<span class="option-name">{{ item.name }}</span>
<span class="option-tag agent-tag">{{ item.categoryName || '智能体' }}</span>
</div>
</a-select-option>
</a-select-opt-group>
<a-select-option
v-for="item in allList"
:key="item.id"
:value="item.id"
>
<div class="option-content">
<img v-if="item.icon" :src="item.icon" class="option-avatar" />
<span class="option-name">{{ item.name }}</span>
<span class="option-tag">{{ item.category || '其他' }}</span>
</div>
</a-select-option>
<!-- 空状态 -->
<template v-if="!loading && userPrompts.length === 0 && favoriteAgents.length === 0">
<template v-if="!loading && allList.length === 0">
<a-select-option disabled value="__empty__">
<span class="empty-text">暂无可选项</span>
</a-select-option>
@@ -55,7 +32,6 @@
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { usePromptStore } from '@/stores/prompt'
import { getAgentList } from '@/api/agent'
import { getJSON, setJSON } from '@/utils/storage'
const props = defineProps({
@@ -79,32 +55,14 @@ const promptStore = usePromptStore()
const loading = ref(false)
const selectedId = ref(null)
const agentList = ref([])
// 用户风格列表
const userPrompts = computed(() => promptStore.promptList || [])
// 收藏的智能体列表
const favoriteAgents = computed(() => {
return agentList.value.filter(agent => agent.isFavorite)
})
// 全部列表(来自 /ai/user-prompt/my-list
const allList = computed(() => promptStore.promptList || [])
// 过滤选项
function filterOption(input, option) {
const key = option.value
const name = option.label || ''
// 根据 key 查找对应的项目名称
if (key.startsWith('prompt-')) {
const id = parseInt(key.replace('prompt-', ''))
const prompt = userPrompts.value.find(p => p.id === id)
return prompt?.name?.toLowerCase().includes(input.toLowerCase())
} else if (key.startsWith('agent-')) {
const id = parseInt(key.replace('agent-', ''))
const agent = agentList.value.find(a => a.id === id)
return agent?.name?.toLowerCase().includes(input.toLowerCase())
}
return false
const item = allList.value.find(p => p.id === option.value)
return item?.name?.toLowerCase().includes(input.toLowerCase())
}
// 处理选择变化
@@ -115,54 +73,14 @@ function handleChange(value) {
return
}
const [type, idStr] = value.split('-')
const id = parseInt(idStr)
let item = null
if (type === 'prompt') {
item = userPrompts.value.find(p => p.id === id)
emit('change', { id, type: 'prompt', item })
} else if (type === 'agent') {
item = agentList.value.find(a => a.id === id)
emit('change', { id, type: 'agent', item })
}
emit('update:modelValue', id)
const item = allList.value.find(p => p.id === value)
emit('change', { id: value, item })
emit('update:modelValue', value)
// 保存选中状态到本地
saveSelectedValue(value)
}
// 加载智能体列表
async function loadAgentList() {
try {
const res = await getAgentList()
if (res.code === 0 && res.data) {
agentList.value = res.data.map(item => ({
id: item.id,
agentId: item.agentId,
name: item.agentName,
description: item.description,
systemPrompt: item.systemPrompt,
avatar: item.icon,
categoryName: item.categoryName || '其他',
isFavorite: item.isFavorite || false
}))
}
} catch (error) {
console.error('加载智能体列表失败:', error)
}
}
// 加载用户风格列表
async function loadUserPrompts() {
try {
await promptStore.loadPromptList()
} catch (error) {
console.error('加载用户风格列表失败:', error)
}
}
// 保存选中状态
async function saveSelectedValue(value) {
try {
@@ -175,28 +93,13 @@ async function saveSelectedValue(value) {
// 恢复选中状态
async function restoreSelectedValue() {
try {
const savedValue = await getJSON(`${props.storageKey}_selected`, null)
if (savedValue) {
// 验证保存的值是否有效
const [type, idStr] = savedValue.split('-')
const id = parseInt(idStr)
if (type === 'prompt') {
const exists = userPrompts.value.some(p => p.id === id)
if (exists) {
selectedId.value = savedValue
emit('update:modelValue', id)
const item = userPrompts.value.find(p => p.id === id)
emit('change', { id, type: 'prompt', item })
}
} else if (type === 'agent') {
const exists = agentList.value.some(a => a.id === id)
if (exists) {
selectedId.value = savedValue
emit('update:modelValue', id)
const item = agentList.value.find(a => a.id === id)
emit('change', { id, type: 'agent', item })
}
const savedId = await getJSON(`${props.storageKey}_selected`, null)
if (savedId) {
const item = allList.value.find(p => p.id === savedId)
if (item) {
selectedId.value = savedId
emit('update:modelValue', savedId)
emit('change', { id: savedId, item })
}
}
} catch (error) {
@@ -208,7 +111,7 @@ async function restoreSelectedValue() {
async function init() {
loading.value = true
try {
await Promise.all([loadUserPrompts(), loadAgentList()])
await promptStore.loadPromptList()
await restoreSelectedValue()
} finally {
loading.value = false
@@ -233,12 +136,6 @@ defineExpose({
</script>
<style scoped lang="less">
.group-label {
font-weight: 600;
color: var(--color-text);
font-size: 13px;
}
.option-content {
display: flex;
align-items: center;
@@ -263,17 +160,9 @@ defineExpose({
font-size: 11px;
padding: 1px 6px;
border-radius: 10px;
flex-shrink: 0;
}
.style-tag {
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
}
.agent-tag {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
flex-shrink: 0;
}
.empty-text {

View File

@@ -10,7 +10,6 @@ const navConfig = [
order: 1,
items: [
{ name: '对标分析', path: 'content-style/benchmark', icon: 'grid', component: () => import('../views/content-style/Benchmark.vue') },
// { name: '文案创作', path: 'content-style/copywriting', icon: 'text', component: () => import('../views/content-style/Copywriting.vue') },
{ name: '热点趋势', path: 'trends/forecast', icon: 'text', component: () => import('../views/trends/Forecast.vue') },
{ name: '智能体', path: 'agents', icon: 'robot', component: () => import('../views/agents/Agents.vue') },
]

View File

@@ -1,904 +0,0 @@
<script setup>
import { ref, onMounted, onActivated, computed, watch } from 'vue'
import { usePromptStore } from '@/stores/prompt'
import { useUserStore } from '@/stores/user'
import MarkdownIt from 'markdown-it'
import { message } from 'ant-design-vue'
import { CommonService } from '@/api/common'
import useVoiceText from '@/hooks/web/useVoiceText'
import GmIcon from '@/components/icons/Icon.vue'
import GradientButton from '@/components/GradientButton.vue'
import PromptSelector from '@/components/PromptSelector.vue'
import { copyToClipboard } from '@/utils/clipboard'
import BasicLayout from '@/layouts/components/BasicLayout.vue'
const promptStore = usePromptStore()
const userStore = useUserStore()
const md = new MarkdownIt()
// 表单数据
const form = ref({
prompt: '',
userInput: '',
amplitude: 50
})
// 生成的文案内容
const generatedContent = ref('')
// 编辑模式相关
const isEditMode = ref(false)
const editableContent = ref('')
const originalContent = ref('')
// 加载状态
const isLoading = ref(false)
const { getVoiceText } = useVoiceText()
// ===== 使用 Store 作为单一数据源 =====
const selectedPromptId = ref(null)
// 处理提示词选择
function handlePromptChange(prompt) {
if (!prompt || !prompt.content) {
message.warning('提示词内容为空')
return
}
selectedPromptId.value = prompt.id
form.value.prompt = prompt.content
promptStore.setPrompt(prompt.content, prompt)
}
/**
* 初始化页面数据
* 通过 Store 加载提示词Store 会自动缓存)
*/
async function initializePage() {
// 恢复之前保存的提示词
if (promptStore.currentPrompt) {
form.value.prompt = promptStore.currentPrompt
}
// 加载提示词列表(自建 + 收藏Store 会自动缓存)
await promptStore.loadPromptList()
// 如果有选中的提示词,同步 ID
if (promptStore.currentVideoInfo?.id) {
selectedPromptId.value = promptStore.currentVideoInfo.id
}
}
// 页面加载时初始化
onMounted(() => {
initializePage()
})
// 生成文案(流式)
async function generateCopywriting() {
const inputContent = form.value.userInput || ''
if (!inputContent.trim()) {
message.warning('请输入内容')
return
}
// 检查是否选择了风格
if (!form.value.prompt || !form.value.prompt.trim()) {
message.warning('请先选择文案风格')
return
}
if (!selectedPromptId.value) {
message.warning('请先选择文案风格')
return
}
isLoading.value = true
generatedContent.value = '' // 清空之前的内容
try {
// 如果看起来是视频/音频链接,先尝试转写;否则直接作为文本
let userText = inputContent
if (isLikelyUrl(inputContent)) {
try {
message.info('正在获取视频转写...')
const transcriptions = await getVoiceText([{ audio_url: inputContent }])
const transcript = Array.isArray(transcriptions) && transcriptions[0] ? (transcriptions[0].value || '') : ''
if (transcript.trim()) {
userText = transcript
} else {
message.warning('未从链接获取到可用的语音文本,将直接使用原始输入')
}
} catch (e) {
console.warn('获取转写失败,使用原始输入:', e)
}
}
// 调用 callWorkflow 流式 API
const requestData = {
audio_prompt: form.value.prompt || '', // 音频提示词
user_text: userText, // 用户输入内容或由链接转写得到的文本
amplitude: form.value.amplitude // 幅度,范围 0-100
}
const ctrl = new AbortController()
let fullText = ''
let errorOccurred = false
let isResolved = false
await new Promise((resolve, reject) => {
// 设置超时
const timeout = setTimeout(() => {
if (!isResolved) {
ctrl.abort()
reject(new Error('请求超时,请稍后重试'))
}
}, 180000) // 3分钟超时
CommonService.callWorkflowStream({
data: requestData,
ctrl,
onMessage: (event) => {
try {
if (errorOccurred) return
const dataStr = event?.data || ''
if (!dataStr) return
try {
const obj = JSON.parse(dataStr)
// 根据实际返回格式解析
const piece = obj?.text || obj?.content || obj?.data || ''
if (piece) {
fullText += piece
generatedContent.value = fullText
}
} catch (parseErr) {
console.warn('解析流数据异常:', parseErr)
}
} catch (e) {
console.warn('解析流数据异常:', e)
}
},
onError: (err) => {
clearTimeout(timeout)
if (!isResolved) {
errorOccurred = true
ctrl.abort()
// 尝试解析错误中的状态码和业务码
const status = err?.response?.status
const data = err?.response?.data
const errorMsg = err?.message || '网络请求失败'
console.error('SSE请求错误:', err)
message.error(errorMsg)
reject(new Error(errorMsg))
}
},
onClose: () => {
clearTimeout(timeout)
if (!isResolved) {
isResolved = true
resolve()
}
}
})
})
generatedContent.value = fullText.trim()
message.success('文案生成成功')
} catch (error) {
console.error('生成文案失败:', error)
message.error('生成文案失败,请重试')
} finally {
isLoading.value = false
}
}
// 获取当前输入值
function getCurrentInputValue() {
return (form.value.userInput || '').trim()
}
// 粗略判断是否为 URL含常见平台域名或 http(s) 开头)
function isLikelyUrl(value) {
if (!value) return false
const v = String(value).trim()
if (/^https?:\/\//i.test(v)) return true
return /(douyin\.com|bilibili\.com|youtube\.com|youtu\.be|tiktok\.com|ixigua\.com|v\.qq\.com)/i.test(v)
}
// 切换编辑模式
function toggleEditMode() {
if (!isEditMode.value) {
// 进入编辑模式
originalContent.value = generatedContent.value
editableContent.value = generatedContent.value
}
isEditMode.value = !isEditMode.value
}
// 保存编辑
function saveEdit() {
generatedContent.value = editableContent.value
isEditMode.value = false
message.success('文案已保存')
}
// 取消编辑
function cancelEdit() {
editableContent.value = originalContent.value
isEditMode.value = false
message.info('已取消编辑')
}
// 复制内容(编辑模式复制编辑区,否则复制生成内容)
async function copyContent() {
const text = isEditMode.value ? (editableContent.value || '') : (generatedContent.value || '')
if (!text.trim()) {
message.warning('没有可复制的内容')
return
}
const success = await copyToClipboard(text)
if (success) {
message.success('文案已复制到剪贴板')
} else {
message.error('复制失败')
}
}
defineOptions({ name: 'ContentStyleCopywriting' })
</script>
<template>
<BasicLayout title="AI 文案创作">
<!-- 主要内容区域 -->
<div class="main-content">
<a-row :gutter="16">
<a-col :lg="10" :md="24">
<div class="form-section">
<a-card class="form-card" :bordered="false" title="创作设置">
<a-form :model="form" layout="vertical" class="form-container">
<a-form-item class="form-item">
<!-- 使用 PromptSelector 组件数据来自 Store -->
<PromptSelector
v-model="selectedPromptId"
@change="handlePromptChange"
/>
</a-form-item>
<!-- 统一输入文本或视频链接 -->
<a-form-item class="form-item">
<template #label>
输入内容
<span class="form-tip-inline">输入要生成的主题/段落或粘贴视频链接以自动转写</span>
</template>
<a-textarea
v-model:value="form.userInput"
placeholder="直接输入文字或粘贴视频链接抖音、B站、YouTube等"
:auto-size="{ minRows: 6, maxRows: 12 }"
class="custom-textarea"
/>
</a-form-item>
<!-- 幅度设置 -->
<a-form-item class="form-item">
<template #label>
创作幅度
<span class="form-tip-inline">调整创作幅度数值越大创意性越强</span>
</template>
<div class="amplitude-row">
<a-slider
v-model:value="form.amplitude"
:min="0"
:max="100"
:tooltip-formatter="(value) => `${value}%`"
style="flex: 1"
/>
<a-input-number
v-model:value="form.amplitude"
:min="0"
:max="100"
style="width: 96px; margin-left: 12px;"
/>
</div>
</a-form-item>
<a-form-item class="form-item">
<GradientButton
text="生成文案"
icon="icon-sparkle"
:disabled="!getCurrentInputValue() || !selectedPromptId || isLoading"
:loading="isLoading"
loading-text="生成中..."
size="large"
:block="true"
@click="generateCopywriting"
/>
</a-form-item>
</a-form>
</a-card>
</div>
</a-col>
<a-col :lg="14" :md="24">
<div class="result-section">
<a-card class="result-card" :bordered="false" title="生成结果">
<template #extra>
<a-space>
<a-button v-if="generatedContent" @click="toggleEditMode" size="small" class="action-btn">
<span class="btn-icon"><GmIcon :name="isEditMode ? 'icon-eye' : 'icon-edit'" :size="14" /></span>
{{ isEditMode ? '预览' : '编辑' }}
</a-button>
<a-button v-if="generatedContent" @click="copyContent" size="small" class="action-btn copy-btn">
<span class="btn-icon"><GmIcon name="icon-copy" :size="14" /></span>
复制
</a-button>
</a-space>
</template>
<div v-if="generatedContent" class="result-content">
<!-- 编辑模式 -->
<div v-if="isEditMode" class="edit-mode">
<a-textarea
v-model:value="editableContent"
:auto-size="{ minRows: 15, maxRows: 30 }"
placeholder="在这里编辑生成的文案内容..."
class="edit-textarea"
/>
<div class="edit-actions">
<a-space>
<a-button @click="saveEdit" type="primary" size="small" class="save-btn">
<span class="btn-icon"><GmIcon name="icon-save" :size="14" /></span>
保存
</a-button>
<a-button @click="cancelEdit" size="small" class="cancel-btn">
<span class="btn-icon"><GmIcon name="icon-cancel" :size="14" /></span>
取消
</a-button>
</a-space>
</div>
</div>
<!-- 预览模式 -->
<div v-else class="generated-content" v-html="md.render(generatedContent)"></div>
</div>
<div v-else class="custom-empty">
<div class="empty-description">
<div class="empty-icon"><GmIcon name="icon-empty-doc" :size="36" /></div>
<div class="empty-title">等待生成文案</div>
<div class="empty-desc">请选择内容类型并填写相关信息,然后点击"生成文案"按钮</div>
</div>
</div>
</a-card>
</div>
</a-col>
</a-row>
</div>
</BasicLayout>
</template>
<style scoped>
/* 主要内容区域 */
.main-content {
padding: 0;
}
/* 卡片样式 */
.form-card,
.result-card {
background: var(--bg-primary);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-color-base); /* Add a subtle border */
box-shadow: var(--box-shadow-base); /* Add a subtle shadow for depth */
}
.form-card :deep(.ant-card-head),
.result-card :deep(.ant-card-head) {
background: var(--bg-primary);
border: none;
padding: 20px 24px;
min-height: auto;
}
.form-card :deep(.ant-card-head-title),
.result-card :deep(.ant-card-head-title) {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
padding: 0;
margin: 0;
}
.form-card :deep(.ant-card-body),
.result-card :deep(.ant-card-body) {
padding: 24px;
}
/* 表单容器 */
.form-container {
padding: 0;
}
.form-item {
margin-bottom: 24px;
}
.form-item :deep(.ant-form-item-label) {
padding-bottom: 6px;
}
.form-item :deep(.ant-form-item-label > label) {
font-weight: 500;
color: var(--text-primary);
font-size: 14px;
margin-bottom: 4px;
}
/* 表单标签后的内联提示(不使用 emoji */
.form-tip-inline {
margin-left: var(--space-2);
font-size: 13px;
color: var(--text-secondary);
font-weight: 400;
}
/* 自定义输入框样式 */
.custom-input,
.custom-textarea {
border-radius: 6px;
border: 1px solid var(--border-light);
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}
.custom-input:focus,
.custom-textarea:focus {
border-color: var(--primary-color, #1677ff);
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2); /* Subtle blue glow */
}
.custom-input:hover,
.custom-textarea:hover {
border-color: var(--border-light-hover, #d9d9d9);
}
.custom-textarea::placeholder {
color: var(--text-disabled);
}
/* 已合并输入:移除单选组相关样式 */
/* 输入区域动画 */
.input-section {
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 旧的块级提示兼容(如他处已有复用时可沿用) */
.form-tip {
display: none;
}
/* 生成按钮样式 - 已替换为 GradientButton 组件 */
/* 操作按钮样式 */
.action-btn {
border-radius: 6px;
font-weight: 500;
font-size: 13px;
padding: 4px 12px;
height: 28px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.action-btn:hover {
background: rgba(22, 119, 255, 0.08);
}
.copy-btn:hover {
background: rgba(22, 119, 255, 0.15);
border-color: var(--primary-color, #1677ff);
color: var(--primary-color, #1677ff);
}
/* 幅度滑块样式 */
.amplitude-row {
display: flex;
align-items: center;
gap: 12px;
}
/* 暗色下滑块可见性增强 */
:deep(.ant-slider) {
padding: 10px 0; /* 增加垂直留白,减少拥挤感 */
}
:deep(.ant-slider-rail) {
background-color: var(--color-gray-200);
height: 4px;
}
:deep(.ant-slider-track) {
background-color: var(--color-primary);
height: 4px;
}
:deep(.ant-slider:hover .ant-slider-track) {
background-color: var(--color-primary);
}
:deep(.ant-slider-handle::after) {
box-shadow: 0 0 0 2px var(--color-primary);
}
:deep(.ant-slider-handle:focus-visible::after),
:deep(.ant-slider-handle:hover::after),
:deep(.ant-slider-handle:active::after) {
box-shadow: 0 0 0 3px var(--color-primary-glow);
}
/* 空状态样式 */
.custom-empty {
padding: 40px 20px;
}
.empty-description {
text-align: center;
padding: 20px;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 16px;
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.empty-desc {
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.6;
max-width: 300px;
margin: 0 auto;
}
/* 编辑模式样式 */
.edit-mode {
padding: 0;
background: transparent;
border-radius: 8px;
}
.edit-textarea {
margin-bottom: 12px;
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-light);
font-size: 14px;
line-height: 1.6;
}
.edit-textarea:focus {
border-color: var(--primary-color, #1677ff);
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2); /* Subtle blue glow */
}
.edit-textarea:hover {
border-color: var(--border-light-hover, #d9d9d9);
}
.edit-textarea::placeholder {
color: var(--text-disabled);
}
.edit-actions {
text-align: right;
padding-top: 12px;
margin-top: 12px;
}
.save-btn {
background: var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: 6px;
font-weight: 500;
}
.save-btn:hover {
background: var(--color-primary);
filter: brightness(1.04);
}
.cancel-btn {
border-radius: 6px;
font-weight: 500;
}
/* 生成内容样式 */
.result-content {
min-height: 400px;
}
.generated-content {
padding: 20px;
background: var(--bg-primary);
border-radius: 6px;
line-height: 1.8;
color: var(--text-primary);
min-height: 400px;
font-size: 14px;
white-space: pre-wrap;
word-break: break-word;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.generated-content :deep(h1) {
font-size: 22px;
font-weight: 600;
margin-bottom: var(--space-4);
color: var(--text-primary);
padding-bottom: var(--space-2);
}
.generated-content :deep(h2) {
font-size: 19px;
font-weight: 600;
margin: 22px 0 12px 0;
color: var(--text-primary);
}
.generated-content :deep(h3) {
font-size: 16px;
font-weight: 600;
margin: 18px 0 10px 0;
color: var(--text-primary);
}
.generated-content :deep(p) {
margin: 12px 0 14px 0;
color: var(--text-primary);
line-height: 1.9;
font-size: 15px;
}
.generated-content :deep(ul),
.generated-content :deep(ol) {
margin: 12px 0;
padding-left: 24px;
}
.generated-content :deep(li) {
margin: 6px 0;
color: var(--text-primary);
line-height: 1.9;
font-size: 15px;
}
.generated-content :deep(strong) {
font-weight: 600;
color: var(--text-primary);
}
.generated-content :deep(code) {
background: var(--bg-secondary);
padding: 3px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13.5px;
color: var(--color-red-500);
}
.generated-content :deep(pre) {
background: var(--bg-secondary);
padding: 16px 18px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
}
.generated-content :deep(pre code) {
background: transparent;
padding: 0;
color: var(--color-red-500);
}
.generated-content :deep(blockquote) {
margin: 16px 0;
padding: 12px 16px;
background: rgba(59, 130, 246, 0.08);
border-left: 4px solid var(--color-primary);
border-radius: 0 6px 6px 0;
font-style: italic;
color: var(--text-primary);
}
/* 提示词标签样式 */
.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(--bg-primary);
border: 1px solid var(--border-light);
border-radius: 16px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.prompt-tag:hover {
border-color: var(--primary-color, #1677ff);
background: rgba(22, 119, 255, 0.08);
}
.prompt-tag-selected {
border-color: var(--primary-color, #1677ff);
background: var(--primary-color, #1677ff);
color: #fff;
}
.prompt-tag-selected:hover {
background: var(--primary-color, #1677ff);
}
.prompt-tag-name {
white-space: nowrap;
user-select: none;
}
.prompt-empty {
padding: 20px;
text-align: center;
}
.prompt-loading {
padding: 20px;
text-align: center;
}
/* 更多提示词弹窗样式 */
.all-prompts-modal {
max-height: 500px;
overflow-y: auto;
}
.all-prompts-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.all-prompt-tag {
display: inline-flex;
align-items: center;
padding: 8px 16px;
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: 16px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.all-prompt-tag:hover {
border-color: var(--primary-color, #1677ff);
background: rgba(22, 119, 255, 0.08);
}
.all-prompt-tag-selected {
border-color: var(--primary-color, #1677ff);
background: var(--primary-color, #1677ff);
color: #fff;
}
.all-prompt-tag-selected:hover {
background: var(--primary-color, #1677ff);
}
.all-prompt-tag-name {
white-space: nowrap;
user-select: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
padding: 16px 0 24px 0;
}
.page-title {
font-size: 1.5rem;
flex-direction: column;
gap: 6px;
}
.title-icon {
font-size: 1.5rem;
}
.main-content {
padding: 0 16px 24px 16px;
}
.form-card,
.result-card {
margin-bottom: 16px;
}
.form-card :deep(.ant-card-body),
.result-card :deep(.ant-card-body) {
padding: 16px;
}
.radio-label {
padding: 10px 12px;
}
.radio-icon {
font-size: 1rem;
width: 28px;
height: 28px;
margin-right: 10px;
}
.generated-content {
padding: 14px;
font-size: 15px;
}
.prompt-tags-grid {
gap: 6px;
}
.prompt-tag {
font-size: 12px;
padding: 5px 12px;
}
}
</style>

View File

@@ -44,16 +44,9 @@ const topicDetails = reactive({
level: 50
})
// 智能体列表
const agentList = ref([])
const loadingAgents = ref(false)
// 当前选中的智能体
const selectedAgent = ref(null)
// 当前选中的风格类型
const selectedStyleType = ref('agent')
// 工具函数
function formatNumber(num) {
if (!num) return '0'
@@ -89,52 +82,20 @@ async function copyContent() {
}
}
// 加载智能体列表(保留用于其他逻辑)
async function loadAgentList() {
loadingAgents.value = true
try {
const res = await getAgentList()
if (res.code === 0 && res.data) {
agentList.value = res.data.map(item => ({
id: item.id,
agentId: item.agentId,
name: item.agentName,
description: item.description,
systemPrompt: item.systemPrompt,
avatar: item.icon,
categoryName: item.categoryName || '其他',
isFavorite: item.isFavorite || false
}))
}
} catch (error) {
console.error('加载智能体列表失败:', error)
} finally {
loadingAgents.value = false
}
}
// 处理风格选择变化
function handleStyleChange(data) {
if (!data) {
selectedAgent.value = null
selectedStyleType.value = null
return
}
const { id, type, item } = data
selectedStyleType.value = type
const { id, item } = data
topicDetails.selectedAgentId = id
if (type === 'agent') {
// 选择的是智能体
selectedAgent.value = item
} else if (type === 'prompt') {
// 选择的是用户风格
selectedAgent.value = {
id: item.id,
name: item.name,
systemPrompt: item.content
}
selectedAgent.value = {
id: item.id,
name: item.name,
systemPrompt: item.content
}
}
@@ -382,8 +343,6 @@ async function handleSearch() {
// 初始化
onMounted(() => {
// 加载智能体列表用于后续逻辑
loadAgentList()
pointsConfigStore.loadConfig()
})
</script>