Initial commit

This commit is contained in:
admin
2025-12-08 14:39:07 +08:00
commit 9d4f78656b
782 changed files with 66418 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
const app = getApp()
Page({
data: {
projects: [],
banners: [],
categories: [],
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo'),
canIUseGetUserProfile: false,
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName'), // 如需尝试获取用户信息可改为false
selectedCategory: 'all',
showcases: [],
categoryStats: {}
},
onLoad() {
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
this.fetchData();
this.fetchBanners();
this.fetchShowcases();
this.fetchCategoryStats();
},
handleCategorySelect(e) {
const type = e.currentTarget.dataset.type;
this.setData({ selectedCategory: type });
this.fetchData(type);
},
fetchBanners() {
const { request } = require('../../utils/request')
request({ url: '/banners/?is_active=true' })
.then(data => {
const list = Array.isArray(data) ? data : (data && data.results) || []
this.setData({ banners: list })
})
.catch(err => console.error('Fetch banners error:', err))
},
handleBannerClick(e) {
const item = e.currentTarget.dataset.item;
if (item.project) {
wx.navigateTo({
url: `/pages/detail/detail?id=${item.project}`
})
} else if (item.link) {
// Simple link handling (copy or webview)
// For simplicity, if it starts with http, copy it
if (item.link.startsWith('http')) {
wx.setClipboardData({
data: item.link,
success: () => wx.showToast({ title: '链接已复制', icon: 'none' })
})
}
}
},
fetchShowcases() {
const { request } = require('../../utils/request')
request({ url: '/student-showcases/?is_active=true' })
.then(data => {
const list = Array.isArray(data) ? data : (data && data.results) || []
this.setData({ showcases: list })
})
.catch(err => console.error('Fetch showcases error:', err))
},
playVideo(e) {
let url = e.currentTarget.dataset.url;
if (!url) return;
// Fix for local development: replace localhost/127.0.0.1 with actual API host
if (url.includes('localhost') || url.includes('127.0.0.1')) {
const app = getApp();
// Extract host from baseUrl (e.g. http://127.0.0.1:8000/api -> http://127.0.0.1:8000)
const apiBase = app.globalData.baseUrl.split('/api')[0];
// Replace the localhost part
url = url.replace(/http:\/\/localhost:\d+/, apiBase)
.replace(/http:\/\/127.0.0.1:\d+/, apiBase);
}
// Use previewMedia for all video urls as our backend uploads files directly now
wx.previewMedia({
sources: [{ url: url, type: 'video' }],
success: () => console.log('Preview success'),
fail: (err) => {
console.error('Preview failed', err);
// Fallback for non-previewable links
wx.setClipboardData({
data: url,
success: () => wx.showToast({ title: '视频链接已复制', icon: 'none' })
})
}
})
},
fetchData(type = 'all') {
const { request } = require('../../utils/request')
let url = '/projects/';
if (type !== 'all') {
url += `?project_type=${type}`;
}
request({ url: url })
.then((data) => {
const list = Array.isArray(data) ? data : (data && data.results) || []
this.setData({ projects: list })
})
.catch((err) => {
console.error(err)
this.setData({
projects: [
{
id: 1,
title: 'Python 数据分析实战',
category: '编程开发',
image:
'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
students: 1205,
rating: 4.8,
duration: '12 周'
},
{
id: 2,
title: '零基础英语口语速成',
category: '语言学习',
image:
'https://images.unsplash.com/photo-1543269865-cbf427effbad?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
students: 850,
rating: 4.9,
duration: '8 周'
}
]
})
})
},
fetchCategoryStats() {
const { request } = require('../../utils/request')
request({ url: '/dashboard/stats' })
.then(res => {
const stats = {};
if (res.pie_chart_data) {
res.pie_chart_data.forEach(item => {
if (item.type) {
stats[item.type] = item.value;
}
});
}
this.setData({ categoryStats: stats });
})
.catch(err => console.error('Fetch stats error:', err))
},
goToDetail(e) {
const id = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/detail/detail?id=${id}`,
})
}
})

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#ff9900",
"navigationBarTextStyle": "white",
"usingComponents": {}
}

View File

@@ -0,0 +1,95 @@
<view class="page-container">
<!-- Header -->
<view class="header">
<view class="header-content">
<view class="title-container">
<view class="title-sub">致力成就</view>
<view class="title-main">终身教育伟大事业</view>
</view>
<view class="bell-btn">🔔</view>
</view>
<!-- Search Bar -->
<view class="search-bar">
<icon type="search" size="16" color="#9ca3af"></icon>
<input type="text" placeholder="搜索项目名称..." placeholder-class="search-placeholder"/>
</view>
</view>
<view class="content-body">
<!-- Banner Swiper -->
<view class="banner-swiper-container">
<swiper class="banner-swiper" indicator-dots="{{true}}" autoplay="{{true}}" interval="{{5000}}" duration="{{500}}" circular="{{true}}" indicator-active-color="#ffffff">
<block wx:for="{{banners}}" wx:key="id">
<swiper-item>
<view class="banner-item" bindtap="handleBannerClick" data-item="{{item}}">
<image src="{{item.image}}" mode="aspectFill" class="banner-image"></image>
<!-- Optional: Overlay text if needed, but usually banners are just images -->
</view>
</swiper-item>
</block>
</swiper>
</view>
<!-- Categories -->
<view class="section">
<view class="section-header">
<text class="section-title">热门分类</text>
</view>
<scroll-view scroll-x class="categories-scroll" enable-flex>
<view class="category-item {{selectedCategory === 'all' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="all">
<text>全部</text>
</view>
<view class="category-item {{selectedCategory === 'training' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="training" wx:if="{{categoryStats.training > 0}}">
<text>在职项目</text>
</view>
<view class="category-item {{selectedCategory === 'competition' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="competition" wx:if="{{categoryStats.competition > 0}}">
<text>典礼&论坛</text>
</view>
<view class="category-item {{selectedCategory === 'grading' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="grading" wx:if="{{categoryStats.grading > 0}}">
<text>校友活动</text>
</view>
</scroll-view>
</view>
<!-- Projects -->
<view class="projects-list">
<block wx:for="{{projects}}" wx:key="id">
<view class="project-card" bindtap="goToDetail" data-id="{{item.id}}">
<image class="project-image" src="{{item.image}}" mode="aspectFill"></image>
<view class="project-info">
<view class="project-header-row">
<view class="project-title">{{item.title}}</view>
<view class="project-category-tag" wx:if="{{item.project_type_display || item.category}}">{{item.project_type_display || item.category}}</view>
</view>
<view class="project-teacher" wx:if="{{item.teacher_name}}">
{{item.teacher_name}}
</view>
<view class="project-meta">
<view class="meta-item"><text class="icon">👥</text> {{item.students}}</view>
<view class="meta-item">{{item.address}}</view>
<view class="meta-item"><text class="icon">🕒</text> {{item.duration}}</view>
</view>
</view>
</view>
</block>
</view>
<!-- Student Showcase -->
<view class="section" wx:if="{{showcases && showcases.length > 0}}" style="margin-top: 40rpx;">
<view class="section-header">
<text class="section-title">精彩视频</text>
</view>
<scroll-view scroll-x class="showcase-scroll" enable-flex>
<view class="showcase-item" wx:for="{{showcases}}" wx:key="id" bindtap="playVideo" data-url="{{item.video_url}}">
<image class="showcase-image" src="{{item.cover_image}}" mode="aspectFill"></image>
<view class="showcase-info">
<text class="showcase-title">{{item.title}}</text>
<text class="showcase-student" wx:if="{{item.student_name}}">{{item.student_name}}</text>
</view>
<view class="play-icon" wx:if="{{item.video_url}}">▶</view>
</view>
</scroll-view>
</view>
</view>
</view>

View File

@@ -0,0 +1,316 @@
.page-container {
padding-bottom: 50rpx;
background-color: var(--background-color);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #ffaa33 0%, #ff8800 100%);
padding: 40rpx 40rpx 100rpx 40rpx;
border-bottom-left-radius: 50rpx;
border-bottom-right-radius: 50rpx;
position: relative;
box-shadow: 0 10rpx 20rpx rgba(255, 153, 0, 0.2);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10rpx;
padding-top: 10rpx;
}
.greeting {
color: rgba(255, 255, 255, 0.9);
font-size: 28rpx;
}
.title-container {
display: flex;
flex-direction: column;
margin-left: 20rpx;
}
.title-sub {
color: rgba(255, 255, 255, 0.95);
font-size: 34rpx;
letter-spacing: 4rpx;
margin-bottom: 8rpx;
font-weight: 500;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
}
.title-main {
color: var(--text-white);
font-size: 60rpx;
font-weight: normal;
font-family: "Xingkai SC", "STXingkai", "STKaiti", "KaiTi", "楷体", "cursive";
letter-spacing: 2rpx;
text-shadow: 0 2rpx 6rpx rgba(0,0,0,0.15);
line-height: 1.2;
}
.bell-btn {
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
padding: 16rpx;
border-radius: 50%;
color: var(--text-white);
font-size: 32rpx;
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.search-bar {
position: absolute;
bottom: -40rpx;
left: 30rpx;
right: 30rpx;
background-color: var(--surface-color);
border-radius: 30rpx;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.search-bar input {
margin-left: 20rpx;
flex: 1;
font-size: 28rpx;
color: var(--text-main);
}
.search-placeholder {
color: var(--text-light);
}
.content-body {
padding: 60rpx 30rpx 0 30rpx;
}
.banner-swiper-container {
border-radius: 30rpx;
overflow: hidden;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
margin-bottom: 40rpx;
}
.banner-swiper {
height: 300rpx;
width: 100%;
}
.banner-item {
width: 100%;
height: 100%;
}
.banner-image {
width: 100%;
height: 100%;
border-radius: 30rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: var(--text-main);
}
.see-all {
font-size: 24rpx;
color: var(--text-light);
}
.categories-scroll {
white-space: nowrap;
width: 100%;
}
.category-item {
display: inline-block;
padding: 16rpx 40rpx;
border-radius: 40rpx;
font-size: 28rpx;
margin-right: 20rpx;
font-weight: 500;
background-color: var(--surface-secondary);
color: var(--text-secondary);
transition: all 0.3s;
}
.category-item.active {
background-color: var(--primary-light);
color: var(--primary-color);
}
.projects-list {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.project-card {
background-color: var(--surface-color);
padding: 24rpx;
border-radius: 30rpx;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: row;
gap: 24rpx;
}
.project-image {
width: 192rpx;
height: 192rpx;
border-radius: 20rpx;
flex-shrink: 0;
background-color: #eee;
}
.project-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.project-header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8rpx;
}
.project-category-tag {
font-size: 20rpx;
color: var(--primary-color);
background-color: var(--primary-light);
padding: 4rpx 12rpx;
border-radius: 8rpx;
white-space: nowrap;
flex-shrink: 0;
}
.project-title {
font-size: 28rpx;
font-weight: bold;
color: var(--text-main);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
flex: 1;
margin-right: 16rpx;
}
.project-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10rpx; /* Ensure space from teacher */
}
.meta-item {
font-size: 20rpx;
color: var(--text-light);
display: flex;
align-items: center;
}
.icon {
margin-right: 4rpx;
}
/* Project Teacher */
.project-teacher {
font-size: 24rpx; /* Slightly larger */
color: var(--text-secondary);
margin-bottom: auto;
margin-top: auto; /* Center vertically in remaining space */
font-weight: 500;
}
/* Student Showcase */
.showcase-scroll {
white-space: nowrap;
width: 100%;
}
.showcase-item {
display: inline-block;
width: 320rpx;
height: 220rpx;
margin-right: 24rpx;
border-radius: 16rpx;
overflow: hidden;
position: relative;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.showcase-image {
width: 100%;
height: 100%;
background-color: #e5e7eb;
}
.showcase-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 16rpx;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
color: var(--text-white);
display: flex;
flex-direction: column;
}
.showcase-title {
font-size: 26rpx;
font-weight: bold;
margin-bottom: 4rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.showcase-student {
font-size: 20rpx;
opacity: 0.9;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60rpx;
height: 60rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
color: var(--text-white);
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
backdrop-filter: blur(4px);
}