Initial commit of akmon project
This commit is contained in:
686
pages/sport/student/assignments.uvue
Normal file
686
pages/sport/student/assignments.uvue
Normal file
@@ -0,0 +1,686 @@
|
||||
<template>
|
||||
<scroll-view direction="vertical" class="assignments-page" :scroll-y="true" :enable-back-to-top="true">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<view class="stats-container">
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ totalAssignments }}</view>
|
||||
<view class="stat-label">总作业</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ completedAssignments }}</view>
|
||||
<view class="stat-label">已完成</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ pendingAssignments }}</view>
|
||||
<view class="stat-label">待完成</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<view class="filter-container">
|
||||
<scroll-view class="filter-scroll" direction="horizontal" :show-scrollbar="false">
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'all' }" @click="filterByStatus('all')">
|
||||
全部
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'pending' }"
|
||||
@click="filterByStatus('pending')">
|
||||
待完成
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'completed' }"
|
||||
@click="filterByStatus('completed')">
|
||||
已完成
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'overdue' }"
|
||||
@click="filterByStatus('overdue')">
|
||||
已逾期
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 作业列表 -->
|
||||
<scroll-view class="assignments-list" scroll-y="true" :refresher-enabled="true"
|
||||
:refresher-triggered="isRefreshing" @refresherrefresh="refreshAssignments">
|
||||
<view class="assignment-item" v-for="assignment in filteredAssignments"
|
||||
:key="getAssignmentIdLocal(assignment)" @click="viewAssignmentDetail(assignment)">
|
||||
<view class="assignment-header">
|
||||
<view class="assignment-title">{{ getAssignmentTitleLocal(assignment) }}</view>
|
||||
<view class="assignment-status" :class="getAssignmentStatusLocal(assignment)">
|
||||
{{ getAssignmentStatusText(assignment) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="assignment-meta">
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon"></text>
|
||||
<text class="meta-text">{{ getProjectNameLocal(assignment) }}</text>
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">⏰</text>
|
||||
<text class="meta-text">{{ formatDueDate(assignment) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="assignment-description">
|
||||
{{ getAssignmentDescriptionLocal(assignment) }}
|
||||
</view>
|
||||
|
||||
<view class="assignment-progress" v-if="getAssignmentProgress(assignment) > 0">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: getAssignmentProgress(assignment) + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ getAssignmentProgress(assignment) }}% 完成</text>
|
||||
</view>
|
||||
|
||||
<view class="assignment-actions">
|
||||
<button class="action-btn primary" @click.stop="startTraining(assignment)"
|
||||
v-if="getAssignmentStatusLocal(assignment) === 'pending'">
|
||||
开始训练
|
||||
</button>
|
||||
<button class="action-btn secondary" @click.stop="viewDetails(assignment)">
|
||||
查看详情
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="filteredAssignments.length === 0 && !isLoading">
|
||||
<text class="empty-icon"></text>
|
||||
<text class="empty-title">暂无作业</text>
|
||||
<text class="empty-description">{{ getEmptyStateText() }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="isLoading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 浮动操作按钮 -->
|
||||
<view class="fab-container">
|
||||
<view class="fab" @click="quickFilter">
|
||||
<text class="fab-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed, onUnmounted } from 'vue'
|
||||
import { onLoad, onResize } from '@dcloudio/uni-app'
|
||||
import {
|
||||
|
||||
getAssignmentId, getAssignmentTitle, getAssignmentDescription,
|
||||
getProjectName, formatDateTime, getAssignmentStatus
|
||||
|
||||
} from '../types.uts'
|
||||
import { getCurrentUserId } from '@/utils/store.uts'
|
||||
import supaClient from '@/components/supadb/aksupainstance.uts'
|
||||
// 响应式数据
|
||||
const assignments = ref<UTSJSONObject[]>([])
|
||||
const filteredAssignments = ref<UTSJSONObject[]>([])
|
||||
const selectedStatus = ref<string>('all')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isRefreshing = ref<boolean>(false)
|
||||
const totalAssignments = ref<number>(0)
|
||||
const completedAssignments = ref<number>(0)
|
||||
const pendingAssignments = ref<number>(0)
|
||||
const assignmentSubscription = ref<any | null>(null)
|
||||
const studentId = ref<string>('')
|
||||
const userId = ref('')
|
||||
|
||||
// Responsive state - using onResize for dynamic updates
|
||||
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
|
||||
|
||||
// Computed properties for responsive design
|
||||
const isLargeScreen = computed(() : boolean => {
|
||||
return screenWidth.value >= 768
|
||||
})
|
||||
|
||||
// 更新统计数据
|
||||
const updateStatistics = () => {
|
||||
totalAssignments.value = assignments.value.length
|
||||
completedAssignments.value = assignments.value.filter(assignment =>
|
||||
getAssignmentStatus(assignment) === 'completed').length
|
||||
pendingAssignments.value = assignments.value.filter(assignment =>
|
||||
getAssignmentStatus(assignment) === 'pending').length
|
||||
}
|
||||
|
||||
// 应用筛选
|
||||
const applyFilter = () => {
|
||||
if (selectedStatus.value === 'all') {
|
||||
filteredAssignments.value = assignments.value
|
||||
} else {
|
||||
filteredAssignments.value = assignments.value.filter(assignment =>
|
||||
getAssignmentStatus(assignment) === selectedStatus.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载作业列表
|
||||
const loadAssignments = async () => {
|
||||
try {
|
||||
isLoading.value = true // 直接从 ak_assignments 表查询,按学生ID筛选
|
||||
const result = await supaClient
|
||||
.from('ak_assignments')
|
||||
.select('*', {})
|
||||
.eq('student_id', studentId.value)
|
||||
.order('created_at', { ascending: false })
|
||||
.execute()
|
||||
|
||||
if (result.error == null) {
|
||||
assignments.value = result.data as UTSJSONObject[]
|
||||
updateStatistics()
|
||||
applyFilter()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '加载失败:' + (result.error?.message ?? '未知错误'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载作业失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载作业失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 示例:实时查询作业状态变化
|
||||
const watchAssignmentUpdates = async () => {
|
||||
try {
|
||||
uni.showToast({
|
||||
title: "订阅功能尚未开放",
|
||||
duration: 3000
|
||||
})
|
||||
// 使用 supaClient 的实时订阅功能
|
||||
// const subscription = supaClient
|
||||
// .from('ak_assignments')
|
||||
// .on('UPDATE', (payload) => {
|
||||
// console.log('作业更新:', payload)
|
||||
// // 实时更新本地数据
|
||||
// updateLocalAssignment(payload.new)
|
||||
// })
|
||||
// .subscribe()
|
||||
|
||||
// // 保存订阅引用以便后续取消
|
||||
// assignmentSubscription.value = subscription
|
||||
} catch (error) {
|
||||
console.error('订阅作业更新失败:', error)
|
||||
}
|
||||
} // 生命周期
|
||||
onLoad((options : OnLoadOptions) => {
|
||||
// 从页面参数获取学生ID,如果没有则从store中获取
|
||||
studentId.value = options['studentId'] ?? ''
|
||||
userId.value = options['id'] ?? getCurrentUserId()
|
||||
if (studentId.value === '') {
|
||||
studentId.value = getCurrentUserId()
|
||||
}
|
||||
console.log('onLoad - studentId:', studentId.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadAssignments()
|
||||
watchAssignmentUpdates()
|
||||
// Initialize screen width
|
||||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||||
})
|
||||
|
||||
// Handle resize events for responsive design
|
||||
onResize((size) => {
|
||||
screenWidth.value = size.size.windowWidth
|
||||
})// 安全获取作业信息
|
||||
const getAssignmentIdLocal = (assignment : UTSJSONObject) : string => {
|
||||
return getAssignmentId(assignment)
|
||||
}
|
||||
|
||||
const getAssignmentTitleLocal = (assignment : UTSJSONObject) : string => {
|
||||
return getAssignmentTitle(assignment)
|
||||
}
|
||||
|
||||
const getAssignmentDescriptionLocal = (assignment : UTSJSONObject) : string => {
|
||||
return getAssignmentDescription(assignment)
|
||||
}
|
||||
|
||||
const getProjectNameLocal = (assignment : UTSJSONObject) : string => {
|
||||
return getProjectName(assignment)
|
||||
}
|
||||
|
||||
const getAssignmentStatusLocal = (assignment : UTSJSONObject) : string => {
|
||||
return getAssignmentStatus(assignment)
|
||||
}
|
||||
const getAssignmentStatusText = (assignment : UTSJSONObject) : string => {
|
||||
const status = getAssignmentStatus(assignment)
|
||||
const statusMap : UTSJSONObject = {
|
||||
'pending': '待完成',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成',
|
||||
'overdue': '已逾期'
|
||||
}
|
||||
const statusText = statusMap.getString(status)
|
||||
if (statusText != null && statusText !== '') {
|
||||
return statusText
|
||||
} else {
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
const getAssignmentProgress = (assignment : UTSJSONObject) : number => {
|
||||
return assignment.getNumber('progress') ?? 0
|
||||
}
|
||||
const formatDueDate = (assignment : UTSJSONObject) : string => {
|
||||
const dueDate = assignment.getString('due_date') ?? ''
|
||||
if (dueDate != null && dueDate !== '') {
|
||||
return '截止:' + formatDateTime(dueDate)
|
||||
}
|
||||
return '无截止时间'
|
||||
}
|
||||
|
||||
// 刷新作业
|
||||
const refreshAssignments = async () => {
|
||||
isRefreshing.value = true
|
||||
await loadAssignments()
|
||||
isRefreshing.value = false
|
||||
}
|
||||
|
||||
// 按状态筛选
|
||||
const filterByStatus = (status : string) => {
|
||||
selectedStatus.value = status
|
||||
applyFilter()
|
||||
}
|
||||
// 获取空状态文本
|
||||
const getEmptyStateText = () : string => {
|
||||
const textMap : UTSJSONObject = {
|
||||
'all': '暂时没有作业,等待老师分配新的训练任务',
|
||||
'pending': '没有待完成的作业',
|
||||
'completed': '还没有完成任何作业',
|
||||
'overdue': '没有逾期的作业'
|
||||
}
|
||||
const text = textMap.getString(selectedStatus.value)
|
||||
if (text != null && text !== '') {
|
||||
return text
|
||||
} else {
|
||||
return '暂无数据'
|
||||
}
|
||||
}
|
||||
// 查看作业详情
|
||||
const viewAssignmentDetail = (assignment : UTSJSONObject) => {
|
||||
const assignmentId = getAssignmentId(assignment)
|
||||
uni.navigateTo({
|
||||
url: `/pages/sport/student/assignment-detail?id=${assignmentId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 开始训练
|
||||
const startTraining = (assignment : UTSJSONObject) => {
|
||||
const assignmentId = getAssignmentId(assignment)
|
||||
uni.navigateTo({
|
||||
url: `/pages/sport/student/training-record?assignmentId=${assignmentId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (assignment : UTSJSONObject) => {
|
||||
viewAssignmentDetail(assignment)
|
||||
}
|
||||
|
||||
// 快速筛选
|
||||
const quickFilter = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['全部作业', '待完成', '已完成', '已逾期'],
|
||||
success: (res) => {
|
||||
const statusMap = ['all', 'pending', 'completed', 'overdue']
|
||||
filterByStatus(statusMap[res.tapIndex!])
|
||||
}
|
||||
})
|
||||
} // 获取当前学生ID(优先从页面参数,然后从store中获取)
|
||||
const getCurrentStudentId = () : string => {
|
||||
try {
|
||||
// 优先使用页面参数传入的学生ID
|
||||
if (studentId.value != null && studentId.value !== '') {
|
||||
return studentId.value
|
||||
}
|
||||
|
||||
// 其次从store获取当前用户ID
|
||||
const userId = getCurrentUserId()
|
||||
if (userId != null && userId !== '') {
|
||||
return userId
|
||||
}
|
||||
|
||||
// 备用方案:从本地存储获取
|
||||
return uni.getStorageSync('current_student_id') || 'demo_student_id'
|
||||
} catch (error) {
|
||||
console.error('获取学生ID失败:', error)
|
||||
return 'demo_student_id'
|
||||
}
|
||||
}// 示例:直接使用 supaClient 创建训练记录
|
||||
const createTrainingRecord = async (assignmentId : string, recordData : UTSJSONObject) => {
|
||||
try {
|
||||
|
||||
const result = await supaClient
|
||||
.from('ak_training_records')
|
||||
.insert({
|
||||
assignment_id: assignmentId,
|
||||
student_id: getCurrentStudentId(),
|
||||
...recordData,
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (result.error == null) {
|
||||
uni.showToast({
|
||||
title: '训练记录保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
return result.data
|
||||
} else {
|
||||
throw new Error(result.error?.message ?? '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建训练记录失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存训练记录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
throw error
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 更新本地作业数据
|
||||
const updateLocalAssignment = (updatedAssignment : UTSJSONObject) => {
|
||||
const assignmentId = getAssignmentId(updatedAssignment)
|
||||
const index = assignments.value.findIndex(assignment =>
|
||||
getAssignmentId(assignment) === assignmentId)
|
||||
|
||||
if (index !== -1) {
|
||||
assignments.value[index] = updatedAssignment
|
||||
updateStatistics()
|
||||
applyFilter()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.assignments-page {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||||
height: 100vh;
|
||||
padding: 20rpx;
|
||||
padding-bottom: 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(10rpx);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.stat-card:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #7f8c8d;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* 筛选器 */
|
||||
.filter-container {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
padding: 15rpx 30rpx;
|
||||
margin-right: 15rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
border-radius: 25rpx;
|
||||
font-size: 28rpx;
|
||||
transition: background-color 0.3s ease, color 0.3s ease, transform 0.3s ease;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #2c3e50;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 作业列表 */
|
||||
.assignments-list {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.assignment-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.assignment-item:active {
|
||||
transform: scale(0.98);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.assignment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.assignment-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.assignment-status.pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.assignment-status.completed {
|
||||
background: #d1edff;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.assignment-status.overdue {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.assignment-meta {
|
||||
display: flex;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.meta-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 24rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 26rpx;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.assignment-description {
|
||||
font-size: 28rpx;
|
||||
color: #34495e;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.assignment-progress {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8rpx;
|
||||
background: #ecf0f1;
|
||||
border-radius: 4rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-image: linear-gradient(to bottom, #667eea, #764ba2);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 24rpx;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.assignment-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
border-radius: 15rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
transition: transform 0.3s ease;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.action-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-image: linear-gradient(to top right, #667eea, #764ba2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #ecf0f1;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 100rpx 40rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 50rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 浮动按钮 */
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
bottom: 40rpx;
|
||||
right: 40rpx;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background-image: linear-gradient(to top right, #667eea, #764ba2);
|
||||
border-radius: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.4);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
Reference in New Issue
Block a user