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:
Michelle Zan
2026-09-29 19:01:36 +08:00
co-authored by Cursor
commit cd05113755
67 changed files with 4845 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
const api = require('../../utils/api')
const media = require('../../utils/media')
const activityUtil = require('../../utils/activity')
const FIELDS = ['nickName', 'name', 'phone', 'company', 'position']
Page({
data: {
avatarUrl: '',
avatarPreview: '',
nickName: '',
name: '',
phone: '',
company: '',
position: '',
letter: '微',
uploading: false,
saving: false
},
onLoad() {
api
.profile()
.then((profile) => {
const next = {
avatarUrl: profile.avatarUrl || '',
avatarPreview: media.absUrl(profile.avatarUrl),
letter: (profile.nickName || profile.name || '微').slice(0, 1)
}
FIELDS.forEach((key) => {
next[key] = profile[key] || ''
})
this.setData(next)
})
.catch(activityUtil.toast)
},
pickAvatar() {
if (this.data.uploading) return
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: (res) => {
const file = res.tempFiles && res.tempFiles[0]
if (!file || !file.tempFilePath) return
this.setData({ uploading: true, avatarPreview: file.tempFilePath })
api
.uploadAvatar(file.tempFilePath)
.then((data) => this.setData({ avatarUrl: data.url, uploading: false }))
.catch((error) => {
this.setData({ uploading: false, avatarPreview: media.absUrl(this.data.avatarUrl) })
activityUtil.toast(error)
})
}
})
},
onInput(event) {
const next = {}
next[event.currentTarget.dataset.field] = event.detail.value
this.setData(next)
},
save() {
if (this.data.saving) return
if (this.data.uploading) {
wx.showToast({ title: '头像上传中,请稍候', icon: 'none' })
return
}
const payload = { avatarUrl: this.data.avatarUrl }
FIELDS.forEach((key) => {
payload[key] = (this.data[key] || '').trim()
})
if (!payload.name && !payload.nickName) {
wx.showToast({ title: '请至少填写昵称或姓名', icon: 'none' })
return
}
if (payload.phone && !/^1\d{10}$/.test(payload.phone)) {
wx.showToast({ title: '请填写正确的手机号', icon: 'none' })
return
}
this.setData({ saving: true })
api
.saveProfile(payload)
.then(() => {
wx.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => wx.navigateBack(), 500)
})
.catch((error) => {
this.setData({ saving: false })
activityUtil.toast(error)
})
}
})
+3
View File
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "编辑资料"
}
+24
View File
@@ -0,0 +1,24 @@
<view class="page">
<view class="card avatar-card">
<view class="avatar-btn" bindtap="pickAvatar">
<image wx:if="{{avatarPreview}}" class="avatar" src="{{avatarPreview}}" mode="aspectFill" />
<view wx:else class="avatar letter">{{letter}}</view>
</view>
<text class="hint">{{uploading ? '头像上传中…' : '点击更换头像'}}</text>
</view>
<view class="card">
<text class="label">昵称</text>
<input class="input" placeholder="填写昵称" placeholder-class="placeholder" value="{{nickName}}" maxlength="32" data-field="nickName" bindinput="onInput" />
<text class="label">姓名</text>
<input class="input" placeholder="真实姓名" placeholder-class="placeholder" value="{{name}}" maxlength="20" data-field="name" bindinput="onInput" />
<text class="label">手机号</text>
<input class="input" type="number" placeholder="11 位手机号" placeholder-class="placeholder" value="{{phone}}" maxlength="11" data-field="phone" bindinput="onInput" />
<text class="label">公司</text>
<input class="input" placeholder="公司 / 机构" placeholder-class="placeholder" value="{{company}}" maxlength="40" data-field="company" bindinput="onInput" />
<text class="label">职位</text>
<input class="input" placeholder="职位" placeholder-class="placeholder" value="{{position}}" maxlength="40" data-field="position" bindinput="onInput" />
</view>
<button class="save" loading="{{saving}}" disabled="{{saving}}" bindtap="save">保存资料</button>
</view>
+94
View File
@@ -0,0 +1,94 @@
.page {
min-height: 100vh;
box-sizing: border-box;
padding: 28rpx 32rpx 48rpx;
background: #f3f5f7;
}
.card {
background: #ffffff;
border-radius: 28rpx;
padding: 12rpx 32rpx 28rpx;
margin-bottom: 24rpx;
}
.avatar-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 36rpx 32rpx;
}
.avatar-btn {
width: 128rpx;
height: 128rpx;
border-radius: 64rpx;
overflow: hidden;
}
.avatar,
.letter {
width: 128rpx;
height: 128rpx;
border-radius: 64rpx;
}
.letter {
background: #128a80;
color: #ffffff;
font-size: 44rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.hint {
margin-top: 16rpx;
color: #9ca3af;
font-size: 24rpx;
}
.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;
}
.save {
width: 100% !important;
height: 96rpx;
margin: 12rpx 0 0;
padding: 0;
border-radius: 20rpx;
background: #128a80;
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
line-height: 96rpx;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.save::after {
border: none;
}
.save[disabled] {
opacity: 0.7;
}