Initial commit of akmon project
This commit is contained in:
890
pages/sport/teacher/projects.uvue
Normal file
890
pages/sport/teacher/projects.uvue
Normal file
@@ -0,0 +1,890 @@
|
||||
<!-- 训练项目管理 - UTSJSONObject 优化版本 -->
|
||||
<template>
|
||||
<scroll-view direction="vertical" class="projects-container" :scroll-y="true" :enable-back-to-top="true">
|
||||
<!-- 统计概览 -->
|
||||
<supadb
|
||||
ref="statsRef"
|
||||
collection="ak_training_projects"
|
||||
:filter="statsFilter"
|
||||
getcount="exact"
|
||||
@process-data="handleStatsData"
|
||||
@error="handleError">
|
||||
</supadb>
|
||||
|
||||
<view class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ stats.total_projects }}</view>
|
||||
<view class="stat-label">总项目数</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ stats.active_projects }}</view>
|
||||
<view class="stat-label">激活项目</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ stats.popular_projects }}</view>
|
||||
<view class="stat-label">热门项目</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ stats.avg_difficulty }}</view>
|
||||
<view class="stat-label">平均难度</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="actions-section">
|
||||
<button class="action-btn primary" @click="createProject">
|
||||
<text class="btn-icon">+</text>
|
||||
<text class="btn-text">新建项目</text>
|
||||
</button>
|
||||
<button class="action-btn secondary" @click="importProjects">
|
||||
<text class="btn-icon">📤</text>
|
||||
<text class="btn-text">导入项目</text>
|
||||
</button>
|
||||
<button class="action-btn secondary" @click="exportProjects">
|
||||
<text class="btn-icon">📥</text>
|
||||
<text class="btn-text">导出项目</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<view class="filter-section">
|
||||
<view class="search-box">
|
||||
<input
|
||||
:value="searchKeyword"
|
||||
class="search-input"
|
||||
placeholder="搜索项目名称..."
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="filter-row">
|
||||
<view class="filter-item" @click="showCategoryPicker">
|
||||
<text class="filter-label">分类</text>
|
||||
<text class="filter-value">{{ selectedCategoryText }}</text>
|
||||
<text class="filter-arrow">></text>
|
||||
</view>
|
||||
<view class="filter-item" @click="showDifficultyPicker">
|
||||
<text class="filter-label">难度</text>
|
||||
<text class="filter-value">{{ selectedDifficultyText }}</text>
|
||||
<text class="filter-arrow">></text>
|
||||
</view>
|
||||
<view class="filter-item" @click="showStatusPicker">
|
||||
<text class="filter-label">状态</text>
|
||||
<text class="filter-value">{{ selectedStatusText }}</text>
|
||||
<text class="filter-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<view class="projects-section">
|
||||
<supadb
|
||||
ref="projectsRef"
|
||||
collection="ak_training_projects"
|
||||
:filter="projectsFilter"
|
||||
getcount="exact"
|
||||
:orderby="sortOrder"
|
||||
:page-size="pageState.pageSize"
|
||||
@process-data="handleProjectsData"
|
||||
@error="handleError">
|
||||
</supadb>
|
||||
|
||||
<view v-if="pageState.loading" class="loading-state">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="pageState.error" class="error-state">
|
||||
<text class="error-text">{{ pageState.error }}</text>
|
||||
<button class="retry-btn" @click="retryLoad">重试</button>
|
||||
</view>
|
||||
|
||||
<view v-else-if="projects.length === 0" class="empty-state">
|
||||
<text class="empty-icon">🏋️♂️</text>
|
||||
<text class="empty-text">暂无训练项目</text>
|
||||
<button class="create-btn" @click="createProject">创建第一个项目</button>
|
||||
</view>
|
||||
|
||||
<view v-else class="projects-list">
|
||||
<view v-for="project in projects" :key="project['id']" class="project-card" @click="viewProject(project)">
|
||||
<view class="card-header"> <view class="project-info">
|
||||
<text class="project-name">{{ project.getString('name') ?? project.getString('title') ?? '未命名项目' }}</text>
|
||||
<text class="project-category">{{ project.getString('category') ?? '' }}</text>
|
||||
</view> <view class="project-badges"> <view class="difficulty-badge" :style="{ backgroundColor: getDifficultyColor(project) }">
|
||||
<text class="badge-text">{{ formatDifficultyLocal(project.getNumber('difficulty') ?? 1) }}</text>
|
||||
</view> <view class="status-badge" :style="{ backgroundColor: getStatusColor(project) }">
|
||||
<text class="badge-text">{{ formatStatusLocal(project.getBoolean('is_active') ?? true) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="project-description">{{ project.getString('description') ?? '暂无描述' }}</text>
|
||||
<view class="project-meta">
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">⏱️</text>
|
||||
<text class="meta-text">{{ project.getNumber('duration') ?? project.getNumber('duration_minutes') ?? 30 }}分钟</text>
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">📊</text>
|
||||
<text class="meta-text">使用{{ project.getNumber('usage_count') ?? 0 }}次</text>
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">📅</text>
|
||||
<text class="meta-text">{{ formatDateLocal(project.getString('created_at') ?? '') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-actions">
|
||||
<button class="action-btn-small primary" @click.stop="editProject(project)">编辑</button>
|
||||
<button class="action-btn-small" @click.stop="toggleProjectStatus(project)">
|
||||
{{ (project.getBoolean('is_active') ?? true) ? '停用' : '启用' }}
|
||||
</button>
|
||||
<button class="action-btn-small danger" @click.stop="deleteProject(project)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<view class="pagination" v-if="pageState.total > pageState.pageSize">
|
||||
<button class="page-btn" :disabled="pageState.currentPage <= 1" @click="changePage(pageState.currentPage - 1)">
|
||||
上一页
|
||||
</button>
|
||||
<text class="page-info">
|
||||
{{ pageState.currentPage }} / {{ Math.ceil(pageState.total / pageState.pageSize) }}
|
||||
</text>
|
||||
<button class="page-btn" :disabled="pageState.currentPage >= Math.ceil(pageState.total / pageState.pageSize)" @click="changePage(pageState.currentPage + 1)">
|
||||
下一页
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type {
|
||||
ProjectData,
|
||||
PageState,
|
||||
StatsData
|
||||
} from '../types.uts'
|
||||
import {
|
||||
formatDifficulty,
|
||||
formatStatus,
|
||||
formatDate,
|
||||
createPageState,
|
||||
getProjectDifficultyColor,
|
||||
getStatusColor,
|
||||
getProjectStatusColor,
|
||||
PROJECT_CATEGORIES,
|
||||
DIFFICULTY_OPTIONS,
|
||||
STATUS_OPTIONS
|
||||
} from '../types.uts'
|
||||
// Local wrapper functions to avoid unref issues
|
||||
const formatDifficultyLocal = (difficulty: number): string => {
|
||||
return formatDifficulty(difficulty)
|
||||
}
|
||||
|
||||
const formatStatusLocal = (isActive: boolean): string => {
|
||||
return formatStatus(isActive)
|
||||
}
|
||||
|
||||
const formatDateLocal = (dateStr: string): string => {
|
||||
return formatDate(dateStr)
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const projects = ref<ProjectData[]>([])
|
||||
const stats = ref<StatsData>({
|
||||
total_projects: 0,
|
||||
active_projects: 0,
|
||||
popular_projects: 0,
|
||||
avg_difficulty: '0.0'
|
||||
})
|
||||
const pageState = ref<PageState>(createPageState(12))
|
||||
|
||||
// 筛选状态
|
||||
const searchKeyword = ref<string>('')
|
||||
const selectedCategoryIndex = ref<number>(0)
|
||||
const selectedDifficultyIndex = ref<number>(0)
|
||||
const selectedStatusIndex = ref<number>(0)
|
||||
const sortByIndex = ref<number>(0)
|
||||
|
||||
// 组件引用
|
||||
const statsRef = ref<SupadbComponentPublicInstance | null>(null)
|
||||
const projectsRef = ref<SupadbComponentPublicInstance | null>(null)
|
||||
|
||||
// 选项数组
|
||||
const categoryOptions = PROJECT_CATEGORIES
|
||||
const difficultyOptions = DIFFICULTY_OPTIONS
|
||||
const statusOptions = STATUS_OPTIONS
|
||||
const sortOptions = [
|
||||
{ value: 'created_at.desc', text: '创建时间(最新)' },
|
||||
{ value: 'created_at.asc', text: '创建时间(最旧)' },
|
||||
{ value: 'name.asc', text: '名称(A-Z)' },
|
||||
{ value: 'name.desc', text: '名称(Z-A)' },
|
||||
{ value: 'difficulty.asc', text: '难度(简单到困难)' },
|
||||
{ value: 'difficulty.desc', text: '难度(困难到简单)' }
|
||||
] // 计算属性
|
||||
const selectedCategoryText = computed(() => {
|
||||
const option = categoryOptions[selectedCategoryIndex.value]
|
||||
return option != null ? (option.text as string) : ''
|
||||
})
|
||||
const selectedDifficultyText = computed(() => {
|
||||
const option = difficultyOptions[selectedDifficultyIndex.value]
|
||||
return option != null ? (option.text as string) : ''
|
||||
})
|
||||
const selectedStatusText = computed(() => {
|
||||
const option = statusOptions[selectedStatusIndex.value]
|
||||
return option != null ? (option.text as string) : ''
|
||||
})
|
||||
|
||||
const statsFilter = computed(() => new UTSJSONObject())
|
||||
const projectsFilter = computed(() => {
|
||||
const filter = new UTSJSONObject()
|
||||
|
||||
// 分类筛选
|
||||
const categoryOption = categoryOptions[selectedCategoryIndex.value]
|
||||
const categoryValue = categoryOption != null ? (categoryOption.value as string) : ''
|
||||
if (categoryValue !== '') {
|
||||
filter.set('category', categoryValue)
|
||||
}
|
||||
|
||||
// 难度筛选
|
||||
const difficultyOption = difficultyOptions[selectedDifficultyIndex.value]
|
||||
const difficultyValue = difficultyOption != null ? (difficultyOption.value as string) : ''
|
||||
if (difficultyValue !== '') {
|
||||
filter.set('difficulty', parseInt(difficultyValue as string))
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
const statusOption = statusOptions[selectedStatusIndex.value]
|
||||
const statusValue = statusOption != null ? (statusOption.value as string) : ''
|
||||
if (statusValue !== '') {
|
||||
filter.set('is_active', statusValue === 'active')
|
||||
}
|
||||
|
||||
// 搜索关键词
|
||||
if (searchKeyword.value.trim() !== '') {
|
||||
const nameFilter = new UTSJSONObject()
|
||||
nameFilter.set('contains', searchKeyword.value.trim())
|
||||
filter.set('name', nameFilter)
|
||||
}
|
||||
|
||||
return filter
|
||||
})
|
||||
const sortOrder = computed(() => {
|
||||
const sortOption = sortOptions[sortByIndex.value]
|
||||
return sortOption != null ? (sortOption.value as string) : 'created_at.desc'
|
||||
})
|
||||
// 样式计算函数
|
||||
const getDifficultyColor = (project: ProjectData): string => {
|
||||
return getProjectDifficultyColor(project.getNumber('difficulty') ?? 1)
|
||||
}
|
||||
|
||||
const getStatusColor = (project: ProjectData): string => {
|
||||
return getProjectStatusColor(project)
|
||||
}
|
||||
|
||||
// 数据处理函数
|
||||
const handleStatsData = (result: UTSJSONObject) => {
|
||||
const data = result.get('data')
|
||||
if (data != null && Array.isArray(data)) {
|
||||
const totalProjects = data.length
|
||||
|
||||
let activeProjects = 0
|
||||
let popularProjects = 0
|
||||
let difficultySum = 0
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const project = data[i] as ProjectData
|
||||
|
||||
if (project.getBoolean('is_active') ?? true) {
|
||||
activeProjects++
|
||||
}
|
||||
|
||||
if ((project.getNumber('usage_count') ?? 0) > 5) {
|
||||
popularProjects++
|
||||
}
|
||||
|
||||
difficultySum += project.getNumber('difficulty') ?? 1
|
||||
}
|
||||
const avgDifficulty = totalProjects > 0 ? (difficultySum / totalProjects).toFixed(1) : '0.0'
|
||||
|
||||
stats.value = {
|
||||
total_projects: totalProjects,
|
||||
active_projects: activeProjects,
|
||||
popular_projects: popularProjects,
|
||||
avg_difficulty: avgDifficulty
|
||||
} as StatsData
|
||||
}
|
||||
}
|
||||
|
||||
const handleProjectsData = (result: UTSJSONObject) => {
|
||||
const data = result.get('data')
|
||||
const total = result.get('total') as number
|
||||
|
||||
if (data != null && Array.isArray(data)) {
|
||||
projects.value = data as ProjectData[]
|
||||
}
|
||||
|
||||
if (total != null) {
|
||||
pageState.value.total = total
|
||||
}
|
||||
|
||||
pageState.value.loading = false
|
||||
}
|
||||
|
||||
const handleError = (error: any) => {
|
||||
console.error('Projects load error:', error)
|
||||
pageState.value.loading = false
|
||||
pageState.value.error = '数据加载失败'
|
||||
uni.showToast({
|
||||
title: '数据加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
// 操作函数
|
||||
const loadData = () => {
|
||||
pageState.value.loading = true
|
||||
pageState.value.error = null
|
||||
|
||||
if (statsRef.value != null) {
|
||||
statsRef.value?.refresh?.()
|
||||
}
|
||||
|
||||
if (projectsRef.value != null) {
|
||||
projectsRef.value?.refresh?.()
|
||||
}
|
||||
}
|
||||
|
||||
const retryLoad = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pageState.value.currentPage = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const changePage = (page: number) => {
|
||||
pageState.value.currentPage = page
|
||||
loadData()
|
||||
}
|
||||
// 选择器函数
|
||||
const showCategoryPicker = () => {
|
||||
const itemList = categoryOptions.map(item => item.text as string)
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
if (typeof res.tapIndex === 'number') {
|
||||
selectedCategoryIndex.value = res.tapIndex
|
||||
pageState.value.currentPage = 1
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showDifficultyPicker = () => {
|
||||
const itemList = difficultyOptions.map(item => item.text as string)
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
if (typeof res.tapIndex === 'number') {
|
||||
selectedDifficultyIndex.value = res.tapIndex
|
||||
pageState.value.currentPage = 1
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showStatusPicker = () => {
|
||||
const itemList = statusOptions.map(item => item.text as string)
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
if (typeof res.tapIndex === 'number') {
|
||||
selectedStatusIndex.value = res.tapIndex
|
||||
pageState.value.currentPage = 1
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 业务操作函数
|
||||
const createProject = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/sport/teacher/project-create'
|
||||
})
|
||||
}
|
||||
const viewProject = (project: ProjectData) => {
|
||||
const projectId = project.getString('id') ?? ''
|
||||
uni.navigateTo({
|
||||
url: `/pages/sport/teacher/project-detail?id=${projectId}`
|
||||
})
|
||||
}
|
||||
|
||||
const editProject = (project: ProjectData) => {
|
||||
const projectId = project.getString('id') ?? ''
|
||||
uni.navigateTo({
|
||||
url: `/pages/sport/teacher/project-edit?id=${projectId}`
|
||||
})
|
||||
}
|
||||
const toggleProjectStatus = (project: ProjectData) => {
|
||||
const currentStatus = project.getBoolean('is_active') ?? true
|
||||
const newStatus = !currentStatus
|
||||
const statusText = newStatus ? '启用' : '停用'
|
||||
const projectName = project.getString('name') ?? project.getString('title') ?? '未命名项目'
|
||||
|
||||
uni.showModal({
|
||||
title: '确认操作',
|
||||
content: `确定要${statusText}项目"${projectName}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// TODO: 调用API更新状态
|
||||
uni.showToast({
|
||||
title: `${statusText}成功`,
|
||||
icon: 'success'
|
||||
})
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteProject = (project: ProjectData) => {
|
||||
const projectName = project.getString('name') ?? project.getString('title') ?? '未命名项目'
|
||||
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除项目"${projectName}"吗?此操作不可恢复`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// TODO: 调用API删除项目
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const importProjects = () => {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const exportProjects = () => {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const onShow = () => {
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.projects-container {
|
||||
display: flex;
|
||||
flex:1;
|
||||
background-color: #f5f5f5;
|
||||
padding: 32rpx;
|
||||
padding-bottom: 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 统计样式 */
|
||||
.stats-section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.stats-grid .stat-card {
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.stats-grid .stat-card:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #007aff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
/* 操作按钮样式 */
|
||||
.actions-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.actions-section .action-btn {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.actions-section .action-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 32rpx;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 筛选器样式 */
|
||||
.filter-section {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.filter-row .filter-item {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.filter-row .filter-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 12rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.filter-value {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.filter-arrow {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* 项目列表样式 */
|
||||
.projects-section {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 64rpx;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.error-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.retry-btn,
|
||||
.create-btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.projects-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.projects-list .project-card {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.projects-list .project-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.project-card { border: 1px solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
background: #ffffff;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
border-color: #007aff;
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.project-category {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
.project-badges {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.project-badges .badge {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.project-badges .badge:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.difficulty-badge,
|
||||
.status-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.project-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.project-meta .meta-item {
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.project-meta .meta-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 20rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.card-actions .action-btn-small {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.card-actions .action-btn-small:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-btn-small {
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 1px solid #007aff;
|
||||
background-color: #ffffff;
|
||||
color: #007aff;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn-small.primary {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn-small.danger {
|
||||
border-color: #ff3b30;
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
/* 分页样式 */ .pagination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.pagination .page-btn {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.pagination .page-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
background-color: #cccccc;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.stats-grid {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.project-meta {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-meta > * {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.project-meta > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user