初始提交:一场soul的创业实验-永平 网站与小程序

Made-with: Cursor
This commit is contained in:
卡若
2026-03-07 22:58:43 +08:00
commit b7c35a89b0
513 changed files with 89020 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
/**
* Soul创业派对 - 资料编辑完整版comprehensive_profile_editor_v1_1
* 温馨提示、头像、基本信息、核心联系方式、个人故事、互助需求、项目介绍
*
* 接口约定(/api/miniprogram/user/profile
* - GET ?userId= 返回 data: { avatar, nickname, mbti, region, industry, businessScale, position, skills, phone, wechatId, ... },空值统一为 ""
* - POST body普通用户提交基础字段VIP 提交全部字段(含 skills、个人故事、互助需求、项目介绍
*
* 表单展示:普通用户仅展示 温馨提示、头像、昵称、MBTI、地区、行业、业务体量、职位、核心联系方式VIP 展示全部
*/
const app = getApp()
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
Page({
data: {
statusBarHeight: 44,
isVip: false,
avatar: '',
nickname: '',
mbti: '',
mbtiIndex: 0,
region: '',
industry: '',
businessScale: '',
position: '',
skills: '',
phone: '',
wechatId: '',
storyBestMonth: '',
storyAchievement: '',
storyTurning: '',
helpOffer: '',
helpNeed: '',
projectIntro: '',
mbtiOptions: MBTI_OPTIONS,
showMbtiPicker: false,
saving: false,
loading: true,
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
this.loadProfile()
},
async loadProfile() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo?.id) {
this.setData({ loading: false })
wx.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => getApp().goBackOrToHome(), 1500)
return
}
try {
const userId = userInfo.id
const [profileRes, vipRes] = await Promise.all([
app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true }),
app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true }),
])
this.setData({ isVip: vipRes?.data?.isVip || false })
const res = profileRes
if (res?.success && res.data) {
const d = res.data
const v = (k) => (d[k] != null && d[k] !== '') ? String(d[k]) : ''
const mbtiIndex = MBTI_OPTIONS.indexOf(v('mbti')) >= 0 ? MBTI_OPTIONS.indexOf(v('mbti')) : 0
this.setData({
avatar: v('avatar'),
nickname: v('nickname'),
mbti: v('mbti'),
mbtiIndex,
region: v('region'),
industry: v('industry'),
businessScale: v('businessScale'),
position: v('position'),
skills: v('skills'),
phone: v('phone'),
wechatId: v('wechatId') || wx.getStorageSync('user_wechat') || '',
storyBestMonth: v('storyBestMonth'),
storyAchievement: v('storyAchievement'),
storyTurning: v('storyTurning'),
helpOffer: v('helpOffer'),
helpNeed: v('helpNeed'),
projectIntro: v('projectIntro'),
loading: false,
})
} else {
this.setData({ loading: false })
}
} catch (e) {
this.setData({ loading: false })
}
},
goBack() { getApp().goBackOrToHome() },
onNicknameInput(e) { this.setData({ nickname: e.detail.value }) },
onRegionInput(e) { this.setData({ region: e.detail.value }) },
onIndustryInput(e) { this.setData({ industry: e.detail.value }) },
onBusinessScaleInput(e) { this.setData({ businessScale: e.detail.value }) },
onPositionInput(e) { this.setData({ position: e.detail.value }) },
onSkillsInput(e) { this.setData({ skills: e.detail.value }) },
onPhoneInput(e) { this.setData({ phone: e.detail.value }) },
onWechatInput(e) { this.setData({ wechatId: e.detail.value }) },
onStoryBestMonthInput(e) { this.setData({ storyBestMonth: e.detail.value }) },
onStoryAchievementInput(e) { this.setData({ storyAchievement: e.detail.value }) },
onStoryTurningInput(e) { this.setData({ storyTurning: e.detail.value }) },
onHelpOfferInput(e) { this.setData({ helpOffer: e.detail.value }) },
onHelpNeedInput(e) { this.setData({ helpNeed: e.detail.value }) },
onProjectIntroInput(e) { this.setData({ projectIntro: e.detail.value }) },
onMbtiPickerChange(e) {
const i = parseInt(e.detail.value, 10)
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
},
chooseAvatar() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempPath = res.tempFiles[0].tempFilePath
wx.showLoading({ title: '上传中...', mask: true })
try {
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempPath,
name: 'file',
formData: { folder: 'avatars' },
success: (r) => {
try {
const data = JSON.parse(r.data)
if (data.success) resolve(data)
else reject(new Error(data.error || '上传失败'))
} catch { reject(new Error('解析失败')) }
},
fail: reject,
})
})
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
this.setData({ avatar: avatarUrl })
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
})
if (app.globalData.userInfo) {
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
}
},
})
},
async saveProfile() {
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
this.setData({ saving: true })
try {
const s = (v) => (v || '').toString().trim()
const isVip = this.data.isVip
const payload = {
userId,
avatar: s(this.data.avatar),
nickname: s(this.data.nickname),
mbti: s(this.data.mbti),
region: s(this.data.region),
industry: s(this.data.industry),
businessScale: s(this.data.businessScale),
position: s(this.data.position),
phone: s(this.data.phone),
wechatId: s(this.data.wechatId),
}
const showHelp = isVip || this.data.helpOffer || this.data.helpNeed
if (isVip) {
payload.skills = s(this.data.skills)
payload.storyBestMonth = s(this.data.storyBestMonth)
payload.storyAchievement = s(this.data.storyAchievement)
payload.storyTurning = s(this.data.storyTurning)
payload.projectIntro = s(this.data.projectIntro)
}
if (showHelp) {
payload.helpOffer = s(this.data.helpOffer)
payload.helpNeed = s(this.data.helpNeed)
}
if (payload.wechatId) wx.setStorageSync('user_wechat', payload.wechatId)
if (payload.phone) wx.setStorageSync('user_phone', payload.phone)
const hasUpdate = Object.keys(payload).some(k => k !== 'userId' && payload[k] !== '')
if (!hasUpdate) {
wx.showToast({ title: '无变更', icon: 'none' })
this.setData({ saving: false })
return
}
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: payload,
})
wx.showToast({ title: '保存成功', icon: 'success' })
if (app.globalData.userInfo) {
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
if (payload.avatar) app.globalData.userInfo.avatar = payload.avatar
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
setTimeout(() => getApp().goBackOrToHome(), 800)
} catch (e) {
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
}
this.setData({ saving: false })
},
})

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "编辑资料",
"usingComponents": {}
}

