This commit is contained in:
2026-02-23 22:29:43 +08:00
parent e1627eb48c
commit 07388db632
6 changed files with 175 additions and 81 deletions

View File

@@ -11,7 +11,6 @@
2. **遵循项目规范**严格执行《CLAUDE.md》文档中既定的编码规范具体包括
- 使用 ES 模块,规范导入语句的排序规则与文件扩展名的使用方式
- 优先使用 `function` 关键字定义函数,而非箭头函数
- 为顶层函数添加显式的返回值类型注解
- 遵循标准的 React 组件开发规范定义显式的组件属性类型Props types
- 采用合理的错误处理方案(尽可能避免滥用 try/catch 语句)

View File

@@ -47,18 +47,6 @@
<span class="loading-text">加载中...</span>
</div>
<!-- 空状态 -->
<div v-else-if="!loading && allPrompts.length === 0" class="prompt-empty-state">
<div class="prompt-empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<p class="prompt-empty-text">没有找到文案风格</p>
</div>
<!-- 更多提示词弹窗 -->
<div v-if="showAllPromptsModal" class="prompt-modal-mask" @click.self="showAllPromptsModal = false">
<div class="prompt-modal">

View File

@@ -1,12 +1,13 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { CommonService } from '@/api/common'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import PromptSelector from '@/components/PromptSelector.vue'
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
import { UserPromptApi } from '@/api/userPrompt'
import PromptSelector from '@/components/PromptSelector.vue'
import { useUserStore } from '@/stores/user'
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
defineOptions({ name: 'ForecastView' })
@@ -45,31 +46,40 @@ const loadingPrompts = ref(false)
const promptSearchKeyword = ref('')
// 工具函数
const formatNumber = (num) => {
function formatNumber(num) {
if (!num) return '0'
return num >= 10000 ? `${(num / 10000).toFixed(1)}w` : num.toString()
return num >= 10000 ? `${(num / 10000).toFixed(1)}w` : String(num)
}
const truncateTitle = (title, maxLength = 28) => {
function truncateTitle(title, maxLength = 28) {
if (!title) return ''
return title.length <= maxLength ? title : `${title.substring(0, maxLength)}...`
}
const handleImageError = (event) => {
function handleImageError(event) {
event.target.style.display = 'none'
}
const openVideo = (topic, event) => {
function openVideo(topic, event) {
event.stopPropagation()
if (topic.videoUrl) window.open(topic.videoUrl, '_blank')
}
const handleSearchKeypress = (event) => {
function handleSearchKeypress(event) {
if (event.key === 'Enter' && !isLoading.value) {
handleSearch()
}
}
async function copyContent() {
try {
await navigator.clipboard.writeText(generatedContent.value)
message.success('已复制')
} catch {
message.error('复制失败')
}
}
// 提示词管理
async function loadUserPrompts() {
if (!userStore.userId) {
@@ -177,22 +187,12 @@ async function handleGenerate() {
const ctrl = new AbortController()
let fullText = ''
let errorOccurred = false
let isResolved = false
let completed = false
await new Promise((resolve, reject) => {
let timeout = null
const cleanup = () => {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
timeout = setTimeout(() => {
if (!isResolved) {
cleanup()
const timeout = setTimeout(() => {
if (!completed) {
completed = true
ctrl.abort()
reject(new Error('请求超时'))
}
@@ -202,25 +202,21 @@ async function handleGenerate() {
data: requestData,
ctrl,
onMessage: (event) => {
if (errorOccurred || !event?.data) return
if (completed || !event?.data) return
const dataStr = event.data.trim()
if (dataStr === '[DONE]') {
cleanup()
if (!isResolved) {
isResolved = true
completed = true
clearTimeout(timeout)
resolve()
}
return
}
if (dataStr.startsWith('[TIMEOUT]')) {
cleanup()
if (!isResolved) {
errorOccurred = true
isResolved = true
completed = true
clearTimeout(timeout)
reject(new Error(dataStr.replace('[TIMEOUT]', '').trim() || '请求超时'))
}
return
}
@@ -231,7 +227,7 @@ async function handleGenerate() {
fullText += piece
generatedContent.value = fullText
}
} catch (e) {
} catch {
if (event.data && !event.data.startsWith('[')) {
fullText += event.data
generatedContent.value = fullText
@@ -239,20 +235,18 @@ async function handleGenerate() {
}
},
onError: (err) => {
cleanup()
if (!isResolved) {
errorOccurred = true
isResolved = true
if (!completed) {
completed = true
clearTimeout(timeout)
ctrl.abort()
const errorMsg = err?.message || '网络请求失败'
message.error(errorMsg)
reject(new Error(errorMsg))
message.error(err?.message || '网络请求失败')
reject(new Error(err?.message || '网络请求失败'))
}
},
onClose: () => {
cleanup()
if (!isResolved) {
isResolved = true
if (!completed) {
completed = true
clearTimeout(timeout)
resolve()
}
}
@@ -273,11 +267,11 @@ async function handleGenerate() {
function extractAudioUrl(video) {
const urlList = video?.play_addr?.url_list
if (Array.isArray(urlList) && urlList.length > 0) {
const lastUrl = urlList[urlList.length - 1]
const firstUrl = urlList[0]
return (lastUrl && lastUrl.trim()) || (firstUrl && firstUrl.trim()) || ''
const lastUrl = urlList[urlList.length - 1]?.trim()
const firstUrl = urlList[0]?.trim()
return lastUrl || firstUrl || ''
}
return (video?.play_addr?.url && video.play_addr.url.trim()) || ''
return video?.play_addr?.url?.trim() || ''
}
function extractCover(video) {
@@ -369,12 +363,12 @@ async function handleSearch() {
}
// 初始化
onMounted(async () => {
onMounted(() => {
if (userStore.userId) {
await loadUserPrompts()
loadUserPrompts()
} else if (userStore.isLoggedIn) {
setTimeout(async () => {
if (userStore.userId) await loadUserPrompts()
setTimeout(() => {
if (userStore.userId) loadUserPrompts()
}, 500)
}
})
@@ -608,7 +602,7 @@ onMounted(async () => {
<span class="result-label">生成结果</span>
<button
class="copy-btn"
@click="navigator.clipboard.writeText(generatedContent); message.success('已复制')"
@click="copyContent"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2"/>

View File

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.tik.dify.service;
import cn.iocoder.yudao.module.tik.dify.client.DifyClient;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatRespVO;
import cn.iocoder.yudao.module.tik.enums.AiModelTypeEnum;
import cn.iocoder.yudao.module.tik.enums.AiPlatformEnum;
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentDO;
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentService;
import cn.iocoder.yudao.module.tik.muye.aimodelconfig.dal.AiModelConfigDO;
@@ -27,13 +29,6 @@ import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class DifyServiceImpl implements DifyService {
/** Dify 平台标识 */
private static final String PLATFORM_DIFY = "dify";
/** Dify 模型类型 - Pro深度版 */
private static final String MODEL_TYPE_WRITING_PRO = "writing_pro";
/** Dify 模型类型 - 标准版 */
private static final String MODEL_TYPE_WRITING_STANDARD = "writing_standard";
@Resource
private AiAgentService aiAgentService;
@@ -58,10 +53,12 @@ public class DifyServiceImpl implements DifyService {
}
// 2. 根据 modelMode 获取对应的积分配置
String modelType = "standard".equals(reqVO.getModelMode())
? MODEL_TYPE_WRITING_STANDARD
: MODEL_TYPE_WRITING_PRO;
AiModelConfigDO config = pointsService.getConfig(PLATFORM_DIFY, modelType);
AiModelTypeEnum modelTypeEnum = "standard".equals(reqVO.getModelMode())
? AiModelTypeEnum.DIFY_WRITING_STANDARD
: AiModelTypeEnum.DIFY_WRITING_PRO;
AiModelConfigDO config = pointsService.getConfig(
AiPlatformEnum.DIFY.getPlatform(),
modelTypeEnum.getModelType());
// 3. 预检积分
pointsService.checkPoints(userId, config.getConsumePoints());

View File

@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.tik.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* AI 模型类型枚举
* 统一管理所有 AI 服务的模型类型标识
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum AiModelTypeEnum implements ArrayValuable<String> {
// ========== Dify 写作模型 ==========
DIFY_WRITING_PRO("writing_pro", "Pro深度版", AiPlatformEnum.DIFY),
DIFY_WRITING_STANDARD("writing_standard", "标准版", AiPlatformEnum.DIFY),
// ========== 数字人模型 ==========
DIGITAL_HUMAN_LATENTSYNC("latentsync", "LatentSync", AiPlatformEnum.DIGITAL_HUMAN),
DIGITAL_HUMAN_KLING("kling", "可灵", AiPlatformEnum.DIGITAL_HUMAN),
;
/**
* 模型类型标识
*/
private final String modelType;
/**
* 模型类型名称
*/
private final String name;
/**
* 所属平台
*/
private final AiPlatformEnum platform;
public static final String[] ARRAYS = Arrays.stream(values()).map(AiModelTypeEnum::getModelType).toArray(String[]::new);
@Override
public String[] array() {
return ARRAYS;
}
/**
* 根据模型类型标识获取枚举
*/
public static AiModelTypeEnum valueOfModelType(String modelType) {
return Arrays.stream(values())
.filter(e -> e.getModelType().equals(modelType))
.findFirst()
.orElse(null);
}
/**
* 根据平台获取该平台下所有模型类型
*/
public static AiModelTypeEnum[] valuesByPlatform(AiPlatformEnum platform) {
return Arrays.stream(values())
.filter(e -> e.getPlatform() == platform)
.toArray(AiModelTypeEnum[]::new);
}
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.tik.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* AI 平台枚举
* 统一管理所有 AI 服务的平台标识
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum AiPlatformEnum implements ArrayValuable<String> {
DIFY("dify", "Dify 平台"),
DIGITAL_HUMAN("digital_human", "数字人平台"),
;
/**
* 平台标识
*/
private final String platform;
/**
* 平台名称
*/
private final String name;
public static final String[] ARRAYS = Arrays.stream(values()).map(AiPlatformEnum::getPlatform).toArray(String[]::new);
@Override
public String[] array() {
return ARRAYS;
}
/**
* 根据平台标识获取枚举
*/
public static AiPlatformEnum valueOfPlatform(String platform) {
return Arrays.stream(values())
.filter(e -> e.getPlatform().equals(platform))
.findFirst()
.orElse(null);
}
}