From 66246278023529059df3e617add910d7a3e4da15 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Fri, 14 Nov 2025 02:15:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/vue.md | 1 + frontend/app/web-gold/src/api/tikhub/types.js | 2 +- .../src/components/GradientButton.vue | 203 +++++ .../src/views/content-style/Copywriting.vue | 188 ++-- .../components/BenchmarkForm.vue | 11 +- .../components/BenchmarkTable.vue | 23 +- .../src/views/system/StyleSettings.vue | 39 +- .../web-gold/src/views/trends/Forecast.vue | 850 +++++++++++++++--- .../ai/tikhup/service/TikHupServiceImpl.java | 97 +- .../app/AppUserPromptController.java | 33 +- 10 files changed, 1224 insertions(+), 223 deletions(-) create mode 100644 frontend/app/web-gold/src/components/GradientButton.vue diff --git a/.cursor/rules/vue.md b/.cursor/rules/vue.md index 767fc359f0..1dfb869cba 100644 --- a/.cursor/rules/vue.md +++ b/.cursor/rules/vue.md @@ -77,6 +77,7 @@ globs: **/*.vue, **/*.ts, components/**/* - 实现完整的错误处理 - 规范事件处理机制 - 为复杂逻辑添加文档注释 +- 代码简洁易于人类阅读 ## 构建与工具链 - 使用 Vite 进行开发 diff --git a/frontend/app/web-gold/src/api/tikhub/types.js b/frontend/app/web-gold/src/api/tikhub/types.js index 2d3a3f5c93..3f35a43934 100644 --- a/frontend/app/web-gold/src/api/tikhub/types.js +++ b/frontend/app/web-gold/src/api/tikhub/types.js @@ -39,7 +39,7 @@ export const InterfaceType = { /** 抖音 - 网页端通用搜索结果 */ DOUYIN_WEB_GENERAL_SEARCH: '12', /** 抖音 - APP通用搜索结果 */ - DOUYIN_SEARCH_GENERAL_SEARCH: '14', + DOUYIN_SEARCH_GENERAL_SEARCH: '13', } /** diff --git a/frontend/app/web-gold/src/components/GradientButton.vue b/frontend/app/web-gold/src/components/GradientButton.vue new file mode 100644 index 0000000000..c4ecb9130e --- /dev/null +++ b/frontend/app/web-gold/src/components/GradientButton.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/frontend/app/web-gold/src/views/content-style/Copywriting.vue b/frontend/app/web-gold/src/views/content-style/Copywriting.vue index 10c2707204..e2e4a58161 100644 --- a/frontend/app/web-gold/src/views/content-style/Copywriting.vue +++ b/frontend/app/web-gold/src/views/content-style/Copywriting.vue @@ -1,5 +1,5 @@ @@ -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; } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tikhup/service/TikHupServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tikhup/service/TikHupServiceImpl.java index cff0539b36..b975a391ce 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tikhup/service/TikHupServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tikhup/service/TikHupServiceImpl.java @@ -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 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 "; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/userprompt/controller/app/AppUserPromptController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/userprompt/controller/app/AppUserPromptController.java index 7ff24cba39..a42689192b 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/userprompt/controller/app/AppUserPromptController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/userprompt/controller/app/AppUserPromptController.java @@ -32,9 +32,36 @@ public class AppUserPromptController { @PostMapping("/create") @Operation(summary = "创建用户提示词") - public CommonResult createUserPrompt(@Valid @RequestBody UserPromptSaveReqVO createReqVO) { - // 设置当前登录用户ID - createReqVO.setUserId(getLoginUserId()); + public CommonResult 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)); }