View File

@@ -0,0 +1,137 @@
<!-- 资料编辑 - comprehensive_profile_editor_v1_1 | input/textarea 用 view 包裹,配色 enhanced -->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack"><text class="back-icon"></text></view>
<text class="nav-title">编辑资料</text>
<view class="nav-placeholder"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="loading" wx:if="{{loading}}">加载中...</view>
<scroll-view wx:else class="scroll-main" scroll-y>
<!-- 温馨提示 -->
<view class="tip-card">
<text class="tip-icon"></text>
<text class="tip-text">温馨提示:需完善手机号和微信号才能使用提现和找伙伴功能</text>
</view>
<!-- 头像 -->
<view class="avatar-section">
<view class="avatar-wrap" bindtap="chooseAvatar">
<view class="avatar-inner">
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
</view>
<view class="avatar-camera">📷</view>
</view>
<text class="avatar-change">更换头像</text>
</view>
<!-- 基本信息 -->
<view class="section">
<view class="form-row">
<text class="form-label">昵称</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入昵称" value="{{nickname}}" bindinput="onNicknameInput"/></view>
</view>
<view class="form-row form-row-2">
<view class="form-item">
<text class="form-label">MBTI</text>
<picker mode="selector" range="{{mbtiOptions}}" value="{{mbtiIndex}}" bindchange="onMbtiPickerChange">
<view class="form-input-wrap form-picker">{{mbti || '请选择'}}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">地区</text>
<view class="form-input-wrap form-input-suffix">
<input class="form-input-inner" placeholder="例如:杭州·余杭区" value="{{region}}" bindinput="onRegionInput"/>
<text class="form-suffix">📍</text>
</view>
</view>
</view>
<view class="form-row">
<text class="form-label">行业</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:新媒体/电商" value="{{industry}}" bindinput="onIndustryInput"/></view>
</view>
<view class="form-row">
<text class="form-label">业务体量</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如年GMV 5000万+" value="{{businessScale}}" bindinput="onBusinessScaleInput"/></view>
</view>
<view class="form-row">
<text class="form-label">职位</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:创始人/联合创始人" value="{{position}}" bindinput="onPositionInput"/></view>
</view>
<view class="form-row" wx:if="{{isVip}}">
<text class="form-label">我擅长</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如短视频制作、IP打造、私域运营" value="{{skills}}" bindinput="onSkillsInput"/></view>
</view>
</view>
<!-- 核心联系方式 -->
<view class="section">
<view class="section-title">
<text class="section-icon">📞</text>
<text>核心联系方式</text>
</view>
<view class="form-row">
<text class="form-label">手机号</text>
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号" value="{{phone}}" bindinput="onPhoneInput"/></view>
</view>
<view class="form-row">
<text class="form-label">微信号</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入微信号" value="{{wechatId}}" bindinput="onWechatInput"/></view>
</view>
</view>
<!-- 个人故事(仅 VIP 展示) -->
<view class="section" wx:if="{{isVip}}">
<view class="section-title">
<text class="section-icon">💡</text>
<text>个人故事</text>
</view>
<view class="form-row">
<text class="form-label">你最赚钱的一个月做的是什么</text>
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如2021年主导电商大促单月GMV突破500W..." value="{{storyBestMonth}}" bindinput="onStoryBestMonthInput"/></view>
</view>
<view class="form-row">
<text class="form-label">最有成就感的一件事</text>
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如帮助3个素人打造个人IP每月稳定变现5万+" value="{{storyAchievement}}" bindinput="onStoryAchievementInput"/></view>
</view>
<view class="form-row">
<text class="form-label">人生的转折点</text>
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:辞去大厂工作开始做自媒体..." value="{{storyTurning}}" bindinput="onStoryTurningInput"/></view>
</view>
</view>
<!-- 互助需求VIP 或 资源对接已填写时展示) -->
<view class="section" wx:if="{{isVip || helpOffer || helpNeed}}">
<view class="section-title">
<text class="section-icon">🤝</text>
<text>互助需求</text>
</view>
<view class="form-row">
<text class="form-label">我能帮助大家什么</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频脚本、账号冷启动、私域转化" value="{{helpOffer}}" bindinput="onHelpOfferInput"/></view>
</view>
<view class="form-row">
<text class="form-label">我需要什么帮助</text>
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:寻找供应链资源、线下活动合作" value="{{helpNeed}}" bindinput="onHelpNeedInput"/></view>
</view>
</view>
<!-- 项目介绍(仅 VIP 展示) -->
<view class="section" wx:if="{{isVip}}">
<view class="section-title">
<text class="section-icon">🚀</text>
<text>项目介绍</text>
</view>
<view class="form-row">
<view class="form-textarea-wrap form-textarea-lg"><textarea class="form-textarea-inner" placeholder="详细介绍您的项目,让潜在伙伴更好地了解您..." value="{{projectIntro}}" bindinput="onProjectIntroInput"/></view>
</view>
</view>
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
{{saving ? '保存中...' : '保存'}}
</view>
<view class="bottom-space"></view>
</scroll-view>
</view>

