Add 卓米会 mini program and admin server.
Initial commit of the WeChat mini program, Express backend, and admin console. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
const media = require('./media')
|
||||
|
||||
function openMap(activity) {
|
||||
if (!activity || activity.latitude == null || activity.longitude == null) {
|
||||
wx.showToast({ title: '暂无会场坐标', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.openLocation({
|
||||
latitude: Number(activity.latitude),
|
||||
longitude: Number(activity.longitude),
|
||||
name: activity.title,
|
||||
address: activity.address,
|
||||
scale: 16
|
||||
})
|
||||
}
|
||||
|
||||
function goSignup(activity) {
|
||||
if (!activity || !activity.id) return
|
||||
wx.navigateTo({ url: '/pages/signup/signup?id=' + activity.id })
|
||||
}
|
||||
|
||||
// Shared links always land on the detail page and carry the sharer's openid for attribution.
|
||||
function shareMessage(activity, fallbackId) {
|
||||
const item = activity || {}
|
||||
const openid = wx.getStorageSync('openid') || ''
|
||||
const cover = item.poster || item.cover
|
||||
const message = {
|
||||
title: item.title || '卓米会',
|
||||
path: '/pages/detail/detail?id=' + (item.id || fallbackId || '') + '&sharer=' + openid
|
||||
}
|
||||
if (cover) message.imageUrl = media.absUrl(cover)
|
||||
return message
|
||||
}
|
||||
|
||||
function toast(error) {
|
||||
wx.showToast({ title: (error && error.message) || '操作失败', icon: 'none' })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openMap: openMap,
|
||||
goSignup: goSignup,
|
||||
shareMessage: shareMessage,
|
||||
toast: toast
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
const requestUtil = require('./request')
|
||||
const authUtil = require('./auth')
|
||||
|
||||
// Runs a logged-in call; if the stored token has expired, logs in again and retries once.
|
||||
function withLogin(run) {
|
||||
return authUtil
|
||||
.ensureLogin()
|
||||
.then(run)
|
||||
.catch((error) => {
|
||||
if (error.statusCode !== 401) throw error
|
||||
return authUtil.ensureLogin().then(run)
|
||||
})
|
||||
}
|
||||
|
||||
function authed(url, method, data) {
|
||||
return withLogin(() =>
|
||||
requestUtil.request({ url: url, method: method || 'GET', data: data, auth: true })
|
||||
)
|
||||
}
|
||||
|
||||
function uploadTo(url, filePath) {
|
||||
return withLogin(() => requestUtil.upload({ url: url, filePath: filePath }))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
currentActivity: () => requestUtil.request({ url: '/api/activity/current' }),
|
||||
pastActivities: () => requestUtil.request({ url: '/api/activity/past' }),
|
||||
activity: (id) => requestUtil.request({ url: '/api/activity/' + id }),
|
||||
|
||||
profile: () => authed('/api/user/profile'),
|
||||
saveProfile: (data) => authed('/api/user/profile', 'POST', data),
|
||||
uploadAvatar: (filePath) => uploadTo('/api/user/avatar', filePath),
|
||||
|
||||
createPast: (data) => authed('/api/activity/past', 'POST', data),
|
||||
pinPast: (id, pinned) => authed('/api/activity/past/' + id + '/pin', 'POST', { pinned: pinned }),
|
||||
removePast: (id) => authed('/api/activity/past/' + id, 'DELETE'),
|
||||
uploadMedia: (filePath) => uploadTo('/api/upload', filePath),
|
||||
|
||||
signup: (data) => authed('/api/signup', 'POST', data),
|
||||
mySignups: () => authed('/api/signup/mine'),
|
||||
checkin: (data) => authed('/api/checkin/scan', 'POST', data)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
const requestUtil = require('./request')
|
||||
|
||||
let pending = null
|
||||
|
||||
function deviceId() {
|
||||
let id = wx.getStorageSync('deviceId')
|
||||
if (!id) {
|
||||
id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
||||
wx.setStorageSync('deviceId', id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function captureSharer(query) {
|
||||
if (!query || !query.sharer) return
|
||||
const selfId = wx.getStorageSync('openid')
|
||||
if (query.sharer !== selfId) wx.setStorageSync('sharer', query.sharer)
|
||||
}
|
||||
|
||||
function login() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success(res) {
|
||||
if (!res.code) {
|
||||
reject(new Error('微信登录未返回 code'))
|
||||
return
|
||||
}
|
||||
requestUtil
|
||||
.request({
|
||||
url: '/api/login',
|
||||
method: 'POST',
|
||||
data: { code: res.code, deviceId: deviceId() }
|
||||
})
|
||||
.then((data) => {
|
||||
wx.setStorageSync('token', data.token)
|
||||
wx.setStorageSync('openid', data.openid)
|
||||
wx.setStorageSync('loginDemo', !!data.demo)
|
||||
resolve(data)
|
||||
})
|
||||
.catch(reject)
|
||||
},
|
||||
fail() {
|
||||
reject(new Error('微信登录失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ensureLogin() {
|
||||
if (wx.getStorageSync('token')) {
|
||||
return Promise.resolve({
|
||||
token: wx.getStorageSync('token'),
|
||||
openid: wx.getStorageSync('openid')
|
||||
})
|
||||
}
|
||||
if (!pending) {
|
||||
pending = login().finally(() => {
|
||||
pending = null
|
||||
})
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
function sourceSharer() {
|
||||
const sharer = wx.getStorageSync('sharer') || ''
|
||||
const openid = wx.getStorageSync('openid') || ''
|
||||
if (!sharer || sharer === openid) return ''
|
||||
return sharer
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
captureSharer: captureSharer,
|
||||
ensureLogin: ensureLogin,
|
||||
sourceSharer: sourceSharer
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
const CURRENT = {
|
||||
id: '93',
|
||||
title: '2026年卓米第93期卓米会',
|
||||
brandName: '卓米会 ZHUOMI',
|
||||
brandDesc: '专注于品牌增长与企业服务的共创平台,连接资源、方法与同行者。',
|
||||
startAt: '2026-06-18 14:00',
|
||||
address: '上海市浦东新区世纪大道 100 号 卓米中心 3F',
|
||||
latitude: 31.235929,
|
||||
longitude: 121.506321,
|
||||
status: 'current',
|
||||
isCurrent: true,
|
||||
poster: ''
|
||||
}
|
||||
|
||||
module.exports = { CURRENT }
|
||||
@@ -0,0 +1,9 @@
|
||||
const config = require('../config')
|
||||
|
||||
function absUrl(url) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//.test(url)) return url
|
||||
return config.BASE + url
|
||||
}
|
||||
|
||||
module.exports = { absUrl: absUrl }
|
||||
@@ -0,0 +1,75 @@
|
||||
const config = require('../config')
|
||||
|
||||
function networkError(err) {
|
||||
const msg = (err && err.errMsg) || ''
|
||||
if (msg.indexOf('url not in domain list') >= 0 || msg.indexOf('合法域名') >= 0) {
|
||||
return new Error('请勾选:不校验合法域名')
|
||||
}
|
||||
return new Error('网络异常,请确认后端已启动')
|
||||
}
|
||||
|
||||
function apiError(statusCode, body) {
|
||||
const error = new Error((body && body.message) || '请求失败')
|
||||
error.statusCode = statusCode
|
||||
return error
|
||||
}
|
||||
|
||||
function authHeader(header) {
|
||||
const token = wx.getStorageSync('token')
|
||||
if (token) header.Authorization = token
|
||||
return header
|
||||
}
|
||||
|
||||
function handle(statusCode, body, auth, resolve, reject) {
|
||||
if (statusCode === 401 && auth) wx.removeStorageSync('token')
|
||||
if (statusCode >= 200 && statusCode < 300 && body && body.ok !== false) {
|
||||
resolve(body.data)
|
||||
return
|
||||
}
|
||||
reject(apiError(statusCode, body))
|
||||
}
|
||||
|
||||
function request(options) {
|
||||
const auth = !!options.auth
|
||||
const header = { 'content-type': 'application/json' }
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: config.BASE + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: auth ? authHeader(header) : header,
|
||||
success(res) {
|
||||
handle(res.statusCode, res.data || {}, auth, resolve, reject)
|
||||
},
|
||||
fail(err) {
|
||||
reject(networkError(err))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function upload(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: config.BASE + options.url,
|
||||
filePath: options.filePath,
|
||||
name: 'file',
|
||||
header: authHeader({}),
|
||||
success(res) {
|
||||
let body = null
|
||||
try {
|
||||
body = JSON.parse(res.data || '{}')
|
||||
} catch (error) {
|
||||
reject(new Error('上传返回异常'))
|
||||
return
|
||||
}
|
||||
handle(res.statusCode, body, true, resolve, reject)
|
||||
},
|
||||
fail(err) {
|
||||
reject(networkError(err))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { request: request, upload: upload }
|
||||
Reference in New Issue
Block a user