在开始前的构思...
结合这段时间在学的 React 和 js ,我想着干脆自己搭个项目试试看。说是项目,其实更像个「技术实验场」------ 把自己学习的前端技术和AI揉到一起,看看能不能碰撞出点不一样的东西。目前它还只是个半成品,很多功能还在打磨,但核心的思路已经很清晰了。
项目亮点是集成 LLM 功能,支持聊天、文生图、语言处理等智能交互;还有完整的旅游应用功能模块,包括首页、搜索、行程、收藏、优惠等。
有了大致方向,我们可以在网上找类似功能的APP进行模仿和参考。在浏览了市面上各类旅游APP后我觉得同程旅行的微信小程序的界面就很符合我的预期:

在程序底部的导航栏有 首页 ,定制旅行 , 里程商城 , 订单 , 我的 这五大板块。我们可以按照它的样式来构思项目结构。
在下面的项目搭建中,我们需要支持5个主要模块:首页 、特惠专区 、我的收藏 、行程 和 个人中心 。每个模块对应不同的业务场景,而程序中的 AI问答功能 我们可以放在行程选项下。
技术栈规划
- 前端框架 我们选择 React,它的组件化特性特别适合构建交互式UI,像旅游客服的聊天界面、个人中心这些模块,拆分成组件后维护起来很方便。再搭配React Hooks管理状态,代码更简洁。
- 路由管理 我们就用 react-router-dom 处理页面跳转,通过标签配置的实现首页、旅游客服页、个人中心这些页面的切换。
- UI组件库 我们选择 react-vant ,这是专为移动端设计的组件库,响应式布局、主题定制都支持,而且体积小。项目里的底部导航栏(Tabbar)可以直接用的它的组件,能省不少样式开发时间。
- 样式处理 我们可以用CSS模块(.module.css)和原子类(如.flex、.flex-col)搭配。CSS模块能避免样式冲突,原子类提高了样式复用性。为了移动端适配还可以引入了 lib-flexible ,自动设置html的font-size,配合PostCSS的pxtorem插件把px转成rem 。
- AI服务 直接选择到官网申请 API Key 来调用大模型。
项目搭建
这是已经搭建好了的源代码根目录:

