Compare commits
2 Commits
1da5d283a9
...
790f1751f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 790f1751f2 | |||
| 711e412afb |
File diff suppressed because it is too large
Load Diff
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="我创建的"
|
||||
:width="700"
|
||||
:footer="null"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div class="favorites-content">
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建风格
|
||||
</a-button>
|
||||
<a-button @click="loadList" :loading="loading">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="favorites-list" v-if="!loading && list.length > 0">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="favorite-card"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="card-name">{{ item.name }}</span>
|
||||
<div class="card-actions">
|
||||
<a-button type="link" size="small" @click="handleEdit(item)">编辑</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除此风格?"
|
||||
@confirm="handleDelete(item.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">{{ item.content }}</div>
|
||||
<div class="card-footer">
|
||||
<span class="card-category" v-if="item.category">{{ item.category }}</span>
|
||||
<span class="card-use-count">使用 {{ item.useCount || 0 }} 次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" v-if="!loading && list.length === 0">
|
||||
<p>暂无收藏的风格</p>
|
||||
<a-button type="primary" @click="handleCreate">立即创建</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-state" v-if="loading">
|
||||
<a-spin />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="editVisible"
|
||||
:title="editingItem ? '编辑风格' : '新建风格'"
|
||||
:width="500"
|
||||
@ok="handleSave"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="风格名称" required>
|
||||
<a-input v-model:value="editForm.name" placeholder="请输入风格名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="风格内容" required>
|
||||
<a-textarea
|
||||
v-model:value="editForm.content"
|
||||
:rows="6"
|
||||
placeholder="请输入风格提示词内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="分类">
|
||||
<a-input v-model:value="editForm.category" placeholder="可选,用于分类管理" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { UserPromptApi } from '@/api/userPrompt'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'refresh'])
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const editVisible = ref(false)
|
||||
const editingItem = ref(null)
|
||||
const saving = ref(false)
|
||||
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
content: '',
|
||||
category: ''
|
||||
})
|
||||
|
||||
// 加载列表
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await UserPromptApi.getUserPromptList()
|
||||
if (res.code === 0) {
|
||||
list.value = res.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新风格
|
||||
function handleCreate() {
|
||||
editingItem.value = null
|
||||
editForm.value = {
|
||||
name: '',
|
||||
content: '',
|
||||
category: ''
|
||||
}
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑风格
|
||||
function handleEdit(item) {
|
||||
editingItem.value = item
|
||||
editForm.value = {
|
||||
name: item.name,
|
||||
content: item.content,
|
||||
category: item.category || ''
|
||||
}
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
// 保存风格
|
||||
async function handleSave() {
|
||||
if (!editForm.value.name.trim()) {
|
||||
message.warning('请输入风格名称')
|
||||
return
|
||||
}
|
||||
if (!editForm.value.content.trim()) {
|
||||
message.warning('请输入风格内容')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const data = {
|
||||
name: editForm.value.name.trim(),
|
||||
content: editForm.value.content.trim(),
|
||||
category: editForm.value.category.trim() || null,
|
||||
status: 1
|
||||
}
|
||||
|
||||
if (editingItem.value) {
|
||||
// 更新
|
||||
await UserPromptApi.updateUserPrompt({
|
||||
id: editingItem.value.id,
|
||||
...data
|
||||
})
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
// 创建
|
||||
await UserPromptApi.createUserPrompt(data)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
editVisible.value = false
|
||||
loadList()
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除风格
|
||||
async function handleDelete(id) {
|
||||
try {
|
||||
await UserPromptApi.deleteUserPrompt(id)
|
||||
message.success('删除成功')
|
||||
loadList()
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.favorites-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.favorites-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.favorite-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.card-category {
|
||||
padding: 2px 8px;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
color: #1890ff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--color-text-secondary);
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
title="批量分组"
|
||||
centered
|
||||
@ok="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择分组" required>
|
||||
<a-select
|
||||
v-model:value="selectedGroupId"
|
||||
placeholder="请选择要添加到的分组"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="group in groupList" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--color-bg-2); border-radius: var(--radius-card);">
|
||||
<p style="margin: 0; font-size: 13px; color: var(--color-text-2);">
|
||||
{{ groupingInfo }}
|
||||
</p>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const props = defineProps({
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
groupList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 可选的信息,组件会显示但不会强依赖
|
||||
fileName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fileCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:open', 'confirm', 'cancel'])
|
||||
|
||||
// 数据
|
||||
const selectedGroupId = ref(null)
|
||||
const modalVisible = ref(false)
|
||||
|
||||
// 计算分组提示信息
|
||||
const groupingInfo = computed(() => {
|
||||
if (props.fileName) {
|
||||
return `将为文件 "${props.fileName}" 添加到选中的分组中`
|
||||
}
|
||||
if (props.fileCount > 0) {
|
||||
return `将为 ${props.fileCount} 个文件添加到选中的分组中`
|
||||
}
|
||||
return '将为选中文件添加到选中的分组中'
|
||||
})
|
||||
|
||||
// 监听 open 变化
|
||||
watch(() => props.open, (newVal) => {
|
||||
modalVisible.value = newVal
|
||||
if (!newVal) {
|
||||
selectedGroupId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 处理确认
|
||||
const handleConfirm = () => {
|
||||
if (!selectedGroupId.value) {
|
||||
message.warning('请选择分组')
|
||||
return
|
||||
}
|
||||
|
||||
emit('confirm', selectedGroupId.value)
|
||||
handleCancel()
|
||||
}
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
selectedGroupId.value = null
|
||||
emit('update:open', false)
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
@@ -1,197 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
title="素材混剪"
|
||||
centered
|
||||
:confirm-loading="loading"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@ok="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="mix-modal__summary">
|
||||
<p>视频分组:{{ getGroupName(videoGroupId) || '未选择' }}</p>
|
||||
<p>视频数量:{{ videoGroupFiles.length }} 个</p>
|
||||
<p style="margin-top: 8px; font-size: 12px; color: var(--color-text-3);">
|
||||
纯画面模式:仅拼接视频片段,无配音、无背景音乐
|
||||
</p>
|
||||
</div>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择视频分组" required>
|
||||
<a-select
|
||||
v-model:value="videoGroupId"
|
||||
placeholder="请选择视频分组"
|
||||
style="width: 100%"
|
||||
@change="handleVideoGroupChange"
|
||||
>
|
||||
<a-select-option v-for="group in groupList" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="视频标题" required>
|
||||
<a-input v-model:value="mixForm.title" placeholder="请输入生成视频标题(仅用于记录)" />
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--color-text-3);">
|
||||
标题仅用于任务记录,不会在视频上显示
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { MaterialService } from '@/api/material'
|
||||
|
||||
const props = defineProps({
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
groupList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:open', 'confirm', 'cancel'])
|
||||
|
||||
// 数据
|
||||
const modalVisible = ref(false)
|
||||
const videoGroupId = ref(null)
|
||||
const videoGroupFiles = ref([])
|
||||
|
||||
const mixForm = reactive({
|
||||
title: ''
|
||||
})
|
||||
|
||||
// 获取分组名称
|
||||
const getGroupName = (groupId) => {
|
||||
if (!groupId) return ''
|
||||
const group = props.groupList.find(g => g.id === groupId)
|
||||
return group?.name || ''
|
||||
}
|
||||
|
||||
// 监听 open 变化
|
||||
watch(() => props.open, (newVal) => {
|
||||
modalVisible.value = newVal
|
||||
if (newVal) {
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
// 处理视频分组变化
|
||||
const handleVideoGroupChange = async (groupId) => {
|
||||
console.log('[MaterialMixModal] handleVideoGroupChange called with groupId:', groupId)
|
||||
|
||||
if (!groupId) {
|
||||
videoGroupFiles.value = []
|
||||
console.log('[MaterialMixModal] groupId is empty, videoGroupFiles cleared')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[MaterialMixModal] Loading video files for group:', groupId)
|
||||
|
||||
try {
|
||||
const res = await MaterialService.getFilePage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
groupId: groupId,
|
||||
fileCategory: 'video'
|
||||
})
|
||||
if (res.code === 0) {
|
||||
videoGroupFiles.value = res.data.list || []
|
||||
console.log('[MaterialMixModal] Loaded video files:', videoGroupFiles.value.length, 'files')
|
||||
} else {
|
||||
console.error('[MaterialMixModal] Failed to load video files:', res.msg)
|
||||
message.error(res.msg || '加载分组文件失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MaterialMixModal] Error loading video files:', error)
|
||||
message.error('加载分组文件失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
mixForm.title = ''
|
||||
videoGroupId.value = null
|
||||
videoGroupFiles.value = []
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
defineExpose({
|
||||
resetForm
|
||||
})
|
||||
|
||||
// 处理确认
|
||||
const handleConfirm = async () => {
|
||||
const title = mixForm.title.trim()
|
||||
|
||||
if (!videoGroupId.value) {
|
||||
message.warning('请选择视频分组')
|
||||
return
|
||||
}
|
||||
if (!title) {
|
||||
message.warning('请输入视频标题')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果当前没有视频文件,重新加载一次
|
||||
if (videoGroupFiles.value.length === 0) {
|
||||
message.loading('正在加载视频文件...', 0)
|
||||
await handleVideoGroupChange(videoGroupId.value)
|
||||
message.destroy()
|
||||
}
|
||||
|
||||
if (videoGroupFiles.value.length === 0) {
|
||||
message.warning('所选分组下没有视频文件,请检查分组设置或更换分组')
|
||||
return
|
||||
}
|
||||
|
||||
// 提取视频URL
|
||||
const videoUrls = videoGroupFiles.value
|
||||
.map(file => file?.fileUrl || file?.imgUrl)
|
||||
.filter(Boolean)
|
||||
|
||||
if (videoUrls.length === 0) {
|
||||
message.warning('视频分组中没有有效的视频文件')
|
||||
return
|
||||
}
|
||||
|
||||
emit('confirm', {
|
||||
title,
|
||||
videoUrls
|
||||
})
|
||||
}
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
emit('update:open', false)
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mix-modal__summary {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--color-bg-2);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.mix-modal__summary p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -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') },
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user