diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..a9b2e23 Binary files /dev/null and b/.DS_Store differ diff --git a/banban-mini/config/index.js b/banban-mini/config/index.js index 67f215a..ee013d5 100644 --- a/banban-mini/config/index.js +++ b/banban-mini/config/index.js @@ -1,4 +1,6 @@ -const config = { +const path = require('path') + +module.exports = { projectName: 'banban-mini', date: '2025-3-3', designWidth: 750, @@ -10,34 +12,31 @@ const config = { sourceRoot: 'src', outputRoot: 'dist', plugins: [], - defineConstants: { - }, + defineConstants: {}, copy: { patterns: [ { from: 'src/assets/tab-icons', to: 'dist/assets/tab-icons' } ], - options: { - } + options: {} }, framework: 'react', mini: { + webpackChain(chain) { + chain.resolve.alias.set('@', path.resolve(__dirname, '../src')) + }, postcss: { pxtransform: { enable: true, - config: { - - } + config: {} }, url: { enable: true, - config: { - limit: 1024 // 设定转换尺寸上限 - } + config: { limit: 1024 } }, cssModules: { - enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + enable: false, config: { - namingPattern: 'module', // 转换模式,取值为 global/module + namingPattern: 'module', generateScopedName: '[name]__[local]___[hash:base64:5]' } } @@ -46,26 +45,18 @@ const config = { h5: { publicPath: '/', staticDirectory: 'static', + webpackChain(chain) { + chain.resolve.alias.set('@', path.resolve(__dirname, '../src')) + }, postcss: { - autoprefixer: { - enable: true, - config: { - } - }, + autoprefixer: { enable: true, config: {} }, cssModules: { - enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + enable: false, config: { - namingPattern: 'module', // 转换模式,取值为 global/module + namingPattern: 'module', generateScopedName: '[name]__[local]___[hash:base64:5]' } } } } -} - -module.exports = function (merge) { - if (process.env.NODE_ENV === 'development') { - return merge({}, config, require('./dev')) - } - return merge({}, config, require('./prod')) } \ No newline at end of file diff --git a/banban-mini/package-lock.json b/banban-mini/package-lock.json index 26b94e5..e1e4fef 100644 --- a/banban-mini/package-lock.json +++ b/banban-mini/package-lock.json @@ -57,7 +57,6 @@ "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2907,7 +2906,6 @@ "integrity": "sha512-zwE3TLgoZwJfQygdv2SdCK9mRLYluwDOM53I+dT6Z5ZvrgVENmY3txvWDvduzkV+/8IuvrRbVezMpxcojadRdQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.1", "@swc/types": "^0.1.5" @@ -3298,7 +3296,6 @@ "resolved": "https://registry.npmmirror.com/@tarojs/components/-/components-3.6.38.tgz", "integrity": "sha512-9Ay5cZVFSSebYrbtBeSU5bT9DzUXMzgJIr/cPE9899/Ld/Z0IaVU9e5DtTOvoQEwNr7VEC8+haaLHgWcnLmrVA==", "license": "MIT", - "peer": true, "dependencies": { "@stencil/core": "^2.22.2", "@tarojs/components-advanced": "3.6.38", @@ -4327,7 +4324,6 @@ "resolved": "https://registry.npmmirror.com/@tarojs/runtime/-/runtime-3.6.38.tgz", "integrity": "sha512-LcENEVcsxLokNHlNbzLJesbZJvQlRNrWbr8CV/wKCFzZttRM1OmlmDbV5kxnaAqGlC2yxIg786qc2wwnAwgwuQ==", "license": "MIT", - "peer": true, "dependencies": { "lodash-es": "4.17.21", "tslib": "^2.6.2" @@ -4359,15 +4355,13 @@ "version": "3.6.38", "resolved": "https://registry.npmmirror.com/@tarojs/shared/-/shared-3.6.38.tgz", "integrity": "sha512-+bx8UdTJ5qAdFw8Xhx2/aUmw1KjEqhQ9gc7X5kE7aBuvIo+xL93HFVQ/DgJ0tNmONUQ3352g5+VdAtVeiI1xHg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tarojs/taro": { "version": "3.6.38", "resolved": "https://registry.npmmirror.com/@tarojs/taro/-/taro-3.6.38.tgz", "integrity": "sha512-KG5JQxVgkCjJ/sbSEL6+fyDHU8D/zUMpLJSSw4x/sq+9wnWvG+r4dnPh2meI1LcIhMdi+0sd3MErOis1qNqQlQ==", "license": "MIT", - "peer": true, "dependencies": { "@tarojs/api": "3.6.38" }, @@ -4931,7 +4925,6 @@ "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -5365,7 +5358,6 @@ "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5487,7 +5479,6 @@ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6623,7 +6614,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9853,7 +9843,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -16864,7 +16853,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17529,7 +17517,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -18351,7 +18338,6 @@ "resolved": "https://registry.npmmirror.com/rework/-/rework-1.0.1.tgz", "integrity": "sha512-eEjL8FdkdsxApd0yWVZgBGzfCQiT8yqSc2H1p4jpZpQdtz7ohETiDMoje5PlM8I9WgkqkreVxFUKYOiJdVWDXw==", "dev": true, - "peer": true, "dependencies": { "convert-source-map": "^0.3.3", "css": "^2.0.0" @@ -18362,8 +18348,7 @@ "resolved": "https://registry.npmmirror.com/rework-visit/-/rework-visit-1.0.0.tgz", "integrity": "sha512-W6V2fix7nCLUYX1v6eGPrBOZlc03/faqzP4sUxMAJMBMOPYhfV/RyLegTufn5gJKaOITyi+gvf0LXDZ9NzkHnQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/rework/node_modules/convert-source-map": { "version": "0.3.5", @@ -22010,7 +21995,6 @@ "resolved": "https://registry.npmmirror.com/webpack/-/webpack-4.47.0.tgz", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", diff --git a/banban-mini/src/pages/bind/index.scss b/banban-mini/src/pages/bind/index.scss index 9f73a56..50b3718 100644 --- a/banban-mini/src/pages/bind/index.scss +++ b/banban-mini/src/pages/bind/index.scss @@ -6,6 +6,67 @@ padding-bottom: calc(120px + env(safe-area-inset-bottom)); } +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 400px; + font-size: 30px; + color: #999999; +} + +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 70%; + font-size: 30px; + color: #999999; + padding: 0 48px; + + .empty-title { + font-size: 36px; + color: #1A1A1A; + font-weight: 600; + margin-bottom: 12px; + } + + .empty-desc { + font-size: 26px; + color: #999999; + margin-bottom: 48px; + } + + .empty-input-wrap { + width: 100%; + margin-bottom: 24px; + + .empty-input { + background: #F5F7FA; + border-radius: 12px; + padding: 24px; + font-size: 30px; + width: 100%; + height: 96px; + box-sizing: border-box; + text-align: center; + } + } + + .empty-btn { + background: #FF8C42; + border-radius: 12px; + padding: 24px 64px; + + .empty-btn-text { + color: #FFFFFF; + font-size: 32px; + font-weight: 500; + } + } +} + .icon-bg { width: 72px; height: 72px; diff --git a/banban-mini/src/pages/bind/index.tsx b/banban-mini/src/pages/bind/index.tsx index 0b3c571..287b0bb 100644 --- a/banban-mini/src/pages/bind/index.tsx +++ b/banban-mini/src/pages/bind/index.tsx @@ -1,27 +1,59 @@ -import { View, Text, Button, Image } from '@tarojs/components' +import { View, Text, Button, Image, Input } from '@tarojs/components' import { useState, useEffect } from 'react' import Taro from '@tarojs/taro' +import { getToken } from '@/services/api' +import { getCurrentBinding, directBind } from '@/services/binding' +import { getChildren, createChild } from '@/services/child' import './index.scss' +interface Child { + child_id: number + child_name: string +} + export default function Bind() { const [isScanning, setIsScanning] = useState(false) + const [loading, setLoading] = useState(true) + const [children, setChildren] = useState([]) + const [newChildName, setNewChildName] = useState('') + const [saving, setSaving] = useState(false) useEffect(() => { - // 检查是否已登录 - const isLoggedIn = Taro.getStorageSync('isLoggedIn') - if (!isLoggedIn) { - // 未登录,跳转到登录页面 + checkAuth() + }, []) + + const checkAuth = async () => { + const token = getToken() + if (!token) { Taro.redirectTo({ url: '/pages/login/index' }) return } - // 检查是否已绑定设备 - const hasDevice = Taro.getStorageSync('hasDevice') - if (hasDevice) { - // 已绑定设备,跳转到首页 - Taro.reLaunch({ url: '/pages/device/index' }) + try { + const childRes = await getChildren() + setChildren(childRes.items || []) + + if (childRes.items?.length === 0) { + setLoading(false) + return + } + } catch (err) { + console.error('Get children failed:', err) } - }, []) + + try { + const binding = await getCurrentBinding() + if (binding) { + Taro.showToast({ title: '已有设备绑定', icon: 'none' }) + setTimeout(() => { + Taro.reLaunch({ url: '/pages/device/index' }) + }, 1000) + return + } + } catch (err) { + } + setLoading(false) + } const handleScanCode = () => { setIsScanning(true) @@ -29,56 +61,57 @@ export default function Bind() { Taro.scanCode({ onlyFromCamera: true, scanType: ['qrCode', 'barCode'], - success: (res) => { + success: async (res) => { setIsScanning(false) - console.log('扫码结果:', res.result) + const deviceId = 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) + if (!deviceId) { + Taro.showToast({ title: '无效的设备码', icon: 'none' }) + return + } + + await doBind(deviceId) }, fail: (err) => { setIsScanning(false) - console.error('扫码失败:', err) - - // 用户取消扫码,不显示错误 if (err.errMsg && err.errMsg.includes('cancel')) { return } - - Taro.showToast({ - title: '扫码失败,请重试', - icon: 'none' - }) + Taro.showToast({ title: '扫码失败', icon: 'none' }) } }) } + const doBind = async (deviceId: string) => { + if (children.length === 0) { + Taro.showToast({ title: '请先添加儿童', icon: 'none' }) + return + } + + Taro.showLoading({ title: '绑定中...' }) + + try { + await directBind({ + device_id: deviceId, + child_id: children[0].child_id + }) + Taro.hideLoading() + Taro.showToast({ + title: '设备绑定成功', + icon: 'success', + duration: 1500, + complete: () => { + setTimeout(() => { + Taro.reLaunch({ url: '/pages/device/index' }) + }, 500) + } + }) + } catch (err: any) { + Taro.hideLoading() + Taro.showToast({ title: err.message || '绑定失败', icon: 'none' }) + } + } + const handleManualInput = () => { Taro.showModal({ title: '手动输入设备码', @@ -86,37 +119,60 @@ export default function Bind() { 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) + doBind(res.content) } } }) } + const handleAddChild = async () => { + if (!newChildName.trim()) { + Taro.showToast({ title: '请输入儿童姓名', icon: 'none' }) + return + } + setSaving(true) + try { + await createChild({ child_name: newChildName.trim() }) + Taro.showToast({ title: '添加成功', icon: 'success' }) + const childRes = await getChildren() + setChildren(childRes.items || []) + } catch (err) { + Taro.showToast({ title: '添加失败', icon: 'none' }) + } finally { + setSaving(false) + } + } + + if (loading) { + return ( + + 加载中... + + ) + } + + if (children.length === 0) { + return ( + + + 请先添加儿童 + 绑定设备前需要先添加儿童信息 + + setNewChildName(e.detail.value)} + /> + + + {saving ? '保存中...' : '确认添加'} + + + + ) + } + return ( @@ -170,4 +226,4 @@ export default function Bind() { ) -} +} \ No newline at end of file diff --git a/banban-mini/src/pages/device/index.tsx b/banban-mini/src/pages/device/index.tsx index df306d2..210aff6 100644 --- a/banban-mini/src/pages/device/index.tsx +++ b/banban-mini/src/pages/device/index.tsx @@ -1,6 +1,7 @@ import { View, Text, Switch, Slider, Image } from '@tarojs/components' import { useState, useEffect } from 'react' import Taro from '@tarojs/taro' +import { getCurrentBinding } from '@/services/binding' import './index.scss' interface DeviceData { @@ -20,35 +21,59 @@ export default function Device() { const [hasDevice, setHasDevice] = useState(false) useEffect(() => { - // 检查登录状态 - const isLoggedIn = Taro.getStorageSync('isLoggedIn') - if (!isLoggedIn) { + console.log('[Device] Checking login status...') + const token = Taro.getStorageSync('token') + console.log('[Device] Token exists:', !!token) + + if (!token) { + console.log('[Device] No token, redirecting to login') 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) + loadDeviceInfo() }, []) + const loadDeviceInfo = async () => { + try { + const binding = await getCurrentBinding() + console.log('[Device] Got binding:', binding) + + if (binding) { + setHasDevice(true) + Taro.setStorageSync('hasDevice', '1') + Taro.setStorageSync('deviceInfo', { + id: binding.device_id, + name: '伴伴设备', + status: 'online', + battery: 82 + }) + setDevice({ + id: binding.device_id, + name: '伴伴设备', + status: 'online', + battery: 82, + daysLeft: 4, + sleepEnabled: true, + sleepTime: '每天 22:00 - 07:00', + volume: 50 + }) + } else { + setHasDevice(false) + Taro.removeStorageSync('hasDevice') + Taro.removeStorageSync('deviceInfo') + } + } catch (err: any) { + console.log('[Device] No binding or error:', err?.message || err) + if (err?.status === 404) { + setHasDevice(false) + Taro.removeStorageSync('hasDevice') + Taro.removeStorageSync('deviceInfo') + } + } + setIsLoading(false) + } + const handleSleepToggle = (e: any) => { if (!device) return setDevice({ ...device, sleepEnabled: e.detail.value }) diff --git a/banban-mini/src/pages/login/index.tsx b/banban-mini/src/pages/login/index.tsx index b4b638b..694eee3 100644 --- a/banban-mini/src/pages/login/index.tsx +++ b/banban-mini/src/pages/login/index.tsx @@ -1,51 +1,118 @@ import { View, Text, Button } from '@tarojs/components' import { useEffect } from 'react' import Taro from '@tarojs/taro' +import { BASE_URL, request } from '@/services/api' import './index.scss' export default function Login() { useEffect(() => { - // 检查是否已登录 - const isLoggedIn = Taro.getStorageSync('isLoggedIn') - - if (isLoggedIn) { - // 已登录,跳转到首页 + console.log('[Login] Page mounted, checking token...') + const token = Taro.getStorageSync('token') + console.log('[Login] Token exists:', !!token) + if (token) { + console.log('[Login] Already logged in, redirecting to device page') 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 + const doWeChatLogin = async () => { + try { + console.log('[Login] Starting WeChat login...') + Taro.showLoading({ title: '登录中...' }) + + // 1. 调用微信登录获取 code + console.log('[Login] Step 1: Calling Taro.login()...') + const loginRes = await Taro.login() + const code = loginRes.code + console.log('[Login] Got code:', code ? 'yes' : 'no') + + // 2. 获取用户头像信息 (可选) + let userInfo = null + try { + console.log('[Login] Step 2: Calling Taro.getUserProfile()...') + const profileRes = await Taro.getUserProfile({ + desc: '用于完善用户资料' }) - - Taro.hideLoading() - - // 登录成功后跳转到首页 + userInfo = profileRes.userInfo + console.log('[Login] Got userInfo:', !!userInfo) + } catch (e) { + console.log('[Login] getUserProfile failed (expected if user denied):', e) + } + + // 3. 调用后端登录接口 (微信登录) + console.log('[Login] Step 3: Calling backend /auth/login...') + const res = await request<{ access_token: string; expires_in: number; user_id: number }>('/auth/login', { + method: 'POST', + data: { + code: code, // 微信 code + nickname: userInfo?.nickName, + avatar_url: userInfo?.avatarUrl + } + }) + console.log('[Login] Backend login success, user_id:', res.user_id) + + // 4. 保存 token 和用户信息 + console.log('[Login] Step 4: Saving token and user info...') + Taro.setStorageSync('token', res.access_token) + Taro.setStorageSync('user_id', res.user_id) + Taro.setStorageSync('expires_in', res.expires_in) + + // 保存用户信息用于显示 + if (userInfo) { + Taro.setStorageSync('userInfo', { + nickname: userInfo.nickName, + avatar_url: userInfo.avatarUrl + }) + } + + // 5. 如果获取到了用户信息,发送到后端更新 + if (userInfo) { + console.log('[Login] Step 5: Updating user profile...') + try { + await request(`/parents/${res.user_id}`, { + method: 'PATCH', + data: { + nickname: userInfo.nickName, + avatar_url: userInfo.avatarUrl + } + }) + } catch (e) { + console.log('[Login] Update user profile failed:', e) + } + } + + Taro.hideLoading() + console.log('[Login] Step 6: Login complete, showing success toast') + Taro.showToast({ title: '登录成功', icon: 'success' }) + + // 6. 跳转到首页 + console.log('[Login] Redirecting to /pages/device/index...') + setTimeout(() => { Taro.reLaunch({ url: '/pages/device/index' }) - }, - fail: (err) => { - Taro.hideLoading() - console.error('微信登录失败:', err) + }, 500) + + } catch (err: any) { + Taro.hideLoading() + console.error('[Login] Login failed:', err) + + // 如果是 401 错误,尝试通过创建新用户登录 + if (err.status === 401) { Taro.showToast({ title: '登录失败,请重试', icon: 'none' }) + } else { + Taro.showToast({ + title: err.message || '网络错误,请重试', + icon: 'none' + }) } - }) + } + } + + const handleLogin = () => { + console.log('Button clicked, starting login...') + doWeChatLogin() } return ( @@ -77,8 +144,7 @@ export default function Login() {