diff --git a/backend/shop/admin.py b/backend/shop/admin.py
index 135afc5..7817c08 100644
--- a/backend/shop/admin.py
+++ b/backend/shop/admin.py
@@ -6,7 +6,7 @@ from django.urls import path, reverse
from django.shortcuts import redirect
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display
-from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment
+from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment, AdminPhoneNumber
import qrcode
from io import BytesIO
import base64
@@ -478,3 +478,9 @@ class WithdrawalAdmin(ModelAdmin):
'fields': ('created_at', 'updated_at')
}),
)
+
+@admin.register(AdminPhoneNumber)
+class AdminPhoneNumberAdmin(ModelAdmin):
+ list_display = ('name', 'phone_number', 'is_active', 'created_at')
+ list_filter = ('is_active',)
+ search_fields = ('name', 'phone_number')
diff --git a/backend/shop/apps.py b/backend/shop/apps.py
index a5c0262..caf0bb9 100644
--- a/backend/shop/apps.py
+++ b/backend/shop/apps.py
@@ -3,3 +3,7 @@ from django.apps import AppConfig
class ShopConfig(AppConfig):
name = 'shop'
+ verbose_name = "商城管理"
+
+ def ready(self):
+ import shop.signals
diff --git a/backend/shop/migrations/0031_adminphonenumber.py b/backend/shop/migrations/0031_adminphonenumber.py
new file mode 100644
index 0000000..d36c7ce
--- /dev/null
+++ b/backend/shop/migrations/0031_adminphonenumber.py
@@ -0,0 +1,27 @@
+# Generated by Django 6.0.1 on 2026-02-16 11:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0030_alter_esp32config_options_alter_service_options_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AdminPhoneNumber',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50, verbose_name='管理员姓名')),
+ ('phone_number', models.CharField(max_length=20, verbose_name='手机号')),
+ ('is_active', models.BooleanField(default=True, verbose_name='是否接收通知')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ],
+ options={
+ 'verbose_name': '管理员通知手机号',
+ 'verbose_name_plural': '管理员通知手机号',
+ },
+ ),
+ ]
diff --git a/backend/shop/models.py b/backend/shop/models.py
index 0c739b8..42346c7 100644
--- a/backend/shop/models.py
+++ b/backend/shop/models.py
@@ -402,3 +402,21 @@ class CourseEnrollment(models.Model):
class Meta:
verbose_name = "课程报名"
verbose_name_plural = "课程报名管理"
+
+
+class AdminPhoneNumber(models.Model):
+ """
+ 管理员通知手机号配置
+ 用于接收订单支付成功等重要通知
+ """
+ name = models.CharField(max_length=50, verbose_name="管理员姓名")
+ phone_number = models.CharField(max_length=20, verbose_name="手机号")
+ is_active = models.BooleanField(default=True, verbose_name="是否接收通知")
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+
+ def __str__(self):
+ return f"{self.name} - {self.phone_number}"
+
+ class Meta:
+ verbose_name = "管理员通知手机号"
+ verbose_name_plural = "管理员通知手机号"
diff --git a/backend/shop/signals.py b/backend/shop/signals.py
new file mode 100644
index 0000000..9236fec
--- /dev/null
+++ b/backend/shop/signals.py
@@ -0,0 +1,54 @@
+from django.db.models.signals import pre_save, post_save
+from django.dispatch import receiver
+from .models import Order
+from .sms_utils import notify_admins_order_paid, notify_user_order_paid, notify_user_order_shipped
+
+@receiver(pre_save, sender=Order)
+def track_order_changes(sender, instance, **kwargs):
+ """
+ 在保存之前检查状态变化
+ """
+ if instance.pk:
+ try:
+ old_instance = Order.objects.get(pk=instance.pk)
+
+ # 检查是否从非支付状态变为支付状态
+ if old_instance.status != 'paid' and instance.status == 'paid':
+ instance._was_paid = True
+
+ # 检查是否发货 (状态变为 shipped 且有单号)
+ # 或者已经是 shipped 状态但刚填入单号
+ if instance.status == 'shipped' and instance.tracking_number:
+ if old_instance.status != 'shipped' or not old_instance.tracking_number:
+ instance._was_shipped = True
+
+ except Order.DoesNotExist:
+ pass
+
+@receiver(post_save, sender=Order)
+def send_order_notifications(sender, instance, created, **kwargs):
+ """
+ 在保存之后发送通知
+ """
+ if created:
+ return
+
+ # 1. 处理支付成功通知
+ if getattr(instance, '_was_paid', False):
+ try:
+ print(f"订单 {instance.id} 支付成功,触发短信通知流程...")
+ notify_admins_order_paid(instance)
+ notify_user_order_paid(instance)
+ # 清除标记防止重复发送 (虽然实例通常是新的,但保险起见)
+ instance._was_paid = False
+ except Exception as e:
+ print(f"发送支付成功短信失败: {str(e)}")
+
+ # 2. 处理发货通知
+ if getattr(instance, '_was_shipped', False):
+ try:
+ print(f"订单 {instance.id} 已发货,触发短信通知流程...")
+ notify_user_order_shipped(instance)
+ instance._was_shipped = False
+ except Exception as e:
+ print(f"发送发货短信失败: {str(e)}")
diff --git a/backend/shop/sms_utils.py b/backend/shop/sms_utils.py
new file mode 100644
index 0000000..80eb105
--- /dev/null
+++ b/backend/shop/sms_utils.py
@@ -0,0 +1,98 @@
+import requests
+import threading
+import json
+from .models import AdminPhoneNumber
+
+# SMS API Configuration
+SMS_API_URL = "https://data.tangledup-ai.com/api/send-sms/diy"
+SIGN_NAME = "叠加态科技云南"
+
+def send_sms(phone_number, template_code, template_params):
+ """
+ 通用发送短信函数 (异步)
+ """
+ def _send():
+ try:
+ payload = {
+ "phone_number": phone_number,
+ "template_code": template_code,
+ "sign_name": SIGN_NAME,
+ "additionalProp1": template_params
+ }
+ headers = {
+ "Content-Type": "application/json",
+ "accept": "application/json"
+ }
+ # print(f"Sending SMS to {phone_number} with params: {template_params}")
+ response = requests.post(SMS_API_URL, json=payload, headers=headers, timeout=15)
+ print(f"SMS Response for {phone_number}: {response.status_code} - {response.text}")
+ except Exception as e:
+ print(f"发送短信异常: {str(e)}")
+
+ threading.Thread(target=_send).start()
+
+def notify_admins_order_paid(order):
+ """
+ 通知管理员有新订单支付成功
+ """
+ # 获取激活的管理员手机号,最多3个
+ admins = AdminPhoneNumber.objects.filter(is_active=True)[:3]
+ if not admins.exists():
+ print("未配置管理员手机号,跳过管理员通知")
+ return
+
+ # 构造参数
+ # 模板变量: consignee, order_id, address
+ # order_id 格式要求: "订单编号/电话号码"
+ params = {
+ "consignee": order.customer_name or "未填写",
+ "order_id": f"{order.id}/{order.phone_number}",
+ "address": order.shipping_address or "无地址"
+ }
+
+ print(f"准备发送管理员通知,共 {admins.count()} 人")
+ for admin in admins:
+ send_sms(admin.phone_number, "SMS_501735480", params)
+
+def notify_user_order_paid(order):
+ """
+ 通知用户下单成功 (支付成功)
+ """
+ if not order.phone_number:
+ return
+
+ # 模板变量: user_nick, address
+ # 尝试获取用户昵称,如果没有则使用收货人姓名
+ user_nick = order.customer_name
+ if order.wechat_user and order.wechat_user.nickname:
+ user_nick = order.wechat_user.nickname
+
+ params = {
+ "user_nick": user_nick or "用户",
+ "address": order.shipping_address or "无地址"
+ }
+
+ print(f"准备发送用户支付成功通知: {order.phone_number}")
+ send_sms(order.phone_number, "SMS_501850529", params)
+
+def notify_user_order_shipped(order):
+ """
+ 通知用户已发货
+ """
+ if not order.phone_number:
+ return
+
+ # 模板变量: user_nick, address, delivery_company, order_id (这里指快递单号)
+ user_nick = order.customer_name
+ if order.wechat_user and order.wechat_user.nickname:
+ user_nick = order.wechat_user.nickname
+
+ params = {
+ "user_nick": user_nick or "用户",
+ "address": order.shipping_address or "无地址",
+ "delivery_company": order.courier_name or "快递",
+ "order_id": order.tracking_number or "暂无单号"
+ }
+
+ print(f"准备发送用户发货通知: {order.phone_number}")
+ send_sms(order.phone_number, "SMS_501650557", params)
diff --git a/deploy_market_page 2.sh b/deploy_market_page 2.sh
new file mode 100644
index 0000000..73fc6d0
--- /dev/null
+++ b/deploy_market_page 2.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# 定义关键变量,方便后续维护修改
+TARGET_DIR="~/data/dev/market_page"
+SUDO_PASSWORD="123quant-speed"
+
+# 脚本执行出错时立即退出
+set -e
+
+# 1. 切换到目标目录(先解析 ~ 为实际家目录)
+echo "===== 切换到目标目录: $TARGET_DIR ====="
+RESOLVED_DIR=$(eval echo $TARGET_DIR)
+cd $RESOLVED_DIR || {
+ echo "错误:目录 $RESOLVED_DIR 不存在!"
+ exit 1
+}
+
+# 2. 停止并移除 Docker 容器(自动输入 sudo 密码)
+echo -e "\n===== 停止 Docker 容器 ====="
+echo $SUDO_PASSWORD | sudo -S docker compose down
+
+# 3. 删除 Docker 镜像(说明:这里默认删除 compose 关联的镜像,也可指定镜像名)
+echo -e "\n===== 删除 Docker 镜像 ====="
+# 方式1:删除 compose.yml 中定义的所有镜像(推荐)
+echo $SUDO_PASSWORD | sudo -S docker compose down --rmi all
+# 方式2:如果你想删除指定镜像,替换上面这行(示例,需修改为你的镜像名)
+# echo $SUDO_PASSWORD | sudo -S docker rmi -f your-image-name:tag
+
+# 4. 拉取 Git 最新代码
+echo -e "\n===== 拉取 Git 代码 ====="
+git pull || {
+ echo "警告:Git pull 失败(可能是本地有未提交的修改),脚本继续执行..."
+}
+
+# 5. 重新启动 Docker 容器(后台运行)
+echo -e "\n===== 启动 Docker 容器 ====="
+echo $SUDO_PASSWORD | sudo -S docker compose up -d
+
+echo -e "\n===== 操作完成!====="
\ No newline at end of file
diff --git a/frontend/public/big_logo.png b/frontend/public/big_logo.png
deleted file mode 100644
index 2242f36..0000000
Binary files a/frontend/public/big_logo.png and /dev/null differ
diff --git a/frontend/public/gXEu5E01.svg b/frontend/public/gXEu5E01.svg
deleted file mode 100644
index 6768690..0000000
--- a/frontend/public/gXEu5E01.svg
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
-Created by potrace 1.10, written by Peter Selinger 2001-2011
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/public/liangji_black.png b/frontend/public/liangji_black.png
deleted file mode 100644
index c2396e0..0000000
Binary files a/frontend/public/liangji_black.png and /dev/null differ
diff --git a/frontend/public/liangji_logo.svg b/frontend/public/liangji_logo.svg
deleted file mode 100644
index 25f8a5d..0000000
--- a/frontend/public/liangji_logo.svg
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- QUANT SPEED
-
-
\ No newline at end of file
diff --git a/frontend/public/liangji_white.png b/frontend/public/liangji_white.png
deleted file mode 100644
index ddf0cd6..0000000
Binary files a/frontend/public/liangji_white.png and /dev/null differ
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/frontend/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/App.css b/frontend/src/App.css
deleted file mode 100644
index 7ca6634..0000000
--- a/frontend/src/App.css
+++ /dev/null
@@ -1,41 +0,0 @@
-#root {
- width: 100%;
- margin: 0;
- padding: 0;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
deleted file mode 100644
index 4714660..0000000
--- a/frontend/src/App.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-
-import React from 'react'
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { AuthProvider } from './context/AuthContext';
-import Layout from './components/Layout';
-import Home from './pages/Home';
-import ProductDetail from './pages/ProductDetail';
-import Payment from './pages/Payment';
-import AIServices from './pages/AIServices';
-import ServiceDetail from './pages/ServiceDetail';
-import VCCourses from './pages/VCCourses';
-import VCCourseDetail from './pages/VCCourseDetail';
-import MyOrders from './pages/MyOrders';
-import ForumList from './pages/ForumList';
-import ForumDetail from './pages/ForumDetail';
-import ActivityDetail from './pages/activity/Detail';
-import 'antd/dist/reset.css';
-import './App.css';
-
-const queryClient = new QueryClient();
-
-function App() {
- return (
-
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
-
-
- )
-}
-
-export default App
diff --git a/frontend/src/animation.js b/frontend/src/animation.js
deleted file mode 100644
index d083bf1..0000000
--- a/frontend/src/animation.js
+++ /dev/null
@@ -1,53 +0,0 @@
-
-// Framer Motion Animation Variants
-
-export const fadeInUp = {
- hidden: { opacity: 0, y: 30 },
- visible: (custom = 0) => ({
- opacity: 1,
- y: 0,
- transition: {
- delay: custom * 0.08,
- duration: 0.6,
- ease: [0.22, 1, 0.36, 1], // Custom easing
- },
- }),
-};
-
-export const staggerContainer = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1,
- },
- },
-};
-
-export const hoverScale = {
- hover: {
- scale: 1.03,
- boxShadow: "0px 10px 20px rgba(0, 0, 0, 0.2)",
- transition: { duration: 0.3 },
- },
-};
-
-export const pageTransition = {
- initial: { opacity: 0, x: 20 },
- animate: { opacity: 1, x: 0 },
- exit: { opacity: 0, x: -20 },
- transition: { duration: 0.3 },
-};
-
-export const buttonTap = {
- scale: 0.95,
-};
-
-export const imageFadeIn = {
- hidden: { opacity: 0, scale: 1.1 },
- visible: {
- opacity: 1,
- scale: 1,
- transition: { duration: 0.5 }
- },
-};
diff --git a/frontend/src/api.js b/frontend/src/api.js
deleted file mode 100644
index 5d074ac..0000000
--- a/frontend/src/api.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import axios from 'axios';
-
-const api = axios.create({
- baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
- timeout: 8000, // 增加超时时间到 10秒
- headers: {
- 'Content-Type': 'application/json',
- }
-});
-
-// 请求拦截器:自动附加 Token
-api.interceptors.request.use((config) => {
- const token = localStorage.getItem('token');
- if (token) {
- config.headers.Authorization = `Bearer ${token}`;
- }
- return config;
-}, (error) => {
- return Promise.reject(error);
-});
-
-export const getConfigs = () => api.get('/configs/');
-export const createOrder = (data) => api.post('/orders/', data);
-export const nativePay = (data) => api.post('/pay/', data);
-export const getOrder = (id) => api.get(`/orders/${id}/`);
-export const queryOrderStatus = (id) => api.get(`/orders/${id}/query_status/`);
-export const initiatePayment = (orderId) => api.post(`/orders/${orderId}/initiate_payment/`);
-export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_payment/`);
-
-export const getServices = () => api.get('/services/');
-export const getServiceDetail = (id) => api.get(`/services/${id}/`);
-export const createServiceOrder = (data) => api.post('/service-orders/', data);
-export const getVCCourses = () => api.get('/courses/');
-export const getVCCourseDetail = (id) => api.get(`/courses/${id}/`);
-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 = () => api.get('/users/me/');
-export const updateUserInfo = (data) => api.post('/wechat/update/', data);
-export const uploadUserAvatar = (data) => {
- // 使用 axios 直接请求外部接口,避免 base URL 和拦截器干扰
- return axios.post('https://data.tangledup-ai.com/upload?folder=uploads/market/avator', data, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- }
- });
-};
-
-// Community / Forum API
-export const getTopics = (params) => api.get('/community/topics/', { params });
-export const getTopicDetail = (id) => api.get(`/community/topics/${id}/`);
-export const createTopic = (data) => api.post('/community/topics/', data);
-export const updateTopic = (id, data) => api.patch(`/community/topics/${id}/`, data);
-export const getReplies = (params) => api.get('/community/replies/', { params });
-export const createReply = (data) => api.post('/community/replies/', data);
-export const uploadMedia = (data) => {
- return api.post('/community/media/', data, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- }
- });
-};
-export const getStarUsers = () => api.get('/users/stars/');
-export const getMyPaidItems = () => api.get('/users/paid-items/');
-export const getAnnouncements = () => api.get('/community/announcements/');
-export const getActivities = () => api.get('/community/activities/');
-export const getActivityDetail = (id) => api.get(`/community/activities/${id}/`);
-export const signUpActivity = (id, data) => api.post(`/community/activities/${id}/signup/`, data);
-export const getMySignups = () => api.get('/community/activities/my_signups/');
-
-export default api;
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/frontend/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/components/CreateTopicModal.jsx b/frontend/src/components/CreateTopicModal.jsx
deleted file mode 100644
index 6ffd63f..0000000
--- a/frontend/src/components/CreateTopicModal.jsx
+++ /dev/null
@@ -1,281 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Modal, Form, Input, Button, message, Upload, Select } from 'antd';
-import { UploadOutlined } from '@ant-design/icons';
-import { createTopic, updateTopic, uploadMedia, getMyPaidItems } from '../api';
-import MDEditor from '@uiw/react-md-editor';
-import rehypeKatex from 'rehype-katex';
-import remarkMath from 'remark-math';
-import 'katex/dist/katex.css';
-
-const { Option } = Select;
-
-const CreateTopicModal = ({ visible, onClose, onSuccess, initialValues, isEditMode, topicId }) => {
- const [form] = Form.useForm();
- const [loading, setLoading] = useState(false);
- const [paidItems, setPaidItems] = useState({ configs: [], courses: [], services: [] });
- const [uploading, setUploading] = useState(false);
- const [mediaIds, setMediaIds] = useState([]);
- // eslint-disable-next-line no-unused-vars
- const [mediaList, setMediaList] = useState([]); // Store uploaded media details for preview
- const [content, setContent] = useState("");
-
- useEffect(() => {
- if (visible) {
- fetchPaidItems();
- if (isEditMode && initialValues) {
- // Edit Mode: Populate form with initial values
- form.setFieldsValue({
- title: initialValues.title,
- category: initialValues.category,
- });
- setContent(initialValues.content);
- form.setFieldValue('content', initialValues.content);
-
- // Handle related item
- let relatedVal = null;
- if (initialValues.related_product) relatedVal = `config_${initialValues.related_product.id || initialValues.related_product}`;
- else if (initialValues.related_course) relatedVal = `course_${initialValues.related_course.id || initialValues.related_course}`;
- else if (initialValues.related_service) relatedVal = `service_${initialValues.related_service.id || initialValues.related_service}`;
-
- if (relatedVal) form.setFieldValue('related_item', relatedVal);
-
- // Note: We start with empty *new* media IDs.
- // Existing media is embedded in content or stored in DB, we don't need to re-upload or track them here unless we want to delete them (which is complex).
- // For now, we just allow adding NEW media.
- setMediaIds([]);
- setMediaList([]);
- } else {
- // Create Mode: Reset form
- setMediaIds([]);
- setMediaList([]);
- setContent("");
- form.resetFields();
- form.setFieldsValue({ content: "", category: 'discussion' });
- }
- }
- }, [visible, isEditMode, initialValues, form]);
-
- const fetchPaidItems = async () => {
- try {
- const res = await getMyPaidItems();
- setPaidItems(res.data);
- } catch (error) {
- console.error("Failed to fetch paid items", error);
- }
- };
-
- const handleUpload = async (file) => {
- const formData = new FormData();
- formData.append('file', file);
- // 默认为 image,如果需要支持视频需根据 file.type 判断
- formData.append('media_type', file.type.startsWith('video') ? 'video' : 'image');
-
- setUploading(true);
- try {
- const res = await uploadMedia(formData);
- // 记录上传的媒体 ID
- if (res.data.id) {
- setMediaIds(prev => [...prev, res.data.id]);
- }
-
- // 确保 URL 是完整的
- // 由于后端现在是转发到外部OSS,返回的URL通常是完整的,但也可能是相对的,这里统一处理
- let url = res.data.file;
-
- // 处理反斜杠问题(防止 Windows 路径风格影响 URL)
- if (url) {
- url = url.replace(/\\/g, '/');
- }
-
- if (url && !url.startsWith('http')) {
- // 如果返回的是相对路径,拼接 API URL 或 Base URL
- const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
- // 移除 baseURL 末尾的 /api 或 /
- const host = baseURL.replace(/\/api\/?$/, '');
- // 确保 url 以 / 开头
- if (!url.startsWith('/')) url = '/' + url;
- url = `${host}${url}`;
- }
-
- // 清理 URL 中的双斜杠 (除协议头外)
- url = url.replace(/([^:]\/)\/+/g, '$1');
-
- // Add to media list for preview
- setMediaList(prev => [...prev, {
- id: res.data.id,
- url: url,
- type: file.type.startsWith('video') ? 'video' : 'image',
- name: file.name
- }]);
-
- // 插入到编辑器
- const insertText = file.type.startsWith('video')
- ? `\n \n`
- : `\n\n`;
-
- const newContent = content + insertText;
- setContent(newContent);
- form.setFieldsValue({ content: newContent });
-
- message.success('上传成功');
- } catch (error) {
- console.error(error);
- message.error('上传失败');
- } finally {
- setUploading(false);
- }
- return false; // 阻止默认上传行为
- };
-
- const handleSubmit = async (values) => {
- setLoading(true);
- try {
- // 处理关联项目 ID (select value format: "type_id")
- const relatedValue = values.related_item;
- // Use content state instead of form value to ensure consistency
- const payload = { ...values, content: content, media_ids: mediaIds };
- delete payload.related_item;
-
- if (relatedValue) {
- const [type, id] = relatedValue.split('_');
- if (type === 'config') payload.related_product = id;
- if (type === 'course') payload.related_course = id;
- if (type === 'service') payload.related_service = id;
- } else {
- // If cleared, set to null
- payload.related_product = null;
- payload.related_course = null;
- payload.related_service = null;
- }
-
- if (isEditMode && topicId) {
- await updateTopic(topicId, payload);
- message.success('修改成功');
- } else {
- await createTopic(payload);
- message.success('发布成功');
- }
-
- form.resetFields();
- if (onSuccess) onSuccess();
- onClose();
- } catch (error) {
- console.error(error);
- message.error((isEditMode ? '修改' : '发布') + '失败: ' + (error.response?.data?.detail || '网络错误'));
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
- 技术讨论
- 求助问答
- 经验分享
-
-
-
-
-
-
- {paidItems.configs.map(i => (
- {i.name}
- ))}
-
-
- {paidItems.courses.map(i => (
- {i.title}
- ))}
-
-
- {paidItems.services.map(i => (
- {i.title}
- ))}
-
-
-
-
-
-
-
-
-
- } loading={uploading} size="small">
- 插入图片/视频
-
-
-
-
-
{
- setContent(val);
- form.setFieldsValue({ content: val });
- }}
- height={400}
- previewOptions={{
- rehypePlugins: [[rehypeKatex]],
- remarkPlugins: [[remarkMath]],
- }}
- />
-
-
-
-
-
- 取消
-
- {isEditMode ? "保存修改" : "立即发布"}
-
-
-
-
-
- );
-};
-
-export default CreateTopicModal;
\ No newline at end of file
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
deleted file mode 100644
index 97b75dd..0000000
--- a/frontend/src/components/Layout.jsx
+++ /dev/null
@@ -1,278 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button, Avatar, Dropdown } from 'antd';
-import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined, UserOutlined, LogoutOutlined, WechatOutlined, TeamOutlined } from '@ant-design/icons';
-import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
-import ParticleBackground from './ParticleBackground';
-import LoginModal from './LoginModal';
-import ProfileModal from './ProfileModal';
-import { motion, AnimatePresence } from 'framer-motion';
-import { useAuth } from '../context/AuthContext';
-
-const { Header, Content, Footer } = AntLayout;
-
-const Layout = ({ children }) => {
- const navigate = useNavigate();
- const location = useLocation();
- const [searchParams] = useSearchParams();
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
- const [loginVisible, setLoginVisible] = useState(false);
- const [profileVisible, setProfileVisible] = useState(false);
-
- const { user, login, logout } = useAuth();
-
- // 全局监听并持久化 ref 参数
- useEffect(() => {
- const ref = searchParams.get('ref');
- if (ref) {
- console.log('[Layout] Capturing sales ref code:', ref);
- localStorage.setItem('ref_code', ref);
- }
- }, [searchParams]);
-
- const handleLogout = () => {
- logout();
- navigate('/');
- };
-
- const userMenu = {
- items: [
- {
- key: 'profile',
- label: '个人设置',
- icon: ,
- onClick: () => setProfileVisible(true)
- },
- {
- key: 'logout',
- label: '退出登录',
- icon: ,
- onClick: handleLogout
- }
- ]
- };
-
- const items = [
- {
- key: '/',
- icon: ,
- label: 'AI 硬件',
- },
- {
- key: '/forum',
- icon: ,
- label: '技术论坛',
- },
- {
- key: '/services',
- icon: ,
- label: 'AI 服务',
- },
- {
- key: '/courses',
- icon: ,
- label: 'VC 课程',
- },
- {
- key: '/my-orders',
- icon: ,
- label: '我的订单',
- },
- ];
-
- const handleMenuClick = (key) => {
- navigate(key);
- setMobileMenuOpen(false);
- };
-
- return (
-
-
-
-
-
- {/* Mobile Drawer Menu */}
- 导航菜单}
- placement="right"
- onClose={() => setMobileMenuOpen(false)}
- open={mobileMenuOpen}
- styles={{ body: { padding: 0, background: '#111' }, header: { background: '#111', borderBottom: '1px solid #333' }, wrapper: { width: 250 } }}
- >
-
- {user ? (
-
-
}
- size="large"
- style={{ marginBottom: 10, cursor: 'pointer' }}
- onClick={() => { setProfileVisible(true); setMobileMenuOpen(false); }}
- />
-
{ setProfileVisible(true); setMobileMenuOpen(false); }} style={{ cursor: 'pointer' }}>
- {user.nickname}
-
-
退出登录
-
- ) : (
-
{ setLoginVisible(true); setMobileMenuOpen(false); }}>登录 / 注册
- )}
-
- handleMenuClick(e.key)}
- style={{ background: 'transparent', borderRight: 'none' }}
- />
-
-
- setLoginVisible(false)}
- onLoginSuccess={(userData) => login(userData)}
- />
-
- setProfileVisible(false)}
- />
-
-
-
-
-
-
- Quant Speed AI Hardware ©{new Date().getFullYear()} Created by Quant Speed Tech
-
-
-
- );
-};
-
-export default Layout;
diff --git a/frontend/src/components/LoginModal.jsx b/frontend/src/components/LoginModal.jsx
deleted file mode 100644
index 057f99c..0000000
--- a/frontend/src/components/LoginModal.jsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { useState } from 'react';
-import { Modal, Form, Input, Button, message } from 'antd';
-import { UserOutlined, LockOutlined, MobileOutlined } from '@ant-design/icons';
-import { sendSms, phoneLogin } from '../api';
-
-const LoginModal = ({ visible, onClose, onLoginSuccess }) => {
- const [form] = Form.useForm();
- const [loading, setLoading] = useState(false);
- const [countdown, setCountdown] = useState(0);
-
- const handleSendCode = async () => {
- try {
- const phone = form.getFieldValue('phone_number');
- if (!phone) {
- message.error('请输入手机号');
- return;
- }
-
- // 简单的手机号校验
- if (!/^1[3-9]\d{9}$/.test(phone)) {
- message.error('请输入有效的手机号');
- return;
- }
-//
- await sendSms({ phone_number: phone });
- message.success('验证码已发送');
-
- setCountdown(60);
- const timer = setInterval(() => {
- setCountdown((prev) => {
- if (prev <= 1) {
- clearInterval(timer);
- return 0;
- }
- return prev - 1;
- });
- }, 1000);
-
- } catch (error) {
- console.error(error);
- message.error('发送失败: ' + (error.response?.data?.error || '网络错误'));
- }
- };
-
- const handleSubmit = async (values) => {
- setLoading(true);
- try {
- const res = await phoneLogin(values);
-
- message.success('登录成功');
- onLoginSuccess(res.data);
- onClose();
- } catch (error) {
- console.error(error);
- message.error('登录失败: ' + (error.response?.data?.error || '网络错误'));
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
- }
- placeholder="手机号码"
- size="large"
- />
-
-
-
-
- }
- placeholder="验证码"
- size="large"
- />
- 0}
- >
- {countdown > 0 ? `${countdown}s` : '获取验证码'}
-
-
-
-
-
-
- 登录
-
-
-
-
- 未注册的手机号验证后将自动创建账号
- 已在小程序绑定的手机号将自动同步身份
-
-
-
- );
-};
-
-export default LoginModal;
diff --git a/frontend/src/components/ModelViewer.jsx b/frontend/src/components/ModelViewer.jsx
deleted file mode 100644
index 969ae84..0000000
--- a/frontend/src/components/ModelViewer.jsx
+++ /dev/null
@@ -1,218 +0,0 @@
-import React, { Suspense, useState, useEffect } from 'react';
-import { Canvas, useLoader } from '@react-three/fiber';
-import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
-import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
-import { OrbitControls, Stage, useProgress, Environment, ContactShadows } from '@react-three/drei';
-import { Spin } from 'antd';
-import JSZip from 'jszip';
-import * as THREE from 'three';
-
-class ErrorBoundary extends React.Component {
- constructor(props) {
- super(props);
- this.state = { hasError: false };
- }
-
- static getDerivedStateFromError(error) {
- return { hasError: true };
- }
-
- componentDidCatch(error, errorInfo) {
- console.error("3D Model Viewer Error:", error, errorInfo);
- }
-
- render() {
- if (this.state.hasError) {
- return (
-
- 3D 模型加载失败
-
- );
- }
-
- return this.props.children;
- }
-}
-
-const Model = ({ objPath, mtlPath, scale = 1 }) => {
- // If mtlPath is provided, load materials first
- const materials = mtlPath ? useLoader(MTLLoader, mtlPath) : null;
-
- const obj = useLoader(OBJLoader, objPath, (loader) => {
- if (materials) {
- materials.preload();
- loader.setMaterials(materials);
- }
- });
-
- const clone = obj.clone();
- return ;
-};
-
-const LoadingOverlay = () => {
- const { progress, active } = useProgress();
- if (!active) return null;
-
- return (
-
-
-
-
- {progress.toFixed(0)}% Loading
-
-
-
- );
-};
-
-const ModelViewer = ({ objPath, mtlPath, scale = 1, autoRotate = true }) => {
- const [paths, setPaths] = useState(null);
- const [unzipping, setUnzipping] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- let isMounted = true;
- const blobUrls = [];
-
- const loadPaths = async () => {
- if (!objPath) return;
-
- // 如果是 zip 文件
- if (objPath.toLowerCase().endsWith('.zip')) {
- setUnzipping(true);
- setError(null);
- try {
- const response = await fetch(objPath);
- const arrayBuffer = await response.arrayBuffer();
- const zip = await JSZip.loadAsync(arrayBuffer);
-
- let extractedObj = null;
- let extractedMtl = null;
- const fileMap = {};
-
- // 1. 提取所有文件并创建 Blob URL 映射
- for (const [filename, file] of Object.entries(zip.files)) {
- if (file.dir) continue;
-
- const content = await file.async('blob');
- const url = URL.createObjectURL(content);
- blobUrls.push(url);
-
- // 记录文件名到 URL 的映射,用于后续材质引用图片等情况
- const baseName = filename.split('/').pop();
- fileMap[baseName] = url;
-
- if (filename.toLowerCase().endsWith('.obj')) {
- extractedObj = url;
- } else if (filename.toLowerCase().endsWith('.mtl')) {
- extractedMtl = url;
- }
- }
-
- if (isMounted) {
- if (extractedObj) {
- setPaths({ obj: extractedObj, mtl: extractedMtl });
- } else {
- setError('压缩包内未找到 .obj 模型文件');
- }
- }
- } catch (err) {
- console.error('Error unzipping model:', err);
- if (isMounted) setError('加载压缩包失败');
- } finally {
- if (isMounted) setUnzipping(false);
- }
- } else {
- // 普通路径
- setPaths({ obj: objPath, mtl: mtlPath });
- }
- };
-
- loadPaths();
-
- return () => {
- isMounted = false;
- // 清理 Blob URL 释放内存
- blobUrls.forEach(url => URL.revokeObjectURL(url));
- };
- }, [objPath, mtlPath]);
-
- if (unzipping) {
- return (
-
- );
- }
-
- if (error) {
- return (
-
- {error}
-
- );
- }
-
- if (!paths) return null;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default ModelViewer;
diff --git a/frontend/src/components/ParticleBackground.jsx b/frontend/src/components/ParticleBackground.jsx
deleted file mode 100644
index 6bb17a1..0000000
--- a/frontend/src/components/ParticleBackground.jsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import React, { useEffect, useRef } from 'react';
-
-const ParticleBackground = () => {
- const canvasRef = useRef(null);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- const ctx = canvas.getContext('2d');
- let animationFrameId;
-
- const resizeCanvas = () => {
- canvas.width = window.innerWidth;
- canvas.height = window.innerHeight;
- };
-
- window.addEventListener('resize', resizeCanvas);
- resizeCanvas();
-
- const particles = [];
- const particleCount = 100;
- const meteors = [];
- const meteorCount = 8;
-
- class Particle {
- constructor() {
- this.x = Math.random() * canvas.width;
- this.y = Math.random() * canvas.height;
- this.vx = (Math.random() - 0.5) * 0.5;
- this.vy = (Math.random() - 0.5) * 0.5;
- this.size = Math.random() * 2;
- this.color = Math.random() > 0.5 ? 'rgba(0, 185, 107, ' : 'rgba(0, 240, 255, '; // Green or Blue
- }
-
- update() {
- this.x += this.vx;
- this.y += this.vy;
-
- if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
- if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
- }
-
- draw() {
- ctx.beginPath();
- ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
- ctx.fillStyle = this.color + Math.random() * 0.5 + ')';
- ctx.fill();
- }
- }
-
- class Meteor {
- constructor() {
- this.reset();
- }
-
- reset() {
- this.x = Math.random() * canvas.width * 1.5; // Start further right
- this.y = Math.random() * -canvas.height; // Start further above
- this.vx = -(Math.random() * 5 + 5); // Faster
- this.vy = Math.random() * 5 + 5; // Faster
- this.len = Math.random() * 150 + 150; // Longer trail
- this.color = Math.random() > 0.5 ? 'rgba(0, 185, 107, ' : 'rgba(0, 240, 255, ';
- this.opacity = 0;
- this.maxOpacity = Math.random() * 0.5 + 0.2;
- this.wait = Math.random() * 300; // Random delay before showing up
- }
-
- update() {
- if (this.wait > 0) {
- this.wait--;
- return;
- }
-
- this.x += this.vx;
- this.y += this.vy;
-
- if (this.opacity < this.maxOpacity) {
- this.opacity += 0.02;
- }
-
- if (this.x < -this.len || this.y > canvas.height + this.len) {
- this.reset();
- }
- }
-
- draw() {
- if (this.wait > 0) return;
-
- const tailX = this.x - this.vx * (this.len / 15);
- const tailY = this.y - this.vy * (this.len / 15);
-
- const gradient = ctx.createLinearGradient(this.x, this.y, tailX, tailY);
- gradient.addColorStop(0, this.color + this.opacity + ')');
- gradient.addColorStop(0.1, this.color + (this.opacity * 0.5) + ')');
- gradient.addColorStop(1, this.color + '0)');
-
- ctx.save();
-
- // Add glow effect
- ctx.shadowBlur = 8;
- ctx.shadowColor = this.color.replace('rgba', 'rgb').replace(', ', ')');
-
- ctx.beginPath();
- ctx.strokeStyle = gradient;
- ctx.lineWidth = 2;
- ctx.lineCap = 'round';
- ctx.moveTo(this.x, this.y);
- ctx.lineTo(tailX, tailY);
- ctx.stroke();
-
- // Add a bright head
- ctx.beginPath();
- ctx.fillStyle = '#fff';
- ctx.arc(this.x, this.y, 1, 0, Math.PI * 2);
- ctx.fill();
-
- ctx.restore();
- }
- }
-
- for (let i = 0; i < particleCount; i++) {
- particles.push(new Particle());
- }
-
- for (let i = 0; i < meteorCount; i++) {
- meteors.push(new Meteor());
- }
-
- const animate = () => {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- // Draw meteors first (in background)
- meteors.forEach(m => {
- m.update();
- m.draw();
- });
-
- // Draw connecting lines
- ctx.lineWidth = 0.5;
- for (let i = 0; i < particleCount; i++) {
- for (let j = i; j < particleCount; j++) {
- const dx = particles[i].x - particles[j].x;
- const dy = particles[i].y - particles[j].y;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- if (distance < 100) {
- ctx.beginPath();
- ctx.strokeStyle = `rgba(100, 255, 218, ${1 - distance / 100})`;
- ctx.moveTo(particles[i].x, particles[i].y);
- ctx.lineTo(particles[j].x, particles[j].y);
- ctx.stroke();
- }
- }
- }
-
- particles.forEach(p => {
- p.update();
- p.draw();
- });
-
- animationFrameId = requestAnimationFrame(animate);
- };
-
- animate();
-
- return () => {
- window.removeEventListener('resize', resizeCanvas);
- cancelAnimationFrame(animationFrameId);
- };
- }, []);
-
- return ;
-};
-
-export default ParticleBackground;
diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx
deleted file mode 100644
index 24f730d..0000000
--- a/frontend/src/components/ProfileModal.jsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Modal, Form, Input, Upload, Button, message, Avatar } from 'antd';
-import { UserOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
-import { useAuth } from '../context/AuthContext';
-import { updateUserInfo, uploadUserAvatar } from '../api';
-
-const ProfileModal = ({ visible, onClose }) => {
- const { user, updateUser } = useAuth();
- const [form] = Form.useForm();
- const [loading, setLoading] = useState(false);
- const [uploading, setUploading] = useState(false);
- const [avatarUrl, setAvatarUrl] = useState('');
-
- useEffect(() => {
- if (visible && user) {
- form.setFieldsValue({
- nickname: user.nickname,
- });
- setAvatarUrl(user.avatar_url);
- }
- }, [visible, user, form]);
-
- const handleUpload = async (file) => {
- const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
- if (!isJpgOrPng) {
- message.error('You can only upload JPG/PNG file!');
- return Upload.LIST_IGNORE;
- }
- const isLt2M = file.size / 1024 / 1024 < 2;
- if (!isLt2M) {
- message.error('Image must smaller than 2MB!');
- return Upload.LIST_IGNORE;
- }
-
- const formData = new FormData();
- formData.append('file', file);
-
- setUploading(true);
- try {
- const res = await uploadUserAvatar(formData);
- if (res.data.success) {
- setAvatarUrl(res.data.file_url);
- message.success('头像上传成功');
- } else {
- message.error('头像上传失败: ' + (res.data.message || '未知错误'));
- }
- } catch (error) {
- console.error('Upload failed:', error);
- message.error('头像上传失败');
- } finally {
- setUploading(false);
- }
- return false; // Prevent default auto upload
- };
-
- const handleOk = async () => {
- try {
- const values = await form.validateFields();
- setLoading(true);
-
- const updateData = {
- nickname: values.nickname,
- avatar_url: avatarUrl
- };
-
- const res = await updateUserInfo(updateData);
- updateUser(res.data);
- message.success('个人信息更新成功');
- onClose();
- } catch (error) {
- console.error('Update failed:', error);
- message.error('更新失败');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
}
- />
-
- : } loading={uploading}>
- {uploading ? '上传中...' : '更换头像'}
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default ProfileModal;
diff --git a/frontend/src/components/activity/ActivityCard.jsx b/frontend/src/components/activity/ActivityCard.jsx
deleted file mode 100644
index 8d9bb15..0000000
--- a/frontend/src/components/activity/ActivityCard.jsx
+++ /dev/null
@@ -1,101 +0,0 @@
-
-import React, { useState, useRef, useLayoutEffect } from 'react';
-import { motion } from 'framer-motion';
-import { CalendarOutlined } from '@ant-design/icons';
-import { useNavigate } from 'react-router-dom';
-import styles from './activity.module.less';
-import { hoverScale } from '../../animation';
-
-const ActivityCard = ({ activity }) => {
- const navigate = useNavigate();
- const [isLoaded, setIsLoaded] = useState(false);
- const [hasError, setHasError] = useState(false);
- const imgRef = useRef(null);
-
- const handleCardClick = () => {
- navigate(`/activity/${activity.id}`);
- };
-
- const getStatus = (startTime) => {
- const now = new Date();
- const start = new Date(startTime);
- if (now < start) return '即将开始';
- return '报名中';
- };
-
- const formatDate = (dateStr) => {
- if (!dateStr) return 'TBD';
- const date = new Date(dateStr);
- return date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
- };
-
- const imgSrc = hasError
- ? 'https://via.placeholder.com/600x400?text=No+Image'
- : (activity.display_banner_url || activity.banner_url || activity.cover_image || 'https://via.placeholder.com/600x400');
-
- // Check if image is already loaded (cached) to prevent flashing
- useLayoutEffect(() => {
- if (imgRef.current && imgRef.current.complete) {
- setIsLoaded(true);
- }
- }, [imgSrc]);
-
- return (
-
-
- {/* Placeholder Background - Always visible behind the image */}
-
-
-
setIsLoaded(true)}
- onError={() => {
- setHasError(true);
- setIsLoaded(true);
- }}
- loading="lazy"
- />
-
-
- {activity.status || getStatus(activity.start_time)}
-
-
{activity.title}
-
-
- {formatDate(activity.start_time)}
-
-
-
-
- );
-};
-
-export default ActivityCard;
diff --git a/frontend/src/components/activity/ActivityCard.stories.jsx b/frontend/src/components/activity/ActivityCard.stories.jsx
deleted file mode 100644
index afd33af..0000000
--- a/frontend/src/components/activity/ActivityCard.stories.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-
-import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
-import ActivityCard from './ActivityCard';
-import '../../index.css'; // Global styles
-import '../../App.css';
-
-export default {
- title: 'Components/Activity/ActivityCard',
- component: ActivityCard,
- decorators: [
- (Story) => (
-
-
-
-
-
- ),
- ],
- tags: ['autodocs'],
-};
-
-const Template = (args) => ;
-
-export const NotStarted = Template.bind({});
-NotStarted.args = {
- activity: {
- id: 1,
- title: 'Future AI Hardware Summit 2026',
- start_time: '2026-12-01T09:00:00',
- status: '即将开始',
- cover_image: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&q=80',
- },
-};
-
-export const Ongoing = Template.bind({});
-Ongoing.args = {
- activity: {
- id: 2,
- title: 'Edge Computing Hackathon',
- start_time: '2025-10-20T10:00:00',
- status: '报名中',
- cover_image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80',
- },
-};
-
-export const Ended = Template.bind({});
-Ended.args = {
- activity: {
- id: 3,
- title: 'Deep Learning Workshop',
- start_time: '2023-05-15T14:00:00',
- status: '已结束',
- cover_image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80',
- },
-};
-
-export const SignedUp = Template.bind({});
-SignedUp.args = {
- activity: {
- id: 4,
- title: 'Exclusive Developer Meetup',
- start_time: '2025-11-11T18:00:00',
- status: '已报名',
- cover_image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&q=80',
- },
-};
diff --git a/frontend/src/components/activity/ActivityList.jsx b/frontend/src/components/activity/ActivityList.jsx
deleted file mode 100644
index 3badb43..0000000
--- a/frontend/src/components/activity/ActivityList.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-
-import React, { useState, useEffect } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { motion, AnimatePresence } from 'framer-motion';
-import { RightOutlined, LeftOutlined } from '@ant-design/icons';
-import { getActivities } from '../../api';
-import ActivityCard from './ActivityCard';
-import styles from './activity.module.less';
-import { fadeInUp, staggerContainer } from '../../animation';
-
-const ActivityList = () => {
- const { data: activities, isLoading, error } = useQuery({
- queryKey: ['activities'],
- queryFn: async () => {
- const res = await getActivities();
- // Handle different response structures
- return Array.isArray(res.data) ? res.data : (res.data?.results || []);
- },
- staleTime: 5 * 60 * 1000, // 5 minutes cache
- });
-
- const [currentIndex, setCurrentIndex] = useState(0);
-
- // Auto-play for desktop carousel
- useEffect(() => {
- if (!activities || activities.length <= 1) return;
- const interval = setInterval(() => {
- setCurrentIndex((prev) => (prev + 1) % activities.length);
- }, 5000);
- return () => clearInterval(interval);
- }, [activities]);
-
- const nextSlide = () => {
- if (!activities) return;
- setCurrentIndex((prev) => (prev + 1) % activities.length);
- };
-
- const prevSlide = () => {
- if (!activities) return;
- setCurrentIndex((prev) => (prev - 1 + activities.length) % activities.length);
- };
-
- if (isLoading) return Loading activities...
;
- if (error) return null; // Or error state
- if (!activities || activities.length === 0) return null;
-
- return (
-
-
-
- 近期活动 / EVENTS
-
-
-
-
-
-
-
- {/* Desktop: Carousel (Show one prominent, but allows list structure if needed)
- User said: "Activity only shows one, and in the form of a sliding page"
- */}
-
-
-
-
-
-
-
-
- {activities.map((_, idx) => (
- setCurrentIndex(idx)}
- />
- ))}
-
-
-
- {/* Mobile: Vertical List/Scroll as requested "Mobile vertical scroll" */}
-
- {activities.map((item, index) => (
-
-
-
- ))}
-
-
- );
-};
-
-export default ActivityList;
diff --git a/frontend/src/components/activity/activity.module.less b/frontend/src/components/activity/activity.module.less
deleted file mode 100644
index da7030b..0000000
--- a/frontend/src/components/activity/activity.module.less
+++ /dev/null
@@ -1,266 +0,0 @@
-
-@import '../../theme.module.less';
-
-.activitySection {
- padding: var(--spacing-lg) 0;
- width: 100%;
-}
-
-.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--spacing-md);
-}
-
-.sectionTitle {
- font-size: 24px;
- font-weight: 700;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- gap: 8px;
-
- &::before {
- content: '';
- display: block;
- width: 4px;
- height: 24px;
- background: var(--primary-color);
- border-radius: 2px;
- }
-}
-
-.controls {
- display: flex;
- gap: var(--spacing-sm);
-
- @media (max-width: 768px) {
- display: none; /* Hide carousel controls on mobile */
- }
-}
-
-.navBtn {
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid rgba(255, 255, 255, 0.2);
- color: var(--text-primary);
- width: 32px;
- height: 32px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.3s;
-
- &:hover {
- background: var(--primary-color);
- border-color: var(--primary-color);
- }
-}
-
-/* Desktop Carousel */
-.desktopCarousel {
- position: relative;
- width: 100%;
- height: 440px; /* 400px card + space for dots */
- overflow: hidden;
-
- @media (max-width: 768px) {
- display: none;
- }
-}
-
-.dots {
- display: flex;
- justify-content: center;
- gap: 8px;
- margin-top: var(--spacing-md);
-}
-
-.dot {
- width: 8px;
- height: 8px;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- cursor: pointer;
- transition: all 0.3s;
-
- &.activeDot {
- background: var(--primary-color);
- transform: scale(1.2);
- }
-}
-
-/* Mobile List */
-.mobileList {
- display: none;
- flex-direction: column;
- gap: var(--spacing-md);
-
- @media (max-width: 768px) {
- display: flex;
- }
-}
-
-/* --- Card Styles --- */
-.activityCard {
- position: relative;
- width: 100%;
- height: 400px;
- border-radius: var(--border-radius-lg);
- overflow: hidden;
- cursor: pointer;
- background: var(--background-card);
- box-shadow: var(--box-shadow-base);
- transition: all 0.3s ease;
-
- @media (max-width: 768px) {
- height: 300px;
- }
-}
-
-.imageContainer {
- width: 100%;
- height: 100%;
- position: relative;
-
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- transition: transform 0.5s ease;
- }
-}
-
-.overlay {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 60%;
- background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- padding: var(--spacing-lg);
- box-sizing: border-box;
-}
-
-.statusTag {
- display: inline-block;
- background: var(--primary-color);
- color: #fff;
- padding: 4px 12px;
- border-radius: 4px;
- font-size: 12px;
- font-weight: 600;
- margin-bottom: var(--spacing-sm);
- width: fit-content;
- text-transform: uppercase;
-}
-
-.title {
- color: var(--text-primary);
- font-size: 24px;
- font-weight: 700;
- margin-bottom: var(--spacing-xs);
- line-height: 1.3;
- text-shadow: 0 2px 4px rgba(0,0,0,0.5);
-
- @media (max-width: 768px) {
- font-size: 18px;
- }
-}
-
-.time {
- color: var(--text-secondary);
- font-size: 14px;
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-/* Detail Page Styles */
-.detailHeader {
- position: relative;
- height: 50vh;
- min-height: 300px;
- width: 100%;
- overflow: hidden;
-}
-
-.detailImage {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.detailContent {
- max-width: 800px;
- margin: -60px auto 0;
- position: relative;
- z-index: 10;
- padding: 0 var(--spacing-lg) 100px; /* Bottom padding for fixed footer */
-}
-
-.infoCard {
- background: var(--background-card);
- padding: var(--spacing-lg);
- border-radius: var(--border-radius-lg);
- box-shadow: var(--box-shadow-base);
- margin-bottom: var(--spacing-lg);
- border: 1px solid var(--border-color);
-}
-
-.richText {
- color: var(--text-secondary);
- line-height: 1.8;
- font-size: 16px;
-
- img {
- max-width: 100%;
- border-radius: var(--border-radius-base);
- margin: var(--spacing-md) 0;
- }
-
- h1, h2, h3 {
- color: var(--text-primary);
- margin-top: var(--spacing-lg);
- }
-}
-
-.fixedFooter {
- position: fixed;
- bottom: 0;
- left: 0;
- width: 100%;
- background: rgba(31, 31, 31, 0.95);
- backdrop-filter: blur(10px);
- padding: var(--spacing-md) var(--spacing-lg);
- border-top: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
- z-index: 100;
- box-shadow: 0 -4px 12px rgba(0,0,0,0.2);
-}
-
-.actionBtn {
- background: var(--primary-color);
- color: #fff;
- border: none;
- padding: 12px 32px;
- border-radius: 24px;
- font-size: 16px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: 0 4px 12px rgba(0, 185, 107, 0.3);
- transition: all 0.3s;
-
- &:disabled {
- background: #555;
- cursor: not-allowed;
- box-shadow: none;
- }
-}
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
deleted file mode 100644
index 7180a6a..0000000
--- a/frontend/src/context/AuthContext.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React, { createContext, useState, useEffect, useContext } from 'react';
-
-import { getUserInfo } from '../api';
-
-const AuthContext = createContext(null);
-
-export const AuthProvider = ({ children }) => {
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- 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) => {
- setUser(userData);
- localStorage.setItem('user', JSON.stringify(userData));
- if (userData.token) {
- localStorage.setItem('token', userData.token);
- }
- };
-
- const logout = () => {
- setUser(null);
- localStorage.removeItem('user');
- localStorage.removeItem('token');
- };
-
- const updateUser = (data) => {
- const newUser = { ...user, ...data };
- setUser(newUser);
- localStorage.setItem('user', JSON.stringify(newUser));
- };
-
- return (
-
- {children}
-
- );
-};
-
-export const useAuth = () => useContext(AuthContext);
diff --git a/frontend/src/index.css b/frontend/src/index.css
deleted file mode 100644
index 922e120..0000000
--- a/frontend/src/index.css
+++ /dev/null
@@ -1,59 +0,0 @@
-body {
- margin: 0;
- padding: 0;
- font-family: 'Orbitron', 'Roboto', sans-serif; /* 假设引入了科技感字体 */
- background-color: #050505;
- color: #fff;
- overflow-x: hidden;
-}
-
-/* 全局滚动条美化 */
-::-webkit-scrollbar {
- width: 8px;
-}
-::-webkit-scrollbar-track {
- background: #000;
-}
-::-webkit-scrollbar-thumb {
- background: #333;
- border-radius: 4px;
-}
-::-webkit-scrollbar-thumb:hover {
- background: #00b96b;
-}
-
-/* 霓虹光效工具类 */
-.neon-text-green {
- color: #00b96b;
- text-shadow: 0 0 5px rgba(0, 185, 107, 0.5), 0 0 10px rgba(0, 185, 107, 0.3);
-}
-
-.neon-text-blue {
- color: #00f0ff;
- text-shadow: 0 0 5px rgba(0, 240, 255, 0.5), 0 0 10px rgba(0, 240, 255, 0.3);
-}
-
-.neon-border {
- border: 1px solid rgba(0, 185, 107, 0.3);
- box-shadow: 0 0 10px rgba(0, 185, 107, 0.1), inset 0 0 10px rgba(0, 185, 107, 0.1);
-}
-
-/* 玻璃拟态 */
-.glass-panel {
- background: rgba(20, 20, 20, 0.6);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 16px;
-}
-
-/* 粒子背景容器 */
-#particle-canvas {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: -1;
- pointer-events: none;
-}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
deleted file mode 100644
index b9a1a6d..0000000
--- a/frontend/src/main.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.jsx'
-
-createRoot(document.getElementById('root')).render(
-
-
- ,
-)
diff --git a/frontend/src/pages/AIServices.jsx b/frontend/src/pages/AIServices.jsx
deleted file mode 100644
index f466b40..0000000
--- a/frontend/src/pages/AIServices.jsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Row, Col, Typography, Button, Spin } from 'antd';
-import { motion } from 'framer-motion';
-import {
- RightOutlined,
- SearchOutlined,
- DatabaseOutlined,
- ThunderboltOutlined,
- CheckCircleOutlined,
- CloudServerOutlined
-} from '@ant-design/icons';
-import { getServices } from '../api';
-import { useNavigate } from 'react-router-dom';
-
-const { Title, Paragraph } = Typography;
-
-const AIServices = () => {
- const [services, setServices] = useState([]);
- const [loading, setLoading] = useState(true);
- const navigate = useNavigate();
-
- useEffect(() => {
- const fetchServices = async () => {
- try {
- const response = await getServices();
- setServices(response.data);
- } catch (error) {
- console.error("Failed to fetch services:", error);
- } finally {
- setLoading(false);
- }
- };
- fetchServices();
- }, []);
-
- if (loading) {
- return (
-
-
-
Loading services...
-
- );
- }
-
- return (
-
-
-
-
- AI 全栈解决方案
-
-
-
- 从数据处理到模型部署,我们为您提供一站式 AI 基础设施服务。
-
-
-
-
- {services.map((item, index) => (
-
- navigate(`/services/${item.id}`)}
- style={{ cursor: 'pointer' }}
- >
-
- {/* HUD 装饰线 */}
-
-
-
-
-
-
-
- {item.display_icon ? (
-
- ) : (
-
- )}
-
-
{item.title}
-
-
-
{item.description}
-
-
- {item.features_list && item.features_list.map((feat, i) => (
-
- ))}
-
-
-
}
- onClick={(e) => {
- e.stopPropagation();
- navigate(`/services/${item.id}`);
- }}
- >
- 了解更多
-
-
-
-
- ))}
-
-
- {/* 动态流程图优化 */}
-
-
-
-
- 服务流程
-
-
-
- {[
- { title: '需求分析', icon: , desc: '深度沟通需求' },
- { title: '数据准备', icon: , desc: '高效数据处理' },
- { title: '模型训练', icon: , desc: '高性能算力' },
- { title: '测试验证', icon: , desc: '多维精度测试' },
- { title: '私有化部署', icon: , desc: '全栈落地部署' }
- ].map((step, i) => (
-
-
-
- {step.icon}
-
-
-
- {step.title}
- {step.desc}
-
-
- {/* 连接线 */}
- {i < 4 && (
-
- )}
-
-
- ))}
-
-
-
-
-
- );
-};
-
-export default AIServices;
diff --git a/frontend/src/pages/ForumDetail.jsx b/frontend/src/pages/ForumDetail.jsx
deleted file mode 100644
index e017747..0000000
--- a/frontend/src/pages/ForumDetail.jsx
+++ /dev/null
@@ -1,372 +0,0 @@
-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 } from 'antd';
-import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined } from '@ant-design/icons';
-import { getTopicDetail, createReply, uploadMedia } from '../api';
-import { useAuth } from '../context/AuthContext';
-import LoginModal from '../components/LoginModal';
-import CreateTopicModal from '../components/CreateTopicModal';
-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 { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
-import 'katex/dist/katex.min.css';
-
-const { Title, Text } = Typography;
-const { TextArea } = Input;
-
-const ForumDetail = () => {
- const { id } = useParams();
- const navigate = useNavigate();
- const { user } = useAuth();
-
- const [loading, setLoading] = useState(true);
- const [topic, setTopic] = useState(null);
- const [replyContent, setReplyContent] = useState('');
- const [submitting, setSubmitting] = useState(false);
- const [loginModalVisible, setLoginModalVisible] = useState(false);
-
- // Edit Topic State
- const [editModalVisible, setEditModalVisible] = useState(false);
-
- // Reply Image State
- const [replyUploading, setReplyUploading] = useState(false);
- const [replyMediaIds, setReplyMediaIds] = useState([]);
-
- const fetchTopic = async () => {
- try {
- const res = await getTopicDetail(id);
- setTopic(res.data);
- } catch (error) {
- console.error(error);
- message.error('加载失败');
- } finally {
- setLoading(false);
- }
- };
-
- const hasFetched = React.useRef(false);
- useEffect(() => {
- if (!hasFetched.current) {
- fetchTopic();
- hasFetched.current = true;
- }
- }, [id]);
-
- const handleSubmitReply = async () => {
- if (!user) {
- setLoginModalVisible(true);
- return;
- }
- if (!replyContent.trim()) {
- message.warning('请输入回复内容');
- return;
- }
-
- setSubmitting(true);
- try {
- await createReply({
- topic: id,
- content: replyContent,
- media_ids: replyMediaIds // Send uploaded media IDs
- });
- message.success('回复成功');
- setReplyContent('');
- setReplyMediaIds([]); // Reset media IDs
- fetchTopic(); // Refresh to show new reply
- } catch (error) {
- console.error(error);
- message.error('回复失败');
- } finally {
- setSubmitting(false);
- }
- };
-
- const handleReplyUpload = async (file) => {
- const formData = new FormData();
- formData.append('file', file);
- formData.append('media_type', file.type.startsWith('video') ? 'video' : 'image');
-
- setReplyUploading(true);
- try {
- const res = await uploadMedia(formData);
- if (res.data.id) {
- setReplyMediaIds(prev => [...prev, res.data.id]);
- }
-
- let url = res.data.file;
- if (url) url = url.replace(/\\/g, '/');
- if (url && !url.startsWith('http')) {
- const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
- const host = baseURL.replace(/\/api\/?$/, '');
- if (!url.startsWith('/')) url = '/' + url;
- url = `${host}${url}`;
- }
- url = url.replace(/([^:]\/)\/+/g, '$1');
-
- const insertText = file.type.startsWith('video')
- ? `\n \n`
- : `\n\n`;
-
- setReplyContent(prev => prev + insertText);
- message.success('上传成功');
- } catch (error) {
- console.error(error);
- message.error('上传失败');
- } finally {
- setReplyUploading(false);
- }
- return false;
- };
-
- if (loading) return Loading...
;
- if (!topic) return Topic not found
;
-
- 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 (
-
- );
- }
- };
-
- return (
-
-
-
}
- style={{ color: '#fff' }}
- onClick={() => navigate('/forum')}
- >
- 返回列表
-
-
- {/* 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) && (
-
}
- onClick={() => setEditModalVisible(true)}
- >
- 编辑帖子
-
- )}
-
-
- {/* Topic Content */}
-
-
- {topic.is_pinned &&
置顶 }
- {topic.product_info &&
{topic.product_info.name} }
-
{topic.title}
-
-
-
- } />
- {topic.author_info?.nickname}
- {topic.is_verified_owner && (
-
-
-
- )}
-
-
-
- {new Date(topic.created_at).toLocaleString()}
-
-
-
- {topic.view_count} 阅读
-
-
-
-
-
-
-
-
- {topic.content}
-
-
-
- {(() => {
- if (topic.media && topic.media.length > 0) {
- return topic.media.filter(m => m.media_type === 'video').map((media) => (
-
-
-
- ));
- }
- return null;
- })()}
-
-
- {/* Replies List */}
-
-
- {topic.replies?.length || 0} 条回复
-
-
- {topic.replies?.map((reply, index) => (
-
-
-
} />
-
-
-
- {reply.author_info?.nickname}
- {new Date(reply.created_at).toLocaleString()}
-
- #{index + 1}
-
-
-
- {reply.content}
-
-
-
-
-
- ))}
-
-
- {/* Reply Form */}
-
- 发表回复
- {user ? (
- <>
-
-
-
setLoginModalVisible(false)}
- onLoginSuccess={() => {}}
- />
-
- {/* Edit Modal */}
- {
- setEditModalVisible(false);
- // Workaround for scroll issue: Force reload page on close
- window.location.reload();
- }}
- onSuccess={() => {
- fetchTopic();
- }}
- initialValues={topic}
- isEditMode={true}
- topicId={topic?.id}
- />
-
- );
-};
-
-export default ForumDetail;
\ No newline at end of file
diff --git a/frontend/src/pages/ForumList.jsx b/frontend/src/pages/ForumList.jsx
deleted file mode 100644
index b70769d..0000000
--- a/frontend/src/pages/ForumList.jsx
+++ /dev/null
@@ -1,328 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip, Tabs, Row, Col } from 'antd';
-import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined } from '@ant-design/icons';
-import { useNavigate } from 'react-router-dom';
-import { motion } from 'framer-motion';
-import { getTopics, getStarUsers, getAnnouncements } from '../api';
-import { useAuth } from '../context/AuthContext';
-import CreateTopicModal from '../components/CreateTopicModal';
-import LoginModal from '../components/LoginModal';
-
-const { Title, Text, Paragraph } = Typography;
-
-const ForumList = () => {
- const navigate = useNavigate();
- const { user } = useAuth();
-
- const [loading, setLoading] = useState(true);
- const [topics, setTopics] = useState([]);
- const [starUsers, setStarUsers] = useState([]);
- const [announcements, setAnnouncements] = useState([]);
- const [searchText, setSearchText] = useState('');
- const [category, setCategory] = useState('all');
- const [createModalVisible, setCreateModalVisible] = useState(false);
- const [loginModalVisible, setLoginModalVisible] = useState(false);
-
- const fetchTopics = async (search = '', cat = '') => {
- setLoading(true);
- try {
- const params = {};
- if (search) params.search = search;
- if (cat && cat !== 'all') params.category = cat;
-
- const res = await getTopics(params);
- setTopics(res.data.results || res.data); // Support pagination result or list
- } catch (error) {
- console.error(error);
- message.error('获取帖子列表失败');
- } finally {
- setLoading(false);
- }
- };
-
- const fetchStarUsers = async () => {
- try {
- const res = await getStarUsers();
- setStarUsers(res.data);
- } catch (error) {
- console.error("Fetch star users failed", error);
- }
- };
-
- const fetchAnnouncements = async () => {
- try {
- const res = await getAnnouncements();
- setAnnouncements(res.data.results || res.data);
- } catch (error) {
- console.error("Fetch announcements failed", error);
- }
- };
-
- useEffect(() => {
- fetchTopics(searchText, category);
- fetchStarUsers();
- fetchAnnouncements();
- }, [category]);
-
- const handleSearch = (value) => {
- setSearchText(value);
- fetchTopics(value, category);
- };
-
- const handleCreateClick = () => {
- if (!user) {
- setLoginModalVisible(true);
- return;
- }
- setCreateModalVisible(true);
- };
-
- const getCategoryIcon = (cat) => {
- switch(cat) {
- case 'help': return ;
- case 'share': return ;
- case 'notice': return ;
- default: return ;
- }
- };
-
- const getCategoryLabel = (cat) => {
- switch(cat) {
- case 'help': return '求助';
- case 'share': return '分享';
- case 'notice': return '公告';
- default: return '讨论';
- }
- };
-
- const items = [
- { key: 'all', label: '全部话题' },
- { key: 'discussion', label: '技术讨论' },
- { key: 'help', label: '求助问答' },
- { key: 'share', label: '经验分享' },
- { key: 'notice', label: '官方公告' },
- ];
-
- return (
-
- {/* Hero Section */}
-
-
-
- Quant Speed Developer Community
-
-
- 技术交流 · 硬件开发 · 官方支持 · 量迹生态
-
-
-
-
- }
- style={{ borderRadius: 8, background: 'rgba(255,255,255,0.1)', border: '1px solid #333', color: '#fff' }}
- onChange={(e) => setSearchText(e.target.value)}
- onPressEnter={(e) => handleSearch(e.target.value)}
- />
- }
- onClick={handleCreateClick}
- style={{ height: 'auto', borderRadius: 8 }}
- >
- 发布新帖
-
-
-
-
- {/* Content Section */}
-
-
-
-
- (
-
- navigate(`/forum/${item.id}`)}
- >
-
-
-
- {item.is_pinned && }>置顶}
-
- {getCategoryLabel(item.category)}
-
- {item.is_verified_owner && (
-
- } color="#00b96b" style={{ margin: 0 }}>认证用户
-
- )}
-
- {item.title}
-
-
-
-
- {item.content.replace(/!\[.*?\]\(.*?\)/g, '[图片]').replace(/[#*`]/g, '')} {/* Simple markdown strip */}
-
-
- {item.content.match(/!\[.*?\]\((.*?)\)/) && (
-
-
-
- )}
-
-
-
- } size="small" />
-
- {item.author_info?.nickname || '匿名用户'}
-
- {item.author_info?.is_star && (
-
-
-
- )}
-
- •
- {new Date(item.created_at).toLocaleDateString()}
- {item.product_info && (
- {item.product_info.name}
- )}
-
-
-
-
-
-
-
{item.replies?.length || 0}
-
-
-
-
{item.view_count || 0}
-
-
-
-
-
- )}
- locale={{ emptyText: 暂无帖子,来发布第一个吧!
}}
- />
-
-
-
- 技术专家榜 }
- style={{ background: 'rgba(20,20,20,0.6)', border: '1px solid rgba(255,255,255,0.1)', backdropFilter: 'blur(10px)' }}
- headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
- >
-
- {starUsers.length > 0 ? (
- starUsers.map(u => (
-
-
} />
-
-
- {u.nickname}
-
-
{u.title || '技术专家'}
-
-
- ))
- ) : (
-
暂无上榜专家
- )}
-
-
-
- 社区公告 }
- style={{ marginTop: 20, background: 'rgba(20,20,20,0.6)', border: '1px solid rgba(255,255,255,0.1)', backdropFilter: 'blur(10px)' }}
- headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
- >
- (
-
- {item.display_image_url && (
-
-
-
- )}
-
- {item.link_url ? (
-
{item.title}
- ) : (
-
{item.title}
- )}
-
-
- {item.content}
-
-
- )}
- locale={{ emptyText: 暂无公告
}}
- />
-
-
-
-
-
-
setCreateModalVisible(false)}
- onSuccess={() => fetchTopics(searchText, category)}
- />
-
- setLoginModalVisible(false)}
- onLoginSuccess={() => {
- setCreateModalVisible(true);
- }}
- />
-
- );
-};
-
-export default ForumList;
diff --git a/frontend/src/pages/Home.css b/frontend/src/pages/Home.css
deleted file mode 100644
index 095bb0f..0000000
--- a/frontend/src/pages/Home.css
+++ /dev/null
@@ -1,78 +0,0 @@
-.tech-card {
- background: rgba(255, 255, 255, 0.05) !important;
- border: 1px solid #303030 !important;
- transition: all 0.3s ease;
- cursor: pointer;
- box-shadow: none !important; /* 强制移除默认阴影 */
- overflow: hidden; /* 确保子元素不会溢出产生黑边 */
- outline: none;
-}
-
-.tech-card:hover {
- border-color: #00b96b !important;
- box-shadow: 0 0 20px rgba(0, 185, 107, 0.4) !important; /* 增强悬停发光 */
- transform: translateY(-5px);
-}
-
-.tech-card .ant-card-body {
- border-top: none !important;
- box-shadow: none !important;
-}
-
-.tech-card-title {
- color: #fff;
- font-size: 18px;
- font-weight: bold;
- margin-bottom: 10px;
-}
-
-.tech-price {
- color: #00b96b;
- font-size: 20px;
- font-weight: bold;
-}
-
-.product-scroll-container {
- overflow-x: auto;
- overflow-y: hidden;
- -webkit-overflow-scrolling: touch;
- padding: 30px 20px; /* 增加左右内边距,为悬停缩放和投影留出空间 */
- margin: 0 -20px; /* 使用负外边距抵消内边距,使滚动条能延伸到版心边缘 */
- width: calc(100% + 40px);
-}
-
-/* 自定义滚动条 */
-.product-scroll-container::-webkit-scrollbar {
- height: 6px;
-}
-
-.product-scroll-container::-webkit-scrollbar-track {
- background: rgba(255, 255, 255, 0.05);
- border-radius: 3px;
- margin: 0 20px; /* 让滚动条轨道在版心内显示 */
-}
-
-.product-scroll-container::-webkit-scrollbar-thumb {
- background: rgba(0, 185, 107, 0.2);
- border-radius: 3px;
- transition: all 0.3s;
-}
-
-.product-scroll-container::-webkit-scrollbar-thumb:hover {
- background: rgba(0, 185, 107, 0.5);
-}
-
-/* 布局对齐 */
-.product-scroll-container .ant-row {
- margin-left: 0 !important;
- margin-right: 0 !important;
- padding: 0;
- display: flex;
- flex-wrap: nowrap;
- justify-content: flex-start;
-}
-
-.product-scroll-container .ant-col {
- flex: 0 0 320px;
- padding: 0 12px;
-}
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
deleted file mode 100644
index 458adcb..0000000
--- a/frontend/src/pages/Home.jsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Card, Row, Col, Tag, Button, Spin, Typography } from 'antd';
-import { RocketOutlined, RightOutlined } from '@ant-design/icons';
-import { useNavigate } from 'react-router-dom';
-import { motion } from 'framer-motion';
-import { getConfigs } from '../api';
-import ActivityList from '../components/activity/ActivityList';
-import './Home.css';
-
-const { Title, Paragraph } = Typography;
-
-const Home = () => {
- const [products, setProducts] = useState([]);
- const [loading, setLoading] = useState(true);
- const [typedText, setTypedText] = useState('');
- const [isTypingComplete, setIsTypingComplete] = useState(false);
- const fullText = "未来已来 AI 核心驱动";
- const navigate = useNavigate();
-
- useEffect(() => {
- fetchProducts();
- let i = 0;
- const typingInterval = setInterval(() => {
- i++;
- setTypedText(fullText.slice(0, i));
- if (i >= fullText.length) {
- clearInterval(typingInterval);
- setIsTypingComplete(true);
- }
- }, 150);
-
- return () => clearInterval(typingInterval);
- }, []);
-
- const fetchProducts = async () => {
- try {
- const response = await getConfigs();
- setProducts(response.data);
- } catch (error) {
- console.error('Failed to fetch products:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const cardVariants = {
- hidden: { opacity: 0, y: 50 },
- visible: (i) => ({
- opacity: 1,
- y: 0,
- transition: {
- delay: i * 0.1,
- duration: 0.5,
- type: "spring",
- stiffness: 100
- }
- }),
- hover: {
- scale: 1.05,
- rotateX: 5,
- rotateY: 5,
- transition: { duration: 0.3 }
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
-
- {typedText}
- {!isTypingComplete && | }
-
-
-
- 量迹 AI 硬件为您提供最强大的边缘计算能力,搭载最新一代神经处理单元,赋能您的每一个创意。
-
-
-
-
-
-
-
-
- {products.map((product, index) => (
-
-
-
- {product.static_image_url ? (
-
- ) : (
-
-
-
- )}
-
- }
- onClick={() => navigate(`/product/${product.id}`)}
- >
-
{product.name}
-
- {product.description}
-
-
- {product.chip_type}
- {product.has_camera && Camera }
- {product.has_microphone && Mic }
-
-
-
¥{product.price}
-
} style={{ background: '#00b96b', borderColor: '#00b96b' }} />
-
-
-
-
- ))}
-
-
-
-
- );
-};
-
-export default Home;
diff --git a/frontend/src/pages/MyOrders.jsx b/frontend/src/pages/MyOrders.jsx
deleted file mode 100644
index 24318e5..0000000
--- a/frontend/src/pages/MyOrders.jsx
+++ /dev/null
@@ -1,342 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions, Tabs } from 'antd';
-import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined, CalendarOutlined } from '@ant-design/icons';
-import { queryMyOrders, getMySignups } from '../api';
-import { motion } from 'framer-motion';
-import LoginModal from '../components/LoginModal';
-import { useAuth } from '../context/AuthContext';
-import { useNavigate } from 'react-router-dom';
-
-const { Title, Text, Paragraph } = Typography;
-
-const MyOrders = () => {
- const [loading, setLoading] = useState(false);
- const [orders, setOrders] = useState([]);
- const [activities, setActivities] = useState([]);
- const [modalVisible, setModalVisible] = useState(false);
- const [currentOrder, setCurrentOrder] = useState(null);
- const [loginVisible, setLoginVisible] = useState(false);
- const navigate = useNavigate();
-
- const { user, login } = useAuth();
-
- useEffect(() => {
- if (user) {
- handleQueryData();
- }
- }, [user]);
-
- const showDetail = (order) => {
- setCurrentOrder(order);
- setModalVisible(true);
- };
-
- const handleQueryData = async () => {
- setLoading(true);
- try {
- const { default: api } = await import('../api');
-
- // Parallel fetch
- const [ordersRes, activitiesRes] = await Promise.allSettled([
- api.get('/orders/'),
- getMySignups()
- ]);
-
- if (ordersRes.status === 'fulfilled') {
- setOrders(ordersRes.value.data);
- }
-
- if (activitiesRes.status === 'fulfilled') {
- setActivities(activitiesRes.value.data);
- }
-
- } catch (error) {
- console.error(error);
- message.error('查询出错');
- } finally {
- setLoading(false);
- }
- };
-
- const getStatusTag = (status) => {
- switch (status) {
- case 'paid': return } color="success">已支付;
- case 'pending': return } color="warning">待支付;
- case 'shipped': return } color="processing">已发货;
- case 'cancelled': return } color="default">已取消;
- default: return {status} ;
- }
- };
-
- return (
-
-
-
-
-
我的订单
- Secure Order Verification System
-
-
- {!user ? (
-
- 请先登录以查看您的订单
- setLoginVisible(true)}>立即登录
-
- ) : (
-
-
- 当前登录用户: {user.nickname}
- }
- >
- 刷新
-
-
-
- 我的订单,
- children: (
- (
-
- showDetail(order)}
- title={订单号: {order.id} {getStatusTag(order.status)} }
- style={{
- background: 'rgba(0,0,0,0.6)',
- border: '1px solid rgba(255,255,255,0.1)',
- marginBottom: 10,
- backdropFilter: 'blur(10px)'
- }}
- headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
- bodyStyle={{ padding: '20px' }}
- >
-
-
- {order.total_price} 元
- {new Date(order.created_at).toLocaleString()}
-
-
-
-
- {order.config_image ? (
-
- ) : (
-
-
-
- )}
-
-
{order.config_name || `商品 ID: ${order.config}`}
-
数量: x{order.quantity}
-
-
-
-
- {(order.courier_name || order.tracking_number) && (
-
-
-
-
- 物流信息
-
-
-
- 快递公司:
- {order.courier_name || '未知'}
-
-
-
快递单号:
- {order.tracking_number ? (
-
e.stopPropagation()}>
-
- {order.tracking_number}
-
-
- ) : (
-
暂无单号
- )}
-
-
-
- )}
-
-
-
- )}
- locale={{ emptyText: 暂无订单信息
}}
- />
- )
- },
- {
- key: '2',
- label: 我的活动 ,
- children: (
- {
- const activity = item.activity_info || item.activity || item;
- return (
-
- navigate(`/activity/${activity.id}`)}
- cover={
-
-
-
- }
- style={{
- background: 'rgba(0,0,0,0.6)',
- border: '1px solid rgba(255,255,255,0.1)',
- marginBottom: 10,
- backdropFilter: 'blur(10px)',
- overflow: 'hidden'
- }}
- headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
- bodyStyle={{ padding: '16px' }}
- >
-
-
{activity.title}
-
-
-
-
- {new Date(activity.start_time).toLocaleDateString()}
-
-
-
-
-
-
- {activity.location || '线上活动'}
-
-
-
-
- {activity.status || '已报名'}
- 查看详情
-
-
-
-
- );
- }}
- locale={{ emptyText: 暂无活动报名
}}
- />
- )
- }
- ]} />
-
- )}
-
-
订单详情}
- open={modalVisible}
- onCancel={() => setModalVisible(false)}
- footer={[
- setModalVisible(false)}>
- 关闭
-
- ]}
- width={600}
- centered
- >
- {currentOrder && (
-
-
- {currentOrder.id}
-
- {currentOrder.config_name}
- {new Date(currentOrder.created_at).toLocaleString()}
- {new Date(currentOrder.updated_at).toLocaleString()}
- {getStatusTag(currentOrder.status)}
-
- ¥{currentOrder.total_price}
-
-
-
-
- {currentOrder.customer_name}
- {currentOrder.phone_number}
- {currentOrder.shipping_address}
-
-
-
- {currentOrder.salesperson_name && (
-
-
- {currentOrder.salesperson_name}
- {currentOrder.salesperson_code && {currentOrder.salesperson_code} }
-
-
- )}
-
- {(currentOrder.status === 'shipped' || currentOrder.courier_name) && (
- <>
- {currentOrder.courier_name || '未知'}
-
- {currentOrder.tracking_number ? (
-
- {currentOrder.tracking_number}
-
- ) : '暂无单号'}
-
- >
- )}
-
- )}
-
-
-
setLoginVisible(false)}
- onLoginSuccess={(userData) => {
- login(userData);
- if (userData.phone_number) {
- handleQueryOrders(userData.phone_number);
- }
- }}
- />
-
-
- );
-};
-
-export default MyOrders;
diff --git a/frontend/src/pages/Payment.css b/frontend/src/pages/Payment.css
deleted file mode 100644
index 2da9ed7..0000000
--- a/frontend/src/pages/Payment.css
+++ /dev/null
@@ -1,52 +0,0 @@
-.payment-container {
- max-width: 600px;
- margin: 50px auto;
- padding: 40px;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid #303030;
- border-radius: 12px;
- text-align: center;
-}
-
-.payment-title {
- color: #fff;
- font-size: 28px;
- margin-bottom: 30px;
-}
-
-.payment-amount {
- font-size: 48px;
- color: #00b96b;
- font-weight: bold;
- margin: 20px 0;
-}
-
-.payment-info {
- text-align: left;
- background: rgba(0,0,0,0.3);
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 30px;
- color: #ccc;
-}
-
-.payment-method {
- display: flex;
- justify-content: center;
- gap: 20px;
- margin-bottom: 30px;
-}
-
-.payment-method-item {
- border: 1px solid #444;
- padding: 10px 20px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.3s;
- color: #fff;
-}
-
-.payment-method-item.active {
- border-color: #00b96b;
- background: rgba(0, 185, 107, 0.1);
-}
diff --git a/frontend/src/pages/Payment.jsx b/frontend/src/pages/Payment.jsx
deleted file mode 100644
index 1286aaa..0000000
--- a/frontend/src/pages/Payment.jsx
+++ /dev/null
@@ -1,206 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, useNavigate, useLocation } from 'react-router-dom';
-import { Button, message, Result, Spin } from 'antd';
-import { WechatOutlined, AlipayCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
-import { QRCodeSVG } from 'qrcode.react';
-import { getOrder, initiatePayment, confirmPayment, nativePay, queryOrderStatus } from '../api';
-import './Payment.css';
-
-const Payment = () => {
- const { orderId: initialOrderId } = useParams();
- const navigate = useNavigate();
- const location = useLocation();
- const [currentOrderId, setCurrentOrderId] = useState(location.state?.order_id || initialOrderId);
- const [order, setOrder] = useState(location.state?.orderInfo || null);
- const [codeUrl, setCodeUrl] = useState(location.state?.codeUrl || null);
- const [loading, setLoading] = useState(!location.state?.orderInfo && !location.state?.codeUrl);
- const [paying, setPaying] = useState(!!location.state?.codeUrl);
- const [paySuccess, setPaySuccess] = useState(false);
- const [paymentMethod, setPaymentMethod] = useState('wechat');
-
- useEffect(() => {
- if (codeUrl && !paying) {
- setPaying(true);
- }
- }, [codeUrl]);
-
- useEffect(() => {
- console.log('Payment page state:', { currentOrderId, order, codeUrl, paying });
- if (!order && !codeUrl) {
- fetchOrder();
- }
- }, [currentOrderId]);
-
- useEffect(() => {
- if (paying && !codeUrl && order) {
- handlePay();
- }
- }, [paying, codeUrl, order]);
-
- // 轮询订单状态
- useEffect(() => {
- let timer;
- if (paying && !paySuccess) {
- timer = setInterval(async () => {
- try {
- const response = await queryOrderStatus(currentOrderId);
- if (response.data.status === 'paid') {
- setPaySuccess(true);
- setPaying(false);
- clearInterval(timer);
- }
- } catch (error) {
- console.error('Check payment status failed:', error);
- }
- }, 3000);
- }
- return () => clearInterval(timer);
- }, [paying, paySuccess, currentOrderId]);
-
- const fetchOrder = async () => {
- try {
- const response = await getOrder(currentOrderId);
- setOrder(response.data);
- } catch (error) {
- console.error('Failed to fetch order:', error);
- // Fallback if getOrder API fails (404/405), we might show basic info or error
- // Assuming for now it works or we handle it
- message.error('无法获取订单信息,请重试');
- } finally {
- setLoading(false);
- }
- };
-
- const handlePay = async () => {
- if (paymentMethod === 'alipay') {
- message.info('暂未开通支付宝支付,请使用微信支付');
- return;
- }
-
- if (codeUrl) {
- setPaying(true);
- return;
- }
-
- if (!order) {
- message.error('正在加载订单信息,请稍后...');
- return;
- }
-
- setPaying(true);
- try {
- const orderData = {
- goodid: order.config || order.goodid,
- quantity: order.quantity,
- customer_name: order.customer_name,
- phone_number: order.phone_number,
- shipping_address: order.shipping_address,
- ref_code: order.ref_code
- };
-
- const response = await nativePay(orderData);
- setCodeUrl(response.data.code_url);
- if (response.data.order_id) {
- setCurrentOrderId(response.data.order_id);
- }
- message.success('支付二维码已生成');
- } catch (error) {
- console.error(error);
- message.error('生成支付二维码失败,请重试');
- setPaying(false);
- }
- };
-
- if (loading) return ;
-
- if (paySuccess) {
- return (
-
- }
- title={支付成功 }
- subTitle={订单 {currentOrderId} 已完成支付,我们将尽快为您发货。 }
- extra={[
- navigate('/')}>
- 返回首页
- ,
- ]}
- />
-
- );
- }
-
- return (
-
-
收银台
-
- {order ? (
- <>
-
¥{order.total_price}
-
-
订单编号: {order.id}
-
商品名称: {order.config_name || 'AI 硬件设备'}
-
收货人: {order.customer_name}
-
- >
- ) : (
-
-
订单 ID: {currentOrderId}
-
无法加载详情,但您可以尝试支付。
-
- )}
-
-
选择支付方式:
-
-
setPaymentMethod('wechat')}
- >
-
- 微信支付
-
-
setPaymentMethod('alipay')}
- >
-
- 支付宝
-
-
-
- {paying && (
-
- {codeUrl ? (
- <>
-
-
-
-
请使用微信扫码支付
-
支付完成后将自动跳转
- >
- ) : (
-
- )}
-
- )}
-
- {!paying && (
-
- 立即支付
-
- )}
-
- );
-};
-
-export default Payment;
diff --git a/frontend/src/pages/ProductDetail.css b/frontend/src/pages/ProductDetail.css
deleted file mode 100644
index 7392d3d..0000000
--- a/frontend/src/pages/ProductDetail.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.product-detail-container {
- color: #fff;
-}
-
-.feature-section {
- padding: 60px 0;
- border-bottom: 1px solid #303030;
- text-align: center;
-}
-
-.feature-title {
- font-size: 32px;
- font-weight: bold;
- margin-bottom: 20px;
- color: #00b96b;
-}
-
-.feature-desc {
- font-size: 18px;
- color: #888;
- max-width: 800px;
- margin: 0 auto;
-}
-
-.spec-tag {
- background: rgba(0, 185, 107, 0.1);
- border: 1px solid #00b96b;
- color: #00b96b;
- padding: 5px 15px;
- border-radius: 4px;
- margin-right: 10px;
- display: inline-block;
-}
diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx
deleted file mode 100644
index bfd2634..0000000
--- a/frontend/src/pages/ProductDetail.jsx
+++ /dev/null
@@ -1,308 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
-import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions, Radio, Alert } from 'antd';
-import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
-import { getConfigs, createOrder, nativePay } from '../api';
-import ModelViewer from '../components/ModelViewer';
-import { useAuth } from '../context/AuthContext';
-import './ProductDetail.css';
-
-const ProductDetail = () => {
- const { id } = useParams();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const [product, setProduct] = useState(null);
- const [loading, setLoading] = useState(true);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [submitting, setSubmitting] = useState(false);
- const [form] = Form.useForm();
-
- const { user } = useAuth();
-
- // 优先从 URL 获取,如果没有则从 localStorage 获取,不再默认绑定 flw666
- const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
-
- useEffect(() => {
- // 自动填充用户信息
- if (user) {
- form.setFieldsValue({
- phone_number: user.phone_number,
- // 如果后端返回了地址信息,这里也可以填充
- // shipping_address: user.shipping_address
- });
- }
- }, [isModalOpen, user]); // 当弹窗打开或用户状态变化时填充
-
- useEffect(() => {
- console.log('[ProductDetail] Current ref_code:', refCode);
- }, [refCode]);
-
- useEffect(() => {
- fetchProduct();
- }, [id]);
-
- const fetchProduct = async () => {
- try {
- const response = await getConfigs();
- const found = response.data.find(p => String(p.id) === id);
- if (found) {
- setProduct(found);
- } else {
- message.error('未找到该产品');
- navigate('/');
- }
- } catch (error) {
- console.error('Failed to fetch product:', error);
- message.error('加载失败');
- } finally {
- setLoading(false);
- }
- };
-
- const handleBuy = async (values) => {
- setSubmitting(true);
- try {
- const isPickup = values.delivery_method === 'pickup';
- const orderData = {
- goodid: product.id,
- quantity: values.quantity,
- customer_name: values.customer_name,
- phone_number: values.phone_number,
- // 如果是自提,手动设置地址,否则使用表单中的地址
- shipping_address: isPickup ? '线下自提' : values.shipping_address,
- ref_code: refCode
- };
-
- console.log('提交订单数据:', orderData); // 调试日志
- const response = await nativePay(orderData);
- message.success('订单已创建,请完成支付');
- navigate(`/payment/${response.data.order_id}`, {
- state: {
- codeUrl: response.data.code_url,
- order_id: response.data.order_id,
- orderInfo: {
- ...orderData,
- id: response.data.order_id,
- config_name: product.name,
- total_price: product.price * values.quantity
- }
- }
- });
- } catch (error) {
- console.error(error);
- message.error('创建订单失败,请检查填写信息');
- } finally {
- setSubmitting(false);
- }
- };
-
- const getModelPaths = (p) => {
- if (!p) return null;
-
- // 优先使用后台配置的 3D 模型 URL
- if (p.model_3d_url) {
- return { obj: p.model_3d_url };
- }
-
- return null;
- };
-
- const modelPaths = getModelPaths(product);
-
- const renderIcon = (feature) => {
- if (feature.display_icon) {
- return ;
- }
-
- const iconProps = { style: { fontSize: 60, color: '#00b96b', marginBottom: 20 } };
-
- switch(feature.icon_name) {
- case 'SafetyCertificate':
- return ;
- case 'Eye':
- return ;
- case 'Thunderbolt':
- return ;
- default:
- return ;
- }
- };
-
- if (loading) return
;
- if (!product) return null;
-
- return (
-
- {/* Hero Section */}
-
-
-
- {modelPaths ? (
-
- ) : product.static_image_url ? (
-
- ) : (
-
- )}
-
-
-
- {product.name}
- {product.description}
-
-
- {product.chip_type}
- {product.has_camera && 高清摄像头 }
- {product.has_microphone && 阵列麦克风 }
-
-
-
-
-
-
-
- {product.stock < 5 && product.stock > 0 && (
-
- )}
-
- {product.stock === 0 && (
-
- )}
-
- }
- onClick={() => setIsModalOpen(true)}
- disabled={product.stock === 0}
- style={{ height: 50, padding: '0 40px', fontSize: 18 }}
- >
- {product.stock === 0 ? '暂时缺货' : '立即购买'}
-
-
-
-
- {/* Feature Section */}
-
- {product.features && product.features.length > 0 ? (
- product.features.map((feature, index) => (
-
- {renderIcon(feature)}
-
{feature.title}
-
{feature.description}
-
- ))
- ) : (
- // Fallback content if no features are configured
- <>
-
-
-
工业级安全标准
-
- 采用军工级加密芯片,保障您的数据隐私安全。无论是边缘计算还是云端同步,全程加密传输,让 AI 应用无后顾之忧。
-
-
-
-
-
-
超清视觉感知
-
- 搭载 4K 高清摄像头与 AI 视觉算法,实时捕捉每一个细节。支持人脸识别、物体检测、姿态分析等多种视觉任务。
-
-
-
-
-
-
极致性能释放
-
- {product.chip_type} 强劲核心,提供高达 XX TOPS 的算力支持。低功耗设计,满足 24 小时全天候运行需求。
-
-
- >
- )}
-
- {product.display_detail_image ? (
-
-
-
- ) : (
-
- 产品详情长图展示区域 (请在后台配置)
-
- )}
-
-
- {/* Order Modal */}
-
setIsModalOpen(false)}
- footer={null}
- destroyOnHidden
- >
-
-
- 快递配送
- 线下自提
-
-
-
-
-
-
-
-
-
-
-
- prevValues.delivery_method !== currentValues.delivery_method}
- >
- {({ getFieldValue }) =>
- getFieldValue('delivery_method') === 'shipping' ? (
-
-
-
- ) : (
-
-
自提地址:昆明市云纺国际商厦B座1406
-
请在工作日 9:00 - 18:00 期间前往提货
-
- )
- }
-
-
-
- setIsModalOpen(false)}>取消
- 提交订单
-
-
-
-
- );
-};
-
-export default ProductDetail;
diff --git a/frontend/src/pages/ServiceDetail.jsx b/frontend/src/pages/ServiceDetail.jsx
deleted file mode 100644
index 66e69f9..0000000
--- a/frontend/src/pages/ServiceDetail.jsx
+++ /dev/null
@@ -1,265 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
-import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd';
-import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons';
-import { getServiceDetail, createServiceOrder } from '../api';
-import { motion } from 'framer-motion';
-
-const { Title, Paragraph } = Typography;
-
-const ServiceDetail = () => {
- const { id } = useParams();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const [service, setService] = useState(null);
- const [loading, setLoading] = useState(true);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [submitting, setSubmitting] = useState(false);
- const [form] = Form.useForm();
-
- // 优先从 URL 获取,如果没有则从 localStorage 获取
- const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
-
- useEffect(() => {
- console.log('[ServiceDetail] Current ref_code:', refCode);
- }, [refCode]);
-
- useEffect(() => {
- const fetchDetail = async () => {
- try {
- const response = await getServiceDetail(id);
- setService(response.data);
- } catch (error) {
- console.error("Failed to fetch service detail:", error);
- } finally {
- setLoading(false);
- }
- };
- fetchDetail();
- }, [id]);
-
- const handlePurchase = async (values) => {
- setSubmitting(true);
- try {
- const orderData = {
- service: service.id,
- customer_name: values.customer_name,
- company_name: values.company_name,
- phone_number: values.phone_number,
- email: values.email,
- requirements: values.requirements,
- ref_code: refCode
- };
- await createServiceOrder(orderData);
- message.success('需求已提交,我们的销售顾问将尽快与您联系!');
- setIsModalOpen(false);
- } catch (error) {
- console.error(error);
- message.error('提交失败,请重试');
- } finally {
- setSubmitting(false);
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (!service) {
- return (
-
-
- navigate('/services')} style={{ marginTop: 20 }}>
- Return to Services
-
-
- );
- }
-
- return (
-
-
}
- style={{ color: '#fff', marginBottom: 20 }}
- onClick={() => navigate('/services')}
- >
- 返回服务列表
-
-
-
-
-
-
-
- {service.title}
-
-
- {service.description}
-
-
-
-
-
- 服务详情
-
-
- 交付周期}>
- {service.delivery_time || '待沟通'}
-
- 交付内容}>
- {service.delivery_content || '根据需求定制'}
-
-
-
-
-
- {service.display_detail_image ? (
-
-
-
- ) : (
-
- 暂无详情图片
-
- )}
-
-
-
-
-
-
服务报价
-
- ¥{service.price}
- / {service.unit} 起
-
-
-
- {service.features_list && service.features_list.map((feat, i) => (
-
- {feat}
-
- ))}
-
-
-
}
- style={{
- height: 50,
- background: service.color,
- borderColor: service.color,
- color: '#000',
- fontWeight: 'bold'
- }}
- onClick={() => setIsModalOpen(true)}
- >
- 立即咨询 / 购买
-
-
- * 具体价格可能因需求复杂度而异,提交需求后我们将提供详细报价单
-
-
-
-
-
-
-
- {/* Purchase Modal */}
-
setIsModalOpen(false)}
- footer={null}
- destroyOnHidden
- >
- 请填写您的联系方式和需求,我们的技术顾问将在 24 小时内与您联系。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setIsModalOpen(false)}>取消
- 提交需求
-
-
-
-
- );
-};
-
-export default ServiceDetail;
diff --git a/frontend/src/pages/VCCourseDetail.jsx b/frontend/src/pages/VCCourseDetail.jsx
deleted file mode 100644
index da3d945..0000000
--- a/frontend/src/pages/VCCourseDetail.jsx
+++ /dev/null
@@ -1,286 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
-import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message } from 'antd';
-import { ArrowLeftOutlined, ClockCircleOutlined, UserOutlined, BookOutlined, FormOutlined } from '@ant-design/icons';
-import { getVCCourseDetail, createOrder } from '../api';
-import { motion } from 'framer-motion';
-
-const { Title, Paragraph } = Typography;
-
-const VCCourseDetail = () => {
- const { id } = useParams();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const [course, setCourse] = useState(null);
- const [loading, setLoading] = useState(true);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [submitting, setSubmitting] = useState(false);
- const [form] = Form.useForm();
-
- // 优先从 URL 获取,如果没有则从 localStorage 获取
- const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
-
- useEffect(() => {
- const fetchDetail = async () => {
- try {
- const response = await getVCCourseDetail(id);
- setCourse(response.data);
- } catch (error) {
- console.error("Failed to fetch course detail:", error);
- } finally {
- setLoading(false);
- }
- };
- fetchDetail();
- }, [id]);
-
- const handleEnroll = async (values) => {
- setSubmitting(true);
- try {
- const orderData = {
- course: course.id,
- customer_name: values.customer_name,
- phone_number: values.phone_number,
- ref_code: refCode,
- quantity: 1,
- // 将其他信息放入收货地址字段中
- shipping_address: `[课程报名] 微信号: ${values.wechat_id || '无'}, 邮箱: ${values.email || '无'}, 备注: ${values.message || '无'}`
- };
-
- await createOrder(orderData);
- message.success('报名咨询已提交,我们的课程顾问将尽快与您联系!');
- setIsModalOpen(false);
- } catch (error) {
- console.error(error);
- message.error('提交失败,请重试');
- } finally {
- setSubmitting(false);
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (!course) {
- return (
-
-
- navigate('/courses')} style={{ marginTop: 20 }}>
- Return to Courses
-
-
- );
- }
-
- return (
-
-
}
- style={{ color: '#fff', marginBottom: 20 }}
- onClick={() => navigate('/courses')}
- >
- 返回课程列表
-
-
-
-
-
-
-
- {course.tag && {course.tag} }
-
- {course.course_type_display || (course.course_type === 'hardware' ? '硬件课程' : '软件课程')}
-
-
-
- {course.title}
-
-
- {course.description}
-
-
-
-
-
- 课程信息
-
-
- 讲师}>
-
- {course.instructor_avatar_url && (
-
- )}
-
{course.instructor}
- {course.instructor_title && (
-
- {course.instructor_title}
-
- )}
-
-
- 时长}>
- {course.duration}
-
- 课时}>
- {course.lesson_count} 课时
-
-
-
- {/* 讲师简介 */}
- {course.instructor_desc && (
-
- 讲师简介:
- {course.instructor_desc}
-
- )}
-
-
- {/* 课程详细内容区域 */}
- {course.content && (
-
-
课程大纲与详情
-
- {course.content}
-
-
- )}
-
-
- {course.display_detail_image ? (
-
-
-
- ) : null}
-
-
-
-
-
-
报名咨询
-
-
- {parseFloat(course.price) > 0 ? (
- <>
- ¥{course.price}
- >
- ) : (
- 免费咨询
- )}
-
-
-
}
- style={{
- height: 50,
- background: '#00f0ff',
- borderColor: '#00f0ff',
- color: '#000',
- fontWeight: 'bold',
- fontSize: '16px'
- }}
- onClick={() => setIsModalOpen(true)}
- >
- 立即报名 / 咨询
-
-
- * 提交后我们的顾问将尽快与您联系确认
-
-
-
-
-
-
-
- {/* Enroll Modal */}
-
setIsModalOpen(false)}
- footer={null}
- destroyOnHidden
- >
- 请填写您的联系方式,我们将为您安排课程顾问。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setIsModalOpen(false)}>取消
- 提交报名
-
-
-
-
- );
-};
-
-export default VCCourseDetail;
\ No newline at end of file
diff --git a/frontend/src/pages/VCCourses.jsx b/frontend/src/pages/VCCourses.jsx
deleted file mode 100644
index cd48a70..0000000
--- a/frontend/src/pages/VCCourses.jsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { motion } from 'framer-motion';
-import { Button, Typography, Spin, Row, Col, Empty, Tag } from 'antd';
-import { ReadOutlined, ClockCircleOutlined, UserOutlined, BookOutlined } from '@ant-design/icons';
-import { getVCCourses } from '../api';
-
-const { Title, Paragraph } = Typography;
-
-const VCCourses = () => {
- const [courses, setCourses] = useState([]);
- const [loading, setLoading] = useState(true);
- const navigate = useNavigate();
-
- useEffect(() => {
- const fetchCourses = async () => {
- try {
- const res = await getVCCourses();
- setCourses(res.data);
- } catch (error) {
- console.error("Failed to fetch VC Courses:", error);
- } finally {
- setLoading(false);
- }
- }
- fetchCourses();
- }, []);
-
- if (loading) return
;
-
- return (
-
-
-
- VC CODING COURSES
-
-
- 探索 VB Coding 软件与硬件课程,开启您的编程之旅。
-
-
-
- {courses.length === 0 ? (
-
- 暂无课程内容} />
-
- ) : (
-
- {courses.map((item, index) => (
-
- navigate(`/courses/${item.id}`)}
- style={{ cursor: 'pointer' }}
- >
-
-
- {item.display_cover_image ? (
-
- ) : (
-
- )}
-
- {item.tag && (
- {item.tag}
- )}
-
- {item.course_type_display || (item.course_type === 'hardware' ? '硬件课程' : '软件课程')}
-
-
-
-
-
{item.title}
-
- {item.instructor}
- {item.duration}
- {item.lesson_count} 课时
-
-
{item.description}
-
- 开始学习
-
-
-
-
-
- ))}
-
- )}
-
- {/* 装饰性背景 */}
-
-
-
-
-
- );
-};
-
-export default VCCourses;
diff --git a/frontend/src/pages/activity/Detail.jsx b/frontend/src/pages/activity/Detail.jsx
deleted file mode 100644
index b825744..0000000
--- a/frontend/src/pages/activity/Detail.jsx
+++ /dev/null
@@ -1,334 +0,0 @@
-
-import React, { useState, useEffect } from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { motion, useScroll, useTransform } from 'framer-motion';
-import { ArrowLeftOutlined, ShareAltOutlined, CalendarOutlined, ClockCircleOutlined, EnvironmentOutlined, UserOutlined } from '@ant-design/icons';
-import confetti from 'canvas-confetti';
-import { message, Spin, Button, Result, Modal, Form, Input } from 'antd';
-import { getActivityDetail, signUpActivity } from '../../api';
-import styles from '../../components/activity/activity.module.less';
-import { pageTransition, buttonTap } from '../../animation';
-import LoginModal from '../../components/LoginModal';
-import { useAuth } from '../../context/AuthContext';
-import ReactMarkdown from 'react-markdown';
-import remarkGfm from 'remark-gfm';
-import rehypeRaw from 'rehype-raw';
-import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
-
-const ActivityDetail = () => {
- const { id } = useParams();
- const navigate = useNavigate();
- const queryClient = useQueryClient();
- const { scrollY } = useScroll();
- const { login } = useAuth();
- const [loginVisible, setLoginVisible] = useState(false);
- const [signupFormVisible, setSignupFormVisible] = useState(false);
- const [form] = Form.useForm();
-
- // Header animation: transparent to white with shadow
- const headerBg = useTransform(scrollY, [0, 60], ['rgba(255,255,255,0)', 'rgba(255,255,255,1)']);
- const headerShadow = useTransform(scrollY, [0, 60], ['none', '0 2px 8px rgba(0,0,0,0.1)']);
- const headerColor = useTransform(scrollY, [0, 60], ['rgba(255,255,255,1)', 'rgba(0,0,0,0.85)']);
- const titleOpacity = useTransform(scrollY, [100, 200], [0, 1]);
-
- const { data: activity, isLoading, error, refetch } = useQuery({
- queryKey: ['activity', id],
- queryFn: async () => {
- try {
- const res = await getActivityDetail(id);
- return res.data;
- } catch (err) {
- throw new Error(err.response?.data?.detail || 'Failed to load activity');
- }
- },
- staleTime: 0, // Ensure fresh data
- refetchOnMount: 'always', // Force refetch on mount
- });
-
- //// /
- // Force a refresh if needed (as requested by user)
- useEffect(() => {
- // 1. Force React Query refetch
- refetch();
-
- // 2. Hard refresh logic after 1 second delay
- const timer = setTimeout(() => {
- const hasRefreshedKey = `has_refreshed_activity_${id}`;
- if (!sessionStorage.getItem(hasRefreshedKey)) {
- sessionStorage.setItem(hasRefreshedKey, 'true');
- window.location.reload();
- }
- }, 0);
-
- return () => clearTimeout(timer);
- }, [id, refetch]);
-
- const signUpMutation = useMutation({
- mutationFn: (values) => signUpActivity(id, { signup_info: values || {} }),
- onSuccess: () => {
- message.success('报名成功!');
- setSignupFormVisible(false);
- confetti({
- particleCount: 150,
- spread: 70,
- origin: { y: 0.6 },
- colors: ['#00b96b', '#1890ff', '#ffffff']
- });
- queryClient.invalidateQueries(['activity', id]);
- queryClient.invalidateQueries(['activities']);
- },
- onError: (err) => {
- message.error(err.response?.data?.detail || err.response?.data?.error || '报名失败,请稍后重试');
- }
- });
-
- const handleShare = async () => {
- const url = window.location.href;
- if (navigator.share) {
- try {
- await navigator.share({
- title: activity?.title,
- text: '来看看这个精彩活动!',
- url: url
- });
- } catch (err) {
- console.log('Share canceled');
- }
- } else {
- navigator.clipboard.writeText(url);
- message.success('链接已复制到剪贴板');
- }
- };
-
- const handleSignUp = () => {
- if (!localStorage.getItem('token')) {
- message.warning('请先登录后报名');
- setLoginVisible(true);
- return;
- }
-
- // Check if we need to collect info
- if (activity.signup_form_config && activity.signup_form_config.length > 0) {
- setSignupFormVisible(true);
- } else {
- // Direct signup if no info needed
- signUpMutation.mutate({});
- }
- };
-
- const handleFormSubmit = (values) => {
- signUpMutation.mutate(values);
- };
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (error) {
- return (
-
- navigate(-1)}>
- 返回列表
-
- ]}
- />
-
- );
- }
-
- const cleanUrl = (url) => {
- if (!url) return '';
- return url.replace(/[`\s]/g, '');
- };
-
- return (
-
- {/* Sticky Header */}
-
- navigate(-1)}
- style={{ cursor: 'pointer', color: headerColor, fontSize: 20 }}
- >
-
-
-
- {activity.title}
-
-
-
-
-
-
- {/* Hero Image with LayoutId for shared transition */}
-
-
- {/* Content */}
-
-
-
{activity.title}
-
-
-
-
- {activity.start_time ? new Date(activity.start_time).toLocaleDateString() : 'TBD'}
-
-
-
- {activity.start_time ? new Date(activity.start_time).toLocaleTimeString() : 'TBD'}
-
-
-
- {activity.location || '线上活动'}
-
-
-
- {activity.current_signups || 0} / {activity.max_participants} 已报名
-
-
-
-
-
- {activity.status || (new Date() < new Date(activity.start_time) ? '报名中' : '已结束')}
-
-
-
-
-
-
活动详情
-
-
- {String(children).replace(/\n$/, '')}
-
- ) : (
-
- {children}
-
- )
- }
- }}
- >
- {activity.description || activity.content || '暂无详情描述'}
-
-
-
-
-
- {/* Fixed Footer */}
-
-
- 距离报名截止
-
- {/* Simple countdown placeholder */}
- 3天 12小时
-
-
-
- {signUpMutation.isPending ? '提交中...' : activity.is_signed_up ? '已报名' : '立即报名'}
-
-
-
- setLoginVisible(false)}
- onLoginSuccess={(userData) => {
- login(userData);
- // Auto trigger signup after login if needed, or just let user click again
- }}
- />
-
- setSignupFormVisible(false)}
- onOk={form.submit}
- confirmLoading={signUpMutation.isPending}
- destroyOnHidden
- >
-
-
-
- ))}
-
-
-
- );
-};
-
-export default ActivityDetail;
diff --git a/frontend/src/theme.module.less b/frontend/src/theme.module.less
deleted file mode 100644
index dd7bd4a..0000000
--- a/frontend/src/theme.module.less
+++ /dev/null
@@ -1,69 +0,0 @@
-
-/* Global Theme Variables */
-:global {
- :root {
- /* Colors */
- --primary-color: #00b96b;
- --secondary-color: #1890ff;
- --background-dark: #1f1f1f;
- --background-card: #2a2a2a;
- --text-primary: #ffffff;
- --text-secondary: rgba(255, 255, 255, 0.65);
- --border-color: rgba(255, 255, 255, 0.1);
-
- /* Typography */
- --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
- --font-size-base: 14px;
- --font-size-lg: 16px;
- --font-size-xl: 20px;
-
- /* Layout */
- --border-radius-base: 8px;
- --border-radius-lg: 16px;
-
- /* Spacing */
- --spacing-xs: 4px;
- --spacing-sm: 8px;
- --spacing-md: 16px;
- --spacing-lg: 24px;
-
- /* Shadows */
- --box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15);
- --box-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.25);
- }
-}
-
-/* Mixins (Less Variables for module usage if needed) */
-@primary-color: var(--primary-color);
-@secondary-color: var(--secondary-color);
-@background-dark: var(--background-dark);
-@background-card: var(--background-card);
-@text-primary: var(--text-primary);
-@text-secondary: var(--text-secondary);
-@border-radius-base: var(--border-radius-base);
-
-.glass-panel {
- background: rgba(42, 42, 42, 0.6);
- backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: var(--border-radius-lg);
-}
-
-.section-title {
- font-size: 24px;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: var(--spacing-lg);
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
-
- &::before {
- content: '';
- display: block;
- width: 4px;
- height: 24px;
- background: var(--primary-color);
- border-radius: 2px;
- }
-}