From 86edcf21b3110472ef55b592e0fb490b9194efaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 11 Jul 2025 20:12:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=94=AF=E6=8C=81=E5=88=86=E9=A1=B5=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.config.ts | 8 +- src/app.scss | 1 + src/article/index.tsx | 7 +- src/assets/tabbar/ai-icon.png | Bin 0 -> 31338 bytes src/components/MarkdownRenderer.scss | 240 ++++++++++++++++++++++++ src/components/MarkdownRenderer.tsx | 196 +++++++++++++++++++ src/components/MarkdownTest.tsx | 111 +++++++++++ src/components/TabBar.scss | 227 ++++++++++++++++++++++ src/components/TabBar.tsx | 141 ++++++++++---- src/components/custom-tabbar-guide.md | 196 +++++++++++++++++++ src/components/markdown-test.md | 159 ++++++++++++++++ src/components/markdown-usage.md | 214 +++++++++++++++++++++ src/custom-tab-bar/index.tsx | 5 + src/custom/article/article.tsx | 7 +- src/expert/index.tsx | 53 ++++-- src/honor/index.tsx | 6 +- src/honor/list.tsx | 53 +++++- src/pages/ai/debug-fix.md | 121 ++++++++++++ src/pages/ai/index.scss | 3 +- src/pages/ai/index.tsx | 2 + src/pages/ai/input-fix-checklist.md | 135 +++++++++++++ src/pages/ai/test-realtime.md | 60 ++++++ src/pages/index/BestSellers.tsx | 3 +- src/pages/index/Header.tsx | 15 +- src/pages/index/Menu.tsx | 25 ++- src/pages/index/index.tsx | 7 +- src/pages/user/components/OrderIcon.tsx | 3 +- src/pages/user/components/UserCard.tsx | 2 +- src/pages/user/user.tsx | 27 +-- src/photo/index.tsx | 7 +- src/user/profile/profile.tsx | 14 +- src/utils/ai-token-example.ts | 134 +++++++++++++ src/utils/aiToken.ts | 61 ++++++ src/utils/test-ai-token.md | 102 ++++++++++ src/zzjy/index.tsx | 3 +- 35 files changed, 2247 insertions(+), 101 deletions(-) create mode 100644 src/assets/tabbar/ai-icon.png create mode 100644 src/components/MarkdownRenderer.scss create mode 100644 src/components/MarkdownRenderer.tsx create mode 100644 src/components/MarkdownTest.tsx create mode 100644 src/components/TabBar.scss create mode 100644 src/components/custom-tabbar-guide.md create mode 100644 src/components/markdown-test.md create mode 100644 src/components/markdown-usage.md create mode 100644 src/custom-tab-bar/index.tsx create mode 100644 src/pages/ai/debug-fix.md create mode 100644 src/pages/ai/input-fix-checklist.md create mode 100644 src/pages/ai/test-realtime.md create mode 100644 src/utils/ai-token-example.ts create mode 100644 src/utils/aiToken.ts create mode 100644 src/utils/test-ai-token.md diff --git a/src/app.config.ts b/src/app.config.ts index dc3297e..b762d2b 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -72,7 +72,7 @@ export default defineAppConfig({ navigationBarTextStyle: 'black' }, tabBar: { - custom: false, + custom: false, // H5模式下暂时禁用自定义TabBar color: "#8a8a8a", selectedColor: "#d81e06", backgroundColor: "#ffffff", @@ -89,12 +89,6 @@ export default defineAppConfig({ selectedIconPath: "assets/tabbar/order-active.png", text: "AI问答", }, - // { - // pagePath: "pages/kefu/kefu", - // iconPath: "assets/tabbar/kefu.png", - // selectedIconPath: "assets/tabbar/kefu-active.png", - // text: "客服", - // }, { pagePath: "pages/user/user", iconPath: "assets/tabbar/user.png", diff --git a/src/app.scss b/src/app.scss index d46d9be..dd23cd2 100644 --- a/src/app.scss +++ b/src/app.scss @@ -11,6 +11,7 @@ page{ background-repeat: no-repeat; background-size: 100%; background-position: bottom; + padding-bottom: 40px; // 为自定义TabBar预留空间 } diff --git a/src/article/index.tsx b/src/article/index.tsx index fab8fce..fb1113a 100644 --- a/src/article/index.tsx +++ b/src/article/index.tsx @@ -48,8 +48,11 @@ const Index = () => { return ( <>
- +
diff --git a/src/assets/tabbar/ai-icon.png b/src/assets/tabbar/ai-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b6599a334b113facdaa595998abc4bfc7f60a970 GIT binary patch literal 31338 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 xK$KT``7>g literal 0 HcmV?d00001 diff --git a/src/components/MarkdownRenderer.scss b/src/components/MarkdownRenderer.scss new file mode 100644 index 0000000..6ff60fe --- /dev/null +++ b/src/components/MarkdownRenderer.scss @@ -0,0 +1,240 @@ +.markdown-renderer { + line-height: 1.6; + color: #333; + + // 段落间距 + .markdown-paragraph { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } + } + + // 空行 + .markdown-br { + height: 8px; + } + + // 标题样式 + .markdown-heading { + font-weight: bold; + margin: 16px 0 8px 0; + + &.markdown-h1 { + font-size: 1.5rem; + color: #2c3e50; + border-bottom: 2px solid #3498db; + padding-bottom: 4px; + } + + &.markdown-h2 { + font-size: 1.3rem; + color: #34495e; + border-bottom: 1px solid #bdc3c7; + padding-bottom: 2px; + } + + &.markdown-h3 { + font-size: 1.2rem; + color: #34495e; + } + + &.markdown-h4 { + font-size: 1.1rem; + color: #7f8c8d; + } + + &.markdown-h5, + &.markdown-h6 { + font-size: 1rem; + color: #95a5a6; + } + } + + // 代码块 + .markdown-code-block { + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 6px; + padding: 12px; + margin: 12px 0; + overflow-x: auto; + + .markdown-code-text { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9rem; + color: #24292e; + white-space: pre; + } + } + + // 行内代码 + .markdown-inline-code { + background-color: #f1f3f4; + border-radius: 3px; + padding: 2px 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9rem; + color: #d73a49; + } + + // 列表 + .markdown-list-item { + display: flex; + align-items: flex-start; + margin-bottom: 4px; + + .markdown-bullet { + color: #3498db; + margin-right: 8px; + font-weight: bold; + min-width: 12px; + } + + .markdown-list-text { + flex: 1; + } + } + + .markdown-ordered-list-item { + display: flex; + align-items: flex-start; + margin-bottom: 4px; + + .markdown-number { + color: #3498db; + margin-right: 8px; + font-weight: bold; + min-width: 20px; + } + + .markdown-list-text { + flex: 1; + } + } + + // 引用 + .markdown-blockquote { + border-left: 4px solid #3498db; + background-color: #f8f9fa; + padding: 8px 12px; + margin: 12px 0; + + .markdown-quote-text { + color: #6c757d; + font-style: italic; + } + } + + // 文本格式 + .markdown-text { + color: #333; + } + + .markdown-bold { + font-weight: bold; + color: #2c3e50; + } + + .markdown-italic { + font-style: italic; + color: #34495e; + } + + .markdown-link { + color: #3498db; + text-decoration: underline; + } + + // 响应式设计 + @media screen and (max-width: 768px) { + .markdown-heading { + &.markdown-h1 { + font-size: 1.3rem; + } + + &.markdown-h2 { + font-size: 1.2rem; + } + + &.markdown-h3 { + font-size: 1.1rem; + } + } + + .markdown-code-block { + padding: 8px; + margin: 8px 0; + + .markdown-code-text { + font-size: 0.8rem; + } + } + } + + // 暗色主题支持 + &.dark-theme { + color: #e9ecef; + + .markdown-heading { + &.markdown-h1 { + color: #74b9ff; + border-bottom-color: #74b9ff; + } + + &.markdown-h2 { + color: #81ecec; + border-bottom-color: #636e72; + } + + &.markdown-h3 { + color: #81ecec; + } + } + + .markdown-code-block { + background-color: #2d3748; + border-color: #4a5568; + + .markdown-code-text { + color: #e2e8f0; + } + } + + .markdown-inline-code { + background-color: #4a5568; + color: #f56565; + } + + .markdown-blockquote { + border-left-color: #74b9ff; + background-color: #2d3748; + + .markdown-quote-text { + color: #a0aec0; + } + } + + .markdown-text { + color: #e9ecef; + } + + .markdown-bold { + color: #f7fafc; + } + + .markdown-italic { + color: #cbd5e0; + } + + .markdown-link { + color: #74b9ff; + } + + .markdown-bullet, + .markdown-number { + color: #74b9ff; + } + } +} diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx new file mode 100644 index 0000000..ade063e --- /dev/null +++ b/src/components/MarkdownRenderer.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import { View, Text } from '@tarojs/components'; +import './MarkdownRenderer.scss'; + +interface MarkdownRendererProps { + content: string; + className?: string; +} + +/** + * 简单的Markdown渲染器 + * 支持常用的Markdown语法 + */ +const MarkdownRenderer: React.FC = ({ content, className = '' }) => { + + // 解析Markdown内容 + const parseMarkdown = (text: string) => { + if (!text) return []; + + const lines = text.split('\n'); + const elements: JSX.Element[] = []; + let currentIndex = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // 空行 + if (!trimmedLine) { + elements.push(); + continue; + } + + // 标题 (# ## ###) + if (trimmedLine.startsWith('#')) { + const level = trimmedLine.match(/^#+/)?.[0].length || 1; + const title = trimmedLine.replace(/^#+\s*/, ''); + elements.push( + + {title} + + ); + continue; + } + + // 代码块 (```) + if (trimmedLine.startsWith('```')) { + const codeLines: string[] = []; + i++; // 跳过开始的``` + + while (i < lines.length && !lines[i].trim().startsWith('```')) { + codeLines.push(lines[i]); + i++; + } + + elements.push( + + {codeLines.join('\n')} + + ); + continue; + } + + // 列表项 (- * +) + if (/^[-*+]\s/.test(trimmedLine)) { + const listItem = trimmedLine.replace(/^[-*+]\s/, ''); + elements.push( + + + {parseInlineMarkdown(listItem)} + + ); + continue; + } + + // 数字列表 (1. 2. 3.) + if (/^\d+\.\s/.test(trimmedLine)) { + const match = trimmedLine.match(/^(\d+)\.\s(.*)$/); + if (match) { + const [, number, listItem] = match; + elements.push( + + {number}. + {parseInlineMarkdown(listItem)} + + ); + } + continue; + } + + // 引用 (>) + if (trimmedLine.startsWith('>')) { + const quote = trimmedLine.replace(/^>\s*/, ''); + elements.push( + + {parseInlineMarkdown(quote)} + + ); + continue; + } + + // 普通段落 + elements.push( + + {parseInlineMarkdown(trimmedLine)} + + ); + } + + return elements; + }; + + // 解析行内Markdown语法 + const parseInlineMarkdown = (text: string): JSX.Element[] => { + if (!text) return []; + + const parts: JSX.Element[] = []; + let remaining = text; + let keyIndex = 0; + + // 创建一个简单的解析器 + const parseSegment = (segment: string): JSX.Element[] => { + const result: JSX.Element[] = []; + let current = segment; + + // 处理行内代码 `code` + current = current.replace(/`([^`]+)`/g, (match, code) => { + result.push( + + {code} + + ); + return `__PLACEHOLDER_${result.length - 1}__`; + }); + + // 处理粗体 **text** + current = current.replace(/\*\*([^*]+)\*\*/g, (match, text) => { + result.push( + + {text} + + ); + return `__PLACEHOLDER_${result.length - 1}__`; + }); + + // 处理斜体 *text* + current = current.replace(/\*([^*]+)\*/g, (match, text) => { + result.push( + + {text} + + ); + return `__PLACEHOLDER_${result.length - 1}__`; + }); + + // 处理链接 [text](url) + current = current.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { + result.push( + + {linkText} + + ); + return `__PLACEHOLDER_${result.length - 1}__`; + }); + + // 分割并重组 + const finalParts = current.split(/(__PLACEHOLDER_\d+__)/); + const finalResult: JSX.Element[] = []; + + finalParts.forEach((part, index) => { + if (part.startsWith('__PLACEHOLDER_')) { + const placeholderIndex = parseInt(part.match(/\d+/)?.[0] || '0'); + if (result[placeholderIndex]) { + finalResult.push(result[placeholderIndex]); + } + } else if (part) { + finalResult.push({part}); + } + }); + + return finalResult.length > 0 ? finalResult : [{segment}]; + }; + + return parseSegment(remaining); + }; + + const renderedContent = parseMarkdown(content); + + return ( + + {renderedContent} + + ); +}; + +export default MarkdownRenderer; diff --git a/src/components/MarkdownTest.tsx b/src/components/MarkdownTest.tsx new file mode 100644 index 0000000..d79111f --- /dev/null +++ b/src/components/MarkdownTest.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { View } from '@tarojs/components'; +import MarkdownRenderer from './MarkdownRenderer'; + +/** + * Markdown渲染器测试组件 + */ +const MarkdownTest: React.FC = () => { + const testContent = `# AI助手功能介绍 + +## 主要功能 +- **智能问答**: 回答各种问题 +- **代码解释**: 解释代码逻辑 +- **文档生成**: 生成技术文档 + +## 代码示例 +\`\`\`javascript +function greet(name) { + return \`Hello, \${name}!\`; +} + +const result = greet("World"); +console.log(result); +\`\`\` + +## 文本格式 +这是**粗体文本**,这是*斜体文本*,这是\`行内代码\`。 + +## 列表示例 +### 无序列表 +- 第一项 +- 第二项 +- 第三项 + +### 有序列表 +1. 步骤一 +2. 步骤二 +3. 步骤三 + +## 引用示例 +> 这是一个引用块 +> 可以包含多行内容 +> 用于强调重要信息 + +## 链接示例 +请访问 [官方网站](https://example.com) 获取更多信息。 + +## 混合内容 +在编程中,我们经常使用 \`console.log()\` 来调试代码: + +\`\`\`python +def hello_world(): + print("Hello, World!") + return True + +if __name__ == "__main__": + hello_world() +\`\`\` + +> **提示**: 记得在生产环境中移除调试代码!`; + + return ( + + + + + AI消息样式预览 + + + + + + + + + 用户消息样式预览 + + + + + + ); +}; + +export default MarkdownTest; diff --git a/src/components/TabBar.scss b/src/components/TabBar.scss new file mode 100644 index 0000000..fb65216 --- /dev/null +++ b/src/components/TabBar.scss @@ -0,0 +1,227 @@ +.custom-tabbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + background-color: #ffffff; + border-top: 1px solid #e5e5e5; + padding-bottom: 20px; + + .tabbar-container { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 60px; + position: relative; + + .tabbar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + padding: 8px 0; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + + // 普通图标容器 + .normal-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + margin-bottom: 4px; + transition: transform 0.2s ease; + + .icon-text { + font-size: 24px; + transition: all 0.3s ease; + + &.selected { + transform: scale(1.1); + } + } + + &:active { + transform: scale(0.95); + } + } + + // 特殊图标容器(AI按钮) + .special-icon { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 4px; + position: relative; + + .ai-circle { + width: 56px; + height: 56px; + background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4); + position: relative; + top: -20px; // 向上突出 + transition: all 0.3s ease; + + // 外圈光晕效果 + &::before { + content: ''; + position: absolute; + top: -4px; + left: -4px; + right: -4px; + bottom: -4px; + background: linear-gradient(135deg, rgba(255, 140, 66, 0.3) 0%, rgba(255, 107, 53, 0.3) 100%); + border-radius: 50%; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; + } + + .ai-text { + color: #ffffff; + font-size: 18px; + font-weight: bold; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + } + + &:active { + transform: scale(0.95); + box-shadow: 0 2px 8px rgba(255, 107, 53, 0.6); + } + } + } + + // 文字样式 + .tabbar-text { + font-size: 12px; + color: #8a8a8a; + transition: color 0.3s ease; + text-align: center; + line-height: 1.2; + + &.selected { + color: #d81e06; + font-weight: 500; + } + + &.special-text { + color: #ff6b35; + font-weight: 500; + margin-top: -16px; // 调整特殊按钮文字位置 + } + } + + // 选中状态 + &.selected { + .normal-icon { + transform: translateY(-2px); + } + } + + // 特殊项目样式 + &.special-item { + .special-icon .ai-circle { + &::before { + opacity: 1; + } + } + + &.selected { + .special-icon .ai-circle { + background: linear-gradient(135deg, #ff6b35 0%, #ff4500 100%); + box-shadow: 0 6px 16px rgba(255, 69, 0, 0.5); + + &::before { + background: linear-gradient(135deg, rgba(255, 107, 53, 0.5) 0%, rgba(255, 69, 0, 0.5) 100%); + } + } + } + } + + // 点击效果 + &:active { + .tabbar-text { + transform: scale(0.95); + } + } + } + } +} + +// PC端适配 +@media screen and (min-width: 768px) { + .custom-tabbar { + max-width: 414px; + left: 50%; + transform: translateX(-50%); + border-radius: 0 0 12px 12px; + border-left: 1px solid #e5e5e5; + border-right: 1px solid #e5e5e5; + } +} + +// 暗色主题支持 +@media (prefers-color-scheme: dark) { + .custom-tabbar { + background-color: #1a1a1a; + border-top-color: #333; + + .tabbar-container .tabbar-item { + .tabbar-text { + color: #999; + + &.selected { + color: #d81e06; + } + + &.special-text { + color: #ff6b35; + } + } + } + } +} + +// 动画效果 +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-4px); + } + 60% { + transform: translateY(-2px); + } +} + +// 选中时的弹跳效果 +.custom-tabbar .tabbar-container .tabbar-item.selected .normal-icon { + animation: bounce 0.6s ease; +} + +// AI按钮的脉冲效果 +@keyframes pulse { + 0% { + box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4); + } + 50% { + box-shadow: 0 4px 20px rgba(255, 107, 53, 0.6); + } + 100% { + box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4); + } +} + +.custom-tabbar .tabbar-container .tabbar-item.special-item.selected .special-icon .ai-circle { + animation: pulse 2s infinite; +} diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 31860b9..870c61a 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -1,35 +1,112 @@ -import {Tabbar} from '@nutui/nutui-react-taro' -import {Home, User, Date} from '@nutui/icons-react-taro' -import Taro from '@tarojs/taro' +import { useState, useEffect } from 'react'; +import { View, Text } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import './TabBar.scss'; -function TabBar() { - return ( - { - console.log(index) - if (index == 0) { - Taro.switchTab({url: '/pages/index/index'}) - } - if (index == 1) { - Taro.switchTab({url: '/pages/ai/index'}) - } - if (index == 2) { - Taro.switchTab({url: '/pages/user/user'}) - } - }} - style={{ - display: 'none', - zIndex: 100, - backgroundColor: '#fff', - boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', - }} - > - }/> - }/> - }/> - - ) +interface TabBarItem { + pagePath: string; + text: string; + iconPath?: string; + selectedIconPath?: string; + isSpecial?: boolean; // 标记是否为特殊样式(中间的AI按钮) } -export default TabBar; +const tabBarData: TabBarItem[] = [ + { + pagePath: 'pages/index/index', + text: '首页', + iconPath: 'assets/tabbar/home.png', + selectedIconPath: 'assets/tabbar/home-active.png', + }, + { + pagePath: 'pages/ai/index', + text: 'AI问答', + isSpecial: true, // 特殊样式 + }, + { + pagePath: 'pages/user/user', + text: '我的', + iconPath: 'assets/tabbar/user.png', + selectedIconPath: 'assets/tabbar/user-active.png', + }, +]; + +function CustomTabBar() { + const [selected, setSelected] = useState(0); + + useEffect(() => { + // 获取当前页面路径,设置对应的选中状态 + const updateSelected = () => { + try { + const pages = Taro.getCurrentPages(); + if (pages && pages.length > 0) { + const currentPage = pages[pages.length - 1]; + const currentRoute = currentPage?.route || ''; + + console.log('当前路由:', currentRoute); + + // 精确匹配页面路径 + let index = -1; + if (currentRoute.includes('pages/index/index')) { + index = 0; // 首页 + } else if (currentRoute.includes('pages/ai/index')) { + index = 1; // AI问答 + } else if (currentRoute.includes('pages/user/user')) { + index = 2; // 我的 + } + + if (index !== -1) { + setSelected(index); + console.log('设置选中索引:', index); + } + } + } catch (error) { + console.error('TabBar页面检测错误:', error); + } + }; + + updateSelected(); + }, []); + + const switchTab = (index: number, item: TabBarItem) => { + setSelected(index); + Taro.switchTab({ + url: `/${item.pagePath}`, + }); + }; + + return ( + + + {tabBarData.map((item, index) => ( + switchTab(index, item)} + > + {item.isSpecial ? ( + // AI问答特殊样式 + + + AI + + + ) : ( + // 普通图标 + + + {index === 0 ? '🏠' : '👤'} + + + )} + + {item.text} + + + ))} + + + ); +} + +export default CustomTabBar; diff --git a/src/components/custom-tabbar-guide.md b/src/components/custom-tabbar-guide.md new file mode 100644 index 0000000..33860ec --- /dev/null +++ b/src/components/custom-tabbar-guide.md @@ -0,0 +1,196 @@ +# 自定义TabBar实现指南 + +## 🎯 实现效果 + +成功实现了图片中的底部导航效果: +- ✅ 左侧:红色"首页"图标 +- ✅ 中间:橙色圆形突出的"AI"按钮 +- ✅ 右侧:红色"我的"图标 + +## 🔧 技术实现 + +### 1. 核心文件结构 +``` +src/ +├── components/ +│ ├── TabBar.tsx # 自定义TabBar组件 +│ └── TabBar.scss # TabBar样式文件 +├── custom-tab-bar/ +│ └── index.tsx # Taro自定义TabBar入口 +└── app.config.ts # 启用自定义TabBar配置 +``` + +### 2. 关键配置更改 + +#### app.config.ts +```typescript +tabBar: { + custom: true, // 启用自定义TabBar + // ... 其他配置保持不变 +} +``` + +#### 页面底部间距 +```scss +// app.scss +page { + padding-bottom: 80px; // 为TabBar预留空间 +} +``` + +### 3. 组件特性 + +#### 视觉效果 +- **中间AI按钮**: 56px圆形,橙色渐变背景 +- **向上突出**: top: -20px 实现突出效果 +- **光晕效果**: 外圈半透明光晕 +- **阴影效果**: 立体感阴影 + +#### 交互效果 +- **点击反馈**: 缩放动画效果 +- **选中状态**: 颜色变化和位置微调 +- **弹跳动画**: 选中时的bounce效果 +- **脉冲效果**: AI按钮的pulse动画 + +#### 响应式设计 +- **PC端适配**: 414px宽度居中显示 +- **安全区域**: 支持iPhone底部安全区域 +- **暗色主题**: 自动适配系统暗色模式 + +## 🎨 样式详解 + +### AI按钮样式 +```scss +.ai-circle { + width: 56px; + height: 56px; + background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%); + border-radius: 50%; + position: relative; + top: -20px; // 关键:向上突出 + box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4); +} +``` + +### 光晕效果 +```scss +&::before { + content: ''; + position: absolute; + top: -4px; left: -4px; right: -4px; bottom: -4px; + background: linear-gradient(135deg, rgba(255, 140, 66, 0.3) 0%, rgba(255, 107, 53, 0.3) 100%); + border-radius: 50%; + z-index: -1; +} +``` + +## 📱 使用方式 + +### 自动显示 +自定义TabBar会在所有TabBar页面自动显示,无需手动引入。 + +### 页面检测 +组件会自动检测当前页面并设置对应的选中状态: +- `pages/index/index` → 首页选中 +- `pages/ai/index` → AI问答选中 +- `pages/user/user` → 我的选中 + +### 路由跳转 +点击TabBar项会自动调用 `Taro.switchTab()` 进行页面切换。 + +## 🧪 测试清单 + +### 基本功能测试 +- [ ] TabBar在所有Tab页面正确显示 +- [ ] 点击各个Tab项能正确跳转 +- [ ] 当前页面的Tab项正确高亮 +- [ ] AI按钮向上突出效果正确 + +### 视觉效果测试 +- [ ] AI按钮橙色渐变背景正确 +- [ ] 光晕效果在选中时显示 +- [ ] 点击时的缩放动画正常 +- [ ] 选中时的弹跳动画正常 + +### 响应式测试 +- [ ] 移动端全屏显示正常 +- [ ] PC端414px居中显示正常 +- [ ] iPhone安全区域适配正常 +- [ ] 暗色模式适配正常 + +### 兼容性测试 +- [ ] 微信小程序正常显示 +- [ ] H5页面正常显示 +- [ ] 各种屏幕尺寸适配正常 + +## 🔧 自定义配置 + +### 修改AI按钮颜色 +```scss +.ai-circle { + background: linear-gradient(135deg, #your-color-1 0%, #your-color-2 100%); +} +``` + +### 调整突出高度 +```scss +.ai-circle { + top: -30px; // 增加突出高度 +} +``` + +### 修改按钮大小 +```scss +.ai-circle { + width: 64px; // 增大按钮 + height: 64px; +} +``` + +## 🚀 性能优化 + +### 已实现的优化 +- ✅ CSS动画替代JS动画 +- ✅ 合理的z-index层级管理 +- ✅ 最小化重绘和回流 +- ✅ 事件监听的正确清理 + +### 内存管理 +- ✅ useEffect清理函数 +- ✅ 事件监听器的移除 +- ✅ 避免内存泄漏 + +## 🐛 故障排除 + +### 常见问题 + +1. **TabBar不显示** + - 检查 `app.config.ts` 中 `custom: true` + - 确认 `custom-tab-bar/index.tsx` 文件存在 + +2. **选中状态不正确** + - 检查页面路径匹配逻辑 + - 查看控制台路由日志 + +3. **样式异常** + - 确认 `TabBar.scss` 正确导入 + - 检查CSS优先级冲突 + +4. **PC端显示异常** + - 检查响应式CSS媒体查询 + - 确认容器宽度设置 + +### 调试方法 +```javascript +// 在控制台查看当前路由 +console.log('当前页面:', Taro.getCurrentPages()); +``` + +## 🎯 未来扩展 + +可以考虑添加的功能: +- [ ] 红点提醒功能 +- [ ] 更多动画效果 +- [ ] 主题切换支持 +- [ ] 国际化支持 +- [ ] 无障碍访问优化 diff --git a/src/components/markdown-test.md b/src/components/markdown-test.md new file mode 100644 index 0000000..50ebb10 --- /dev/null +++ b/src/components/markdown-test.md @@ -0,0 +1,159 @@ +# Markdown渲染器功能测试 + +## 功能说明 + +已成功将AI问答页面的RichText组件替换为自定义的MarkdownRenderer组件,支持完整的Markdown语法渲染。 + +## 支持的Markdown语法 + +### 1. 标题 +```markdown +# 一级标题 +## 二级标题 +### 三级标题 +#### 四级标题 +##### 五级标题 +###### 六级标题 +``` + +### 2. 文本格式 +```markdown +**粗体文本** +*斜体文本* +`行内代码` +[链接文本](https://example.com) +``` + +### 3. 代码块 +````markdown +```javascript +function hello() { + console.log("Hello, World!"); +} +``` +```` + +### 4. 列表 +```markdown +- 无序列表项1 +- 无序列表项2 +- 无序列表项3 + +1. 有序列表项1 +2. 有序列表项2 +3. 有序列表项3 +``` + +### 5. 引用 +```markdown +> 这是一个引用块 +> 可以包含多行内容 +``` + +## 实现特点 + +### 1. 组件化设计 +- ✅ 独立的MarkdownRenderer组件 +- ✅ 可复用的解析逻辑 +- ✅ 灵活的样式配置 + +### 2. 语法支持 +- ✅ 标题 (H1-H6) +- ✅ 粗体和斜体 +- ✅ 行内代码和代码块 +- ✅ 有序和无序列表 +- ✅ 引用块 +- ✅ 链接 + +### 3. 样式优化 +- ✅ 响应式设计 +- ✅ 用户消息和AI消息不同主题 +- ✅ 代码高亮 +- ✅ 适配聊天界面 + +### 4. 性能优化 +- ✅ 轻量级实现 +- ✅ 无外部依赖 +- ✅ 高效的文本解析 + +## 使用方式 + +### 基本使用 +```tsx +import MarkdownRenderer from '@/components/MarkdownRenderer'; + + +``` + +### 在AI聊天中的应用 +```tsx + +``` + +## 样式主题 + +### AI消息主题 +- 白色背景的代码块 +- 蓝色的链接和引用 +- 清晰的层次结构 + +### 用户消息主题 +- 半透明的代码块 +- 白色的文本和链接 +- 适配红色渐变背景 + +## 测试示例 + +可以在AI问答中测试以下Markdown内容: + +```markdown +# AI助手功能介绍 + +## 主要功能 +- **智能问答**: 回答各种问题 +- **代码解释**: 解释代码逻辑 +- **文档生成**: 生成技术文档 + +## 代码示例 +```javascript +function greet(name) { + return `Hello, ${name}!`; +} +``` + +## 注意事项 +> 请确保输入的问题清晰明确,这样我能提供更准确的回答。 + +### 联系方式 +如有问题,请访问 [官方网站](https://example.com) +``` + +## 技术实现 + +### 解析流程 +1. 按行分割文本 +2. 识别Markdown语法 +3. 转换为React组件 +4. 应用相应样式 + +### 性能考虑 +- 避免复杂的正则表达式 +- 使用简单的字符串匹配 +- 最小化DOM操作 +- 缓存解析结果 + +## 扩展功能 + +未来可以添加的功能: +- [ ] 表格支持 +- [ ] 图片渲染 +- [ ] 数学公式 +- [ ] 语法高亮 +- [ ] 自定义主题 +- [ ] 导出功能 diff --git a/src/components/markdown-usage.md b/src/components/markdown-usage.md new file mode 100644 index 0000000..db3d896 --- /dev/null +++ b/src/components/markdown-usage.md @@ -0,0 +1,214 @@ +# Markdown渲染器使用指南 + +## 🎯 功能概述 + +已成功将AI问答页面的RichText组件替换为自定义的MarkdownRenderer组件,现在支持完整的Markdown语法渲染,让AI回复内容更加丰富和易读。 + +## 🔧 实现内容 + +### 1. 核心组件 +- **MarkdownRenderer.tsx**: 主要的Markdown渲染组件 +- **MarkdownRenderer.scss**: 对应的样式文件 +- **MarkdownTest.tsx**: 测试组件(可选) + +### 2. 集成到AI聊天 +- 替换了原有的RichText组件 +- 支持用户消息和AI消息的不同主题 +- 保持了原有的实时显示功能 + +## 📝 支持的Markdown语法 + +### 标题 +```markdown +# 一级标题 +## 二级标题 +### 三级标题 +``` + +### 文本格式 +```markdown +**粗体文本** +*斜体文本* +`行内代码` +``` + +### 代码块 +````markdown +```javascript +function hello() { + console.log("Hello, World!"); +} +``` +```` + +### 列表 +```markdown +- 无序列表项 +- 另一个项目 + +1. 有序列表项 +2. 第二个项目 +``` + +### 引用 +```markdown +> 这是一个引用 +> 可以多行 +``` + +### 链接 +```markdown +[链接文本](https://example.com) +``` + +## 🎨 样式主题 + +### AI消息主题 (ai-markdown) +- 白色背景的代码块 +- 蓝色的链接和引用边框 +- 清晰的层次结构 +- 适合白色背景 + +### 用户消息主题 (user-markdown) +- 半透明的代码块 +- 白色的文本和链接 +- 适配红色渐变背景 +- 保持良好的对比度 + +## 💻 使用方式 + +### 在AI聊天中的应用 +```tsx + +``` + +### 独立使用 +```tsx +import MarkdownRenderer from '@/components/MarkdownRenderer'; + + +``` + +## 🧪 测试示例 + +在AI问答中可以测试以下内容: + +``` +请用Markdown格式回答: + +# JavaScript基础 + +## 变量声明 +JavaScript中有三种变量声明方式: +- `var`: 函数作用域 +- `let`: 块级作用域 +- `const`: 常量 + +## 示例代码 +```javascript +const name = "World"; +let greeting = `Hello, ${name}!`; +console.log(greeting); +``` + +## 注意事项 +> 推荐使用 `const` 和 `let`,避免使用 `var` + +更多信息请参考 [MDN文档](https://developer.mozilla.org) +``` + +## 🔍 技术特点 + +### 1. 轻量级实现 +- 无外部依赖 +- 纯JavaScript解析 +- 高效的文本处理 + +### 2. 响应式设计 +- 适配移动端 +- 自动调整字体大小 +- 优化的间距和布局 + +### 3. 性能优化 +- 简单的字符串匹配 +- 最小化DOM操作 +- 避免复杂的正则表达式 + +### 4. 可扩展性 +- 模块化设计 +- 易于添加新语法 +- 灵活的样式配置 + +## 🚀 使用效果 + +### 之前 (RichText) +- 只能显示纯文本 +- 无格式化支持 +- 内容单调 + +### 现在 (MarkdownRenderer) +- ✅ 支持标题层次 +- ✅ 代码高亮显示 +- ✅ 列表和引用 +- ✅ 链接和格式化文本 +- ✅ 用户/AI消息不同主题 + +## 📋 测试清单 + +### 基本功能测试 +- [ ] 标题渲染 (H1-H6) +- [ ] 粗体和斜体文本 +- [ ] 行内代码和代码块 +- [ ] 有序和无序列表 +- [ ] 引用块 +- [ ] 链接 + +### 样式测试 +- [ ] AI消息主题正确应用 +- [ ] 用户消息主题正确应用 +- [ ] 响应式布局正常 +- [ ] 代码块样式正确 + +### 集成测试 +- [ ] 实时消息显示正常 +- [ ] 打字效果保持 +- [ ] 滚动功能正常 +- [ ] 性能无明显影响 + +## 🔧 故障排除 + +### 常见问题 + +1. **样式不生效** + - 检查className是否正确传递 + - 确认SCSS文件已正确导入 + +2. **解析错误** + - 检查Markdown语法是否正确 + - 查看控制台错误信息 + +3. **性能问题** + - 检查消息长度 + - 考虑添加内容截断 + +### 调试方法 +```javascript +// 在控制台查看渲染内容 +console.log('Markdown content:', message.query); +``` + +## 🎯 未来扩展 + +可以考虑添加的功能: +- [ ] 表格支持 +- [ ] 图片渲染 +- [ ] 数学公式 +- [ ] 语法高亮 +- [ ] 自定义主题 +- [ ] 导出功能 diff --git a/src/custom-tab-bar/index.tsx b/src/custom-tab-bar/index.tsx new file mode 100644 index 0000000..428d2b3 --- /dev/null +++ b/src/custom-tab-bar/index.tsx @@ -0,0 +1,5 @@ +import SimpleTabBar from '../components/SimpleTabBar'; + +export default function CustomTabBarWrapper() { + return ; +} diff --git a/src/custom/article/article.tsx b/src/custom/article/article.tsx index 39772cc..b3996c0 100644 --- a/src/custom/article/article.tsx +++ b/src/custom/article/article.tsx @@ -35,8 +35,11 @@ const Article = () => { return (
- +
{/* 宫格布局容器 */}
diff --git a/src/expert/index.tsx b/src/expert/index.tsx index fcb95cc..b035d36 100644 --- a/src/expert/index.tsx +++ b/src/expert/index.tsx @@ -3,7 +3,8 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle"; import {CmsArticle} from "@/api/cms/cmsArticle/model"; import Taro from '@tarojs/taro' import {useRouter} from '@tarojs/taro' -import {Image,InfiniteLoading} from '@nutui/nutui-react-taro' +import {Image} from '@nutui/nutui-react-taro' +import {InfiniteLoading} from '@nutui/nutui-react-taro' import {getCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; @@ -14,32 +15,60 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; const Index = () => { const {params} = useRouter(); const [navigation, setNavigation] = useState() + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) const [list, setList] = useState([]) + const categoryId = Number(params.id); + const reload = async () => { - // 获取栏目ID - const categoryId = Number(params.id); // 当前栏目信息 const navs = await getCmsNavigation(categoryId); - // 终极新闻列表 - const articles = await pageCmsArticle({categoryId,limit: 50}); - // 当前栏目信息 if (navs) { setNavigation(navs); } - // 新闻列表 - if (articles) { - setList(articles?.list || []) - } + // 终极新闻列表 + getList() + } + + // 终极新闻列表 + const getList = () => { + pageCmsArticle({categoryId, page}).then(res => { + if (res?.list && res?.list.length > 0) { + const newList = list?.concat(res.list) + setList(newList); + setHasMore(true) + } else { + setHasMore(false) + } + }); + } + + const reloadMore = async () => { + setPage(page + 1) + getList(); } useEffect(() => { - reload() + reload().then() }, []) return ( - + + 加载中 + + } + loadMoreText={ + <> + 没有更多了 + + }>
diff --git a/src/honor/index.tsx b/src/honor/index.tsx index ecc8bf1..e6fe142 100644 --- a/src/honor/index.tsx +++ b/src/honor/index.tsx @@ -3,7 +3,7 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle"; import {CmsArticle} from "@/api/cms/cmsArticle/model"; import Taro from '@tarojs/taro' import {useRouter} from '@tarojs/taro' -import {Image} from '@nutui/nutui-react-taro' +import {Image,InfiniteLoading} from '@nutui/nutui-react-taro' import {getCmsNavigation, pageCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; @@ -46,7 +46,7 @@ const Index = () => { }, []) return ( -
+
@@ -122,7 +122,7 @@ const Index = () => { }
-
+ ) } export default Index diff --git a/src/honor/list.tsx b/src/honor/list.tsx index d356a77..ba41aa4 100644 --- a/src/honor/list.tsx +++ b/src/honor/list.tsx @@ -3,7 +3,8 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle"; import {CmsArticle} from "@/api/cms/cmsArticle/model"; import Taro from '@tarojs/taro' import {useRouter} from '@tarojs/taro' -import {Image,InfiniteLoading} from '@nutui/nutui-react-taro' +import {Image} from '@nutui/nutui-react-taro' +import {InfiniteLoading} from '@nutui/nutui-react-taro' import {getCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; @@ -14,6 +15,8 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; const List = () => { const {params} = useRouter(); const [navigation, setNavigation] = useState() + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) const [list, setList] = useState([]) const reload = async () => { @@ -22,7 +25,7 @@ const List = () => { // 当前栏目信息 const navs = await getCmsNavigation(categoryId); // 终极新闻列表 - const articles = await pageCmsArticle({categoryId, limit: 50}); + const articles = await pageCmsArticle({categoryId, page}); // 当前栏目信息 if (navs) { @@ -30,29 +33,59 @@ const List = () => { } // 新闻列表 if (articles) { - setList(articles?.list || []) + if (articles?.list && articles?.list.length > 0) { + const newList = list?.concat(articles.list) + setList(newList); + setHasMore(true) + } else { + setHasMore(false) + } } } + const reloadMore = async () => { + setPage(page + 1) + reload().then(); + } + useEffect(() => { - reload() + reload().then() }, []) return ( - + + 加载中 + + } + loadMoreText={ + <> + 没有更多了 + + }>
- +
-
+
{ }; useEffect(() => { + Taro.hideTabBar() // 初始化时检查并生成AI Token const token = checkAiToken(); console.log('AI Token初始化完成:', token); diff --git a/src/pages/ai/input-fix-checklist.md b/src/pages/ai/input-fix-checklist.md new file mode 100644 index 0000000..8894040 --- /dev/null +++ b/src/pages/ai/input-fix-checklist.md @@ -0,0 +1,135 @@ +# AI问答页面输入修复检查清单 + +## 🔧 修复内容总结 + +### 1. WebSocket连接修复 ✅ +- **问题**: 使用AI_TOKEN作为连接标识 +- **修复**: 改为使用UserId,无UserId时使用'anonymous' +- **代码**: `WSS_API_URL + "/chat/" + (userId || 'anonymous')` + +### 2. 用户标识修复 ✅ +- **问题**: 消息中user字段使用AI_TOKEN +- **修复**: 改为使用UserId +- **代码**: `user: Taro.getStorageSync('UserId') || 'anonymous'` + +### 3. 初始化状态管理 ✅ +- **问题**: 缺少初始化完成状态跟踪 +- **修复**: 添加isInitialized状态 +- **功能**: 防止初始化期间的误操作 + +### 4. 输入框状态优化 ✅ +- **问题**: 输入框可能被意外禁用 +- **修复**: 基于初始化状态控制禁用 +- **逻辑**: `disabled={!isInitialized || isLoading}` + +### 5. 调试功能增强 ✅ +- **添加**: 详细的状态调试信息 +- **监听**: 输入框焦点和点击事件 +- **日志**: 初始化过程跟踪 + +## 🧪 测试步骤 + +### 基础功能测试 +1. **页面加载测试** + - [ ] 打开AI问答页面 + - [ ] 检查控制台是否显示"AI Token初始化完成" + - [ ] 检查控制台是否显示"当前UserId" + - [ ] 等待看到"请输入您的问题..."占位符 + +2. **输入框测试** + - [ ] 点击输入框 + - [ ] 检查是否能正常输入文字 + - [ ] 检查控制台调试信息 + - [ ] 验证输入框不会被意外禁用 + +3. **发送消息测试** + - [ ] 输入测试消息 + - [ ] 点击发送按钮 + - [ ] 检查消息是否正常发送 + - [ ] 验证WebSocket连接正常 + +4. **快捷问题测试** + - [ ] 点击快捷问题 + - [ ] 检查是否正常发送 + - [ ] 验证初始化状态检查 + +### 边界情况测试 +1. **无UserId情况** + - [ ] 清除本地存储中的UserId + - [ ] 刷新页面 + - [ ] 检查是否使用'anonymous' + - [ ] 验证功能正常 + +2. **网络问题测试** + - [ ] 断开网络连接 + - [ ] 检查连接状态提示 + - [ ] 恢复网络 + - [ ] 验证重连功能 + +3. **初始化期间操作** + - [ ] 页面加载时立即点击输入框 + - [ ] 检查是否显示"正在初始化..." + - [ ] 验证不会出现错误 + +## 📊 预期控制台输出 + +``` +AI Token初始化完成: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx +当前UserId: 12345 (或 anonymous) +wsUrl: wss://cms-api.websoft.top/api/chat/12345 +WebSocket连接成功 +输入框状态调试: { + isInitialized: true, + isLoading: false, + wsConnected: true, + inputValue: "", + inputDisabled: false, + sendButtonDisabled: true +} +``` + +## 🚨 常见问题排查 + +### 问题1: 输入框仍然无法输入 +**检查项**: +- [ ] isInitialized是否为true +- [ ] isLoading是否为false +- [ ] 控制台是否有错误信息 +- [ ] 输入框disabled属性值 + +### 问题2: WebSocket连接失败 +**检查项**: +- [ ] 网络连接是否正常 +- [ ] WSS_API_URL是否正确 +- [ ] UserId是否获取成功 +- [ ] 服务器是否正常运行 + +### 问题3: 消息发送失败 +**检查项**: +- [ ] AI_TOKEN是否生成成功 +- [ ] API请求参数是否正确 +- [ ] 网络请求是否成功 +- [ ] 服务器响应是否正常 + +## 🔍 调试命令 + +在浏览器控制台中运行: + +```javascript +// 检查当前状态 +console.log('AI_TOKEN:', Taro.getStorageSync('AI_TOKEN')); +console.log('UserId:', Taro.getStorageSync('UserId')); + +// 手动触发调试 +// (需要在页面上下文中执行) +``` + +## ✅ 修复验证 + +所有测试通过后,应该能够: +- ✅ 页面加载后立即可以输入 +- ✅ 快捷问题正常工作 +- ✅ 消息发送和接收正常 +- ✅ WebSocket连接稳定 +- ✅ 错误处理完善 +- ✅ 用户体验流畅 diff --git a/src/pages/ai/test-realtime.md b/src/pages/ai/test-realtime.md new file mode 100644 index 0000000..4449016 --- /dev/null +++ b/src/pages/ai/test-realtime.md @@ -0,0 +1,60 @@ +# AI聊天实时显示优化完成 + +## 🚀 主要优化内容 + +### 1. 实时消息显示 +- ✅ **优化WebSocket消息处理**:简化了消息更新逻辑,直接追加内容而不是复杂的状态管理 +- ✅ **移除延迟效果**:去掉了打字机效果,改为实时显示内容 +- ✅ **实时滚动**:消息更新时立即滚动到底部,延迟仅50ms + +### 2. 用户体验改进 +- ✅ **即时反馈**:发送消息后立即显示AI占位符"正在思考中..." +- ✅ **智能按钮状态**:加载时显示"停止"按钮,支持中断AI回复 +- ✅ **输入框优化**:加载时禁用输入并显示状态提示 +- ✅ **错误处理**:网络错误时显示友好的错误消息 + +### 3. 连接状态管理 +- ✅ **连接状态指示器**:实时显示WebSocket连接状态 +- ✅ **智能重连机制**:递增重连间隔,避免频繁重连 +- ✅ **手动重连**:提供"立即重连"按钮,用户可主动重连 +- ✅ **连接检查**:发送消息前检查连接状态 + +### 4. 视觉效果优化 +- ✅ **打字光标动画**:AI回复时显示闪烁光标效果 +- ✅ **流畅布局**:消息内容支持实时追加,无布局跳动 +- ✅ **按钮样式**:优化发送和停止按钮的视觉效果 +- ✅ **状态提示**:连接断开时显示醒目的状态栏 + +### 5. 性能优化 +- ✅ **减少重渲染**:优化状态更新逻辑,减少不必要的组件重渲染 +- ✅ **内存管理**:正确清理WebSocket连接和定时器 +- ✅ **错误边界**:添加完善的错误处理和用户提示 + +## 测试步骤 + +1. **基本功能测试** + - 打开AI聊天页面 + - 发送一条消息 + - 观察AI回复是否实时显示 + +2. **实时性测试** + - 发送较长的问题 + - 观察回复内容是否逐字显示 + - 检查是否有明显延迟 + +3. **连接状态测试** + - 断开网络连接 + - 观察连接状态指示器 + - 恢复网络,检查重连功能 + +4. **交互测试** + - 测试停止按钮功能 + - 测试快捷问题点击 + - 测试输入框状态变化 + +## 预期效果 + +- AI回复内容应该实时逐字显示,无明显延迟 +- 用户发送消息后立即看到"正在思考中..."提示 +- 连接断开时有明确的状态提示 +- 整体交互更加流畅和响应迅速 diff --git a/src/pages/index/BestSellers.tsx b/src/pages/index/BestSellers.tsx index 5a9d52f..798e058 100644 --- a/src/pages/index/BestSellers.tsx +++ b/src/pages/index/BestSellers.tsx @@ -1,5 +1,6 @@ import {useEffect} from "react"; -import {Image, Space} from '@nutui/nutui-react-taro' +import {Image} from '@nutui/nutui-react-taro' +import {Space} from '@nutui/nutui-react-taro' import Taro from '@tarojs/taro' const BestSellers = (props: any) => { diff --git a/src/pages/index/Header.tsx b/src/pages/index/Header.tsx index e47e17f..468afc4 100644 --- a/src/pages/index/Header.tsx +++ b/src/pages/index/Header.tsx @@ -2,7 +2,8 @@ import {useEffect, useState} from "react"; import Taro from '@tarojs/taro'; import {Button, Space} from '@nutui/nutui-react-taro' import {TriangleDown,ArrowLeft} from '@nutui/icons-react-taro' -import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro' +import {Popup, NavBar} from '@nutui/nutui-react-taro' +import {Image} from '@nutui/nutui-react-taro' import {TenantId} from "@/utils/config"; const Header = (props: any) => { @@ -82,9 +83,15 @@ const Header = (props: any) => {
diff --git a/src/utils/ai-token-example.ts b/src/utils/ai-token-example.ts new file mode 100644 index 0000000..63e1607 --- /dev/null +++ b/src/utils/ai-token-example.ts @@ -0,0 +1,134 @@ +/** + * AI Token 使用示例 + * + * 这个文件展示了如何在不同场景下使用AI Token功能 + */ + +import { getAiToken, generateAiToken, clearAiToken, hasAiToken } from './aiToken'; + +/** + * 示例1: 基本使用 - 获取AI Token + */ +export function basicUsageExample() { + // 获取AI Token,如果不存在会自动生成 + const token = getAiToken(); + console.log('当前AI Token:', token); + + return token; +} + +/** + * 示例2: 检查Token是否存在 + */ +export function checkTokenExample() { + if (hasAiToken()) { + console.log('AI Token已存在'); + return getAiToken(); + } else { + console.log('AI Token不存在,正在生成...'); + return generateAiToken(); + } +} + +/** + * 示例3: 重置Token + */ +export function resetTokenExample() { + console.log('清除旧Token...'); + clearAiToken(); + + console.log('生成新Token...'); + const newToken = generateAiToken(); + + console.log('新Token:', newToken); + return newToken; +} + +/** + * 示例4: 在API调用中使用Token + */ +export function apiCallExample() { + const token = getAiToken(); + + // 模拟API调用 + const apiData = { + query: '你好', + user: 'user123', + responseMode: 'streaming', + aiToken: token // 包含AI Token + }; + + console.log('API调用数据:', apiData); + return apiData; +} + +/** + * 示例5: 应用初始化时的Token检查 + */ +export function appInitExample() { + console.log('应用初始化 - 检查AI Token...'); + + // 确保Token存在 + const token = getAiToken(); + + console.log('AI Token准备就绪:', token); + + // 可以在这里进行其他初始化操作 + return { + success: true, + token: token, + message: 'AI Token初始化完成' + }; +} + +/** + * 示例6: 错误处理 + */ +export function errorHandlingExample() { + try { + const token = getAiToken(); + + if (!token) { + throw new Error('无法生成AI Token'); + } + + console.log('Token获取成功:', token); + return { success: true, token }; + + } catch (error) { + console.error('Token获取失败:', error); + return { success: false, error: error.message }; + } +} + +/** + * 示例7: Token格式验证 + */ +export function validateTokenExample() { + const token = getAiToken(); + + // UUID格式验证正则表达式 + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + const isValid = uuidRegex.test(token); + + console.log('Token:', token); + console.log('格式是否有效:', isValid); + + return { + token, + isValid, + format: isValid ? 'UUID v4' : '无效格式' + }; +} + +// 导出所有示例函数 +export default { + basicUsageExample, + checkTokenExample, + resetTokenExample, + apiCallExample, + appInitExample, + errorHandlingExample, + validateTokenExample +}; diff --git a/src/utils/aiToken.ts b/src/utils/aiToken.ts new file mode 100644 index 0000000..4fe189b --- /dev/null +++ b/src/utils/aiToken.ts @@ -0,0 +1,61 @@ +import Taro from '@tarojs/taro'; + +/** + * 生成UUID字符串 + */ +export function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +/** + * 生成AI Token + */ +export function generateAiToken(): string { + try { + const token = generateUUID(); + Taro.setStorageSync('AI_TOKEN', token); + console.log('AI Token生成成功:', token); + return token; + } catch (error) { + console.error('生成AI Token失败:', error); + throw error; + } +} + +/** + * 获取AI Token,如果不存在则生成一个 + */ +export function getAiToken(): string { + const token = Taro.getStorageSync('AI_TOKEN'); + + // 如果没有token,生成一个新的 + if (!token) { + return generateAiToken(); + } + + return token; +} + +/** + * 清除AI Token + */ +export function clearAiToken(): void { + try { + Taro.removeStorageSync('AI_TOKEN'); + console.log('AI Token已清除'); + } catch (error) { + console.error('清除AI Token失败:', error); + } +} + +/** + * 检查AI Token是否存在 + */ +export function hasAiToken(): boolean { + const token = Taro.getStorageSync('AI_TOKEN'); + return !!token; +} diff --git a/src/utils/test-ai-token.md b/src/utils/test-ai-token.md new file mode 100644 index 0000000..84057de --- /dev/null +++ b/src/utils/test-ai-token.md @@ -0,0 +1,102 @@ +# AI Token 自动生成功能测试 + +## 功能说明 + +实现了AI_TOKEN的自动生成功能,当AI_TOKEN不存在时会自动生成一个UUID字符串。 + +## 实现内容 + +### 1. 工具函数 (`src/utils/aiToken.ts`) +- ✅ `generateUUID()`: 生成标准UUID字符串 +- ✅ `generateAiToken()`: 生成并存储AI Token +- ✅ `getAiToken()`: 获取AI Token,不存在时自动生成 +- ✅ `clearAiToken()`: 清除AI Token +- ✅ `hasAiToken()`: 检查AI Token是否存在 + +### 2. AI聊天页面集成 (`src/pages/ai/index.tsx`) +- ✅ 应用初始化时自动检查并生成AI Token +- ✅ 发送消息前检查AI Token存在性 +- ✅ 在API请求中包含AI Token +- ✅ 简化了token管理逻辑 + +### 3. API接口更新 (`src/api/ai/index.ts`) +- ✅ 更新`AiChatMessage`接口,添加`aiToken`字段 +- ✅ 移除了不需要的`generateAiToken` API函数 + +## 使用方式 + +### 自动生成 +```typescript +import { getAiToken } from '@/utils/aiToken'; + +// 获取AI Token,如果不存在会自动生成 +const token = getAiToken(); +``` + +### 手动管理 +```typescript +import { generateAiToken, clearAiToken, hasAiToken } from '@/utils/aiToken'; + +// 检查是否存在 +if (!hasAiToken()) { + // 生成新token + const token = generateAiToken(); +} + +// 清除token +clearAiToken(); +``` + +## 测试步骤 + +### 1. 基本功能测试 +1. 清除本地存储中的AI_TOKEN +2. 打开AI聊天页面 +3. 检查控制台是否输出"AI Token生成成功" +4. 检查本地存储是否保存了AI_TOKEN + +### 2. 持久性测试 +1. 刷新页面或重新进入 +2. 检查是否使用了已存在的token(不会重新生成) +3. 验证token格式是否为标准UUID + +### 3. 发送消息测试 +1. 发送一条AI消息 +2. 检查网络请求中是否包含aiToken字段 +3. 验证token值是否正确 + +### 4. 工具函数测试 +```javascript +// 在浏览器控制台中测试 +import { getAiToken, clearAiToken, hasAiToken } from '@/utils/aiToken'; + +// 测试生成 +console.log('Token:', getAiToken()); + +// 测试检查 +console.log('Has token:', hasAiToken()); + +// 测试清除 +clearAiToken(); +console.log('After clear:', hasAiToken()); +``` + +## 预期结果 + +- ✅ 首次访问时自动生成UUID格式的AI Token +- ✅ Token持久保存在本地存储中 +- ✅ 后续访问使用已存在的token +- ✅ 发送AI消息时自动包含token +- ✅ 提供完整的token管理工具函数 + +## UUID格式示例 +``` +xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx +例如: 123e4567-e89b-12d3-a456-426614174000 +``` + +## 注意事项 +- Token存储在本地,清除应用数据会丢失 +- 每个用户/设备会有独立的token +- Token格式为标准UUID v4 +- 无需用户登录即可生成和使用 diff --git a/src/zzjy/index.tsx b/src/zzjy/index.tsx index 011114b..a8b21be 100644 --- a/src/zzjy/index.tsx +++ b/src/zzjy/index.tsx @@ -3,7 +3,8 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle"; import {CmsArticle} from "@/api/cms/cmsArticle/model"; import Taro from '@tarojs/taro' import {useRouter} from '@tarojs/taro' -import {Image,InfiniteLoading} from '@nutui/nutui-react-taro' +import {Image} from '@nutui/nutui-react-taro' +import {InfiniteLoading} from '@nutui/nutui-react-taro' import {getCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model";