This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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': '设置是否为视频课程及视频链接'
|
||||||
}),
|
}),
|
||||||
('课程安排', {
|
('课程安排', {
|
||||||
|
|||||||
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="是否视频课程")
|
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="勾选后,前端将显示具体的开课时间")
|
||||||
|
|||||||
@@ -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配置序列化器
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
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