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 0000000..b6599a3 Binary files /dev/null and b/src/assets/tabbar/ai-icon.png differ 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";