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,91 @@
|
||||
const api = require('../../utils/api')
|
||||
const media = require('../../utils/media')
|
||||
const activityUtil = require('../../utils/activity')
|
||||
|
||||
function decorate(item) {
|
||||
const cover = item.cover || item.poster || ''
|
||||
return Object.assign({}, item, { coverUrl: cover ? media.absUrl(cover) : '' })
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
loaded: false,
|
||||
deleting: false,
|
||||
isAdmin: false
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadPage()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadPage().then(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
loadPage() {
|
||||
const profile = api.profile().catch(() => null)
|
||||
return Promise.all([api.pastActivities(), profile])
|
||||
.then((results) => {
|
||||
const isAdmin = !!(results[1] && results[1].isAdmin)
|
||||
this.setData({
|
||||
list: (results[0] || []).map(decorate),
|
||||
loaded: true,
|
||||
isAdmin: isAdmin,
|
||||
deleting: isAdmin && this.data.deleting
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setData({ loaded: true })
|
||||
activityUtil.toast(error)
|
||||
})
|
||||
},
|
||||
|
||||
openDetail(event) {
|
||||
if (this.data.deleting) return
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + event.currentTarget.dataset.id })
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
if (!this.data.isAdmin) return
|
||||
wx.navigateTo({ url: '/pages/past-edit/past-edit' })
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
if (!this.data.isAdmin) return
|
||||
if (!this.data.list.length) {
|
||||
wx.showToast({ title: '还没有可以删除的活动', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ deleting: !this.data.deleting })
|
||||
},
|
||||
|
||||
remove(event) {
|
||||
if (!this.data.isAdmin) return
|
||||
const id = event.currentTarget.dataset.id
|
||||
wx.showModal({
|
||||
title: '删除这场活动?',
|
||||
content: event.currentTarget.dataset.title,
|
||||
confirmColor: '#DC2626',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
api
|
||||
.removePast(id)
|
||||
.then(() => {
|
||||
const list = this.data.list.filter((item) => item.id !== id)
|
||||
this.setData({ list: list, deleting: list.length > 0 && this.data.deleting })
|
||||
})
|
||||
.catch(activityUtil.toast)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
togglePin(event) {
|
||||
if (!this.data.isAdmin) return
|
||||
const dataset = event.currentTarget.dataset
|
||||
api
|
||||
.pinPast(dataset.id, String(dataset.pinned) !== '1')
|
||||
.then(() => this.loadPage())
|
||||
.catch(activityUtil.toast)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "往期活动",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<view class="page">
|
||||
<view wx:if="{{isAdmin}}" class="actions">
|
||||
<view class="btn add" bindtap="onAdd">增加</view>
|
||||
<view class="btn del {{deleting ? 'active' : ''}}" bindtap="onDelete">{{deleting ? '完成' : '删除'}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loaded && !list.length}}" class="empty">还没有往期活动</view>
|
||||
|
||||
<view
|
||||
class="card {{item.pinned ? 'pinned' : ''}}"
|
||||
wx:for="{{list}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="openDetail"
|
||||
>
|
||||
<view class="cover theme-{{item.theme}}">
|
||||
<image wx:if="{{item.coverUrl}}" class="cover-image" src="{{item.coverUrl}}" mode="aspectFill" lazy-load />
|
||||
<view wx:if="{{item.coverUrl}}" class="cover-shade"></view>
|
||||
<text wx:if="{{item.pinned}}" class="pin-badge">置顶</text>
|
||||
<text wx:if="{{item.video}}" class="video-badge">▶ 视频</text>
|
||||
<text class="cover-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="body">
|
||||
<view class="title-row">
|
||||
<text class="name">{{item.title}}</text>
|
||||
<view wx:if="{{isAdmin}}" class="ops">
|
||||
<text
|
||||
wx:if="{{!deleting}}"
|
||||
class="pin-btn"
|
||||
data-id="{{item.id}}"
|
||||
data-pinned="{{item.pinned ? 1 : 0}}"
|
||||
catchtap="togglePin"
|
||||
>{{item.pinned ? '取消置顶' : '置顶'}}</text>
|
||||
<text
|
||||
wx:else
|
||||
class="remove"
|
||||
data-id="{{item.id}}"
|
||||
data-title="{{item.title}}"
|
||||
catchtap="remove"
|
||||
>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="time">{{item.startAt}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,163 @@
|
||||
.page {
|
||||
padding: 28rpx 32rpx 48rpx;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 84rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add {
|
||||
background: #128a80;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.del {
|
||||
background: #ffffff;
|
||||
color: #dc2626;
|
||||
border: 2rpx solid #fecaca;
|
||||
}
|
||||
|
||||
.del.active {
|
||||
background: #fff1f2;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 160rpx;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 28rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.card:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card.pinned {
|
||||
box-shadow: 0 8rpx 24rpx rgba(18, 138, 128, 0.14);
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: relative;
|
||||
height: 272rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-image,
|
||||
.cover-shade {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cover-shade {
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.45) 100%);
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #ffffff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pin-badge,
|
||||
.video-badge {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
height: 44rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
line-height: 44rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pin-badge {
|
||||
left: 20rpx;
|
||||
}
|
||||
|
||||
.video-badge {
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.theme-0 {
|
||||
background: linear-gradient(135deg, #0b6b66 0%, #13857c 100%);
|
||||
}
|
||||
|
||||
.theme-1 {
|
||||
background: linear-gradient(135deg, #1aa39a 0%, #4fd1c5 100%);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 28rpx 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.ops {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #9ca3af;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
color: #128a80;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: #dc2626;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
Reference in New Issue
Block a user