Initial commit

This commit is contained in:
2026-06-05 17:46:51 +08:00
commit c04c890186
96 changed files with 6477 additions and 0 deletions

81
miniprogram/utils/api.ts Normal file
View File

@@ -0,0 +1,81 @@
import { Post, UserProfile, NearbyUser, Pet } from '../types/index'
type CloudResult<T> = { result: { code: number; data: T; message?: string } }
async function call<T>(name: string, data?: Record<string, unknown>): Promise<T> {
const res = await wx.cloud.callFunction({ name, data: data || {} }) as CloudResult<T>
if (res.result.code !== 0) throw new Error(res.result.message || '请求失败')
return res.result.data
}
export const api = {
login: () =>
call<{ userInfo: UserProfile; token: string }>('login'),
getUserProfile: (userId: string) =>
call<UserProfile>('getUser', { userId }),
getPosts: (params: { page?: number; type?: 'feed' | 'nearby' | 'hot'; latitude?: number; longitude?: number }) =>
call<{ list: Post[]; total: number; hasMore: boolean }>('getPosts', params),
getUserPosts: (userId: string, page = 0) =>
call<{ list: Post[]; total: number }>('getPosts', { userId, page }),
createPost: (params: {
content: string
images: string[]
video?: string
location?: { name: string; latitude: number; longitude: number }
hashtags: string[]
mood?: string
petId?: string
}) => call<Post>('createPost', params),
deletePost: (postId: string) =>
call<void>('deletePost', { postId }),
likePost: (postId: string) =>
call<{ liked: boolean; count: number }>('likePost', { postId }),
getComments: (postId: string, page = 0) =>
call<{ list: Comment[]; total: number }>('getComments', { postId, page }),
addComment: (postId: string, content: string) =>
call<Comment>('addComment', { postId, content }),
getNearbyPets: (latitude: number, longitude: number, radiusKm = 5) =>
call<NearbyUser[]>('getNearbyPets', { latitude, longitude, radiusKm }),
updateLocation: (latitude: number, longitude: number) =>
call<void>('updateLocation', { latitude, longitude }),
sendGreeting: (toUserId: string, petId: string) =>
call<void>('sendGreeting', { toUserId, petId }),
followUser: (targetId: string) =>
call<{ following: boolean }>('followUser', { targetId }),
getFollowList: (type: 'following' | 'followers', userId?: string) =>
call<UserProfile[]>('getFollowList', { type, userId }),
getMessages: (page = 0) =>
call<{ list: any[]; unreadCount: number }>('getMessages', { page }),
sendMessage: (toId: string, content: string, type = 'text') =>
call<void>('sendMessage', { toId, content, type }),
updatePet: (pet: Partial<Pet> & { _id?: string }) =>
call<Pet>('updatePet', { pet }),
uploadImage: async (filePath: string): Promise<string> => {
const ext = filePath.split('.').pop() || 'jpg'
const cloudPath = `posts/${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`
const res = await wx.cloud.uploadFile({ cloudPath, filePath })
return res.fileID
},
getFileURL: async (fileID: string): Promise<string> => {
const res = await wx.cloud.getTempFileURL({ fileList: [fileID] })
return res.fileList[0]?.tempFileURL || fileID
},
}

74
miniprogram/utils/auth.ts Normal file
View File

@@ -0,0 +1,74 @@
import { UserProfile } from '../types/index'
import { api } from './api'
const TOKEN_KEY = 'wangquan_token'
const USER_KEY = 'wangquan_user'
export async function login(): Promise<UserProfile> {
const { userInfo } = await api.login()
wx.setStorageSync(USER_KEY, JSON.stringify(userInfo))
return userInfo
}
export function getCachedUser(): UserProfile | null {
try {
const raw = wx.getStorageSync(USER_KEY)
return raw ? JSON.parse(raw) : null
} catch {
return null
}
}
export function clearAuth(): void {
wx.removeStorageSync(TOKEN_KEY)
wx.removeStorageSync(USER_KEY)
}
export async function requireLocation(): Promise<{ latitude: number; longitude: number }> {
return new Promise((resolve, reject) => {
wx.getLocation({
type: 'gcj02',
success: res => resolve({ latitude: res.latitude, longitude: res.longitude }),
fail: () => {
wx.showModal({
title: '需要位置权限',
content: '汪圈需要获取您的位置来显示附近的宠物,请在设置中开启位置权限',
confirmText: '去设置',
success: res => {
if (res.confirm) wx.openSetting({})
reject(new Error('位置权限被拒绝'))
},
})
},
})
})
}
export async function chooseAndUploadMedia(count = 9): Promise<string[]> {
return new Promise((resolve, reject) => {
wx.chooseMedia({
count,
mediaType: ['image'],
sourceType: ['album', 'camera'],
maxDuration: 30,
camera: 'back',
success: async res => {
try {
wx.showLoading({ title: '上传中...', mask: true })
const uploads = res.tempFiles.map(f => {
const ext = f.tempFilePath.split('.').pop() || 'jpg'
const cloudPath = `posts/${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`
return wx.cloud.uploadFile({ cloudPath, filePath: f.tempFilePath })
})
const results = await Promise.all(uploads)
wx.hideLoading()
resolve(results.map(r => r.fileID))
} catch (e) {
wx.hideLoading()
reject(e)
}
},
fail: reject,
})
})
}

