891 lines
21 KiB
Plaintext
891 lines
21 KiB
Plaintext
<!-- 训练项目管理 - 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>
|