From 9215ec3b421bc4bd07070cdd90d3e2a646e2b4fe Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 26 Feb 2026 15:10:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/shop/serializers.py | 15 +- .../components/MarkdownReader/CodeBlock.tsx | 42 ++++ .../src/components/MarkdownReader/index.scss | 75 ++++++ .../src/components/MarkdownReader/index.tsx | 90 +++++++ miniprogram/src/pages/forum/index.scss | 236 +++++++++--------- miniprogram/src/pages/user/index.config.ts | 4 +- miniprogram/src/pages/user/index.scss | 52 ++++ miniprogram/src/pages/user/index.tsx | 43 +++- .../src/subpackages/forum/activity/detail.tsx | 26 +- .../src/subpackages/forum/detail/index.tsx | 18 +- miniprogram/src/utils/request.ts | 2 +- 11 files changed, 453 insertions(+), 150 deletions(-) create mode 100644 miniprogram/src/components/MarkdownReader/CodeBlock.tsx create mode 100644 miniprogram/src/components/MarkdownReader/index.scss create mode 100644 miniprogram/src/components/MarkdownReader/index.tsx diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 123d470..924343c 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -20,10 +20,21 @@ class CommissionLogSerializer(serializers.ModelSerializer): } class WeChatUserSerializer(serializers.ModelSerializer): + is_admin = serializers.SerializerMethodField() + has_web_account = serializers.SerializerMethodField() + class Meta: model = WeChatUser - 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'] + fields = ['id', 'openid', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number', 'is_star', 'title', 'skills', 'is_admin', 'has_web_account'] + read_only_fields = ['id', 'openid', 'phone_number', 'is_star', 'title', 'skills', 'is_admin', 'has_web_account'] + + def get_is_admin(self, obj): + # 检查是否关联了系统用户且具有管理员权限 + return bool(obj.user and obj.user.is_staff) + + def get_has_web_account(self, obj): + # 检查是否关联了系统用户(即网页账号) + return obj.user is not None class DistributorSerializer(serializers.ModelSerializer): user_info = WeChatUserSerializer(source='user', read_only=True) diff --git a/miniprogram/src/components/MarkdownReader/CodeBlock.tsx b/miniprogram/src/components/MarkdownReader/CodeBlock.tsx new file mode 100644 index 0000000..3a992b1 --- /dev/null +++ b/miniprogram/src/components/MarkdownReader/CodeBlock.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react' +import { View, Text, ScrollView } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { AtIcon } from 'taro-ui' +import './index.scss' + +interface Props { + code: string + language?: string +} + +const CodeBlock: React.FC = ({ code, language }) => { + const [copied, setCopied] = useState(false) + + const handleCopy = (e) => { + e.stopPropagation() + Taro.setClipboardData({ + data: code, + success: () => { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + }) + } + + return ( + + + {language || 'text'} + + + {copied ? '已复制' : '复制'} + + + + {code} + + + ) +} + +export default CodeBlock diff --git a/miniprogram/src/components/MarkdownReader/index.scss b/miniprogram/src/components/MarkdownReader/index.scss new file mode 100644 index 0000000..5c27351 --- /dev/null +++ b/miniprogram/src/components/MarkdownReader/index.scss @@ -0,0 +1,75 @@ +.markdown-reader { + .markdown-text { + /* Inherit font styles and color from parent */ + font-size: inherit; + line-height: inherit; + color: inherit; + + /* Ensure rich text images are responsive */ + image { + max-width: 100%; + } + } +} + +.markdown-code-block { + margin: 16px 0; + border-radius: 8px; + overflow: hidden; + background-color: #1e1e1e; + border: 1px solid #333; + + .code-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background-color: #252526; + border-bottom: 1px solid #333; + + .language { + color: #9cdcfe; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + } + + .copy-btn { + display: flex; + align-items: center; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.1); + transition: background-color 0.2s; + + &:active { + background-color: rgba(255, 255, 255, 0.2); + } + + .copy-text { + color: #ccc; + font-size: 12px; + margin-left: 4px; + } + } + } + + .code-content { + padding: 12px; + background-color: #1e1e1e; + max-height: 400px; + box-sizing: border-box; + + .code-text { + color: #d4d4d4; + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + line-height: 1.5; + white-space: pre; + display: block; + width: max-content; + min-width: 100%; + } + } +} diff --git a/miniprogram/src/components/MarkdownReader/index.tsx b/miniprogram/src/components/MarkdownReader/index.tsx new file mode 100644 index 0000000..f90c6e6 --- /dev/null +++ b/miniprogram/src/components/MarkdownReader/index.tsx @@ -0,0 +1,90 @@ +import React, { useMemo } from 'react' +import { View, RichText } from '@tarojs/components' +import { marked, Renderer } from 'marked' +import CodeBlock from './CodeBlock' +import './index.scss' + +interface Props { + content: string + themeColor?: string +} + +const MarkdownReader: React.FC = ({ content, themeColor = '#00b96b' }) => { + const elements = useMemo(() => { + if (!content) return [] + + const tokens = marked.lexer(content) + const result: React.ReactNode[] = [] + let currentTokens: any[] = [] + + // Configure renderer + const renderer = new Renderer() + + renderer.table = (header, body) => { + return `
+ + ${header} + ${body} +
+
` + } + + renderer.tablecell = (content, flags) => { + const type = flags.header ? 'th' : 'td' + const style = [ + 'border: 1px solid rgba(255,255,255,0.1)', + 'padding: 10px', + flags.header ? 'background-color: rgba(255,255,255,0.05); font-weight: 700; color: #fff;' : 'color: #ddd;', + flags.align ? `text-align: ${flags.align}` : 'text-align: left' + ].join(';') + return `<${type} style="${style}">${content}` + } + + renderer.image = (href, title, text) => { + return `${text || ''}` + } + + renderer.link = (href, title, text) => { + return `${text}` + } + + // Process tokens + tokens.forEach((token, index) => { + if (token.type === 'code') { + // Flush accumulated tokens + if (currentTokens.length > 0) { + // preserve links if any + (currentTokens as any).links = (tokens as any).links + const html = marked.parser(currentTokens as any, { renderer, breaks: true }) + result.push() + currentTokens = [] + } + + // Add code block + result.push( + + + + ) + } else { + currentTokens.push(token) + } + }) + + // Flush remaining tokens + if (currentTokens.length > 0) { + (currentTokens as any).links = (tokens as any).links + const html = marked.parser(currentTokens as any, { renderer, breaks: true }) + result.push() + } + + return result + }, [content, themeColor]) + + return {elements} +} + +export default MarkdownReader diff --git a/miniprogram/src/pages/forum/index.scss b/miniprogram/src/pages/forum/index.scss index fcfad1e..93c484b 100644 --- a/miniprogram/src/pages/forum/index.scss +++ b/miniprogram/src/pages/forum/index.scss @@ -48,9 +48,9 @@ .title { position: relative; - font-size: 36px; /* Increased from 32px */ + font-size: 42px; /* Increased from 36px */ font-weight: 800; - margin-bottom: 12px; + margin-bottom: 16px; color: #fff; letter-spacing: -0.5px; text-shadow: 0 2px 10px rgba(0,0,0,0.5); @@ -67,8 +67,8 @@ .subtitle { position: relative; color: #aaa; - font-size: 17px; /* Increased from 16px */ - margin-bottom: 30px; + font-size: 19px; /* Increased from 17px */ + margin-bottom: 36px; font-weight: 500; z-index: 1; } @@ -76,8 +76,8 @@ .search-box { position: relative; display: flex; - gap: 12px; - margin-bottom: 24px; + gap: 14px; + margin-bottom: 28px; z-index: 2; .at-search-bar { @@ -90,9 +90,9 @@ .at-search-bar__input-cnt { background-color: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255,255,255,0.1); - border-radius: 26px; /* More rounded */ + border-radius: 30px; /* More rounded */ transition: all 0.3s ease; - height: 48px; /* Taller touch target (from 44px) */ + height: 56px; /* Taller touch target (from 48px) */ display: flex; align-items: center; @@ -105,11 +105,11 @@ .at-search-bar__input { color: #fff; - font-size: 17px; /* Larger input text (from 16px) */ + font-size: 18px; /* Larger input text (from 17px) */ } .at-search-bar__placeholder { - font-size: 16px; + font-size: 17px; } } @@ -117,9 +117,9 @@ background: linear-gradient(135deg, #00b96b 0%, #009456 100%); color: #fff; border: none; - border-radius: 26px; - padding: 0 24px; - font-size: 16px; /* Larger button text */ + border-radius: 30px; + padding: 0 28px; + font-size: 18px; /* Larger button text */ font-weight: 600; display: flex; align-items: center; @@ -135,10 +135,10 @@ } .section-container { - margin: 0 16px 20px; + margin: 0 16px 24px; background: #1e1e1e; /* Card background */ - border-radius: 16px; - padding: 16px; + border-radius: 20px; + padding: 20px; border: 1px solid rgba(255, 255, 255, 0.05); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); animation: fadeInUp 0.6s ease-out forwards; @@ -146,13 +146,13 @@ .section-header { display: flex; align-items: center; - gap: 8px; - margin-bottom: 16px; - padding-bottom: 12px; + gap: 10px; + margin-bottom: 20px; + padding-bottom: 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); .section-title { - font-size: 16px; + font-size: 20px; /* Increased from 16px */ font-weight: 700; color: #fff; letter-spacing: 0.5px; @@ -160,19 +160,19 @@ } .announcement-swiper { - height: 40px; + height: 48px; .announcement-item { - height: 40px; + height: 48px; display: flex; align-items: center; background: rgba(255, 255, 255, 0.03); - padding: 0 12px; - border-radius: 8px; - border-left: 3px solid #ff4d4f; + padding: 0 16px; + border-radius: 10px; + border-left: 4px solid #ff4d4f; .item-text { - font-size: 14px; + font-size: 16px; /* Increased from 14px */ color: #ddd; overflow: hidden; text-overflow: ellipsis; @@ -184,17 +184,17 @@ .star-users-scroll { white-space: nowrap; width: 100%; - padding-bottom: 5px; /* Scrollbar space if visible */ + padding-bottom: 8px; /* Scrollbar space if visible */ .star-user-card { display: inline-flex; flex-direction: column; align-items: center; - margin-right: 16px; - width: 80px; + margin-right: 18px; + width: 90px; background: rgba(255, 255, 255, 0.03); - padding: 12px 8px; - border-radius: 12px; + padding: 16px 10px; + border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.05); transition: transform 0.2s; @@ -203,32 +203,32 @@ } .user-avatar { - width: 48px; - height: 48px; + width: 60px; /* Increased from 48px */ + height: 60px; border-radius: 50%; border: 2px solid #ffd700; - margin-bottom: 8px; + margin-bottom: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); } .user-name { - font-size: 12px; + font-size: 14px; /* Increased from 12px */ font-weight: 600; color: #eee; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-bottom: 2px; + margin-bottom: 4px; } .user-title { - font-size: 10px; + font-size: 12px; /* Increased from 10px */ color: #888; background: rgba(255, 215, 0, 0.1); color: #ffd700; - padding: 2px 6px; - border-radius: 4px; + padding: 3px 8px; + border-radius: 6px; } } } @@ -236,8 +236,8 @@ .tabs-wrapper { background-color: transparent; /* Changed from black */ - margin-bottom: 10px; - padding: 0 10px; + margin-bottom: 16px; + padding: 0 12px; /* Override Taro UI default white background */ .at-tabs { @@ -253,37 +253,37 @@ .at-tabs__item { color: #888; - font-size: 17px; /* Increased from 16px */ - padding: 14px 20px; /* Larger touch target */ + font-size: 18px; /* Increased from 17px */ + padding: 16px 24px; /* Larger touch target */ transition: all 0.3s; &--active { color: #fff; /* White active text */ font-weight: 700; - font-size: 20px; /* Increased from 19px */ + font-size: 22px; /* Increased from 20px */ text-shadow: 0 0 10px rgba(0, 185, 107, 0.4); } } .at-tabs__item-underline { background-color: #00b96b; - height: 4px; /* Slightly thicker */ - border-radius: 2px; - bottom: 5px; - width: 28px !important; /* Short underline style */ - margin-left: calc(50% - 14px); /* Center specific width underline */ + height: 5px; /* Slightly thicker */ + border-radius: 3px; + bottom: 6px; + width: 32px !important; /* Short underline style */ + margin-left: calc(50% - 16px); /* Center specific width underline */ } } .topic-list { - padding: 10px 16px; + padding: 12px 18px; .topic-card { background: #1e1e1e; border: 1px solid rgba(255,255,255,0.05); - border-radius: 16px; - padding: 24px; /* Increased from 20px */ - margin-bottom: 24px; /* Increased spacing */ + border-radius: 20px; + padding: 28px; /* Increased from 24px */ + margin-bottom: 28px; /* Increased spacing */ position: relative; box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: transform 0.2s, box-shadow 0.2s; @@ -311,13 +311,13 @@ display: flex; align-items: center; flex-wrap: wrap; - gap: 10px; - margin-bottom: 14px; + gap: 12px; + margin-bottom: 18px; .tag { - font-size: 12px; /* Slightly larger */ - padding: 4px 10px; - border-radius: 6px; + font-size: 14px; /* Slightly larger */ + padding: 6px 12px; + border-radius: 8px; font-weight: 600; background: rgba(255,255,255,0.1); color: #aaa; @@ -336,19 +336,19 @@ } .card-title { - font-size: 22px; /* Increased from 19px */ + font-size: 26px; /* Increased from 22px */ font-weight: 700; color: #fff; flex: 1; - line-height: 1.4; + line-height: 1.5; } } .card-content { - font-size: 17px; /* Increased from 15px */ + font-size: 19px; /* Increased from 17px */ color: #ccc; /* Slightly brighter for better contrast */ - margin-bottom: 20px; - line-height: 1.7; + margin-bottom: 24px; + line-height: 1.8; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; /* Show 3 lines */ @@ -356,13 +356,13 @@ } .card-image { - margin-bottom: 20px; - border-radius: 12px; + margin-bottom: 24px; + border-radius: 16px; overflow: hidden; image { width: 100%; - max-height: 220px; /* Taller image preview */ + max-height: 240px; /* Taller image preview */ object-fit: cover; display: block; /* Remove inline spacing */ } @@ -372,19 +372,19 @@ display: flex; justify-content: space-between; align-items: center; - font-size: 14px; /* Increased from 13px */ + font-size: 15px; /* Increased from 14px */ color: #888; - padding-top: 16px; + padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.05); .author-info { display: flex; align-items: center; - gap: 10px; + gap: 12px; .avatar { - width: 40px; /* Larger avatar */ - height: 40px; + width: 48px; /* Larger avatar */ + height: 48px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.1); } @@ -402,16 +402,16 @@ .stats { display: flex; - gap: 20px; + gap: 24px; .stat-item { display: flex; align-items: center; - gap: 6px; + gap: 8px; .at-icon { - font-size: 18px; - margin-right: 0; + font-size: 20px; + margin-right: 0; } } } @@ -421,25 +421,25 @@ .empty-state { text-align: center; - padding: 60px 20px; + padding: 80px 20px; color: #666; - font-size: 14px; + font-size: 16px; &::before { content: '📭'; /* Simple icon */ display: block; - font-size: 40px; - margin-bottom: 10px; + font-size: 50px; + margin-bottom: 12px; opacity: 0.5; } } .fab { position: fixed; - right: 24px; - bottom: 50px; - width: 56px; - height: 56px; + right: 30px; + bottom: 60px; + width: 64px; + height: 64px; background: linear-gradient(135deg, #00b96b 0%, #009456 100%); border-radius: 50%; display: flex; @@ -477,11 +477,11 @@ .layout-header { background-color: #15191f; border-bottom: 1px solid rgba(255, 255, 255, 0.05); - padding: 18px 24px; + padding: 20px 28px; .layout-header__title { color: #00b96b; /* Tech green */ - font-size: 18px; + font-size: 20px; /* Increased from 18px */ font-weight: 700; letter-spacing: 1px; text-shadow: 0 0 10px rgba(0, 185, 107, 0.3); @@ -500,7 +500,7 @@ } .expert-modal-content { - padding: 30px 24px 60px; + padding: 36px 28px 70px; color: #fff; background: radial-gradient(circle at 50% 10%, rgba(0, 185, 107, 0.08), transparent 60%); @@ -508,16 +508,16 @@ display: flex; flex-direction: column; align-items: center; - margin-bottom: 36px; + margin-bottom: 40px; position: relative; .avatar-container { position: relative; - margin-bottom: 24px; + margin-bottom: 28px; .expert-avatar { - width: 100px; - height: 100px; + width: 120px; /* Increased from 100px */ + height: 120px; 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); @@ -527,7 +527,7 @@ .avatar-ring { position: absolute; - top: -12px; left: -12px; right: -12px; bottom: -12px; + top: -14px; left: -14px; right: -14px; bottom: -14px; border-radius: 50%; border: 1px dashed rgba(255, 215, 0, 0.5); animation: spin 12s linear infinite; @@ -536,7 +536,7 @@ &::after { content: ''; position: absolute; - top: -6px; left: -6px; right: -6px; bottom: -6px; + top: -8px; left: -8px; right: -8px; bottom: -8px; border-radius: 50%; border: 1px solid rgba(0, 185, 107, 0.3); /* Outer green ring */ animation: spin 8s reverse linear infinite; @@ -548,10 +548,10 @@ text-align: center; .expert-name { - font-size: 26px; + font-size: 30px; /* Increased from 26px */ font-weight: 800; color: #fff; - margin-bottom: 12px; + margin-bottom: 14px; text-shadow: 0 0 15px rgba(0, 185, 107, 0.5); letter-spacing: 0.5px; } @@ -559,10 +559,10 @@ .expert-title-badge { display: inline-flex; align-items: center; - gap: 8px; + gap: 10px; background: linear-gradient(90deg, rgba(255, 215, 0, 0.15), rgba(255, 215, 0, 0.05)); - padding: 6px 16px; - border-radius: 20px; + padding: 8px 20px; + border-radius: 24px; border: 1px solid rgba(255, 215, 0, 0.4); box-shadow: 0 0 15px rgba(255, 215, 0, 0.15); @@ -571,7 +571,7 @@ } text { - font-size: 14px; + font-size: 16px; /* Increased from 14px */ color: #ffd700; font-weight: 700; letter-spacing: 1px; @@ -581,10 +581,10 @@ } .expert-skills-section { - margin-bottom: 30px; + margin-bottom: 36px; background: rgba(255, 255, 255, 0.02); - border-radius: 20px; - padding: 24px; + border-radius: 24px; + padding: 28px; border: 1px solid rgba(255, 255, 255, 0.06); position: relative; overflow: hidden; @@ -594,9 +594,9 @@ content: ''; position: absolute; top: 0; left: 0; - width: 10px; height: 10px; - border-top: 2px solid #00b96b; - border-left: 2px solid #00b96b; + width: 12px; height: 12px; + border-top: 3px solid #00b96b; + border-left: 3px solid #00b96b; border-radius: 4px 0 0 0; } @@ -604,22 +604,22 @@ content: ''; position: absolute; bottom: 0; right: 0; - width: 10px; height: 10px; - border-bottom: 2px solid #00b96b; - border-right: 2px solid #00b96b; + width: 12px; height: 12px; + border-bottom: 3px solid #00b96b; + border-right: 3px solid #00b96b; border-radius: 0 0 4px 0; } .section-label { display: flex; align-items: center; - margin-bottom: 20px; + margin-bottom: 24px; .label-text { - font-size: 15px; + font-size: 17px; /* Increased from 15px */ font-weight: 700; color: #00b96b; - margin-right: 12px; + margin-right: 14px; text-transform: uppercase; letter-spacing: 1px; } @@ -635,14 +635,14 @@ .skills-grid { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 14px; .skill-tag { display: flex; align-items: center; background: rgba(0, 185, 107, 0.08); - padding: 8px 16px; - border-radius: 6px; + padding: 10px 20px; + border-radius: 8px; border: 1px solid rgba(0, 185, 107, 0.25); transition: all 0.3s; position: relative; @@ -652,7 +652,7 @@ &::before { content: ''; position: absolute; - top: 0; left: 0; width: 3px; height: 100%; + top: 0; left: 0; width: 4px; height: 100%; background: #00b96b; opacity: 0.6; } @@ -664,14 +664,14 @@ } .skill-icon { - width: 20px; - height: 20px; - margin-right: 8px; + width: 24px; /* Increased from 20px */ + height: 24px; + margin-right: 10px; filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); } .skill-text { - font-size: 13px; + font-size: 15px; /* Increased from 13px */ color: #e0e0e0; font-weight: 500; letter-spacing: 0.5px; diff --git a/miniprogram/src/pages/user/index.config.ts b/miniprogram/src/pages/user/index.config.ts index d724fda..4cb9920 100644 --- a/miniprogram/src/pages/user/index.config.ts +++ b/miniprogram/src/pages/user/index.config.ts @@ -1,3 +1,5 @@ export default definePageConfig({ - navigationBarTitleText: '个人中心' + navigationBarTitleText: '个人中心', + enablePullDownRefresh: true, + backgroundTextStyle: 'dark' }) diff --git a/miniprogram/src/pages/user/index.scss b/miniprogram/src/pages/user/index.scss index 160db3c..12ee38d 100644 --- a/miniprogram/src/pages/user/index.scss +++ b/miniprogram/src/pages/user/index.scss @@ -80,6 +80,58 @@ margin-bottom: 8px; text-shadow: 0 0 10px rgba(0,0,0,0.5); } + + .badges-row { + display: flex; + gap: 10px; + margin-bottom: 12px; + flex-wrap: wrap; + + .badge { + display: flex; + align-items: center; + padding: 4px 16px; + border-radius: 20px; + font-size: 20px; + font-weight: bold; + backdrop-filter: blur(5px); + + .badge-icon { margin-right: 6px; font-size: 22px; } + + &.star { + background: rgba(255, 215, 0, 0.15); + border: 1px solid rgba(255, 215, 0, 0.6); + color: #ffd700; + box-shadow: 0 0 15px rgba(255, 215, 0, 0.1); + } + + &.admin { + background: rgba(255, 71, 87, 0.15); + border: 1px solid rgba(255, 71, 87, 0.6); + color: #ff4757; + box-shadow: 0 0 15px rgba(255, 71, 87, 0.1); + } + + &.web { + transition: all 0.3s ease; + + &.active { + background: rgba(30, 144, 255, 0.15); + border: 1px solid rgba(30, 144, 255, 0.6); + color: #1e90ff; + box-shadow: 0 0 15px rgba(30, 144, 255, 0.1); + } + + &.disabled { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #666; + filter: grayscale(1); + opacity: 0.7; + } + } + } + } .uid { font-size: 24px; diff --git a/miniprogram/src/pages/user/index.tsx b/miniprogram/src/pages/user/index.tsx index 035a12a..3edca11 100644 --- a/miniprogram/src/pages/user/index.tsx +++ b/miniprogram/src/pages/user/index.tsx @@ -1,6 +1,7 @@ import { View, Text, Image, Button, Checkbox, CheckboxGroup, RichText } from '@tarojs/components' -import Taro, { useDidShow } from '@tarojs/taro' +import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro' import { useState } from 'react' +import { login as silentLogin } from '../../utils/request' import './index.scss' export default function UserIndex() { @@ -14,6 +15,19 @@ export default function UserIndex() { if (info) setUserInfo(info) }) + usePullDownRefresh(async () => { + try { + const res = await silentLogin() + if (res) { + setUserInfo(res) + } + Taro.stopPullDownRefresh() + } catch (e) { + Taro.stopPullDownRefresh() + Taro.showToast({ title: '刷新失败', icon: 'none' }) + } + }) + const goOrders = () => Taro.navigateTo({ url: '/pages/order/list' }) const goDistributor = () => Taro.navigateTo({ url: '/subpackages/distributor/index' }) const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' }) @@ -296,6 +310,33 @@ export default function UserIndex() { {userInfo?.nickname || '未登录用户'} + + {userInfo && ( + + {/* 明星技术用户 */} + {userInfo.is_star && ( + + 🌟 + 技术专家 + + )} + + {/* 管理员 */} + {userInfo.is_admin && ( + + 🛡️ + 管理员 + + )} + + {/* 网页用户 */} + + 🌐 + 网页用户 + + + )} + ID: {userInfo ? (userInfo.phone_number || userInfo.id || '----') : '----'} {!userInfo && ( diff --git a/miniprogram/src/subpackages/forum/activity/detail.tsx b/miniprogram/src/subpackages/forum/activity/detail.tsx index 8f9eed0..b61252b 100644 --- a/miniprogram/src/subpackages/forum/activity/detail.tsx +++ b/miniprogram/src/subpackages/forum/activity/detail.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react' import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro' -import { View, Text, Image, Button, RichText, Picker } from '@tarojs/components' -import { AtIcon, AtProgress, AtModal, AtModalHeader, AtModalContent, AtModalAction, AtInput, AtTextarea, AtRadio, AtCheckbox } from 'taro-ui' +import { View, Text, Image, Button, Picker } from '@tarojs/components' +import { AtIcon, AtModal, AtModalHeader, AtModalContent, AtModalAction, AtInput, AtTextarea, AtRadio, AtCheckbox } from 'taro-ui' import { getActivityDetail, signupActivity } from '../../../api' -import { marked } from 'marked' +import MarkdownReader from '../../../components/MarkdownReader' import './detail.scss' const ActivityDetail = () => { @@ -13,7 +13,6 @@ const ActivityDetail = () => { const [activity, setActivity] = useState(null) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) - const [htmlContent, setHtmlContent] = useState('') const [signupPercentage, setSignupPercentage] = useState(0) // Signup Form State @@ -51,11 +50,6 @@ const ActivityDetail = () => { const percent = Math.min(100, Math.round((data.current_signups || 0) / data.max_participants * 100)) setSignupPercentage(percent) } - - if (data.description) { - const html = marked.parse(data.description) - setHtmlContent((html as string).replace(/ { const handleModalConfirm = () => { // Validate - if (activity.signup_form_config) { + if (activity.signup_form_config && Array.isArray(activity.signup_form_config)) { for (const field of activity.signup_form_config) { + // Defensive programming: skip invalid fields + if (!field || typeof field !== 'object') continue + if (field.required && !formData[field.name]) { Taro.showToast({ title: `请填写${field.label}`, icon: 'none' }) return @@ -233,7 +230,7 @@ const ActivityDetail = () => { - + @@ -270,7 +267,10 @@ const ActivityDetail = () => { 填写报名信息 - {activity.signup_form_config && activity.signup_form_config.map((field, idx) => { + {activity.signup_form_config && Array.isArray(activity.signup_form_config) && activity.signup_form_config.map((field, idx) => { + // Defensive programming: skip invalid fields or known bad data + if (!field || typeof field !== 'object' || field.label === '自定义报名配置') return null + if (field.type === 'select') { const currentOption = field.options?.find(opt => opt.value === formData[field.name]) return ( @@ -375,4 +375,4 @@ const ActivityDetail = () => { ) } -export default ActivityDetail \ No newline at end of file +export default ActivityDetail diff --git a/miniprogram/src/subpackages/forum/detail/index.tsx b/miniprogram/src/subpackages/forum/detail/index.tsx index 9bab2a7..db83f7e 100644 --- a/miniprogram/src/subpackages/forum/detail/index.tsx +++ b/miniprogram/src/subpackages/forum/detail/index.tsx @@ -1,9 +1,9 @@ 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 { View, Text, Image, Video, Input, ScrollView } from '@tarojs/components' import { AtActivityIndicator, AtIcon, AtActionSheet, AtActionSheetItem, AtFloatLayout } from 'taro-ui' import { getTopicDetail, createReply, uploadMedia, getStarUsers } from '../../../api' -import { marked } from 'marked' +import MarkdownReader from '../../../components/MarkdownReader' import './detail.scss' const ForumDetail = () => { @@ -14,7 +14,6 @@ const ForumDetail = () => { const [loading, setLoading] = useState(true) const [replyContent, setReplyContent] = useState('') const [sending, setSending] = useState(false) - const [htmlContent, setHtmlContent] = useState('') const [userInfo, setUserInfo] = useState(null) // Star Users & Mention @@ -30,14 +29,6 @@ const ForumDetail = () => { const res = await getTopicDetail(Number(id)) const topicData = res.data || res setTopic(topicData) - - // Parse markdown - if (topicData.content) { - const html = marked.parse(topicData.content) - // Basic fix for images to fit screen - const styledHtml = (html as string).replace(/ { - + {topic.media && topic.media.length > 0 && ( @@ -244,8 +235,7 @@ const ForumDetail = () => { - {/* Simple markdown render for replies or just text if complex */} - + diff --git a/miniprogram/src/utils/request.ts b/miniprogram/src/utils/request.ts index 134706e..686f8ef 100644 --- a/miniprogram/src/utils/request.ts +++ b/miniprogram/src/utils/request.ts @@ -63,7 +63,7 @@ export const login = async () => { if (res.statusCode === 200 && res.data.token) { Taro.setStorageSync('token', res.data.token) Taro.setStorageSync('openid', res.data.openid) - // Save other info if needed + Taro.setStorageSync('userInfo', res.data) return res.data } else { throw new Error(res.data.error || 'Login failed')