feat: 功能优化

This commit is contained in:
2025-11-14 02:15:14 +08:00
parent c652d0ddf3
commit 6624627802
10 changed files with 1224 additions and 223 deletions

View File

@@ -77,6 +77,7 @@ globs: **/*.vue, **/*.ts, components/**/*
- 实现完整的错误处理
- 规范事件处理机制
- 为复杂逻辑添加文档注释
- 代码简洁易于人类阅读
## 构建与工具链
- 使用 Vite 进行开发

View File

@@ -39,7 +39,7 @@ export const InterfaceType = {
/** 抖音 - 网页端通用搜索结果 */
DOUYIN_WEB_GENERAL_SEARCH: '12',
/** 抖音 - APP通用搜索结果 */
DOUYIN_SEARCH_GENERAL_SEARCH: '14',
DOUYIN_SEARCH_GENERAL_SEARCH: '13',
}
/**

View File

@@ -0,0 +1,203 @@
<template>
<button
:class="['gradient-button', { 'gradient-button--disabled': disabled, 'gradient-button--loading': loading }]"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="gradient-button__loading">
<a-spin size="small" />
<span v-if="loadingText" class="gradient-button__loading-text">{{ loadingText }}</span>
</span>
<span v-else class="gradient-button__content">
<slot name="icon">
<span v-if="icon" class="gradient-button__icon">
<GmIcon :name="icon" :size="iconSize" />
</span>
</slot>
<span class="gradient-button__text">
<slot>{{ text }}</slot>
</span>
</span>
</button>
</template>
<script setup>
import { computed } from 'vue'
import GmIcon from '@/components/icons/Icon.vue'
const props = defineProps({
// 按钮文本
text: {
type: String,
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 加载中的文本
loadingText: {
type: String,
default: ''
},
// 图标名称
icon: {
type: String,
default: ''
},
// 图标大小
iconSize: {
type: Number,
default: 16
},
// 按钮大小
size: {
type: String,
default: 'large', // large, middle, small
validator: (value) => ['large', 'middle', 'small'].includes(value)
},
// 是否块级按钮
block: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
const handleClick = (event) => {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
const buttonClass = computed(() => {
return [
`gradient-button--${props.size}`,
{
'gradient-button--block': props.block
}
]
})
</script>
<style scoped>
.gradient-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid rgba(24, 144, 255, 0.3);
border-radius: 4px;
font-size: 14px;
font-weight: 500;
color: #ffffff;
cursor: pointer;
transition: all 0.15s ease;
background: linear-gradient(135deg, #1890FF 0%, #40A9FF 100%);
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
user-select: none;
}
.gradient-button:hover {
background: linear-gradient(135deg, #1890FF 0%, #40A9FF 100%);
border-color: rgba(24, 144, 255, 0.5);
box-shadow: 0 0 6px rgba(24, 144, 255, 0.25);
}
.gradient-button:active {
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
}
.gradient-button__content,
.gradient-button__loading {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.gradient-button__icon {
display: inline-flex;
align-items: center;
}
.gradient-button__text {
line-height: 1;
}
.gradient-button__loading-text {
margin-left: 4px;
}
/* 尺寸变体 */
.gradient-button--large {
padding: 10px 20px;
font-size: 15px;
min-height: 36px;
}
.gradient-button--middle {
padding: 8px 16px;
font-size: 14px;
min-height: 32px;
}
.gradient-button--small {
padding: 4px 12px;
font-size: 13px;
min-height: 24px;
}
/* 块级按钮 */
.gradient-button--block {
width: 100%;
display: flex;
}
/* 禁用状态 */
.gradient-button--disabled,
.gradient-button:disabled {
opacity: 0.4;
cursor: not-allowed;
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
border-color: rgba(24, 144, 255, 0.2);
box-shadow: none;
}
.gradient-button--disabled:hover,
.gradient-button:disabled:hover {
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
border-color: rgba(24, 144, 255, 0.2);
box-shadow: none;
}
/* 加载状态 */
.gradient-button--loading {
cursor: wait;
}
.gradient-button--loading:hover {
transform: none;
}
/* 响应式调整 */
@media (max-width: 768px) {
.gradient-button--large {
padding: 8px 16px;
font-size: 14px;
min-height: 32px;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, onActivated, computed, watch } from 'vue'
import { usePromptStore } from '@/stores/prompt'
import MarkdownIt from 'markdown-it'
import { message } from 'ant-design-vue'
@@ -8,6 +8,8 @@ import useVoiceText from '@gold/hooks/web/useVoiceText'
import GmIcon from '@/components/icons/Icon.vue'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import GradientButton from '@/components/GradientButton.vue'
import { setJSON, getJSON } from '@/utils/storage'
const promptStore = usePromptStore()
const userStore = useUserStore()
@@ -86,8 +88,8 @@ async function loadUserPrompts() {
return timeB - timeA
})
// 如果用户没有选择提示词,默认选中第一个
autoSelectFirstPromptIfNeeded()
// 如果用户没有选择提示词,尝试恢复本地存储的选中项或默认选中第一个
await restoreOrSelectPrompt()
} else {
throw new Error(response?.msg || response?.message || '加载失败')
}
@@ -100,24 +102,86 @@ async function loadUserPrompts() {
}
/**
* 自动选中第一个提示词(如果用户还没有选择)
* 保存选中的提示词ID到本地存储
*/
function autoSelectFirstPromptIfNeeded() {
const hasPrompts = allPrompts.value.length > 0
const hasNoSelection = !selectedPromptId.value && !form.value.prompt
if (hasPrompts && hasNoSelection) {
const firstPrompt = allPrompts.value[0]
if (firstPrompt?.content) {
selectedPromptId.value = firstPrompt.id
form.value.prompt = firstPrompt.content
promptStore.setPrompt(firstPrompt.content, firstPrompt)
}
async function saveSelectedPromptId(promptId) {
if (promptId) {
await setJSON('copywriting_selected_prompt_id', promptId)
}
}
/**
* 从本地存储恢复选中的提示词ID
*/
async function loadSelectedPromptId() {
try {
const savedId = await getJSON('copywriting_selected_prompt_id', null)
return savedId
} catch (error) {
console.error('加载保存的提示词ID失败:', error)
return null
}
}
/**
* 根据ID选中提示词
*/
async function selectPromptById(promptId) {
if (!promptId) return false
const prompt = allPrompts.value.find(p => p.id === promptId)
if (prompt && prompt.content) {
selectedPromptId.value = prompt.id
form.value.prompt = prompt.content
promptStore.setPrompt(prompt.content, prompt)
await saveSelectedPromptId(promptId)
return true
}
return false
}
/**
* 恢复或选中提示词
* 优先级:本地存储的选中项 > 第一个提示词
*/
async function restoreOrSelectPrompt() {
if (allPrompts.value.length === 0) {
return false
}
// 如果已经有选中项且内容存在,不需要重新选择
if (selectedPromptId.value && form.value.prompt) {
// 验证选中的提示词是否还在列表中
const currentPrompt = allPrompts.value.find(p => p.id === selectedPromptId.value)
if (currentPrompt && currentPrompt.content === form.value.prompt) {
return true // 已经正确选中,无需操作
}
}
// 尝试恢复本地存储的选中项
const savedPromptId = await loadSelectedPromptId()
if (savedPromptId) {
const restored = await selectPromptById(savedPromptId)
if (restored) {
return true // 成功恢复保存的选中项
}
}
// 如果没有保存的选中项或恢复失败,则选中第一个
const firstPrompt = allPrompts.value[0]
if (firstPrompt?.content) {
selectedPromptId.value = firstPrompt.id
form.value.prompt = firstPrompt.content
promptStore.setPrompt(firstPrompt.content, firstPrompt)
await saveSelectedPromptId(firstPrompt.id)
return true
}
return false
}
// 选择提示词
function selectPrompt(prompt) {
async function selectPrompt(prompt) {
if (!prompt || !prompt.content) {
message.warning('提示词内容为空')
return
@@ -126,6 +190,7 @@ function selectPrompt(prompt) {
selectedPromptId.value = prompt.id
form.value.prompt = prompt.content
promptStore.setPrompt(prompt.content, prompt)
await saveSelectedPromptId(prompt.id)
showAllPromptsModal.value = false
}
@@ -189,6 +254,29 @@ onMounted(() => {
initializePage()
})
/**
* keep-alive 激活时的处理
* 1. 如果有提示词列表,尝试恢复或选中提示词
* 2. 如果没有提示词列表但用户已登录,加载提示词列表
*/
onActivated(async () => {
// 如果已经有提示词列表,尝试恢复或选中提示词
if (allPrompts.value.length > 0) {
await restoreOrSelectPrompt()
}
// 如果提示词列表为空,但用户已登录,则尝试加载
else if (userStore.userId) {
await loadUserPrompts()
}
// 如果用户未登录,等待用户信息加载
else if (!userStore.userId && userStore.isLoggedIn) {
await ensureUserInfoLoaded()
if (userStore.userId) {
await loadUserPrompts()
}
}
})
// 监听 userId 变化:如果之前没有 userId现在有了则自动加载提示词
watch(() => userStore.userId, async (newUserId, oldUserId) => {
const userIdChanged = newUserId && !oldUserId
@@ -500,23 +588,16 @@ defineOptions({ name: 'ContentStyleCopywriting' })
</a-form-item>
<a-form-item class="form-item">
<a-button
type="primary"
@click="generateCopywriting"
<GradientButton
text="生成文案"
icon="icon-sparkle"
:disabled="!getCurrentInputValue() || !selectedPromptId || isLoading"
:loading="isLoading"
block
loading-text="生成中..."
size="large"
class="generate-btn"
>
<template v-if="!isLoading">
<span class="btn-icon"><GmIcon name="icon-sparkle" :size="16" /></span>
生成文案
</template>
<template v-else>
生成中...
</template>
</a-button>
:block="true"
@click="generateCopywriting"
/>
</a-form-item>
</a-form>
</a-card>
@@ -814,47 +895,7 @@ defineOptions({ name: 'ContentStyleCopywriting' })
display: none;
}
/* 生成按钮样式 */
.generate-btn {
margin-top: 16px;
height: 40px;
border-radius: 6px;
background: var(--color-primary);
border: none;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.generate-btn:hover {
background: var(--color-primary);
box-shadow: var(--glow-primary);
}
.generate-btn:active {
background: var(--color-primary);
}
.btn-icon {
display: inline-flex;
align-items: center;
font-size: 16px;
}
/* 按钮禁用态保持主色,不换颜色(仅本页) */
:deep(.ant-btn-primary[disabled]),
:deep(.ant-btn-primary[disabled]:hover),
:deep(.ant-btn-primary[disabled]:active),
:deep(.ant-btn-primary.ant-btn-disabled),
:deep(.ant-btn-primary.ant-btn-disabled:hover),
:deep(.ant-btn-primary.ant-btn-disabled:active) {
background: var(--color-primary) !important;
border-color: var(--color-primary) !important;
color: #fff !important;
opacity: 0.6; /* 仅降低不透明度,不改变颜色 */
cursor: not-allowed;
box-shadow: none !important;
}
/* 生成按钮样式 - 已替换为 GradientButton 组件 */
/* 操作按钮样式 */
.action-btn {
@@ -1248,11 +1289,6 @@ defineOptions({ name: 'ContentStyleCopywriting' })
margin-right: 10px;
}
.generate-btn {
height: 36px;
font-size: 13px;
}
.generated-content {
padding: 14px;
font-size: 15px;

View File

@@ -1,5 +1,6 @@
<script setup>
import { ref } from 'vue'
import GradientButton from '@/components/GradientButton.vue'
const props = defineProps({
modelValue: {
@@ -52,9 +53,13 @@ function handleReset() {
<div class="form-hint">数量越大越全面但分析时间更长建议 2030</div>
</a-form-item>
<a-space>
<a-button type="primary" :loading="loading" @click="handleAnalyze">
{{ loading ? '分析中' : '开始分析' }}
</a-button>
<GradientButton
text="开始分析"
:loading="loading"
loading-text="分析中"
size="middle"
@click="handleAnalyze"
/>
<a-button @click="handleReset">清空</a-button>
</a-space>
</a-form>

View File

@@ -3,8 +3,9 @@ import { reactive, h } from 'vue'
import { DownloadOutlined } from '@ant-design/icons-vue'
import { formatTime } from '../utils/benchmarkUtils'
import ExpandedRowContent from './ExpandedRowContent.vue'
import GradientButton from '@/components/GradientButton.vue'
const props = defineProps({
defineProps({
data: {
type: Array,
required: true,
@@ -81,9 +82,12 @@ function onExpandedRowKeysChange(keys) {
</template>
导出Excel ({{ selectedRowKeys.length }}/10)
</a-button>
<a-button size="small" type="primary" class="batch-btn" @click="$emit('batchAnalyze')">
批量分析 ({{ selectedRowKeys.length }})
</a-button>
<GradientButton
:text="`批量分析 (${selectedRowKeys.length})`"
size="small"
@click="$emit('batchAnalyze')"
:disabled="selectedRowKeys.length === 0"
/>
</a-space>
</div>
<a-table
@@ -145,9 +149,14 @@ function onExpandedRowKeysChange(keys) {
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" type="primary" :loading="record._analyzing" :disabled="record._analyzing" @click="$emit('analyze', record)">
{{ record._analyzing ? '分析中…' : '分析' }}
</a-button>
<GradientButton
text="分析"
size="small"
:loading="record._analyzing"
loading-text="分析中"
:disabled="record._analyzing"
@click="$emit('analyze', record)"
/>
</a-space>
</template>
</template>

View File

@@ -3,10 +3,8 @@ import { ref, onMounted, reactive, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import dayjs from 'dayjs'
const userStore = useUserStore()
// 表格数据
const dataSource = ref([])
@@ -105,10 +103,11 @@ const columns = [
width: 150,
fixed: 'right',
customRender: ({ record }) => {
return h('div', { style: { display: 'flex', gap: '8px' } }, [
return h('div', { class: 'action-buttons' }, [
h('a-button', {
type: 'link',
size: 'small',
class: 'action-btn action-btn-edit',
onClick: () => handleEdit(record),
}, [
h(EditOutlined),
@@ -118,6 +117,7 @@ const columns = [
type: 'link',
size: 'small',
danger: true,
class: 'action-btn action-btn-delete',
onClick: () => handleDelete(record),
}, [
h(DeleteOutlined),
@@ -199,7 +199,7 @@ async function handleSave() {
try {
await editFormRef.value.validate()
} catch (error) {
return
console.error('保存提示词失败:', error)
}
loading.value = true
@@ -485,5 +485,36 @@ onMounted(() => {
:deep(.ant-pagination) {
margin: 16px;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
/* 使用 :deep() 穿透 Ant Design 的样式 */
:deep(.action-btn) {
transition: all 0.2s ease;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
}
:deep(.action-btn-edit:hover),
:deep(.action-btn-edit:hover .anticon) {
background: rgba(24, 144, 255, 0.1) !important;
color: #1890FF !important;
}
:deep(.action-btn-delete:hover),
:deep(.action-btn-delete:hover .anticon) {
background: rgba(24, 144, 255, 0.1) !important;
color: #1890FF !important;
}
:deep(.action-btn:hover) {
transform: translateY(-1px);
}
</style>

View File

@@ -1,7 +1,11 @@
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
import { CommonService } from '@/api/common'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import GradientButton from '@/components/GradientButton.vue'
// 定义组件名称
defineOptions({
@@ -18,36 +22,240 @@ defineOptions({
// 搜索关键词
const searchKeyword = ref('')
// 搜索参数
const searchParams = reactive({
keyword: '',
offset: 0, // 偏移量第一次请求时为0后续从返回数据中的 cursor 获取
sort_type: 1, // 0:综合排序 1:最多点赞 2:最新发布
publish_time: 7, // 0:不限 1:最近一天 7:最近一周 180:最近半年
filter_duration: '0', // '0':不限 '0-1':1分钟以内 '1-5':1-5分钟 '5-10000':5分钟以上
content_type: 0 // 0:不限 1:视频 2:图集
})
// 加载状态
const isLoading = ref(false)
// 热点列表数据(模拟数据)
// 热点列表数据
const hotTopics = ref([])
// 选中的热点
const selectedTopic = ref(null)
// 当前 cursor用于翻页
const currentCursor = ref(null)
// 用户store
const userStore = useUserStore()
// 右侧详细信息
const topicDetails = reactive({
title: '',
copywriting: '',
stylePrompt: ''
stylePrompt: '',
stylePromptId: null
})
// 生成的文案内容
const generatedContent = ref('')
// 生成文案的loading状态
const isGenerating = ref(false)
// 提示词相关状态
const allPrompts = ref([])
const loadingPrompts = ref(false)
const showAllPromptsModal = ref(false)
const promptSearchKeyword = ref('')
const DISPLAY_COUNT = 6 // 展示的提示词数量
// 计算属性:展示的部分提示词
const displayPrompts = computed(() => {
return allPrompts.value.slice(0, DISPLAY_COUNT)
})
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
const filteredPrompts = computed(() => {
if (!promptSearchKeyword.value.trim()) {
return allPrompts.value
}
const keyword = promptSearchKeyword.value.trim().toLowerCase()
return allPrompts.value.filter(p =>
p.name.toLowerCase().includes(keyword) ||
(p.content && p.content.toLowerCase().includes(keyword))
)
})
/**
* 加载用户提示词列表
*/
async function loadUserPrompts() {
if (!userStore.userId) {
console.warn('用户未登录,无法加载提示词')
return
}
loadingPrompts.value = true
try {
const response = await UserPromptApi.getUserPromptPage({
pageNo: 1,
pageSize: 100,
status: 1 // 只获取启用状态的提示词
})
if (response?.data?.list) {
allPrompts.value = response.data.list
// 如果没有选中的提示词,自动选中第一个
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
const firstPrompt = allPrompts.value[0]
topicDetails.stylePromptId = firstPrompt.id
topicDetails.stylePrompt = firstPrompt.content || ''
}
}
} catch (error) {
console.error('加载提示词失败:', error)
message.error('加载提示词失败')
} finally {
loadingPrompts.value = false
}
}
/**
* 选择提示词
*/
function selectPrompt(prompt) {
if (!prompt || !prompt.content) {
message.warning('提示词内容为空')
return
}
topicDetails.stylePromptId = prompt.id
topicDetails.stylePrompt = prompt.content
showAllPromptsModal.value = false
}
// 点击创作按钮
const handleCreate = (topic) => {
selectedTopic.value = topic.id
topicDetails.title = topic.title
topicDetails.copywriting = ''
topicDetails.stylePrompt = ''
// 提取文案:使用标题作为初始文案
topicDetails.copywriting = topic.title || ''
// 保持已选择的风格提示词,如果没有则使用第一个
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
const firstPrompt = allPrompts.value[0]
topicDetails.stylePromptId = firstPrompt.id
topicDetails.stylePrompt = firstPrompt.content || ''
}
}
// 立即生成
const handleGenerate = () => {
console.log('生成内容', topicDetails)
// TODO: 调用生成API
// 生成文案(流式)
async function handleGenerate() {
if (!topicDetails.copywriting || !topicDetails.copywriting.trim()) {
message.warning('请输入文案内容')
return
}
// 检查是否选择了提示词风格
if (!topicDetails.stylePrompt || !topicDetails.stylePrompt.trim()) {
message.warning('请先选择提示词风格')
return
}
isGenerating.value = true
generatedContent.value = '' // 清空之前的内容
try {
// 调用 callWorkflow 流式 API
const requestData = {
audio_prompt: topicDetails.stylePrompt || '', // 音频提示词
user_text: topicDetails.copywriting.trim(), // 用户输入内容
amplitude: 50 // 幅度默认50%
}
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 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 {
isGenerating.value = false
}
}
// 初始化:加载提示词
onMounted(async () => {
// 等待用户信息加载
if (userStore.userId) {
await loadUserPrompts()
} else if (userStore.isLoggedIn) {
// 如果已登录但userId未加载等待一下
setTimeout(async () => {
if (userStore.userId) {
await loadUserPrompts()
}
}, 500)
}
})
// 切换平台
// const switchPlatform = (platformId) => {
// activePlatform.value = platformId
@@ -57,51 +265,103 @@ const handleGenerate = () => {
// topicDetails.stylePrompt = ''
// }
// 搜索热点
/**
* 处理搜索结果响应
*/
function processSearchResults(response, startId = 1) {
try {
// 提取 cursor用于翻页
const cursor = response?.data?.cursor || null
currentCursor.value = cursor
// 处理搜索结果
const dataList = response?.data?.data || []
const searchResults = dataList
.map(el => el.aweme_info)
.filter(el => el)
.map((item, index) => ({
id: startId + index,
title: item.desc || '无标题',
videoId: item.aweme_id,
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`,
// 封面图片:优先使用 origin_cover其次 cover再次 dynamic_cover最后 animated_cover
cover: item?.video?.origin_cover?.url_list?.[0]
|| item?.video?.cover?.url_list?.[0]
|| item?.video?.dynamic_cover?.url_list?.[0]
|| item?.video?.animated_cover?.url_list?.[0]
|| item?.cover?.url_list?.[0]
|| '',
// 作者信息
author: item.author?.nickname || item.author?.unique_id || '未知',
authorAvatar: item.author?.avatar_thumb?.url_list?.[0] || item.author?.avatar_larger?.url_list?.[0] || '',
// 统计数据
playCount: item.statistics?.play_count || 0,
diggCount: item.statistics?.digg_count || 0,
commentCount: item.statistics?.comment_count || 0,
shareCount: item.statistics?.share_count || 0,
collectCount: item.statistics?.collect_count || 0,
}))
return searchResults
} catch (error) {
console.error('处理搜索结果失败:', error)
return []
}
}
/**
* 搜索热点
*/
const handleSearch = async () => {
if (!searchKeyword.value.trim()) {
message.warning('请输入搜索关键词')
return
}
// 重置参数
searchParams.offset = 0
searchParams.keyword = searchKeyword.value.trim()
currentCursor.value = null
hotTopics.value = []
isLoading.value = true
try {
const response = await TikhubService.postTikHup(
{
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH, // 使用网页端通用搜索结果接口
methodType: MethodType.POST,
urlParams: {
keyword: searchKeyword.value.trim(),
sort_type: '1',
offset: 0,
count: 20,
publish_time: 7
},
paramType: ParamType.JSON
}
)
// 处理搜索结果
const searchResults = response.data.data.map(el => el.aweme_info).filter(el => el).map((item, index) => ({
id: hotTopics.value.length + index + 1,
title: item.desc || '无标题',
videoId: item.aweme_id,
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`, // 视频链接
author: item.author.nickname,
// 统计数据
playCount: item.statistics?.play_count || 0, // 播放量
diggCount: item.statistics?.digg_count || 0, // 点赞量(喜欢)
commentCount: item.statistics?.comment_count || 0, // 评论数
shareCount: item.statistics?.share_count || 0, // 分享数
collectCount: item.statistics?.collect_count || 0, // 收藏数
}))
// 构建请求参数POST JSON 格式)
// 注意API 要求 sort_type、publish_time、content_type 为字符串类型
const urlParams = {
keyword: searchParams.keyword,
offset: String(searchParams.offset),
count: '20',
sort_type: String(searchParams.sort_type),
publish_time: String(searchParams.publish_time),
filter_duration: searchParams.filter_duration, // 已经是字符串
content_type: String(searchParams.content_type),
}
// 将搜索结果添加到列表顶部
hotTopics.value = [...searchResults, ...hotTopics.value]
const response = await TikhubService.postTikHup({
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH,
methodType: MethodType.POST,
urlParams: urlParams,
paramType: ParamType.JSON
})
// 处理搜索结果
const searchResults = processSearchResults(response)
if (searchResults.length === 0) {
message.warning('未找到相关结果')
hotTopics.value = []
return
}
// 替换列表
hotTopics.value = searchResults
message.success(`找到 ${searchResults.length} 个结果`)
} catch (error) {
console.error('搜索失败:', error)
message.error(error?.message || '搜索失败,请稍后重试')
hotTopics.value = []
} finally {
isLoading.value = false
}
@@ -137,6 +397,11 @@ const truncateTitle = (title, maxLength = 30) => {
if (title.length <= maxLength) return title
return title.substring(0, maxLength) + '...'
}
// 处理图片加载错误
const handleImageError = (event) => {
event.target.style.display = 'none'
}
</script>
<template>
@@ -166,20 +431,58 @@ const truncateTitle = (title, maxLength = 30) => {
<div class="search-input-wrapper">
<input v-model="searchKeyword" type="text" placeholder="输入关键词搜索抖音热点..." class="search-input"
:disabled="isLoading" @keypress="handleSearchKeypress" />
<button @click="handleSearch" :disabled="isLoading || !searchKeyword.trim()" class="search-btn"
:class="{ 'search-btn--loading': isLoading }">
<svg v-if="!isLoading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button @click="handleSearch" :disabled="isLoading || !searchKeyword.trim()" class="search-btn">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg v-else class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
</button>
</div>
<!-- 搜索参数配置 -->
<div class="search-params">
<div class="param-row">
<div class="param-item">
<label class="param-label">排序方式</label>
<select v-model="searchParams.sort_type" class="param-select">
<option :value="0">综合排序</option>
<option :value="1">最多点赞</option>
<option :value="2">最新发布</option>
</select>
</div>
<div class="param-item">
<label class="param-label">发布时间</label>
<select v-model="searchParams.publish_time" class="param-select">
<option value="0">不限</option>
<option value="1">最近一天</option>
<option value="7">最近一周</option>
<option value="180">最近半年</option>
</select>
</div>
<div class="param-item">
<label class="param-label">视频时长</label>
<select v-model="searchParams.filter_duration" class="param-select">
<option value="0">不限</option>
<option value="0-1">1分钟以内</option>
<option value="1-5">1-5分钟</option>
<option value="5-10000">5分钟以上</option>
</select>
</div>
</div>
<div class="param-row">
<div class="param-item">
<label class="param-label">内容类型</label>
<select v-model="searchParams.content_type" class="param-select">
<option value="0">不限</option>
<option value="1">视频</option>
<option value="2">图集</option>
</select>
</div>
</div>
</div>
</div>
<!-- 热点列表 -->
@@ -199,19 +502,52 @@ const truncateTitle = (title, maxLength = 30) => {
<div v-for="topic in hotTopics" :key="topic.id" @click="handleCreate(topic)" class="topic-item"
:class="{ 'topic-item--selected': selectedTopic === topic.id }">
<div class="flex flex-col flex-1 min-w-0">
<div class="flex items-center mb-2">
<span class="topic-number">{{ topic.id }}</span>
<span
v-if="topic.videoUrl"
@click="openVideo(topic, $event)"
class="flex-1 topic-title topic-title--clickable"
:title="topic.title"
>
{{ truncateTitle(topic.title) }}
</span>
<span v-else class="flex-1 topic-title" :title="topic.title">
{{ truncateTitle(topic.title) }}
</span>
<!-- 封面和标题 -->
<div class="flex items-start gap-3 mb-2">
<!-- 封面图片 -->
<div class="topic-cover-wrapper">
<img
v-if="topic.cover"
:src="topic.cover"
alt="封面"
class="topic-cover"
@error="handleImageError"
/>
<div v-else class="topic-cover-placeholder">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
</div>
<!-- 标题和作者 -->
<div class="flex-1 min-w-0">
<div class="flex items-center mb-1">
<span class="topic-number">{{ topic.id }}</span>
<span
v-if="topic.videoUrl"
@click="openVideo(topic, $event)"
class="flex-1 topic-title topic-title--clickable"
:title="topic.title"
>
{{ truncateTitle(topic.title) }}
</span>
<span v-else class="flex-1 topic-title" :title="topic.title">
{{ truncateTitle(topic.title) }}
</span>
</div>
<!-- 作者信息 -->
<div class="flex items-center gap-2 text-xs text-gray-500 topic-author">
<img
v-if="topic.authorAvatar"
:src="topic.authorAvatar"
alt="作者头像"
class="author-avatar"
@error="handleImageError"
/>
<span>{{ topic.author }}</span>
</div>
</div>
</div>
<!-- 统计信息 -->
<div v-if="topic.diggCount || topic.playCount || topic.commentCount || topic.collectCount || topic.shareCount" class="flex flex-wrap items-center gap-4 text-xs text-gray-500 topic-stats">
@@ -247,11 +583,19 @@ const truncateTitle = (title, maxLength = 30) => {
</span>
</div>
</div>
<button @click.stop="handleCreate(topic)" class="create-btn">
创作
</button>
<GradientButton
text="创作"
size="small"
@click.stop="handleCreate(topic)"
/>
</div>
</div>
<!-- Loading 指示器 -->
<div v-if="isLoading" class="loading-indicator">
<a-spin size="small" />
<span class="loading-text">搜索中...</span>
</div>
</div>
</section>
@@ -275,23 +619,107 @@ const truncateTitle = (title, maxLength = 30) => {
<!-- 风格提示词 -->
<div>
<label class="form-label">风格提示词</label>
<input v-model="topicDetails.stylePrompt" type="text" placeholder="例如:专业、权威、温暖、幽默等" class="form-input" />
<div class="form-label-wrapper">
<label class="form-label">风格提示词</label>
<a-button
v-if="allPrompts.length > DISPLAY_COUNT"
size="small"
type="link"
@click="showAllPromptsModal = true"
style="padding: 0; height: auto; font-size: 12px;">
更多 ({{ allPrompts.length }})
</a-button>
</div>
<!-- 提示词标签展示区域 -->
<div v-if="displayPrompts.length > 0" class="prompt-tags-container">
<div class="prompt-tags-grid">
<div
v-for="prompt in displayPrompts"
:key="prompt.id"
class="prompt-tag"
:class="{ 'prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
@click="selectPrompt(prompt)">
<span class="prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="!loadingPrompts" class="prompt-empty">
<div style="color: var(--color-text-secondary); font-size: 12px; text-align: center; padding: 20px;">
您可以在视频分析页面保存风格
</div>
</div>
<!-- 加载状态 -->
<div v-else class="prompt-loading">
<a-spin size="small" />
</div>
</div>
<!-- 立即生成按钮 -->
<!-- 生成文案按钮 -->
<div class="pt-2">
<button @click="handleGenerate" class="generate-btn">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
clip-rule="evenodd" />
</svg>
立即生成
</button>
<GradientButton
text="生成文案"
icon="icon-sparkle"
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating"
:loading="isGenerating"
loading-text="生成中..."
size="middle"
:block="true"
@click="handleGenerate"
/>
</div>
<!-- 生成的文案显示区域 -->
<div v-if="generatedContent" class="generated-content-section">
<div class="form-label">生成结果</div>
<div class="generated-content-wrapper">
<div class="generated-content-text">{{ generatedContent }}</div>
</div>
</div>
</div>
</section>
<!-- 更多提示词弹窗 -->
<a-modal
v-model:open="showAllPromptsModal"
title="选择提示词风格"
:width="600"
:footer="null">
<div class="prompt-modal-content">
<!-- 搜索框 -->
<a-input
v-model:value="promptSearchKeyword"
placeholder="搜索提示词..."
style="margin-bottom: 16px;"
allow-clear>
<template #prefix>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</template>
</a-input>
<!-- 提示词列表 -->
<div v-if="filteredPrompts.length > 0" class="all-prompts-grid">
<div
v-for="prompt in filteredPrompts"
:key="prompt.id"
class="all-prompt-tag"
:class="{ 'all-prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
@click="selectPrompt(prompt)">
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-else style="text-align: center; padding: 40px; color: var(--color-text-secondary);">
没有找到匹配的提示词
</div>
</div>
</a-modal>
</div>
</div>
</template>
@@ -368,6 +796,52 @@ const truncateTitle = (title, maxLength = 30) => {
border-bottom: 1px solid var(--color-border);
}
/* 搜索参数配置 */
.search-params {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--color-border);
}
.param-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.param-item {
flex: 1;
min-width: 120px;
}
.param-label {
display: block;
font-size: 12px;
color: var(--color-text-secondary);
margin-bottom: 4px;
}
.param-select {
width: 100%;
padding: 6px 8px;
font-size: 13px;
color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 4px;
transition: all 0.2s;
}
.param-select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
.param-select:hover {
border-color: var(--color-primary);
}
.search-input-wrapper {
display: flex;
gap: 8px;
@@ -426,8 +900,18 @@ const truncateTitle = (title, maxLength = 30) => {
transform: none;
}
.search-btn--loading {
opacity: 0.8;
/* Loading 指示器 */
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
color: var(--color-text-secondary);
}
.loading-text {
font-size: 14px;
}
/* 热点列表 */
@@ -536,6 +1020,47 @@ const truncateTitle = (title, maxLength = 30) => {
opacity: 0.8;
}
/* 封面图片 */
.topic-cover-wrapper {
flex-shrink: 0;
width: 80px;
height: 45px;
border-radius: 4px;
overflow: hidden;
background: var(--color-bg);
border: 1px solid var(--color-border);
}
.topic-cover {
width: 100%;
height: 100%;
object-fit: cover;
}
.topic-cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
opacity: 0.4;
}
/* 作者信息 */
.topic-author {
padding-left: 32px;
margin-top: 4px;
}
.author-avatar {
width: 16px;
height: 16px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
/* 统计信息 */
.topic-stats {
padding-left: 32px;
@@ -554,28 +1079,6 @@ const truncateTitle = (title, maxLength = 30) => {
vertical-align: middle;
}
/* 创作按钮 */
.create-btn {
margin-left: auto;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
color: white;
background: var(--color-primary);
border: none;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 176, 48, 0.2);
transition: all 0.2s;
flex-shrink: 0;
align-self: flex-start;
margin-top: 4px;
}
.create-btn:hover {
box-shadow: 0 0 12px rgba(0, 176, 48, 0.5);
filter: brightness(1.1);
transform: scale(1.05);
}
/* 详情内容 */
.detail-content {
@@ -620,23 +1123,142 @@ const truncateTitle = (title, maxLength = 30) => {
resize: none;
}
.generate-btn {
width: 100%;
padding: 8px 24px;
background: var(--color-primary);
color: white;
font-weight: 600;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 176, 48, 0.3);
transition: all 0.2s;
/* 表单标签包装器 */
.form-label-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.generate-btn:hover {
box-shadow: 0 0 12px rgba(0, 176, 48, 0.4);
filter: brightness(1.05);
transform: scale(1.01);
/* 提示词标签样式 */
.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;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
color: var(--color-text);
}
.prompt-tag:hover {
border-color: var(--color-primary);
background: rgba(24, 144, 255, 0.08);
transform: translateY(-1px);
}
.prompt-tag-selected {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.prompt-tag-selected:hover {
background: var(--color-primary);
filter: brightness(1.1);
}
.prompt-tag-name {
white-space: nowrap;
user-select: none;
}
.prompt-empty {
padding: 20px;
text-align: center;
}
.prompt-loading {
display: flex;
justify-content: center;
padding: 20px;
}
/* 生成结果区域 */
.generated-content-section {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--color-border);
}
.generated-content-wrapper {
margin-top: 12px;
padding: 16px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
max-height: 400px;
overflow-y: auto;
}
.generated-content-text {
color: var(--color-text);
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 更多提示词弹窗样式 */
.prompt-modal-content {
max-height: 500px;
overflow-y: auto;
}
.all-prompts-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.all-prompt-tag {
display: inline-flex;
align-items: center;
padding: 8px 16px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
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);
transform: translateY(-1px);
}
.all-prompt-tag-selected {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.all-prompt-tag-selected:hover {
background: var(--color-primary);
filter: brightness(1.1);
}
.all-prompt-tag-name {
white-space: nowrap;
user-select: none;
}
</style>

View File

@@ -59,36 +59,103 @@ public class TikHupServiceImpl implements TikHupService {
}
@Override
public Object postTikHup(String type,String methodType,String urlParams,String paramType){
public Object postTikHup(String type, String methodType, String urlParams, String paramType) {
// 1. 参数校验
if (StringUtils.isBlank(type)) {
log.error("postTikHup: type 参数为空");
return CommonResult.error(400, "接口类型不能为空");
}
if (StringUtils.isBlank(methodType)) {
log.error("postTikHup: methodType 参数为空");
return CommonResult.error(400, "请求方法类型不能为空");
}
// 2. 获取接口配置信息
TikTokenVO tikTokenVO = tikTokenMapper.getInterfaceUrl(type);
String Authorization = tikTokenVO.getPlatformToken();
if (tikTokenVO == null) {
log.error("postTikHup: 未找到接口类型 {} 的配置信息", type);
return CommonResult.error(404, "未找到接口类型 " + type + " 的配置信息");
}
String authorization = tikTokenVO.getPlatformToken();
String url = tikTokenVO.getPlatformUrl();
try{
if (StringUtils.isBlank(authorization)) {
log.error("postTikHup: 接口类型 {} 的 token 为空", type);
return CommonResult.error(500, "接口配置错误token 为空");
}
if (StringUtils.isBlank(url)) {
log.error("postTikHup: 接口类型 {} 的 URL 为空", type);
return CommonResult.error(500, "接口配置错误URL 为空");
}
// 3. 统一转换为小写进行比较(兼容大小写)
String methodTypeLower = methodType.toLowerCase();
String paramTypeLower = paramType != null ? paramType.toLowerCase() : "";
try {
Unirest.setTimeouts(0, 0);
HttpResponse<String> response;
if("post".equals(methodType) && "json".equals(paramType)){
// 4. 根据请求方法和参数类型构建请求
if ("post".equals(methodTypeLower) && "json".equals(paramTypeLower)) {
// POST + JSON: 将 urlParams 作为 JSON body
log.debug("postTikHup: POST JSON 请求, URL: {}, Body: {}", url, urlParams);
response = Unirest.post(url)
.header("Authorization", "Bearer "+Authorization)
.header("Authorization", "Bearer " + authorization)
.header("Content-Type", "application/json")
.body(urlParams)
.asString();
} else if("post".equals(methodType)){
} else if ("post".equals(methodTypeLower)) {
// POST + 表单参数: 将 urlParams 作为 URL 查询参数
log.debug("postTikHup: POST 表单请求, URL: {}?{}", url, urlParams);
response = Unirest.post(url + "?" + urlParams)
.header("Authorization", "Bearer "+Authorization)
.header("Authorization", "Bearer " + authorization)
.asString();
} else {
response = Unirest.get(url + "?" + urlParams)
.header("Authorization", "Bearer "+Authorization)
// GET 或其他方法: 将 urlParams 作为 URL 查询参数
// 处理 URL 拼接:如果 URL 已包含查询参数,使用 & 连接,否则使用 ? 连接
String finalUrl = url;
if (urlParams != null && !urlParams.trim().isEmpty()) {
if (url.contains("?")) {
// URL 已包含查询参数,使用 & 连接
finalUrl = url + "&" + urlParams;
} else {
// URL 不包含查询参数,使用 ? 连接
finalUrl = url + "?" + urlParams;
}
}
log.info("postTikHup: GET 请求, 原始URL: {}, 参数: {}, 最终URL: {}", url, urlParams, finalUrl);
response = Unirest.get(finalUrl)
.header("Authorization", "Bearer " + authorization)
.asString();
}
Long userId = SecurityFrameworkUtils.getLoginUser().getId();
if(response.getBody() != null){
return response.getBody();
// 5. 检查响应状态码
int statusCode = response.getStatus();
String responseBody = response.getBody();
if (statusCode == 200) {
if (StringUtils.isNotBlank(responseBody)) {
// 尝试解析为 JSON如果失败则直接返回字符串
try {
return JSON.parseObject(responseBody);
} catch (Exception e) {
// 如果不是 JSON直接返回字符串
return responseBody;
}
} else {
log.warn("postTikHup: 接口返回空响应, URL: {}", url);
return CommonResult.error(500, "接口返回空响应");
}
} else {
log.error("postTikHup: 接口调用失败, URL: {}, 状态码: {}, 响应: {}", url, statusCode, responseBody);
return CommonResult.error(statusCode, "接口调用失败: " + (StringUtils.isNotBlank(responseBody) ? responseBody : "HTTP " + statusCode));
}
}catch (Exception e){
log.error("{}接口调用异常",url);
} catch (Exception e) {
log.error("postTikHup: 接口调用异常, URL: {}, 错误信息: {}", url, e.getMessage(), e);
return CommonResult.error(500, "接口调用异常: " + e.getMessage());
}
return new HashMap<>();
}
private final String appKey = "sldJ4XSpYp3rKALZ ";

View File

@@ -32,9 +32,36 @@ public class AppUserPromptController {
@PostMapping("/create")
@Operation(summary = "创建用户提示词")
public CommonResult<Long> createUserPrompt(@Valid @RequestBody UserPromptSaveReqVO createReqVO) {
// 设置当前登录用户ID
createReqVO.setUserId(getLoginUserId());
public CommonResult<Long> createUserPrompt(@RequestBody UserPromptSaveReqVO createReqVO) {
// 设置当前登录用户ID(在验证之前设置,避免 @NotNull 验证失败)
Long userId = getLoginUserId();
if (userId == null) {
return CommonResult.error(401, "用户未登录");
}
createReqVO.setUserId(userId);
// 手动验证必要字段
if (createReqVO.getName() == null || createReqVO.getName().trim().isEmpty()) {
return CommonResult.error(400, "提示词名称不能为空");
}
if (createReqVO.getContent() == null || createReqVO.getContent().trim().isEmpty()) {
return CommonResult.error(400, "提示词内容不能为空");
}
if (createReqVO.getStatus() == null) {
return CommonResult.error(400, "状态不能为空");
}
// 设置默认值(如果前端没有传递)
if (createReqVO.getIsPublic() == null) {
createReqVO.setIsPublic(false); // 默认私有
}
if (createReqVO.getSort() == null) {
createReqVO.setSort(0); // 默认排序为 0
}
if (createReqVO.getUseCount() == null) {
createReqVO.setUseCount(0); // 默认使用次数为 0
}
return success(userPromptService.createUserPrompt(createReqVO));
}