Initial commit
@@ -30,3 +30,12 @@ export function clearFiles() {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function replaceUrl(data) {
|
||||||
|
return request({
|
||||||
|
url: '/file/replace_url/',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<template slot-scope="{row}"><span>{{ row.teacher_name }}</span></template>
|
<template slot-scope="{row}"><span>{{ row.teacher_name }}</span></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="封面" width="80px" align="center">
|
<el-table-column label="封面" width="80px" align="center">
|
||||||
<template slot-scope="{row}"><img :src="row.image" width="50" height="50" style="object-fit:cover"></template>
|
<template slot-scope="{row}"><img :src="resolveUrl(row.image)" width="50" height="50" style="object-fit:cover"></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="学习人数" width="80px" align="center">
|
<el-table-column label="学习人数" width="80px" align="center">
|
||||||
<template slot-scope="{row}"><span>{{ row.students }}</span></template>
|
<template slot-scope="{row}"><span>{{ row.students }}</span></template>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleAvatarSuccess"
|
:on-success="handleAvatarSuccess"
|
||||||
:headers="upHeaders">
|
:headers="upHeaders">
|
||||||
<img v-if="temp.image" :src="temp.image" class="avatar">
|
<img v-if="temp.image" :src="resolveUrl(temp.image)" class="avatar">
|
||||||
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
|
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -362,6 +362,16 @@ export default {
|
|||||||
detail: ''
|
detail: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
resolveUrl(url) {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http://localhost:8000/media/')) {
|
||||||
|
return url.replace('http://localhost:8000', '')
|
||||||
|
}
|
||||||
|
if (url.startsWith('http://127.0.0.1:8000/media/')) {
|
||||||
|
return url.replace('http://127.0.0.1:8000', '')
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
},
|
||||||
handleCreate() {
|
handleCreate() {
|
||||||
this.resetTemp()
|
this.resetTemp()
|
||||||
this.dialogStatus = 'create'
|
this.dialogStatus = 'create'
|
||||||
|
|||||||
@@ -44,6 +44,13 @@
|
|||||||
@click="handleClear"
|
@click="handleClear"
|
||||||
size="small"
|
size="small"
|
||||||
>一键清空</el-button>
|
>一键清空</el-button>
|
||||||
|
<el-button
|
||||||
|
class="filter-item"
|
||||||
|
type="warning"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleReplaceUrl"
|
||||||
|
size="small"
|
||||||
|
>一键修改IP/URL</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="listLoading"
|
v-loading="listLoading"
|
||||||
@@ -86,6 +93,21 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog title="一键修改IP/URL" :visible.sync="dialogFormVisible" width="30%">
|
||||||
|
<el-form :model="temp" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
|
||||||
|
<el-form-item label="原IP/URL" prop="old_url">
|
||||||
|
<el-input v-model="temp.old_url" placeholder="请输入原IP/URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新IP/URL" prop="new_url">
|
||||||
|
<el-input v-model="temp.new_url" placeholder="请输入新IP/URL" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="replaceUrlData">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<pagination
|
<pagination
|
||||||
v-show="fileList.count>0"
|
v-show="fileList.count>0"
|
||||||
:total="fileList.count"
|
:total="fileList.count"
|
||||||
@@ -96,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { getFileList, deleteFile, clearFiles } from "@/api/file"
|
import { getFileList, deleteFile, clearFiles, replaceUrl } from "@/api/file"
|
||||||
import Pagination from "@/components/Pagination"
|
import Pagination from "@/components/Pagination"
|
||||||
export default {
|
export default {
|
||||||
components: { Pagination },
|
components: { Pagination },
|
||||||
@@ -108,6 +130,11 @@ export default {
|
|||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20
|
page_size: 20
|
||||||
},
|
},
|
||||||
|
dialogFormVisible: false,
|
||||||
|
temp: {
|
||||||
|
old_url: '',
|
||||||
|
new_url: ''
|
||||||
|
},
|
||||||
enabledOptions: [
|
enabledOptions: [
|
||||||
{ key: "文档", display_name: "文档" },
|
{ key: "文档", display_name: "文档" },
|
||||||
{ key: "图片", display_name: "图片" },
|
{ key: "图片", display_name: "图片" },
|
||||||
@@ -175,6 +202,35 @@ export default {
|
|||||||
this.getList()
|
this.getList()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
handleReplaceUrl() {
|
||||||
|
this.temp = {
|
||||||
|
old_url: '',
|
||||||
|
new_url: ''
|
||||||
|
}
|
||||||
|
this.dialogFormVisible = true
|
||||||
|
},
|
||||||
|
replaceUrlData() {
|
||||||
|
if (!this.temp.old_url || !this.temp.new_url) {
|
||||||
|
this.$message.error('请填写完整')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$confirm('确认修改? 此操作不可恢复!', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
replaceUrl(this.temp).then(response => {
|
||||||
|
this.$notify({
|
||||||
|
title: 'Success',
|
||||||
|
message: response.data.message || '修改成功',
|
||||||
|
type: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
this.dialogFormVisible = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ module.exports = {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
// target: 'http://localhost:8000',
|
// target: 'http://localhost:8000',
|
||||||
target: process.env.PROXY_TARGET || 'http://192.168.5.95:8000',
|
target: process.env.PROXY_TARGET || 'http://192.168.5.112:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/media': {
|
||||||
|
target: process.env.PROXY_TARGET || 'http://192.168.5.112:8000',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import random
|
import random
|
||||||
|
import socket
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from apps.crm.models import Project, StudentShowcase, Teacher
|
from apps.crm.models import Project, StudentShowcase, Teacher
|
||||||
@@ -9,7 +10,18 @@ class Command(BaseCommand):
|
|||||||
help = 'Populate database with business mock data using existing local images'
|
help = 'Populate database with business mock data using existing local images'
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
self.base_url = "http://192.168.5.95:8000"
|
# Get local IP
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(('8.8.8.8', 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
except Exception:
|
||||||
|
ip = '127.0.0.1'
|
||||||
|
|
||||||
|
self.base_url = f"http://{ip}:8000"
|
||||||
|
self.stdout.write(f"Using Base URL: {self.base_url}")
|
||||||
|
|
||||||
self.media_root = settings.MEDIA_ROOT
|
self.media_root = settings.MEDIA_ROOT
|
||||||
self.projects_dir = os.path.join(self.media_root, 'projects')
|
self.projects_dir = os.path.join(self.media_root, 'projects')
|
||||||
self.showcases_dir = os.path.join(self.media_root, 'showcases')
|
self.showcases_dir = os.path.join(self.media_root, 'showcases')
|
||||||
@@ -108,14 +120,12 @@ class Command(BaseCommand):
|
|||||||
src_img_name = source_project_images[i % len(source_project_images)]
|
src_img_name = source_project_images[i % len(source_project_images)]
|
||||||
local_path = self.copy_image('projects', src_img_name, 'projects', p_data['target_name'])
|
local_path = self.copy_image('projects', src_img_name, 'projects', p_data['target_name'])
|
||||||
|
|
||||||
full_url = f"{self.base_url}{local_path}"
|
|
||||||
|
|
||||||
Project.objects.update_or_create(
|
Project.objects.update_or_create(
|
||||||
title=p_data['title'],
|
title=p_data['title'],
|
||||||
defaults={
|
defaults={
|
||||||
'project_type': p_data['project_type'],
|
'project_type': p_data['project_type'],
|
||||||
'teacher': p_data['teacher'],
|
'teacher': p_data['teacher'],
|
||||||
'image': full_url,
|
'image': local_path,
|
||||||
'detail': p_data['detail']
|
'detail': p_data['detail']
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -149,12 +159,10 @@ class Command(BaseCommand):
|
|||||||
# Wait, LS showed showcase_1.jpg in 'projects' folder!
|
# Wait, LS showed showcase_1.jpg in 'projects' folder!
|
||||||
local_path = self.copy_image('projects', src_img_name, 'showcases', v_data['target_name'])
|
local_path = self.copy_image('projects', src_img_name, 'showcases', v_data['target_name'])
|
||||||
|
|
||||||
full_url = f"{self.base_url}{local_path}"
|
|
||||||
|
|
||||||
StudentShowcase.objects.update_or_create(
|
StudentShowcase.objects.update_or_create(
|
||||||
title=v_data['title'],
|
title=v_data['title'],
|
||||||
defaults={
|
defaults={
|
||||||
'cover_image': full_url,
|
'cover_image': local_path,
|
||||||
'video_url': v_data['video_url'],
|
'video_url': v_data['video_url'],
|
||||||
'description': v_data['description']
|
'description': v_data['description']
|
||||||
}
|
}
|
||||||
|
|||||||
43
admin/server/apps/crm/migrations/0032_auto_20251212_1146.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 3.2.23 on 2025-12-12 03:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crm', '0031_student_city'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='banner',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(max_length=500, verbose_name='图片URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(default='https://images.unsplash.com/photo-1526379095098-d400fd0bf935', max_length=500, verbose_name='封面图片URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='student',
|
||||||
|
name='avatar',
|
||||||
|
field=models.CharField(default='https://images.unsplash.com/photo-1535713875002-d1d0cf377fde', max_length=500, verbose_name='微信头像'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='studenthonor',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(default='https://images.unsplash.com/photo-1579548122080-c35fd6820ecb', max_length=500, verbose_name='证书图片URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='studentshowcase',
|
||||||
|
name='cover_image',
|
||||||
|
field=models.CharField(max_length=500, verbose_name='封面图片URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='studentshowcase',
|
||||||
|
name='video_url',
|
||||||
|
field=models.CharField(blank=True, max_length=500, null=True, verbose_name='视频链接URL'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -49,7 +49,7 @@ class Project(models.Model):
|
|||||||
# category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name="所属分类", related_name="projects")
|
# category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name="所属分类", related_name="projects")
|
||||||
teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="教学中心", related_name="projects")
|
teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="教学中心", related_name="projects")
|
||||||
custom_teacher = models.CharField(max_length=100, verbose_name="自定义教学中心", default="", blank=True)
|
custom_teacher = models.CharField(max_length=100, verbose_name="自定义教学中心", default="", blank=True)
|
||||||
image = models.URLField(verbose_name="封面图片URL", default="https://images.unsplash.com/photo-1526379095098-d400fd0bf935")
|
image = models.CharField(max_length=500, verbose_name="封面图片URL", default="https://images.unsplash.com/photo-1526379095098-d400fd0bf935")
|
||||||
detail = models.TextField(verbose_name="项目详情", blank=True, default="")
|
detail = models.TextField(verbose_name="项目详情", blank=True, default="")
|
||||||
students = models.IntegerField(default=0, verbose_name="学习人数")
|
students = models.IntegerField(default=0, verbose_name="学习人数")
|
||||||
address = models.CharField(max_length=200, verbose_name="地址", default="", blank=True)
|
address = models.CharField(max_length=200, verbose_name="地址", default="", blank=True)
|
||||||
@@ -106,7 +106,7 @@ class Coupon(models.Model):
|
|||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class Banner(models.Model):
|
class Banner(models.Model):
|
||||||
image = models.URLField(verbose_name="图片URL")
|
image = models.CharField(max_length=500, verbose_name="图片URL")
|
||||||
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联项目")
|
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联项目")
|
||||||
link = models.CharField(max_length=200, blank=True, null=True, verbose_name="跳转链接")
|
link = models.CharField(max_length=200, blank=True, null=True, verbose_name="跳转链接")
|
||||||
sort_order = models.IntegerField(default=0, verbose_name="排序")
|
sort_order = models.IntegerField(default=0, verbose_name="排序")
|
||||||
@@ -134,7 +134,7 @@ class Student(models.Model):
|
|||||||
address = models.CharField(max_length=200, verbose_name="地址", null=True, blank=True)
|
address = models.CharField(max_length=200, verbose_name="地址", null=True, blank=True)
|
||||||
city = models.CharField(max_length=50, verbose_name="城市", null=True, blank=True)
|
city = models.CharField(max_length=50, verbose_name="城市", null=True, blank=True)
|
||||||
# 已经有avatar字段,对应微信头像
|
# 已经有avatar字段,对应微信头像
|
||||||
avatar = models.URLField(verbose_name="微信头像", default="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde")
|
avatar = models.CharField(max_length=500, verbose_name="微信头像", default="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde")
|
||||||
is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
||||||
|
|
||||||
enrolled_projects = models.ManyToManyField(Project, through='StudentProject', related_name='enrolled_students', verbose_name="已报名项目", blank=True)
|
enrolled_projects = models.ManyToManyField(Project, through='StudentProject', related_name='enrolled_students', verbose_name="已报名项目", blank=True)
|
||||||
@@ -255,7 +255,7 @@ def update_student_learning_count_on_delete(sender, instance, **kwargs):
|
|||||||
class StudentHonor(models.Model):
|
class StudentHonor(models.Model):
|
||||||
student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name="honors", verbose_name="学员")
|
student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name="honors", verbose_name="学员")
|
||||||
title = models.CharField(max_length=100, verbose_name="荣誉标题")
|
title = models.CharField(max_length=100, verbose_name="荣誉标题")
|
||||||
image = models.URLField(verbose_name="证书图片URL", default="https://images.unsplash.com/photo-1579548122080-c35fd6820ecb")
|
image = models.CharField(max_length=500, verbose_name="证书图片URL", default="https://images.unsplash.com/photo-1579548122080-c35fd6820ecb")
|
||||||
date = models.DateField(verbose_name="获得日期")
|
date = models.DateField(verbose_name="获得日期")
|
||||||
description = models.TextField(verbose_name="荣誉描述", blank=True, default="")
|
description = models.TextField(verbose_name="荣誉描述", blank=True, default="")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||||
@@ -270,8 +270,8 @@ class StudentHonor(models.Model):
|
|||||||
|
|
||||||
class StudentShowcase(models.Model):
|
class StudentShowcase(models.Model):
|
||||||
title = models.CharField(max_length=100, verbose_name="标题")
|
title = models.CharField(max_length=100, verbose_name="标题")
|
||||||
cover_image = models.URLField(verbose_name="封面图片URL")
|
cover_image = models.CharField(max_length=500, verbose_name="封面图片URL")
|
||||||
video_url = models.URLField(verbose_name="视频链接URL", blank=True, null=True)
|
video_url = models.CharField(max_length=500, verbose_name="视频链接URL", blank=True, null=True)
|
||||||
description = models.TextField(verbose_name="描述", blank=True, default="")
|
description = models.TextField(verbose_name="描述", blank=True, default="")
|
||||||
student = models.ForeignKey(Student, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联学员", related_name="showcases")
|
student = models.ForeignKey(Student, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联学员", related_name="showcases")
|
||||||
sort_order = models.IntegerField(default=0, verbose_name="排序")
|
sort_order = models.IntegerField(default=0, verbose_name="排序")
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Category, Teacher, TeachingCenter, Project, Coupon, Banner, Student, StudentCoupon, StudentProject, StudentHonor, StudentShowcase, Notification, NotificationBatch
|
from .models import Category, Teacher, TeachingCenter, Project, Coupon, Banner, Student, StudentCoupon, StudentProject, StudentHonor, StudentShowcase, Notification, NotificationBatch
|
||||||
|
|
||||||
|
class AbsoluteURLField(serializers.CharField):
|
||||||
|
def to_representation(self, value):
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
if value.startswith(('http://', 'https://')):
|
||||||
|
return value
|
||||||
|
request = self.context.get('request')
|
||||||
|
if request:
|
||||||
|
return request.build_absolute_uri(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if data and isinstance(data, str) and data.startswith(('http://', 'https://')):
|
||||||
|
# If it's an absolute URL, try to extract the relative path
|
||||||
|
# We assume standard media URL structure '/media/'
|
||||||
|
if '/media/' in data:
|
||||||
|
return '/media/' + data.split('/media/', 1)[1]
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
class CategorySerializer(serializers.ModelSerializer):
|
class CategorySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
@@ -19,13 +38,24 @@ class TeachingCenterSerializer(serializers.ModelSerializer):
|
|||||||
class ProjectSerializer(serializers.ModelSerializer):
|
class ProjectSerializer(serializers.ModelSerializer):
|
||||||
# category_name = serializers.CharField(source='category.name', read_only=True)
|
# category_name = serializers.CharField(source='category.name', read_only=True)
|
||||||
# category_color = serializers.CharField(source='category.color', read_only=True)
|
# category_color = serializers.CharField(source='category.color', read_only=True)
|
||||||
|
category_name = serializers.CharField(source='get_project_type_display', read_only=True)
|
||||||
|
category_color = serializers.SerializerMethodField()
|
||||||
teacher_name = serializers.SerializerMethodField()
|
teacher_name = serializers.SerializerMethodField()
|
||||||
project_type_display = serializers.CharField(source='get_project_type_display', read_only=True)
|
project_type_display = serializers.CharField(source='get_project_type_display', read_only=True)
|
||||||
|
image = AbsoluteURLField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
def get_category_color(self, obj):
|
||||||
|
colors = {
|
||||||
|
'training': 'bg-cyan-100 text-cyan-600',
|
||||||
|
'competition': 'bg-purple-100 text-purple-600',
|
||||||
|
'grading': 'bg-blue-100 text-blue-600'
|
||||||
|
}
|
||||||
|
return colors.get(obj.project_type, 'bg-gray-100 text-gray-600')
|
||||||
|
|
||||||
def get_teacher_name(self, obj):
|
def get_teacher_name(self, obj):
|
||||||
if obj.teacher:
|
if obj.teacher:
|
||||||
return obj.teacher.name
|
return obj.teacher.name
|
||||||
@@ -62,6 +92,7 @@ class StudentCouponSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class BannerSerializer(serializers.ModelSerializer):
|
class BannerSerializer(serializers.ModelSerializer):
|
||||||
project_title = serializers.CharField(source='project.title', read_only=True)
|
project_title = serializers.CharField(source='project.title', read_only=True)
|
||||||
|
image = AbsoluteURLField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Banner
|
model = Banner
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@@ -74,6 +105,7 @@ class StudentSerializer(serializers.ModelSerializer):
|
|||||||
coupons = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='student_coupons')
|
coupons = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='student_coupons')
|
||||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
teacher = serializers.PrimaryKeyRelatedField(source='responsible_teacher', queryset=Teacher.objects.all(), required=False, allow_null=True)
|
teacher = serializers.PrimaryKeyRelatedField(source='responsible_teacher', queryset=Teacher.objects.all(), required=False, allow_null=True)
|
||||||
|
avatar = AbsoluteURLField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Student
|
model = Student
|
||||||
@@ -102,7 +134,7 @@ class StudentProjectSerializer(serializers.ModelSerializer):
|
|||||||
student_name = serializers.CharField(source='student.name', read_only=True)
|
student_name = serializers.CharField(source='student.name', read_only=True)
|
||||||
student_phone = serializers.CharField(source='student.phone', read_only=True)
|
student_phone = serializers.CharField(source='student.phone', read_only=True)
|
||||||
project_title = serializers.CharField(source='project.title', read_only=True)
|
project_title = serializers.CharField(source='project.title', read_only=True)
|
||||||
project_image = serializers.CharField(source='project.image', read_only=True)
|
project_image = AbsoluteURLField(source='project.image', read_only=True)
|
||||||
project_type = serializers.CharField(source='project.project_type', read_only=True)
|
project_type = serializers.CharField(source='project.project_type', read_only=True)
|
||||||
project_type_display = serializers.CharField(source='project.get_project_type_display', read_only=True)
|
project_type_display = serializers.CharField(source='project.get_project_type_display', read_only=True)
|
||||||
|
|
||||||
@@ -113,10 +145,11 @@ class StudentProjectSerializer(serializers.ModelSerializer):
|
|||||||
class StudentHonorSerializer(serializers.ModelSerializer):
|
class StudentHonorSerializer(serializers.ModelSerializer):
|
||||||
student_name = serializers.CharField(source='student.name', read_only=True)
|
student_name = serializers.CharField(source='student.name', read_only=True)
|
||||||
student_phone = serializers.CharField(source='student.phone', read_only=True)
|
student_phone = serializers.CharField(source='student.phone', read_only=True)
|
||||||
student_avatar = serializers.CharField(source='student.avatar', read_only=True)
|
student_avatar = AbsoluteURLField(source='student.avatar', read_only=True)
|
||||||
student_openid = serializers.CharField(source='student.openid', read_only=True)
|
student_openid = serializers.CharField(source='student.openid', read_only=True)
|
||||||
student_teaching_center = serializers.CharField(source='student.teacher.name', read_only=True)
|
student_teaching_center = serializers.CharField(source='student.teacher.name', read_only=True)
|
||||||
student_company_name = serializers.CharField(source='student.company_name', read_only=True)
|
student_company_name = serializers.CharField(source='student.company_name', read_only=True)
|
||||||
|
image = AbsoluteURLField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StudentHonor
|
model = StudentHonor
|
||||||
@@ -124,6 +157,8 @@ class StudentHonorSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class StudentShowcaseSerializer(serializers.ModelSerializer):
|
class StudentShowcaseSerializer(serializers.ModelSerializer):
|
||||||
student_name = serializers.CharField(source='student.name', read_only=True)
|
student_name = serializers.CharField(source='student.name', read_only=True)
|
||||||
|
cover_image = AbsoluteURLField()
|
||||||
|
video_url = AbsoluteURLField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StudentShowcase
|
model = StudentShowcase
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class DashboardStatsView(APIView):
|
|||||||
pie_chart_data.append({
|
pie_chart_data.append({
|
||||||
'type': type_code,
|
'type': type_code,
|
||||||
'name': type_mapping.get(type_code, type_code),
|
'name': type_mapping.get(type_code, type_code),
|
||||||
'value': item['total_students'],
|
'value': item['total_students'] or 0,
|
||||||
'itemStyle': { 'color': color }
|
'itemStyle': { 'color': color }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -344,6 +344,24 @@ class CategoryViewSet(viewsets.ModelViewSet):
|
|||||||
pagination_class = None # Return all categories without pagination for the app
|
pagination_class = None # Return all categories without pagination for the app
|
||||||
search_fields = ['name']
|
search_fields = ['name']
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
# Instead of database categories, return Project.PROJECT_TYPE_CHOICES
|
||||||
|
data = []
|
||||||
|
# Define some default colors if needed, or mapped by type
|
||||||
|
colors = {
|
||||||
|
'training': 'bg-cyan-100 text-cyan-600',
|
||||||
|
'competition': 'bg-purple-100 text-purple-600',
|
||||||
|
'grading': 'bg-blue-100 text-blue-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
for code, name in Project.PROJECT_TYPE_CHOICES:
|
||||||
|
data.append({
|
||||||
|
'id': code,
|
||||||
|
'name': name,
|
||||||
|
'color': colors.get(code, 'bg-gray-100 text-gray-600')
|
||||||
|
})
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
class TeacherViewSet(viewsets.ModelViewSet):
|
class TeacherViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Teacher.objects.all()
|
queryset = Teacher.objects.all()
|
||||||
serializer_class = TeacherSerializer
|
serializer_class = TeacherSerializer
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ class FileSerializer(serializers.ModelSerializer):
|
|||||||
model = File
|
model = File
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
ret = super().to_representation(instance)
|
||||||
|
request = self.context.get('request')
|
||||||
|
if instance.path and request:
|
||||||
|
if not instance.path.startswith(('http://', 'https://')):
|
||||||
|
ret['path'] = request.build_absolute_uri(instance.path)
|
||||||
|
return ret
|
||||||
|
|
||||||
class DictTypeSerializer(serializers.ModelSerializer):
|
class DictTypeSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
数据字典类型序列化
|
数据字典类型序列化
|
||||||
|
|||||||
@@ -424,26 +424,8 @@ class FileViewSet(CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListM
|
|||||||
# Ensure forward slashes for URL
|
# Ensure forward slashes for URL
|
||||||
file_name = instance.file.name.replace('\\', '/')
|
file_name = instance.file.name.replace('\\', '/')
|
||||||
|
|
||||||
# 开发阶段,本机上传的视频或图片用本机IP保存
|
# Save relative path
|
||||||
if settings.DEBUG and (type == '视频' or type == '图片'):
|
instance.path = settings.MEDIA_URL + file_name
|
||||||
try:
|
|
||||||
# 获取本机IP
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.connect(('8.8.8.8', 80))
|
|
||||||
ip = s.getsockname()[0]
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
# 获取端口
|
|
||||||
host = self.request.get_host()
|
|
||||||
port = host.split(':')[1] if ':' in host else '80'
|
|
||||||
|
|
||||||
# 构建URL
|
|
||||||
instance.path = f"{self.request.scheme}://{ip}:{port}{settings.MEDIA_URL}{file_name}"
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取本机IP失败: {e}")
|
|
||||||
instance.path = self.request.build_absolute_uri(settings.MEDIA_URL + file_name)
|
|
||||||
else:
|
|
||||||
instance.path = self.request.build_absolute_uri(settings.MEDIA_URL + file_name)
|
|
||||||
|
|
||||||
logger.info(f"File uploaded: {instance.path}")
|
logger.info(f"File uploaded: {instance.path}")
|
||||||
instance.save()
|
instance.save()
|
||||||
@@ -459,3 +441,21 @@ class FileViewSet(CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListM
|
|||||||
file_obj.file.delete(save=False)
|
file_obj.file.delete(save=False)
|
||||||
file_obj.delete(soft=False)
|
file_obj.delete(soft=False)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False)
|
||||||
|
def replace_url(self, request):
|
||||||
|
old_url = request.data.get('old_url')
|
||||||
|
new_url = request.data.get('new_url')
|
||||||
|
|
||||||
|
if not old_url or not new_url:
|
||||||
|
return Response({'error': 'Please provide both old_url and new_url'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
count = 0
|
||||||
|
for file_obj in queryset:
|
||||||
|
if file_obj.path and old_url in file_obj.path:
|
||||||
|
file_obj.path = file_obj.path.replace(old_url, new_url)
|
||||||
|
file_obj.save()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return Response({'message': f'Updated {count} files'}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 91 KiB |
BIN
admin/server/media/2025/12/12/上海台风.mp4
Normal file
29
project.config.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"appid": "wx2d9b9759137ef46b",
|
||||||
|
"compileType": "miniprogram",
|
||||||
|
"libVersion": "3.12.1",
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": [],
|
||||||
|
"include": []
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"coverView": true,
|
||||||
|
"es6": true,
|
||||||
|
"postcss": true,
|
||||||
|
"minified": true,
|
||||||
|
"enhance": true,
|
||||||
|
"showShadowRootInWxmlPanel": true,
|
||||||
|
"packNpmRelationList": [],
|
||||||
|
"babelSetting": {
|
||||||
|
"ignore": [],
|
||||||
|
"disablePlugins": [],
|
||||||
|
"outputPath": ""
|
||||||
|
},
|
||||||
|
"condition": false
|
||||||
|
},
|
||||||
|
"condition": {},
|
||||||
|
"editorSetting": {
|
||||||
|
"tabIndent": "insertSpaces",
|
||||||
|
"tabSize": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
7
project.private.config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||||
|
"projectname": "geminiWX",
|
||||||
|
"setting": {
|
||||||
|
"compileHotReLoad": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const env = 'development'
|
const env = 'development'
|
||||||
const configs = {
|
const configs = {
|
||||||
development: {
|
development: {
|
||||||
baseUrl: 'http://192.168.5.95:8000/api'
|
baseUrl: 'http://192.168.5.112:8000/api'
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
baseUrl: 'https://your-domain.example.com/api'
|
baseUrl: 'https://your-domain.example.com/api'
|
||||||
|
|||||||
@@ -43,13 +43,13 @@
|
|||||||
<view class="category-item {{selectedCategory === 'all' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="all">
|
<view class="category-item {{selectedCategory === 'all' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="all">
|
||||||
<text>全部</text>
|
<text>全部</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="category-item {{selectedCategory === 'training' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="training" wx:if="{{categoryStats.training > 0}}">
|
<view class="category-item {{selectedCategory === 'training' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="training">
|
||||||
<text>在职项目</text>
|
<text>在职项目</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="category-item {{selectedCategory === 'competition' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="competition" wx:if="{{categoryStats.competition > 0}}">
|
<view class="category-item {{selectedCategory === 'competition' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="competition">
|
||||||
<text>典礼&论坛</text>
|
<text>典礼&论坛</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="category-item {{selectedCategory === 'grading' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="grading" wx:if="{{categoryStats.grading > 0}}">
|
<view class="category-item {{selectedCategory === 'grading' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="grading">
|
||||||
<text>校友活动</text>
|
<text>校友活动</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|||||||