Initial commit

This commit is contained in:
admin
2025-12-12 14:06:35 +08:00
parent 61c61a77a2
commit c7af3b3e44
30 changed files with 271 additions and 44 deletions

View File

@@ -30,3 +30,12 @@ export function clearFiles() {
method: 'delete' method: 'delete'
}) })
} }
export function replaceUrl(data) {
return request({
url: '/file/replace_url/',
method: 'post',
data
})
}

View File

@@ -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'

View File

@@ -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()
})
})
} }
} }
}; };

View File

@@ -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
} }
}, },

View File

@@ -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']
} }

View 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'),
),
]

View File

@@ -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="排序")

View File

@@ -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

View File

@@ -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

View File

@@ -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):
""" """
数据字典类型序列化 数据字典类型序列化

View File

@@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

29
project.config.json Normal file
View 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
}
}

View File

@@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "geminiWX",
"setting": {
"compileHotReLoad": true
}
}

View File

@@ -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'

View File

@@ -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>