feat: add read-only smoke share page and profile share entry
This commit is contained in:
@@ -51,3 +51,15 @@ export function unlockAiAdvice(data) {
|
||||
export function getStats(params = {}) {
|
||||
return request.get('/smoke/stats', params)
|
||||
}
|
||||
|
||||
export function createShare(data = {}) {
|
||||
return request.post('/smoke/share', data)
|
||||
}
|
||||
|
||||
export function getShareData(shareToken, params = {}) {
|
||||
return request.get(`/smoke/share/${shareToken}`, params)
|
||||
}
|
||||
|
||||
export function revokeShare(shareToken) {
|
||||
return request.post(`/smoke/share/${shareToken}/revoke`)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
"navigationBarTitleText": "历史记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/share/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "戒烟分享"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/index",
|
||||
"style": {
|
||||
|
||||
+101
-1
@@ -7,6 +7,26 @@
|
||||
|
||||
<view class="section">
|
||||
<view class="menu-list">
|
||||
<view class="menu-item">
|
||||
<view class="menu-icon menu-icon-green">🔗</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">分享戒烟记录</text>
|
||||
<text class="menu-desc">{{ shareDesc }}</text>
|
||||
</view>
|
||||
<button class="share-btn" open-type="share" :disabled="shareLoading || !shareToken">
|
||||
{{ shareLoading ? '生成中' : '分享' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="prepareShareToken(true)">
|
||||
<view class="menu-icon menu-icon-gray">🔄</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">刷新分享链接</text>
|
||||
<text class="menu-desc">生成新的有效分享令牌</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="goOnboarding">
|
||||
<view class="menu-icon menu-icon-green">📝</view>
|
||||
<view class="menu-content">
|
||||
@@ -46,16 +66,68 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onShareAppMessage } from '@dcloudio/uni-app'
|
||||
import { createShare } from '@/api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLogin } from '@/hooks/useLogin'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { waitForLogin } = useLogin()
|
||||
|
||||
const shareToken = ref('')
|
||||
const shareExpireAt = ref('')
|
||||
const shareLoading = ref(false)
|
||||
|
||||
const userName = computed(() => userStore.user?.nickname || '戒烟用户')
|
||||
const userAvatar = computed(() => userStore.user?.avatar_url || '/static/images/default-avatar.png')
|
||||
|
||||
const shareDesc = computed(() => {
|
||||
if (!shareToken.value) {
|
||||
return shareLoading.value ? '正在生成分享信息...' : '先生成分享令牌后即可分享给朋友'
|
||||
}
|
||||
return `有效期至 ${formatExpire(shareExpireAt.value)},仅查看权限`
|
||||
})
|
||||
|
||||
const sharePath = computed(() => {
|
||||
if (!shareToken.value) {
|
||||
return '/pages/index/index'
|
||||
}
|
||||
return `/pages/share/index?share_token=${shareToken.value}`
|
||||
})
|
||||
|
||||
function formatExpire(value) {
|
||||
if (!value) return '--'
|
||||
const d = new Date(value)
|
||||
if (Number.isNaN(d.getTime())) return value
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${y}-${m}-${day} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
async function prepareShareToken(showToast = false) {
|
||||
if (shareLoading.value) return
|
||||
shareLoading.value = true
|
||||
try {
|
||||
const res = await createShare({ days: 7 })
|
||||
shareToken.value = res.data?.share_token || ''
|
||||
shareExpireAt.value = res.data?.expire_at || ''
|
||||
if (showToast) {
|
||||
uni.showToast({ title: '分享链接已刷新', icon: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('prepareShareToken error:', e)
|
||||
if (showToast) {
|
||||
uni.showToast({ title: '生成分享失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
shareLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goOnboarding() {
|
||||
uni.navigateTo({ url: '/pages/onboarding/index' })
|
||||
}
|
||||
@@ -99,8 +171,16 @@ function logout() {
|
||||
})
|
||||
}
|
||||
|
||||
onShareAppMessage(() => {
|
||||
return {
|
||||
title: `${userName.value}的戒烟记录(仅查看)`,
|
||||
path: sharePath.value
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await waitForLogin()
|
||||
await prepareShareToken(false)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -189,6 +269,26 @@ onMounted(async () => {
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
margin: 0;
|
||||
padding: 10rpx 20rpx;
|
||||
line-height: 1.4;
|
||||
font-size: 24rpx;
|
||||
border: none;
|
||||
border-radius: 999rpx;
|
||||
color: #ffffff;
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.share-btn[disabled] {
|
||||
background: #9ca3af;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
text-align: center;
|
||||
padding: 28rpx;
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="loading" class="state-wrap">
|
||||
<text class="state-text">加载分享数据中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="errorText" class="state-wrap">
|
||||
<text class="state-text">{{ errorText }}</text>
|
||||
<button class="retry-btn" @tap="reload">重新加载</button>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view class="owner-card">
|
||||
<image class="avatar" :src="owner.avatar_url || defaultAvatar" mode="aspectFill"></image>
|
||||
<view class="owner-main">
|
||||
<text class="owner-name">{{ owner.nickname || '戒烟用户' }}</text>
|
||||
<text class="owner-desc">分享了自己的戒烟记录(只读)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="overview-grid">
|
||||
<view class="overview-item">
|
||||
<text class="overview-label">今日吸烟</text>
|
||||
<text class="overview-value">{{ overview.today_count || 0 }}</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-label">今日忍住</text>
|
||||
<text class="overview-value">{{ overview.resisted_count || 0 }}</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-label">连续记录</text>
|
||||
<text class="overview-value">{{ overview.streak_days || 0 }}天</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-label">较昨日变化</text>
|
||||
<text class="overview-value" :class="overview.exceeded_yesterday ? 'warning' : 'success'">
|
||||
{{ overview.exceeded_yesterday ? '+' : '-' }}{{ overview.reduced_from_yesterday || 0 }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">统计报表</text>
|
||||
<view class="range-tabs">
|
||||
<view
|
||||
v-for="item in rangeTabs"
|
||||
:key="item.value"
|
||||
class="range-item"
|
||||
:class="{ active: range === item.value }"
|
||||
@tap="switchRange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stats-block">
|
||||
<text class="stats-key">日均支数</text>
|
||||
<text class="stats-val">{{ stats.daily_average || 0 }}</text>
|
||||
</view>
|
||||
<view class="stats-block">
|
||||
<text class="stats-key">变化幅度</text>
|
||||
<text class="stats-val">{{ stats.change_percent || 0 }}%</text>
|
||||
</view>
|
||||
<view class="stats-block">
|
||||
<text class="stats-key">范围忍住</text>
|
||||
<text class="stats-val">{{ stats.resisted_total || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="trend-wrap">
|
||||
<view v-for="(item, index) in stats.trend || []" :key="index" class="trend-item">
|
||||
<text class="trend-label">{{ item.label }}</text>
|
||||
<view class="trend-bar-bg">
|
||||
<view class="trend-bar" :style="{ width: trendWidth(item.count) + '%' }"></view>
|
||||
</view>
|
||||
<text class="trend-count">{{ item.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">记录详情(只读)</text>
|
||||
<view class="range-tabs">
|
||||
<view
|
||||
v-for="item in logTypeTabs"
|
||||
:key="item.value"
|
||||
class="range-item"
|
||||
:class="{ active: logType === item.value }"
|
||||
@tap="switchLogType(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="logs.length === 0" class="empty-box">
|
||||
<text class="empty-text">暂无记录</text>
|
||||
</view>
|
||||
<view v-else class="log-list">
|
||||
<view v-for="item in logs" :key="item.id" class="log-item" @tap="showDetail(item)">
|
||||
<view class="log-top">
|
||||
<text class="log-time">{{ displayTime(item) }}</text>
|
||||
<text class="log-type" :class="resolveType(item) === 'resisted' ? 'resisted' : 'smoke'">
|
||||
{{ resolveType(item) === 'resisted' ? '已忍住' : '已抽烟' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="log-meta">
|
||||
<text>数量:{{ item.num ?? 0 }}</text>
|
||||
<text>强度:{{ levelLabel(item.level) }}</text>
|
||||
</view>
|
||||
<text class="log-remark">{{ (item.remark && String(item.remark).trim()) || '无备注' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
v-if="logs.length < total"
|
||||
class="load-more"
|
||||
:disabled="loadingMore"
|
||||
@tap="loadMore"
|
||||
>
|
||||
{{ loadingMore ? '加载中...' : '加载更多' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getShareData } from '@/api'
|
||||
|
||||
const defaultAvatar = '/static/images/default-avatar.png'
|
||||
|
||||
const loading = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
const errorText = ref('')
|
||||
|
||||
const shareToken = ref('')
|
||||
const range = ref('week')
|
||||
const logType = ref('all')
|
||||
|
||||
const owner = ref({})
|
||||
const overview = ref({})
|
||||
const stats = ref({})
|
||||
const logs = ref([])
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
const rangeTabs = [
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '年', value: 'year' }
|
||||
]
|
||||
|
||||
const logTypeTabs = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '已抽烟', value: 'smoke' },
|
||||
{ label: '已忍住', value: 'resisted' }
|
||||
]
|
||||
|
||||
const maxTrend = computed(() => {
|
||||
const values = (stats.value?.trend || []).map((item) => Number(item.count) || 0)
|
||||
const max = Math.max(...values, 0)
|
||||
return max <= 0 ? 1 : max
|
||||
})
|
||||
|
||||
function trendWidth(count) {
|
||||
const n = Number(count) || 0
|
||||
return Math.max(8, Math.round((n / maxTrend.value) * 100))
|
||||
}
|
||||
|
||||
function resolveType(item) {
|
||||
if ((item?.level || 0) === 0 && (item?.num || 0) === 0) {
|
||||
return 'resisted'
|
||||
}
|
||||
return 'smoke'
|
||||
}
|
||||
|
||||
function levelLabel(level) {
|
||||
const value = Number(level)
|
||||
if (Number.isNaN(value)) return '未知'
|
||||
if (value <= 1) return '轻微'
|
||||
if (value === 2) return '中等'
|
||||
if (value === 3) return '明显'
|
||||
if (value === 4) return '强烈'
|
||||
return '极强'
|
||||
}
|
||||
|
||||
function displayTime(item) {
|
||||
if (item?.smoke_at) {
|
||||
return String(item.smoke_at).replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
if (item?.smoke_time) {
|
||||
return String(item.smoke_time).slice(0, 10)
|
||||
}
|
||||
if (item?.createtime) {
|
||||
const ts = Number(item.createtime)
|
||||
if (!Number.isNaN(ts) && ts > 0) {
|
||||
const d = new Date(ts * 1000)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const h = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
const s = String(d.getSeconds()).padStart(2, '0')
|
||||
return `${y}-${m}-${day} ${h}:${mm}:${s}`
|
||||
}
|
||||
}
|
||||
return '--'
|
||||
}
|
||||
|
||||
async function fetchShare(resetLogs = false) {
|
||||
if (!shareToken.value) {
|
||||
errorText.value = '分享参数缺失'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (resetLogs) {
|
||||
page.value = 1
|
||||
logs.value = []
|
||||
}
|
||||
|
||||
const params = {
|
||||
range: range.value,
|
||||
type: logType.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getShareData(shareToken.value, params)
|
||||
const payload = res.data || {}
|
||||
owner.value = payload.owner || {}
|
||||
overview.value = payload.overview || {}
|
||||
stats.value = payload.stats || {}
|
||||
|
||||
const logPayload = payload.logs || {}
|
||||
const items = logPayload.items || []
|
||||
if (resetLogs) {
|
||||
logs.value = items
|
||||
} else {
|
||||
logs.value = [...logs.value, ...items]
|
||||
}
|
||||
total.value = Number(logPayload.total || 0)
|
||||
page.value = Number(logPayload.page || page.value)
|
||||
} catch (e) {
|
||||
console.error('fetchShare error:', e)
|
||||
errorText.value = e?.message || '分享已失效或不可访问'
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function switchRange(next) {
|
||||
if (range.value === next) return
|
||||
range.value = next
|
||||
loading.value = true
|
||||
errorText.value = ''
|
||||
await fetchShare(true)
|
||||
}
|
||||
|
||||
async function switchLogType(next) {
|
||||
if (logType.value === next) return
|
||||
logType.value = next
|
||||
loading.value = true
|
||||
errorText.value = ''
|
||||
await fetchShare(true)
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loadingMore.value || logs.value.length >= total.value) return
|
||||
loadingMore.value = true
|
||||
page.value += 1
|
||||
await fetchShare(false)
|
||||
}
|
||||
|
||||
function showDetail(item) {
|
||||
uni.showModal({
|
||||
title: '记录详情',
|
||||
showCancel: false,
|
||||
content: [
|
||||
`时间:${displayTime(item)}`,
|
||||
`类型:${resolveType(item) === 'resisted' ? '已忍住' : '已抽烟'}`,
|
||||
`数量:${item.num ?? 0}`,
|
||||
`强度:${levelLabel(item.level)}`,
|
||||
`备注:${(item.remark && String(item.remark).trim()) || '无备注'}`
|
||||
].join('\n')
|
||||
})
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
loading.value = true
|
||||
errorText.value = ''
|
||||
await fetchShare(true)
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
shareToken.value = String(options?.share_token || '').trim()
|
||||
await fetchShare(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.owner-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.owner-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.owner-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.owner-desc {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
margin-top: 20rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.overview-item {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.overview-label {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.overview-value {
|
||||
margin-top: 10rpx;
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.overview-value.success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.overview-value.warning {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.range-tabs {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.range-item {
|
||||
padding: 8rpx 18rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
border-radius: 999rpx;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.range-item.active {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-top: 18rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.stats-block {
|
||||
padding: 14rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.stats-key {
|
||||
font-size: 20rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stats-val {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.trend-wrap {
|
||||
margin-top: 18rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.trend-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 3fr 0.8fr;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.trend-label,
|
||||
.trend-count {
|
||||
font-size: 22rpx;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.trend-bar-bg {
|
||||
height: 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #e5e7eb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trend-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #34d399, #10b981);
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
margin-top: 14rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 18rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.log-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.log-type.smoke {
|
||||
background: #ffedd5;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.log-type.resisted {
|
||||
background: #dcfce7;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
font-size: 22rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.log-remark {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 14rpx;
|
||||
border: none;
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user