feat(circle): 添加酒友圈社交模块
- 新增酒友圈、圈子详情、发布动态、好友列表、活动创建等页面配置 - 添加酒友圈相关API接口,包括动态、评论、好友、活动等功能 - 实现模拟数据用于前端开发和测试 - 创建评论项、空状态、活动卡片、动态卡片、好友项等组件 - 编写酒友圈模块详细的接口文档,定义数据结构和业务规则
This commit is contained in:
+104
@@ -154,3 +154,107 @@ export async function refreshAuthToken() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 酒友圈 API(当前返回 Mock 数据,后端就绪后切换为 SDK 调用)
|
||||
// ==========================================
|
||||
import { MOCK_FEEDS, MOCK_COMMENTS, MOCK_FRIENDS, MOCK_FRIEND_REQUESTS, MOCK_EVENTS } from './mock-data'
|
||||
|
||||
/** 模拟网络延迟 */
|
||||
function mockDelay(data, ms = 300) {
|
||||
return new Promise(resolve => setTimeout(() => resolve({ data }), ms))
|
||||
}
|
||||
|
||||
/** 获取动态信息流 */
|
||||
export function GetCircleFeeds({ page = 1, pageSize = 10 } = {}) {
|
||||
// TODO: 后端就绪后替换为 client.GetCircleFeeds(...)
|
||||
const start = (page - 1) * pageSize
|
||||
const list = MOCK_FEEDS.slice(start, start + pageSize)
|
||||
return mockDelay({ list, total: MOCK_FEEDS.length, hasMore: start + pageSize < MOCK_FEEDS.length })
|
||||
}
|
||||
|
||||
/** 点赞动态 */
|
||||
export function LikeFeed({ feedId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 取消点赞 */
|
||||
export function UnlikeFeed({ feedId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 获取动态评论 */
|
||||
export function GetFeedComments({ feedId }) {
|
||||
const list = MOCK_COMMENTS[feedId] || []
|
||||
return mockDelay({ list })
|
||||
}
|
||||
|
||||
/** 发表评论 */
|
||||
export function AddComment({ feedId, content }) {
|
||||
return mockDelay({ success: true, comment: { id: 'c_' + Date.now(), content, time: '刚刚' } })
|
||||
}
|
||||
|
||||
/** 发布动态 */
|
||||
export function PublishFeed({ text, images, recordId, visibility }) {
|
||||
return mockDelay({ success: true, feedId: 'feed_' + Date.now() })
|
||||
}
|
||||
|
||||
/** 获取酒友列表 */
|
||||
export function GetFriends({ keyword = '' } = {}) {
|
||||
const list = keyword
|
||||
? MOCK_FRIENDS.filter(f => f.nickname.includes(keyword))
|
||||
: MOCK_FRIENDS
|
||||
return mockDelay({ list })
|
||||
}
|
||||
|
||||
/** 获取好友请求 */
|
||||
export function GetFriendRequests() {
|
||||
return mockDelay({ list: MOCK_FRIEND_REQUESTS })
|
||||
}
|
||||
|
||||
/** 发送好友请求 */
|
||||
export function SendFriendRequest({ userId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 接受好友请求 */
|
||||
export function AcceptFriendRequest({ requestId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 删除酒友 */
|
||||
export function RemoveFriend({ userId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 获取酒局列表 */
|
||||
export function GetEvents({ status = '' } = {}) {
|
||||
const list = status ? MOCK_EVENTS.filter(e => e.status === status) : MOCK_EVENTS
|
||||
return mockDelay({ list })
|
||||
}
|
||||
|
||||
/** 获取酒局详情 */
|
||||
export function GetEventDetail({ eventId }) {
|
||||
const evt = MOCK_EVENTS.find(e => e.id === eventId) || MOCK_EVENTS[0]
|
||||
return mockDelay(evt)
|
||||
}
|
||||
|
||||
/** 发起酒局 */
|
||||
export function CreateEvent({ title, time, location, maxPeople, note }) {
|
||||
return mockDelay({ success: true, eventId: 'evt_' + Date.now() })
|
||||
}
|
||||
|
||||
/** 报名酒局 */
|
||||
export function JoinEvent({ eventId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 取消报名 */
|
||||
export function QuitEvent({ eventId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
/** 酒局签到 */
|
||||
export function CheckInEvent({ eventId }) {
|
||||
return mockDelay({ success: true })
|
||||
}
|
||||
|
||||
@@ -148,3 +148,184 @@ export function getUserInfo() {
|
||||
}
|
||||
return MOCK_USER
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 酒友圈 Mock 数据
|
||||
// ==========================================
|
||||
|
||||
// 模拟酒友列表
|
||||
export const MOCK_FRIENDS = [
|
||||
{ id: 'f001', nickname: '老张', avatar: '', lastDrink: '昨晚喝了飞天茅台', lastDrinkTime: '2小时前', online: true },
|
||||
{ id: 'f002', nickname: '酒仙李白', avatar: '', lastDrink: '青岛啤酒×3', lastDrinkTime: '5小时前', online: true },
|
||||
{ id: 'f003', nickname: '小红', avatar: '', lastDrink: '梅见青梅酒', lastDrinkTime: '昨天', online: false },
|
||||
{ id: 'f004', nickname: '阿伟', avatar: '', lastDrink: '百威经典×2', lastDrinkTime: '昨天', online: false },
|
||||
{ id: 'f005', nickname: '品酒师Tony', avatar: '', lastDrink: '麦卡伦12年', lastDrinkTime: '3天前', online: true },
|
||||
{ id: 'f006', nickname: '醉翁之意', avatar: '', lastDrink: '奔富Bin389', lastDrinkTime: '3天前', online: false }
|
||||
]
|
||||
|
||||
// 模拟好友请求
|
||||
export const MOCK_FRIEND_REQUESTS = [
|
||||
{ id: 'req001', userId: 'u101', nickname: '夜猫子', avatar: '', message: '一起喝过酒,加个好友', time: '1小时前' },
|
||||
{ id: 'req002', userId: 'u102', nickname: '杯中物', avatar: '', message: '酒友圈看到你的动态', time: '3小时前' }
|
||||
]
|
||||
|
||||
// 模拟动态信息流
|
||||
export const MOCK_FEEDS = [
|
||||
{
|
||||
id: 'feed001',
|
||||
userId: 'f001',
|
||||
nickname: '老张',
|
||||
avatar: '',
|
||||
time: '2小时前',
|
||||
text: '今晚和几个老兄弟整了瓶飞天,配重庆火锅,巴适得很!',
|
||||
drinks: [{ category: 'baijiu', name: '飞天茅台53度', amount: '100ml' }],
|
||||
feeling: 'tipsy',
|
||||
images: [],
|
||||
likes: 12,
|
||||
liked: false,
|
||||
comments: 3
|
||||
},
|
||||
{
|
||||
id: 'feed002',
|
||||
userId: 'f002',
|
||||
nickname: '酒仙李白',
|
||||
avatar: '',
|
||||
time: '5小时前',
|
||||
text: '夏天就是要冰啤酒配小龙虾,三瓶百威下肚,人生圆满。',
|
||||
drinks: [{ category: 'beer', name: '百威经典', amount: '3瓶' }],
|
||||
feeling: 'buzzed',
|
||||
images: [],
|
||||
likes: 8,
|
||||
liked: true,
|
||||
comments: 5
|
||||
},
|
||||
{
|
||||
id: 'feed003',
|
||||
userId: 'f003',
|
||||
nickname: '小红',
|
||||
avatar: '',
|
||||
time: '昨天 21:30',
|
||||
text: '第一次尝试青梅酒,酸酸甜甜的,适合女生微醺~',
|
||||
drinks: [{ category: 'fruit', name: '梅见青梅酒', amount: '200ml' }],
|
||||
feeling: 'tipsy',
|
||||
images: [],
|
||||
likes: 15,
|
||||
liked: false,
|
||||
comments: 7
|
||||
},
|
||||
{
|
||||
id: 'feed004',
|
||||
userId: 'f005',
|
||||
nickname: '品酒师Tony',
|
||||
avatar: '',
|
||||
time: '昨天 19:00',
|
||||
text: '麦卡伦12年雪莉桶,闻香有太妃糖和干果的气息,入口丝滑。推荐搭配黑巧克力。',
|
||||
drinks: [{ category: 'whisky', name: '麦卡伦12年', amount: '3 shot' }],
|
||||
feeling: 'tipsy',
|
||||
images: [],
|
||||
likes: 22,
|
||||
liked: false,
|
||||
comments: 4
|
||||
},
|
||||
{
|
||||
id: 'feed005',
|
||||
userId: 'f004',
|
||||
nickname: '阿伟',
|
||||
avatar: '',
|
||||
time: '前天 20:15',
|
||||
text: '公司团建,五粮液配水煮鱼,喝到位了。明天又要搬砖...',
|
||||
drinks: [{ category: 'baijiu', name: '五粮液普五52度', amount: '150ml' }, { category: 'beer', name: '雪花勇闯天涯', amount: '1瓶' }],
|
||||
feeling: 'drunk',
|
||||
images: [],
|
||||
likes: 6,
|
||||
liked: false,
|
||||
comments: 2
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟评论数据
|
||||
export const MOCK_COMMENTS = {
|
||||
feed001: [
|
||||
{ id: 'c001', userId: 'f002', nickname: '酒仙李白', avatar: '', content: '茅台配火锅,土豪啊老张!', time: '1小时前' },
|
||||
{ id: 'c002', userId: 'f003', nickname: '小红', avatar: '', content: '看着就馋了,下次带上我', time: '45分钟前' },
|
||||
{ id: 'c003', userId: 'f005', nickname: '品酒师Tony', avatar: '', content: '飞天53度确实是经典,好酒!', time: '30分钟前' }
|
||||
],
|
||||
feed002: [
|
||||
{ id: 'c004', userId: 'f001', nickname: '老张', avatar: '', content: '夏天标配!', time: '4小时前' },
|
||||
{ id: 'c005', userId: 'f004', nickname: '阿伟', avatar: '', content: '小龙虾哪买的?看着不错', time: '3小时前' }
|
||||
],
|
||||
feed003: [
|
||||
{ id: 'c006', userId: 'f001', nickname: '老张', avatar: '', content: '梅见确实适合入门', time: '昨天' },
|
||||
{ id: 'c007', userId: 'f002', nickname: '酒仙李白', avatar: '', content: '下次试试蜜桃味', time: '昨天' }
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟酒局活动
|
||||
export const MOCK_EVENTS = [
|
||||
{
|
||||
id: 'evt001',
|
||||
title: '周五夜·老地方火锅局',
|
||||
organizer: { id: 'f001', nickname: '老张', avatar: '' },
|
||||
time: '2026-07-25 19:00',
|
||||
location: '渝味晓宇火锅(解放碑店)',
|
||||
maxPeople: 8,
|
||||
joined: 5,
|
||||
participants: [
|
||||
{ id: 'f001', nickname: '老张', avatar: '' },
|
||||
{ id: 'f002', nickname: '酒仙李白', avatar: '' },
|
||||
{ id: 'f003', nickname: '小红', avatar: '' },
|
||||
{ id: 'f004', nickname: '阿伟', avatar: '' },
|
||||
{ id: 'f005', nickname: '品酒师Tony', avatar: '' }
|
||||
],
|
||||
status: 'open',
|
||||
note: '自带酒水,AA制餐费',
|
||||
isJoined: false,
|
||||
isOrganizer: false
|
||||
},
|
||||
{
|
||||
id: 'evt002',
|
||||
title: '威士忌品鉴之夜',
|
||||
organizer: { id: 'f005', nickname: '品酒师Tony', avatar: '' },
|
||||
time: '2026-07-27 20:00',
|
||||
location: 'Malt Bar(国贸店)',
|
||||
maxPeople: 6,
|
||||
joined: 3,
|
||||
participants: [
|
||||
{ id: 'f005', nickname: '品酒师Tony', avatar: '' },
|
||||
{ id: 'f001', nickname: '老张', avatar: '' },
|
||||
{ id: 'f006', nickname: '醉翁之意', avatar: '' }
|
||||
],
|
||||
status: 'open',
|
||||
note: '品鉴3款单一麦芽,费用AA',
|
||||
isJoined: true,
|
||||
isOrganizer: false
|
||||
},
|
||||
{
|
||||
id: 'evt003',
|
||||
title: '啤酒花园烧烤趴',
|
||||
organizer: { id: 'f002', nickname: '酒仙李白', avatar: '' },
|
||||
time: '2026-07-20 18:00',
|
||||
location: '望京啤酒花园',
|
||||
maxPeople: 12,
|
||||
joined: 10,
|
||||
participants: [],
|
||||
status: 'ongoing',
|
||||
note: '畅饮模式,不醉不归',
|
||||
isJoined: false,
|
||||
isOrganizer: false
|
||||
},
|
||||
{
|
||||
id: 'evt004',
|
||||
title: '红酒配牛排·优雅之夜',
|
||||
organizer: { id: 'f003', nickname: '小红', avatar: '' },
|
||||
time: '2026-07-15 19:30',
|
||||
location: 'The Grill(三里屯)',
|
||||
maxPeople: 4,
|
||||
joined: 4,
|
||||
participants: [],
|
||||
status: 'ended',
|
||||
note: '已圆满结束,下次再约',
|
||||
isJoined: false,
|
||||
isOrganizer: false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="comment-item">
|
||||
<view class="comment-avatar-wrap">
|
||||
<image v-if="comment.avatar" class="comment-avatar" :src="comment.avatar" mode="aspectFill"></image>
|
||||
<view v-else class="comment-avatar comment-avatar-ph">
|
||||
<text class="comment-avatar-text">{{ comment.nickname ? comment.nickname[0] : '酒' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="comment-body">
|
||||
<view class="comment-top">
|
||||
<text class="comment-name">{{ comment.nickname }}</text>
|
||||
<text class="comment-time">{{ comment.time }}</text>
|
||||
</view>
|
||||
<text class="comment-content">{{ comment.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CommentItem',
|
||||
props: {
|
||||
comment: { type: Object, default: () => ({}) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.comment-item {
|
||||
display: flex;
|
||||
gap: $sp-md;
|
||||
padding: $sp-lg 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
}
|
||||
|
||||
.comment-avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comment-avatar-ph {
|
||||
background: $bg-elevated;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.comment-avatar-text {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $lavender;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.comment-name {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<view class="empty-state">
|
||||
<text class="empty-icon">{{ icon }}</text>
|
||||
<text class="empty-title">{{ title }}</text>
|
||||
<text class="empty-desc" v-if="desc">{{ desc }}</text>
|
||||
<button v-if="actionText" class="empty-action" @click="$emit('action')">{{ actionText }}</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EmptyState',
|
||||
props: {
|
||||
icon: { type: String, default: '🍻' },
|
||||
title: { type: String, default: '暂无内容' },
|
||||
desc: { type: String, default: '' },
|
||||
actionText: { type: String, default: '' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx $sp-xl;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: $sp-lg;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-medium;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
text-align: center;
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
margin-top: $sp-xl;
|
||||
padding: $sp-md $sp-2xl;
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
color: $text-on-amber;
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
border-radius: $radius-full;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<view class="event-card" @click="$emit('tap', event)">
|
||||
<!-- 状态标签 -->
|
||||
<view class="event-status" :class="'status-' + event.status">
|
||||
<text class="event-status-text">{{ statusLabel }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<text class="event-title">{{ event.title }}</text>
|
||||
|
||||
<!-- 信息行 -->
|
||||
<view class="event-info">
|
||||
<view class="event-info-row">
|
||||
<text class="event-info-icon">🕐</text>
|
||||
<text class="event-info-text">{{ event.time }}</text>
|
||||
</view>
|
||||
<view class="event-info-row">
|
||||
<text class="event-info-icon">📍</text>
|
||||
<text class="event-info-text">{{ event.location }}</text>
|
||||
</view>
|
||||
<view class="event-info-row">
|
||||
<text class="event-info-icon">👥</text>
|
||||
<text class="event-info-text">{{ event.joined }}/{{ event.maxPeople }} 人已报名</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部:发起人 + 操作 -->
|
||||
<view class="event-footer">
|
||||
<view class="event-organizer">
|
||||
<view class="event-org-avatar">
|
||||
<text class="event-org-text">{{ event.organizer ? event.organizer.nickname[0] : '酒' }}</text>
|
||||
</view>
|
||||
<text class="event-org-name">{{ event.organizer ? event.organizer.nickname : '' }}</text>
|
||||
</view>
|
||||
<view class="event-action" v-if="event.status === 'open'">
|
||||
<text class="event-action-text" v-if="event.isJoined">✓ 已报名</text>
|
||||
<text class="event-action-text event-action-join" v-else>报名</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EventCard',
|
||||
props: {
|
||||
event: { type: Object, default: () => ({}) }
|
||||
},
|
||||
computed: {
|
||||
statusLabel() {
|
||||
const map = { open: '报名中', ongoing: '进行中', ended: '已结束' }
|
||||
return map[this.event.status] || '报名中'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.event-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-md;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:active {
|
||||
background: $bg-card-alt;
|
||||
}
|
||||
}
|
||||
|
||||
.event-status {
|
||||
display: inline-flex;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: $radius-full;
|
||||
margin-bottom: $sp-md;
|
||||
|
||||
&.status-open {
|
||||
background: rgba(94,198,160,0.12);
|
||||
}
|
||||
&.status-ongoing {
|
||||
background: rgba(232,168,56,0.12);
|
||||
}
|
||||
&.status-ended {
|
||||
background: $bg-elevated;
|
||||
}
|
||||
}
|
||||
|
||||
.event-status-text {
|
||||
font-size: $fs-xs;
|
||||
font-weight: $fw-medium;
|
||||
|
||||
.status-open & { color: $mint; }
|
||||
.status-ongoing & { color: $amber; }
|
||||
.status-ended & { color: $text-tertiary; }
|
||||
}
|
||||
|
||||
.event-title {
|
||||
display: block;
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.event-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.event-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
}
|
||||
|
||||
.event-info-icon {
|
||||
font-size: $fs-sm;
|
||||
width: 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.event-info-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.event-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: $sp-md;
|
||||
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.event-organizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
}
|
||||
|
||||
.event-org-avatar {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-elevated;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-org-text {
|
||||
font-size: $fs-xs;
|
||||
font-weight: $fw-bold;
|
||||
color: $lavender;
|
||||
}
|
||||
|
||||
.event-org-name {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.event-action-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: $radius-full;
|
||||
background: $bg-elevated;
|
||||
}
|
||||
|
||||
.event-action-join {
|
||||
color: $text-on-amber;
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<view class="feed-card" @click="$emit('tap', feed)">
|
||||
<!-- 头部:头像+昵称+时间 -->
|
||||
<view class="feed-header">
|
||||
<view class="feed-avatar-wrap">
|
||||
<image v-if="feed.avatar" class="feed-avatar" :src="feed.avatar" mode="aspectFill"></image>
|
||||
<view v-else class="feed-avatar feed-avatar-ph">
|
||||
<text class="feed-avatar-text">{{ feed.nickname ? feed.nickname[0] : '酒' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="feed-meta">
|
||||
<text class="feed-name">{{ feed.nickname }}</text>
|
||||
<text class="feed-time">{{ feed.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 正文 -->
|
||||
<text class="feed-text">{{ feed.text }}</text>
|
||||
|
||||
<!-- 饮酒标签 -->
|
||||
<view class="feed-drinks" v-if="feed.drinks && feed.drinks.length">
|
||||
<view class="feed-drink-tag" v-for="(d, i) in feed.drinks" :key="i">
|
||||
<text class="feed-drink-emoji">{{ categoryEmoji(d.category) }}</text>
|
||||
<text class="feed-drink-name">{{ d.name }} {{ d.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 感受标签 -->
|
||||
<view class="feed-feeling" v-if="feed.feeling">
|
||||
<text class="feed-feeling-text">{{ feelingLabel(feed.feeling) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 图片 -->
|
||||
<view class="feed-images" v-if="feed.images && feed.images.length">
|
||||
<image
|
||||
v-for="(img, i) in feed.images"
|
||||
:key="i"
|
||||
class="feed-img"
|
||||
:class="'feed-img-' + Math.min(feed.images.length, 3)"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<view class="feed-actions">
|
||||
<view class="feed-action" :class="{ active: feed.liked }" @click.stop="$emit('like', feed)">
|
||||
<text class="feed-action-icon">{{ feed.liked ? '❤️' : '🤍' }}</text>
|
||||
<text class="feed-action-num">{{ feed.likes || '' }}</text>
|
||||
</view>
|
||||
<view class="feed-action" @click.stop="$emit('comment', feed)">
|
||||
<text class="feed-action-icon">💬</text>
|
||||
<text class="feed-action-num">{{ feed.comments || '' }}</text>
|
||||
</view>
|
||||
<view class="feed-action" @click.stop="$emit('share', feed)">
|
||||
<text class="feed-action-icon">↗</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FeedCard',
|
||||
props: {
|
||||
feed: { type: Object, default: () => ({}) }
|
||||
},
|
||||
methods: {
|
||||
categoryEmoji(cat) {
|
||||
const map = { baijiu: '🏺', beer: '🍺', wine: '🍷', whisky: '🥃', huangjiu: '🫖', sake: '🍶', fruit: '🍹', cocktail: '🍸' }
|
||||
return map[cat] || '🍻'
|
||||
},
|
||||
feelingLabel(f) {
|
||||
const map = { sober: '😌 清醒', buzzed: '😊 微醺', tipsy: '😄 小醉', drunk: '🥴 喝多了' }
|
||||
return map[f] || '🍻 喝了点'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feed-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-md;
|
||||
}
|
||||
|
||||
.feed-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.feed-avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feed-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.feed-avatar-ph {
|
||||
background: linear-gradient(135deg, rgba(232,168,56,0.2), rgba(139,133,184,0.2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feed-avatar-text {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.feed-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.feed-name {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.feed-time {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.feed-text {
|
||||
display: block;
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
line-height: $lh-loose;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.feed-drinks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.feed-drink-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
background: $bg-elevated;
|
||||
border-radius: $radius-full;
|
||||
border: 1rpx solid var(--border-micro, rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
.feed-drink-emoji {
|
||||
font-size: $fs-sm;
|
||||
}
|
||||
|
||||
.feed-drink-name {
|
||||
font-size: $fs-xs;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.feed-feeling {
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.feed-feeling-text {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
padding: 6rpx 16rpx;
|
||||
background: rgba(232,168,56,0.06);
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.feed-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.feed-img {
|
||||
border-radius: $radius-md;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.feed-img-1 {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
}
|
||||
|
||||
.feed-img-2 {
|
||||
width: calc(50% - 8rpx);
|
||||
}
|
||||
|
||||
.feed-img-3 {
|
||||
width: calc(33.33% - 11rpx);
|
||||
}
|
||||
|
||||
.feed-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-xl;
|
||||
padding-top: $sp-md;
|
||||
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.feed-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 0;
|
||||
|
||||
&.active .feed-action-num {
|
||||
color: $coral;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-action-icon {
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.feed-action-num {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<view class="friend-item" @click="$emit('tap', friend)">
|
||||
<view class="friend-avatar-wrap">
|
||||
<image v-if="friend.avatar" class="friend-avatar" :src="friend.avatar" mode="aspectFill"></image>
|
||||
<view v-else class="friend-avatar friend-avatar-ph">
|
||||
<text class="friend-avatar-text">{{ friend.nickname ? friend.nickname[0] : '酒' }}</text>
|
||||
</view>
|
||||
<view v-if="friend.online" class="friend-online"></view>
|
||||
</view>
|
||||
<view class="friend-info">
|
||||
<text class="friend-name">{{ friend.nickname }}</text>
|
||||
<text class="friend-last">{{ friend.lastDrink }} · {{ friend.lastDrinkTime }}</text>
|
||||
</view>
|
||||
<slot name="action"></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FriendItem',
|
||||
props: {
|
||||
friend: { type: Object, default: () => ({}) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.friend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
padding: $sp-lg;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
margin-bottom: $sp-sm;
|
||||
|
||||
&:active {
|
||||
background: $bg-card-alt;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-avatar-wrap {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.friend-avatar-ph {
|
||||
background: linear-gradient(135deg, rgba(232,168,56,0.15), rgba(139,133,184,0.15));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.friend-avatar-text {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.friend-online {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: $mint;
|
||||
border: 3rpx solid $bg-card;
|
||||
}
|
||||
|
||||
.friend-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.friend-last {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
# 干杯日记 - 酒友圈模块接口文档
|
||||
|
||||
> 版本: v1.0
|
||||
> 日期: 2026-07-20
|
||||
> 模块: 酒友圈(社交模块)
|
||||
> 小程序: 干杯日记(uni-app 微信小程序)
|
||||
> 基础URL: `https://your-domain.com/api/v1`
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
酒友圈是干杯日记的核心社交模块,包含三大子功能:
|
||||
|
||||
| 子功能 | 说明 |
|
||||
|--------|------|
|
||||
| 动态信息流 | 酒友发布饮酒动态,支持点赞、评论 |
|
||||
| 酒友管理 | 好友请求、酒友列表、搜索、删除 |
|
||||
| 约酒活动 | 发起酒局、报名、取消、签到 |
|
||||
|
||||
**前端当前状态:** 所有接口已在前端 `common/api.js` 中预留调用方法,暂用 Mock 数据。后端实现后前端仅需切换调用方式,无需改动页面逻辑。**请严格按照本文档的字段名和数据结构实现**,确保前端零改动对接。
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用约定
|
||||
|
||||
### 2.1 鉴权
|
||||
- 请求头:`Authorization: Bearer {token}`
|
||||
- 所有接口均需登录态
|
||||
|
||||
### 2.2 统一响应格式
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 错误码
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 0 | 成功 |
|
||||
| 400 | 参数错误 |
|
||||
| 401 | 未登录/token过期 |
|
||||
| 403 | 无权限(如:非酒友关系、非发起人) |
|
||||
| 404 | 资源不存在 |
|
||||
| 409 | 冲突(如:重复报名、已是酒友) |
|
||||
| 500 | 服务器错误 |
|
||||
|
||||
### 2.4 时间格式
|
||||
- 所有时间字段使用 ISO 8601:`2026-07-20T22:30:00.000Z`
|
||||
- 前端会自行转换为相对时间("2小时前"等)
|
||||
|
||||
---
|
||||
|
||||
## 3. 动态信息流
|
||||
|
||||
### 3.1 获取动态列表
|
||||
```
|
||||
GET /circle/feeds
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 否 | 页码,默认1 |
|
||||
| pageSize | int | 否 | 每页数量,默认10 |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": "feed001",
|
||||
"userId": "user_002",
|
||||
"nickname": "老张",
|
||||
"avatar": "https://xxx/avatar.jpg",
|
||||
"time": "2026-07-20T20:30:00.000Z",
|
||||
"text": "今晚和几个老兄弟整了瓶飞天,配重庆火锅,巴适得很!",
|
||||
"drinks": [
|
||||
{ "category": "baijiu", "name": "飞天茅台53度", "amount": "100ml" }
|
||||
],
|
||||
"feeling": "tipsy",
|
||||
"images": ["https://xxx/img1.jpg"],
|
||||
"likes": 12,
|
||||
"liked": false,
|
||||
"comments": 3
|
||||
}
|
||||
],
|
||||
"total": 50,
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
**Feed 字段说明:**
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 动态ID |
|
||||
| userId | string | 发布者用户ID |
|
||||
| nickname | string | 发布者昵称 |
|
||||
| avatar | string | 发布者头像URL(可为空字符串) |
|
||||
| time | string | 发布时间 ISO 8601 |
|
||||
| text | string | 动态文字内容 |
|
||||
| drinks | DrinkTag[] | 关联酒水标签(见下方结构) |
|
||||
| feeling | string/null | 感受ID:sober/buzzed/tipsy/drunk |
|
||||
| images | string[] | 配图URL数组(最多9张) |
|
||||
| likes | int | 点赞数 |
|
||||
| liked | boolean | 当前用户是否已点赞 |
|
||||
| comments | int | 评论数 |
|
||||
|
||||
**DrinkTag 结构:**
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| category | string | 酒类ID(baijiu/beer/wine/whisky/huangjiu/sake/fruit/cocktail) |
|
||||
| name | string | 酒名(如"飞天茅台53度") |
|
||||
| amount | string | 用量展示文本(如"100ml"、"3瓶") |
|
||||
|
||||
**业务规则:**
|
||||
- 仅返回当前用户**酒友**发布的动态(visibility=friends)+ 公开动态(visibility=public)
|
||||
- 按发布时间倒序
|
||||
- 自己的动态也包含在内
|
||||
|
||||
---
|
||||
|
||||
### 3.2 发布动态
|
||||
```
|
||||
POST /circle/feeds
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"text": "今晚小酌一杯",
|
||||
"images": ["https://xxx/img1.jpg"],
|
||||
"recordId": "record_1689234567890",
|
||||
"visibility": "friends"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| text | string | 否 | 文字内容(最长500字),text和images至少一项非空 |
|
||||
| images | string[] | 否 | 图片URL数组(最多9张,先调上传接口获取URL) |
|
||||
| recordId | string/null | 否 | 关联的饮酒记录ID,传入时自动提取酒水信息生成drinks标签 |
|
||||
| visibility | string | 否 | 可见范围:friends(默认)/private |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"feedId": "feed_1689234567890"
|
||||
}
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 传入 recordId 时,后端从记录中提取 drinks 信息填充到动态的 drinks 字段
|
||||
- UGC 内容(text)需接入微信内容安全审核(msgSecCheck)
|
||||
|
||||
---
|
||||
|
||||
### 3.3 点赞动态
|
||||
```
|
||||
POST /circle/feeds/:id/like
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
### 3.4 取消点赞
|
||||
```
|
||||
POST /circle/feeds/:id/unlike
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
> 重复点赞/取消应幂等处理,不报错。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 获取动态评论
|
||||
```
|
||||
GET /circle/feeds/:id/comments
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": "c001",
|
||||
"userId": "user_003",
|
||||
"nickname": "酒仙李白",
|
||||
"avatar": "https://xxx/avatar3.jpg",
|
||||
"content": "茅台配火锅,土豪啊老张!",
|
||||
"time": "2026-07-20T21:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Comment 字段说明:**
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 评论ID |
|
||||
| userId | string | 评论者ID |
|
||||
| nickname | string | 评论者昵称 |
|
||||
| avatar | string | 评论者头像 |
|
||||
| content | string | 评论内容 |
|
||||
| time | string | 评论时间 ISO 8601 |
|
||||
|
||||
> 按时间正序返回(最早在前)。
|
||||
|
||||
---
|
||||
|
||||
### 3.6 发表评论
|
||||
```
|
||||
POST /circle/feeds/:id/comments
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{ "content": "看着就馋了,下次带上我" }
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| content | string | 是 | 评论内容(最长200字) |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"comment": {
|
||||
"id": "c_1689234567890",
|
||||
"content": "看着就馋了,下次带上我",
|
||||
"time": "2026-07-20T22:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 评论需接入内容安全审核
|
||||
- 评论成功后,动态的 comments 计数 +1
|
||||
|
||||
---
|
||||
|
||||
## 4. 酒友管理
|
||||
|
||||
### 4.1 获取酒友列表
|
||||
```
|
||||
GET /circle/friends
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| keyword | string | 否 | 按昵称模糊搜索 |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": "user_002",
|
||||
"nickname": "老张",
|
||||
"avatar": "https://xxx/avatar.jpg",
|
||||
"lastDrink": "昨晚喝了飞天茅台",
|
||||
"lastDrinkTime": "2026-07-20T20:30:00.000Z",
|
||||
"online": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Friend 字段说明:**
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 酒友用户ID |
|
||||
| nickname | string | 昵称 |
|
||||
| avatar | string | 头像URL |
|
||||
| lastDrink | string | 最近一次饮酒摘要文本(如"昨晚喝了飞天茅台"),无记录则为空 |
|
||||
| lastDrinkTime | string | 最近饮酒时间 ISO 8601 |
|
||||
| online | boolean | 是否在线(24小时内活跃视为在线) |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 获取好友请求列表
|
||||
```
|
||||
GET /circle/friend-requests
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": "req001",
|
||||
"userId": "user_101",
|
||||
"nickname": "夜猫子",
|
||||
"avatar": "https://xxx/avatar101.jpg",
|
||||
"message": "一起喝过酒,加个好友",
|
||||
"time": "2026-07-20T21:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> 仅返回**待处理**的请求(status=pending),按时间倒序。
|
||||
|
||||
---
|
||||
|
||||
### 4.3 发送好友请求
|
||||
```
|
||||
POST /circle/friend-requests
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"userId": "user_101",
|
||||
"message": "一起喝过酒,加个好友"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| userId | string | 是 | 目标用户ID |
|
||||
| message | string | 否 | 验证消息(最长50字) |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 已是酒友返回 409
|
||||
- 已有待处理请求则幂等返回成功
|
||||
|
||||
---
|
||||
|
||||
### 4.4 接受好友请求
|
||||
```
|
||||
POST /circle/friend-requests/:id/accept
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 接受后双方建立酒友关系
|
||||
- 请求状态更新为 accepted
|
||||
|
||||
---
|
||||
|
||||
### 4.5 删除酒友
|
||||
```
|
||||
DELETE /circle/friends/:userId
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 双向解除酒友关系
|
||||
- 删除后对方动态不再出现在自己的信息流中
|
||||
|
||||
---
|
||||
|
||||
## 5. 约酒活动(酒局)
|
||||
|
||||
### 5.1 获取酒局列表
|
||||
```
|
||||
GET /circle/events
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| status | string | 否 | 状态筛选:open/ongoing/ended,不传返回全部 |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": "evt001",
|
||||
"title": "周五夜·老地方火锅局",
|
||||
"organizer": {
|
||||
"id": "user_002",
|
||||
"nickname": "老张",
|
||||
"avatar": "https://xxx/avatar.jpg"
|
||||
},
|
||||
"time": "2026-07-25T19:00:00.000Z",
|
||||
"location": "渝味晓宇火锅(解放碑店)",
|
||||
"maxPeople": 8,
|
||||
"joined": 5,
|
||||
"participants": [
|
||||
{ "id": "user_002", "nickname": "老张", "avatar": "https://xxx/avatar.jpg" }
|
||||
],
|
||||
"status": "open",
|
||||
"note": "自带酒水,AA制餐费",
|
||||
"isJoined": false,
|
||||
"isOrganizer": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Event 字段说明:**
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 酒局ID |
|
||||
| title | string | 酒局名称(最长30字) |
|
||||
| organizer | UserInfo | 发起人信息 |
|
||||
| time | string | 开始时间 ISO 8601 |
|
||||
| location | string | 地点文本 |
|
||||
| maxPeople | int | 人数上限(2-50) |
|
||||
| joined | int | 已报名人数(含发起人) |
|
||||
| participants | UserInfo[] | 参与者列表 |
|
||||
| status | string | 状态:open(报名中)/ongoing(进行中)/ended(已结束) |
|
||||
| note | string | 备注 |
|
||||
| isJoined | boolean | 当前用户是否已报名 |
|
||||
| isOrganizer | boolean | 当前用户是否为发起人 |
|
||||
|
||||
**状态流转规则(后端自动维护):**
|
||||
- `open`:当前时间 < 开始时间
|
||||
- `ongoing`:开始时间 <= 当前时间 < 开始时间+6小时
|
||||
- `ended`:当前时间 >= 开始时间+6小时
|
||||
|
||||
**可见范围:** 仅酒友可见(同动态信息流规则)
|
||||
|
||||
---
|
||||
|
||||
### 5.2 获取酒局详情
|
||||
```
|
||||
GET /circle/events/:id
|
||||
```
|
||||
|
||||
**响应 data:** 同 5.1 中单个 Event 结构
|
||||
|
||||
---
|
||||
|
||||
### 5.3 发起酒局
|
||||
```
|
||||
POST /circle/events
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"title": "周五夜·老地方火锅局",
|
||||
"time": "2026-07-25T19:00:00.000Z",
|
||||
"location": "渝味晓宇火锅(解放碑店)",
|
||||
"maxPeople": 8,
|
||||
"note": "自带酒水,AA制餐费"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| title | string | 是 | 酒局名称(最长30字) |
|
||||
| time | string | 是 | 开始时间(必须为未来时间) |
|
||||
| location | string | 是 | 地点(最长50字) |
|
||||
| maxPeople | int | 是 | 人数上限(2-50) |
|
||||
| note | string | 否 | 备注(最长200字) |
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"eventId": "evt_1689234567890"
|
||||
}
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 发起人自动算作已报名(joined 初始为1)
|
||||
- 发起人 isOrganizer = true
|
||||
|
||||
---
|
||||
|
||||
### 5.4 报名酒局
|
||||
```
|
||||
POST /circle/events/:id/join
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 人数已满返回 409
|
||||
- 酒局非 open 状态返回 403
|
||||
- 重复报名幂等处理
|
||||
|
||||
---
|
||||
|
||||
### 5.5 取消报名
|
||||
```
|
||||
POST /circle/events/:id/quit
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 发起人不可取消报名(返回 403)
|
||||
- 未报名状态幂等处理
|
||||
|
||||
---
|
||||
|
||||
### 5.6 酒局签到
|
||||
```
|
||||
POST /circle/events/:id/checkin
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
**业务规则:**
|
||||
- 仅 ongoing 状态可签到(返回 403)
|
||||
- 仅已报名用户可签到
|
||||
- 签到后可在参与者信息中标记 checkedIn(可选扩展)
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库表设计参考
|
||||
|
||||
### 6.1 酒友关系表 (friendships)
|
||||
```sql
|
||||
CREATE TABLE friendships (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id VARCHAR(32) NOT NULL,
|
||||
friend_id VARCHAR(32) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE INDEX idx_user_friend (user_id, friend_id),
|
||||
INDEX idx_friend (friend_id)
|
||||
);
|
||||
```
|
||||
> 双向关系存两条记录(A->B 和 B->A),便于查询。
|
||||
|
||||
### 6.2 好友请求表 (friend_requests)
|
||||
```sql
|
||||
CREATE TABLE friend_requests (
|
||||
id VARCHAR(32) PRIMARY KEY,
|
||||
from_user_id VARCHAR(32) NOT NULL,
|
||||
to_user_id VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(64),
|
||||
status ENUM('pending', 'accepted', 'rejected') DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_to_status (to_user_id, status),
|
||||
INDEX idx_from_to (from_user_id, to_user_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 6.3 动态表 (feeds)
|
||||
```sql
|
||||
CREATE TABLE feeds (
|
||||
id VARCHAR(32) PRIMARY KEY,
|
||||
user_id VARCHAR(32) NOT NULL,
|
||||
text VARCHAR(512),
|
||||
drinks JSON,
|
||||
feeling VARCHAR(16),
|
||||
images JSON,
|
||||
record_id VARCHAR(32),
|
||||
visibility ENUM('friends', 'private') DEFAULT 'friends',
|
||||
like_count INT DEFAULT 0,
|
||||
comment_count INT DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user_time (user_id, created_at),
|
||||
INDEX idx_created (created_at)
|
||||
);
|
||||
```
|
||||
|
||||
### 6.4 评论表 (comments)
|
||||
```sql
|
||||
CREATE TABLE comments (
|
||||
id VARCHAR(32) PRIMARY KEY,
|
||||
feed_id VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(32) NOT NULL,
|
||||
content VARCHAR(256) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_feed (feed_id, created_at)
|
||||
);
|
||||
```
|
||||
|
||||
### 6.5 酒局表 (events)
|
||||
```sql
|
||||
CREATE TABLE events (
|
||||
id VARCHAR(32) PRIMARY KEY,
|
||||
organizer_id VARCHAR(32) NOT NULL,
|
||||
title VARCHAR(64) NOT NULL,
|
||||
start_time DATETIME NOT NULL,
|
||||
location VARCHAR(128) NOT NULL,
|
||||
max_people INT NOT NULL DEFAULT 6,
|
||||
joined_count INT NOT NULL DEFAULT 1,
|
||||
note VARCHAR(256),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_organizer (organizer_id),
|
||||
INDEX idx_start_time (start_time)
|
||||
);
|
||||
```
|
||||
|
||||
### 6.6 酒局报名表 (event_participants)
|
||||
```sql
|
||||
CREATE TABLE event_participants (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(32) NOT NULL,
|
||||
checked_in TINYINT(1) DEFAULT 0,
|
||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE INDEX idx_event_user (event_id, user_id),
|
||||
INDEX idx_user (user_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 前端对接说明
|
||||
|
||||
### 7.1 前端调用位置
|
||||
所有接口调用集中在 `common/api.js`,当前为 Mock 实现,方法签名如下:
|
||||
|
||||
```js
|
||||
// 动态
|
||||
GetCircleFeeds({ page, pageSize }) // GET /circle/feeds
|
||||
PublishFeed({ text, images, recordId, visibility }) // POST /circle/feeds
|
||||
LikeFeed({ feedId }) // POST /circle/feeds/:id/like
|
||||
UnlikeFeed({ feedId }) // POST /circle/feeds/:id/unlike
|
||||
GetFeedComments({ feedId }) // GET /circle/feeds/:id/comments
|
||||
AddComment({ feedId, content }) // POST /circle/feeds/:id/comments
|
||||
|
||||
// 酒友
|
||||
GetFriends({ keyword }) // GET /circle/friends
|
||||
GetFriendRequests() // GET /circle/friend-requests
|
||||
SendFriendRequest({ userId }) // POST /circle/friend-requests
|
||||
AcceptFriendRequest({ requestId }) // POST /circle/friend-requests/:id/accept
|
||||
RemoveFriend({ userId }) // DELETE /circle/friends/:userId
|
||||
|
||||
// 酒局
|
||||
GetEvents({ status }) // GET /circle/events
|
||||
GetEventDetail({ eventId }) // GET /circle/events/:id
|
||||
CreateEvent({ title, time, location, maxPeople, note }) // POST /circle/events
|
||||
JoinEvent({ eventId }) // POST /circle/events/:id/join
|
||||
QuitEvent({ eventId }) // POST /circle/events/:id/quit
|
||||
CheckInEvent({ eventId }) // POST /circle/events/:id/checkin
|
||||
```
|
||||
|
||||
### 7.2 响应解包约定
|
||||
前端 SDK 会自动解包 `data.data`,因此接口实际返回给前端的结构为:
|
||||
```json
|
||||
{ "code": 0, "data": { ... } }
|
||||
```
|
||||
前端拿到的就是 `data` 内的对象。
|
||||
|
||||
### 7.3 注意事项
|
||||
1. **字段名严格一致**:前端已按本文档字段名渲染,请勿更改命名(如 `likes` 不能改为 `likeCount`)
|
||||
2. **头像可为空**:avatar 字段允许空字符串,前端有默认占位头像
|
||||
3. **时间格式**:统一 ISO 8601,前端自行格式化
|
||||
4. **内容安全**:text、content、title、note 等 UGC 字段需接入微信 msgSecCheck 审核
|
||||
5. **幂等性**:点赞/取消、报名/取消等操作需幂等,避免前端重试导致计数错误
|
||||
+48
@@ -48,6 +48,48 @@
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/circle/circle",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/circle-detail/circle-detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/circle-publish/circle-publish",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/friends/friends",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-create/event-create",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-detail/event-detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
@@ -70,6 +112,12 @@
|
||||
"iconPath": "static/tab-home.png",
|
||||
"selectedIconPath": "static/tab-home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/circle/circle",
|
||||
"text": "酒友圈",
|
||||
"iconPath": "static/tab-circle.png",
|
||||
"selectedIconPath": "static/tab-circle-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* === 悬浮数据卡片 === */
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Reference in New Issue
Block a user