feat(circle): 添加酒友圈社交模块

- 新增酒友圈、圈子详情、发布动态、好友列表、活动创建等页面配置
- 添加酒友圈相关API接口,包括动态、评论、好友、活动等功能
- 实现模拟数据用于前端开发和测试
- 创建评论项、空状态、活动卡片、动态卡片、好友项等组件
- 编写酒友圈模块详细的接口文档,定义数据结构和业务规则
This commit is contained in:
cg
2026-07-20 22:53:04 +08:00
parent 9f5c69e1e5
commit fabbb167cf
18 changed files with 3992 additions and 18 deletions
+430
View File
@@ -0,0 +1,430 @@
<template>
<view :class="themeClass" class="page-container publish-page">
<!-- 顶部导航 -->
<view class="publish-nav" :style="{ paddingTop: navPaddingTop }">
<view class="publish-nav-inner">
<view class="publish-back" @click="goBack">
<text class="publish-back-icon"></text>
</view>
<text class="publish-nav-title">发布动态</text>
<view class="publish-send" :class="{ disabled: !canSubmit }" @click="handlePublish">
<text class="publish-send-text">发布</text>
</view>
</view>
</view>
<scroll-view class="publish-scroll" scroll-y>
<view class="publish-body">
<!-- 关联饮酒记录 -->
<view class="publish-record" v-if="lastRecord">
<view class="record-badge">
<text class="record-badge-icon">🍻</text>
</view>
<view class="record-info">
<text class="record-title">关联最近饮酒记录</text>
<text class="record-desc">{{ recordSummary }}</text>
</view>
<view class="record-check" :class="{ checked: linkRecord }" @click="linkRecord = !linkRecord">
<text class="record-check-icon">{{ linkRecord ? '✓' : '' }}</text>
</view>
</view>
<!-- 文字输入 -->
<textarea
class="publish-textarea"
v-model="text"
placeholder="分享你的饮酒故事..."
placeholder-class="publish-placeholder"
:maxlength="500"
auto-height
/>
<!-- 图片选择 -->
<view class="publish-images">
<view class="publish-img-item" v-for="(img, i) in images" :key="i">
<image class="publish-img" :src="img" mode="aspectFill"></image>
<view class="publish-img-del" @click="removeImage(i)">
<text class="publish-img-del-icon">×</text>
</view>
</view>
<view v-if="images.length < 9" class="publish-img-add" @click="chooseImage">
<text class="publish-img-add-icon">+</text>
<text class="publish-img-add-text">{{ images.length }}/9</text>
</view>
</view>
<!-- 可见范围 -->
<view class="publish-visibility">
<text class="publish-vis-label">谁可以看</text>
<view class="publish-vis-options">
<view
class="publish-vis-opt"
:class="{ active: visibility === 'friends' }"
@click="visibility = 'friends'"
>
<text>所有酒友</text>
</view>
<view
class="publish-vis-opt"
:class="{ active: visibility === 'private' }"
@click="visibility = 'private'"
>
<text>仅自己</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { PublishFeed } from '../../common/api'
import { getRecords } from '../../common/mock-data'
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
data() {
const menuBtn = uni.getMenuButtonBoundingClientRect()
return {
navPaddingTop: menuBtn.top + 'px',
text: '',
images: [],
visibility: 'friends',
linkRecord: true,
lastRecord: null,
publishing: false
}
},
computed: {
canSubmit() {
return (this.text.trim() || this.images.length) && !this.publishing
},
recordSummary() {
if (!this.lastRecord) return ''
const drinks = this.lastRecord.drinks || []
if (!drinks.length) return '戒酒打卡'
return drinks.map(d => `${d.brand || d.product || d.category} ${d.amount}${d.unit}`).join('、')
}
},
onLoad() {
this.loadLastRecord()
},
methods: {
goBack() {
uni.navigateBack()
},
loadLastRecord() {
try {
const records = getRecords()
const drank = records.find(r => r.mode === 'drank')
if (drank) this.lastRecord = drank
} catch (e) { /* ignore */ }
},
chooseImage() {
const remaining = 9 - this.images.length
uni.chooseImage({
count: remaining,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.images = [...this.images, ...res.tempFilePaths].slice(0, 9)
}
})
},
removeImage(index) {
this.images.splice(index, 1)
},
async handlePublish() {
if (!this.canSubmit) return
this.publishing = true
try {
await PublishFeed({
text: this.text,
images: this.images,
recordId: this.linkRecord && this.lastRecord ? this.lastRecord.id : null,
visibility: this.visibility
})
uni.showToast({ title: '发布成功 🍻', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
} catch (e) {
uni.showToast({ title: '发布失败', icon: 'none' })
}
this.publishing = false
}
}
}
</script>
<style lang="scss" scoped>
.publish-page {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.publish-nav {
background: $bg-base;
padding-left: $sp-lg;
padding-right: $sp-lg;
position: relative;
z-index: 10;
}
.publish-nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
}
.publish-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: $bg-card;
&:active { background: $bg-card-alt; }
}
.publish-back-icon {
font-size: $fs-xl;
color: $text-primary;
font-weight: $fw-bold;
}
.publish-nav-title {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.publish-send {
padding: $sp-sm $sp-lg;
background: linear-gradient(135deg, $amber, $amber-deep);
border-radius: $radius-full;
&.disabled { opacity: 0.4; }
&:active { transform: scale(0.95); }
}
.publish-send-text {
font-size: $fs-sm;
font-weight: $fw-bold;
color: $text-on-amber;
}
.publish-scroll {
flex: 1;
height: 0;
}
.publish-body {
padding: $sp-lg;
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-2xl});
}
/* 关联记录 */
.publish-record {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-lg;
background: rgba(232,168,56,0.06);
border: 1rpx solid rgba(232,168,56,0.15);
border-radius: $radius-lg;
margin-bottom: $sp-xl;
}
.record-badge {
width: 72rpx;
height: 72rpx;
border-radius: $radius-md;
background: rgba(232,168,56,0.12);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.record-badge-icon {
font-size: 36rpx;
}
.record-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.record-title {
font-size: $fs-sm;
font-weight: $fw-medium;
color: $text-primary;
}
.record-desc {
font-size: $fs-xs;
color: $text-tertiary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-check {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 2rpx solid var(--border-subtle, rgba(255,255,255,0.10));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.checked {
background: $amber;
border-color: $amber;
}
}
.record-check-icon {
font-size: $fs-sm;
color: $text-on-amber;
font-weight: $fw-bold;
}
/* 文字输入 */
.publish-textarea {
width: 100%;
min-height: 240rpx;
font-size: $fs-base;
color: $text-primary;
line-height: $lh-loose;
padding: $sp-lg;
background: $bg-card;
border-radius: $radius-xl;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
margin-bottom: $sp-xl;
box-sizing: border-box;
}
.publish-placeholder {
color: $text-tertiary;
}
/* 图片 */
.publish-images {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
margin-bottom: $sp-xl;
}
.publish-img-item {
position: relative;
width: calc(33.33% - 11rpx);
height: 200rpx;
}
.publish-img {
width: 100%;
height: 100%;
border-radius: $radius-md;
}
.publish-img-del {
position: absolute;
top: -12rpx;
right: -12rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: $coral;
display: flex;
align-items: center;
justify-content: center;
}
.publish-img-del-icon {
font-size: $fs-sm;
color: #fff;
font-weight: $fw-bold;
}
.publish-img-add {
width: calc(33.33% - 11rpx);
height: 200rpx;
border-radius: $radius-md;
border: 2rpx dashed var(--border-dashed, rgba(255,255,255,0.12));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
&:active {
background: var(--bg-hover, rgba(255,255,255,0.08));
}
}
.publish-img-add-icon {
font-size: $fs-xl;
color: $text-tertiary;
}
.publish-img-add-text {
font-size: $fs-xs;
color: $text-tertiary;
}
/* 可见范围 */
.publish-visibility {
display: flex;
align-items: center;
justify-content: space-between;
padding: $sp-lg;
background: $bg-card;
border-radius: $radius-lg;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
}
.publish-vis-label {
font-size: $fs-base;
color: $text-primary;
font-weight: $fw-medium;
}
.publish-vis-options {
display: flex;
gap: $sp-sm;
}
.publish-vis-opt {
padding: 8rpx 24rpx;
border-radius: $radius-full;
background: $bg-elevated;
border: 1rpx solid var(--border-micro, rgba(255,255,255,0.05));
text {
font-size: $fs-sm;
color: $text-tertiary;
}
&.active {
background: rgba(232,168,56,0.12);
border-color: rgba(232,168,56,0.3);
text {
color: $amber;
font-weight: $fw-medium;
}
}
}
</style>