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 = {}) {
|
export function getStats(params = {}) {
|
||||||
return request.get('/smoke/stats', 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": "历史记录"
|
"navigationBarTitleText": "历史记录"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/share/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "戒烟分享"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/profile/index",
|
"path": "pages/profile/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
+101
-1
@@ -7,6 +7,26 @@
|
|||||||
|
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="menu-list">
|
<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-item" @tap="goOnboarding">
|
||||||
<view class="menu-icon menu-icon-green">📝</view>
|
<view class="menu-icon menu-icon-green">📝</view>
|
||||||
<view class="menu-content">
|
<view class="menu-content">
|
||||||
@@ -46,16 +66,68 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { useUserStore } from '@/stores/user'
|
||||||
import { useLogin } from '@/hooks/useLogin'
|
import { useLogin } from '@/hooks/useLogin'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { waitForLogin } = useLogin()
|
const { waitForLogin } = useLogin()
|
||||||
|
|
||||||
|
const shareToken = ref('')
|
||||||
|
const shareExpireAt = ref('')
|
||||||
|
const shareLoading = ref(false)
|
||||||
|
|
||||||
const userName = computed(() => userStore.user?.nickname || '戒烟用户')
|
const userName = computed(() => userStore.user?.nickname || '戒烟用户')
|
||||||
const userAvatar = computed(() => userStore.user?.avatar_url || '/static/images/default-avatar.png')
|
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() {
|
function goOnboarding() {
|
||||||
uni.navigateTo({ url: '/pages/onboarding/index' })
|
uni.navigateTo({ url: '/pages/onboarding/index' })
|
||||||
}
|
}
|
||||||
@@ -99,8 +171,16 @@ function logout() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: `${userName.value}的戒烟记录(仅查看)`,
|
||||||
|
path: sharePath.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await waitForLogin()
|
await waitForLogin()
|
||||||
|
await prepareShareToken(false)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -189,6 +269,26 @@ onMounted(async () => {
|
|||||||
color: #9CA3AF;
|
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 {
|
.logout-btn {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 28rpx;
|
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