Files
zhuomi-hui/server/lib/serialize.js
T
Michelle ZanandCursor 0ca2e87bf2 Ship signup sheet, subscribe reminders, cover crop, and theme updates.
Wire real WeChat app config, prefer activity cover over posters, and add admin cover cropping for the home banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-08 13:06:43 +08:00

134 lines
3.9 KiB
JavaScript

const { uid, str, has } = require('./http')
const { isAdmin } = require('./auth')
const STATUSES = ['current', 'past', 'draft']
const DEFAULT_BRAND = '卓米会 ZHUOMI'
function publicActivity(activity, posters) {
const poster = (posters || [])
.filter((item) => item.activityId === activity.id && item.enabled && item.image)
.sort((a, b) => a.sort - b.sort)[0]
return {
id: activity.id,
title: activity.title,
brandName: activity.brandName || DEFAULT_BRAND,
brandDesc: activity.brandDesc || '',
startAt: activity.startAt || '',
address: activity.address || '',
latitude: activity.latitude,
longitude: activity.longitude,
status: activity.status,
isCurrent: !!activity.isCurrent,
pinned: !!activity.pinned,
pinnedAt: activity.pinnedAt || '',
theme: activity.theme || 0,
cover: activity.cover || '',
video: activity.video || '',
// Activity cover wins over linked posters so admin “封面图片” updates show immediately.
poster: activity.cover || (poster && poster.image) || '',
createdAt: activity.createdAt
}
}
function currentActivity(data) {
return (
data.activities.find((item) => item.isCurrent && item.status !== 'draft') ||
data.activities.find((item) => item.status === 'current') ||
null
)
}
function byPinnedThenNewest(a, b) {
const ap = a.pinned ? 1 : 0
const bp = b.pinned ? 1 : 0
if (ap !== bp) return bp - ap
if (ap) return String(b.pinnedAt || '').localeCompare(String(a.pinnedAt || ''))
return String(b.createdAt).localeCompare(String(a.createdAt))
}
function pastActivities(data) {
return data.activities.filter((item) => item.status === 'past').sort(byPinnedThenNewest)
}
function nextPastTheme(data) {
return data.activities.filter((item) => item.status === 'past').length % 2
}
function setPinned(activity, pinned) {
const now = new Date().toISOString()
activity.pinned = !!pinned
activity.pinnedAt = pinned ? now : ''
activity.updatedAt = now
return activity
}
const PROFILE_FIELDS = ['nickName', 'avatarUrl', 'name', 'phone', 'company', 'position']
function publicProfile(user, data) {
const profile = { openid: user.openid }
PROFILE_FIELDS.forEach((key) => {
profile[key] = user[key] || ''
})
profile.isAdmin = isAdmin(user.openid, data)
return profile
}
function coordinate(value) {
if (value === '' || value == null) return null
const num = Number(value)
return Number.isFinite(num) ? num : null
}
function activityPayload(body, prev, data) {
const now = new Date().toISOString()
const status = STATUSES.includes(body.status) ? body.status : 'draft'
const keep = (key, fallback) => (has(body, key) ? str(body[key]) : prev ? prev[key] || fallback : fallback)
let pinned = prev ? !!prev.pinned : false
let pinnedAt = prev ? prev.pinnedAt || '' : ''
if (status !== 'past') {
pinned = false
pinnedAt = ''
} else if (has(body, 'pinned')) {
const requested = body.pinned === true || body.pinned === 'on' || body.pinned === 'true'
if (requested !== pinned) pinnedAt = requested ? now : ''
pinned = requested
}
let theme = Number(body.theme)
if (!has(body, 'theme') || !Number.isFinite(theme)) {
theme = prev ? prev.theme || 0 : status === 'past' ? nextPastTheme(data) : 0
}
return {
id: prev ? prev.id : uid('a'),
title: str(body.title, 60),
brandName: str(body.brandName) || DEFAULT_BRAND,
brandDesc: str(body.brandDesc, 300),
cover: keep('cover', ''),
video: keep('video', ''),
startAt: str(body.startAt, 32),
address: str(body.address, 120),
latitude: coordinate(body.latitude),
longitude: coordinate(body.longitude),
status,
isCurrent: !!body.isCurrent,
pinned,
pinnedAt,
theme,
createdAt: prev ? prev.createdAt : now,
updatedAt: now
}
}
module.exports = {
publicActivity,
currentActivity,
pastActivities,
nextPastTheme,
setPinned,
publicProfile,
activityPayload,
DEFAULT_BRAND
}