其中要搭建的模块有:
功能模块 🗂️
- LLM/ : 包含与 AI 聊天相关的功能
- hooks/ : 自定义 React Hooks
组件库 🧩
- BlankLayout/ : 空白布局组件
- MainLayout/ : 主布局组件
页面组件 📱
- Home/ : 首页
- Trip/ : 旅行相关页面(主要)
- Account/ : 账户页面 (主要)
- Collection/ : 收藏页面
- Discount/ : 优惠页面
- Login/ : 登录页面
- Search/ : 搜索页面
实战
全局设置
1. 路由配置
jsx
import './App.css'
import {
Suspense, // 用于懒加载组件的加载状态管理
lazy // 动态导入组件(懒加载)
} from 'react';
import {
Routes, // 路由配置组件
Route, // 单个路由规则
Navigate // 用于路由重定向
} from 'react-router-dom'
import MainLayout from '@/components/MainLayout' // 带底部导航的主布局
import BlankLayout from '@/components/BlankLayout' // 空白布局(无导航)
// 懒加载各页面组件(按需加载,提高首屏加载速度)
const Home = lazy(() => import('@/pages/Home'));
const Discount = lazy(() => import('@/pages/Discount'));
const Collection = lazy(() => import('@/pages/Collection'));
const Trip = lazy(() => import('@/pages/Trip'));
const Account = lazy(() => import('@/pages/Account'));
const Search = lazy(() => import('@/pages/Search'));
function App() {
return (
<>
{/* 包裹路由以处理懒加载时的加载状态 */}
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{/* 使用MainLayout的路由组(包含底部导航) */}
<Route element={<MainLayout />}>
{/* 根路径重定向到首页 */}
<Route path="/" element={<Navigate to="/home" />}/>
{/* 底部导航对应的五个主页面 */}
<Route path="/home" element={<Home/>}/>
<Route path="/discount" element={<Discount/>}/>
<Route path="/collection" element={<Collection/>}/>
<Route path="/trip" element={<Trip/>}/>
<Route path="/account" element={<Account/>}/>
</Route>
{/* 使用BlankLayout的路由组(无导航,独立页面) */}
<Route element={<BlankLayout />}>
<Route path="/search" element={<Search />}/>
</Route>
</Routes>
</Suspense>
</>
)
}
export default App
这里我们通过 React 的懒加载功能(lazy
和 Suspense
)按需加载页面组件以优化性能,使用 react-router-dom
定义路由规则,将首页、特惠专区等页面纳入带底部导航的 MainLayout
布局,将搜索页纳入无导航的 BlankLayout
布局,并设置根路径重定向到首页,实现了应用页面的路由管理和布局区分。
2. 统一基础样式
css
.flex {
display: flex; /* 使用Flexbox布局 */
}
.flex-col {
flex-direction: column; /* 设置为垂直方向的Flex布局 */
}
.flex-1 {
flex:1; /* 子元素占据剩余空间的比例 */
}
/* 高度相关样式 */
.h-screen {
height: 100vh; /* 元素高度为视口高度 */
}
.h-all {
height: 100%; /* 元素高度为父容器高度 */
}
/* 定位相关样式 */
.fixed-loading{
position: fixed; /* 固定定位 */
top: 50px; /* 距离顶部50px */
left: 50%; /* 水平居中 */
transform: translateX(-50%); /* 精确居中 */
z-index: 9999; /* 层级最高 */
}
/* 间距相关样式(使用移动端常用的4px基准) */
.mt2 {
margin-top: 8px; /* 顶部间距(2*4px) */
}
.mt3 {
margin-top: 12px; /* 顶部间距(3*4px) */
}
.ml4 {
margin-left: 16px; /* 左侧间距(4*4px) */
}
3. 实现移动端响应式布局
js
export default {
plugins: {
"postcss-pxtorem": {
rootValue: 75, // 以 iPhone6 为参考,1rem = 37.5px
propList: ['*'], // 所有属性都转换
exclude: /node_modules/i, // 排除 node_modules 中的文件
},
},
}
什么是 PostCSS 和 postcss-pxtorem?
- PostCSS :一个 CSS 处理工具,通过插件机制转换 CSS 代码(如自动添加前缀、单位转换等)。
- postcss-pxtorem :PostCSS 的插件之一,用于将 CSS 中的 px 单位自动转换为 rem 单位。
这段代码的目的是实现移动端响应式布局 ,通过 px 转 rem 的自动转换,结合 lib-flexible (在package.json 中引入)动态调整根元素字体大小,让页面在不同尺寸的移动设备上都能保持良好的显示效果,避免出现布局错乱或元素过大/过小的问题
底部导航栏功能与样式
底部导航栏在 MainLayout
组件中实现,我们使用 react-vant
的 Tabbar
组件和react-router-dom
进行路由导航。
jsx
import {
useState,
useEffect
} from 'react';
import {
Tabbar, // 底部导航栏组件
} from 'react-vant';
import {
HomeO, // 首页图标
Search, // 搜索图标
FriendsO, // 收藏图标
SettingO, // 设置图标
UserO // 用户图标
} from '@react-vant/icons';
import {
Outlet, // 渲染子路由组件
useNavigate, // 用于编程式导航
useLocation // 获取当前路由信息
} from 'react-router-dom'
// 底部导航栏配置
const tabs = [
{ icon: <HomeO />, title: '首页', path: '/home'},
{ icon: <Search />, title: '特惠专区', path: '/discount'},
{ icon: <FriendsO />, title: '我的收藏', path: '/collection'},
{ icon: <SettingO />, title: '行程', path: '/trip'},
{ icon: <UserO />, title: '我的账户', path: '/account'}
]
// 主布局组件,包含底部导航和路由出口
const MainLayout = () => {
const [active, setActive] = useState(0); // 当前激活的导航索引
const navigate = useNavigate(); // 导航钩子
const location = useLocation(); // 当前路由位置钩子
// 监听路由变化,更新导航激活状态
useEffect(() => {
console.log(location.pathname); // 打印当前路径
// 查找匹配当前路径的导航项索引
const index = tabs.findIndex(
tab => location.pathname.startsWith(tab.path)
);
console.log(index); // 打印匹配的索引
if (index !== -1) setActive(index);
}, [location.pathname]); // 依赖路径变化触发
return (
<div
className="flex flex-col h-screen" // 整页高度的垂直布局
style={{paddingBottom: '50px'}} // 为底部导航留出空间
>
<div className="flex-1"> // 内容区域占满剩余空间
<Outlet /> // 渲染子路由组件
</div>
{/* 底部导航栏 */}
<Tabbar value={active} onChange={
(key) => {
setActive(key); // 更新激活状态
navigate(tabs[key].path); // 导航到对应路径
}
}>
{tabs.map((tab, index) => (
<Tabbar.Item
key={index}
icon={tab.icon}
>
{tab.title}
</Tabbar.Item>
))}
</Tabbar>
</div>
)
}
export default MainLayout;
在这里我们通过 react-vant 的 Tabbar 组件实现底部导航栏,结合 react-router-dom 的相关功能实现路由管理。组件中定义了包含首页、特惠专区等五个导航项的配置数组,通过 useState 管理激活状态,useEffect 监听路由变化以自动匹配对应导航项,点击导航项时会更新激活状态并跳转至对应路径,同时使用 Outlet 渲染子路由内容,整体采用 flex 布局确保内容区域与底部导航的合理显示。
实际效果:
AI问答功能
首先我们在项目根目录下创建 .env.local
文件,再把从官网拿到的 Key 放进去:

