diff --git a/backend/shop/admin.py b/backend/shop/admin.py index f822092..945ad88 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -401,8 +401,8 @@ class OrderAdmin(ModelAdmin): ) @admin.register(WeChatUser) -class WeChatUserAdmin(ModelAdmin): - list_display = ('nickname', 'phone_number', 'is_star', 'title', 'avatar_display', 'gender_display', 'province', 'city', 'created_at') +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') readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at') @@ -418,25 +418,38 @@ class WeChatUserAdmin(ModelAdmin): return choices.get(obj.gender, '未知') gender_display.short_description = "性别" - fieldsets = ( - ('基本信息', { - 'fields': ('user', 'nickname', 'phone_number', 'avatar_url', 'gender') - }), - ('专家认证', { - 'fields': ('is_star', 'title'), - 'description': '标记为明星技术用户/专家,将在社区中展示' - }), - ('位置信息', { + def get_fieldsets(self, request, obj=None): + fieldsets = [ + ('基本信息', { + 'fields': ('user', 'nickname', 'phone_number', 'avatar_url', 'gender') + }), + ] + + if obj and obj.is_star: + fieldsets.append(('专家认证', { + 'fields': ('is_star', 'title', 'skills', 'order'), + 'description': '标记为明星技术用户/专家,将在社区中展示' + })) + else: + fieldsets.append(('专家认证', { + 'fields': ('is_star',), + 'description': '标记为明星技术用户/专家,将在社区中展示。保存后若为专家用户,可进一步编辑专家信息。' + })) + + fieldsets.append(('位置信息', { 'fields': ('country', 'province', 'city') - }), - ('认证信息', { + })) + + fieldsets.append(('认证信息', { 'fields': ('openid', 'unionid', 'session_key'), 'classes': ('collapse',) - }), - ('时间信息', { + })) + + fieldsets.append(('时间信息', { 'fields': ('created_at', 'updated_at') - }), - ) + })) + + return fieldsets @admin.register(Distributor) class DistributorAdmin(ModelAdmin): diff --git a/backend/shop/migrations/0035_wechatuser_skills.py b/backend/shop/migrations/0035_wechatuser_skills.py new file mode 100644 index 0000000..bed7b22 --- /dev/null +++ b/backend/shop/migrations/0035_wechatuser_skills.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-24 09:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0034_remove_vccourse_schedule_time_vccourse_end_time_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='wechatuser', + name='skills', + field=models.JSONField(blank=True, default=list, help_text="格式: [{'icon': 'url', 'text': 'React'}, ...]", verbose_name='专家技能'), + ), + ] diff --git a/backend/shop/migrations/0036_alter_wechatuser_options_wechatuser_order.py b/backend/shop/migrations/0036_alter_wechatuser_options_wechatuser_order.py new file mode 100644 index 0000000..0cae35d --- /dev/null +++ b/backend/shop/migrations/0036_alter_wechatuser_options_wechatuser_order.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.1 on 2026-02-24 09:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0035_wechatuser_skills'), + ] + + operations = [ + migrations.AlterModelOptions( + name='wechatuser', + options={'ordering': ['order', '-created_at'], 'verbose_name': '微信用户', 'verbose_name_plural': '微信用户管理'}, + ), + migrations.AddField( + model_name='wechatuser', + name='order', + field=models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序权重'), + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index 361e771..920fca7 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -24,6 +24,8 @@ class WeChatUser(models.Model): # 明星技术用户/专家标识 is_star = models.BooleanField(default=False, verbose_name="是否明星技术用户") title = models.CharField(max_length=50, default="技术专家", verbose_name="专家头衔", blank=True) + skills = models.JSONField(default=list, verbose_name="专家技能", blank=True, help_text="格式: [{'icon': 'url', 'text': 'React'}, ...]") + order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") @@ -34,6 +36,7 @@ class WeChatUser(models.Model): class Meta: verbose_name = "微信用户" verbose_name_plural = "微信用户管理" + ordering = ['order', '-created_at'] class Distributor(models.Model): diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 173a07b..123d470 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', 'openid', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number', 'is_star', 'title'] - read_only_fields = ['id', 'openid', 'phone_number', 'is_star', 'title'] + fields = ['id', 'openid', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number', 'is_star', 'title', 'skills'] + read_only_fields = ['id', 'openid', 'phone_number', 'is_star', 'title', 'skills'] class DistributorSerializer(serializers.ModelSerializer): user_info = WeChatUserSerializer(source='user', read_only=True) diff --git a/backend/shop/views.py b/backend/shop/views.py index bf4b02d..a8767e3 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -1573,7 +1573,7 @@ class WeChatUserViewSet(viewsets.ReadOnlyModelViewSet): """ 获取明星技术用户列表 """ - stars = WeChatUser.objects.filter(is_star=True).order_by('-created_at') + stars = WeChatUser.objects.filter(is_star=True).order_by('order', '-created_at') serializer = self.get_serializer(stars, many=True) return Response(serializer.data) diff --git a/frontend/src/pages/ForumDetail.jsx b/frontend/src/pages/ForumDetail.jsx index 9aec200..2c22aa6 100644 --- a/frontend/src/pages/ForumDetail.jsx +++ b/frontend/src/pages/ForumDetail.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Upload, Tooltip, Grid, Modal } from 'antd'; -import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined } from '@ant-design/icons'; +import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined, StarFilled, CloseOutlined } from '@ant-design/icons'; import { getTopicDetail, createReply, uploadMedia, getStarUsers } from '../api'; import { useAuth } from '../context/AuthContext'; import LoginModal from '../components/LoginModal'; @@ -44,10 +44,6 @@ const ForumDetail = () => { // Star Users State const [starUsers, setStarUsers] = useState([]); - // User Info Modal State - const [userInfoModalVisible, setUserInfoModalVisible] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); - const fetchTopic = async () => { try { const res = await getTopicDetail(id); @@ -85,10 +81,16 @@ const ForumDetail = () => { message.info(`已添加 @${nickname}`); }; + // Expert Info Modal + const [expertModalVisible, setExpertModalVisible] = useState(false); + const [selectedExpert, setSelectedExpert] = useState(null); + const showUserTitle = (author) => { - if (author) { - setSelectedUser(author); - setUserInfoModalVisible(true); + if (author?.is_star) { + setSelectedExpert(author); + setExpertModalVisible(true); + } else if (author?.title) { + message.info(author.title); } }; @@ -231,13 +233,13 @@ const ForumDetail = () => { -
showUserTitle(topic.author_info)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}> - } - size={isMobile ? 'small' : 'default'} - /> -
+ } + size={isMobile ? 'small' : 'default'} + style={{ cursor: 'pointer' }} + onClick={() => showUserTitle(topic.author_info)} + /> {topic.author_info?.nickname} {topic.is_verified_owner && ( @@ -303,13 +305,13 @@ const ForumDetail = () => { styles={{ body: { padding: isMobile ? '15px' : '24px' } }} >
-
showUserTitle(reply.author_info)} style={{ cursor: 'pointer', height: 'fit-content' }}> - } - size={isMobile ? 'small' : 'default'} - /> -
+ } + size={isMobile ? 'small' : 'default'} + style={{ cursor: 'pointer' }} + onClick={() => showUserTitle(reply.author_info)} + />
@@ -419,42 +421,52 @@ const ForumDetail = () => { onClose={() => setLoginModalVisible(false)} onLoginSuccess={() => {}} /> - - setUserInfoModalVisible(false)} - footer={null} - centered - width={300} - styles={{ body: { textAlign: 'center', padding: '30px 20px' } }} - > - {selectedUser && ( -
- } - style={{ marginBottom: 16, border: '2px solid #f0f0f0' }} - /> - {selectedUser.nickname} - - {(selectedUser.is_star || selectedUser.title) && ( - - {selectedUser.title || '技术专家'} - - )} - - {selectedUser.is_verified_owner && ( -
- - 已认证购买用户 -
- )} -
- )} -
+ setExpertModalVisible(false)} + footer={null} + title={技术专家信息} + styles={{ + content: { background: 'rgba(30, 30, 30, 0.95)', border: '1px solid rgba(255, 255, 255, 0.1)', backdropFilter: 'blur(10px)' }, + header: { background: 'transparent', borderBottom: '1px solid rgba(255, 255, 255, 0.1)' }, + body: { background: 'transparent' }, + mask: { backdropFilter: 'blur(4px)' } + }} + closeIcon={} + bodyStyle={{ textAlign: 'center' }} + > + {selectedExpert && ( +
+ } style={{ border: '3px solid #ffd700', marginBottom: 15, boxShadow: '0 0 15px rgba(255, 215, 0, 0.2)' }} /> + {selectedExpert.nickname} + {selectedExpert.title || '技术专家'} + + {selectedExpert.skills && selectedExpert.skills.length > 0 && ( +
+ 擅长技能 + + {selectedExpert.skills.map((skill, idx) => ( +
+ {skill.icon && } + {skill.text} +
+ ))} +
+
+ )} +
+ )} +
+ {/* Edit Modal */} { const [category, setCategory] = useState('all'); const [createModalVisible, setCreateModalVisible] = useState(false); const [loginModalVisible, setLoginModalVisible] = useState(false); + const [expertModalVisible, setExpertModalVisible] = useState(false); + const [selectedExpert, setSelectedExpert] = useState(null); + + const showExpertInfo = (user) => { + setSelectedExpert(user); + setExpertModalVisible(true); + }; const fetchTopics = async (search = '', cat = '') => { setLoading(true); @@ -185,9 +192,9 @@ const ForumList = () => { {/* Mobile Experts */}
{starUsers.map(u => ( -
+
showExpertInfo(u)}> } offset={[-5, 5]}> - } style={{ border: '2px solid rgba(255, 215, 0, 0.3)' }} /> + } style={{ border: '2px solid rgba(255, 215, 0, 0.3)', cursor: 'pointer' }} />
{u.nickname} @@ -307,7 +314,7 @@ const ForumList = () => {
{starUsers.length > 0 ? ( starUsers.map(u => ( -
+
showExpertInfo(u)}> } />
@@ -370,6 +377,51 @@ const ForumList = () => { setCreateModalVisible(true); }} /> + + setExpertModalVisible(false)} + footer={null} + title={技术专家信息} + styles={{ + content: { background: 'rgba(30, 30, 30, 0.95)', border: '1px solid rgba(255, 255, 255, 0.1)', backdropFilter: 'blur(10px)' }, + header: { background: 'transparent', borderBottom: '1px solid rgba(255, 255, 255, 0.1)' }, + body: { background: 'transparent' }, + mask: { backdropFilter: 'blur(4px)' } + }} + closeIcon={} + bodyStyle={{ textAlign: 'center' }} + > + {selectedExpert && ( +
+ } style={{ border: '3px solid #ffd700', marginBottom: 15, boxShadow: '0 0 15px rgba(255, 215, 0, 0.2)' }} /> + {selectedExpert.nickname} + {selectedExpert.title || '技术专家'} + + {selectedExpert.skills && selectedExpert.skills.length > 0 && ( +
+ 擅长技能 + + {selectedExpert.skills.map((skill, idx) => ( +
+ {skill.icon && } + {skill.text} +
+ ))} +
+
+ )} +
+ )} +
); }; diff --git a/miniprogram/src/pages/forum/index.scss b/miniprogram/src/pages/forum/index.scss index aa7aa9c..fcfad1e 100644 --- a/miniprogram/src/pages/forum/index.scss +++ b/miniprogram/src/pages/forum/index.scss @@ -23,6 +23,11 @@ 100% { transform: scale(1); box-shadow: 0 4px 12px rgba(0, 185, 107, 0.4); } } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .hero-section { padding: 60px 20px 30px; text-align: center; @@ -455,4 +460,224 @@ color: #fff; } } + + /* Expert Modal Styles - Tech & Dark Theme */ + .at-float-layout { + .at-float-layout__overlay { + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(8px); + } + + .at-float-layout__container { + background-color: #0f1216 !important; /* Deep dark tech background */ + border-top: 1px solid rgba(0, 185, 107, 0.3); /* Tech green border */ + box-shadow: 0 -10px 40px rgba(0, 185, 107, 0.15); + border-radius: 24px 24px 0 0; /* More rounded top */ + + .layout-header { + background-color: #15191f; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding: 18px 24px; + + .layout-header__title { + color: #00b96b; /* Tech green */ + font-size: 18px; + font-weight: 700; + letter-spacing: 1px; + text-shadow: 0 0 10px rgba(0, 185, 107, 0.3); + } + + .layout-header__btn-close { + color: #666; + } + } + + .layout-body { + background-color: #0f1216; + padding: 0; + } + } + } + + .expert-modal-content { + padding: 30px 24px 60px; + color: #fff; + background: radial-gradient(circle at 50% 10%, rgba(0, 185, 107, 0.08), transparent 60%); + + .expert-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 36px; + position: relative; + + .avatar-container { + position: relative; + margin-bottom: 24px; + + .expert-avatar { + width: 100px; + height: 100px; + border-radius: 50%; + border: 2px solid #ffd700; /* Gold for expert */ + box-shadow: 0 0 30px rgba(255, 215, 0, 0.25), inset 0 0 10px rgba(255, 215, 0, 0.2); + z-index: 2; + position: relative; + } + + .avatar-ring { + position: absolute; + top: -12px; left: -12px; right: -12px; bottom: -12px; + border-radius: 50%; + border: 1px dashed rgba(255, 215, 0, 0.5); + animation: spin 12s linear infinite; + z-index: 1; + + &::after { + content: ''; + position: absolute; + top: -6px; left: -6px; right: -6px; bottom: -6px; + border-radius: 50%; + border: 1px solid rgba(0, 185, 107, 0.3); /* Outer green ring */ + animation: spin 8s reverse linear infinite; + } + } + } + + .expert-info { + text-align: center; + + .expert-name { + font-size: 26px; + font-weight: 800; + color: #fff; + margin-bottom: 12px; + text-shadow: 0 0 15px rgba(0, 185, 107, 0.5); + letter-spacing: 0.5px; + } + + .expert-title-badge { + display: inline-flex; + align-items: center; + gap: 8px; + background: linear-gradient(90deg, rgba(255, 215, 0, 0.15), rgba(255, 215, 0, 0.05)); + padding: 6px 16px; + border-radius: 20px; + border: 1px solid rgba(255, 215, 0, 0.4); + box-shadow: 0 0 15px rgba(255, 215, 0, 0.15); + + .at-icon { + text-shadow: 0 0 5px #ffd700; + } + + text { + font-size: 14px; + color: #ffd700; + font-weight: 700; + letter-spacing: 1px; + } + } + } + } + + .expert-skills-section { + margin-bottom: 30px; + background: rgba(255, 255, 255, 0.02); + border-radius: 20px; + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.06); + position: relative; + overflow: hidden; + + /* Tech corner accent */ + &::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 10px; height: 10px; + border-top: 2px solid #00b96b; + border-left: 2px solid #00b96b; + border-radius: 4px 0 0 0; + } + + &::after { + content: ''; + position: absolute; + bottom: 0; right: 0; + width: 10px; height: 10px; + border-bottom: 2px solid #00b96b; + border-right: 2px solid #00b96b; + border-radius: 0 0 4px 0; + } + + .section-label { + display: flex; + align-items: center; + margin-bottom: 20px; + + .label-text { + font-size: 15px; + font-weight: 700; + color: #00b96b; + margin-right: 12px; + text-transform: uppercase; + letter-spacing: 1px; + } + + .label-line { + flex: 1; + height: 1px; + background: linear-gradient(90deg, rgba(0, 185, 107, 0.5), transparent); + opacity: 0.6; + } + } + + .skills-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + + .skill-tag { + display: flex; + align-items: center; + background: rgba(0, 185, 107, 0.08); + padding: 8px 16px; + border-radius: 6px; + border: 1px solid rgba(0, 185, 107, 0.25); + transition: all 0.3s; + position: relative; + overflow: hidden; + + /* Left accent bar */ + &::before { + content: ''; + position: absolute; + top: 0; left: 0; width: 3px; height: 100%; + background: #00b96b; + opacity: 0.6; + } + + &:active { + background: rgba(0, 185, 107, 0.2); + transform: scale(0.98); + box-shadow: 0 0 10px rgba(0, 185, 107, 0.2); + } + + .skill-icon { + width: 20px; + height: 20px; + margin-right: 8px; + filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); + } + + .skill-text { + font-size: 13px; + color: #e0e0e0; + font-weight: 500; + letter-spacing: 0.5px; + } + } + } + } + } } diff --git a/miniprogram/src/pages/forum/index.tsx b/miniprogram/src/pages/forum/index.tsx index a6feb1f..384da72 100644 --- a/miniprogram/src/pages/forum/index.tsx +++ b/miniprogram/src/pages/forum/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react' import Taro, { usePullDownRefresh, useReachBottom, useDidShow, useShareAppMessage, useShareTimeline } from '@tarojs/taro' import { View, Text, Image, Swiper, SwiperItem, ScrollView } from '@tarojs/components' -import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator } from 'taro-ui' +import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator, AtFloatLayout } from 'taro-ui' import { getTopics, getAnnouncements, getStarUsers } from '../../api' import './index.scss' @@ -16,6 +16,10 @@ const ForumList = () => { const [currentTab, setCurrentTab] = useState(0) const isMounted = useRef(false) + // Expert Detail + const [showExpert, setShowExpert] = useState(false) + const [selectedExpert, setSelectedExpert] = useState(null) + const categories = [ { title: '全部话题', key: 'all' }, { title: '技术讨论', key: 'discussion' }, @@ -180,10 +184,8 @@ const ForumList = () => { const showUserTitle = (e, user) => { e.stopPropagation() if (user.is_star || user.title) { - Taro.showToast({ - title: user.title || '技术专家', - icon: 'none' - }) + setSelectedExpert(user) + setShowExpert(true) } } @@ -324,6 +326,43 @@ const ForumList = () => { + + setShowExpert(false)}> + {selectedExpert && ( + + + + + + + + {selectedExpert.nickname} + + + {selectedExpert.title || '技术专家'} + + + + + {selectedExpert.skills && selectedExpert.skills.length > 0 && ( + + + 擅长技能 + + + + {selectedExpert.skills.map((skill, idx) => ( + + {typeof skill === 'object' && skill.icon && } + {typeof skill === 'object' ? skill.text : skill} + + ))} + + + )} + + )} + ) } diff --git a/miniprogram/src/subpackages/forum/detail/detail.scss b/miniprogram/src/subpackages/forum/detail/detail.scss index 9032f5d..84b3d31 100644 --- a/miniprogram/src/subpackages/forum/detail/detail.scss +++ b/miniprogram/src/subpackages/forum/detail/detail.scss @@ -302,7 +302,7 @@ background: rgba(255,255,255,0.1); } } - + .send-btn { color: #00b96b; font-weight: 700; @@ -310,4 +310,255 @@ padding: 0 8px; } } + + /* Expert Modal Styles - Tech & Dark Theme */ + .at-float-layout { + .at-float-layout__overlay { + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(8px); + } + + .at-float-layout__container { + background-color: #0f1216 !important; /* Deep dark tech background */ + border-top: 1px solid rgba(0, 185, 107, 0.3); /* Tech green border */ + box-shadow: 0 -10px 40px rgba(0, 185, 107, 0.15); + border-radius: 24px 24px 0 0; /* More rounded top */ + + .layout-header { + background-color: #15191f; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding: 18px 24px; + + .layout-header__title { + color: #00b96b; /* Tech green */ + font-size: 18px; + font-weight: 700; + letter-spacing: 1px; + text-shadow: 0 0 10px rgba(0, 185, 107, 0.3); + } + + .layout-header__btn-close { + color: #666; + } + } + + .layout-body { + background-color: #0f1216; + padding: 0; + } + } + } + + .expert-modal-content { + padding: 30px 24px 60px; + color: #fff; + background: radial-gradient(circle at 50% 10%, rgba(0, 185, 107, 0.08), transparent 60%); + + .expert-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 36px; + position: relative; + + .avatar-container { + position: relative; + margin-bottom: 24px; + + .expert-avatar { + width: 100px; + height: 100px; + border-radius: 50%; + border: 2px solid #ffd700; /* Gold for expert */ + box-shadow: 0 0 30px rgba(255, 215, 0, 0.25), inset 0 0 10px rgba(255, 215, 0, 0.2); + z-index: 2; + position: relative; + } + + .avatar-ring { + position: absolute; + top: -12px; left: -12px; right: -12px; bottom: -12px; + border-radius: 50%; + border: 1px dashed rgba(255, 215, 0, 0.5); + animation: spin 12s linear infinite; + z-index: 1; + + &::after { + content: ''; + position: absolute; + top: -6px; left: -6px; right: -6px; bottom: -6px; + border-radius: 50%; + border: 1px solid rgba(0, 185, 107, 0.3); /* Outer green ring */ + animation: spin 8s reverse linear infinite; + } + } + } + + .expert-info { + text-align: center; + + .expert-name { + font-size: 26px; + font-weight: 800; + color: #fff; + margin-bottom: 12px; + text-shadow: 0 0 15px rgba(0, 185, 107, 0.5); + letter-spacing: 0.5px; + } + + .expert-title-badge { + display: inline-flex; + align-items: center; + gap: 8px; + background: linear-gradient(90deg, rgba(255, 215, 0, 0.15), rgba(255, 215, 0, 0.05)); + padding: 6px 16px; + border-radius: 20px; + border: 1px solid rgba(255, 215, 0, 0.4); + box-shadow: 0 0 15px rgba(255, 215, 0, 0.15); + + .at-icon { + text-shadow: 0 0 5px #ffd700; + } + + text { + font-size: 14px; + color: #ffd700; + font-weight: 700; + letter-spacing: 1px; + } + } + } + } + + .expert-skills-section { + margin-bottom: 30px; + background: rgba(255, 255, 255, 0.02); + border-radius: 20px; + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.06); + position: relative; + overflow: hidden; + + /* Tech corner accent */ + &::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 10px; height: 10px; + border-top: 2px solid #00b96b; + border-left: 2px solid #00b96b; + border-radius: 4px 0 0 0; + } + + &::after { + content: ''; + position: absolute; + bottom: 0; right: 0; + width: 10px; height: 10px; + border-bottom: 2px solid #00b96b; + border-right: 2px solid #00b96b; + border-radius: 0 0 4px 0; + } + + .section-label { + display: flex; + align-items: center; + margin-bottom: 20px; + + .label-text { + font-size: 15px; + font-weight: 700; + color: #00b96b; + margin-right: 12px; + text-transform: uppercase; + letter-spacing: 1px; + } + + .label-line { + flex: 1; + height: 1px; + background: linear-gradient(90deg, rgba(0, 185, 107, 0.5), transparent); + opacity: 0.6; + } + } + + .skills-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + + .skill-tag { + display: flex; + align-items: center; + background: rgba(0, 185, 107, 0.08); + padding: 8px 16px; + border-radius: 6px; + border: 1px solid rgba(0, 185, 107, 0.25); + transition: all 0.3s; + position: relative; + overflow: hidden; + + /* Left accent bar */ + &::before { + content: ''; + position: absolute; + top: 0; left: 0; width: 3px; height: 100%; + background: #00b96b; + opacity: 0.6; + } + + &:active { + background: rgba(0, 185, 107, 0.2); + transform: scale(0.98); + box-shadow: 0 0 10px rgba(0, 185, 107, 0.2); + } + + .skill-icon { + width: 20px; + height: 20px; + margin-right: 8px; + filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); + } + + .skill-text { + font-size: 13px; + color: #e0e0e0; + font-weight: 500; + letter-spacing: 0.5px; + } + } + } + } + + .expert-action { + display: flex; + justify-content: center; + + .action-btn { + width: 100%; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #00b96b 0%, #009456 100%); + border-radius: 26px; + box-shadow: 0 4px 20px rgba(0, 185, 107, 0.3); + gap: 6px; + transition: all 0.3s; + border: 1px solid rgba(255, 255, 255, 0.1); + + &:active { + transform: scale(0.98); + box-shadow: 0 2px 10px rgba(0, 185, 107, 0.2); + } + + text { + color: #fff; + font-size: 16px; + font-weight: 700; + letter-spacing: 1px; + } + } + } + } } diff --git a/miniprogram/src/subpackages/forum/detail/index.tsx b/miniprogram/src/subpackages/forum/detail/index.tsx index cdceb05..149e009 100644 --- a/miniprogram/src/subpackages/forum/detail/index.tsx +++ b/miniprogram/src/subpackages/forum/detail/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import Taro, { useRouter, useShareAppMessage, useDidShow } from '@tarojs/taro' import { View, Text, Image, Video, RichText, Input, ScrollView } from '@tarojs/components' -import { AtActivityIndicator, AtIcon, AtActionSheet, AtActionSheetItem } from 'taro-ui' +import { AtActivityIndicator, AtIcon, AtActionSheet, AtActionSheetItem, AtFloatLayout } from 'taro-ui' import { getTopicDetail, createReply, uploadMedia, getStarUsers } from '../../../api' import { marked } from 'marked' import './detail.scss' @@ -21,6 +21,10 @@ const ForumDetail = () => { const [starUsers, setStarUsers] = useState([]) const [showStarUsers, setShowStarUsers] = useState(false) + // Expert Detail + const [showExpert, setShowExpert] = useState(false) + const [selectedExpert, setSelectedExpert] = useState(null) + const fetchDetail = async () => { try { const res = await getTopicDetail(Number(id)) @@ -162,6 +166,14 @@ const ForumDetail = () => { if (loading) return if (!topic) return 话题不存在 + const showUserTitle = (e, user) => { + e.stopPropagation() + if (user.is_star || user.title) { + setSelectedExpert(user) + setShowExpert(true) + } + } + return ( @@ -172,7 +184,7 @@ const ForumDetail = () => { {topic.title} - + showUserTitle(e, topic.author_info)}> {topic.author_info?.nickname} {topic.is_verified_owner && } @@ -214,7 +226,7 @@ const ForumDetail = () => { {topic.replies?.map((reply, idx) => ( - + showUserTitle(e, reply.author_info)} /> @@ -271,6 +283,43 @@ const ForumDetail = () => { ))} + + setShowExpert(false)}> + {selectedExpert && ( + + + + + + + + {selectedExpert.nickname} + + + {selectedExpert.title || '技术专家'} + + + + + {selectedExpert.skills && selectedExpert.skills.length > 0 && ( + + + 擅长技能 + + + + {selectedExpert.skills.map((skill, idx) => ( + + {typeof skill === 'object' && skill.icon && } + {typeof skill === 'object' ? skill.text : skill} + + ))} + + + )} + + )} + ) }