feat(P01): 迁移 Taro 小程序项目代码

- 迁移前端源码 (src/)
- 迁移后端服务 (server/)
- 迁移配置文件 (package.json, tsconfig.json 等)
- 更新需求概要文档
- 更新架构设计文档
- 更新接口定义文档
- 更新环境配置文档
- 创建测试目录结构和配置

项目技术栈:
- Taro 4.1.9 (跨端框架)
- React 18
- TypeScript
- NestJS (后端)
- Tailwind CSS 4
- shadcn/ui 组件库
This commit is contained in:
Dev AI
2026-05-22 16:25:05 +08:00
parent 00ce240c01
commit fb348f3740
106 changed files with 13015 additions and 182 deletions
+122
View File
@@ -0,0 +1,122 @@
import Taro from '@tarojs/taro'
import { canUseDOM, isH5 } from './platform'
export type Rect = { left: number; top: number; width: number; height: number }
const toNumber = (v: unknown) => {
if (typeof v === 'number') return v
if (typeof v === 'string') {
const n = parseFloat(v)
return Number.isFinite(n) ? n : 0
}
return 0
}
const normalizeRect = (r: Record<string, unknown> | DOMRect | null | undefined): Rect | null => {
if (!r) return null
if ('left' in r && 'top' in r && 'width' in r && 'height' in r) {
return {
left: toNumber(r.left),
top: toNumber(r.top),
width: toNumber(r.width),
height: toNumber(r.height),
}
}
return null
}
export const getViewport = () => {
if (isH5() && canUseDOM()) {
return { width: window.innerWidth, height: window.innerHeight }
}
try {
const info = Taro.getSystemInfoSync()
return {
width: toNumber(info.windowWidth),
height: toNumber(info.windowHeight),
}
} catch {
return { width: 375, height: 667 }
}
}
const getRectH5 = (id: string): Rect | null => {
if (!isH5() || !canUseDOM()) return null
const el = document.getElementById(id)
if (!el) return null
const r = el.getBoundingClientRect()
return normalizeRect(r)
}
export const getRectById = (id: string): Promise<Rect | null> => {
const h5Rect = getRectH5(id)
if (h5Rect) return Promise.resolve(h5Rect)
return new Promise(resolve => {
const query = Taro.createSelectorQuery()
query
.select(`#${id}`)
.boundingClientRect(res => {
const rect = Array.isArray(res) ? res[0] : res
if (rect && 'left' in rect) {
resolve(normalizeRect(rect))
} else {
resolve(null)
}
})
.exec()
})
}
export const computePosition = (params: {
triggerRect: Rect
contentRect: Rect
align: 'start' | 'center' | 'end'
side: 'top' | 'bottom' | 'left' | 'right'
sideOffset: number
}) => {
const { triggerRect, contentRect, align, side, sideOffset } = params
const { width: vw, height: vh } = getViewport()
const margin = 8
const crossStart =
side === 'left' || side === 'right' ? triggerRect.top : triggerRect.left
const crossSize =
side === 'left' || side === 'right'
? triggerRect.height
: triggerRect.width
const contentCrossSize =
side === 'left' || side === 'right'
? contentRect.height
: contentRect.width
const cross = (() => {
if (align === 'start') return crossStart
if (align === 'end') return crossStart + crossSize - contentCrossSize
return crossStart + crossSize / 2 - contentCrossSize / 2
})()
let left = 0
let top = 0
if (side === 'bottom' || side === 'top') {
left = cross
top =
side === 'bottom'
? triggerRect.top + triggerRect.height + sideOffset
: triggerRect.top - contentRect.height - sideOffset
} else {
top = cross
left =
side === 'right'
? triggerRect.left + triggerRect.width + sideOffset
: triggerRect.left - contentRect.width - sideOffset
}
const maxLeft = Math.max(margin, vw - contentRect.width - margin)
const maxTop = Math.max(margin, vh - contentRect.height - margin)
left = Math.min(Math.max(left, margin), maxLeft)
top = Math.min(Math.max(top, margin), maxTop)
return { left, top }
}