diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc
index a0437bf..e314e88 100644
Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc
index ffa483a..b8e2040 100644
Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc
index 114fcab..97bf4b1 100644
Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ
diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py
index 7715f9d..1a0637f 100644
--- a/backend/shop/serializers.py
+++ b/backend/shop/serializers.py
@@ -22,8 +22,8 @@ class CommissionLogSerializer(serializers.ModelSerializer):
class WeChatUserSerializer(serializers.ModelSerializer):
class Meta:
model = WeChatUser
- fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number', 'is_star', 'title']
- read_only_fields = ['id', 'phone_number', 'is_star', 'title']
+ fields = ['id', 'openid', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number', 'is_star', 'title']
+ read_only_fields = ['id', 'openid', 'phone_number', 'is_star', 'title']
class DistributorSerializer(serializers.ModelSerializer):
user_info = WeChatUserSerializer(source='user', read_only=True)
diff --git a/backend/shop/urls.py b/backend/shop/urls.py
index 3b32104..7b6e9e3 100644
--- a/backend/shop/urls.py
+++ b/backend/shop/urls.py
@@ -15,7 +15,7 @@ router.register(r'courses', VCCourseViewSet)
router.register(r'course-enrollments', CourseEnrollmentViewSet)
router.register(r'service-orders', ServiceOrderViewSet)
router.register(r'distributor', DistributorViewSet, basename='distributor')
-router.register(r'users', WeChatUserViewSet)
+router.register(r'users', WeChatUserViewSet, basename='wechatuser')
urlpatterns = [
re_path(r'^finish/?$', payment_finish, name='payment-finish'),
diff --git a/backend/shop/views.py b/backend/shop/views.py
index 4786630..9d04602 100644
--- a/backend/shop/views.py
+++ b/backend/shop/views.py
@@ -1001,6 +1001,7 @@ def wechat_login(request):
return Response({
'token': token,
+ 'id': user.id,
'openid': openid,
'is_new': created,
'nickname': user.nickname
@@ -1092,6 +1093,7 @@ def phone_login(request):
return Response({
'token': token,
+ 'id': user.id,
'openid': user.openid,
'nickname': user.nickname,
'avatar_url': user.avatar_url,
@@ -1341,6 +1343,15 @@ class WeChatUserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = WeChatUser.objects.all()
serializer_class = WeChatUserSerializer
+ @action(detail=False, methods=['get'])
+ def me(self, request):
+ """获取当前用户信息"""
+ user = get_current_wechat_user(request)
+ if not user:
+ return Response({'error': 'Unauthorized'}, status=401)
+ serializer = self.get_serializer(user)
+ return Response(serializer.data)
+
@action(detail=False, methods=['get'])
def stars(self, request):
"""
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 8c41445..bbc75ad 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -37,15 +37,7 @@ export const enrollCourse = (data) => api.post('/course-enrollments/', data);
export const sendSms = (data) => api.post('/auth/send-sms/', data);
export const queryMyOrders = (data) => api.post('/orders/my_orders/', data);
export const phoneLogin = (data) => api.post('/auth/phone-login/', data);
-export const getUserInfo = () => {
- // 如果没有获取用户信息的接口,可以暂时从本地解析或依赖 update_user_info 的返回
- // 但后端有 /wechat/update/ 可以返回用户信息,或者我们可以加一个 /auth/me/
- // 目前 phone_login 返回了用户信息,前端可以保存。
- // 如果需要刷新,可以复用 update_user_info(虽然名字叫update,但传空通常返回当前信息,需确认后端逻辑)
- // 查看后端逻辑:update_user_info 是 patch 更新,如果 data 为空,update 不会执行但会返回 serializer.data
- return api.post('/wechat/update/', {});
-};
-
+export const getUserInfo = () => api.get('/users/me/');
export const updateUserInfo = (data) => api.post('/wechat/update/', data);
export const uploadUserAvatar = (data) => {
// 使用 axios 直接请求外部接口,避免 base URL 和拦截器干扰
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
index 973540c..7180a6a 100644
--- a/frontend/src/context/AuthContext.jsx
+++ b/frontend/src/context/AuthContext.jsx
@@ -1,5 +1,7 @@
import React, { createContext, useState, useEffect, useContext } from 'react';
+import { getUserInfo } from '../api';
+
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
@@ -7,16 +9,46 @@ export const AuthProvider = ({ children }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
- const storedUser = localStorage.getItem('user');
- if (storedUser) {
- try {
- setUser(JSON.parse(storedUser));
- } catch (e) {
- console.error("Failed to parse user from storage", e);
- localStorage.removeItem('user');
- }
- }
- setLoading(false);
+ const initAuth = async () => {
+ const storedToken = localStorage.getItem('token');
+ const storedUser = localStorage.getItem('user');
+
+ if (storedToken) {
+ try {
+ // 1. 优先尝试从本地获取
+ if (storedUser) {
+ try {
+ const parsedUser = JSON.parse(storedUser);
+ // 如果本地数据包含 ID,直接使用
+ if (parsedUser.id) {
+ setUser(parsedUser);
+ } else {
+ // 如果没有 ID,标记为需要刷新
+ throw new Error("Missing ID in stored user");
+ }
+ } catch (e) {
+ // 解析失败或数据不完整,继续从服务器获取
+ }
+ }
+
+ // 2. 总是尝试从服务器获取最新信息(或作为兜底)
+ // 这样可以确保 ID 存在,且信息是最新的
+ const res = await getUserInfo();
+ if (res.data) {
+ setUser(res.data);
+ localStorage.setItem('user', JSON.stringify(res.data));
+ }
+ } catch (error) {
+ console.error("Failed to fetch user info:", error);
+ // 如果 token 失效,可能需要登出?
+ // 暂时不强制登出,只清除无效的本地 user
+ if (!user) localStorage.removeItem('user');
+ }
+ }
+ setLoading(false);
+ };
+
+ initAuth();
}, []);
const login = (userData) => {
diff --git a/frontend/src/pages/ForumDetail.jsx b/frontend/src/pages/ForumDetail.jsx
index 3a108e3..cce658d 100644
--- a/frontend/src/pages/ForumDetail.jsx
+++ b/frontend/src/pages/ForumDetail.jsx
@@ -167,7 +167,14 @@ const ForumDetail = () => {
返回列表
- {user && topic.author === user.id && (
+ {/* Debug Info: Remove in production */}
+ {/*
+ User ID: {user?.id} ({typeof user?.id})
+ Topic Author: {topic.author} ({typeof topic.author})
+ Match: {String(topic.author) === String(user?.id) ? 'Yes' : 'No'}
+
*/}
+
+ {user && String(topic.author) === String(user.id) && (
diff --git a/miniprogram/README.md b/miniprogram/README.md
index a80ba37..952d911 100644
--- a/miniprogram/README.md
+++ b/miniprogram/README.md
@@ -20,6 +20,11 @@ Taro + React + TypeScript 微信小程序项目,对接 Django 后端,支持
## 快速开始
+小程序id
+
+wxdf2ca73e6c0929f0
+
+
### 1. 环境准备
确保已安装 Node.js (>=16) 和 npm。
diff --git a/miniprogram/config/index.js b/miniprogram/config/index.js
index 2d4ff5e..a2d29ea 100644
--- a/miniprogram/config/index.js
+++ b/miniprogram/config/index.js
@@ -20,9 +20,14 @@ const config = {
}
},
framework: 'react',
- compiler: 'webpack5',
+ compiler: {
+ type: 'webpack5',
+ prebundle: {
+ enable: false
+ }
+ },
cache: {
- enable: true // Enable cache for better build performance
+ enable: false // Disable cache to fix prebundle error
},
mini: {
postcss: {
diff --git a/miniprogram/src/pages/forum/index.scss b/miniprogram/src/pages/forum/index.scss
index 8465aaa..66937e2 100644
--- a/miniprogram/src/pages/forum/index.scss
+++ b/miniprogram/src/pages/forum/index.scss
@@ -65,20 +65,123 @@
}
}
+ .section-container {
+ margin: 0 10px 15px;
+ background: #141414; /* Darker card background */
+ border-radius: 12px;
+ padding: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+
+ .section-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ color: #fff;
+ }
+ }
+
+ .announcement-swiper {
+ height: 40px;
+
+ .announcement-item {
+ height: 40px;
+ display: flex;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.05);
+ padding: 0 12px;
+ border-radius: 6px;
+
+ .item-text {
+ font-size: 14px;
+ color: #ccc;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+
+ .star-users-scroll {
+ white-space: nowrap;
+ width: 100%;
+
+ .star-user-card {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ margin-right: 20px;
+ width: 80px;
+ background: rgba(255, 255, 255, 0.05);
+ padding: 12px 8px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+
+ .user-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: 2px solid #ffd700;
+ margin-bottom: 8px;
+ }
+
+ .user-name {
+ font-size: 12px;
+ font-weight: bold;
+ color: #fff;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 2px;
+ }
+
+ .user-title {
+ font-size: 10px;
+ color: #888;
+ }
+ }
+ }
+ }
+
.tabs-wrapper {
background-color: #000;
-
+ margin-bottom: 10px;
+
+ /* Override Taro UI default white background */
+ .at-tabs {
+ background-color: transparent;
+ height: auto;
+ }
+
+ .at-tabs__header {
+ background-color: transparent;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ text-align: left;
+ }
+
.at-tabs__item {
color: #888;
+ font-size: 14px;
+ padding: 12px 24px;
&--active {
color: #00b96b;
font-weight: bold;
+ font-size: 16px;
}
}
.at-tabs__item-underline {
background-color: #00b96b;
+ bottom: 0;
}
}
diff --git a/miniprogram/src/pages/forum/index.tsx b/miniprogram/src/pages/forum/index.tsx
index e529459..057f27d 100644
--- a/miniprogram/src/pages/forum/index.tsx
+++ b/miniprogram/src/pages/forum/index.tsx
@@ -1,13 +1,14 @@
import React, { useState, useEffect } from 'react'
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'
-import { View, Text, Image, Button } from '@tarojs/components'
-import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator, AtFab } from 'taro-ui'
-import { getTopics } from '../../api'
-import { useLogin } from '../../utils/hooks' // Assuming a hook or just use Taro.getStorageSync
+import { View, Text, Image, Swiper, SwiperItem, ScrollView } from '@tarojs/components'
+import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator } from 'taro-ui'
+import { getTopics, getAnnouncements, getStarUsers } from '../../api'
import './index.scss'
const ForumList = () => {
const [topics, setTopics] = useState([])
+ const [announcements, setAnnouncements] = useState([])
+ const [starUsers, setStarUsers] = useState([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
@@ -15,13 +16,26 @@ const ForumList = () => {
const [currentTab, setCurrentTab] = useState(0)
const categories = [
- { title: '全部', key: 'all' },
- { title: '讨论', key: 'discussion' },
- { title: '求助', key: 'help' },
- { title: '分享', key: 'share' },
- { title: '公告', key: 'notice' },
+ { title: '全部话题', key: 'all' },
+ { title: '技术讨论', key: 'discussion' },
+ { title: '求助问答', key: 'help' },
+ { title: '经验分享', key: 'share' },
+ { title: '官方公告', key: 'notice' },
]
+ const fetchExtraData = async () => {
+ try {
+ const [announceRes, starRes] = await Promise.all([
+ getAnnouncements(),
+ getStarUsers()
+ ])
+ setAnnouncements(announceRes.results || announceRes.data || [])
+ setStarUsers(starRes.data || [])
+ } catch (err) {
+ console.error('Fetch extra data failed', err)
+ }
+ }
+
const fetchList = async (reset = false) => {
if (loading) return
if (!reset && !hasMore) return
@@ -62,10 +76,12 @@ const ForumList = () => {
useEffect(() => {
fetchList(true)
+ fetchExtraData()
}, [currentTab])
usePullDownRefresh(() => {
fetchList(true)
+ fetchExtraData()
})
useReachBottom(() => {
@@ -127,7 +143,7 @@ const ForumList = () => {
- Quant Speed Community
+ Quant Speed Developer Community
技术交流 · 硬件开发 · 官方支持
@@ -137,11 +153,59 @@ const ForumList = () => {
onChange={handleSearch}
onActionClick={onSearchConfirm}
onConfirm={onSearchConfirm}
- placeholder='搜索话题...'
+ placeholder='搜索感兴趣的话题...'
/>
+
+
+ 发布新帖
+
+ {/* Announcements Section */}
+ {announcements.length > 0 && (
+
+
+
+ 社区公告
+
+
+ {announcements.map(item => (
+
+
+ {item.title}
+
+
+ ))}
+
+
+ )}
+
+ {/* Star Users Section */}
+ {starUsers.length > 0 && (
+
+
+
+ 技术专家榜
+
+
+ {starUsers.map(user => (
+
+
+ {user.nickname}
+ {user.title || '专家'}
+
+ ))}
+
+
+ )}
+
AI 全栈解决方案
从数据处理到模型部署,我们为您提供一站式 AI 基础设施服务。
+
+
+ Taro.navigateTo({ url: '/pages/courses/index' })}
+ >
+ 探索 VB 课程
+ →
+
+
diff --git a/miniprogram/src/subpackages/forum/create/index.tsx b/miniprogram/src/subpackages/forum/create/index.tsx
index ab83383..62a0175 100644
--- a/miniprogram/src/subpackages/forum/create/index.tsx
+++ b/miniprogram/src/subpackages/forum/create/index.tsx
@@ -1,10 +1,13 @@
-import React, { useState } from 'react'
-import Taro from '@tarojs/taro'
+import React, { useState, useEffect } from 'react'
+import Taro, { useRouter } from '@tarojs/taro'
import { View, Text, Input, Textarea, Button, Picker } from '@tarojs/components'
-import { createTopic, uploadMedia } from '../../../api'
+import { createTopic, updateTopic, getTopicDetail, uploadMedia } from '../../../api'
import './create.scss'
const CreateTopic = () => {
+ const router = useRouter()
+ const { id } = router.params
+
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [categoryIndex, setCategoryIndex] = useState(0)
@@ -16,6 +19,30 @@ const CreateTopic = () => {
{ key: 'share', label: '经验分享' },
]
+ useEffect(() => {
+ if (id) {
+ const fetchDetail = async () => {
+ Taro.showLoading({ title: '加载中...' })
+ try {
+ const res = await getTopicDetail(Number(id))
+ const topic = res.data
+ setTitle(topic.title)
+ setContent(topic.content)
+ const idx = categories.findIndex(c => c.key === topic.category)
+ if (idx !== -1) setCategoryIndex(idx)
+
+ Taro.setNavigationBarTitle({ title: '编辑话题' })
+ } catch (error) {
+ console.error(error)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+ fetchDetail()
+ }
+ }, [id])
+
const handleCategoryChange = (e) => {
setCategoryIndex(e.detail.value)
}
@@ -68,19 +95,28 @@ const CreateTopic = () => {
setLoading(true)
try {
- const res = await createTopic({
- title,
- content,
- category: categories[categoryIndex].key
- })
+ if (id) {
+ await updateTopic(Number(id), {
+ title,
+ content,
+ category: categories[categoryIndex].key
+ })
+ Taro.showToast({ title: '更新成功', icon: 'success' })
+ } else {
+ await createTopic({
+ title,
+ content,
+ category: categories[categoryIndex].key
+ })
+ Taro.showToast({ title: '发布成功', icon: 'success' })
+ }
- Taro.showToast({ title: '发布成功', icon: 'success' })
setTimeout(() => {
Taro.navigateBack()
}, 1500)
} catch (error) {
console.error(error)
- Taro.showToast({ title: '发布失败', icon: 'none' })
+ Taro.showToast({ title: id ? '更新失败' : '发布失败', icon: 'none' })
} finally {
setLoading(false)
}
@@ -127,7 +163,7 @@ const CreateTopic = () => {
onClick={handleSubmit}
disabled={loading}
>
- {loading ? '发布中...' : '发布话题'}
+ {loading ? (id ? '更新中...' : '发布中...') : (id ? '更新话题' : '发布话题')}
)
diff --git a/miniprogram/src/subpackages/forum/detail/index.tsx b/miniprogram/src/subpackages/forum/detail/index.tsx
index 253ea6b..a93d11a 100644
--- a/miniprogram/src/subpackages/forum/detail/index.tsx
+++ b/miniprogram/src/subpackages/forum/detail/index.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'
-import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
-import { View, Text, Image, Video, RichText, Input, Button } from '@tarojs/components'
+import Taro, { useRouter, useShareAppMessage, useDidShow } from '@tarojs/taro'
+import { View, Text, Image, Video, RichText, Input, ScrollView } from '@tarojs/components'
import { AtActivityIndicator, AtIcon } from 'taro-ui'
import { getTopicDetail, createReply, uploadMedia } from '../../../api'
import { marked } from 'marked'
@@ -15,6 +15,7 @@ const ForumDetail = () => {
const [replyContent, setReplyContent] = useState('')
const [sending, setSending] = useState(false)
const [htmlContent, setHtmlContent] = useState('')
+ const [userInfo, setUserInfo] = useState(null)
const fetchDetail = async () => {
try {
@@ -37,11 +38,20 @@ const ForumDetail = () => {
}
useEffect(() => {
+ const info = Taro.getStorageSync('userInfo')
+ if (info) setUserInfo(info)
+
if (id) {
fetchDetail()
}
}, [id])
+ useDidShow(() => {
+ if (id && !loading) {
+ fetchDetail()
+ }
+ })
+
useShareAppMessage(() => {
return {
title: topic?.title || '技术社区',
@@ -53,6 +63,12 @@ const ForumDetail = () => {
setReplyContent(e.detail.value)
}
+ const handleEdit = () => {
+ Taro.navigateTo({
+ url: `/subpackages/forum/create/index?id=${id}`
+ })
+ }
+
const handleUpload = async () => {
try {
const res = await Taro.chooseMedia({
@@ -125,6 +141,8 @@ const ForumDetail = () => {
return (
+
+
{topic.is_pinned && 置顶}
@@ -140,6 +158,13 @@ const ForumDetail = () => {
{new Date(topic.created_at).toLocaleDateString()}
•
{topic.view_count} 阅读
+
+ {userInfo && topic.author === userInfo.id && (
+
+
+ 编辑
+
+ )}
@@ -177,6 +202,8 @@ const ForumDetail = () => {
))}
+
+