env.local
VITE_DEEPSEEK_API_KEY=sk-dU...HdrBB(已省略)
然后我们在 LLM 文件夹下创建一个index.js
文件:
js
// Kimi AI的API接口地址
const KIM_CHAT_API_URL = 'https://api.moonshot.cn/v1/chat/completions';
export const chat = async (
messages,
api_url = DEEPSEEK_CHAT_API_URL,
api_key = import.meta.env.VITE_DEEPSEEK_API_KEY,
model = 'deepseek-chat'
) => {
try {
// 发送HTTP请求到AI API
const response = await fetch(api_url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
body: JSON.stringify({
model,
messages,
stream: false, // 不使用流式响应
})
});
// 解析响应JSON数据
const data = await response.json();
// 提取回复内容并格式化返回
return {
code: 0, // 状态码,0表示成功
data: {
role: 'assistant',
content: data.choices[0].message.content
}
};
} catch (err) {
// 错误处理
console.error('请求出错:', err);
return {
code: 1, // 原代码此处有误,应改为非零值表示错误
msg: '出错了...'
};
}
};
export const kimiChat = async (messages) => {
// 复用chat函数,指定Kimi AI的API地址和模型
const res = await chat(
messages,
KIM_CHAT_API_URL,
import.meta.env.VITE_KIMI_API_KEY,
'moonshot-v1-auto'
);
return res;
};
export const generateAvatar = async (text) => {
// 设计头像生成的提示词,引导AI生成奶龙风格的头像描述
const prompt = `
你是一位漫画设计师,需要为用户设计头像,主打日漫风格。
用户的信息是${text}
要求有个性,有设计感。
`;
return prompt;
};
- 基础设置:定义了 DeepSeek 和 Kimi 两个 AI 平台的 API 地址常量。
- 核心功能 :
chat
函数是核心接口,通过 fetch API 向 AI 平台发送 HTTP 请求,支持自定义 API 地址、密钥和模型。函数接收对话消息数组,返回 AI 的回复内容,并包含错误处理逻辑。 - 平台封装 :
kimiChat
函数是对chat
的二次封装,专门用于调用 Kimi AI 的接口,简化了参数传递。 - 扩展功能 :
generateAvatar
函数用于生成头像设计的提示词,引导 AI 生成特定风格(日漫风格)的头像描述。
在这里通过index.js
给组件提供了统一的调用接口,方便我们在应用中集成 AI 对话功能。
自定义 hook
js
import {
useEffect
} from 'react'
// 自定义钩子:用于设置页面标题
export function useTitle(title) {
// 当组件挂载时设置文档标题
useEffect(() => {
document.title = title // 修改页面标题为传入的title值
}, [])
}
export default useTitle;
写自定义 hook 是为了复用设置页面标题的逻辑,避免在多个组件中重复编写相同代码,提高代码可维护性。
模块化 css
account.module.css
css
.container {
background-color: #f5f5f5;
height: 100%;
}
.user {
background-color: #fff;
padding: 20px 16px;
display: flex;
align-items: center;
}
.nickname {
font-size: 36px;
font-weight: 500;
}
.level{
font-size: 28px;
color: #999;
margin-top: 8px;
}
.slogan{
font-size: 24px;
color: #ccc;
margin-top: 8px;
}
因为需要展示用户头像、昵称、等级等信息,样式更偏向于卡片式布局和信息展示(如 .nickname 设置了较大的字号和字重, .slogan 使用灰色调区分层级)。
trip.module.css
css
.chatArea {
overflow-y: auto;
padding: 12px;
background-color: #f7f8fa;
}
.inputArea {
padding: 16px;
border-top: 1px solid #ddd;
background: white;
}
.input {
margin-right: 8px;
}
.messageLeft, .messageRight {
max-width: 70%;
padding: 8px 12px;
margin: 12px 0;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
background-color: #fff;
}
.messageRight {
background-color: #4fc08d;
color: white;
margin-left: 30%;
}
在聊天界面需要实现消息列表、输入框等交互元素,样式更注重对话气泡布局和交互反馈( 在 .messageLeft 和 .messageRight 分别定义了左右气泡的背景色、边距,.inputArea 设置了输入区域的边框和内边距)。
部分页面展示
1. Account 页面(我的)
jsx
import useTitle from '@/hooks/useTitle'
import {
useState
} from 'react';
import {
Image,
Cell,
CellGroup,
ActionSheet,
Popup,
Loading
} from 'react-vant'
import {
ServiceO,
FriendsO,
StarO,
SettingO,
UserCircleO
} from '@react-vant/icons'
import {
generateAvatar
} from '@/llm'
import styles from './account.module.css';
const Account = () => {
const [userInfo, setUserInfo] = useState({
nickname: '哈基米',
level: '5级',
slogan: '胖宝宝好胖好可爱',
avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'
})
useTitle("我的")
const [showActionSheet, setShowActionSheet] = useState(false);
const handleAction = async (e) => {
console.log(e)
if (e.type === 1) {
// AI生成头像
const text =
`昵称: ${userInfo.nickname},
slogan: ${userInfo.slogan}`
const newAvatar = await generateAvatar(text)
}else if (e.type === 2) {
// 图片上传
}
}
const actions = [
{
name: 'AI生成头像',
color: '#123123',
type: 1
},
{
name: '上传头像',
color: '#ee0a24',
type: 2
}
]
return (
<div className={styles.container}>
<div className={styles.user}>
<Image
round
width= "64px"
height="64px"
src={userInfo.avatar}
style={{cursor: 'pointer'}}
onClick={() => setShowActionSheet(true)}
/>
<div className="ml4">
<div className={styles.nickname}>昵称:{userInfo.nickname}</div>
<div className={styles.level}>等级:{userInfo.level}</div>
<div className={styles.slogan}>签名:{userInfo.slogan}</div>
</div>
</div>
<div className="mt3">
<CellGroup inset>
<Cell title="服务" icon={<ServiceO />} isLink />
</CellGroup>
<CellGroup inset className="mt2">
<Cell title="收藏" icon={<StarO />} isLink />
<Cell title="朋友圈" icon={<FriendsO />} isLink />
</CellGroup>
<CellGroup inset className="mt2">
<Cell title="设置" icon={<SettingO />} isLink />
</CellGroup>
</div>
<ActionSheet
visible={showActionSheet}
actions={actions}
cancelText='取消'
onCancel={() => setShowActionSheet(false)}
onSelect={(e) => handleAction(e)}
>
</ActionSheet>
</div>
)
}
export default Account
当用户点击头像时会弹出动作面板,可选择 "AI 生成头像" 或 "上传头像",其中 AI 生成头像功能会调用相关函数,基于用户昵称和签名生成头像。页面使用 react-vant
组件库构建 UI,通过状态管理控制动作面板的显示与隐藏。
实际效果:

