diff --git a/.gitignore b/.gitignore index abd9835..3e46355 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /pnpm-lock.yaml /.swc/ /.idea/ +node_modules +node_modules diff --git a/docs/PC端手机版样式指南.md b/docs/PC端手机版样式指南.md new file mode 100644 index 0000000..0047838 --- /dev/null +++ b/docs/PC端手机版样式指南.md @@ -0,0 +1,111 @@ +# PC端显示手机版样式指南 + +## 功能说明 + +本项目已实现PC端浏览器中显示手机版样式的功能,让用户在大屏幕设备上也能体验到移动端的界面效果。 + +## 实现原理 + +通过CSS媒体查询和容器限制,在PC端(屏幕宽度≥768px)时: +1. 限制页面最大宽度为414px(iPhone 6/7/8 Plus尺寸) +2. 页面居中显示 +3. 添加阴影和圆角效果,模拟手机屏幕 +4. 设置渐变背景,增强视觉效果 + +## 使用方法 + +### 1. 页面组件包装 + +在需要支持PC端手机版显示的页面组件中,使用 `mobile-container` 类包装: + +```tsx +function MyPage() { + return ( +
+ {/* 页面内容 */} +
+ ); +} +``` + +### 2. 样式文件引入 + +确保在全局样式文件中引入了手机版容器样式: + +```scss +// src/app.scss +@import './styles/mobile-container.scss'; +``` + +## 响应式断点 + +- **移动端** (≤767px): 保持原有全屏显示 +- **平板端** (768px-1023px): 414px宽度居中显示 +- **桌面端** (1024px-1439px): 375px宽度居中显示 +- **大屏端** (≥1440px): 414px宽度居中显示 + +## 视觉效果 + +### PC端效果 +- 页面宽度: 414px +- 居中显示 +- 圆角边框: 12px +- 阴影效果: 0 0 30px rgba(0, 0, 0, 0.15) +- 渐变背景: 蓝紫色渐变 + +### 移动端效果 +- 保持原有全屏显示 +- 无额外样式影响 + +## 已适配页面 + +以下页面已添加手机版容器支持: + +1. **首页** (`src/pages/index/index.tsx`) +2. **文章列表** (`src/custom/article/article.tsx`) +3. **文章详情** (`src/pages/article/detail.tsx`) +4. **用户中心** (`src/pages/user/user.tsx`) + +## 添加新页面支持 + +为新页面添加PC端手机版显示支持: + +```tsx +// 1. 导入样式(如果使用独立样式文件) +import './page.scss'; + +// 2. 使用容器包装 +function NewPage() { + return ( +
+ {/* 页面内容 */} +
+ ); +} +``` + +## 注意事项 + +1. **固定定位元素**: 使用 `fixed` 定位的元素需要特别处理,确保在PC端不超出容器范围 +2. **底部导航**: TabBar组件已自动适配PC端样式 +3. **图片资源**: 确保图片能够正确缩放适应容器宽度 +4. **交互体验**: PC端保持移动端的交互方式(点击、滑动等) + +## 自定义配置 + +可以通过修改 `src/styles/mobile-container.scss` 来调整: + +- 容器宽度 +- 阴影效果 +- 圆角大小 +- 背景颜色 +- 响应式断点 + +## 浏览器兼容性 + +- Chrome 60+ +- Firefox 55+ +- Safari 12+ +- Edge 79+ + +支持所有现代浏览器的CSS Grid和Flexbox特性。 diff --git a/docs/RichText使用指南.md b/docs/RichText使用指南.md new file mode 100644 index 0000000..ee72fe8 --- /dev/null +++ b/docs/RichText使用指南.md @@ -0,0 +1,91 @@ +# Taro RichText 富文本组件使用指南 + +## 基本用法 + +```tsx +import { RichText } from '@tarojs/components'; + +// 显示HTML字符串 + +``` + +## 主要属性 + +### nodes +- **类型**: `string | Array` +- **说明**: 富文本内容,支持HTML字符串或节点数组 + +### space +- **类型**: `'ensp' | 'emsp' | 'nbsp'` +- **说明**: 显示连续空格的方式 + - `ensp`: 中文字符空格一半大小 + - `emsp`: 中文字符空格大小 + - `nbsp`: 根据字体设置的空格大小 + +### selectable +- **类型**: `boolean` +- **默认值**: `false` +- **说明**: 富文本是否可以长按选中 + +## HTML内容处理 + +### 支持的HTML标签 +- 文本标签: `p`, `span`, `div`, `h1-h6`, `strong`, `b`, `em`, `i` +- 列表标签: `ul`, `ol`, `li` +- 其他标签: `img`, `a`, `br`, `hr` + +### 样式处理 +富文本内容的样式通过CSS全局样式定义: + +```scss +.content { + :global { + p { margin: 16px 0; } + img { max-width: 100%; } + // 更多样式... + } +} +``` + +## 实际应用示例 + +### 文章详情页面 +```tsx + + + +``` + +### 协议页面 +```tsx + + + +``` + +## 注意事项 + +1. **安全性**: RichText会直接渲染HTML,注意防范XSS攻击 +2. **性能**: 大量富文本内容可能影响性能 +3. **兼容性**: 不同平台对HTML标签支持程度不同 +4. **样式**: 使用`:global`确保样式能正确应用到富文本内容 + +## 常见问题 + +### 图片不显示 +- 检查图片URL是否正确 +- 确保图片域名在小程序白名单中 + +### 样式不生效 +- 使用`:global`包裹富文本样式 +- 检查CSS选择器优先级 + +### 内容被截断 +- 检查容器高度设置 +- 使用`max-width: 100%`防止内容溢出 diff --git a/docs/殊诚AI接口文档.pdf b/docs/殊诚AI接口文档.pdf new file mode 100644 index 0000000..b9f67ae Binary files /dev/null and b/docs/殊诚AI接口文档.pdf differ diff --git a/docs/贵港司法局AI paramJsonStr文档.pdf b/docs/贵港司法局AI paramJsonStr文档.pdf new file mode 100644 index 0000000..1e71136 Binary files /dev/null and b/docs/贵港司法局AI paramJsonStr文档.pdf differ diff --git a/docs/贵港司法局AI接口文档.pdf b/docs/贵港司法局AI接口文档.pdf new file mode 100644 index 0000000..08e77a3 Binary files /dev/null and b/docs/贵港司法局AI接口文档.pdf differ diff --git a/docs/贵港港北武装部红色宣传AI paramJsonStr文档.pdf b/docs/贵港港北武装部红色宣传AI paramJsonStr文档.pdf new file mode 100644 index 0000000..5593314 Binary files /dev/null and b/docs/贵港港北武装部红色宣传AI paramJsonStr文档.pdf differ diff --git a/docs/贵港祥安e家AI paramJsonStr文档.pdf b/docs/贵港祥安e家AI paramJsonStr文档.pdf new file mode 100644 index 0000000..fd7b3cd Binary files /dev/null and b/docs/贵港祥安e家AI paramJsonStr文档.pdf differ diff --git a/src/api/ai/index.ts b/src/api/ai/index.ts new file mode 100644 index 0000000..47bb114 --- /dev/null +++ b/src/api/ai/index.ts @@ -0,0 +1,50 @@ +import request from '@/utils/request'; +import type { ApiResult } from '@/api/index'; + +/** + * AI聊天消息接口 + */ +export interface AiChatMessage { + query: string; + user?: string; + responseMode?: string; + conversationId?: string; + type?: string; + requestType?: number; +} + +/** + * AI聊天响应接口 + */ +export interface AiChatResponse { + answer: string; + taskId: string; +} + +/** + * 发送AI聊天消息 + */ +export async function sendAiMessage(data: AiChatMessage) { + const res = await request.post>( + '/chat/message', + data + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 停止AI聊天 + */ +export async function stopAiMessage(data: { taskId: string; type?: string }) { + const res = await request.post>( + '/chat/messageStop', + data + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/app.config.ts b/src/app.config.ts index e510941..7d45227 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -46,13 +46,15 @@ export default defineAppConfig({ "root": "honor", "pages": [ "index", - "detail" + "detail", + "list" ] }, { "root": "expert", "pages": [ - "index" + "index", + "detail" ] }, ], diff --git a/src/article/detail.scss b/src/article/detail.scss new file mode 100644 index 0000000..2b461ff --- /dev/null +++ b/src/article/detail.scss @@ -0,0 +1,6 @@ +.content{ + img{ + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); + margin-bottom: 40px !important; +} +} diff --git a/src/article/detail.tsx b/src/article/detail.tsx index 22d5313..5617037 100644 --- a/src/article/detail.tsx +++ b/src/article/detail.tsx @@ -1,26 +1,26 @@ import {useEffect, useState} from 'react' -import {Tag} from '@nutui/nutui-react-taro' +// import {Tag} from '@nutui/nutui-react-taro' import {useRouter} from '@tarojs/taro' import {Divider} from '@nutui/nutui-react-taro' import {CmsArticle} from "@/api/cms/cmsArticle/model" -import {Eye} from '@nutui/icons-react-taro' +// import {Eye} from '@nutui/icons-react-taro' // 显示html富文本 import {View, RichText} from '@tarojs/components' -import Line from "@/components/Gap"; import {getCmsArticle} from "@/api/cms/cmsArticle"; +import './detail.scss'; function Detail() { const {params} = useRouter(); // 文章详情 const [item, setItem] = useState() // 浏览量 - const [views, setViews] = useState() + // const [views, setViews] = useState() const reload = () => { getCmsArticle(Number(params.id)).then(data => { if (data) { setItem(data) - setViews(data.actualViews) + // setViews(data.actualViews) } }) } @@ -32,10 +32,10 @@ function Detail() { return (
{item?.title}
-
- {item?.categoryName} -
{views}
-
+ {/*
*/} + {/* {item?.categoryName}*/} + {/*
{views}
*/} + {/*
*/} -
) } diff --git a/src/expert/detail.config.ts b/src/expert/detail.config.ts new file mode 100644 index 0000000..349c0e2 --- /dev/null +++ b/src/expert/detail.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '详情', + navigationBarBackgroundColor: '#ffe0e0' +}) diff --git a/src/expert/detail.css b/src/expert/detail.css new file mode 100644 index 0000000..ff6adb4 --- /dev/null +++ b/src/expert/detail.css @@ -0,0 +1,10 @@ +.content{ + img{ + margin: 12px; + background-color: #F2FE03; + padding: 6px; + border-radius: 16px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); + margin-bottom: 40px !important; + } +} diff --git a/src/expert/detail.tsx b/src/expert/detail.tsx new file mode 100644 index 0000000..b2acdcc --- /dev/null +++ b/src/expert/detail.tsx @@ -0,0 +1,46 @@ +import {useEffect, useState} from 'react' +import {useRouter} from '@tarojs/taro' +import {Divider} from '@nutui/nutui-react-taro' +import {CmsArticle} from "@/api/cms/cmsArticle/model" +// 显示html富文本 +import {View, RichText} from '@tarojs/components' +// import Line from "@/components/Gap"; +import {getCmsArticle} from "@/api/cms/cmsArticle"; +import './detail.css'; + +function Detail() { + const {params} = useRouter(); + // 文章详情 + const [item, setItem] = useState() + + const reload = () => { + getCmsArticle(Number(params.id)).then(data => { + if (data) { + setItem(data) + } + }) + } + + useEffect(() => { + reload(); + }, []); + + return ( +
+
{item?.title}
+ + {/*{item?.title}*/} + + + +
+ ) +} + +export default Detail diff --git a/src/expert/index.scss b/src/expert/index.scss index da2819e..1345fe3 100644 --- a/src/expert/index.scss +++ b/src/expert/index.scss @@ -1,18 +1,18 @@ .veteran-page { min-height: 100vh; background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%); - + .hero-section { position: relative; padding: 40px 20px 60px; background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%); overflow: hidden; - + .hero-content { position: relative; z-index: 2; text-align: center; - + .hero-title { font-size: 28px; font-weight: bold; @@ -20,7 +20,7 @@ margin-bottom: 20px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } - + .hero-subtitle { font-size: 14px; line-height: 1.6; @@ -30,7 +30,7 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } } - + .hero-decoration { position: absolute; top: 0; @@ -38,7 +38,7 @@ width: 100%; height: 100%; pointer-events: none; - + .decoration-circle { position: absolute; top: -50px; @@ -49,7 +49,7 @@ border-radius: 50%; animation: rotate 20s linear infinite; } - + .decoration-star { position: absolute; bottom: 20px; @@ -60,7 +60,7 @@ border-right: 15px solid transparent; border-bottom: 10px solid rgba(255, 255, 255, 0.1); transform: rotate(35deg); - + &::before { content: ''; position: absolute; @@ -76,10 +76,10 @@ } } } - + .veteran-list { padding: 20px 15px; - + .veteran-card { background: #fff; border-radius: 12px; @@ -91,16 +91,16 @@ border: 2px solid #d32f2f; transition: all 0.3s ease; cursor: pointer; - + &:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); } - + .veteran-avatar { flex-shrink: 0; margin-right: 15px; - + .avatar-img { width: 60px; height: 60px; @@ -110,17 +110,17 @@ background: #f5f5f5; } } - + .veteran-info { flex: 1; - + .veteran-name { font-size: 18px; font-weight: bold; color: #d32f2f; margin: 0 0 8px 0; } - + .veteran-description { font-size: 13px; line-height: 1.5; @@ -146,36 +146,36 @@ .veteran-page { .hero-section { padding: 30px 15px 50px; - + .hero-content { .hero-title { font-size: 24px; } - + .hero-subtitle { font-size: 13px; } } } - + .veteran-list { padding: 15px 10px; - + .veteran-card { padding: 15px; - + .veteran-avatar { .avatar-img { width: 50px; height: 50px; } } - + .veteran-info { .veteran-name { font-size: 16px; } - + .veteran-description { font-size: 12px; } diff --git a/src/expert/index.tsx b/src/expert/index.tsx index 17a051f..306fdb6 100644 --- a/src/expert/index.tsx +++ b/src/expert/index.tsx @@ -4,7 +4,7 @@ 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 {getCmsNavigation, pageCmsNavigation} from "@/api/cms/cmsNavigation"; +import {getCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; /** @@ -14,7 +14,6 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; const Index = () => { const {params} = useRouter(); const [navigation, setNavigation] = useState() - const [childCategory, setChildCategory] = useState([]) const [list, setList] = useState([]) const reload = async () => { @@ -22,8 +21,6 @@ const Index = () => { const categoryId = Number(params.id); // 当前栏目信息 const navs = await getCmsNavigation(categoryId); - // 二级栏目 - const childCateogry = await pageCmsNavigation({parentId: categoryId}); // 终极新闻列表 const articles = await pageCmsArticle({categoryId}); @@ -31,10 +28,6 @@ const Index = () => { if (navs) { setNavigation(navs); } - // 获取子级栏目 - if (childCateogry) { - setChildCategory(childCateogry.list) - } // 新闻列表 if (articles) { setList(articles?.list || []) @@ -46,56 +39,51 @@ const Index = () => { }, []) return ( - <> +
-
-
- { - // 子级栏目 - childCategory.map((item, index) => { - return ( -
Taro.navigateTo({url: `./index?id=${item.navigationId}`})} - > - {/* 图片容器 */} -
- {item.title -
-
- ) - }) - } -
-
+
+
{ // 终极文章列表 list.map((item, index) => { return (
Taro.navigateTo({url: `./detail?id=${item.articleId}`})} > {/* 图片容器 */} -
+
{item.title
{/* 标题 */} -
- {item.title} +
+

{item.title}

+

+ {item.comments || '暂无'} +

) @@ -103,7 +91,7 @@ const Index = () => { }
- +
) } export default Index diff --git a/src/honor/detail.scss b/src/honor/detail.scss new file mode 100644 index 0000000..ff6adb4 --- /dev/null +++ b/src/honor/detail.scss @@ -0,0 +1,10 @@ +.content{ + img{ + margin: 12px; + background-color: #F2FE03; + padding: 6px; + border-radius: 16px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); + margin-bottom: 40px !important; + } +} diff --git a/src/honor/detail.tsx b/src/honor/detail.tsx index 42238cb..187bc4d 100644 --- a/src/honor/detail.tsx +++ b/src/honor/detail.tsx @@ -1,26 +1,21 @@ import {useEffect, useState} from 'react' -import {Tag} from '@nutui/nutui-react-taro' import {useRouter} from '@tarojs/taro' -import {Divider} from '@nutui/nutui-react-taro' import {CmsArticle} from "@/api/cms/cmsArticle/model" -import {Eye} from '@nutui/icons-react-taro' // 显示html富文本 import {View, RichText} from '@tarojs/components' -import Line from "@/components/Gap"; +// import Line from "@/components/Gap"; import {getCmsArticle} from "@/api/cms/cmsArticle"; +import './detail.scss'; function Detail() { const {params} = useRouter(); // 文章详情 const [item, setItem] = useState() - // 浏览量 - const [views, setViews] = useState() const reload = () => { getCmsArticle(Number(params.id)).then(data => { if (data) { setItem(data) - setViews(data.actualViews) } }) } @@ -30,20 +25,38 @@ function Detail() { }, []); return ( -
-
{item?.title}
-
- {item?.categoryName} -
{views}
+
+
+
+ {/* 标题 */} +
+ {item?.title} +
+
- - + -
) } diff --git a/src/honor/index.tsx b/src/honor/index.tsx index eae41a5..e662183 100644 --- a/src/honor/index.tsx +++ b/src/honor/index.tsx @@ -4,7 +4,7 @@ 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 {getCmsNavigation} from "@/api/cms/cmsNavigation"; +import {getCmsNavigation, pageCmsNavigation} from "@/api/cms/cmsNavigation"; import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; /** @@ -14,6 +14,7 @@ import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; const Index = () => { const {params} = useRouter(); const [navigation, setNavigation] = useState() + const [childCategory, setChildCategory] = useState([]) const [list, setList] = useState([]) const reload = async () => { @@ -21,6 +22,8 @@ const Index = () => { const categoryId = Number(params.id); // 当前栏目信息 const navs = await getCmsNavigation(categoryId); + // 二级栏目 + const childCateogry = await pageCmsNavigation({parentId: categoryId}); // 终极新闻列表 const articles = await pageCmsArticle({categoryId}); @@ -28,6 +31,10 @@ const Index = () => { if (navs) { setNavigation(navs); } + // 获取子级栏目 + if (childCateogry) { + setChildCategory(childCateogry.list) + } // 新闻列表 if (articles) { setList(articles?.list || []) @@ -39,12 +46,46 @@ const Index = () => { }, []) return ( - <> +
-
+
+ { + // 子级栏目 + childCategory && childCategory.map((item, index) => { + return ( +
Taro.navigateTo({url: `./list?id=${item.navigationId}`})} + > + {/* 标题 */} +
+ {item.title} +
+
+ ) + }) + } { // 终极文章列表 list.map((item, index) => { @@ -80,7 +121,8 @@ const Index = () => { }) }
- + +
) } export default Index diff --git a/src/honor/list.config.ts b/src/honor/list.config.ts new file mode 100644 index 0000000..a0adf57 --- /dev/null +++ b/src/honor/list.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '光荣榜', + navigationBarBackgroundColor: '#d32f2f', + navigationBarTextStyle: 'white' +}) diff --git a/src/honor/list.scss b/src/honor/list.scss new file mode 100644 index 0000000..1345fe3 --- /dev/null +++ b/src/honor/list.scss @@ -0,0 +1,186 @@ +.veteran-page { + min-height: 100vh; + background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%); + + .hero-section { + position: relative; + padding: 40px 20px 60px; + background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%); + overflow: hidden; + + .hero-content { + position: relative; + z-index: 2; + text-align: center; + + .hero-title { + font-size: 28px; + font-weight: bold; + color: #fff; + margin-bottom: 20px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + + .hero-subtitle { + font-size: 14px; + line-height: 1.6; + color: rgba(255, 255, 255, 0.9); + margin: 0 auto; + max-width: 320px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + } + } + + .hero-decoration { + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + pointer-events: none; + + .decoration-circle { + position: absolute; + top: -50px; + right: -50px; + width: 150px; + height: 150px; + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + animation: rotate 20s linear infinite; + } + + .decoration-star { + position: absolute; + bottom: 20px; + right: 30px; + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-bottom: 10px solid rgba(255, 255, 255, 0.1); + transform: rotate(35deg); + + &::before { + content: ''; + position: absolute; + left: -15px; + top: 3px; + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-bottom: 10px solid rgba(255, 255, 255, 0.1); + transform: rotate(-70deg); + } + } + } + } + + .veteran-list { + padding: 20px 15px; + + .veteran-card { + background: #fff; + border-radius: 12px; + margin-bottom: 15px; + padding: 20px; + display: flex; + align-items: flex-start; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: 2px solid #d32f2f; + transition: all 0.3s ease; + cursor: pointer; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); + } + + .veteran-avatar { + flex-shrink: 0; + margin-right: 15px; + + .avatar-img { + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #d32f2f; + background: #f5f5f5; + } + } + + .veteran-info { + flex: 1; + + .veteran-name { + font-size: 18px; + font-weight: bold; + color: #d32f2f; + margin: 0 0 8px 0; + } + + .veteran-description { + font-size: 13px; + line-height: 1.5; + color: #666; + text-align: justify; + } + } + } + } +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* 响应式设计 */ +@media (max-width: 375px) { + .veteran-page { + .hero-section { + padding: 30px 15px 50px; + + .hero-content { + .hero-title { + font-size: 24px; + } + + .hero-subtitle { + font-size: 13px; + } + } + } + + .veteran-list { + padding: 15px 10px; + + .veteran-card { + padding: 15px; + + .veteran-avatar { + .avatar-img { + width: 50px; + height: 50px; + } + } + + .veteran-info { + .veteran-name { + font-size: 16px; + } + + .veteran-description { + font-size: 12px; + } + } + } + } + } +} diff --git a/src/honor/list.tsx b/src/honor/list.tsx new file mode 100644 index 0000000..b7dfbc7 --- /dev/null +++ b/src/honor/list.tsx @@ -0,0 +1,123 @@ +import {useEffect, useState} from "react"; +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 {getCmsNavigation} from "@/api/cms/cmsNavigation"; +import {CmsNavigation} from "@/api/cms/cmsNavigation/model"; + +/** + * 文章终极列表 + * @constructor + */ +const List = () => { + const {params} = useRouter(); + const [navigation, setNavigation] = useState() + const [list, setList] = useState([]) + + const reload = async () => { + // 获取栏目ID + const categoryId = Number(params.id); + // 当前栏目信息 + const navs = await getCmsNavigation(categoryId); + // 终极新闻列表 + const articles = await pageCmsArticle({categoryId}); + + // 当前栏目信息 + if (navs) { + setNavigation(navs); + } + // 新闻列表 + if (articles) { + setList(articles?.list || []) + } + } + + useEffect(() => { + reload() + }, []) + + return ( +
+
+ +
+
+
+ {/* 标题 */} +
+ {navigation?.categoryName} +
+
+
+
+
+ { + // 终极文章列表 + list.map((item, index) => { + return ( +
Taro.navigateTo({url: `./detail?id=${item.articleId}`})} + > + { + // 图片容器 + item.image && ( +
+ {item.title +
+ ) + } + {/* 标题 */} +
+

{item.title}

+

+ {item.comments || '暂无'} +

+
+
+ ) + }) + } +
+
+
+ ) +} +export default List diff --git a/src/pages/ai/index.config.ts b/src/pages/ai/index.config.ts new file mode 100644 index 0000000..5a4c284 --- /dev/null +++ b/src/pages/ai/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: 'AI问答' +}) diff --git a/src/pages/ai/index.scss b/src/pages/ai/index.scss new file mode 100644 index 0000000..c9028af --- /dev/null +++ b/src/pages/ai/index.scss @@ -0,0 +1,289 @@ +.ai-chat { + height: 94vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; + + .chat-header { + background: linear-gradient(135deg, #a6ea66 0%, #ead1ff 100%); + color: white; + padding: 16px; + text-align: center; + font-size: 18px; + font-weight: bold; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .chat-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .messages-container { + flex: 1; + padding: 16px; + overflow-y: auto; + scroll-behavior: smooth; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 2px; + } + } + + .message { + margin-bottom: 16px; + display: flex; + align-items: flex-start; + animation: fadeInUp 0.3s ease-out; + + &.user { + flex-direction: row-reverse; + + .message-content { + background: linear-gradient(135deg, #ff3535 0%, #FF0000 100%); + color: white; + margin-left: 0; + margin-right: 12px; + } + } + + &.ai { + flex-direction: row; + + .message-content { + background: white; + color: #333; + margin-left: 12px; + margin-right: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + } + + .avatar { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; + + &.user-avatar { + background: linear-gradient(135deg, #df2626 0%, #d10a0a 100%); + color: white; + } + + &.ai-avatar { + background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); + color: white; + } + } + + .message-content { + max-width: 70%; + padding: 12px 16px; + border-radius: 18px; + word-wrap: break-word; + line-height: 1.5; + font-size: 14px; + position: relative; + + .typing-indicator { + display: flex; + align-items: center; + gap: 4px; + + .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #999; + animation: typing 1.4s infinite ease-in-out; + + &:nth-child(1) { animation-delay: -0.32s; } + &:nth-child(2) { animation-delay: -0.16s; } + &:nth-child(3) { animation-delay: 0s; } + } + } + } + } + + .input-container { + background: white; + padding: 16px; + border-top: 1px solid #e0e0e0; + display: flex; + align-items: flex-end; + gap: 12px; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); + + .input-wrapper { + flex: 1; + position: relative; + } + + .message-input { + width: 100%; + min-height: 40px; + max-height: 120px; + padding: 10px 16px; + border: 1px solid #e0e0e0; + border-radius: 20px; + font-size: 14px; + line-height: 1.5; + resize: none; + outline: none; + background: #f9f9f9; + transition: all 0.3s ease; + + &:focus { + border-color: #ffc6c6; + background: white; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1); + } + + &::placeholder { + color: #999; + } + } + + .send-button { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #ff0000 0%, #af1403 100%); + border: none; + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + flex-shrink: 0; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + } + + &:active { + transform: scale(0.95); + } + + &:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + } + } + + .quick-questions { + padding: 16px; + background: white; + border-top: 1px solid #e0e0e0; + + .quick-title { + font-size: 14px; + color: #666; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; + } + + .questions-list { + display: flex; + flex-direction: column; + gap: 8px; + + .question-item { + padding: 12px 16px; + background: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 12px; + font-size: 14px; + color: #333; + cursor: pointer; + transition: all 0.3s ease; + text-align: left; + + &:hover { + background: #667eea; + color: white; + border-color: #667eea; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2); + } + + &:active { + transform: translateY(0); + } + } + } + } + + .empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + text-align: center; + + .empty-icon { + font-size: 64px; + margin-bottom: 16px; + opacity: 0.5; + } + + .empty-title { + font-size: 18px; + font-weight: bold; + color: #333; + margin-bottom: 8px; + } + + .empty-desc { + font-size: 14px; + color: #666; + line-height: 1.5; + } + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes typing { + 0%, 80%, 100% { + transform: scale(0); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} diff --git a/src/pages/ai/index.tsx b/src/pages/ai/index.tsx new file mode 100644 index 0000000..c4c6657 --- /dev/null +++ b/src/pages/ai/index.tsx @@ -0,0 +1,269 @@ +import { useEffect, useState, useRef } from "react"; +import { View, Textarea } from '@tarojs/components'; +// import Toast from '@tarojs/taro'; +import Taro from '@tarojs/taro'; +import { Button } from '@nutui/nutui-react-taro'; +import { User, ArrowUp } from '@nutui/icons-react-taro'; +import { sendAiMessage, stopAiMessage } from '@/api/ai'; +import { createWebSocket } from '@/utils/websocket'; +import './index.scss'; +import {APP_API_URL} from "@/utils/server"; + +// 消息类型 +interface Message { + id?: string; + type?: 'user' | 'ai'; + query?: string; + timestamp?: number; + isTyping?: boolean; + user?: string; + responseMode?: string; +} + +/** + * AI问答页面 + */ +const AiChat = () => { + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [currentTaskId, setCurrentTaskId] = useState(''); + const messagesEndRef = useRef(null); + const wsRef = useRef(null); + + // 快捷问题 + const quickQuestions = [ + "当兵的注意事项?", + "男兵应征报名对象:", + "当兵公务员考试?", + "请问您还有什么问题吗?" + ]; + + // 滚动到底部 + const scrollToBottom = () => { + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 100); + }; + + // 初始化WebSocket连接 + const initWebSocket = () => { + const wsUrl = APP_API_URL.replace('https', 'ws') + '/chat/' + Taro.getStorageSync('UserId') || 'token'; + // const wsUrl = 'ws://127.0.0.1:9000/chat/test' + wsRef.current = createWebSocket(wsUrl); + + wsRef.current.onMessage((data: any) => { + console.log('收到WebSocket消息:', data); + + if (data.answer) { + if (data.answer === '__END__') { + // 消息结束,移除typing状态 + setMessages(prev => prev.map(msg => + msg.isTyping ? { ...msg, isTyping: false } : msg + )); + setIsLoading(false); + setCurrentTaskId(''); + } else { + // 更新AI回复消息 + setMessages(prev => { + const lastMessage = prev[prev.length - 1]; + if (lastMessage && lastMessage.type === 'ai' && lastMessage.isTyping) { + // 更新最后一条AI消息 + return prev.map((msg, index) => + index === prev.length - 1 + ? { ...msg, query: msg.query + data.answer } + : msg + ); + } else { + // 创建新的AI消息 + return [...prev, { + id: Date.now().toString(), + type: 'ai', + query: data.answer, + timestamp: Date.now(), + isTyping: true + }]; + } + }); + + if (data.taskId) { + setCurrentTaskId(data.taskId); + } + } + scrollToBottom(); + } + }); + + wsRef.current.onOpen(() => { + console.log('WebSocket连接成功'); + }); + + wsRef.current.onError((error: any) => { + console.error('WebSocket连接错误:', error); + // Toast.showToast({ title: '连接失败,请检查网络', icon: 'error'}); + }); + + wsRef.current.connect().catch((error: any) => { + console.error('WebSocket连接失败:', error); + }); + }; + + useEffect(() => { + initWebSocket(); + Taro.hideTabBar(); + return () => { + if (wsRef.current) { + wsRef.current.close(); + } + }; + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // 发送消息 + const handleSendMessage = async (content: string) => { + if (!content.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + type: 'user', + query: content.trim(), + timestamp: Date.now(), + user: `${Taro.getStorageSync('UserId')}` + }; + setMessages(prev => [...prev, userMessage]); + setInputValue(''); + setIsLoading(true); + + try { + await sendAiMessage({ + query: content.trim(), + user: `${Taro.getStorageSync('UserId')}`, + responseMode: 'streaming', + }); + } catch (error) { + console.error('发送消息失败:', error); + // Toast.showToast({ title: '发送失败,请重试', icon: 'error'}); + setIsLoading(false); + } + }; + + // 停止AI回复 + const handleStopMessage = async () => { + if (currentTaskId) { + try { + await stopAiMessage({ taskId: currentTaskId }); + setIsLoading(false); + setCurrentTaskId(''); + setMessages(prev => prev.map(msg => + msg.isTyping ? { ...msg, isTyping: false } : msg + )); + } catch (error) { + console.error('停止消息失败:', error); + } + } + }; + + // 处理快捷问题点击 + const handleQuickQuestion = (question: string) => { + console.log(question,'qqq') + handleSendMessage(question); + }; + + return ( + + {/**/} + {/* AI智能问答*/} + {/**/} + + + + {messages.length === 0 ? ( + + 🤖 + 您好!我是AI助手 + + 我可以为您解答各种问题,请输入您想了解的内容 + + + ) : ( + messages.map((message) => ( + + + {message.type === 'user' ? : '🤖'} + + + {message.isTyping ? ( + + + + + + ) : ( + message.query + )} + + + )) + )} + + + + {messages.length === 0 && ( + + + 💡 您可以问我: + + + {quickQuestions.map((question, index) => ( + handleQuickQuestion(question)} + > + {question} + + ))} + + + )} + + + +