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,121 @@
|
||||
const api = require('../../utils/api')
|
||||
const media = require('../../utils/media')
|
||||
const activityUtil = require('../../utils/activity')
|
||||
|
||||
function chooseOne(mediaType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: [mediaType],
|
||||
sourceType: ['album', 'camera'],
|
||||
maxDuration: 60,
|
||||
success: (res) => {
|
||||
const file = res.tempFiles && res.tempFiles[0]
|
||||
if (file && file.tempFilePath) resolve(file.tempFilePath)
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function uploadWithLoading(filePath) {
|
||||
wx.showLoading({ title: '上传中', mask: true })
|
||||
return api.uploadMedia(filePath).then(
|
||||
(data) => {
|
||||
wx.hideLoading()
|
||||
return data
|
||||
},
|
||||
(error) => {
|
||||
wx.hideLoading()
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
title: '',
|
||||
startAt: '',
|
||||
pinned: false,
|
||||
cover: '',
|
||||
coverPreview: '',
|
||||
video: '',
|
||||
saving: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
api
|
||||
.profile()
|
||||
.then((profile) => {
|
||||
if (profile && profile.isAdmin) return
|
||||
wx.showToast({ title: '仅管理员可操作', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
})
|
||||
.catch((error) => {
|
||||
activityUtil.toast(error)
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
})
|
||||
},
|
||||
|
||||
onTitle(event) {
|
||||
this.setData({ title: event.detail.value })
|
||||
},
|
||||
|
||||
onTime(event) {
|
||||
this.setData({ startAt: event.detail.value })
|
||||
},
|
||||
|
||||
togglePinned() {
|
||||
this.setData({ pinned: !this.data.pinned })
|
||||
},
|
||||
|
||||
pickCover() {
|
||||
chooseOne('image')
|
||||
.then(uploadWithLoading)
|
||||
.then((data) => this.setData({ cover: data.url, coverPreview: media.absUrl(data.url) }))
|
||||
.catch((error) => {
|
||||
if (error && error.message) activityUtil.toast(error)
|
||||
})
|
||||
},
|
||||
|
||||
clearCover() {
|
||||
this.setData({ cover: '', coverPreview: '' })
|
||||
},
|
||||
|
||||
pickVideo() {
|
||||
chooseOne('video')
|
||||
.then(uploadWithLoading)
|
||||
.then((data) => this.setData({ video: data.url }))
|
||||
.catch((error) => {
|
||||
if (error && error.message) activityUtil.toast(error)
|
||||
})
|
||||
},
|
||||
|
||||
clearVideo() {
|
||||
this.setData({ video: '' })
|
||||
},
|
||||
|
||||
save() {
|
||||
if (this.data.saving) return
|
||||
const title = this.data.title.trim()
|
||||
const startAt = this.data.startAt.trim()
|
||||
if (!title || !startAt) {
|
||||
wx.showToast({ title: '请填写名称和时间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ saving: true })
|
||||
api
|
||||
.createPast({
|
||||
title: title,
|
||||
startAt: startAt,
|
||||
pinned: this.data.pinned,
|
||||
cover: this.data.cover,
|
||||
video: this.data.video
|
||||
})
|
||||
.then(() => wx.navigateBack())
|
||||
.catch((error) => {
|
||||
this.setData({ saving: false })
|
||||
activityUtil.toast(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "新增往期"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<view class="page">
|
||||
<view class="card">
|
||||
<text class="label">活动名称</text>
|
||||
<input
|
||||
class="input"
|
||||
placeholder="例如:卓米会城市沙龙"
|
||||
placeholder-class="placeholder"
|
||||
value="{{title}}"
|
||||
maxlength="40"
|
||||
bindinput="onTitle"
|
||||
/>
|
||||
<text class="label">活动时间</text>
|
||||
<input
|
||||
class="input"
|
||||
placeholder="例如:2026-05-22 19:00"
|
||||
placeholder-class="placeholder"
|
||||
value="{{startAt}}"
|
||||
maxlength="32"
|
||||
bindinput="onTime"
|
||||
/>
|
||||
|
||||
<text class="label">封面图片</text>
|
||||
<view class="media-row">
|
||||
<image wx:if="{{cover}}" class="preview" src="{{coverPreview}}" mode="aspectFill" />
|
||||
<view class="media-btn" bindtap="pickCover">{{cover ? '更换图片' : '选择图片'}}</view>
|
||||
<view wx:if="{{cover}}" class="media-btn ghost" bindtap="clearCover">清除</view>
|
||||
</view>
|
||||
|
||||
<text class="label">活动视频</text>
|
||||
<view class="media-row">
|
||||
<text class="media-name">{{video ? '已选择视频' : '未选择'}}</text>
|
||||
<view class="media-btn" bindtap="pickVideo">{{video ? '更换视频' : '选择视频'}}</view>
|
||||
<view wx:if="{{video}}" class="media-btn ghost" bindtap="clearVideo">清除</view>
|
||||
</view>
|
||||
|
||||
<view class="pin-row" bindtap="togglePinned">
|
||||
<view class="check {{pinned ? 'on' : ''}}"></view>
|
||||
<text>创建后置顶(可同时置顶多个)</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="save {{saving ? 'busy' : ''}}" bindtap="save">{{saving ? '保存中…' : '保存'}}</view>
|
||||
</view>
|
||||
@@ -0,0 +1,119 @@
|
||||
.page {
|
||||
padding: 28rpx 32rpx 48rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 12rpx 32rpx 28rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-top: 28rpx;
|
||||
color: #6b7280;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 88rpx;
|
||||
margin-top: 8rpx;
|
||||
border-bottom: 1rpx solid #eef0f3;
|
||||
font-size: 32rpx;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #c4c9d1;
|
||||
}
|
||||
|
||||
.pin-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-top: 32rpx;
|
||||
padding-bottom: 8rpx;
|
||||
color: #374151;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.media-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #eef0f3;
|
||||
}
|
||||
|
||||
.media-name {
|
||||
color: #6b7280;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.media-btn {
|
||||
height: 64rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #128a80;
|
||||
color: #ffffff;
|
||||
font-size: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-btn.ghost {
|
||||
background: #ffffff;
|
||||
color: #6b7280;
|
||||
border: 2rpx solid #e5e7eb;
|
||||
}
|
||||
|
||||
.check {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #cbd5e1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.check.on {
|
||||
background: #128a80;
|
||||
border-color: #128a80;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.check.on::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 4rpx;
|
||||
width: 10rpx;
|
||||
height: 18rpx;
|
||||
border: 4rpx solid #ffffff;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.save {
|
||||
height: 96rpx;
|
||||
margin-top: 36rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #128a80;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.save.busy {
|
||||
opacity: 0.7;
|
||||
}
|
||||
Reference in New Issue
Block a user