add banbanmini
@@ -0,0 +1,55 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
"pages/login/index",
|
||||
"pages/bind/index",
|
||||
"pages/device/index",
|
||||
"pages/chat/index",
|
||||
"pages/chat/detail/index",
|
||||
"pages/location/index",
|
||||
"pages/sleep/index",
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: "light",
|
||||
navigationBarBackgroundColor: "#F5F7FA",
|
||||
navigationBarTitleText: "伴伴",
|
||||
navigationBarTextStyle: "black",
|
||||
},
|
||||
tabBar: {
|
||||
color: "#999999",
|
||||
selectedColor: "#FF8C42",
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderStyle: "white",
|
||||
list: [
|
||||
{
|
||||
pagePath: "pages/device/index",
|
||||
text: "首页",
|
||||
iconPath: "assets/tab-icons/device-icon.png",
|
||||
selectedIconPath: "assets/tab-icons/device-selected.png",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/chat/index",
|
||||
text: "玩伴",
|
||||
iconPath: "assets/tab-icons/chat-icon.png",
|
||||
selectedIconPath: "assets/tab-icons/chat-selected.png",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/location/index",
|
||||
text: "定位",
|
||||
iconPath: "assets/tab-icons/location-icon.png",
|
||||
selectedIconPath: "assets/tab-icons/location-selected.png",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/sleep/index",
|
||||
text: "管理",
|
||||
iconPath: "assets/tab-icons/manage-icon.png",
|
||||
selectedIconPath: "assets/tab-icons/manage-selected.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
permission: {
|
||||
"scope.userLocation": {
|
||||
desc: "用于定位设备位置",
|
||||
},
|
||||
},
|
||||
requiredPrivateInfos: ["getLocation"],
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
page {
|
||||
background-color: #F5F7FA;
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 20px 40px;
|
||||
background-color: #4A90E2;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-size: 30px;
|
||||
|
||||
&.btn-outline {
|
||||
background-color: transparent;
|
||||
border: 2px solid #4A90E2;
|
||||
color: #4A90E2;
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
&.btn-success {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useLaunch } from '@tarojs/taro'
|
||||
import './app.scss'
|
||||
|
||||
function App(props) {
|
||||
useLaunch(() => {
|
||||
console.log('App launched.')
|
||||
})
|
||||
|
||||
return props.children
|
||||
}
|
||||
|
||||
export default App
|
||||
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 4C19.3137 4 22 6.23858 22 9C22 10.514 21.1969 11.8708 19.932 12.7212C20.0933 13.5042 20.6128 14.1611 20.6729 14.2341C20.8407 14.4379 20.8252 14.7346 20.6358 14.9198C20.4463 15.105 20.1472 15.1166 19.9431 14.9467C19.9238 14.9306 18.988 14.1365 18.0673 13.9113C17.4124 13.9701 16.7188 14 16 14C12.6863 14 10 11.7614 10 9C10 6.23858 12.6863 4 16 4Z" fill="#9CA3AF"/>
|
||||
<path d="M8.5 8C12.6421 8 16 10.9101 16 14.5C16 18.0899 12.6421 21 8.5 21C7.30691 21 6.17887 20.7584 5.17688 20.3274C4.12002 20.6543 2.87198 21.611 2.84665 21.6303C2.56948 21.8407 2.17647 21.7853 1.96865 21.5068C1.76082 21.2282 1.81734 20.832 2.09503 20.622C2.12261 20.6013 3.03362 19.8973 3.23594 18.6017C1.86877 17.5135 1 16.0829 1 14.5C1 10.9101 4.35786 8 8.5 8Z" fill="#9CA3AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 872 B |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 4C19.3137 4 22 6.23858 22 9C22 10.514 21.1969 11.8708 19.932 12.7212C20.0933 13.5042 20.6128 14.1611 20.6729 14.2341C20.8407 14.4379 20.8252 14.7346 20.6358 14.9198C20.4463 15.105 20.1472 15.1166 19.9431 14.9467C19.9238 14.9306 18.988 14.1365 18.0673 13.9113C17.4124 13.9701 16.7188 14 16 14C12.6863 14 10 11.7614 10 9C10 6.23858 12.6863 4 16 4Z" fill="#F97316"/>
|
||||
<path d="M8.5 8C12.6421 8 16 10.9101 16 14.5C16 18.0899 12.6421 21 8.5 21C7.30691 21 6.17887 20.7584 5.17688 20.3274C4.12002 20.6543 2.87198 21.611 2.84665 21.6303C2.56948 21.8407 2.17647 21.7853 1.96865 21.5068C1.76082 21.2282 1.81734 20.832 2.09503 20.622C2.12261 20.6013 3.03362 19.8973 3.23594 18.6017C1.86877 17.5135 1 16.0829 1 14.5C1 10.9101 4.35786 8 8.5 8Z" fill="#F97316"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 872 B |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#9CA3AF">
|
||||
<rect x="10.5" y="3" width="3" height="3" rx="1" />
|
||||
<rect x="2" y="9.5" width="3" height="5" rx="1.5" />
|
||||
<rect x="19" y="9.5" width="3" height="5" rx="1.5" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 6C5.34315 6 4 7.34315 4 9V17C4 18.6569 5.34315 20 7 20H17C18.6569 20 20 18.6569 20 17V9C20 7.34315 18.6569 6 17 6H7ZM9 12C9.82843 12 10.5 11.3284 10.5 10.5C10.5 9.67157 9.82843 9 9 9C8.17157 9 7.5 9.67157 7.5 10.5C7.5 11.3284 8.17157 12 9 12ZM16.5 10.5C16.5 11.3284 15.8284 12 15 12C14.1716 12 13.5 11.3284 13.5 10.5C13.5 9.67157 14.1716 9 15 9C15.8284 9 16.5 9.67157 16.5 10.5ZM8 14.5C8 14.2239 8.22386 14 8.5 14H10C10.2761 14 10.5 14.2239 10.5 14.5C10.5 14.7761 10.2761 15 10 15H8.5C8.22386 15 8 14.7761 8 14.5ZM11.5 14.5C11.5 14.2239 11.7239 14 12 14H13.5C13.7761 14 14 14.2239 14 14.5C14 14.7761 13.7761 15 13.5 15H12C11.7239 15 11.5 14.7761 11.5 14.5ZM15.5 14.5C15.5 14.2239 15.7239 14 16 14H16.5C16.7761 14 17 14.2239 17 14.5C17 14.7761 16.7761 15 16.5 15H16C15.7239 15 15.5 14.7761 15.5 14.5Z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#F97316">
|
||||
<rect x="10.5" y="3" width="3" height="3" rx="1" />
|
||||
<rect x="2" y="9.5" width="3" height="5" rx="1.5" />
|
||||
<rect x="19" y="9.5" width="3" height="5" rx="1.5" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 6C5.34315 6 4 7.34315 4 9V17C4 18.6569 5.34315 20 7 20H17C18.6569 20 20 18.6569 20 17V9C20 7.34315 18.6569 6 17 6H7ZM9 12C9.82843 12 10.5 11.3284 10.5 10.5C10.5 9.67157 9.82843 9 9 9C8.17157 9 7.5 9.67157 7.5 10.5C7.5 11.3284 8.17157 12 9 12ZM16.5 10.5C16.5 11.3284 15.8284 12 15 12C14.1716 12 13.5 11.3284 13.5 10.5C13.5 9.67157 14.1716 9 15 9C15.8284 9 16.5 9.67157 16.5 10.5ZM8 14.5C8 14.2239 8.22386 14 8.5 14H10C10.2761 14 10.5 14.2239 10.5 14.5C10.5 14.7761 10.2761 15 10 15H8.5C8.22386 15 8 14.7761 8 14.5ZM11.5 14.5C11.5 14.2239 11.7239 14 12 14H13.5C13.7761 14 14 14.2239 14 14.5C14 14.7761 13.7761 15 13.5 15H12C11.7239 15 11.5 14.7761 11.5 14.5ZM15.5 14.5C15.5 14.2239 15.7239 14 16 14H16.5C16.7761 14 17 14.2239 17 14.5C17 14.7761 16.7761 15 16.5 15H16C15.7239 15 15.5 14.7761 15.5 14.5Z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C8.13401 2 5 5.13401 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13401 15.866 2 12 2ZM12 11.5C13.3807 11.5 14.5 10.3807 14.5 9C14.5 7.61929 13.3807 6.5 12 6.5C10.6193 6.5 9.5 7.61929 9.5 9C9.5 10.3807 10.6193 11.5 12 11.5Z" fill="#9CA3AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C8.13401 2 5 5.13401 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13401 15.866 2 12 2ZM12 11.5C13.3807 11.5 14.5 10.3807 14.5 9C14.5 7.61929 13.3807 6.5 12 6.5C10.6193 6.5 9.5 7.61929 9.5 9C9.5 10.3807 10.6193 11.5 12 11.5Z" fill="#F97316"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.14 12.94C19.18 12.64 19.2 12.33 19.2 12C19.2 11.67 19.18 11.36 19.14 11.06L21.17 9.48C21.35 9.34 21.4 9.07 21.29 8.87L19.37 5.55C19.25 5.33 19 5.26 18.78 5.33L16.39 6.29C15.89 5.91 15.36 5.59 14.77 5.35L14.41 2.81C14.37 2.57 14.17 2.4 13.93 2.4H10.09C9.85 2.4 9.65 2.57 9.61 2.81L9.25 5.35C8.66 5.59 8.13 5.92 7.63 6.29L5.24 5.33C5.02 5.25 4.77 5.32 4.65 5.55L2.73 8.87C2.62 9.08 2.67 9.34 2.85 9.48L4.88 11.06C4.84 11.36 4.82 11.67 4.82 12C4.82 12.33 4.84 12.64 4.88 12.94L2.85 14.52C2.67 14.66 2.62 14.93 2.73 15.13L4.65 18.45C4.77 18.67 5.02 18.74 5.24 18.67L7.63 17.71C8.13 18.09 8.66 18.41 9.25 18.65L9.61 21.19C9.65 21.43 9.85 21.6 10.09 21.6H13.93C14.17 21.6 14.37 21.43 14.41 21.19L14.77 18.65C15.36 18.41 15.89 18.08 16.39 17.71L18.78 18.67C19 18.75 19.25 18.68 19.37 18.45L21.29 15.13C21.4 14.92 21.35 14.66 21.17 14.52L19.14 12.94ZM12 15.6C10.01 15.6 8.4 13.99 8.4 12C8.4 10.01 10.01 8.4 12 8.4C13.99 8.4 15.6 10.01 15.6 12C15.6 13.99 13.99 15.6 12 15.6Z" fill="#9CA3AF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.14 12.94C19.18 12.64 19.2 12.33 19.2 12C19.2 11.67 19.18 11.36 19.14 11.06L21.17 9.48C21.35 9.34 21.4 9.07 21.29 8.87L19.37 5.55C19.25 5.33 19 5.26 18.78 5.33L16.39 6.29C15.89 5.91 15.36 5.59 14.77 5.35L14.41 2.81C14.37 2.57 14.17 2.4 13.93 2.4H10.09C9.85 2.4 9.65 2.57 9.61 2.81L9.25 5.35C8.66 5.59 8.13 5.92 7.63 6.29L5.24 5.33C5.02 5.25 4.77 5.32 4.65 5.55L2.73 8.87C2.62 9.08 2.67 9.34 2.85 9.48L4.88 11.06C4.84 11.36 4.82 11.67 4.82 12C4.82 12.33 4.84 12.64 4.88 12.94L2.85 14.52C2.67 14.66 2.62 14.93 2.73 15.13L4.65 18.45C4.77 18.67 5.02 18.74 5.24 18.67L7.63 17.71C8.13 18.09 8.66 18.41 9.25 18.65L9.61 21.19C9.65 21.43 9.85 21.6 10.09 21.6H13.93C14.17 21.6 14.37 21.43 14.41 21.19L14.77 18.65C15.36 18.41 15.89 18.08 16.39 17.71L18.78 18.67C19 18.75 19.25 18.68 19.37 18.45L21.29 15.13C21.4 14.92 21.35 14.66 21.17 14.52L19.14 12.94ZM12 15.6C10.01 15.6 8.4 13.99 8.4 12C8.4 10.01 10.01 8.4 12 8.4C13.99 8.4 15.6 10.01 15.6 12C15.6 13.99 13.99 15.6 12 15.6Z" fill="#F97316"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 829 B |
|
After Width: | Height: | Size: 890 B |
|
After Width: | Height: | Size: 437 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 259 B |
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '聊天记录',
|
||||
navigationStyle: 'custom'
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// 聊天相关接口定义
|
||||
|
||||
export interface ChatConversation {
|
||||
id: number
|
||||
name: string
|
||||
avatar: string
|
||||
lastMessage: string
|
||||
time: string
|
||||
unreadCount?: number
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: number
|
||||
type: 'user' | 'ai'
|
||||
content: string
|
||||
time: string
|
||||
isAudio?: boolean
|
||||
audioDuration?: string
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// Mock 数据 - 会话列表
|
||||
const mockConversations: ChatConversation[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '阳光知心伴伴',
|
||||
avatar: '🎤',
|
||||
lastMessage: '因为太阳光里面其实藏着七种颜色呀...',
|
||||
time: '14:31',
|
||||
unreadCount: 2
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '智慧小博士',
|
||||
avatar: '📚',
|
||||
lastMessage: '好的,我来给你讲一个关于恐龙的故事...',
|
||||
time: '昨天'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '音乐小精灵',
|
||||
avatar: '🎵',
|
||||
lastMessage: '《拔萝卜》已经唱完啦,还要听别的吗?',
|
||||
time: '昨天'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '故事大王',
|
||||
avatar: '📖',
|
||||
lastMessage: '从前有一只勇敢的小兔子...',
|
||||
time: '周一'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '英语小老师',
|
||||
avatar: '🔤',
|
||||
lastMessage: 'Apple, A-P-P-L-E, apple!',
|
||||
time: '周一'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock 数据 - 消息列表
|
||||
const mockMessages: Record<number, ChatMessage[]> = {
|
||||
1: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
content: '天空为什么是蓝色的呀?',
|
||||
time: '今天 14:30'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'ai',
|
||||
content: '因为太阳光里面其实藏着七种颜色呀。当光穿过大气层的时候,蓝色的光最调皮,到处乱跑,所以我们看到的天空就是蓝色的啦!',
|
||||
time: '今天 14:31'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'user',
|
||||
content: '给我唱一首拔萝卜吧!',
|
||||
time: '今天 15:15'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'ai',
|
||||
content: '',
|
||||
time: '今天 15:15',
|
||||
isAudio: true,
|
||||
audioDuration: '01:45'
|
||||
}
|
||||
],
|
||||
2: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
content: '你知道恐龙吗?',
|
||||
time: '昨天 10:30'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'ai',
|
||||
content: '当然知道!恐龙是生活在很久很久以前的大型爬行动物,有的恐龙比房子还要大呢!',
|
||||
time: '昨天 10:31'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'user',
|
||||
content: '那给我讲一个恐龙的故事吧',
|
||||
time: '昨天 10:32'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'ai',
|
||||
content: '好的,我来给你讲一个关于恐龙的故事...',
|
||||
time: '昨天 10:33'
|
||||
}
|
||||
],
|
||||
3: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
content: '我想听拔萝卜',
|
||||
time: '昨天 16:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'ai',
|
||||
content: '',
|
||||
time: '昨天 16:01',
|
||||
isAudio: true,
|
||||
audioDuration: '02:30'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'ai',
|
||||
content: '《拔萝卜》已经唱完啦,还要听别的吗?',
|
||||
time: '昨天 16:04'
|
||||
}
|
||||
],
|
||||
4: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
content: '给我讲个故事吧',
|
||||
time: '周一 20:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'ai',
|
||||
content: '从前有一只勇敢的小兔子...',
|
||||
time: '周一 20:01'
|
||||
}
|
||||
],
|
||||
5: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
content: '苹果用英语怎么说?',
|
||||
time: '周一 18:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'ai',
|
||||
content: 'Apple, A-P-P-L-E, apple!',
|
||||
time: '周一 18:01'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Mock 数据 - 角色信息
|
||||
const mockCharacters: Record<number, Character> = {
|
||||
1: { id: 1, name: '阳光知心伴伴', description: '温柔音色 / 开朗耐心', icon: '🎤' },
|
||||
2: { id: 2, name: '智慧小博士', description: '博学多才 / 爱讲故事', icon: '📚' },
|
||||
3: { id: 3, name: '音乐小精灵', description: '歌声甜美 / 会唱儿歌', icon: '🎵' },
|
||||
4: { id: 4, name: '故事大王', description: '故事丰富 / 生动有趣', icon: '📖' },
|
||||
5: { id: 5, name: '英语小老师', description: '英语启蒙 / 发音标准', icon: '🔤' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
export function getConversations(): Promise<ChatConversation[]> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([...mockConversations])
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息列表
|
||||
* @param conversationId 会话ID
|
||||
*/
|
||||
export function getMessages(conversationId: number): Promise<ChatMessage[]> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockMessages[conversationId] || [])
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色信息
|
||||
* @param characterId 角色ID
|
||||
*/
|
||||
export function getCharacter(characterId: number): Promise<Character | null> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockCharacters[characterId] || null)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||