2. Trip 页面(旅游智能客服)
jsx
import { useState } from 'react';
import {
Button, Input, Loading, Toast // 导入react-vant组件
} from 'react-vant'
import { ChatO, UserO } from '@react-vant/icons'; // 导入图标组件
import useTitle from '@/hooks/useTitle' // 导入自定义标题钩子
import { chat } from '@/llm' // 导入聊天API函数
import styles from './trip.module.css'; // 导入模块样式
const Trip = () => {
useTitle('旅游智能客服') // 设置页面标题
// 管理输入框内容状态
const [text, setText] = useState("");
// 管理发送中状态
const [isSending, setIsSending] = useState(false);
// 管理消息列表状态(初始包含两条示例消息)
const [messages, setMessages] = useState([
{ id: 1, content: '你好', role: 'user' },
{ id: 1, content: 'hello, I am your assistant~~', role: 'assistant' }
]);
// 处理聊天发送逻辑
const handleChat = async () => {
if (text.trim() === "") { // 检查输入是否为空
Toast.info('内容不能为空');
return;
}
setIsSending(true); // 显示发送中状态
setText(""); // 清空输入框
// 添加用户消息到消息列表
setMessages((prev) => [...prev, {
id: prev.length + 1,
content: text,
role: 'user'
}]);
// 调用聊天API获取回复
const newMessage = await chat([{
role: 'user',
content: text
}]);
// 添加AI回复到消息列表
setMessages((prev) => [...prev, newMessage.data]);
setIsSending(false); // 隐藏发送中状态
}
return (
<div className="flex flex-col h-all">
{/* 聊天消息显示区域 */}
<div className={`flex-1 ${styles.chatArea}`}>
{messages.map((msg, index) => (
<div key={index} className={`flex ${msg.role === 'user' ? styles.messageRight : styles.messageLeft}`}>
{msg.role === 'assistant' ? <ChatO /> : <UserO />} // 根据角色显示不同图标
<div className={styles.messageContent}>{msg.content}</div>
</div>
))}
</div>
{/* 输入区域 */}
<div className={`flex ${styles.inputArea}`}>
<Input
value={text}
onChange={(e) => setText(e)}
placeholder="请输入消息"
className={`flex-1 ${styles.input}`}
/>
<Button disabled={isSending} type="primary" onClick={handleChat} >发送</Button>
</div>
{/* 发送中加载提示 */}
{isSending && (<div className="fixed-loading"><Loading type="ball" /></div>)}
</div>
)
}
export default Trip
当用户输入内容并点击发送时,组件通过text
状态获取输入数据,校验通过后立即将用户消息添加到messages
数组,此时UI会自动更新并显示新发送的消息;同时,isSending
状态切换为true
,触发加载动画并禁用发送按钮以避免重复提交,在调用chat
接口获取AI回复的过程中,所有交互状态均由isSending
控制,当接口返回结果后,新的助手消息被追加到messages
数组,UI再次自动更新展示回复内容,同时isSending
重置为false
,恢复发送按钮功能。
当用户在底部输入框中输入消息内容,点击右侧"发送"按钮提交;若输入框为空,弹出提示框"内容不能为空"

后续
目前这个项目才刚刚初步实现了一点点功能,仍然需要完善,篇幅有限,文章就先到这里结束了。