优化:文章列表支持分页加载
This commit is contained in:
@@ -72,7 +72,7 @@ export default defineAppConfig({
|
|||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'black'
|
||||||
},
|
},
|
||||||
tabBar: {
|
tabBar: {
|
||||||
custom: false,
|
custom: false, // H5模式下暂时禁用自定义TabBar
|
||||||
color: "#8a8a8a",
|
color: "#8a8a8a",
|
||||||
selectedColor: "#d81e06",
|
selectedColor: "#d81e06",
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
@@ -89,12 +89,6 @@ export default defineAppConfig({
|
|||||||
selectedIconPath: "assets/tabbar/order-active.png",
|
selectedIconPath: "assets/tabbar/order-active.png",
|
||||||
text: "AI问答",
|
text: "AI问答",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// pagePath: "pages/kefu/kefu",
|
|
||||||
// iconPath: "assets/tabbar/kefu.png",
|
|
||||||
// selectedIconPath: "assets/tabbar/kefu-active.png",
|
|
||||||
// text: "客服",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
pagePath: "pages/user/user",
|
pagePath: "pages/user/user",
|
||||||
iconPath: "assets/tabbar/user.png",
|
iconPath: "assets/tabbar/user.png",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ page{
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
|
padding-bottom: 40px; // 为自定义TabBar预留空间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ const Index = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{padding: navigation?.span + 'px'}}>
|
<div style={{padding: navigation?.span + 'px'}}>
|
||||||
<Image src={navigation?.style} width={'100%'}
|
<Image
|
||||||
height={'auto'}/>
|
src={navigation?.style}
|
||||||
|
style={{width: '100%', height: 'auto'}}
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'bg-white rounded-lg py-3 px-2'}>
|
<div className={'bg-white rounded-lg py-3 px-2'}>
|
||||||
<div className={'grid grid-cols-2 gap-3'}>
|
<div className={'grid grid-cols-2 gap-3'}>
|
||||||
|
|||||||
BIN
src/assets/tabbar/ai-icon.png
Normal file
BIN
src/assets/tabbar/ai-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
240
src/components/MarkdownRenderer.scss
Normal file
240
src/components/MarkdownRenderer.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/components/MarkdownRenderer.tsx
Normal file
196
src/components/MarkdownRenderer.tsx
Normal file
@@ -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<MarkdownRendererProps> = ({ 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(<View key={`empty-${currentIndex++}`} className="markdown-br" />);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题 (# ## ###)
|
||||||
|
if (trimmedLine.startsWith('#')) {
|
||||||
|
const level = trimmedLine.match(/^#+/)?.[0].length || 1;
|
||||||
|
const title = trimmedLine.replace(/^#+\s*/, '');
|
||||||
|
elements.push(
|
||||||
|
<View key={`heading-${currentIndex++}`} className={`markdown-heading markdown-h${level}`}>
|
||||||
|
<Text>{title}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代码块 (```)
|
||||||
|
if (trimmedLine.startsWith('```')) {
|
||||||
|
const codeLines: string[] = [];
|
||||||
|
i++; // 跳过开始的```
|
||||||
|
|
||||||
|
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
||||||
|
codeLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push(
|
||||||
|
<View key={`code-block-${currentIndex++}`} className="markdown-code-block">
|
||||||
|
<Text className="markdown-code-text">{codeLines.join('\n')}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表项 (- * +)
|
||||||
|
if (/^[-*+]\s/.test(trimmedLine)) {
|
||||||
|
const listItem = trimmedLine.replace(/^[-*+]\s/, '');
|
||||||
|
elements.push(
|
||||||
|
<View key={`list-${currentIndex++}`} className="markdown-list-item">
|
||||||
|
<Text className="markdown-bullet">•</Text>
|
||||||
|
<Text className="markdown-list-text">{parseInlineMarkdown(listItem)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字列表 (1. 2. 3.)
|
||||||
|
if (/^\d+\.\s/.test(trimmedLine)) {
|
||||||
|
const match = trimmedLine.match(/^(\d+)\.\s(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
const [, number, listItem] = match;
|
||||||
|
elements.push(
|
||||||
|
<View key={`ordered-list-${currentIndex++}`} className="markdown-ordered-list-item">
|
||||||
|
<Text className="markdown-number">{number}.</Text>
|
||||||
|
<Text className="markdown-list-text">{parseInlineMarkdown(listItem)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用 (>)
|
||||||
|
if (trimmedLine.startsWith('>')) {
|
||||||
|
const quote = trimmedLine.replace(/^>\s*/, '');
|
||||||
|
elements.push(
|
||||||
|
<View key={`quote-${currentIndex++}`} className="markdown-blockquote">
|
||||||
|
<Text className="markdown-quote-text">{parseInlineMarkdown(quote)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通段落
|
||||||
|
elements.push(
|
||||||
|
<View key={`paragraph-${currentIndex++}`} className="markdown-paragraph">
|
||||||
|
<Text className="markdown-text">{parseInlineMarkdown(trimmedLine)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析行内Markdown语法
|
||||||
|
const parseInlineMarkdown = (text: string): JSX.Element[] => {
|
||||||
|
if (!text) return [<Text key="empty"></Text>];
|
||||||
|
|
||||||
|
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(
|
||||||
|
<Text key={`code-${keyIndex++}`} className="markdown-inline-code">
|
||||||
|
{code}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
return `__PLACEHOLDER_${result.length - 1}__`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理粗体 **text**
|
||||||
|
current = current.replace(/\*\*([^*]+)\*\*/g, (match, text) => {
|
||||||
|
result.push(
|
||||||
|
<Text key={`bold-${keyIndex++}`} className="markdown-bold">
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
return `__PLACEHOLDER_${result.length - 1}__`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理斜体 *text*
|
||||||
|
current = current.replace(/\*([^*]+)\*/g, (match, text) => {
|
||||||
|
result.push(
|
||||||
|
<Text key={`italic-${keyIndex++}`} className="markdown-italic">
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
return `__PLACEHOLDER_${result.length - 1}__`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理链接 [text](url)
|
||||||
|
current = current.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
|
||||||
|
result.push(
|
||||||
|
<Text key={`link-${keyIndex++}`} className="markdown-link">
|
||||||
|
{linkText}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
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(<Text key={`text-${keyIndex++}`}>{part}</Text>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalResult.length > 0 ? finalResult : [<Text key={`default-${keyIndex++}`}>{segment}</Text>];
|
||||||
|
};
|
||||||
|
|
||||||
|
return parseSegment(remaining);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderedContent = parseMarkdown(content);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={`markdown-renderer ${className}`}>
|
||||||
|
{renderedContent}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownRenderer;
|
||||||
111
src/components/MarkdownTest.tsx
Normal file
111
src/components/MarkdownTest.tsx
Normal file
@@ -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 (
|
||||||
|
<View style={{ padding: '20px', backgroundColor: '#f5f5f5' }}>
|
||||||
|
<View style={{ marginBottom: '20px' }}>
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
}}>
|
||||||
|
<View style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '10px',
|
||||||
|
color: '#333'
|
||||||
|
}}>
|
||||||
|
AI消息样式预览
|
||||||
|
</View>
|
||||||
|
<MarkdownRenderer
|
||||||
|
content={testContent}
|
||||||
|
className="ai-markdown"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<View style={{
|
||||||
|
background: 'linear-gradient(135deg, #ff3535 0%, #FF0000 100%)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}>
|
||||||
|
<View style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '10px',
|
||||||
|
color: '#fff'
|
||||||
|
}}>
|
||||||
|
用户消息样式预览
|
||||||
|
</View>
|
||||||
|
<MarkdownRenderer
|
||||||
|
content={testContent}
|
||||||
|
className="user-markdown"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownTest;
|
||||||
227
src/components/TabBar.scss
Normal file
227
src/components/TabBar.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,35 +1,112 @@
|
|||||||
import {Tabbar} from '@nutui/nutui-react-taro'
|
import { useState, useEffect } from 'react';
|
||||||
import {Home, User, Date} from '@nutui/icons-react-taro'
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro';
|
||||||
|
import './TabBar.scss';
|
||||||
|
|
||||||
function TabBar() {
|
interface TabBarItem {
|
||||||
return (
|
pagePath: string;
|
||||||
<Tabbar
|
text: string;
|
||||||
fixed
|
iconPath?: string;
|
||||||
onSwitch={(index) => {
|
selectedIconPath?: string;
|
||||||
console.log(index)
|
isSpecial?: boolean; // 标记是否为特殊样式(中间的AI按钮)
|
||||||
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)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabbar.Item title="首页" icon={<Home size={18}/>}/>
|
|
||||||
<Tabbar.Item title="AI问答" icon={<Date size={18}/>}/>
|
|
||||||
<Tabbar.Item title="我的" icon={<User size={18}/>}/>
|
|
||||||
</Tabbar>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
|
<View className="custom-tabbar">
|
||||||
|
<View className="tabbar-container">
|
||||||
|
{tabBarData.map((item, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
className={`tabbar-item ${selected === index ? 'selected' : ''} ${item.isSpecial ? 'special-item' : ''}`}
|
||||||
|
onClick={() => switchTab(index, item)}
|
||||||
|
>
|
||||||
|
{item.isSpecial ? (
|
||||||
|
// AI问答特殊样式
|
||||||
|
<View className="special-icon">
|
||||||
|
<View className="ai-circle">
|
||||||
|
<Text className="ai-text">AI</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
// 普通图标
|
||||||
|
<View className="normal-icon">
|
||||||
|
<Text className={`icon-text ${selected === index ? 'selected' : ''}`}>
|
||||||
|
{index === 0 ? '🏠' : '👤'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Text className={`tabbar-text ${selected === index ? 'selected' : ''} ${item.isSpecial ? 'special-text' : ''}`}>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomTabBar;
|
||||||
|
|||||||
196
src/components/custom-tabbar-guide.md
Normal file
196
src/components/custom-tabbar-guide.md
Normal file
@@ -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());
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 未来扩展
|
||||||
|
|
||||||
|
可以考虑添加的功能:
|
||||||
|
- [ ] 红点提醒功能
|
||||||
|
- [ ] 更多动画效果
|
||||||
|
- [ ] 主题切换支持
|
||||||
|
- [ ] 国际化支持
|
||||||
|
- [ ] 无障碍访问优化
|
||||||
159
src/components/markdown-test.md
Normal file
159
src/components/markdown-test.md
Normal file
@@ -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';
|
||||||
|
|
||||||
|
<MarkdownRenderer
|
||||||
|
content="# 标题\n这是**粗体**文本"
|
||||||
|
className="custom-style"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在AI聊天中的应用
|
||||||
|
```tsx
|
||||||
|
<MarkdownRenderer
|
||||||
|
content={message.query}
|
||||||
|
className={`message-markdown ${message.type === 'user' ? 'user-markdown' : 'ai-markdown'}`}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 样式主题
|
||||||
|
|
||||||
|
### AI消息主题
|
||||||
|
- 白色背景的代码块
|
||||||
|
- 蓝色的链接和引用
|
||||||
|
- 清晰的层次结构
|
||||||
|
|
||||||
|
### 用户消息主题
|
||||||
|
- 半透明的代码块
|
||||||
|
- 白色的文本和链接
|
||||||
|
- 适配红色渐变背景
|
||||||
|
|
||||||
|
## 测试示例
|
||||||
|
|
||||||
|
可以在AI问答中测试以下Markdown内容:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# AI助手功能介绍
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
- **智能问答**: 回答各种问题
|
||||||
|
- **代码解释**: 解释代码逻辑
|
||||||
|
- **文档生成**: 生成技术文档
|
||||||
|
|
||||||
|
## 代码示例
|
||||||
|
```javascript
|
||||||
|
function greet(name) {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
> 请确保输入的问题清晰明确,这样我能提供更准确的回答。
|
||||||
|
|
||||||
|
### 联系方式
|
||||||
|
如有问题,请访问 [官方网站](https://example.com)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 解析流程
|
||||||
|
1. 按行分割文本
|
||||||
|
2. 识别Markdown语法
|
||||||
|
3. 转换为React组件
|
||||||
|
4. 应用相应样式
|
||||||
|
|
||||||
|
### 性能考虑
|
||||||
|
- 避免复杂的正则表达式
|
||||||
|
- 使用简单的字符串匹配
|
||||||
|
- 最小化DOM操作
|
||||||
|
- 缓存解析结果
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
未来可以添加的功能:
|
||||||
|
- [ ] 表格支持
|
||||||
|
- [ ] 图片渲染
|
||||||
|
- [ ] 数学公式
|
||||||
|
- [ ] 语法高亮
|
||||||
|
- [ ] 自定义主题
|
||||||
|
- [ ] 导出功能
|
||||||
214
src/components/markdown-usage.md
Normal file
214
src/components/markdown-usage.md
Normal file
@@ -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
|
||||||
|
<MarkdownRenderer
|
||||||
|
content={message.query || '正在思考中...'}
|
||||||
|
className={`message-markdown ${message.type === 'user' ? 'user-markdown' : 'ai-markdown'}`}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 独立使用
|
||||||
|
```tsx
|
||||||
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||||
|
|
||||||
|
<MarkdownRenderer
|
||||||
|
content="# 标题\n这是**粗体**文本"
|
||||||
|
className="custom-style"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 测试示例
|
||||||
|
|
||||||
|
在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);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 未来扩展
|
||||||
|
|
||||||
|
可以考虑添加的功能:
|
||||||
|
- [ ] 表格支持
|
||||||
|
- [ ] 图片渲染
|
||||||
|
- [ ] 数学公式
|
||||||
|
- [ ] 语法高亮
|
||||||
|
- [ ] 自定义主题
|
||||||
|
- [ ] 导出功能
|
||||||
5
src/custom-tab-bar/index.tsx
Normal file
5
src/custom-tab-bar/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import SimpleTabBar from '../components/SimpleTabBar';
|
||||||
|
|
||||||
|
export default function CustomTabBarWrapper() {
|
||||||
|
return <SimpleTabBar />;
|
||||||
|
}
|
||||||
@@ -35,8 +35,11 @@ const Article = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mobile-container">
|
<div className="mobile-container">
|
||||||
<Image src={navigation?.style} width={'100%'}
|
<Image
|
||||||
height={'auto'}/>
|
src={navigation?.style}
|
||||||
|
style={{width: '100%', height: 'auto'}}
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
<div className={'bg-white rounded-lg py-3 px-2'}>
|
<div className={'bg-white rounded-lg py-3 px-2'}>
|
||||||
{/* 宫格布局容器 */}
|
{/* 宫格布局容器 */}
|
||||||
<div className={'grid grid-cols-3'}>
|
<div className={'grid grid-cols-3'}>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
|||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useRouter} 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 {getCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||||
|
|
||||||
@@ -14,32 +15,60 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
|||||||
const Index = () => {
|
const Index = () => {
|
||||||
const {params} = useRouter();
|
const {params} = useRouter();
|
||||||
const [navigation, setNavigation] = useState<CmsNavigation>()
|
const [navigation, setNavigation] = useState<CmsNavigation>()
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [list, setList] = useState<CmsArticle[]>([])
|
const [list, setList] = useState<CmsArticle[]>([])
|
||||||
|
|
||||||
const reload = async () => {
|
|
||||||
// 获取栏目ID
|
|
||||||
const categoryId = Number(params.id);
|
const categoryId = Number(params.id);
|
||||||
|
|
||||||
|
const reload = async () => {
|
||||||
// 当前栏目信息
|
// 当前栏目信息
|
||||||
const navs = await getCmsNavigation(categoryId);
|
const navs = await getCmsNavigation(categoryId);
|
||||||
// 终极新闻列表
|
|
||||||
const articles = await pageCmsArticle({categoryId,limit: 50});
|
|
||||||
|
|
||||||
// 当前栏目信息
|
// 当前栏目信息
|
||||||
if (navs) {
|
if (navs) {
|
||||||
setNavigation(navs);
|
setNavigation(navs);
|
||||||
}
|
}
|
||||||
// 新闻列表
|
// 终极新闻列表
|
||||||
if (articles) {
|
getList()
|
||||||
setList(articles?.list || [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 终极新闻列表
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
reload()
|
reload().then()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteLoading className={'bg-red-200 h-full'}>
|
<InfiniteLoading
|
||||||
|
className={'bg-red-200 h-full'}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={reloadMore}
|
||||||
|
loadingText={
|
||||||
|
<>
|
||||||
|
加载中
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
loadMoreText={
|
||||||
|
<>
|
||||||
|
没有更多了
|
||||||
|
</>
|
||||||
|
}>
|
||||||
<div style={{padding: navigation?.span + 'px'}}>
|
<div style={{padding: navigation?.span + 'px'}}>
|
||||||
<Image src={navigation?.style} width={'100%'}
|
<Image src={navigation?.style} width={'100%'}
|
||||||
height={'auto'}/>
|
height={'auto'}/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
|||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useRouter} 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 {getCmsNavigation, pageCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ const Index = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'bg-red-200 h-full'}>
|
<InfiniteLoading className={'bg-red-200 h-full'}>
|
||||||
<div style={{padding: navigation?.span + 'px'}}>
|
<div style={{padding: navigation?.span + 'px'}}>
|
||||||
<Image src={navigation?.style} width={'100%'}
|
<Image src={navigation?.style} width={'100%'}
|
||||||
height={'auto'}/>
|
height={'auto'}/>
|
||||||
@@ -122,7 +122,7 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</InfiniteLoading>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default Index
|
export default Index
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
|||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useRouter} 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 {getCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
|||||||
const List = () => {
|
const List = () => {
|
||||||
const {params} = useRouter();
|
const {params} = useRouter();
|
||||||
const [navigation, setNavigation] = useState<CmsNavigation>()
|
const [navigation, setNavigation] = useState<CmsNavigation>()
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [list, setList] = useState<CmsArticle[]>([])
|
const [list, setList] = useState<CmsArticle[]>([])
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
@@ -22,7 +25,7 @@ const List = () => {
|
|||||||
// 当前栏目信息
|
// 当前栏目信息
|
||||||
const navs = await getCmsNavigation(categoryId);
|
const navs = await getCmsNavigation(categoryId);
|
||||||
// 终极新闻列表
|
// 终极新闻列表
|
||||||
const articles = await pageCmsArticle({categoryId, limit: 50});
|
const articles = await pageCmsArticle({categoryId, page});
|
||||||
|
|
||||||
// 当前栏目信息
|
// 当前栏目信息
|
||||||
if (navs) {
|
if (navs) {
|
||||||
@@ -30,29 +33,59 @@ const List = () => {
|
|||||||
}
|
}
|
||||||
// 新闻列表
|
// 新闻列表
|
||||||
if (articles) {
|
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(() => {
|
useEffect(() => {
|
||||||
reload()
|
reload().then()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteLoading className={'bg-red-200'} style={{height: '100vh'}}>
|
<InfiniteLoading
|
||||||
|
className={'bg-red-200'}
|
||||||
|
style={{height: '100vh'}}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={reloadMore}
|
||||||
|
loadingText={
|
||||||
|
<>
|
||||||
|
加载中
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
loadMoreText={
|
||||||
|
<>
|
||||||
|
没有更多了
|
||||||
|
</>
|
||||||
|
}>
|
||||||
<div style={{padding: navigation?.span + 'px'}}>
|
<div style={{padding: navigation?.span + 'px'}}>
|
||||||
<Image src={navigation?.style} width={'100%'}
|
<Image
|
||||||
height={'auto'}/>
|
src={navigation?.style || ''}
|
||||||
|
style={{width: '100%', height: 'auto'}}
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'p-4'}>
|
<div className={'p-3'}>
|
||||||
<div
|
<div
|
||||||
className={'relative'}
|
className={'relative'}
|
||||||
style={{
|
style={{
|
||||||
background: 'url(https://oss.wsdns.cn/20250708/d7a8aad52f6048e5adce13ef0ea86216.png)',
|
background: 'url(https://oss.wsdns.cn/20250708/d7a8aad52f6048e5adce13ef0ea86216.png)',
|
||||||
backgroundSize: '100%',
|
backgroundSize: '120%',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
borderRadius: '40px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '67px',
|
height: '65px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
|
|||||||
121
src/pages/ai/debug-fix.md
Normal file
121
src/pages/ai/debug-fix.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# AI问答页面输入问题修复
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
第一次无法输入提问内容的可能原因:
|
||||||
|
|
||||||
|
1. ✅ **WebSocket连接问题**: 使用了错误的连接参数(AI_TOKEN而不是UserId)
|
||||||
|
2. ✅ **用户标识错误**: 消息中的user字段使用了AI_TOKEN而不是UserId
|
||||||
|
3. ✅ **初始化时序问题**: 输入框可能在初始化完成前被禁用
|
||||||
|
4. ✅ **状态管理问题**: 缺少初始化状态跟踪
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
|
||||||
|
### 1. WebSocket连接修复
|
||||||
|
```typescript
|
||||||
|
// 修复前
|
||||||
|
wsRef.current = createWebSocket(WSS_API_URL + "/chat/" + Taro.getStorageSync('AI_TOKEN'));
|
||||||
|
|
||||||
|
// 修复后
|
||||||
|
const userId = Taro.getStorageSync('UserId') || 'anonymous';
|
||||||
|
wsRef.current = createWebSocket(WSS_API_URL + "/chat/" + userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 用户标识修复
|
||||||
|
```typescript
|
||||||
|
// 修复前
|
||||||
|
user: `${Taro.getStorageSync('AI_TOKEN')}`
|
||||||
|
|
||||||
|
// 修复后
|
||||||
|
user: `${Taro.getStorageSync('UserId') || 'anonymous'}`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 初始化状态管理
|
||||||
|
```typescript
|
||||||
|
// 添加初始化状态
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
// 初始化完成后设置状态
|
||||||
|
setIsInitialized(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 输入框状态优化
|
||||||
|
```typescript
|
||||||
|
// 输入框禁用逻辑
|
||||||
|
disabled={!isInitialized || isLoading}
|
||||||
|
|
||||||
|
// 占位符文本
|
||||||
|
placeholder={
|
||||||
|
!isInitialized ? "正在初始化..." :
|
||||||
|
isLoading ? "AI正在回复中..." :
|
||||||
|
"请输入您的问题..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 发送按钮状态优化
|
||||||
|
```typescript
|
||||||
|
// 发送按钮禁用逻辑
|
||||||
|
disabled={!isInitialized || !inputValue.trim()}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 调试功能
|
||||||
|
- 添加了详细的状态调试信息
|
||||||
|
- 输入框焦点和点击事件监听
|
||||||
|
- 初始化完成状态跟踪
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 1. 基本输入测试
|
||||||
|
1. 打开AI问答页面
|
||||||
|
2. 等待初始化完成(看到"请输入您的问题...")
|
||||||
|
3. 点击输入框,检查是否能正常输入
|
||||||
|
4. 输入文字,检查是否正常显示
|
||||||
|
|
||||||
|
### 2. 快捷问题测试
|
||||||
|
1. 点击快捷问题按钮
|
||||||
|
2. 检查是否能正常发送消息
|
||||||
|
3. 验证初始化状态检查
|
||||||
|
|
||||||
|
### 3. 状态调试测试
|
||||||
|
1. 打开浏览器控制台
|
||||||
|
2. 查看初始化日志:
|
||||||
|
- "AI Token初始化完成: xxx"
|
||||||
|
- "输入框状态调试: {...}"
|
||||||
|
3. 点击输入框查看状态变化
|
||||||
|
|
||||||
|
### 4. WebSocket连接测试
|
||||||
|
1. 检查控制台WebSocket连接日志
|
||||||
|
2. 验证连接URL是否正确
|
||||||
|
3. 确认连接状态指示器工作正常
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
|
||||||
|
- ✅ 页面加载后输入框立即可用
|
||||||
|
- ✅ 输入框状态正确显示
|
||||||
|
- ✅ 快捷问题正常工作
|
||||||
|
- ✅ WebSocket连接正常
|
||||||
|
- ✅ 消息发送功能正常
|
||||||
|
|
||||||
|
## 调试信息示例
|
||||||
|
|
||||||
|
控制台应该显示类似信息:
|
||||||
|
```
|
||||||
|
AI Token初始化完成: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||||
|
wsUrl: wss://cms-api.websoft.top/api/chat/12345
|
||||||
|
WebSocket连接成功
|
||||||
|
输入框状态调试: {
|
||||||
|
isInitialized: true,
|
||||||
|
isLoading: false,
|
||||||
|
wsConnected: true,
|
||||||
|
inputValue: "",
|
||||||
|
inputDisabled: false,
|
||||||
|
sendButtonDisabled: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保UserId存在,否则使用'anonymous'作为默认值
|
||||||
|
2. AI_TOKEN仍然用于API请求认证
|
||||||
|
3. WebSocket连接使用UserId进行房间区分
|
||||||
|
4. 初始化状态确保用户体验流畅
|
||||||
@@ -13,10 +13,11 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-chat {
|
.ai-chat {
|
||||||
height: 98vh;
|
height: calc(100vh - 80px); // 减去TabBar高度
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
margin-bottom: 80px; // 为TabBar预留空间
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
background: linear-gradient(135deg, #a6ea66 0%, #ead1ff 100%);
|
background: linear-gradient(135deg, #a6ea66 0%, #ead1ff 100%);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|||||||
import {View, RichText} from '@tarojs/components'
|
import {View, RichText} from '@tarojs/components'
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import {WSS_API_URL} from "@/utils/server";
|
import {WSS_API_URL} from "@/utils/server";
|
||||||
|
import SimpleH5TabBar from "@/components/SimpleH5TabBar";
|
||||||
|
|
||||||
// 消息类型
|
// 消息类型
|
||||||
interface Message {
|
interface Message {
|
||||||
@@ -162,6 +163,7 @@ const AiChat = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Taro.hideTabBar()
|
||||||
// 初始化时检查并生成AI Token
|
// 初始化时检查并生成AI Token
|
||||||
const token = checkAiToken();
|
const token = checkAiToken();
|
||||||
console.log('AI Token初始化完成:', token);
|
console.log('AI Token初始化完成:', token);
|
||||||
|
|||||||
135
src/pages/ai/input-fix-checklist.md
Normal file
135
src/pages/ai/input-fix-checklist.md
Normal file
@@ -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连接稳定
|
||||||
|
- ✅ 错误处理完善
|
||||||
|
- ✅ 用户体验流畅
|
||||||
60
src/pages/ai/test-realtime.md
Normal file
60
src/pages/ai/test-realtime.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# AI聊天实时显示优化完成
|
||||||
|
|
||||||
|
## 🚀 主要优化内容
|
||||||
|
|
||||||
|
### 1. 实时消息显示
|
||||||
|
- ✅ **优化WebSocket消息处理**:简化了消息更新逻辑,直接追加内容而不是复杂的状态管理
|
||||||
|
- ✅ **移除延迟效果**:去掉了打字机效果,改为实时显示内容
|
||||||
|
- ✅ **实时滚动**:消息更新时立即滚动到底部,延迟仅50ms
|
||||||
|
|
||||||
|
### 2. 用户体验改进
|
||||||
|
- ✅ **即时反馈**:发送消息后立即显示AI占位符"正在思考中..."
|
||||||
|
- ✅ **智能按钮状态**:加载时显示"停止"按钮,支持中断AI回复
|
||||||
|
- ✅ **输入框优化**:加载时禁用输入并显示状态提示
|
||||||
|
- ✅ **错误处理**:网络错误时显示友好的错误消息
|
||||||
|
|
||||||
|
### 3. 连接状态管理
|
||||||
|
- ✅ **连接状态指示器**:实时显示WebSocket连接状态
|
||||||
|
- ✅ **智能重连机制**:递增重连间隔,避免频繁重连
|
||||||
|
- ✅ **手动重连**:提供"立即重连"按钮,用户可主动重连
|
||||||
|
- ✅ **连接检查**:发送消息前检查连接状态
|
||||||
|
|
||||||
|
### 4. 视觉效果优化
|
||||||
|
- ✅ **打字光标动画**:AI回复时显示闪烁光标效果
|
||||||
|
- ✅ **流畅布局**:消息内容支持实时追加,无布局跳动
|
||||||
|
- ✅ **按钮样式**:优化发送和停止按钮的视觉效果
|
||||||
|
- ✅ **状态提示**:连接断开时显示醒目的状态栏
|
||||||
|
|
||||||
|
### 5. 性能优化
|
||||||
|
- ✅ **减少重渲染**:优化状态更新逻辑,减少不必要的组件重渲染
|
||||||
|
- ✅ **内存管理**:正确清理WebSocket连接和定时器
|
||||||
|
- ✅ **错误边界**:添加完善的错误处理和用户提示
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. **基本功能测试**
|
||||||
|
- 打开AI聊天页面
|
||||||
|
- 发送一条消息
|
||||||
|
- 观察AI回复是否实时显示
|
||||||
|
|
||||||
|
2. **实时性测试**
|
||||||
|
- 发送较长的问题
|
||||||
|
- 观察回复内容是否逐字显示
|
||||||
|
- 检查是否有明显延迟
|
||||||
|
|
||||||
|
3. **连接状态测试**
|
||||||
|
- 断开网络连接
|
||||||
|
- 观察连接状态指示器
|
||||||
|
- 恢复网络,检查重连功能
|
||||||
|
|
||||||
|
4. **交互测试**
|
||||||
|
- 测试停止按钮功能
|
||||||
|
- 测试快捷问题点击
|
||||||
|
- 测试输入框状态变化
|
||||||
|
|
||||||
|
## 预期效果
|
||||||
|
|
||||||
|
- AI回复内容应该实时逐字显示,无明显延迟
|
||||||
|
- 用户发送消息后立即看到"正在思考中..."提示
|
||||||
|
- 连接断开时有明确的状态提示
|
||||||
|
- 整体交互更加流畅和响应迅速
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {useEffect} from "react";
|
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'
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
const BestSellers = (props: any) => {
|
const BestSellers = (props: any) => {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {useEffect, useState} from "react";
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import {Button, Space} from '@nutui/nutui-react-taro'
|
import {Button, Space} from '@nutui/nutui-react-taro'
|
||||||
import {TriangleDown,ArrowLeft} from '@nutui/icons-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";
|
import {TenantId} from "@/utils/config";
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
@@ -82,9 +83,15 @@ const Header = (props: any) => {
|
|||||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
<div style={{display: 'flex', alignItems: 'center'}}>
|
||||||
<Button style={{color: '#000'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
<Button style={{color: '#000'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
||||||
<Space>
|
<Space>
|
||||||
<Avatar
|
<Image
|
||||||
size="22"
|
src={props.user?.avatar || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMzAiIGZpbGw9IiNGNUY1RjUiLz4KPGNpcmNsZSBjeD0iMzAiIGN5PSIyNCIgcj0iMTAiIGZpbGw9IiNEOUQ5RDkiLz4KPHBhdGggZD0iTTEwIDUwQzEwIDQwIDIwIDM1IDMwIDM1QzQwIDM1IDUwIDQwIDUwIDUwIiBmaWxsPSIjRDlEOUQ5Ii8+Cjwvc3ZnPgo='}
|
||||||
src={props.user?.avatar}
|
style={{
|
||||||
|
width: '22px',
|
||||||
|
height: '22px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<span style={{color: '#000'}}>{props.user?.nickname}</span>
|
<span style={{color: '#000'}}>{props.user?.nickname}</span>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -23,11 +23,26 @@ const Page = () => {
|
|||||||
<div className={'py-2 my-3 mx-2'}>
|
<div className={'py-2 my-3 mx-2'}>
|
||||||
<div className={'bg-white grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-2'}>
|
<div className={'bg-white grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-2'}>
|
||||||
{
|
{
|
||||||
navItems?.map((item) => (
|
navItems?.map((item, index) => (
|
||||||
<div className={'flex flex-col justify-center items-center'} onClick={() => Taro.navigateTo({url: `/${item.model}/index?id=${item.navigationId}`})}>
|
<div
|
||||||
<Image className={'shadow-xl rounded-lg'} style={{borderRadius: '8px'}} src={item.icon}
|
key={item.navigationId || index}
|
||||||
height={90} width={90}/>
|
className={'flex flex-col justify-center items-center'}
|
||||||
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>{item?.title}</div>
|
onClick={() => Taro.navigateTo({url: `/${item.model}/index?id=${item.navigationId}`})}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className={'shadow-xl rounded-lg'}
|
||||||
|
style={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
width: '90px',
|
||||||
|
height: '90px',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
src={item.icon}
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>
|
||||||
|
{item?.title}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {getSiteInfo} from "@/api/layout";
|
|||||||
import Login from "./Login";
|
import Login from "./Login";
|
||||||
import Banner from "./Banner";
|
import Banner from "./Banner";
|
||||||
import Menu from "./Menu";
|
import Menu from "./Menu";
|
||||||
import TabBar from "@/components/TabBar";
|
|
||||||
import Image from "./Image";
|
import Image from "./Image";
|
||||||
|
import SimpleH5TabBar from "@/components/SimpleH5TabBar";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
@@ -29,7 +29,7 @@ function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Taro.showTabBar()
|
Taro.hideTabBar()
|
||||||
// 获取站点信息
|
// 获取站点信息
|
||||||
getSiteInfo().then((data) => {
|
getSiteInfo().then((data) => {
|
||||||
console.log(data, 'siteInfo')
|
console.log(data, 'siteInfo')
|
||||||
@@ -45,8 +45,9 @@ function Home() {
|
|||||||
<Image/>
|
<Image/>
|
||||||
<Menu/>
|
<Menu/>
|
||||||
<Banner/>
|
<Banner/>
|
||||||
<TabBar/>
|
|
||||||
</>)}
|
</>)}
|
||||||
|
{/* H5模式下显示自定义TabBar */}
|
||||||
|
{process.env.TARO_ENV === 'h5' && <SimpleH5TabBar current={0} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {navigateTo} from '@tarojs/taro'
|
import {navigateTo} from '@tarojs/taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {Button} from '@tarojs/components';
|
import {Button, Image} from '@tarojs/components';
|
||||||
import {Image} from '@nutui/nutui-react-taro'
|
|
||||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||||
import {TenantId} from "@/utils/config";
|
import {TenantId} from "@/utils/config";
|
||||||
import {User} from "@/api/system/user/model";
|
import {User} from "@/api/system/user/model";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Avatar} from '@nutui/nutui-react-taro'
|
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
import {Avatar} from '@nutui/nutui-react-taro'
|
||||||
import {User} from "@/api/system/user/model";
|
import {User} from "@/api/system/user/model";
|
||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ import {useEffect} from 'react'
|
|||||||
import UserCard from "./components/UserCard";
|
import UserCard from "./components/UserCard";
|
||||||
import UserCell from "./components/UserCell";
|
import UserCell from "./components/UserCell";
|
||||||
import TabBar from "@/components/TabBar";
|
import TabBar from "@/components/TabBar";
|
||||||
|
import SimpleH5TabBar from "@/components/SimpleH5TabBar";
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Taro.hideTabBar()
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: '#ffefef', height: '100vh'}}>
|
<div style={{backgroundColor: '#ffefef', height: '100vh'}}>
|
||||||
<div className={'fixed w-full'}>
|
<div className={'fixed w-full'}>
|
||||||
<UserCard />
|
<UserCard/>
|
||||||
<UserCell />
|
<UserCell/>
|
||||||
<TabBar/>
|
{/* 小程序模式显示原TabBar,H5模式显示H5TabBar */}
|
||||||
|
{process.env.TARO_ENV !== 'h5' && <TabBar/>}
|
||||||
|
{process.env.TARO_ENV === 'h5' && <SimpleH5TabBar current={2}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ const Index = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={'bg-red-200 h-full'}>
|
<div className={'bg-red-200 h-full'}>
|
||||||
<div style={{padding: navigation?.span + 'px'}}>
|
<div style={{padding: navigation?.span + 'px'}}>
|
||||||
<Image src={navigation?.style} width={'100%'}
|
<Image
|
||||||
height={'auto'}/>
|
src={navigation?.style}
|
||||||
|
style={{width: '100%', height: 'auto'}}
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'py-3 px-3'}>
|
<div className={'py-3 px-3'}>
|
||||||
<div className={'grid grid-cols-2 gap-3'}>
|
<div className={'grid grid-cols-2 gap-3'}>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import {Cell, Avatar} from '@nutui/nutui-react-taro';
|
import {Cell} from '@nutui/nutui-react-taro';
|
||||||
import {ArrowRight} from '@nutui/icons-react-taro'
|
import {ArrowRight} from '@nutui/icons-react-taro'
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {ConfigProvider} from '@nutui/nutui-react-taro'
|
import {ConfigProvider} from '@nutui/nutui-react-taro'
|
||||||
|
import {Image} from '@nutui/nutui-react-taro'
|
||||||
import Taro, {getCurrentInstance} from '@tarojs/taro'
|
import Taro, {getCurrentInstance} from '@tarojs/taro'
|
||||||
import {getUserInfo, updateUserInfo} from "@/api/layout";
|
import {getUserInfo, updateUserInfo} from "@/api/layout";
|
||||||
import {TenantId} from "@/utils/config";
|
import {TenantId} from "@/utils/config";
|
||||||
@@ -116,7 +117,16 @@ function Profile() {
|
|||||||
<Cell title={'头像'} align={'center'} extra={
|
<Cell title={'头像'} align={'center'} extra={
|
||||||
<>
|
<>
|
||||||
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
|
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
|
||||||
<Avatar src={FormData?.avatar} size="54"/>
|
<Image
|
||||||
|
src={FormData?.avatar || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMzAiIGZpbGw9IiNGNUY1RjUiLz4KPGNpcmNsZSBjeD0iMzAiIGN5PSIyNCIgcj0iMTAiIGZpbGw9IiNEOUQ5RDkiLz4KPHBhdGggZD0iTTEwIDUwQzEwIDQwIDIwIDM1IDMwIDM1QzQwIDM1IDUwIDQwIDUwIDUwIiBmaWxsPSIjRDlEOUQ5Ii8+Cjwvc3ZnPgo='}
|
||||||
|
style={{
|
||||||
|
width: '54px',
|
||||||
|
height: '54px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<ArrowRight color="#cccccc" className={'ml-1'} size={20}/>
|
<ArrowRight color="#cccccc" className={'ml-1'} size={20}/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
134
src/utils/ai-token-example.ts
Normal file
134
src/utils/ai-token-example.ts
Normal file
@@ -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
|
||||||
|
};
|
||||||
61
src/utils/aiToken.ts
Normal file
61
src/utils/aiToken.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
102
src/utils/test-ai-token.md
Normal file
102
src/utils/test-ai-token.md
Normal file
@@ -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
|
||||||
|
- 无需用户登录即可生成和使用
|
||||||
@@ -3,7 +3,8 @@ import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
|||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useRouter} 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 {getCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user