代码支持
Some checks failed
Deploy to Server / deploy (push) Has been cancelled

This commit is contained in:
jeremygan2021
2026-03-01 17:39:36 +08:00
parent b31e8fff09
commit 84e30d26af
10 changed files with 97 additions and 17 deletions

View File

@@ -101,19 +101,7 @@ DATABASES = {
#从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。 #从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。
DB_HOST = os.environ.get('DB_HOST', '6.6.6.66') # DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
if DB_HOST:
DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'market'),
'USER': os.environ.get('DB_USER', 'market'),
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
'HOST': DB_HOST,
'PORT': os.environ.get('DB_PORT', '5432'),
}
# DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
# if DB_HOST: # if DB_HOST:
# DATABASES['default'] = { # DATABASES['default'] = {
# 'ENGINE': 'django.db.backends.postgresql', # 'ENGINE': 'django.db.backends.postgresql',
@@ -121,10 +109,22 @@ if DB_HOST:
# 'USER': os.environ.get('DB_USER', 'market'), # 'USER': os.environ.get('DB_USER', 'market'),
# 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'), # 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
# 'HOST': DB_HOST, # 'HOST': DB_HOST,
# 'PORT': os.environ.get('DB_PORT', '6433'), # 'PORT': os.environ.get('DB_PORT', '5432'),
# } # }
DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
if DB_HOST:
DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'market'),
'USER': os.environ.get('DB_USER', 'market'),
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
'HOST': DB_HOST,
'PORT': os.environ.get('DB_PORT', '6433'),
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators

View File

@@ -264,7 +264,7 @@ class VCCourseAdmin(OrderableAdminMixin, ModelAdmin):
'fields': ('title', 'description', 'course_type', 'tag', 'price') 'fields': ('title', 'description', 'course_type', 'tag', 'price')
}), }),
('视频设置', { ('视频设置', {
'fields': ('is_video_course', 'video_url'), 'fields': ('is_video_course', 'video_url', 'video_embed_code'),
'description': '设置是否为视频课程及视频链接' 'description': '设置是否为视频课程及视频链接'
}), }),
('课程安排', { ('课程安排', {

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.1 on 2026-03-01 09:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0038_vccourse_is_video_course_vccourse_video_url'),
]
operations = [
migrations.AddField(
model_name='vccourse',
name='video_embed_code',
field=models.TextField(blank=True, help_text='支持iframe嵌入代码优先级高于视频URL', null=True, verbose_name='视频嵌入代码'),
),
]

View File

@@ -365,6 +365,7 @@ class VCCourse(models.Model):
# 视频课程相关 # 视频课程相关
is_video_course = models.BooleanField(default=False, verbose_name="是否视频课程") is_video_course = models.BooleanField(default=False, verbose_name="是否视频课程")
video_url = models.URLField(blank=True, null=True, verbose_name="视频课程URL", help_text="仅当用户付费或报名后可见") video_url = models.URLField(blank=True, null=True, verbose_name="视频课程URL", help_text="仅当用户付费或报名后可见")
video_embed_code = models.TextField(blank=True, null=True, verbose_name="视频嵌入代码", help_text="支持iframe嵌入代码优先级高于视频URL")
# 课程时间安排 # 课程时间安排
is_fixed_schedule = models.BooleanField(default=False, verbose_name="是否固定时间课程", help_text="勾选后,前端将显示具体的开课时间") is_fixed_schedule = models.BooleanField(default=False, verbose_name="是否固定时间课程", help_text="勾选后,前端将显示具体的开课时间")

View File

@@ -176,6 +176,7 @@ class VCCourseSerializer(serializers.ModelSerializer):
display_detail_image = serializers.SerializerMethodField() display_detail_image = serializers.SerializerMethodField()
course_type_display = serializers.CharField(source='get_course_type_display', read_only=True) course_type_display = serializers.CharField(source='get_course_type_display', read_only=True)
video_url = serializers.SerializerMethodField() video_url = serializers.SerializerMethodField()
video_embed_code = serializers.SerializerMethodField()
is_purchased = serializers.SerializerMethodField() is_purchased = serializers.SerializerMethodField()
class Meta: class Meta:
@@ -234,6 +235,18 @@ class VCCourseSerializer(serializers.ModelSerializer):
return None return None
def get_video_embed_code(self, obj):
"""
仅当用户已付费/报名时返回视频嵌入代码
"""
if not obj.is_video_course:
return None
if self._check_purchased(obj):
return obj.video_embed_code
return None
class ESP32ConfigSerializer(serializers.ModelSerializer): class ESP32ConfigSerializer(serializers.ModelSerializer):
""" """
ESP32配置序列化器 ESP32配置序列化器

View File

@@ -238,7 +238,12 @@ const VCCourseDetail = () => {
position: 'relative', position: 'relative',
aspectRatio: '16/9' aspectRatio: '16/9'
}}> }}>
{course.video_url ? ( {course.video_embed_code ? (
<div
style={{ width: '100%', height: '100%' }}
dangerouslySetInnerHTML={{ __html: course.video_embed_code }}
/>
) : course.video_url ? (
<video <video
src={course.video_url} src={course.video_url}
controls controls

View File

@@ -7,6 +7,7 @@ export default defineAppConfig({
'pages/courses/detail', 'pages/courses/detail',
'pages/forum/index', 'pages/forum/index',
'pages/goods/detail', 'pages/goods/detail',
'pages/webview/index',
'pages/cart/cart', 'pages/cart/cart',
'pages/order/checkout', 'pages/order/checkout',
'pages/order/payment', 'pages/order/payment',

View File

@@ -71,6 +71,19 @@ export default function CourseDetail() {
return `${year}/${month}/${day} ${hour}:${minute}` return `${year}/${month}/${day} ${hour}:${minute}`
} }
const extractIframeSrc = (html: string) => {
if (!html) return null
const match = html.match(/src=["'](.*?)["']/)
return match ? match[1] : null
}
const handleOpenWebview = (url: string) => {
if (!url) return
Taro.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(url)}`
})
}
return ( return (
<View className='page-container'> <View className='page-container'>
<ScrollView scrollY className='scroll-content'> <ScrollView scrollY className='scroll-content'>
@@ -94,7 +107,19 @@ export default function CourseDetail() {
{detail.is_video_course && ( {detail.is_video_course && (
<View className='section video-section'> <View className='section video-section'>
<Text className='section-title'></Text> <Text className='section-title'></Text>
{detail.video_url ? ( {detail.video_embed_code ? (
<View className='video-locked' onClick={() => {
const src = extractIframeSrc(detail.video_embed_code)
if (src) handleOpenWebview(src)
else Taro.showToast({ title: '无法解析视频地址', icon: 'none' })
}}>
<Image src={detail.cover_image_url} className='locked-bg' mode='aspectFill' />
<View className='locked-overlay'>
<View className='lock-icon' style={{fontSize: '40px'}}></View>
<Text className='lock-text'></Text>
</View>
</View>
) : detail.video_url ? (
<Video <Video
src={detail.video_url} src={detail.video_url}
className='course-video' className='course-video'

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '加载中...'
}

View File

@@ -0,0 +1,14 @@
import { WebView } from '@tarojs/components'
import { useRouter } from '@tarojs/taro'
export default function WebViewPage() {
const router = useRouter()
const { url } = router.params
if (!url) return null
// Ensure url has protocol if missing (e.g. starts with //)
const fullUrl = url.startsWith('//') ? `https:${url}` : url
return <WebView src={fullUrl} />
}