专家
All checks were successful
Deploy to Server / deploy (push) Successful in 35s

This commit is contained in:
jeremygan2021
2026-02-24 17:52:12 +08:00
parent aac110ba1e
commit 46cf1727e1
12 changed files with 775 additions and 91 deletions

View File

@@ -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;
}
}
}
}
}
}

View File

@@ -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<any>(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 = () => {
<View className='fab' onClick={navigateToCreate}>
<AtIcon value='add' size='24' color='#fff' />
</View>
<AtFloatLayout isOpened={showExpert} title="技术专家信息" onClose={() => setShowExpert(false)}>
{selectedExpert && (
<View className='expert-modal-content'>
<View className='expert-header'>
<View className='avatar-container'>
<Image src={selectedExpert.avatar_url} className='expert-avatar' />
<View className='avatar-ring'></View>
</View>
<View className='expert-info'>
<View className='expert-name'>{selectedExpert.nickname}</View>
<View className='expert-title-badge'>
<AtIcon value='sketch' size='14' color='#ffd700' />
<Text>{selectedExpert.title || '技术专家'}</Text>
</View>
</View>
</View>
{selectedExpert.skills && selectedExpert.skills.length > 0 && (
<View className='expert-skills-section'>
<View className='section-label'>
<Text className='label-text'></Text>
<View className='label-line'></View>
</View>
<View className='skills-grid'>
{selectedExpert.skills.map((skill, idx) => (
<View key={idx} className='skill-tag'>
{typeof skill === 'object' && skill.icon && <Image src={skill.icon} className='skill-icon' />}
<Text className='skill-text'>{typeof skill === 'object' ? skill.text : skill}</Text>
</View>
))}
</View>
</View>
)}
</View>
)}
</AtFloatLayout>
</View>
)
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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<any[]>([])
const [showStarUsers, setShowStarUsers] = useState(false)
// Expert Detail
const [showExpert, setShowExpert] = useState(false)
const [selectedExpert, setSelectedExpert] = useState<any>(null)
const fetchDetail = async () => {
try {
const res = await getTopicDetail(Number(id))
@@ -162,6 +166,14 @@ const ForumDetail = () => {
if (loading) return <View className='forum-detail-page'><AtActivityIndicator mode='center' /></View>
if (!topic) return <View className='forum-detail-page'><View style={{padding: 20, textAlign: 'center'}}></View></View>
const showUserTitle = (e, user) => {
e.stopPropagation()
if (user.is_star || user.title) {
setSelectedExpert(user)
setShowExpert(true)
}
}
return (
<View className='forum-detail-page'>
<ScrollView scrollY style={{height: '100vh'}}>
@@ -172,7 +184,7 @@ const ForumDetail = () => {
<Text className='title'>{topic.title}</Text>
<View className='meta'>
<View className='author'>
<View className='author' onClick={(e) => showUserTitle(e, topic.author_info)}>
<Image className='avatar' src={topic.author_info?.avatar_url || 'https://via.placeholder.com/30'} />
<Text style={{fontWeight: 600, color: '#ccc'}}>{topic.author_info?.nickname}</Text>
{topic.is_verified_owner && <AtIcon value='check-circle' size='14' color='#00b96b' />}
@@ -214,7 +226,7 @@ const ForumDetail = () => {
{topic.replies?.map((reply, idx) => (
<View key={reply.id} className='reply-card'>
<Image className='avatar' src={reply.author_info?.avatar_url || 'https://via.placeholder.com/30'} />
<Image className='avatar' src={reply.author_info?.avatar_url || 'https://via.placeholder.com/30'} onClick={(e) => showUserTitle(e, reply.author_info)} />
<View className='reply-main'>
<View className='reply-header'>
<View style={{display: 'flex', flexDirection: 'column'}}>
@@ -271,6 +283,43 @@ const ForumDetail = () => {
</AtActionSheetItem>
))}
</AtActionSheet>
<AtFloatLayout isOpened={showExpert} title="技术专家信息" onClose={() => setShowExpert(false)}>
{selectedExpert && (
<View className='expert-modal-content'>
<View className='expert-header'>
<View className='avatar-container'>
<Image src={selectedExpert.avatar_url} className='expert-avatar' />
<View className='avatar-ring'></View>
</View>
<View className='expert-info'>
<View className='expert-name'>{selectedExpert.nickname}</View>
<View className='expert-title-badge'>
<AtIcon value='sketch' size='14' color='#ffd700' />
<Text>{selectedExpert.title || '技术专家'}</Text>
</View>
</View>
</View>
{selectedExpert.skills && selectedExpert.skills.length > 0 && (
<View className='expert-skills-section'>
<View className='section-label'>
<Text className='label-text'></Text>
<View className='label-line'></View>
</View>
<View className='skills-grid'>
{selectedExpert.skills.map((skill, idx) => (
<View key={idx} className='skill-tag'>
{typeof skill === 'object' && skill.icon && <Image src={skill.icon} className='skill-icon' />}
<Text className='skill-text'>{typeof skill === 'object' ? skill.text : skill}</Text>
</View>
))}
</View>
</View>
)}
</View>
)}
</AtFloatLayout>
</View>
)
}