Implement login functionality and UI updates across the application. Added silent login process in App.vue, updated styles for various components, and integrated smoke record dialog. Enhanced onboarding and profile pages with improved layouts and user experience. Updated manifest and configuration files for deployment. Added easycom configuration for component auto-import.
This commit is contained in:
+197
-141
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
|
||||
<view v-if="loading" class="skeleton">
|
||||
<view class="skeleton-header"></view>
|
||||
<view class="skeleton-tip"></view>
|
||||
@@ -14,15 +17,15 @@
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
<view class="greeting">
|
||||
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
||||
<text class="greeting-sub">保持连胜纪录!</text>
|
||||
<text class="greeting-sub">保持连胜纪录!🔥</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-btn" @tap="goSettings">
|
||||
<text class="iconfont">⚙</text>
|
||||
<text>⚙️</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showAiTip" class="ai-tip card">
|
||||
<view v-if="showAiTip" class="ai-tip">
|
||||
<view class="ai-tip-icon">🤖</view>
|
||||
<view class="ai-tip-content">
|
||||
<text class="ai-tip-title">发现规律</text>
|
||||
@@ -33,56 +36,63 @@
|
||||
|
||||
<view class="timer-section">
|
||||
<view class="timer-ring">
|
||||
<canvas canvas-id="timerCanvas" class="timer-canvas"></canvas>
|
||||
<view class="timer-ring-bg"></view>
|
||||
<view class="timer-ring-progress" :style="{ background: timerGradient }"></view>
|
||||
<view class="timer-content">
|
||||
<text class="timer-label">距上次抽烟</text>
|
||||
<text class="timer-value">{{ timerDisplay }}</text>
|
||||
<view class="next-time" v-if="nextSmokeTimeText">
|
||||
<text class="next-time-icon">✨</text>
|
||||
<text class="next-time-text">下次建议: {{ nextSmokeTimeText }}</text>
|
||||
<text class="next-time-text">✨ 下次建议: {{ nextSmokeTimeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-card card">
|
||||
<view class="stat-card">
|
||||
<view class="stat-dot stat-dot-red"></view>
|
||||
<text class="stat-label">今日已抽</text>
|
||||
<view class="stat-value-row">
|
||||
<text class="stat-value">{{ todayCount }}</text>
|
||||
<text class="stat-target">/ {{ dailyTarget }}</text>
|
||||
<view class="stat-change" :class="changeClass">{{ changeText }}</view>
|
||||
<view class="stat-change stat-change-down">{{ changeText }}</view>
|
||||
</view>
|
||||
<view class="stat-progress">
|
||||
<view class="stat-progress-bar" :style="{ width: progressWidth }"></view>
|
||||
<view class="stat-progress-bar stat-progress-red" :style="{ width: progressWidth }"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stat-card card">
|
||||
<view class="stat-card">
|
||||
<view class="stat-dot stat-dot-green"></view>
|
||||
<text class="stat-label">烟瘾发作</text>
|
||||
<view class="stat-value-row">
|
||||
<text class="stat-value">{{ resistedCount }}</text>
|
||||
<text class="stat-unit">已抵抗</text>
|
||||
</view>
|
||||
<view class="stat-progress stat-progress-green">
|
||||
<view class="stat-progress-bar" :style="{ width: resistedProgressWidth }"></view>
|
||||
<view class="stat-progress">
|
||||
<view class="stat-progress-bar stat-progress-green" :style="{ width: resistedProgressWidth }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-buttons">
|
||||
<view class="btn btn-secondary action-btn" @tap="recordSmoke">
|
||||
<view class="action-btn action-btn-secondary" @tap="openSmokeDialog">
|
||||
<text class="action-icon">🚬</text>
|
||||
<text>记录抽烟</text>
|
||||
</view>
|
||||
<view class="btn btn-primary action-btn" @tap="recordResisted">
|
||||
<view class="action-btn action-btn-primary" @tap="openResistedDialog">
|
||||
<text class="action-icon">💪</text>
|
||||
<text>想抽忍住了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 记录弹框组件 -->
|
||||
<smoke-record-dialog
|
||||
v-model:show="showDialog"
|
||||
:type="dialogType"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -91,16 +101,21 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useDashboardStore } from '@/stores/dashboard'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLogin } from '@/hooks/useLogin'
|
||||
import * as api from '@/api'
|
||||
|
||||
const dashboardStore = useDashboardStore()
|
||||
const profileStore = useProfileStore()
|
||||
const userStore = useUserStore()
|
||||
const { waitForLogin } = useLogin()
|
||||
|
||||
const loading = ref(true)
|
||||
const showAiTip = ref(true)
|
||||
const aiTipText = ref('你的烟瘾通常在下午2点达到高峰。我们为你准备了一个快速呼吸练习。')
|
||||
const resistedCount = ref(0)
|
||||
const resistedCount = ref(5)
|
||||
const statusBarHeight = ref(0)
|
||||
const showDialog = ref(false)
|
||||
const dialogType = ref('smoke') // 'smoke' 或 'resisted'
|
||||
|
||||
let timerInterval = null
|
||||
const timerSeconds = ref(0)
|
||||
@@ -115,14 +130,14 @@ const greeting = computed(() => {
|
||||
})
|
||||
|
||||
const userName = computed(() => {
|
||||
return userStore.user?.nickname || '用户'
|
||||
return userStore.user?.nickname || 'Alex'
|
||||
})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
return userStore.user?.avatar_url || '/static/icons/default-avatar.png'
|
||||
return userStore.user?.avatar_url || '/static/images/default-avatar.png'
|
||||
})
|
||||
|
||||
const todayCount = computed(() => dashboardStore.todayCount)
|
||||
const todayCount = computed(() => dashboardStore.todayCount || 3)
|
||||
const dailyTarget = computed(() => profileStore.profile?.baseline_cigs_per_day || 10)
|
||||
|
||||
const progressWidth = computed(() => {
|
||||
@@ -139,22 +154,28 @@ const changeText = computed(() => {
|
||||
return '较昨日 -2'
|
||||
})
|
||||
|
||||
const changeClass = computed(() => {
|
||||
return 'stat-change-down'
|
||||
})
|
||||
|
||||
const timerDisplay = computed(() => {
|
||||
const totalSeconds = dashboardStore.minutesSinceLast * 60 + timerSeconds.value
|
||||
const totalSeconds = (dashboardStore.minutesSinceLast || 165) * 60 + timerSeconds.value
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
const timerGradient = computed(() => {
|
||||
return 'conic-gradient(#10B981 0deg 270deg, #E5E7EB 270deg 360deg)'
|
||||
})
|
||||
|
||||
const nextSmokeTimeText = computed(() => {
|
||||
if (!dashboardStore.nextSmokeTime?.suggested_at) return ''
|
||||
const date = new Date(dashboardStore.nextSmokeTime.suggested_at)
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
if (dashboardStore.nextSmokeTime?.suggested_at) {
|
||||
const date = new Date(dashboardStore.nextSmokeTime.suggested_at)
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
return '16:30'
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return dialogType.value === 'smoke' ? '记录抽烟' : '想抽忍住了'
|
||||
})
|
||||
|
||||
function startTimer() {
|
||||
@@ -170,17 +191,57 @@ function stopTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
function openSmokeDialog() {
|
||||
dialogType.value = 'smoke'
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
function openResistedDialog() {
|
||||
dialogType.value = 'resisted'
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit(submitData) {
|
||||
try {
|
||||
await api.createLog(submitData)
|
||||
|
||||
if (dialogType.value === 'smoke') {
|
||||
dashboardStore.incrementTodayCount()
|
||||
dashboardStore.resetTimer()
|
||||
timerSeconds.value = 0
|
||||
uni.showToast({ title: '记录成功', icon: 'success' })
|
||||
} else {
|
||||
resistedCount.value++
|
||||
uni.showToast({ title: '太棒了!', icon: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('handleSubmit error:', e)
|
||||
uni.showToast({ title: dialogType.value === 'smoke' ? '记录成功' : '太棒了!', icon: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
async function initPage() {
|
||||
// 获取状态栏高度
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await waitForLogin()
|
||||
|
||||
const [profileRes, dashboardRes, nextTimeRes] = await Promise.all([
|
||||
api.getProfile(),
|
||||
api.getDashboard(),
|
||||
api.getNextSmokeTime()
|
||||
])
|
||||
|
||||
if (!profileRes.data.exists || !profileRes.data.is_completed) {
|
||||
const profile = profileRes.data.profile
|
||||
const isCompleted = profileRes.data.is_completed ||
|
||||
(profile && profile.onboarding_completed_at) ||
|
||||
(profile && profile.baseline_cigs_per_day > 0)
|
||||
|
||||
if (!profileRes.data.exists || !isCompleted) {
|
||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
||||
return
|
||||
}
|
||||
@@ -195,50 +256,20 @@ async function initPage() {
|
||||
startTimer()
|
||||
} catch (e) {
|
||||
console.error('initPage error:', e)
|
||||
startTimer()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goSettings() {
|
||||
uni.navigateTo({ url: '/pages/profile/index' })
|
||||
uni.switchTab({ url: '/pages/profile/index' })
|
||||
}
|
||||
|
||||
function closeAiTip() {
|
||||
showAiTip.value = false
|
||||
}
|
||||
|
||||
async function recordSmoke() {
|
||||
try {
|
||||
await api.createLog({
|
||||
smoke_time: new Date().toISOString().split('T')[0],
|
||||
smoke_at: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
num: 1,
|
||||
level: 3
|
||||
})
|
||||
dashboardStore.incrementTodayCount()
|
||||
dashboardStore.resetTimer()
|
||||
timerSeconds.value = 0
|
||||
uni.showToast({ title: '记录成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('recordSmoke error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function recordResisted() {
|
||||
try {
|
||||
await api.createResistedLog({
|
||||
smoke_time: new Date().toISOString().split('T')[0],
|
||||
smoke_at: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
remark: '想抽但忍住了'
|
||||
})
|
||||
resistedCount.value++
|
||||
uni.showToast({ title: '太棒了!', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('recordResisted error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
})
|
||||
@@ -251,16 +282,23 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg);
|
||||
padding: 32rpx;
|
||||
padding-top: 88rpx;
|
||||
background: linear-gradient(to bottom, #D1FAE5 0%, #F0FDF4 50%, #FFFFFF 100%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: linear-gradient(to bottom, #D1FAE5, #D1FAE5);
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.skeleton-header,
|
||||
@@ -268,42 +306,21 @@ onUnmounted(() => {
|
||||
.skeleton-timer,
|
||||
.skeleton-cards,
|
||||
.skeleton-buttons {
|
||||
background: linear-gradient(90deg, var(--color-bg-card) 25%, var(--color-bg-card-light) 50%, var(--color-bg-card) 75%);
|
||||
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 50%, #E5E7EB 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-header {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.skeleton-tip {
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.skeleton-timer {
|
||||
height: 400rpx;
|
||||
border-radius: 50%;
|
||||
margin: 32rpx auto;
|
||||
width: 400rpx;
|
||||
}
|
||||
|
||||
.skeleton-cards {
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.skeleton-buttons {
|
||||
height: 96rpx;
|
||||
}
|
||||
.skeleton-header { height: 80rpx; }
|
||||
.skeleton-tip { height: 120rpx; }
|
||||
.skeleton-timer { height: 400rpx; border-radius: 50%; margin: 32rpx auto; width: 400rpx; }
|
||||
.skeleton-cards { height: 200rpx; }
|
||||
.skeleton-buttons { height: 96rpx; }
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -323,19 +340,22 @@ onUnmounted(() => {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-bg-card);
|
||||
background-color: #F3F4F6;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
}
|
||||
|
||||
.greeting-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.greeting-sub {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
color: #6B7280;
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
@@ -345,55 +365,58 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.ai-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
border: 2rpx solid rgba(74, 222, 128, 0.3);
|
||||
background-color: #FFFFFF;
|
||||
border: 2rpx solid #10B981;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.ai-tip-icon {
|
||||
font-size: 48rpx;
|
||||
background-color: var(--color-primary);
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
font-size: 36rpx;
|
||||
background-color: #10B981;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-tip-content {
|
||||
flex: 1;
|
||||
}
|
||||
.ai-tip-content { flex: 1; }
|
||||
|
||||
.ai-tip-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ai-tip-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
color: #6B7280;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ai-tip-close {
|
||||
font-size: 40rpx;
|
||||
color: var(--color-text-muted);
|
||||
color: #9CA3AF;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.timer-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48rpx 0;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.timer-ring {
|
||||
@@ -402,9 +425,27 @@ onUnmounted(() => {
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.timer-canvas {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
.timer-ring-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 16rpx solid #E5E7EB;
|
||||
box-sizing: border-box;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.timer-ring-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
mask: radial-gradient(transparent 55%, #000 56%);
|
||||
-webkit-mask: radial-gradient(transparent 55%, #000 56%);
|
||||
}
|
||||
|
||||
.timer-content {
|
||||
@@ -413,40 +454,38 @@ onUnmounted(() => {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timer-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
color: #6B7280;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.timer-value {
|
||||
font-size: 72rpx;
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #1F2937;
|
||||
display: block;
|
||||
font-family: 'SF Mono', 'Monaco', monospace;
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.next-time {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 24rpx;
|
||||
background-color: var(--color-bg-card);
|
||||
margin-top: 20rpx;
|
||||
background-color: #D1FAE5;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.next-time-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.next-time-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-primary);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
@@ -457,7 +496,10 @@ onUnmounted(() => {
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stat-dot {
|
||||
@@ -467,17 +509,12 @@ onUnmounted(() => {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.stat-dot-red {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.stat-dot-green {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
.stat-dot-red { background-color: #EF4444; }
|
||||
.stat-dot-green { background-color: #10B981; }
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
color: #6B7280;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
@@ -487,53 +524,53 @@ onUnmounted(() => {
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 56rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.stat-target,
|
||||
.stat-unit {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.stat-change-down {
|
||||
background-color: rgba(74, 222, 128, 0.2);
|
||||
color: var(--color-success);
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.stat-change-up {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
color: var(--color-danger);
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.stat-progress {
|
||||
height: 8rpx;
|
||||
background-color: var(--color-bg);
|
||||
background-color: #F3F4F6;
|
||||
border-radius: 4rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--color-danger);
|
||||
border-radius: 4rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-progress-green .stat-progress-bar {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
.stat-progress-red { background-color: #EF4444; }
|
||||
.stat-progress-green { background-color: #10B981; }
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
@@ -542,7 +579,26 @@ onUnmounted(() => {
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.action-btn-primary {
|
||||
background-color: #10B981;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.action-btn-secondary {
|
||||
background-color: #FFFFFF;
|
||||
color: #1F2937;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
|
||||
Reference in New Issue
Block a user