View File

@@ -0,0 +1,103 @@
/* 资料编辑 - comprehensive_profile_editor_v1_1 | 配色 enhancedinput/textarea 用 view 包裹 */
.page {
background: #050B14; min-height: 100vh; color: #fff;
width: 100%; box-sizing: border-box; overflow-x: hidden;
}
.nav-bar {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
height: 44px; padding: 0 24rpx;
background: rgba(5,11,20,0.9); backdrop-filter: blur(8rpx);
border-bottom: 1rpx solid rgba(255,255,255,0.08);
}
.nav-back { width: 60rpx; padding: 16rpx 0; }
.back-icon { font-size: 44rpx; color: #5EEAD4; }
.nav-title { font-size: 36rpx; font-weight: 600; }
.nav-placeholder { width: 60rpx; }
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
.scroll-main { width: 100%; height: calc(100vh - 88rpx); padding: 24rpx; box-sizing: border-box; overflow-x: hidden; }
.tip-card {
display: flex; align-items: flex-start; gap: 24rpx;
padding: 32rpx;
background: rgba(94,234,212,0.08); border: 1rpx solid rgba(94,234,212,0.25);
border-radius: 24rpx; margin-bottom: 48rpx;
}
.tip-icon { font-size: 40rpx; color: #5EEAD4; flex-shrink: 0; }
.tip-text { font-size: 26rpx; color: rgba(94,234,212,0.95); line-height: 1.6; }
.avatar-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 48rpx; }
.avatar-wrap {
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%;
border: 4rpx solid #5EEAD4; box-shadow: 0 0 30rpx rgba(94,234,212,0.3);
}
.avatar-inner {
width: 100%; height: 100%; border-radius: 50%; overflow: hidden;
}
.avatar-img { width: 100%; height: 100%; display: block; }
.avatar-placeholder {
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
font-size: 72rpx; font-weight: bold; color: #5EEAD4; background: rgba(94,234,212,0.2);
}
.avatar-camera {
position: absolute; bottom: -8rpx; right: -8rpx;
width: 56rpx; height: 56rpx; background: #5EEAD4; color: #000;
border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx;
border: 4rpx solid #050B14; box-sizing: border-box;
}
.avatar-change { font-size: 28rpx; color: #5EEAD4; font-weight: 500; margin-top: 16rpx; }
.section {
margin-bottom: 48rpx; padding-top: 32rpx;
border-top: 1rpx solid rgba(255,255,255,0.08);
}
.section-title { display: flex; align-items: center; gap: 16rpx; font-size: 32rpx; font-weight: 600; margin-bottom: 32rpx; }
.section-icon { font-size: 40rpx; }
.form-row { margin-bottom: 32rpx; }
.form-row:last-child { margin-bottom: 0; }
.form-row-2 { display: flex; gap: 24rpx; }
.form-row-2 .form-item { flex: 1; min-width: 0; }
.form-label { display: block; font-size: 24rpx; color: #94A3B8; margin-bottom: 12rpx; margin-left: 8rpx; }
/* input/textarea 用 view 包裹padding 写在 view 上 */
.form-input-wrap {
padding: 24rpx 32rpx;
background: #17212F; border: 1rpx solid rgba(255,255,255,0.08);
border-radius: 24rpx;
box-sizing: border-box; min-width: 0; width: 100%;
}
.form-input-suffix { position: relative; padding-right: 64rpx; }
.form-input-suffix .form-suffix {
position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
font-size: 32rpx; color: #94A3B8;
}
.form-input-inner {
width: 100%; max-width: 100%; font-size: 28rpx; color: #fff; background: transparent;
box-sizing: border-box; display: block;
}
.form-picker { color: #fff; }
.form-textarea-wrap {
padding: 24rpx 32rpx;
background: #17212F; border: 1rpx solid rgba(255,255,255,0.08);
border-radius: 24rpx; min-height: 160rpx;
box-sizing: border-box; min-width: 0; width: 100%;
}
.form-textarea-wrap.form-textarea-lg { min-height: 240rpx; }
.form-textarea-inner {
width: 100%; max-width: 100%; min-height: 112rpx; font-size: 28rpx; color: #fff;
background: transparent; line-height: 1.5; box-sizing: border-box; display: block;
}
.form-textarea-lg .form-textarea-inner { min-height: 192rpx; }
.save-btn {
width: 100%; height: 96rpx; line-height: 96rpx; text-align: center;
background: #5EEAD4; color: #050B14; font-size: 36rpx; font-weight: bold;
border-radius: 24rpx; margin-top: 48rpx; margin-bottom: 48rpx;
}
.save-btn[disabled] { opacity: 0.6; }
.bottom-space { height: 120rpx; }