add banbanmini
This commit is contained in:
3
banban-mini/src/pages/bind/index.config.ts
Normal file
3
banban-mini/src/pages/bind/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
195
banban-mini/src/pages/bind/index.scss
Normal file
195
banban-mini/src/pages/bind/index.scss
Normal file
@@ -0,0 +1,195 @@
|
||||
.bind-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 120px;
|
||||
padding-bottom: calc(120px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.icon-bg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&.orange {
|
||||
background: #FFF0E6;
|
||||
}
|
||||
|
||||
.control-icon-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.bind-header {
|
||||
padding: 80px 32px 32px;
|
||||
text-align: center;
|
||||
|
||||
.title {
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-area {
|
||||
margin: 0 48px 48px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scan-frame {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24px;
|
||||
padding: 60px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.scan-text {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: #FF8C42;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scan-hint {
|
||||
font-size: 26px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-corners {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-color: #FF8C42;
|
||||
border-style: solid;
|
||||
|
||||
&.top-left {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
border-width: 6px 0 0 6px;
|
||||
border-radius: 24px 0 0 0;
|
||||
}
|
||||
|
||||
&.top-right {
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
border-width: 6px 6px 0 0;
|
||||
border-radius: 0 24px 0 0;
|
||||
}
|
||||
|
||||
&.bottom-left {
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
border-width: 0 0 6px 6px;
|
||||
border-radius: 0 0 0 24px;
|
||||
}
|
||||
|
||||
&.bottom-right {
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
border-width: 0 6px 6px 0;
|
||||
border-radius: 0 0 24px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bind-options {
|
||||
margin: 0 48px 48px;
|
||||
|
||||
.option-item {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
padding: 28px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 36px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
font-size: 30px;
|
||||
color: #1A1A1A;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.option-arrow {
|
||||
font-size: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bind-tips {
|
||||
margin: 0 48px;
|
||||
|
||||
.tips-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.tip-number {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: #FF8C42;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
173
banban-mini/src/pages/bind/index.tsx
Normal file
173
banban-mini/src/pages/bind/index.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { View, Text, Button, Image } from '@tarojs/components'
|
||||
import { useState, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
export default function Bind() {
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// 检查是否已登录
|
||||
const isLoggedIn = Taro.getStorageSync('isLoggedIn')
|
||||
if (!isLoggedIn) {
|
||||
// 未登录,跳转到登录页面
|
||||
Taro.redirectTo({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已绑定设备
|
||||
const hasDevice = Taro.getStorageSync('hasDevice')
|
||||
if (hasDevice) {
|
||||
// 已绑定设备,跳转到首页
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleScanCode = () => {
|
||||
setIsScanning(true)
|
||||
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => {
|
||||
setIsScanning(false)
|
||||
console.log('扫码结果:', res.result)
|
||||
|
||||
// 模拟绑定设备
|
||||
Taro.showLoading({ title: '绑定中...' })
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.hideLoading()
|
||||
|
||||
// 保存设备信息
|
||||
Taro.setStorageSync('hasDevice', true)
|
||||
Taro.setStorageSync('deviceInfo', {
|
||||
id: res.result || 'DEVICE_' + Date.now(),
|
||||
name: '小夏的伴伴',
|
||||
status: 'online',
|
||||
battery: 82,
|
||||
bindTime: new Date().toLocaleString()
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '设备绑定成功',
|
||||
icon: 'success',
|
||||
duration: 2000,
|
||||
complete: () => {
|
||||
// 绑定成功,跳转到首页
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
}, 1500)
|
||||
},
|
||||
fail: (err) => {
|
||||
setIsScanning(false)
|
||||
console.error('扫码失败:', err)
|
||||
|
||||
// 用户取消扫码,不显示错误
|
||||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||||
return
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '扫码失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleManualInput = () => {
|
||||
Taro.showModal({
|
||||
title: '手动输入设备码',
|
||||
editable: true,
|
||||
placeholderText: '请输入设备底部的设备码',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
// 模拟绑定设备
|
||||
Taro.showLoading({ title: '绑定中...' })
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.hideLoading()
|
||||
|
||||
Taro.setStorageSync('hasDevice', true)
|
||||
Taro.setStorageSync('deviceInfo', {
|
||||
id: res.content,
|
||||
name: '小夏的伴伴',
|
||||
status: 'online',
|
||||
battery: 82,
|
||||
bindTime: new Date().toLocaleString()
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '设备绑定成功',
|
||||
icon: 'success',
|
||||
duration: 2000,
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='bind-page'>
|
||||
<View className='bind-header'>
|
||||
<Text className='title'>绑定设备</Text>
|
||||
<Text className='subtitle'>请扫描设备底部的二维码进行绑定</Text>
|
||||
</View>
|
||||
|
||||
<View className='scan-area'>
|
||||
<View className='scan-frame' onClick={handleScanCode}>
|
||||
<View className='icon-bg orange'>
|
||||
<Image
|
||||
className='control-icon-img'
|
||||
src={require('../../assets/tab-icons/rings.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
<Text className='scan-text'>{isScanning ? '正在扫码...' : '点击扫码'}</Text>
|
||||
<Text className='scan-hint'>请将二维码放入框内</Text>
|
||||
</View>
|
||||
|
||||
<View className='scan-corners'>
|
||||
<View className='corner top-left'></View>
|
||||
<View className='corner top-right'></View>
|
||||
<View className='corner bottom-left'></View>
|
||||
<View className='corner bottom-right'></View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='bind-options'>
|
||||
<View className='option-item' onClick={handleManualInput}>
|
||||
<Text className='option-icon'>⌨️</Text>
|
||||
<Text className='option-text'>手动输入设备码</Text>
|
||||
<Text className='option-arrow'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='bind-tips'>
|
||||
<Text className='tips-title'>绑定帮助</Text>
|
||||
<View className='tip-item'>
|
||||
<Text className='tip-number'>1</Text>
|
||||
<Text className='tip-text'>找到设备底部的二维码或设备码</Text>
|
||||
</View>
|
||||
<View className='tip-item'>
|
||||
<Text className='tip-number'>2</Text>
|
||||
<Text className='tip-text'>点击上方扫码区域进行扫描</Text>
|
||||
</View>
|
||||
<View className='tip-item'>
|
||||
<Text className='tip-number'>3</Text>
|
||||
<Text className='tip-text'>等待绑定成功提示</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
3
banban-mini/src/pages/chat/detail/index.config.ts
Normal file
3
banban-mini/src/pages/chat/detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
206
banban-mini/src/pages/chat/detail/index.scss
Normal file
206
banban-mini/src/pages/chat/detail/index.scss
Normal file
@@ -0,0 +1,206 @@
|
||||
.chat-detail-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: #FFFFFF;
|
||||
padding: 80px 24px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #EEEEEE;
|
||||
|
||||
.back-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.back-arrow-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.character-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.character-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.character-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.character-name {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.character-desc {
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.time-divider {
|
||||
text-align: center;
|
||||
margin: 24px 0;
|
||||
|
||||
text {
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
background: #E5E5E5;
|
||||
padding: 8px 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
|
||||
&.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&.ai {
|
||||
.ai-avatar {
|
||||
margin-right: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
background: #FFE4C4;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-img {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
background: #FEF3E8;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.robot-emoji {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 68%;
|
||||
padding: 18px 22px;
|
||||
font-size: 30px;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.user & {
|
||||
background: #FF8C42;
|
||||
color: #FFFFFF;
|
||||
border-radius: 20px 20px 6px 20px;
|
||||
margin-left: 14px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.ai & {
|
||||
background: #FFFFFF;
|
||||
color: #1A1A1A;
|
||||
border-radius: 20px 20px 20px 6px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.audio-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: inherit;
|
||||
margin-right: 14px;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.2s;
|
||||
|
||||
&.playing {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
&.orange {
|
||||
background: rgba(255, 140, 66, 0.2);
|
||||
&.playing {
|
||||
background: rgba(255, 140, 66, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.audio-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: inherit;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.audio-duration {
|
||||
font-size: 24px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
banban-mini/src/pages/chat/detail/index.tsx
Normal file
156
banban-mini/src/pages/chat/detail/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { getMessages, getCharacter, ChatMessage, Character } from '../../../services/chat'
|
||||
import './index.scss'
|
||||
|
||||
export default function ChatDetail() {
|
||||
const router = useRouter()
|
||||
const { id } = router.params
|
||||
const conversationId = parseInt(id || '1')
|
||||
|
||||
const [character, setCharacter] = useState<Character>({
|
||||
id: conversationId,
|
||||
name: decodeURIComponent(router.params.name || 'AI 玩伴'),
|
||||
description: '温柔音色 / 开朗耐心',
|
||||
icon: '🎤'
|
||||
})
|
||||
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [playingId, setPlayingId] = useState<number | null>(null)
|
||||
const audioCtx = useRef<Taro.InnerAudioContext | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadChatData()
|
||||
return () => {
|
||||
// 页面卸载时销毁音频
|
||||
audioCtx.current?.destroy()
|
||||
}
|
||||
}, [conversationId])
|
||||
|
||||
const loadChatData = async () => {
|
||||
const char = await getCharacter(conversationId)
|
||||
if (char) {
|
||||
setCharacter(char)
|
||||
Taro.setNavigationBarTitle({ title: char.name })
|
||||
}
|
||||
const msgs = await getMessages(conversationId)
|
||||
setMessages(msgs)
|
||||
}
|
||||
|
||||
const handlePlayAudio = (msg: ChatMessage) => {
|
||||
if (!msg.audioUrl) return
|
||||
|
||||
// 如果点的是正在播放的那条,就停止
|
||||
if (playingId === msg.id) {
|
||||
audioCtx.current?.stop()
|
||||
audioCtx.current?.destroy()
|
||||
audioCtx.current = null
|
||||
setPlayingId(null)
|
||||
return
|
||||
}
|
||||
|
||||
// 停掉上一条
|
||||
audioCtx.current?.stop()
|
||||
audioCtx.current?.destroy()
|
||||
|
||||
const ctx = Taro.createInnerAudioContext()
|
||||
ctx.src = msg.audioUrl
|
||||
ctx.autoplay = true
|
||||
ctx.onPlay(() => setPlayingId(msg.id))
|
||||
ctx.onEnded(() => {
|
||||
setPlayingId(null)
|
||||
ctx.destroy()
|
||||
audioCtx.current = null
|
||||
})
|
||||
ctx.onError(() => {
|
||||
setPlayingId(null)
|
||||
ctx.destroy()
|
||||
audioCtx.current = null
|
||||
Taro.showToast({ title: '播放失败', icon: 'none' })
|
||||
})
|
||||
audioCtx.current = ctx
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
audioCtx.current?.stop()
|
||||
audioCtx.current?.destroy()
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='chat-detail-page'>
|
||||
<View className='chat-header'>
|
||||
<View className='back-btn' onClick={handleBack}>
|
||||
<Image src={require('../../../assets/tab-icons/arrow-left.png')} className='back-arrow-img' />
|
||||
</View>
|
||||
<View className='character-info'>
|
||||
<View className='character-avatar'>
|
||||
<Text>{character.icon}</Text>
|
||||
</View>
|
||||
<View className='character-text'>
|
||||
<Text className='character-name'>{character.name}</Text>
|
||||
<Text className='character-desc'>{character.description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView className='chat-list' scrollY>
|
||||
{messages.map((msg, index) => (
|
||||
<View key={msg.id}>
|
||||
{(index === 0 || messages[index - 1].type !== msg.type ||
|
||||
messages[index - 1].time !== msg.time) && (
|
||||
<View className='time-divider'>
|
||||
<Text>{msg.time}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{msg.type === 'user' ? (
|
||||
<View className='message-item user'>
|
||||
<View className='message-bubble'>
|
||||
{msg.isAudio ? (
|
||||
<View className='audio-message' onClick={() => handlePlayAudio(msg)}>
|
||||
<View className={`audio-icon ${playingId === msg.id ? 'playing' : ''}`}>
|
||||
{playingId === msg.id ? '⏸' : '▶'}
|
||||
</View>
|
||||
<View className='audio-info'>
|
||||
<Text className='audio-title'>录音文件</Text>
|
||||
<Text className='audio-duration'>{msg.audioDuration}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text>{msg.content}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='user-avatar'>
|
||||
<Text className='avatar-img'>👦</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='message-item ai'>
|
||||
<View className='ai-avatar'>
|
||||
<Text className='robot-emoji'>{character.icon}</Text>
|
||||
</View>
|
||||
<View className='message-bubble'>
|
||||
{msg.isAudio ? (
|
||||
<View className='audio-message' onClick={() => handlePlayAudio(msg)}>
|
||||
<View className={`audio-icon orange ${playingId === msg.id ? 'playing' : ''}`}>
|
||||
{playingId === msg.id ? '⏸' : '▶'}
|
||||
</View>
|
||||
<View className='audio-info'>
|
||||
<Text className='audio-title'>录音文件</Text>
|
||||
<Text className='audio-duration'>{msg.audioDuration}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text>{msg.content}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
4
banban-mini/src/pages/chat/index.config.ts
Normal file
4
banban-mini/src/pages/chat/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '聊天记录',
|
||||
navigationStyle: 'custom'
|
||||
}
|
||||
125
banban-mini/src/pages/chat/index.scss
Normal file
125
banban-mini/src/pages/chat/index.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
.chat-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 120px;
|
||||
padding-bottom: calc(120px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 80px 32px 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
height: calc(100vh - 200px);
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 16px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.1s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.conversation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.conversation-name {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.conversation-time {
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.last-message {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
background: #FF4D4F;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.unread-count {
|
||||
font-size: 22px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
63
banban-mini/src/pages/chat/index.tsx
Normal file
63
banban-mini/src/pages/chat/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { useState, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getConversations, ChatConversation } from '../../services/chat'
|
||||
import './index.scss'
|
||||
|
||||
export default function Chat() {
|
||||
const [conversations, setConversations] = useState<ChatConversation[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
loadConversations()
|
||||
}, [])
|
||||
|
||||
const loadConversations = async () => {
|
||||
const data = await getConversations()
|
||||
setConversations(data)
|
||||
}
|
||||
|
||||
const handleConversationClick = (conversation: ChatConversation) => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/chat/detail/index?id=${conversation.id}&name=${encodeURIComponent(conversation.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='chat-page'>
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>AI 玩伴</Text>
|
||||
<Text className='page-subtitle'>选择玩伴开始对话</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView className='conversation-list' scrollY>
|
||||
{conversations.map((conversation) => (
|
||||
<View
|
||||
key={conversation.id}
|
||||
className='conversation-item'
|
||||
onClick={() => handleConversationClick(conversation)}
|
||||
>
|
||||
<View className='conversation-avatar'>
|
||||
<Text className='avatar-icon'>{conversation.avatar}</Text>
|
||||
</View>
|
||||
<View className='conversation-content'>
|
||||
<View className='conversation-header'>
|
||||
<Text className='conversation-name'>{conversation.name}</Text>
|
||||
<Text className='conversation-time'>{conversation.time}</Text>
|
||||
</View>
|
||||
<View className='conversation-footer'>
|
||||
<Text className='last-message' numberOfLines={1}>
|
||||
{conversation.lastMessage}
|
||||
</Text>
|
||||
{conversation.unreadCount && conversation.unreadCount > 0 && (
|
||||
<View className='unread-badge'>
|
||||
<Text className='unread-count'>{conversation.unreadCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
3
banban-mini/src/pages/device/index.config.ts
Normal file
3
banban-mini/src/pages/device/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
291
banban-mini/src/pages/device/index.scss
Normal file
291
banban-mini/src/pages/device/index.scss
Normal file
@@ -0,0 +1,291 @@
|
||||
.device-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 160px;
|
||||
padding-bottom: calc(160px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(160px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 80px 32px 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||
border-radius: 28px;
|
||||
padding: 40px 36px;
|
||||
margin: 0 24px 32px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4CD964;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-section {
|
||||
.battery-percent {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
line-height: 1;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
.percent {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-label {
|
||||
font-size: 26px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.device-avatar {
|
||||
position: absolute;
|
||||
right: 36px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.robot-icon {
|
||||
.robot-img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
margin: 0 24px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.control-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
|
||||
&.volume-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
|
||||
.volume-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.control-left {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.control-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.icon-bg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
|
||||
&.purple {
|
||||
background: #F0E6FF;
|
||||
}
|
||||
|
||||
&.orange {
|
||||
background: #FFF0E6;
|
||||
}
|
||||
|
||||
.control-icon-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.control-name {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.control-detail {
|
||||
font-size: 26px;
|
||||
color: #FF8C42;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
|
||||
.volume-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #F0F0F0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.bg-orange {
|
||||
background: #FEF3E8;
|
||||
}
|
||||
|
||||
&.bg-green {
|
||||
background: #E8F8F0;
|
||||
}
|
||||
|
||||
&.bg-blue {
|
||||
background: #E8F0FE;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-text {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 32px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
174
banban-mini/src/pages/device/index.tsx
Normal file
174
banban-mini/src/pages/device/index.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { View, Text, Switch, Slider, Image } from '@tarojs/components'
|
||||
import { useState, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
interface DeviceData {
|
||||
id: string
|
||||
name: string
|
||||
status: 'online' | 'offline'
|
||||
battery: number
|
||||
daysLeft: number
|
||||
sleepEnabled: boolean
|
||||
sleepTime: string
|
||||
volume: number
|
||||
}
|
||||
|
||||
export default function Device() {
|
||||
const [device, setDevice] = useState<DeviceData | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [hasDevice, setHasDevice] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// 检查登录状态
|
||||
const isLoggedIn = Taro.getStorageSync('isLoggedIn')
|
||||
if (!isLoggedIn) {
|
||||
Taro.redirectTo({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查设备绑定状态
|
||||
const deviceInfo = Taro.getStorageSync('deviceInfo')
|
||||
const hasDeviceFlag = Taro.getStorageSync('hasDevice')
|
||||
|
||||
setHasDevice(!!hasDeviceFlag)
|
||||
|
||||
if (deviceInfo && hasDeviceFlag) {
|
||||
setDevice({
|
||||
id: deviceInfo.id,
|
||||
name: deviceInfo.name || '小夏的伴伴',
|
||||
status: deviceInfo.status || 'online',
|
||||
battery: deviceInfo.battery || 82,
|
||||
daysLeft: 4,
|
||||
sleepEnabled: true,
|
||||
sleepTime: '每天 22:00 - 07:00',
|
||||
volume: 50
|
||||
})
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
const handleSleepToggle = (e: any) => {
|
||||
if (!device) return
|
||||
setDevice({ ...device, sleepEnabled: e.detail.value })
|
||||
Taro.showToast({
|
||||
title: e.detail.value ? '已开启定时休眠' : '已关闭定时休眠',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const handleVolumeChange = (e: any) => {
|
||||
if (!device) return
|
||||
setDevice({ ...device, volume: e.detail.value })
|
||||
}
|
||||
|
||||
const switchTab = (url: string) => {
|
||||
Taro.switchTab({ url })
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className='device-page'>
|
||||
<View className='loading'>
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 未绑定设备,跳转到绑定页面
|
||||
if (!hasDevice || !device) {
|
||||
Taro.redirectTo({ url: '/pages/bind/index' })
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='device-page'>
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>{device.name}</Text>
|
||||
<Text className='page-subtitle'>设备运行状态良好</Text>
|
||||
</View>
|
||||
|
||||
<View className='status-card'>
|
||||
<View className='status-badge'>
|
||||
<View className='status-dot'></View>
|
||||
<Text className='status-text'>4G 在线</Text>
|
||||
</View>
|
||||
<View className='battery-section'>
|
||||
<Text className='battery-percent'>{device.battery}<Text className='percent'>%</Text></Text>
|
||||
<Text className='battery-label'>剩余电量 (预计续航 {device.daysLeft} 天)</Text>
|
||||
</View>
|
||||
<View className='device-avatar'>
|
||||
<View className='robot-icon'>
|
||||
<Image className='robot-img' src={require('../../assets/tab-icons/robot.png')} mode='aspectFit' />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-title'>基础控制</View>
|
||||
<View className='control-card'>
|
||||
<View className='control-item'>
|
||||
<View className='control-left'>
|
||||
<View className='icon-bg purple'>
|
||||
<Image
|
||||
className='control-icon-img'
|
||||
src={require('../../assets/tab-icons/moon.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
<View className='control-info'>
|
||||
<Text className='control-name'>定时休眠</Text>
|
||||
<Text className='control-detail'>{device.sleepTime}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
checked={device.sleepEnabled}
|
||||
onChange={handleSleepToggle}
|
||||
color='#FF8C42'
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='divider'></View>
|
||||
|
||||
<View className='control-item volume-item'>
|
||||
<View className='volume-header'>
|
||||
<View className='control-left'>
|
||||
<View className='icon-bg orange'>
|
||||
<Image
|
||||
className='control-icon-img'
|
||||
src={require('../../assets/tab-icons/orange-speaker.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
<Text className='control-name'>外放音量</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='volume-slider'>
|
||||
<Image
|
||||
className='volume-icon'
|
||||
src={require('../../assets/tab-icons/unspeaker.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
<Slider
|
||||
className='slider'
|
||||
min={0}
|
||||
max={100}
|
||||
value={device.volume}
|
||||
onChange={handleVolumeChange}
|
||||
activeColor='#FF8C42'
|
||||
backgroundColor='#E5E5E5'
|
||||
blockColor='#FF8C42'
|
||||
/>
|
||||
<Image
|
||||
className='volume-icon'
|
||||
src={require('../../assets/tab-icons/orange-speaker.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
3
banban-mini/src/pages/location/index.config.ts
Normal file
3
banban-mini/src/pages/location/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
150
banban-mini/src/pages/location/index.scss
Normal file
150
banban-mini/src/pages/location/index.scss
Normal file
@@ -0,0 +1,150 @@
|
||||
.location-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 120px;
|
||||
padding-bottom: calc(120px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 80px 32px 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.map-section {
|
||||
height: 50vh;
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.location-marker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.marker-pin {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background: #FF8C42;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 6px 20px rgba(255, 140, 66, 0.4);
|
||||
|
||||
.marker-emoji {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.location-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
margin: 0 24px 24px;
|
||||
padding: 28px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
|
||||
.location-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.location-update {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #E8F8F0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 28px;
|
||||
color: #4CD964;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.location-detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #F8F9FA;
|
||||
border-radius: 16px;
|
||||
padding: 22px;
|
||||
|
||||
.location-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #E8F0FE;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
margin-right: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
flex: 1;
|
||||
|
||||
.location-address {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.location-desc {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
banban-mini/src/pages/location/index.tsx
Normal file
128
banban-mini/src/pages/location/index.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { View, Text, Map } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
interface LocationInfo {
|
||||
address: string
|
||||
updateTime: string
|
||||
locationType: string
|
||||
}
|
||||
|
||||
interface Coordinates {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
|
||||
// 模拟接口:获取设备位置(返回随机经纬度)
|
||||
const fetchDeviceLocation = async (): Promise<Coordinates> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
// 在北京范围内生成随机坐标(大致范围)
|
||||
const baseLat = 39.9042
|
||||
const baseLng = 116.4074
|
||||
const randomOffset = () => (Math.random() - 0.5) * 0.1 // 约 ±5km 的偏移
|
||||
|
||||
return {
|
||||
latitude: baseLat + randomOffset(),
|
||||
longitude: baseLng + randomOffset()
|
||||
}
|
||||
}
|
||||
|
||||
export default function Location() {
|
||||
const [location, setLocation] = useState<LocationInfo>({
|
||||
address: '阳光社区 3期 附近',
|
||||
updateTime: '刚刚',
|
||||
locationType: '网络定位'
|
||||
})
|
||||
|
||||
const [coordinates, setCoordinates] = useState<Coordinates>({
|
||||
latitude: 39.9042,
|
||||
longitude: 116.4074
|
||||
})
|
||||
|
||||
// 每次页面显示时获取最新位置
|
||||
useDidShow(() => {
|
||||
loadLocation()
|
||||
})
|
||||
|
||||
const loadLocation = async () => {
|
||||
try {
|
||||
const coords = await fetchDeviceLocation()
|
||||
setCoordinates(coords)
|
||||
} catch (error) {
|
||||
console.error('获取位置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshLocation = async () => {
|
||||
Taro.showToast({ title: '正在刷新位置...', icon: 'loading' })
|
||||
|
||||
try {
|
||||
const coords = await fetchDeviceLocation()
|
||||
setCoordinates(coords)
|
||||
setLocation({
|
||||
...location,
|
||||
updateTime: '刚刚'
|
||||
})
|
||||
Taro.showToast({ title: '位置已更新', icon: 'success' })
|
||||
} catch (error) {
|
||||
Taro.showToast({ title: '位置更新失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='location-page'>
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>设备定位</Text>
|
||||
<Text className='page-subtitle'>基于运营商网络基站定位</Text>
|
||||
</View>
|
||||
|
||||
<View className='map-section'>
|
||||
<Map
|
||||
className='map'
|
||||
latitude={coordinates.latitude}
|
||||
longitude={coordinates.longitude}
|
||||
scale={15}
|
||||
markers={[{
|
||||
id: 1,
|
||||
latitude: coordinates.latitude,
|
||||
longitude: coordinates.longitude,
|
||||
title: '设备位置',
|
||||
width: 40,
|
||||
height: 40
|
||||
}]}
|
||||
showLocation
|
||||
/>
|
||||
<View className='location-marker'>
|
||||
<View className='marker-pin'>
|
||||
<Text className='marker-emoji'>🤖</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='location-card'>
|
||||
<View className='card-header'>
|
||||
<View className='header-left'>
|
||||
<Text className='location-title'>当前大致位置</Text>
|
||||
<Text className='location-update'>更新于 {location.updateTime} ({location.locationType})</Text>
|
||||
</View>
|
||||
<View className='refresh-btn' onClick={refreshLocation}>
|
||||
<Text className='refresh-icon'>↻</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='location-detail'>
|
||||
<View className='location-icon'>
|
||||
<Text>📍</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-address'>{location.address}</Text>
|
||||
<Text className='location-desc'>通过设备 4G 信号基站解析获得,误差范围约 50-200 米。</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
114
banban-mini/src/pages/login/index.scss
Normal file
114
banban-mini/src/pages/login/index.scss
Normal file
@@ -0,0 +1,114 @@
|
||||
.login-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding-top: 120px;
|
||||
padding-bottom: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
|
||||
border-radius: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 0 8px 32px rgba(255, 140, 66, 0.3);
|
||||
|
||||
.logo-icon {
|
||||
font-size: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 30px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.login-content {
|
||||
flex: 1;
|
||||
padding: 0 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 80px;
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.feature-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn-wrapper {
|
||||
margin-bottom: 32px;
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: #07C160;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 36px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 32px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agreement {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
94
banban-mini/src/pages/login/index.tsx
Normal file
94
banban-mini/src/pages/login/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
export default function Login() {
|
||||
useEffect(() => {
|
||||
// 检查是否已登录
|
||||
const isLoggedIn = Taro.getStorageSync('isLoggedIn')
|
||||
|
||||
if (isLoggedIn) {
|
||||
// 已登录,跳转到首页
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLogin = () => {
|
||||
Taro.showLoading({ title: '登录中...' })
|
||||
|
||||
// 调用微信登录
|
||||
Taro.login({
|
||||
success: (loginRes) => {
|
||||
console.log('微信登录成功:', loginRes)
|
||||
|
||||
// 模拟后端验证 - 实际项目中需要将code发送到服务器
|
||||
// 这里假设登录成功
|
||||
Taro.setStorageSync('isLoggedIn', true)
|
||||
Taro.setStorageSync('userInfo', {
|
||||
nickName: '微信家长',
|
||||
avatarUrl: '',
|
||||
isAdmin: true,
|
||||
openid: loginRes.code
|
||||
})
|
||||
|
||||
Taro.hideLoading()
|
||||
|
||||
// 登录成功后跳转到首页
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
},
|
||||
fail: (err) => {
|
||||
Taro.hideLoading()
|
||||
console.error('微信登录失败:', err)
|
||||
Taro.showToast({
|
||||
title: '登录失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='login-page'>
|
||||
<View className='login-header'>
|
||||
<View className='logo'>
|
||||
<Text className='logo-icon'>🤖</Text>
|
||||
</View>
|
||||
<Text className='title'>伴伴</Text>
|
||||
<Text className='subtitle'>让关爱随时随地</Text>
|
||||
</View>
|
||||
|
||||
<View className='login-content'>
|
||||
<View className='features'>
|
||||
<View className='feature-item'>
|
||||
<Text className='feature-icon'>💬</Text>
|
||||
<Text className='feature-text'>AI 智能对话</Text>
|
||||
</View>
|
||||
<View className='feature-item'>
|
||||
<Text className='feature-icon'>📍</Text>
|
||||
<Text className='feature-text'>实时定位追踪</Text>
|
||||
</View>
|
||||
<View className='feature-item'>
|
||||
<Text className='feature-icon'>🔋</Text>
|
||||
<Text className='feature-text'>设备状态监控</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='login-btn-wrapper'>
|
||||
<Button
|
||||
className='login-btn'
|
||||
openType='getUserInfo'
|
||||
onGetUserInfo={handleLogin}
|
||||
>
|
||||
<Text className='btn-icon'>🌐</Text>
|
||||
<Text className='btn-text'>微信一键登录</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Text className='agreement'>
|
||||
登录即代表您同意《用户协议》和《隐私政策》
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
3
banban-mini/src/pages/sleep/index.config.ts
Normal file
3
banban-mini/src/pages/sleep/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
162
banban-mini/src/pages/sleep/index.scss
Normal file
162
banban-mini/src/pages/sleep/index.scss
Normal file
@@ -0,0 +1,162 @@
|
||||
.manage-page {
|
||||
min-height: 100%;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 120px;
|
||||
padding-bottom: calc(120px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 80px 32px 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
margin: 0 24px 24px;
|
||||
padding: 28px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
|
||||
.user-avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: #FEF3E8;
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 22px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-img {
|
||||
font-size: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
|
||||
.user-name {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
background: #E8F8F0;
|
||||
border-radius: 14px;
|
||||
padding: 10px 16px;
|
||||
|
||||
text {
|
||||
font-size: 24px;
|
||||
color: #4CD964;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 26px 24px;
|
||||
|
||||
&:active {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-bg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 18px;
|
||||
|
||||
&.orange {
|
||||
background: #FFF0E6;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #E8F8F0;
|
||||
}
|
||||
|
||||
&.red {
|
||||
background: #FFE8E8;
|
||||
}
|
||||
|
||||
.control-icon-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
font-size: 30px;
|
||||
color: #1A1A1A;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.menu-value {
|
||||
font-size: 28px;
|
||||
color: #999999;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 30px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #F0F0F0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
text-align: center;
|
||||
padding: 48px 0;
|
||||
|
||||
.version-text {
|
||||
font-size: 26px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
142
banban-mini/src/pages/sleep/index.tsx
Normal file
142
banban-mini/src/pages/sleep/index.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
interface UserInfo {
|
||||
name: string
|
||||
role: string
|
||||
isVerified: boolean
|
||||
avatar: string
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
icon: string
|
||||
name: string
|
||||
value: string
|
||||
arrow?: boolean
|
||||
}
|
||||
|
||||
export default function Sleep() {
|
||||
const [user] = useState<UserInfo>({
|
||||
name: '微信家长',
|
||||
role: '管理员身份',
|
||||
isVerified: true,
|
||||
avatar: '👤'
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||||
iconBgClass: 'orange',
|
||||
name: '儿童资料 (用于称呼)',
|
||||
value: '小夏 (6岁)',
|
||||
arrow: true
|
||||
},
|
||||
{
|
||||
icon: require('../../assets/tab-icons/rings.png'),
|
||||
iconBgClass: 'green',
|
||||
name: '绑定新设备',
|
||||
value: '',
|
||||
arrow: true
|
||||
},
|
||||
{
|
||||
icon: require('../../assets/tab-icons/broken-rings.png'),
|
||||
iconBgClass: 'red',
|
||||
name: '解除设备绑定',
|
||||
value: '',
|
||||
arrow: true
|
||||
}
|
||||
]
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
if (item.name === '绑定新设备') {
|
||||
// 跳转到绑定设备页面
|
||||
Taro.navigateTo({ url: '/pages/bind/index' })
|
||||
} else if (item.name === '解除设备绑定') {
|
||||
Taro.showModal({
|
||||
title: '解除设备绑定',
|
||||
content: '确定要解除当前设备的绑定吗?解除后需要重新扫码绑定。',
|
||||
confirmColor: '#FF8C42',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 清除设备信息
|
||||
Taro.removeStorageSync('hasDevice')
|
||||
Taro.removeStorageSync('deviceInfo')
|
||||
|
||||
Taro.showToast({
|
||||
title: '已解除绑定',
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({ url: '/pages/bind/index' })
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: `点击了 ${item.name}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='manage-page'>
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>设置与管理</Text>
|
||||
</View>
|
||||
|
||||
<View className='user-card'>
|
||||
<View className='user-avatar'>
|
||||
<Text className='avatar-img'>{user.avatar}</Text>
|
||||
</View>
|
||||
<View className='user-info'>
|
||||
<Text className='user-name'>{user.name}</Text>
|
||||
<Text className='user-role'>{user.role}</Text>
|
||||
</View>
|
||||
{user.isVerified && (
|
||||
<View className='verified-badge'>
|
||||
<Text>已实名</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className='menu-card'>
|
||||
{menuItems.map((item, index) => (
|
||||
<View key={index}>
|
||||
<View className='menu-item' onClick={() => handleMenuClick(item)}>
|
||||
<View className='menu-left'>
|
||||
<View className={`icon-bg ${item.iconBgClass}`}>
|
||||
<Image
|
||||
className='control-icon-img'
|
||||
src={item.icon}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
<Text className='menu-name'>{item.name}</Text>
|
||||
</View>
|
||||
<View className='menu-right'>
|
||||
{item.value && (
|
||||
<Text className='menu-value'>{item.value}</Text>
|
||||
)}
|
||||
{item.arrow && <Text className='arrow'>›</Text>}
|
||||
</View>
|
||||
</View>
|
||||
{index < menuItems.length - 1 && (
|
||||
<View className='divider'></View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className='version-info'>
|
||||
<Text className='version-text'>伴伴 Companion V1.1.0</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user