This commit is contained in:
@@ -101,19 +101,7 @@ DATABASES = {
|
||||
|
||||
#从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。
|
||||
|
||||
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')
|
||||
# DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
|
||||
# if DB_HOST:
|
||||
# DATABASES['default'] = {
|
||||
# 'ENGINE': 'django.db.backends.postgresql',
|
||||
@@ -121,10 +109,22 @@ if DB_HOST:
|
||||
# 'USER': os.environ.get('DB_USER', 'market'),
|
||||
# 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
|
||||
# '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
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ class VCCourseAdmin(OrderableAdminMixin, ModelAdmin):
|
||||
'fields': ('title', 'description', 'course_type', 'tag', 'price')
|
||||
}),
|
||||
('视频设置', {
|
||||
'fields': ('is_video_course', 'video_url'),
|
||||
'fields': ('is_video_course', 'video_url', 'video_embed_code'),
|
||||
'description': '设置是否为视频课程及视频链接'
|
||||
}),
|
||||
('课程安排', {
|
||||
|
||||
18
backend/shop/migrations/0039_vccourse_video_embed_code.py
Normal file
18
backend/shop/migrations/0039_vccourse_video_embed_code.py
Normal 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='视频嵌入代码'),
|
||||
),
|
||||
]
|
||||
@@ -365,6 +365,7 @@ class VCCourse(models.Model):
|
||||
# 视频课程相关
|
||||
is_video_course = models.BooleanField(default=False, verbose_name="是否视频课程")
|
||||
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="勾选后,前端将显示具体的开课时间")
|
||||
|
||||
@@ -176,6 +176,7 @@ class VCCourseSerializer(serializers.ModelSerializer):
|
||||
display_detail_image = serializers.SerializerMethodField()
|
||||
course_type_display = serializers.CharField(source='get_course_type_display', read_only=True)
|
||||
video_url = serializers.SerializerMethodField()
|
||||
video_embed_code = serializers.SerializerMethodField()
|
||||
is_purchased = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
@@ -234,6 +235,18 @@ class VCCourseSerializer(serializers.ModelSerializer):
|
||||
|
||||
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):
|
||||
"""
|
||||
ESP32配置序列化器
|
||||
|
||||
@@ -238,7 +238,12 @@ const VCCourseDetail = () => {
|
||||
position: 'relative',
|
||||
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
|
||||
src={course.video_url}
|
||||
controls
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineAppConfig({
|
||||
'pages/courses/detail',
|
||||
'pages/forum/index',
|
||||
'pages/goods/detail',
|
||||
'pages/webview/index',
|
||||
'pages/cart/cart',
|
||||
'pages/order/checkout',
|
||||
'pages/order/payment',
|
||||
|
||||
@@ -71,6 +71,19 @@ export default function CourseDetail() {
|
||||
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 (
|
||||
<View className='page-container'>
|
||||
<ScrollView scrollY className='scroll-content'>
|
||||
@@ -94,7 +107,19 @@ export default function CourseDetail() {
|
||||
{detail.is_video_course && (
|
||||
<View className='section video-section'>
|
||||
<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
|
||||
src={detail.video_url}
|
||||
className='course-video'
|
||||
|
||||
3
miniprogram/src/pages/webview/index.config.ts
Normal file
3
miniprogram/src/pages/webview/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '加载中...'
|
||||
}
|
||||
14
miniprogram/src/pages/webview/index.tsx
Normal file
14
miniprogram/src/pages/webview/index.tsx
Normal 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} />
|
||||
}
|
||||
Reference in New Issue
Block a user