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

View File

@@ -47,18 +47,6 @@
<span class="loading-text">加载中...</span> <span class="loading-text">加载中...</span>
</div> </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 v-if="showAllPromptsModal" class="prompt-modal-mask" @click.self="showAllPromptsModal = false">
<div class="prompt-modal"> <div class="prompt-modal">

View File

@@ -1,12 +1,13 @@
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue' import { onMounted, reactive, ref } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { CommonService } from '@/api/common' 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 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' }) defineOptions({ name: 'ForecastView' })
@@ -45,31 +46,40 @@ const loadingPrompts = ref(false)
const promptSearchKeyword = ref('') const promptSearchKeyword = ref('')
// 工具函数 // 工具函数
const formatNumber = (num) => { function formatNumber(num) {
if (!num) return '0' 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 '' if (!title) return ''
return title.length <= maxLength ? title : `${title.substring(0, maxLength)}...` return title.length <= maxLength ? title : `${title.substring(0, maxLength)}...`
} }
const handleImageError = (event) => { function handleImageError(event) {
event.target.style.display = 'none' event.target.style.display = 'none'
} }
const openVideo = (topic, event) => { function openVideo(topic, event) {
event.stopPropagation() event.stopPropagation()
if (topic.videoUrl) window.open(topic.videoUrl, '_blank') if (topic.videoUrl) window.open(topic.videoUrl, '_blank')
} }
const handleSearchKeypress = (event) => { function handleSearchKeypress(event) {
if (event.key === 'Enter' && !isLoading.value) { if (event.key === 'Enter' && !isLoading.value) {
handleSearch() handleSearch()
} }
} }
async function copyContent() {
try {
await navigator.clipboard.writeText(generatedContent.value)
message.success('已复制')
} catch {
message.error('复制失败')
}
}
// 提示词管理 // 提示词管理
async function loadUserPrompts() { async function loadUserPrompts() {
if (!userStore.userId) { if (!userStore.userId) {
@@ -177,22 +187,12 @@ async function handleGenerate() {
const ctrl = new AbortController() const ctrl = new AbortController()
let fullText = '' let fullText = ''
let errorOccurred = false let completed = false
let isResolved = false
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
let timeout = null const timeout = setTimeout(() => {
if (!completed) {
const cleanup = () => { completed = true
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
timeout = setTimeout(() => {
if (!isResolved) {
cleanup()
ctrl.abort() ctrl.abort()
reject(new Error('请求超时')) reject(new Error('请求超时'))
} }
@@ -202,25 +202,21 @@ async function handleGenerate() {
data: requestData, data: requestData,
ctrl, ctrl,
onMessage: (event) => { onMessage: (event) => {
if (errorOccurred || !event?.data) return if (completed || !event?.data) return
const dataStr = event.data.trim() const dataStr = event.data.trim()
if (dataStr === '[DONE]') { if (dataStr === '[DONE]') {
cleanup() completed = true
if (!isResolved) { clearTimeout(timeout)
isResolved = true resolve()
resolve()
}
return return
} }
if (dataStr.startsWith('[TIMEOUT]')) { if (dataStr.startsWith('[TIMEOUT]')) {
cleanup() completed = true
if (!isResolved) { clearTimeout(timeout)
errorOccurred = true reject(new Error(dataStr.replace('[TIMEOUT]', '').trim() || '请求超时'))
isResolved = true
reject(new Error(dataStr.replace('[TIMEOUT]', '').trim() || '请求超时'))
}
return return
} }
@@ -231,7 +227,7 @@ async function handleGenerate() {
fullText += piece fullText += piece
generatedContent.value = fullText generatedContent.value = fullText
} }
} catch (e) { } catch {
if (event.data && !event.data.startsWith('[')) { if (event.data && !event.data.startsWith('[')) {
fullText += event.data fullText += event.data
generatedContent.value = fullText generatedContent.value = fullText
@@ -239,20 +235,18 @@ async function handleGenerate() {
} }
}, },
onError: (err) => { onError: (err) => {
cleanup() if (!completed) {
if (!isResolved) { completed = true
errorOccurred = true clearTimeout(timeout)
isResolved = true
ctrl.abort() ctrl.abort()
const errorMsg = err?.message || '网络请求失败' message.error(err?.message || '网络请求失败')
message.error(errorMsg) reject(new Error(err?.message || '网络请求失败'))
reject(new Error(errorMsg))
} }
}, },
onClose: () => { onClose: () => {
cleanup() if (!completed) {
if (!isResolved) { completed = true
isResolved = true clearTimeout(timeout)
resolve() resolve()
} }
} }
@@ -273,11 +267,11 @@ async function handleGenerate() {
function extractAudioUrl(video) { function extractAudioUrl(video) {
const urlList = video?.play_addr?.url_list const urlList = video?.play_addr?.url_list
if (Array.isArray(urlList) && urlList.length > 0) { if (Array.isArray(urlList) && urlList.length > 0) {
const lastUrl = urlList[urlList.length - 1] const lastUrl = urlList[urlList.length - 1]?.trim()
const firstUrl = urlList[0] const firstUrl = urlList[0]?.trim()
return (lastUrl && lastUrl.trim()) || (firstUrl && firstUrl.trim()) || '' return lastUrl || firstUrl || ''
} }
return (video?.play_addr?.url && video.play_addr.url.trim()) || '' return video?.play_addr?.url?.trim() || ''
} }
function extractCover(video) { function extractCover(video) {
@@ -369,12 +363,12 @@ async function handleSearch() {
} }
// 初始化 // 初始化
onMounted(async () => { onMounted(() => {
if (userStore.userId) { if (userStore.userId) {
await loadUserPrompts() loadUserPrompts()
} else if (userStore.isLoggedIn) { } else if (userStore.isLoggedIn) {
setTimeout(async () => { setTimeout(() => {
if (userStore.userId) await loadUserPrompts() if (userStore.userId) loadUserPrompts()
}, 500) }, 500)
} }
}) })
@@ -608,7 +602,7 @@ onMounted(async () => {
<span class="result-label">生成结果</span> <span class="result-label">生成结果</span>
<button <button
class="copy-btn" 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"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="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.client.DifyClient;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatReqVO; import cn.iocoder.yudao.module.tik.dify.vo.DifyChatReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatRespVO; 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.dal.AiAgentDO;
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentService; import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentService;
import cn.iocoder.yudao.module.tik.muye.aimodelconfig.dal.AiModelConfigDO; import cn.iocoder.yudao.module.tik.muye.aimodelconfig.dal.AiModelConfigDO;
@@ -27,13 +29,6 @@ import java.util.concurrent.atomic.AtomicReference;
@Slf4j @Slf4j
public class DifyServiceImpl implements DifyService { 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 @Resource
private AiAgentService aiAgentService; private AiAgentService aiAgentService;
@@ -58,10 +53,12 @@ public class DifyServiceImpl implements DifyService {
} }
// 2. 根据 modelMode 获取对应的积分配置 // 2. 根据 modelMode 获取对应的积分配置
String modelType = "standard".equals(reqVO.getModelMode()) AiModelTypeEnum modelTypeEnum = "standard".equals(reqVO.getModelMode())
? MODEL_TYPE_WRITING_STANDARD ? AiModelTypeEnum.DIFY_WRITING_STANDARD
: MODEL_TYPE_WRITING_PRO; : AiModelTypeEnum.DIFY_WRITING_PRO;
AiModelConfigDO config = pointsService.getConfig(PLATFORM_DIFY, modelType); AiModelConfigDO config = pointsService.getConfig(
AiPlatformEnum.DIFY.getPlatform(),
modelTypeEnum.getModelType());
// 3. 预检积分 // 3. 预检积分
pointsService.checkPoints(userId, config.getConsumePoints()); 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);
}
}