diff --git a/backend/shop/admin.py b/backend/shop/admin.py
index 0f9407e..41578b7 100644
--- a/backend/shop/admin.py
+++ b/backend/shop/admin.py
@@ -232,7 +232,8 @@ class ServiceOrderAdmin(ModelAdmin):
@admin.register(VCCourse)
class VCCourseAdmin(OrderableAdminMixin, ModelAdmin):
- list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at', 'order_actions')
+ list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at', 'order', 'order_actions')
+ list_editable = ('order',)
search_fields = ('title', 'description', 'instructor', 'tag')
list_filter = ('course_type', 'instructor', 'tag')
fieldsets = (
@@ -404,11 +405,44 @@ class OrderAdmin(ModelAdmin):
}),
)
+class GenderFilter(admin.SimpleListFilter):
+ title = '性别'
+ parameter_name = 'gender'
+
+ def lookups(self, request, model_admin):
+ return (
+ (1, '男'),
+ (2, '女'),
+ (0, '未知'),
+ )
+
+ def queryset(self, request, queryset):
+ if self.value():
+ return queryset.filter(gender=self.value())
+ return queryset
+
+class UserSourceFilter(admin.SimpleListFilter):
+ title = '用户来源'
+ parameter_name = 'user_source'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('miniprogram', '仅小程序用户'),
+ ('both', '网页小程序都已注册'),
+ )
+
+ def queryset(self, request, queryset):
+ if self.value() == 'miniprogram':
+ return queryset.filter(user__isnull=True)
+ if self.value() == 'both':
+ return queryset.filter(user__isnull=False)
+ return queryset
+
@admin.register(WeChatUser)
class WeChatUserAdmin(OrderableAdminMixin, ModelAdmin):
list_display = ('nickname', 'phone_number', 'is_star', 'title', 'avatar_display', 'gender_display', 'province', 'city', 'created_at', 'order_actions')
search_fields = ('nickname', 'openid', 'phone_number')
- list_filter = ('is_star', 'gender', 'province', 'city', 'created_at')
+ list_filter = ('is_star', GenderFilter, UserSourceFilter, 'province', 'city', 'created_at')
readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at')
def avatar_display(self, obj):
diff --git a/backend/shop/models.py b/backend/shop/models.py
index 084abeb..1c507ef 100644
--- a/backend/shop/models.py
+++ b/backend/shop/models.py
@@ -376,6 +376,13 @@ class VCCourse(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前")
+ def save(self, *args, **kwargs):
+ is_new = self.pk is None
+ super().save(*args, **kwargs)
+ if is_new and self.order == 0:
+ VCCourse.objects.filter(pk=self.pk).update(order=self.pk)
+ self.order = self.pk
+
def __str__(self):
return self.title
diff --git a/frontend/src/pages/VCCourseDetail.jsx b/frontend/src/pages/VCCourseDetail.jsx
index a10f03e..b30f4e7 100644
--- a/frontend/src/pages/VCCourseDetail.jsx
+++ b/frontend/src/pages/VCCourseDetail.jsx
@@ -5,6 +5,14 @@ import { ArrowLeftOutlined, ClockCircleOutlined, UserOutlined, BookOutlined, For
import { getVCCourseDetail, createOrder, nativePay, queryOrderStatus } from '../api';
import { useAuth } from '../context/AuthContext';
import { QRCodeSVG } from 'qrcode.react';
+import ReactMarkdown from 'react-markdown';
+import remarkMath from 'remark-math';
+import rehypeKatex from 'rehype-katex';
+import remarkGfm from 'remark-gfm';
+import rehypeRaw from 'rehype-raw';
+import 'katex/dist/katex.min.css';
+import styles from './VCCourseDetail.module.less';
+import CodeBlock from '../components/CodeBlock';
const { Title, Paragraph } = Typography;
@@ -28,6 +36,34 @@ const VCCourseDetail = () => {
// 优先从 URL 获取,如果没有则从 localStorage 获取
const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
+ const markdownComponents = {
+ // eslint-disable-next-line no-unused-vars
+ code({node, inline, className, children, ...props}) {
+ const match = /language-(\w+)/.exec(className || '')
+ return !inline && match ? (
+
+ {children}
+
+ )
+ },
+ // eslint-disable-next-line no-unused-vars
+ img({node, ...props}) {
+ return (
+
+ );
+ }
+ };
+
useEffect(() => {
const fetchDetail = async () => {
try {
@@ -262,8 +298,14 @@ const VCCourseDetail = () => {
{course.content && (