View File

@@ -0,0 +1,58 @@
export const CLOUD_ENV = 'YOUR_CLOUD_ENV_ID'
export const COLORS = {
pink: '#ff4f91',
orange: '#ff9f1c',
yellow: '#ffe15a',
green: '#2fd37a',
mint: '#67e8c9',
blue: '#4d8dff',
violet: '#8c5cff',
ink: '#272235',
bgPrimary: '#fff9fb',
textPrimary: '#272235',
textSecondary: '#6a6178',
textTertiary: '#9b8fa8',
}
export const AVATAR_GRADIENTS = [
'linear-gradient(135deg, #ffd6e8, #fff1a6)',
'linear-gradient(135deg, #ffe7a0, #b7f4d2)',
'linear-gradient(135deg, #c9f7df, #d9d2ff)',
'linear-gradient(135deg, #cfe8ff, #ffd6e8)',
'linear-gradient(135deg, #e3d8ff, #ffd6e8)',
]
export const MOOD_OPTIONS = [
{ label: '开心 😄', value: 'happy' },
{ label: '累了 😮‍💨', value: 'tired' },
{ label: '求玩伴 🐾', value: 'playmate' },
{ label: '晒娃 📸', value: 'show' },
{ label: '散步中 🌿', value: 'walking' },
{ label: '在医院 🏥', value: 'hospital' },
]
export const HOT_HASHTAGS = [
'#上海遛狗',
'#求玩伴',
'#静安区',
'#比熊',
'#柴犬',
'#金毛',
'#布偶猫',
'#英短',
'#宠物友好',
'#周末遛狗',
]
export const BREED_OPTIONS = {
dog: ['比熊', '柴犬', '金毛', '泰迪', '边牧', '法斗', '哈士奇', '萨摩耶', '拉布拉多', '其他'],
cat: ['英短', '布偶', '美短', '加菲', '暹罗', '橘猫', '黑猫', '其他'],
other: ['兔子', '仓鼠', '鹦鹉', '龟', '其他'],
}
export const NEARBY_RADIUS_KM = 5
export const PAGE_SIZE = 10
export const HEARTBEAT_INTERVAL_MS = 60000

View File

@@ -0,0 +1,72 @@
export function formatDistance(meters: number): string {
if (meters < 1000) return `${Math.round(meters)}m`
return `${(meters / 1000).toFixed(1)}km`
}
export function formatTime(isoString: string): string {
const date = new Date(isoString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMin = Math.floor(diffMs / 60000)
const diffHour = Math.floor(diffMin / 60)
const diffDay = Math.floor(diffHour / 24)
if (diffMin < 1) return '刚刚'
if (diffMin < 60) return `${diffMin}分钟前`
if (diffHour < 24) return `${diffHour}小时前`
if (diffDay < 7) return `${diffDay}天前`
const month = date.getMonth() + 1
const day = date.getDate()
return `${month}${day}`
}
export function formatLastSeen(isoString: string): string {
const diffMin = Math.floor((Date.now() - new Date(isoString).getTime()) / 60000)
if (diffMin < 2) return '刚刚在线'
if (diffMin < 60) return `${diffMin}分钟前`
const diffHour = Math.floor(diffMin / 60)
if (diffHour < 24) return `${diffHour}小时前`
return '昨天在线'
}
export function formatCount(n: number): string {
if (n < 1000) return String(n)
if (n < 10000) return `${(n / 1000).toFixed(1)}k`
return `${Math.floor(n / 10000)}w`
}
export function getAvatarGradient(seed: string): string {
const gradients = [
'linear-gradient(135deg, #ffd6e8, #fff1a6)',
'linear-gradient(135deg, #ffe7a0, #b7f4d2)',
'linear-gradient(135deg, #c9f7df, #d9d2ff)',
'linear-gradient(135deg, #cfe8ff, #ffd6e8)',
'linear-gradient(135deg, #e3d8ff, #c9f7df)',
]
let hash = 0
for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) & 0xffffffff
return gradients[Math.abs(hash) % gradients.length]
}
export function getPetEmoji(species: string, breed?: string): string {
if (species === 'cat') return '🐱'
if (species === 'other') return '🐾'
const dogBreedEmoji: Record<string, string> = {
'比熊': '🐩',
'柴犬': '🐕',
'金毛': '🦮',
'哈士奇': '🐺',
'萨摩耶': '🐕‍🦺',
}
return (breed && dogBreedEmoji[breed]) || '🐶'
}
export function calcDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371000
const dLat = (lat2 - lat1) * Math.PI / 180
const dLng = (lng2 - lng1) * Math.PI / 180
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}