|
@@ -1,59 +1,49 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="family-feed">
|
|
<view class="family-feed">
|
|
|
- <!-- 标题 -->
|
|
|
|
|
- <view class="feed-header" v-if="showHeader">
|
|
|
|
|
- <view class="feed-header-left">
|
|
|
|
|
|
|
+ <!-- 单条轮播行:标题 + 从下往上翻动 -->
|
|
|
|
|
+ <view class="feed-row" v-if="visibleList.length > 0">
|
|
|
|
|
+ <view class="feed-header" v-if="showHeader">
|
|
|
<text class="feed-title">动态</text>
|
|
<text class="feed-title">动态</text>
|
|
|
- <text class="feed-icon">📢</text>
|
|
|
|
|
- <view v-if="unreadCount > 0" class="feed-count-badge">
|
|
|
|
|
- <text class="feed-count-text">{{ unreadCount }} 条未读</text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <text class="feed-icon animate-spin">📢</text>
|
|
|
</view>
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 未读动态卡片栈(从下往上翻) -->
|
|
|
|
|
- <view v-if="displayItems.length > 0" class="feed-stack">
|
|
|
|
|
- <view
|
|
|
|
|
- v-for="(item, idx) in displayItems"
|
|
|
|
|
- :key="getItemKey(item)"
|
|
|
|
|
- class="feed-card"
|
|
|
|
|
- :class="{'feed-card-closing': isClosing(item)}"
|
|
|
|
|
- :style="getCardStyle(idx, item)"
|
|
|
|
|
- @click="openDetail(item)"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="feed-card-icon-wrap">
|
|
|
|
|
- <text class="feed-card-icon">{{ getItemIcon(item) }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="feed-card-body">
|
|
|
|
|
- <text class="feed-card-title line-clamp-1">{{ getItemTitle(item) }}</text>
|
|
|
|
|
- <text class="feed-card-time" v-if="item.time">{{ item.time }}</text>
|
|
|
|
|
|
|
+ <view class="feed-viewport"
|
|
|
|
|
+ @touchstart="onTouchStart"
|
|
|
|
|
+ @touchend="onTouchEnd"
|
|
|
|
|
+ @touchcancel="onTouchEnd">
|
|
|
|
|
+ <view class="feed-item"
|
|
|
|
|
+ :class="itemClass"
|
|
|
|
|
+ :style="itemInlineStyle"
|
|
|
|
|
+ @click="onItemTap(currentItem)">
|
|
|
|
|
+ <text class="feed-item-text">{{ currentItem.text }}</text>
|
|
|
|
|
+ <view class="feed-item-dot" v-if="currentItem.unread"></view>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="feed-card-arrow">›</text>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <text class="feed-count" v-if="visibleList.length > 1">{{ visibleList.length }} 条</text>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
- <!-- 空态 -->
|
|
|
|
|
<view v-else class="feed-empty">
|
|
<view v-else class="feed-empty">
|
|
|
- <text class="feed-empty-text">暂无未读动态</text>
|
|
|
|
|
|
|
+ <text class="feed-empty-text">暂无动态</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 详情弹窗 -->
|
|
|
|
|
- <view v-if="detailItem" class="feed-detail-mask" @click="closeDetail">
|
|
|
|
|
- <view class="feed-detail-panel" @click.stop="noop">
|
|
|
|
|
|
|
+ <!-- 详情弹层 -->
|
|
|
|
|
+ <view class="feed-detail-mask" v-if="detailVisible" @click="closeDetail">
|
|
|
|
|
+ <view class="feed-detail-card" @click.stop>
|
|
|
<view class="feed-detail-head">
|
|
<view class="feed-detail-head">
|
|
|
- <text class="feed-detail-title">{{ getItemTitle(detailItem) }}</text>
|
|
|
|
|
- <view class="feed-detail-close" @click="closeDetail">
|
|
|
|
|
- <text class="feed-detail-close-text">✕</text>
|
|
|
|
|
|
|
+ <view class="feed-detail-icon-wrap">
|
|
|
|
|
+ <text class="feed-detail-icon">{{ detailIcon }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
- <scroll-view class="feed-detail-content" scroll-y>
|
|
|
|
|
- <text class="feed-detail-body">{{ getItemContent(detailItem) }}</text>
|
|
|
|
|
- </scroll-view>
|
|
|
|
|
- <view class="feed-detail-foot">
|
|
|
|
|
- <text class="feed-detail-time" v-if="detailItem.time">{{ detailItem.time }}</text>
|
|
|
|
|
- <view class="feed-detail-btn" @click="closeDetail">
|
|
|
|
|
- <text class="feed-detail-btn-text">我知道了</text>
|
|
|
|
|
|
|
+ <view class="feed-detail-head-texts">
|
|
|
|
|
+ <text class="feed-detail-type">{{ detailTypeLabel }}</text>
|
|
|
|
|
+ <text class="feed-detail-time" v-if="detailTime">{{ detailTime }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <text class="feed-detail-close" @click="closeDetail">×</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="feed-detail-title">{{ detailTitle }}</text>
|
|
|
|
|
+ <text class="feed-detail-content">{{ detailContent }}</text>
|
|
|
|
|
+ <view class="feed-detail-amount" v-if="detailAmount">
|
|
|
|
|
+ <text class="feed-detail-amount-text">{{ detailAmount }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="feed-detail-btn" @click="closeDetail">
|
|
|
|
|
+ <text class="feed-detail-btn-text">知道了</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -61,121 +51,164 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
|
|
+var TYPE_META = {
|
|
|
|
|
+ energy: { icon: '⚡', label: '能量变动' },
|
|
|
|
|
+ points: { icon: '🪙', label: '积分变动' },
|
|
|
|
|
+ cf: { icon: '💰', label: 'CF 值变动' },
|
|
|
|
|
+ task: { icon: '✅', label: '任务完成' },
|
|
|
|
|
+ notice: { icon: '📢', label: '家庭公告' },
|
|
|
|
|
+ notification: { icon: '📬', label: '系统通知' },
|
|
|
|
|
+ feed: { icon: '📣', label: '家庭动态' }
|
|
|
|
|
+}
|
|
|
|
|
+var DEFAULT_META = { icon: '📣', label: '消息' }
|
|
|
|
|
+
|
|
|
export default {
|
|
export default {
|
|
|
name: 'FamilyFeed',
|
|
name: 'FamilyFeed',
|
|
|
props: {
|
|
props: {
|
|
|
- items: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- default: function() { return [] }
|
|
|
|
|
- },
|
|
|
|
|
- notices: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- default: function() { return [] }
|
|
|
|
|
- },
|
|
|
|
|
- showHeader: {
|
|
|
|
|
- type: Boolean,
|
|
|
|
|
- default: true
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ items: { type: Array, default: function() { return [] } },
|
|
|
|
|
+ notices: { type: Array, default: function() { return [] } },
|
|
|
|
|
+ showHeader: { type: Boolean, default: true },
|
|
|
|
|
+ interval: { type: Number, default: 3000 }
|
|
|
},
|
|
},
|
|
|
data: function() {
|
|
data: function() {
|
|
|
return {
|
|
return {
|
|
|
- detailItem: null,
|
|
|
|
|
- closingKeys: []
|
|
|
|
|
|
|
+ // state: 'entering' | 'visible' | 'leaving'
|
|
|
|
|
+ // entering: jump to translateY(100%), next frame animates to 0
|
|
|
|
|
+ // visible: displayed for interval ms, then animates out
|
|
|
|
|
+ // leaving: animates to translateY(-100%), then advance to next item
|
|
|
|
|
+ state: 'entering',
|
|
|
|
|
+ currentIndex: 0,
|
|
|
|
|
+ _touchPaused: false,
|
|
|
|
|
+ detail: null,
|
|
|
|
|
+ detailVisible: false,
|
|
|
|
|
+ _timer: null
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
|
- // 公告(notices prop)优先展示,再拼未读动态(items),按 feedKey 去重,
|
|
|
|
|
- // 避免 familyFeed 在父组件被 slice(0,10) 截断时公告漏显
|
|
|
|
|
- displayItems: function() {
|
|
|
|
|
- var seen = {}
|
|
|
|
|
- var result = []
|
|
|
|
|
- var i
|
|
|
|
|
|
|
+ visibleList: function() {
|
|
|
|
|
+ var list = [], seen = {}, i
|
|
|
|
|
+ var items = this.items || []
|
|
|
|
|
+ for (i = 0; i < items.length; i++) {
|
|
|
|
|
+ var it = items[i]
|
|
|
|
|
+ if (!it) continue
|
|
|
|
|
+ var key = it._feedKey || ''
|
|
|
|
|
+ if (key && seen[key]) continue
|
|
|
|
|
+ if (key) seen[key] = true
|
|
|
|
|
+ list.push(it)
|
|
|
|
|
+ }
|
|
|
var notices = this.notices || []
|
|
var notices = this.notices || []
|
|
|
for (i = 0; i < notices.length; i++) {
|
|
for (i = 0; i < notices.length; i++) {
|
|
|
var n = notices[i]
|
|
var n = notices[i]
|
|
|
if (!n) continue
|
|
if (!n) continue
|
|
|
- var raw = n.notice || {}
|
|
|
|
|
- var nKey = 'notice-' + (raw.id !== undefined ? raw.id : '')
|
|
|
|
|
- if (!nKey || seen[nKey]) continue
|
|
|
|
|
- seen[nKey] = true
|
|
|
|
|
- result.push({
|
|
|
|
|
- text: n.text || raw.title || '公告',
|
|
|
|
|
|
|
+ var raw = n.notice || n
|
|
|
|
|
+ var nkey = 'notice-' + (raw.id !== undefined ? raw.id : '')
|
|
|
|
|
+ if (nkey !== 'notice-' && seen[nkey]) continue
|
|
|
|
|
+ if (nkey !== 'notice-') seen[nkey] = true
|
|
|
|
|
+ list.push({
|
|
|
|
|
+ text: n.text || raw.title || '',
|
|
|
time: n.time || '',
|
|
time: n.time || '',
|
|
|
type: 'notice',
|
|
type: 'notice',
|
|
|
- title: raw.title || n.text || '公告',
|
|
|
|
|
|
|
+ title: raw.title || '公告',
|
|
|
content: raw.content || '',
|
|
content: raw.content || '',
|
|
|
|
|
+ amount: '',
|
|
|
unread: false,
|
|
unread: false,
|
|
|
- _feedKey: nKey,
|
|
|
|
|
|
|
+ _feedKey: nkey,
|
|
|
_srcId: raw.id
|
|
_srcId: raw.id
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
- var items = this.items || []
|
|
|
|
|
- for (i = 0; i < items.length; i++) {
|
|
|
|
|
- var item = items[i]
|
|
|
|
|
- if (!item) continue
|
|
|
|
|
- var key = this.getItemKey(item)
|
|
|
|
|
- if (seen[key]) continue
|
|
|
|
|
- seen[key] = true
|
|
|
|
|
- result.push(item)
|
|
|
|
|
- }
|
|
|
|
|
- return result
|
|
|
|
|
|
|
+ return list
|
|
|
},
|
|
},
|
|
|
- unreadCount: function() {
|
|
|
|
|
- return this.displayItems.length
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- methods: {
|
|
|
|
|
- getItemKey: function(item) {
|
|
|
|
|
- return item._feedKey || ('feed-' + (item.title || '') + '-' + (item.time || ''))
|
|
|
|
|
|
|
+ currentItem: function() {
|
|
|
|
|
+ var list = this.visibleList
|
|
|
|
|
+ if (!list || list.length === 0) return null
|
|
|
|
|
+ return list[this.currentIndex % list.length]
|
|
|
},
|
|
},
|
|
|
- getItemIcon: function(item) {
|
|
|
|
|
- if (item.type === 'notification') return '📬'
|
|
|
|
|
- if (item.type === 'notice') return '📢'
|
|
|
|
|
- if (item.type === 'task') return '🎯'
|
|
|
|
|
- return '✨'
|
|
|
|
|
|
|
+ itemClass: function() {
|
|
|
|
|
+ if (this.state === 'visible') return 'feed-item-visible'
|
|
|
|
|
+ if (this.state === 'leaving') return 'feed-item-leaving'
|
|
|
|
|
+ return 'feed-item-entering'
|
|
|
},
|
|
},
|
|
|
- getItemTitle: function(item) {
|
|
|
|
|
- return item.title || item.text || '动态'
|
|
|
|
|
|
|
+ itemInlineStyle: function() {
|
|
|
|
|
+ var style = {}
|
|
|
|
|
+ if (this.state === 'entering') { style.transition = 'none' }
|
|
|
|
|
+ return style
|
|
|
},
|
|
},
|
|
|
- getItemContent: function(item) {
|
|
|
|
|
- return item.content || item.text || '暂无更多内容'
|
|
|
|
|
|
|
+ detailMeta: function() {
|
|
|
|
|
+ var type = (this.detail && this.detail.type) || ''
|
|
|
|
|
+ return TYPE_META[type] || DEFAULT_META
|
|
|
},
|
|
},
|
|
|
- // 入场错峰:从下往上逐张翻入(最底下的卡片先入场)
|
|
|
|
|
- getCardStyle: function(idx, item) {
|
|
|
|
|
- if (this.isClosing(item)) {
|
|
|
|
|
- return 'animation-delay:0ms;'
|
|
|
|
|
|
|
+ detailIcon: function() { return this.detailMeta.icon },
|
|
|
|
|
+ detailTypeLabel: function() { return this.detailMeta.label },
|
|
|
|
|
+ detailTitle: function() { return (this.detail && this.detail.title) || '消息详情' },
|
|
|
|
|
+ detailContent: function() { return (this.detail && this.detail.content) || (this.detail && this.detail.text) || '暂无详细内容' },
|
|
|
|
|
+ detailTime: function() { return (this.detail && this.detail.time) || '' },
|
|
|
|
|
+ detailAmount: function() { return (this.detail && this.detail.amount) || '' }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ visibleList: function(newList) {
|
|
|
|
|
+ if (!newList || newList.length === 0) {
|
|
|
|
|
+ this._stopAutoPlay()
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- var total = this.displayItems.length
|
|
|
|
|
- var delay = Math.min((total - 1 - idx) * 120, 600)
|
|
|
|
|
- return 'animation-delay:' + delay + 'ms;'
|
|
|
|
|
- },
|
|
|
|
|
- isClosing: function(item) {
|
|
|
|
|
- var key = this.getItemKey(item)
|
|
|
|
|
- return this.closingKeys.indexOf(key) !== -1
|
|
|
|
|
|
|
+ if (this.currentIndex >= newList.length) { this.currentIndex = 0 }
|
|
|
|
|
+ this.state = 'entering'
|
|
|
|
|
+ this._startAutoPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted: function() { this._startAutoPlay() },
|
|
|
|
|
+ beforeDestroy: function() { this._stopAutoPlay() },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ _startAutoPlay: function() {
|
|
|
|
|
+ this._stopAutoPlay()
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ this._timer = setTimeout(function() { self._tick() }, 50)
|
|
|
},
|
|
},
|
|
|
- openDetail: function(item) {
|
|
|
|
|
- if (this.isClosing(item)) return
|
|
|
|
|
- this.detailItem = item
|
|
|
|
|
|
|
+ _stopAutoPlay: function() {
|
|
|
|
|
+ if (this._timer) { clearTimeout(this._timer); this._timer = null }
|
|
|
},
|
|
},
|
|
|
- // 关闭:先播放翻出动画,再通知父组件标记已读并从列表移除
|
|
|
|
|
- closeDetail: function() {
|
|
|
|
|
|
|
+ _tick: function() {
|
|
|
var self = this
|
|
var self = this
|
|
|
- var item = this.detailItem
|
|
|
|
|
- if (!item) return
|
|
|
|
|
- this.detailItem = null
|
|
|
|
|
- var key = this.getItemKey(item)
|
|
|
|
|
- if (this.closingKeys.indexOf(key) === -1) {
|
|
|
|
|
- this.closingKeys.push(key)
|
|
|
|
|
|
|
+ if (self.visibleList.length === 0) return
|
|
|
|
|
+ if (self._touchPaused || self.detailVisible) {
|
|
|
|
|
+ self._timer = setTimeout(function() { self._tick() }, 200)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (self.state === 'entering') {
|
|
|
|
|
+ self._timer = setTimeout(function() {
|
|
|
|
|
+ if (self._touchPaused || self.detailVisible) { self._tick(); return }
|
|
|
|
|
+ self.state = 'visible'
|
|
|
|
|
+ self._timer = setTimeout(function() { self._tick() }, 400)
|
|
|
|
|
+ }, 50)
|
|
|
|
|
+ } else if (self.state === 'visible') {
|
|
|
|
|
+ self._timer = setTimeout(function() {
|
|
|
|
|
+ if (self._touchPaused || self.detailVisible) { self._tick(); return }
|
|
|
|
|
+ self.state = 'leaving'
|
|
|
|
|
+ self._timer = setTimeout(function() { self._tick() }, 400)
|
|
|
|
|
+ }, self.interval)
|
|
|
|
|
+ } else if (self.state === 'leaving') {
|
|
|
|
|
+ self._timer = setTimeout(function() {
|
|
|
|
|
+ if (self._touchPaused || self.detailVisible) { self._tick(); return }
|
|
|
|
|
+ self.currentIndex = (self.currentIndex + 1) % (self.visibleList.length || 1)
|
|
|
|
|
+ self.state = 'entering'
|
|
|
|
|
+ self._tick()
|
|
|
|
|
+ }, 400)
|
|
|
}
|
|
}
|
|
|
- setTimeout(function() {
|
|
|
|
|
- var keys = self.closingKeys.slice()
|
|
|
|
|
- var i = keys.indexOf(key)
|
|
|
|
|
- if (i !== -1) keys.splice(i, 1)
|
|
|
|
|
- self.closingKeys = keys
|
|
|
|
|
- self.$emit('viewed', item)
|
|
|
|
|
- }, 380)
|
|
|
|
|
},
|
|
},
|
|
|
- noop: function() {}
|
|
|
|
|
|
|
+ onTouchStart: function() { this._touchPaused = true; this._stopAutoPlay() },
|
|
|
|
|
+ onTouchEnd: function() { this._touchPaused = false; this._startAutoPlay() },
|
|
|
|
|
+ onItemTap: function(item) {
|
|
|
|
|
+ if (!item) return
|
|
|
|
|
+ this.detail = item
|
|
|
|
|
+ this.detailVisible = true
|
|
|
|
|
+ this._touchPaused = false
|
|
|
|
|
+ this._stopAutoPlay()
|
|
|
|
|
+ this.$emit('viewed', item)
|
|
|
|
|
+ },
|
|
|
|
|
+ closeDetail: function() {
|
|
|
|
|
+ this.detailVisible = false
|
|
|
|
|
+ this.detail = null
|
|
|
|
|
+ this._startAutoPlay()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
@@ -190,257 +223,138 @@ export default {
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-header {
|
|
|
|
|
|
|
+.feed-row {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 6rpx 4rpx 12rpx;
|
|
|
|
|
|
|
+ height: 48rpx;
|
|
|
|
|
+ line-height: 48rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-header-left {
|
|
|
|
|
|
|
+.feed-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 6rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-title {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #333;
|
|
|
|
|
|
|
+ gap: 4rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-right: 12rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-icon {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.feed-title { font-size: 24rpx; font-weight: bold; color: #333; }
|
|
|
|
|
+.feed-icon { font-size: 20rpx; }
|
|
|
|
|
|
|
|
-.feed-count-badge {
|
|
|
|
|
- margin-left: 12rpx;
|
|
|
|
|
- background: #ff5b5b;
|
|
|
|
|
- border-radius: 20rpx;
|
|
|
|
|
- padding: 2rpx 14rpx;
|
|
|
|
|
|
|
+@keyframes spin {
|
|
|
|
|
+ 0%, 100% { transform: rotate(0deg); }
|
|
|
|
|
+ 25% { transform: rotate(-15deg); }
|
|
|
|
|
+ 75% { transform: rotate(15deg); }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.feed-count-text {
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
|
|
+.animate-spin {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ animation: spin 1.2s ease-in-out infinite;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* ===== 翻转卡片栈 ===== */
|
|
|
|
|
-.feed-stack {
|
|
|
|
|
- perspective: 800rpx;
|
|
|
|
|
|
|
+/* 视口:固定一行高度,溢出裁剪 */
|
|
|
|
|
+.feed-viewport {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ height: 48rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ position: relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-card {
|
|
|
|
|
|
|
+/* 条目:绝对定位叠放,CSS transition 驱动翻转 */
|
|
|
|
|
+.feed-item {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0; left: 0; right: 0;
|
|
|
|
|
+ height: 48rpx;
|
|
|
|
|
+ line-height: 48rpx;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
- padding: 18rpx 20rpx;
|
|
|
|
|
- background: #f7f8fa;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- transform-origin: center bottom;
|
|
|
|
|
- transform-style: preserve-3d;
|
|
|
|
|
- animation: feedFlipUp 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
|
|
|
|
|
+ transition: transform 0.4s ease-in-out;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-card:active {
|
|
|
|
|
- opacity: 0.85;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.feed-item-entering { transform: translateY(100%); }
|
|
|
|
|
+.feed-item-visible { transform: translateY(0); }
|
|
|
|
|
+.feed-item-leaving { transform: translateY(-100%); }
|
|
|
|
|
|
|
|
-/* 入场:从下往上翻入 */
|
|
|
|
|
-@keyframes feedFlipUp {
|
|
|
|
|
- 0% {
|
|
|
|
|
- transform: translateY(40rpx) rotateX(-90deg);
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- }
|
|
|
|
|
- 100% {
|
|
|
|
|
- transform: translateY(0) rotateX(0deg);
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.feed-item-text {
|
|
|
|
|
+ flex: 1; min-width: 0;
|
|
|
|
|
+ font-size: 24rpx; color: #666;
|
|
|
|
|
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 关闭:翻出(向下折叠 + 淡出) */
|
|
|
|
|
-.feed-card-closing {
|
|
|
|
|
- animation: feedFlipDown 0.35s ease-in both;
|
|
|
|
|
|
|
+.feed-item-dot {
|
|
|
|
|
+ width: 12rpx; height: 12rpx;
|
|
|
|
|
+ border-radius: 50%; background: #EF4444;
|
|
|
|
|
+ margin-left: 8rpx; flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-@keyframes feedFlipDown {
|
|
|
|
|
- 0% {
|
|
|
|
|
- transform: translateY(0) rotateX(0deg);
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- }
|
|
|
|
|
- 100% {
|
|
|
|
|
- transform: translateY(-20rpx) rotateX(-75deg);
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.feed-count {
|
|
|
|
|
+ flex-shrink: 0; margin-left: 12rpx;
|
|
|
|
|
+ font-size: 20rpx; color: #F97316;
|
|
|
|
|
+ background: rgba(249,115,22,0.1);
|
|
|
|
|
+ border-radius: 999rpx; padding: 2rpx 12rpx; line-height: 1.4;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-card-icon-wrap {
|
|
|
|
|
- width: 56rpx;
|
|
|
|
|
- height: 56rpx;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- margin-right: 16rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-card-icon {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-card-body {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-card-title {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- line-height: 34rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-card-time {
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- color: #999;
|
|
|
|
|
- margin-top: 4rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-card-arrow {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #ccc;
|
|
|
|
|
- margin-left: 12rpx;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
|
|
+.feed-empty {
|
|
|
|
|
+ height: 48rpx; line-height: 48rpx; text-align: center;
|
|
|
}
|
|
}
|
|
|
|
|
+.feed-empty-text { font-size: 24rpx; color: #ccc; }
|
|
|
|
|
|
|
|
-/* ===== 详情弹窗 ===== */
|
|
|
|
|
|
|
+/* ===== 详情弹层 ===== */
|
|
|
.feed-detail-mask {
|
|
.feed-detail-mask {
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
- z-index: 999;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- padding: 60rpx 40rpx;
|
|
|
|
|
- perspective: 900rpx;
|
|
|
|
|
|
|
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
|
|
|
+ background: rgba(0,0,0,0.5); z-index: 2000;
|
|
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
|
|
+ padding: 0 60rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-detail-panel {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 20rpx;
|
|
|
|
|
- padding: 30rpx 28rpx;
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- animation: feedPanelIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
|
|
|
|
|
+.feed-detail-card {
|
|
|
|
|
+ width: 100%; max-height: 80vh; overflow-y: auto;
|
|
|
|
|
+ background: #fff; border-radius: 24rpx;
|
|
|
|
|
+ padding: 32rpx 28rpx 24rpx;
|
|
|
|
|
+ animation: feedDetailIn 0.25s ease;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-@keyframes feedPanelIn {
|
|
|
|
|
- 0% {
|
|
|
|
|
- transform: translateY(80rpx) rotateX(40deg);
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- }
|
|
|
|
|
- 100% {
|
|
|
|
|
- transform: translateY(0) rotateX(0deg);
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+@keyframes feedDetailIn {
|
|
|
|
|
+ from { opacity: 0; transform: scale(0.92); }
|
|
|
|
|
+ to { opacity: 1; transform: scale(1); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.feed-detail-head {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: row;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- padding-bottom: 20rpx;
|
|
|
|
|
- border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.feed-detail-head { display: flex; flex-direction: row; align-items: center; }
|
|
|
|
|
|
|
|
-.feed-detail-title {
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- margin-right: 16rpx;
|
|
|
|
|
|
|
+.feed-detail-icon-wrap {
|
|
|
|
|
+ width: 64rpx; height: 64rpx; border-radius: 20rpx;
|
|
|
|
|
+ background: rgba(249,115,22,0.1);
|
|
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
|
|
+ margin-right: 16rpx; flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
+.feed-detail-icon { font-size: 34rpx; }
|
|
|
|
|
|
|
|
-.feed-detail-close {
|
|
|
|
|
- width: 48rpx;
|
|
|
|
|
- height: 48rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.feed-detail-head-texts { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
|
|
|
+.feed-detail-type { font-size: 26rpx; font-weight: bold; color: #333; }
|
|
|
|
|
+.feed-detail-time { font-size: 20rpx; color: #999; margin-top: 4rpx; }
|
|
|
|
|
+.feed-detail-close { font-size: 40rpx; color: #bbb; line-height: 1; padding: 0 8rpx; flex-shrink: 0; }
|
|
|
|
|
|
|
|
-.feed-detail-close-text {
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- color: #999;
|
|
|
|
|
|
|
+.feed-detail-title {
|
|
|
|
|
+ display: block; font-size: 30rpx; font-weight: bold; color: #333;
|
|
|
|
|
+ line-height: 1.4; margin-top: 24rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
.feed-detail-content {
|
|
.feed-detail-content {
|
|
|
- max-height: 480rpx;
|
|
|
|
|
- margin-top: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-detail-body {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: #555;
|
|
|
|
|
- line-height: 42rpx;
|
|
|
|
|
- word-break: break-all;
|
|
|
|
|
- display: block;
|
|
|
|
|
|
|
+ display: block; font-size: 26rpx; color: #666; line-height: 1.6; margin-top: 12rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.feed-detail-foot {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: row;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- margin-top: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-detail-time {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #999;
|
|
|
|
|
|
|
+.feed-detail-amount {
|
|
|
|
|
+ display: inline-block; margin-top: 16rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #F97316, #FB923C);
|
|
|
|
|
+ border-radius: 999rpx; padding: 6rpx 20rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+.feed-detail-amount-text { font-size: 24rpx; color: #fff; font-weight: bold; }
|
|
|
.feed-detail-btn {
|
|
.feed-detail-btn {
|
|
|
- background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
|
|
|
- border-radius: 30rpx;
|
|
|
|
|
- padding: 12rpx 40rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-detail-btn-text {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* ===== 空态 ===== */
|
|
|
|
|
-.feed-empty {
|
|
|
|
|
- height: 60rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.feed-empty-text {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #ccc;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.line-clamp-1 {
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- text-overflow: ellipsis;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
|
|
+ margin-top: 28rpx; height: 76rpx; border-radius: 38rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #F97316, #FB923C);
|
|
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
}
|
|
}
|
|
|
|
|
+.feed-detail-btn-text { font-size: 28rpx; color: #fff; font-weight: bold; }
|
|
|
</style>
|
|
</style>
|