Initial commit: Happy Up monorepo through Sprint 5.
Document-driven MVP with FastAPI backend, Vue H5, WeChat mini shell, product demo, and Docker dev stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Happy Up 微信小程序壳
|
||||
|
||||
Sprint 5 最小可用壳,复用 FastAPI 后端,与 H5 共用同一套 API。
|
||||
|
||||
## 开发
|
||||
|
||||
1. 启动 API:`make api-dev`
|
||||
2. 微信开发者工具导入本目录 `apps/mini`
|
||||
3. 详情 → 本地设置 → 勾选「不校验合法域名」
|
||||
4. 默认 API:`http://127.0.0.1:8000`(可在 `app.js` 修改 `apiBase`)
|
||||
|
||||
## 页面
|
||||
|
||||
| 页面 | 说明 |
|
||||
|------|------|
|
||||
| login | 手机验证码登录(682139) |
|
||||
| home | 儿童档案列表 |
|
||||
| screening | 直传 + 分析 + 报告摘要 |
|
||||
|
||||
完整 UI 与 Tab 导航在 `apps/web` H5;小程序后续可嵌入 web-view 或继续原生迭代。
|
||||
@@ -0,0 +1,12 @@
|
||||
App({
|
||||
globalData: {
|
||||
apiBase: 'http://127.0.0.1:8000',
|
||||
token: '',
|
||||
},
|
||||
onLaunch() {
|
||||
const token = wx.getStorageSync('happy_up_token')
|
||||
if (token) {
|
||||
this.globalData.token = token
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/login/login",
|
||||
"pages/home/home",
|
||||
"pages/screening/screening"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTitleText": "Happy Up",
|
||||
"navigationBarBackgroundColor": "#1a6fb5",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#eef2f7"
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
page {
|
||||
background: #eef2f7;
|
||||
color: #1f2937;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 24rpx;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 24rpx;
|
||||
background: #1a6fb5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #6b7280;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
children: [],
|
||||
error: '',
|
||||
},
|
||||
onShow() {
|
||||
this.load()
|
||||
},
|
||||
async load() {
|
||||
try {
|
||||
const data = await api.listChildren()
|
||||
this.setData({ children: data.list || [], error: '' })
|
||||
} catch (err) {
|
||||
this.setData({ error: err.message || '加载失败' })
|
||||
}
|
||||
},
|
||||
goScreening() {
|
||||
wx.navigateTo({ url: '/pages/screening/screening' })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "首页"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<view class="card">
|
||||
<view>儿童档案</view>
|
||||
<view class="muted">{{error}}</view>
|
||||
<block wx:for="{{children}}" wx:key="id">
|
||||
<view>{{item.name}} · {{item.birthday}}</view>
|
||||
</block>
|
||||
<button class="btn-primary" bindtap="goScreening">去筛查</button>
|
||||
</view>
|
||||
@@ -0,0 +1,23 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
phone: '18600000000',
|
||||
code: '682139',
|
||||
error: '',
|
||||
},
|
||||
onPhone(e) {
|
||||
this.setData({ phone: e.detail.value })
|
||||
},
|
||||
onCode(e) {
|
||||
this.setData({ code: e.detail.value })
|
||||
},
|
||||
async submit() {
|
||||
try {
|
||||
await api.login(this.data.phone, this.data.code)
|
||||
wx.redirectTo({ url: '/pages/home/home' })
|
||||
} catch (err) {
|
||||
this.setData({ error: err.message || '登录失败' })
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<view class="card">
|
||||
<view>手机号验证码登录</view>
|
||||
<view class="muted">开发验证码 682139</view>
|
||||
<input value="{{phone}}" bindinput="onPhone" placeholder="手机号" />
|
||||
<input value="{{code}}" bindinput="onCode" placeholder="验证码" />
|
||||
<view class="muted">{{error}}</view>
|
||||
<button class="btn-primary" bindtap="submit">登录</button>
|
||||
</view>
|
||||
@@ -0,0 +1,6 @@
|
||||
input {
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx;
|
||||
background: #f9fafb;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
status: '准备就绪',
|
||||
reportSummary: '',
|
||||
},
|
||||
async run() {
|
||||
this.setData({ status: '读取档案…' })
|
||||
try {
|
||||
const children = await api.listChildren()
|
||||
const child = (children.list || [])[0]
|
||||
if (!child) throw new Error('暂无儿童档案')
|
||||
|
||||
this.setData({ status: '获取上传凭证…' })
|
||||
const tokenData = await api.request('/api/videos/upload-token', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
childId: child.id,
|
||||
fileName: 'mini.mp4',
|
||||
contentType: 'video/mp4',
|
||||
size: 1024,
|
||||
},
|
||||
})
|
||||
|
||||
if (tokenData.storage === 'local') {
|
||||
this.setData({ status: '直传本地存储…' })
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: tokenData.uploadUrl,
|
||||
method: 'PUT',
|
||||
header: {
|
||||
'Content-Type': 'video/mp4',
|
||||
'X-Upload-Token': tokenData.uploadToken,
|
||||
'X-Object-Key': tokenData.objectKey,
|
||||
},
|
||||
data: new Uint8Array([0, 0, 0, 24, 102, 116, 121, 112, 105, 115, 111, 109]).buffer,
|
||||
success(res) {
|
||||
if (res.statusCode === 204) resolve()
|
||||
else reject(new Error('直传失败'))
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.setData({ status: '提交分析…' })
|
||||
const video = await api.request('/api/videos', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
childId: child.id,
|
||||
objectKey: tokenData.objectKey,
|
||||
scene: 'front_posture',
|
||||
},
|
||||
})
|
||||
|
||||
const task = await api.request('/api/analysis/tasks', {
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': `mini-${Date.now()}` },
|
||||
data: {
|
||||
childId: child.id,
|
||||
videoId: video.id,
|
||||
taskType: 'posture_screening',
|
||||
},
|
||||
})
|
||||
|
||||
const report = await api.request(`/api/reports/${task.reportId}`)
|
||||
this.setData({
|
||||
status: '完成',
|
||||
reportSummary: report.summary,
|
||||
})
|
||||
} catch (err) {
|
||||
this.setData({ status: err.message || '失败' })
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "筛查联调"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<view class="card">
|
||||
<view>筛查流程</view>
|
||||
<view class="muted">{{status}}</view>
|
||||
<view wx:if="{{reportSummary}}">{{reportSummary}}</view>
|
||||
<button class="btn-primary" bindtap="run">一键联调</button>
|
||||
</view>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Happy Up 微信小程序壳 · Sprint 5",
|
||||
"packOptions": {
|
||||
"ignore": []
|
||||
},
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"appid": "touristappid",
|
||||
"projectname": "happy-up-mini",
|
||||
"libVersion": "3.6.0"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"desc": "Happy Up mini program sitemap",
|
||||
"rules": [{ "action": "allow", "page": "*" }]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
const app = getApp()
|
||||
|
||||
function request(path, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase}${path}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: app.globalData.token ? `Bearer ${app.globalData.token}` : '',
|
||||
...(options.header || {}),
|
||||
},
|
||||
success(res) {
|
||||
const body = res.data
|
||||
if (res.statusCode >= 200 && res.statusCode < 300 && body.code === 0) {
|
||||
resolve(body.data)
|
||||
return
|
||||
}
|
||||
reject(new Error(body.message || `HTTP ${res.statusCode}`))
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function login(phone, code) {
|
||||
return request('/api/auth/login', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
loginType: 'phone_code',
|
||||
credential: phone,
|
||||
code,
|
||||
},
|
||||
}).then((data) => {
|
||||
app.globalData.token = data.token
|
||||
wx.setStorageSync('happy_up_token', data.token)
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
function listChildren() {
|
||||
return request('/api/children')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
request,
|
||||
login,
|
||||
listChildren,
|
||||
}
|
||||
Reference in New Issue
Block a user