From 5916d7eb3aaf50e3232bb1c12e86f28d2a8de744 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Wed, 25 Feb 2026 00:22:15 +0800 Subject: [PATCH] new --- backend/shop/admin.py | 38 +++++- backend/shop/models.py | 7 ++ frontend/src/pages/VCCourseDetail.jsx | 46 +++++++- frontend/src/pages/VCCourseDetail.module.less | 109 ++++++++++++++++++ frontend/src/pages/VCCourses.jsx | 2 +- miniprogram/src/pages/courses/detail.tsx | 6 +- miniprogram/src/pages/courses/index.tsx | 12 +- 7 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 frontend/src/pages/VCCourseDetail.module.less 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 ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {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 && (
课程大纲与详情 -
- {course.content} +
+ + {course.content} +
)} diff --git a/frontend/src/pages/VCCourseDetail.module.less b/frontend/src/pages/VCCourseDetail.module.less new file mode 100644 index 0000000..0ab99af --- /dev/null +++ b/frontend/src/pages/VCCourseDetail.module.less @@ -0,0 +1,109 @@ +.markdown-body { + color: #ddd; + font-size: 16px; + line-height: 1.8; + + h1, h2, h3, h4, h5, h6 { + color: #fff; + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + } + + h1 { font-size: 2em; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 0.3em; } + h2 { font-size: 1.5em; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 0.3em; } + h3 { font-size: 1.25em; } + h4 { font-size: 1em; } + h5 { font-size: 0.875em; } + h6 { font-size: 0.85em; color: #888; } + + p { + margin-top: 0; + margin-bottom: 16px; + } + + a { + color: #1890ff; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + + ul, ol { + margin-top: 0; + margin-bottom: 16px; + padding-left: 2em; + } + + li { + word-wrap: break-all; + } + + blockquote { + margin: 0 0 16px; + padding: 0 1em; + color: #8b949e; + border-left: 0.25em solid #30363d; + } + + /* Table Styles */ + table { + display: block; + width: 100%; + width: max-content; + max-width: 100%; + overflow: auto; + border-spacing: 0; + border-collapse: collapse; + margin-top: 0; + margin-bottom: 16px; + + thead { + background-color: rgba(255, 255, 255, 0.1); + } + + tr { + background-color: transparent; + border-top: 1px solid rgba(255, 255, 255, 0.1); + + &:nth-child(2n) { + background-color: rgba(255, 255, 255, 0.05); + } + } + + th, td { + padding: 6px 13px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + th { + font-weight: 600; + text-align: left; + /* Ensure text color is readable */ + color: #fff; + } + + td { + color: #ddd; + } + } + + /* Inline Code */ + code:not([class*="language-"]) { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(110, 118, 129, 0.4); + border-radius: 6px; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + } + + /* Images */ + img { + max-width: 100%; + box-sizing: content-box; + background-color: transparent; + } +} diff --git a/frontend/src/pages/VCCourses.jsx b/frontend/src/pages/VCCourses.jsx index 214bd8d..9dea3b6 100644 --- a/frontend/src/pages/VCCourses.jsx +++ b/frontend/src/pages/VCCourses.jsx @@ -35,7 +35,7 @@ const VCCourses = () => { VC CODING COURSES - 探索 VB Coding 软件与硬件课程,开启您的编程之旅。 + 探索 VC Coding 软件与硬件课程,开启您的编程之旅。
diff --git a/miniprogram/src/pages/courses/detail.tsx b/miniprogram/src/pages/courses/detail.tsx index 2d35f16..621c3dd 100644 --- a/miniprogram/src/pages/courses/detail.tsx +++ b/miniprogram/src/pages/courses/detail.tsx @@ -39,7 +39,7 @@ export default function CourseDetail() { useShareAppMessage(() => { return { - title: detail?.title || 'VB 课程详情', + title: detail?.title || 'VC 课程详情', path: `/pages/courses/detail?id=${detail?.id}`, imageUrl: detail?.cover_image_url } @@ -47,7 +47,7 @@ export default function CourseDetail() { useShareTimeline(() => { return { - title: detail?.title || 'VB 课程详情', + title: detail?.title || 'VC 课程详情', query: `id=${detail?.id}`, imageUrl: detail?.cover_image_url } @@ -80,7 +80,7 @@ export default function CourseDetail() { {detail.title} - {typeMap[detail.course_type] || 'VB课程'} + {typeMap[detail.course_type] || 'VC课程'} {detail.tag && {detail.tag}} ¥{detail.price} diff --git a/miniprogram/src/pages/courses/index.tsx b/miniprogram/src/pages/courses/index.tsx index eabcb90..ea204c2 100644 --- a/miniprogram/src/pages/courses/index.tsx +++ b/miniprogram/src/pages/courses/index.tsx @@ -26,14 +26,14 @@ export default function CourseIndex() { useShareAppMessage(() => { return { - title: 'VB COURSES - 探索 VB 编程课程', + title: 'VC COURSES - 探索 VC 编程课程', path: '/pages/courses/index' } }) useShareTimeline(() => { return { - title: 'VB COURSES - 探索 VB 编程课程' + title: 'VC COURSES - 探索 VC 编程课程' } }) @@ -48,14 +48,14 @@ export default function CourseIndex() { - VB COURSES - 探索 VB 编程课程 + VC COURSES + 探索 VC 编程课程 {courseList.length === 0 ? ( - 暂无 VB 课程内容 + 暂无 VC 课程内容 ) : ( courseList.map((item) => ( @@ -64,7 +64,7 @@ export default function CourseIndex() { {item.cover_image_url ? ( ) : ( - VB + VC )}