feat(circle): 添加酒友圈社交模块
- 新增酒友圈、圈子详情、发布动态、好友列表、活动创建等页面配置 - 添加酒友圈相关API接口,包括动态、评论、好友、活动等功能 - 实现模拟数据用于前端开发和测试 - 创建评论项、空状态、活动卡片、动态卡片、好友项等组件 - 编写酒友圈模块详细的接口文档,定义数据结构和业务规则
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container detail-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="detail-nav" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="detail-nav-inner">
|
||||
<view class="detail-back" @click="goBack">
|
||||
<text class="detail-back-icon">‹</text>
|
||||
</view>
|
||||
<text class="detail-nav-title">动态详情</text>
|
||||
<view class="detail-nav-ph"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态内容 -->
|
||||
<scroll-view class="detail-scroll" scroll-y>
|
||||
<view class="detail-body">
|
||||
<FeedCard
|
||||
v-if="feed"
|
||||
:feed="feed"
|
||||
@like="handleLike"
|
||||
/>
|
||||
|
||||
<!-- 评论区 -->
|
||||
<view class="comments-section">
|
||||
<view class="comments-header">
|
||||
<text class="comments-title">评论</text>
|
||||
<text class="comments-count">{{ comments.length }}</text>
|
||||
</view>
|
||||
<CommentItem
|
||||
v-for="c in comments"
|
||||
:key="c.id"
|
||||
:comment="c"
|
||||
/>
|
||||
<EmptyState
|
||||
v-if="!comments.length"
|
||||
icon="💬"
|
||||
title="暂无评论"
|
||||
desc="来说点什么吧"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部评论输入 -->
|
||||
<view class="comment-bar">
|
||||
<input
|
||||
class="comment-input"
|
||||
v-model="commentText"
|
||||
placeholder="写评论..."
|
||||
placeholder-class="comment-placeholder"
|
||||
confirm-type="send"
|
||||
@confirm="submitComment"
|
||||
/>
|
||||
<view class="comment-send" :class="{ disabled: !commentText.trim() }" @click="submitComment">
|
||||
<text class="comment-send-text">发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FeedCard from '../../components/FeedCard.vue'
|
||||
import CommentItem from '../../components/CommentItem.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import { GetCircleFeeds, GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api'
|
||||
import { MOCK_FEEDS } from '../../common/mock-data'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
components: { FeedCard, CommentItem, EmptyState },
|
||||
data() {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
return {
|
||||
navPaddingTop: menuBtn.top + 'px',
|
||||
feedId: '',
|
||||
feed: null,
|
||||
comments: [],
|
||||
commentText: ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.feedId = options.id || ''
|
||||
this.loadFeed()
|
||||
this.loadComments()
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadFeed() {
|
||||
// 从 Mock 数据中查找对应动态
|
||||
this.feed = MOCK_FEEDS.find(f => f.id === this.feedId) || MOCK_FEEDS[0]
|
||||
},
|
||||
async loadComments() {
|
||||
try {
|
||||
const res = await GetFeedComments({ feedId: this.feedId })
|
||||
this.comments = res.data.list
|
||||
} catch (e) {
|
||||
console.warn('加载评论失败', e)
|
||||
}
|
||||
},
|
||||
async handleLike(feed) {
|
||||
feed.liked = !feed.liked
|
||||
feed.likes += feed.liked ? 1 : -1
|
||||
try {
|
||||
if (feed.liked) await LikeFeed({ feedId: feed.id })
|
||||
else await UnlikeFeed({ feedId: feed.id })
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
async submitComment() {
|
||||
const text = this.commentText.trim()
|
||||
if (!text) return
|
||||
try {
|
||||
await AddComment({ feedId: this.feedId, content: text })
|
||||
this.comments.push({
|
||||
id: 'c_' + Date.now(),
|
||||
userId: 'me',
|
||||
nickname: '我',
|
||||
avatar: '',
|
||||
content: text,
|
||||
time: '刚刚'
|
||||
})
|
||||
this.commentText = ''
|
||||
if (this.feed) this.feed.comments = (this.feed.comments || 0) + 1
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-nav {
|
||||
background: $bg-base;
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.detail-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.detail-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
|
||||
&:active {
|
||||
background: $bg-card-alt;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-back-icon {
|
||||
font-size: $fs-xl;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.detail-nav-title {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.detail-nav-ph {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
.detail-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
padding: $sp-md $sp-lg;
|
||||
padding-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
margin-top: $sp-lg;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-xl;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
padding: $sp-xl;
|
||||
}
|
||||
|
||||
.comments-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.comments-count {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 底部评论栏 */
|
||||
.comment-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
padding: $sp-md $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-md});
|
||||
background: $bg-card;
|
||||
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
padding: 0 $sp-lg;
|
||||
background: $bg-elevated;
|
||||
border-radius: $radius-full;
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.comment-placeholder {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.comment-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);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-send-text {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container circle-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="circle-header" :style="{ paddingTop: headerPaddingTop }">
|
||||
<text class="circle-title">酒友圈</text>
|
||||
<!-- Tab 切换 -->
|
||||
<view class="circle-tabs">
|
||||
<view
|
||||
v-for="(tab, i) in tabs"
|
||||
:key="i"
|
||||
class="circle-tab"
|
||||
:class="{ active: currentTab === i }"
|
||||
@click="switchTab(i)"
|
||||
>
|
||||
<text class="circle-tab-text">{{ tab }}</text>
|
||||
<view v-if="currentTab === i" class="circle-tab-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态 Tab -->
|
||||
<scroll-view
|
||||
v-if="currentTab === 0"
|
||||
class="circle-scroll"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view class="circle-content">
|
||||
<FeedCard
|
||||
v-for="feed in feeds"
|
||||
:key="feed.id"
|
||||
:feed="feed"
|
||||
@like="handleLike"
|
||||
@comment="goDetail"
|
||||
@tap="goDetail"
|
||||
/>
|
||||
<view v-if="loading" class="circle-loading">
|
||||
<text class="circle-loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="!hasMore && feeds.length" class="circle-nomore">
|
||||
<text class="circle-nomore-text">— 没有更多了 —</text>
|
||||
</view>
|
||||
<EmptyState
|
||||
v-if="!feeds.length && !loading"
|
||||
icon="🍻"
|
||||
title="还没有动态"
|
||||
desc="发布你的第一条饮酒动态吧"
|
||||
actionText="发动态"
|
||||
@action="goPublish"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 酒友 Tab -->
|
||||
<scroll-view v-if="currentTab === 1" class="circle-scroll" scroll-y>
|
||||
<view class="circle-content">
|
||||
<!-- 好友请求提醒 -->
|
||||
<view v-if="friendRequests.length" class="request-banner" @click="goFriends">
|
||||
<text class="request-icon">🔔</text>
|
||||
<text class="request-text">{{ friendRequests.length }} 条新的好友请求</text>
|
||||
<text class="request-arrow">›</text>
|
||||
</view>
|
||||
<FriendItem
|
||||
v-for="f in friends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
@tap="goFriends"
|
||||
/>
|
||||
<EmptyState
|
||||
v-if="!friends.length"
|
||||
icon="👥"
|
||||
title="还没有酒友"
|
||||
desc="邀请好友一起记录饮酒生活"
|
||||
actionText="邀请好友"
|
||||
@action="goFriends"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 酒局 Tab -->
|
||||
<scroll-view v-if="currentTab === 2" class="circle-scroll" scroll-y>
|
||||
<view class="circle-content">
|
||||
<EventCard
|
||||
v-for="evt in events"
|
||||
:key="evt.id"
|
||||
:event="evt"
|
||||
@tap="goEventDetail"
|
||||
/>
|
||||
<EmptyState
|
||||
v-if="!events.length"
|
||||
icon="🎉"
|
||||
title="暂无酒局"
|
||||
desc="发起一场酒局,约上酒友一起喝"
|
||||
actionText="发起酒局"
|
||||
@action="goCreateEvent"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- FAB 发布按钮 -->
|
||||
<view class="fab" @click="handleFab">
|
||||
<text class="fab-icon">{{ currentTab === 2 ? '🎉' : '✏️' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FeedCard from '../../components/FeedCard.vue'
|
||||
import FriendItem from '../../components/FriendItem.vue'
|
||||
import EventCard from '../../components/EventCard.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import { GetCircleFeeds, LikeFeed, UnlikeFeed, GetFriends, GetFriendRequests, GetEvents } from '../../common/api'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
components: { FeedCard, FriendItem, EventCard, EmptyState },
|
||||
data() {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
return {
|
||||
tabs: ['动态', '酒友', '酒局'],
|
||||
currentTab: 0,
|
||||
headerPaddingTop: (menuBtn.top + 8) + 'px',
|
||||
// 动态
|
||||
feeds: [],
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
// 酒友
|
||||
friends: [],
|
||||
friendRequests: [],
|
||||
// 酒局
|
||||
events: []
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadFeeds(true)
|
||||
this.loadFriends()
|
||||
this.loadEvents()
|
||||
},
|
||||
methods: {
|
||||
switchTab(i) {
|
||||
this.currentTab = i
|
||||
},
|
||||
// === 动态 ===
|
||||
async loadFeeds(reset = false) {
|
||||
if (this.loading) return
|
||||
if (reset) {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await GetCircleFeeds({ page: this.page, pageSize: 10 })
|
||||
const data = res.data
|
||||
if (reset) {
|
||||
this.feeds = data.list
|
||||
} else {
|
||||
this.feeds = [...this.feeds, ...data.list]
|
||||
}
|
||||
this.hasMore = data.hasMore
|
||||
this.page++
|
||||
} catch (e) {
|
||||
console.warn('加载动态失败', e)
|
||||
}
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
},
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadFeeds(true)
|
||||
},
|
||||
loadMore() {
|
||||
if (this.hasMore && !this.loading) {
|
||||
this.loadFeeds(false)
|
||||
}
|
||||
},
|
||||
async handleLike(feed) {
|
||||
feed.liked = !feed.liked
|
||||
feed.likes += feed.liked ? 1 : -1
|
||||
try {
|
||||
if (feed.liked) {
|
||||
await LikeFeed({ feedId: feed.id })
|
||||
} else {
|
||||
await UnlikeFeed({ feedId: feed.id })
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
goDetail(feed) {
|
||||
uni.navigateTo({ url: `/pages/circle-detail/circle-detail?id=${feed.id}` })
|
||||
},
|
||||
goPublish() {
|
||||
uni.navigateTo({ url: '/pages/circle-publish/circle-publish' })
|
||||
},
|
||||
// === 酒友 ===
|
||||
async loadFriends() {
|
||||
try {
|
||||
const [friendsRes, reqRes] = await Promise.all([
|
||||
GetFriends({}),
|
||||
GetFriendRequests()
|
||||
])
|
||||
this.friends = friendsRes.data.list
|
||||
this.friendRequests = reqRes.data.list
|
||||
} catch (e) {
|
||||
console.warn('加载酒友失败', e)
|
||||
}
|
||||
},
|
||||
goFriends() {
|
||||
uni.navigateTo({ url: '/pages/friends/friends' })
|
||||
},
|
||||
// === 酒局 ===
|
||||
async loadEvents() {
|
||||
try {
|
||||
const res = await GetEvents({})
|
||||
this.events = res.data.list
|
||||
} catch (e) {
|
||||
console.warn('加载酒局失败', e)
|
||||
}
|
||||
},
|
||||
goEventDetail(evt) {
|
||||
uni.navigateTo({ url: `/pages/event-detail/event-detail?id=${evt.id}` })
|
||||
},
|
||||
goCreateEvent() {
|
||||
uni.navigateTo({ url: '/pages/event-create/event-create' })
|
||||
},
|
||||
// === FAB ===
|
||||
handleFab() {
|
||||
if (this.currentTab === 2) {
|
||||
this.goCreateEvent()
|
||||
} else {
|
||||
this.goPublish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.circle-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.circle-header {
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
padding-bottom: $sp-md;
|
||||
background: $bg-base;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.circle-title {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.circle-tabs {
|
||||
display: flex;
|
||||
gap: $sp-xl;
|
||||
}
|
||||
|
||||
.circle-tab {
|
||||
position: relative;
|
||||
padding-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.circle-tab-text {
|
||||
font-size: $fs-base;
|
||||
color: $text-tertiary;
|
||||
font-weight: $fw-medium;
|
||||
transition: color $duration-fast $ease-out;
|
||||
|
||||
.circle-tab.active & {
|
||||
color: $text-primary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-tab-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 3rpx;
|
||||
background: $amber;
|
||||
}
|
||||
|
||||
.circle-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.circle-content {
|
||||
padding: $sp-md $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
|
||||
}
|
||||
|
||||
.circle-loading,
|
||||
.circle-nomore {
|
||||
text-align: center;
|
||||
padding: $sp-xl 0;
|
||||
}
|
||||
|
||||
.circle-loading-text,
|
||||
.circle-nomore-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 好友请求横幅 */
|
||||
.request-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
padding: $sp-lg;
|
||||
background: rgba(232,168,56,0.08);
|
||||
border: 1rpx solid rgba(232,168,56,0.2);
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $sp-md;
|
||||
|
||||
&:active {
|
||||
background: rgba(232,168,56,0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.request-icon {
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.request-text {
|
||||
flex: 1;
|
||||
font-size: $fs-sm;
|
||||
color: $amber;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
|
||||
.request-arrow {
|
||||
font-size: $fs-xl;
|
||||
color: $amber;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* FAB */
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: $sp-xl;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 180rpx);
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-amber;
|
||||
z-index: 50;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container create-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="create-nav" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="create-nav-inner">
|
||||
<view class="create-back" @click="goBack">
|
||||
<text class="create-back-icon">‹</text>
|
||||
</view>
|
||||
<text class="create-nav-title">发起酒局</text>
|
||||
<view class="create-nav-ph"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="create-scroll" scroll-y>
|
||||
<view class="create-body">
|
||||
<!-- 标题 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">酒局名称</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.title"
|
||||
placeholder="例如:周五夜·老地方火锅局"
|
||||
placeholder-class="form-placeholder"
|
||||
:maxlength="30"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 时间 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">时间</text>
|
||||
<picker mode="date" :value="form.date" @change="onDateChange">
|
||||
<view class="form-picker">
|
||||
<text :class="form.date ? 'form-picker-text' : 'form-picker-ph'">{{ form.date || '选择日期' }}</text>
|
||||
<text class="form-picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
<picker mode="time" :value="form.time" @change="onTimeChange">
|
||||
<view class="form-picker" style="margin-top: 16rpx;">
|
||||
<text :class="form.time ? 'form-picker-text' : 'form-picker-ph'">{{ form.time || '选择时间' }}</text>
|
||||
<text class="form-picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 地点 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">地点</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.location"
|
||||
placeholder="输入聚会地点"
|
||||
placeholder-class="form-placeholder"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 人数 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">人数上限</text>
|
||||
<view class="form-stepper">
|
||||
<view class="stepper-btn" @click="changePeople(-1)">
|
||||
<text class="stepper-btn-text">−</text>
|
||||
</view>
|
||||
<text class="stepper-value">{{ form.maxPeople }}</text>
|
||||
<view class="stepper-btn" @click="changePeople(1)">
|
||||
<text class="stepper-btn-text">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">备注(选填)</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="form.note"
|
||||
placeholder="例如:自带酒水,AA制餐费"
|
||||
placeholder-class="form-placeholder"
|
||||
:maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 发布按钮 -->
|
||||
<button class="create-submit" :class="{ disabled: !canSubmit }" @click="handleSubmit">
|
||||
发布酒局
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CreateEvent } from '../../common/api'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
return {
|
||||
navPaddingTop: menuBtn.top + 'px',
|
||||
form: {
|
||||
title: '',
|
||||
date: '',
|
||||
time: '',
|
||||
location: '',
|
||||
maxPeople: 6,
|
||||
note: ''
|
||||
},
|
||||
submitting: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canSubmit() {
|
||||
return this.form.title.trim() && this.form.date && this.form.time && this.form.location.trim() && !this.submitting
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
onDateChange(e) {
|
||||
this.form.date = e.detail.value
|
||||
},
|
||||
onTimeChange(e) {
|
||||
this.form.time = e.detail.value
|
||||
},
|
||||
changePeople(delta) {
|
||||
const val = this.form.maxPeople + delta
|
||||
if (val >= 2 && val <= 50) {
|
||||
this.form.maxPeople = val
|
||||
}
|
||||
},
|
||||
async handleSubmit() {
|
||||
if (!this.canSubmit) return
|
||||
this.submitting = true
|
||||
try {
|
||||
await CreateEvent({
|
||||
title: this.form.title,
|
||||
time: `${this.form.date} ${this.form.time}`,
|
||||
location: this.form.location,
|
||||
maxPeople: this.form.maxPeople,
|
||||
note: this.form.note
|
||||
})
|
||||
uni.showToast({ title: '酒局已发布 🎉', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 800)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发布失败', icon: 'none' })
|
||||
}
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.create-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-nav {
|
||||
background: $bg-base;
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
}
|
||||
|
||||
.create-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.create-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
|
||||
&:active { background: $bg-card-alt; }
|
||||
}
|
||||
|
||||
.create-back-icon {
|
||||
font-size: $fs-xl;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.create-nav-title {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.create-nav-ph {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
.create-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.create-body {
|
||||
padding: $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-2xl});
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-medium;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 $sp-lg;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-placeholder {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 $sp-lg;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.form-picker-text {
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.form-picker-ph {
|
||||
font-size: $fs-base;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.form-picker-arrow {
|
||||
font-size: $fs-xl;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
padding: $sp-lg;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
line-height: $lh-normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 人数步进器 */
|
||||
.form-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-xl;
|
||||
}
|
||||
|
||||
.stepper-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active { background: $bg-card-alt; }
|
||||
}
|
||||
|
||||
.stepper-btn-text {
|
||||
font-size: $fs-xl;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.stepper-value {
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-black;
|
||||
color: $amber;
|
||||
min-width: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 提交 */
|
||||
.create-submit {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
color: $text-on-amber;
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
border-radius: $radius-full;
|
||||
border: none;
|
||||
margin-top: $sp-xl;
|
||||
|
||||
&::after { border: none; }
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container evt-detail-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="evt-nav" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="evt-nav-inner">
|
||||
<view class="evt-back" @click="goBack">
|
||||
<text class="evt-back-icon">‹</text>
|
||||
</view>
|
||||
<text class="evt-nav-title">酒局详情</text>
|
||||
<view class="evt-nav-ph"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="evt-scroll" scroll-y>
|
||||
<view class="evt-body" v-if="event">
|
||||
<!-- 状态 -->
|
||||
<view class="evt-status" :class="'status-' + event.status">
|
||||
<text class="evt-status-text">{{ statusLabel }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<text class="evt-title">{{ event.title }}</text>
|
||||
|
||||
<!-- 信息卡片 -->
|
||||
<view class="evt-info-card">
|
||||
<view class="evt-info-row">
|
||||
<text class="evt-info-icon">🕐</text>
|
||||
<view class="evt-info-content">
|
||||
<text class="evt-info-label">时间</text>
|
||||
<text class="evt-info-value">{{ event.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="evt-info-row">
|
||||
<text class="evt-info-icon">📍</text>
|
||||
<view class="evt-info-content">
|
||||
<text class="evt-info-label">地点</text>
|
||||
<text class="evt-info-value">{{ event.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="evt-info-row">
|
||||
<text class="evt-info-icon">👥</text>
|
||||
<view class="evt-info-content">
|
||||
<text class="evt-info-label">人数</text>
|
||||
<text class="evt-info-value">{{ event.joined }}/{{ event.maxPeople }} 人已报名</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="evt-info-row" v-if="event.note">
|
||||
<text class="evt-info-icon">📝</text>
|
||||
<view class="evt-info-content">
|
||||
<text class="evt-info-label">备注</text>
|
||||
<text class="evt-info-value">{{ event.note }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发起人 -->
|
||||
<view class="evt-organizer">
|
||||
<text class="evt-section-label">发起人</text>
|
||||
<view class="evt-org-row">
|
||||
<view class="evt-org-avatar">
|
||||
<text class="evt-org-avatar-text">{{ event.organizer ? event.organizer.nickname[0] : '酒' }}</text>
|
||||
</view>
|
||||
<text class="evt-org-name">{{ event.organizer ? event.organizer.nickname : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 参与者 -->
|
||||
<view class="evt-participants" v-if="event.participants && event.participants.length">
|
||||
<text class="evt-section-label">已报名 ({{ event.participants.length }})</text>
|
||||
<view class="evt-people-grid">
|
||||
<view class="evt-person" v-for="p in event.participants" :key="p.id">
|
||||
<view class="evt-person-avatar">
|
||||
<text class="evt-person-text">{{ p.nickname[0] }}</text>
|
||||
</view>
|
||||
<text class="evt-person-name">{{ p.nickname }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="evt-action-bar" v-if="event">
|
||||
<button
|
||||
v-if="event.status === 'open' && !event.isJoined && !event.isOrganizer"
|
||||
class="evt-btn evt-btn-primary"
|
||||
@click="handleJoin"
|
||||
>
|
||||
报名参加
|
||||
</button>
|
||||
<button
|
||||
v-if="event.status === 'open' && event.isJoined && !event.isOrganizer"
|
||||
class="evt-btn evt-btn-ghost"
|
||||
@click="handleQuit"
|
||||
>
|
||||
取消报名
|
||||
</button>
|
||||
<button
|
||||
v-if="event.status === 'ongoing' && event.isJoined"
|
||||
class="evt-btn evt-btn-primary"
|
||||
@click="handleCheckIn"
|
||||
>
|
||||
到场签到
|
||||
</button>
|
||||
<view v-if="event.status === 'ended'" class="evt-ended-tip">
|
||||
<text class="evt-ended-text">酒局已结束</text>
|
||||
</view>
|
||||
<view v-if="event.isOrganizer" class="evt-ended-tip">
|
||||
<text class="evt-ended-text">你是发起人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GetEventDetail, JoinEvent, QuitEvent, CheckInEvent } from '../../common/api'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
return {
|
||||
navPaddingTop: menuBtn.top + 'px',
|
||||
eventId: '',
|
||||
event: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusLabel() {
|
||||
if (!this.event) return ''
|
||||
const map = { open: '报名中', ongoing: '进行中', ended: '已结束' }
|
||||
return map[this.event.status] || '报名中'
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.eventId = options.id || ''
|
||||
this.loadDetail()
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadDetail() {
|
||||
try {
|
||||
const res = await GetEventDetail({ eventId: this.eventId })
|
||||
this.event = res.data
|
||||
} catch (e) {
|
||||
console.warn('加载酒局详情失败', e)
|
||||
}
|
||||
},
|
||||
async handleJoin() {
|
||||
try {
|
||||
await JoinEvent({ eventId: this.eventId })
|
||||
this.event.isJoined = true
|
||||
this.event.joined++
|
||||
uni.showToast({ title: '报名成功 🎉', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '报名失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async handleQuit() {
|
||||
uni.showModal({
|
||||
title: '取消报名',
|
||||
content: '确定要退出这个酒局吗?',
|
||||
confirmText: '退出',
|
||||
confirmColor: '#FF6B6B',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await QuitEvent({ eventId: this.eventId })
|
||||
this.event.isJoined = false
|
||||
this.event.joined--
|
||||
uni.showToast({ title: '已取消报名', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async handleCheckIn() {
|
||||
try {
|
||||
await CheckInEvent({ eventId: this.eventId })
|
||||
uni.showToast({ title: '签到成功 🍻', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '签到失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.evt-detail-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.evt-nav {
|
||||
background: $bg-base;
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
}
|
||||
|
||||
.evt-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.evt-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
|
||||
&:active { background: $bg-card-alt; }
|
||||
}
|
||||
|
||||
.evt-back-icon {
|
||||
font-size: $fs-xl;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.evt-nav-title {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.evt-nav-ph {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
.evt-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.evt-body {
|
||||
padding: $sp-lg;
|
||||
padding-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.evt-status {
|
||||
display: inline-flex;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: $radius-full;
|
||||
margin-bottom: $sp-lg;
|
||||
|
||||
&.status-open { background: rgba(94,198,160,0.12); }
|
||||
&.status-ongoing { background: rgba(232,168,56,0.12); }
|
||||
&.status-ended { background: $bg-elevated; }
|
||||
}
|
||||
|
||||
.evt-status-text {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-medium;
|
||||
|
||||
.status-open & { color: $mint; }
|
||||
.status-ongoing & { color: $amber; }
|
||||
.status-ended & { color: $text-tertiary; }
|
||||
}
|
||||
|
||||
.evt-title {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-xl;
|
||||
line-height: $lh-tight;
|
||||
}
|
||||
|
||||
/* 信息卡片 */
|
||||
.evt-info-card {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-xl;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
padding: $sp-xl;
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.evt-info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $sp-md;
|
||||
padding: $sp-md 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
}
|
||||
|
||||
.evt-info-icon {
|
||||
font-size: $fs-base;
|
||||
width: 40rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.evt-info-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.evt-info-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.evt-info-value {
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
|
||||
/* 发起人 & 参与者 */
|
||||
.evt-section-label {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
font-weight: $fw-medium;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.evt-organizer {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.evt-org-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
.evt-org-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(232,168,56,0.15), rgba(139,133,184,0.15));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.evt-org-avatar-text {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.evt-org-name {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.evt-people-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $sp-lg;
|
||||
}
|
||||
|
||||
.evt-person {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
width: 100rpx;
|
||||
}
|
||||
|
||||
.evt-person-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-elevated;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.evt-person-text {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $lavender;
|
||||
}
|
||||
|
||||
.evt-person-name {
|
||||
font-size: $fs-xs;
|
||||
color: $text-secondary;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.evt-action-bar {
|
||||
padding: $sp-md $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-md});
|
||||
background: $bg-card;
|
||||
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.evt-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
text-align: center;
|
||||
border-radius: $radius-full;
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
border: none;
|
||||
|
||||
&::after { border: none; }
|
||||
&:active { transform: scale(0.98); }
|
||||
}
|
||||
|
||||
.evt-btn-primary {
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
color: $text-on-amber;
|
||||
}
|
||||
|
||||
.evt-btn-ghost {
|
||||
background: $bg-elevated;
|
||||
color: $coral;
|
||||
border: 1rpx solid rgba(255,107,107,0.2);
|
||||
}
|
||||
|
||||
.evt-ended-tip {
|
||||
text-align: center;
|
||||
padding: $sp-md 0;
|
||||
}
|
||||
|
||||
.evt-ended-text {
|
||||
font-size: $fs-base;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container friends-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="friends-nav" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="friends-nav-inner">
|
||||
<view class="friends-back" @click="goBack">
|
||||
<text class="friends-back-icon">‹</text>
|
||||
</view>
|
||||
<text class="friends-nav-title">我的酒友</text>
|
||||
<view class="friends-nav-ph"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="friends-search">
|
||||
<view class="search-box">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="搜索酒友"
|
||||
placeholder-class="search-placeholder"
|
||||
confirm-type="search"
|
||||
@confirm="doSearch"
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @click="clearSearch">
|
||||
<text class="search-clear-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="friends-scroll" scroll-y>
|
||||
<view class="friends-body">
|
||||
<!-- 好友请求区 -->
|
||||
<view v-if="requests.length && !keyword" class="requests-section">
|
||||
<text class="section-label">新的好友请求</text>
|
||||
<view class="request-item" v-for="req in requests" :key="req.id">
|
||||
<view class="request-avatar">
|
||||
<text class="request-avatar-text">{{ req.nickname[0] }}</text>
|
||||
</view>
|
||||
<view class="request-info">
|
||||
<text class="request-name">{{ req.nickname }}</text>
|
||||
<text class="request-msg">{{ req.message }}</text>
|
||||
</view>
|
||||
<view class="request-accept" @click="acceptRequest(req)">
|
||||
<text class="request-accept-text">接受</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 酒友列表 -->
|
||||
<view v-if="friends.length" class="friends-list">
|
||||
<text class="section-label" v-if="!keyword">全部酒友 ({{ friends.length }})</text>
|
||||
<text class="section-label" v-else>搜索结果 ({{ friends.length }})</text>
|
||||
<FriendItem
|
||||
v-for="f in friends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
>
|
||||
<template #action>
|
||||
<view class="friend-del" @click.stop="confirmRemove(f)">
|
||||
<text class="friend-del-text">删除</text>
|
||||
</view>
|
||||
</template>
|
||||
</FriendItem>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<EmptyState
|
||||
v-if="!friends.length && !requests.length"
|
||||
icon="👥"
|
||||
title="还没有酒友"
|
||||
desc="分享小程序给好友,邀请他们加入"
|
||||
actionText="邀请好友"
|
||||
@action="inviteFriends"
|
||||
/>
|
||||
<EmptyState
|
||||
v-if="keyword && !friends.length"
|
||||
icon="🔍"
|
||||
title="未找到相关酒友"
|
||||
desc="换个关键词试试"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FriendItem from '../../components/FriendItem.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import { GetFriends, GetFriendRequests, AcceptFriendRequest, RemoveFriend } from '../../common/api'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
components: { FriendItem, EmptyState },
|
||||
data() {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
return {
|
||||
navPaddingTop: menuBtn.top + 'px',
|
||||
keyword: '',
|
||||
friends: [],
|
||||
requests: [],
|
||||
searchTimer: null
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadData() {
|
||||
try {
|
||||
const [friendsRes, reqRes] = await Promise.all([
|
||||
GetFriends({}),
|
||||
GetFriendRequests()
|
||||
])
|
||||
this.friends = friendsRes.data.list
|
||||
this.requests = reqRes.data.list
|
||||
} catch (e) {
|
||||
console.warn('加载酒友失败', e)
|
||||
}
|
||||
},
|
||||
onSearchInput() {
|
||||
clearTimeout(this.searchTimer)
|
||||
this.searchTimer = setTimeout(() => this.doSearch(), 300)
|
||||
},
|
||||
async doSearch() {
|
||||
try {
|
||||
const res = await GetFriends({ keyword: this.keyword.trim() })
|
||||
this.friends = res.data.list
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
clearSearch() {
|
||||
this.keyword = ''
|
||||
this.doSearch()
|
||||
},
|
||||
async acceptRequest(req) {
|
||||
try {
|
||||
await AcceptFriendRequest({ requestId: req.id })
|
||||
this.requests = this.requests.filter(r => r.id !== req.id)
|
||||
uni.showToast({ title: '已添加酒友 🍻', icon: 'none' })
|
||||
this.loadData()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
confirmRemove(friend) {
|
||||
uni.showModal({
|
||||
title: '删除酒友',
|
||||
content: `确定要删除「${friend.nickname}」吗?`,
|
||||
confirmText: '删除',
|
||||
confirmColor: '#FF6B6B',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await RemoveFriend({ userId: friend.id })
|
||||
this.friends = this.friends.filter(f => f.id !== friend.id)
|
||||
uni.showToast({ title: '已删除', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
inviteFriends() {
|
||||
// 触发分享
|
||||
uni.showToast({ title: '请点击右上角分享', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '来「干杯日记」一起记录饮酒生活吧!',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.friends-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.friends-nav {
|
||||
background: $bg-base;
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
}
|
||||
|
||||
.friends-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.friends-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
|
||||
&:active { background: $bg-card-alt; }
|
||||
}
|
||||
|
||||
.friends-back-icon {
|
||||
font-size: $fs-xl;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.friends-nav-title {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.friends-nav-ph {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* 搜索 */
|
||||
.friends-search {
|
||||
padding: $sp-md $sp-lg;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
padding: 0 $sp-lg;
|
||||
height: 76rpx;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-full;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: $fs-sm;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-clear-icon {
|
||||
font-size: $fs-lg;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.friends-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.friends-body {
|
||||
padding: 0 $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-2xl});
|
||||
}
|
||||
|
||||
.section-label {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
font-weight: $fw-medium;
|
||||
margin-bottom: $sp-md;
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
|
||||
/* 好友请求 */
|
||||
.requests-section {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.request-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
padding: $sp-lg;
|
||||
background: rgba(232,168,56,0.05);
|
||||
border: 1rpx solid rgba(232,168,56,0.12);
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.request-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-elevated;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.request-avatar-text {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.request-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.request-name {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.request-msg {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.request-accept {
|
||||
padding: 8rpx 24rpx;
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
border-radius: $radius-full;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active { transform: scale(0.95); }
|
||||
}
|
||||
|
||||
.request-accept-text {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
}
|
||||
|
||||
/* 删除按钮 */
|
||||
.friend-del {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: $radius-full;
|
||||
background: rgba(255,107,107,0.1);
|
||||
|
||||
&:active { background: rgba(255,107,107,0.2); }
|
||||
}
|
||||
|
||||
.friend-del-text {
|
||||
font-size: $fs-xs;
|
||||
color: $coral;
|
||||
}
|
||||
</style>
|
||||
+64
-18
@@ -5,6 +5,7 @@
|
||||
<view class="hero-bg">
|
||||
<view class="hero-orb hero-orb-1"></view>
|
||||
<view class="hero-orb hero-orb-2"></view>
|
||||
<text class="hero-deco">🥃</text>
|
||||
</view>
|
||||
<!-- 右上角分享按钮(避让胶囊) -->
|
||||
<button class="hero-share-btn" open-type="share" :style="shareBtnStyle">
|
||||
@@ -17,11 +18,16 @@
|
||||
<text class="hero-avatar-icon">👤</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hero-name">{{ user.nickname || '酒友' }}</text>
|
||||
<view class="hero-level-tag">
|
||||
<text class="hero-level-text">{{ drinkLevel }}</text>
|
||||
<view class="hero-info">
|
||||
<text class="hero-greeting">{{ greeting }}</text>
|
||||
<view class="hero-name-row">
|
||||
<text class="hero-name">{{ user.nickname || '酒友' }}</text>
|
||||
<view class="hero-level-tag">
|
||||
<text class="hero-level-text">{{ drinkLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hero-joined">{{ joinedDate }} 加入 · 干杯日记</text>
|
||||
</view>
|
||||
<text class="hero-joined">{{ joinedDate }} 加入</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -219,6 +225,14 @@ export default {
|
||||
const map = { baijiu: '白酒之魂', beer: '啤酒达人', wine: '红酒雅士', whisky: '威士忌行家', huangjiu: '黄酒知音', sake: '清酒爱好者', fruit: '果酒小清新', cocktail: '调酒师' }
|
||||
return map[fav] || '多元品鉴师'
|
||||
},
|
||||
greeting() {
|
||||
const h = new Date().getHours()
|
||||
if (h < 6) return '🌙 夜深了,微醺正好'
|
||||
if (h < 11) return '☀️ 早上好,新的一天'
|
||||
if (h < 14) return '🌤 中午好,小酌怡情'
|
||||
if (h < 18) return '🌇 下午好,来一杯?'
|
||||
return '🌙 晚上好,今晚喝点?'
|
||||
},
|
||||
personaDesc() {
|
||||
const days = this.stats.totalDays || 0
|
||||
if (days === 0) return '开始记录你的第一杯吧'
|
||||
@@ -335,7 +349,7 @@ export default {
|
||||
return ACHIEVEMENTS[index % ACHIEVEMENTS.length].icon
|
||||
},
|
||||
goCircle() {
|
||||
uni.showToast({ title: '酒友圈即将上线,敬请期待 🍻', icon: 'none', duration: 2000 })
|
||||
uni.switchTab({ url: '/pages/circle/circle' })
|
||||
},
|
||||
goHome() {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
@@ -390,7 +404,7 @@ export default {
|
||||
/* === 沉浸式头部 === */
|
||||
.hero-header {
|
||||
position: relative;
|
||||
padding: 0 $sp-lg 80rpx;
|
||||
padding: 0 $sp-lg 64rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -447,22 +461,32 @@ export default {
|
||||
background: rgba(139,133,184,0.06);
|
||||
}
|
||||
|
||||
.hero-deco {
|
||||
position: absolute;
|
||||
right: -20rpx;
|
||||
bottom: -30rpx;
|
||||
font-size: 200rpx;
|
||||
opacity: 0.06;
|
||||
transform: rotate(-15deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-top: $sp-lg;
|
||||
padding-top: $sp-xl;
|
||||
}
|
||||
|
||||
.avatar-ring {
|
||||
width: 168rpx; height: 168rpx;
|
||||
width: 128rpx; height: 128rpx;
|
||||
border-radius: 50%;
|
||||
padding: 4rpx;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(160deg, rgba(232,168,56,0.7), rgba(232,168,56,0.2), rgba(139,133,184,0.3));
|
||||
margin-bottom: $sp-xl;
|
||||
box-shadow: 0 0 48rpx rgba(232,168,56,0.15);
|
||||
box-shadow: 0 0 40rpx rgba(232,168,56,0.12);
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
@@ -478,28 +502,49 @@ export default {
|
||||
}
|
||||
|
||||
.hero-avatar-icon {
|
||||
font-size: 72rpx;
|
||||
font-size: 56rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hero-info {
|
||||
flex: 1;
|
||||
margin-left: $sp-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hero-greeting {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
margin-bottom: 14rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.hero-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 14rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-md;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.hero-level-tag {
|
||||
padding: 8rpx 24rpx;
|
||||
background: rgba(232,168,56,0.08);
|
||||
border: 1rpx solid rgba(232,168,56,0.18);
|
||||
padding: 6rpx 18rpx;
|
||||
background: rgba(232,168,56,0.10);
|
||||
border: 1rpx solid rgba(232,168,56,0.20);
|
||||
border-radius: $radius-full;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.hero-level-text {
|
||||
font-size: $fs-xs;
|
||||
font-size: 20rpx;
|
||||
color: $amber;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
@@ -508,6 +553,7 @@ export default {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
opacity: 0.7;
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
|
||||
/* === 悬浮数据卡片 === */
|
||||
|
||||
Reference in New Issue
Block a user