跳到主要内容

前端代码结构

SurveyKing Pro 前端采用 React 18 + TypeScript + Ant Design 技术栈,采用现代化的前端开发架构。

📁 项目目录结构

client/
├── public/ # 静态资源目录
│ ├── favicon.ico # 网站图标
│ ├── logo.png # Logo 图片
│ └── icons/ # 应用图标集合
├── src/ # 源代码目录
│ ├── components/ # 公共组件
│ │ ├── Answer/ # 答题相关组件
│ │ ├── AudioRecord/ # 音频录制组件
│ │ ├── Charts/ # 图表组件
│ │ ├── FormulaEditor/ # 公式编辑器
│ │ ├── HeaderDropdown/ # 头部下拉菜单
│ │ └── ... # 其他组件
│ ├── pages/ # 页面组件
│ │ ├── Survey/ # 问卷相关页面
│ │ ├── Exam/ # 考试相关页面
│ │ ├── Analysis/ # 分析页面
│ │ └── ... # 其他页面
│ ├── services/ # API 服务层
│ │ ├── api.ts # API 接口定义
│ │ ├── survey.ts # 问卷相关服务
│ │ └── user.ts # 用户相关服务
│ ├── store/ # 状态管理
│ │ ├── index.ts # Store 配置
│ │ ├── slices/ # Redux Slices
│ │ └── hooks.ts # 自定义 Hooks
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数
│ ├── constants/ # 常量定义
│ ├── locales/ # 国际化文件
│ ├── layouts/ # 布局组件
│ ├── shared/ # 共享模块
│ ├── app.tsx # 应用入口组件
│ └── app.less # 全局样式
├── package.json # 项目配置
└── tsconfig.json # TypeScript 配置

🧩 核心模块详解

1. 组件模块 (components/)

Answer/ - 答题组件

// 问卷答题核心组件
components/Answer/
├── index.tsx # 答题主组件
├── QuestionTypes/ # 题型组件
│ ├── SingleChoice.tsx # 单选题
│ ├── MultipleChoice.tsx # 多选题
│ ├── TextInput.tsx # 文本输入
│ └── Rating.tsx # 评分题
├── AnswerSheet.tsx # 答题卡
├── SubmitButton.tsx # 提交按钮
└── styles.less # 样式文件

Charts/ - 图表组件

// 数据可视化图表组件
components/Charts/
├── BarChart.tsx # 柱状图
├── PieChart.tsx # 饼图
├── LineChart.tsx # 折线图
├── RadarChart.tsx # 雷达图
└── ChartContainer.tsx # 图表容器

2. 页面模块 (pages/)

Survey/ - 问卷页面

pages/Survey/
├── List/ # 问卷列表
│ ├── index.tsx # 列表主页
│ ├── SurveyCard.tsx # 问卷卡片
│ └── FilterPanel.tsx # 筛选面板
├── Edit/ # 问卷编辑
│ ├── index.tsx # 编辑主页
│ ├── QuestionEditor.tsx # 题目编辑器
│ ├── LogicEditor.tsx # 逻辑编辑器
│ └── PreviewModal.tsx # 预览弹窗
├── Answer/ # 问卷答题
│ ├── index.tsx # 答题主页
│ └── ThankYouPage.tsx # 感谢页面
└── Analysis/ # 数据分析
├── index.tsx # 分析主页
├── StatisticsPanel.tsx # 统计面板
└── ResponseTable.tsx # 回答表格

3. 服务模块 (services/)

API 服务层设计

// services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['Survey', 'User', 'Answer'],
endpoints: (builder) => ({
// 定义端点
}),
});

// services/survey.ts
export const surveyApi = api.injectEndpoints({
endpoints: (builder) => ({
getSurveys: builder.query<Survey[], void>({
query: () => 'surveys',
providesTags: ['Survey'],
}),
getSurvey: builder.query<Survey, string>({
query: (id) => `surveys/${id}`,
providesTags: (result, error, id) => [{ type: 'Survey', id }],
}),
createSurvey: builder.mutation<Survey, Partial<Survey>>({
query: (body) => ({
url: 'surveys',
method: 'POST',
body,
}),
invalidatesTags: ['Survey'],
}),
updateSurvey: builder.mutation<
Survey,
{ id: string; body: Partial<Survey> }
>({
query: ({ id, body }) => ({
url: `surveys/${id}`,
method: 'PUT',
body,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Survey', id }],
}),
}),
});

export const {
useGetSurveysQuery,
useGetSurveyQuery,
useCreateSurveyMutation,
useUpdateSurveyMutation,
} = surveyApi;

4. 状态管理 (store/)

Redux Store 配置

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { api } from '../services/api';
import authSlice from './slices/authSlice';
import uiSlice from './slices/uiSlice';

export const store = configureStore({
reducer: {
auth: authSlice,
ui: uiSlice,
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// store/slices/authSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
}

const initialState: AuthState = {
user: null,
token: localStorage.getItem('token'),
isAuthenticated: false,
};

const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
login: (state, action: PayloadAction<{ user: User; token: string }>) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
localStorage.setItem('token', action.payload.token);
},
logout: (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
localStorage.removeItem('token');
},
},
});

export const { login, logout } = authSlice.actions;
export default authSlice.reducer;

5. 类型定义 (types/)

TypeScript 类型系统

// types/index.ts
export interface User {
id: string;
username: string;
email: string;
role: 'admin' | 'user';
avatar?: string;
createdAt: string;
updatedAt: string;
}

export interface Survey {
id: string;
title: string;
description: string;
status: 'draft' | 'published' | 'closed';
questions: Question[];
settings: SurveySettings;
createdBy: string;
createdAt: string;
updatedAt: string;
}

export interface Question {
id: string;
type: QuestionType;
title: string;
description?: string;
required: boolean;
options?: Option[];
validation?: ValidationRule[];
logic?: LogicRule[];
}

export type QuestionType =
| 'single-choice'
| 'multiple-choice'
| 'text-input'
| 'textarea'
| 'number-input'
| 'date-picker'
| 'rating'
| 'file-upload';

export interface Option {
id: string;
text: string;
value: string;
image?: string;
}

export interface Answer {
id: string;
surveyId: string;
respondentId: string;
answers: Record<string, any>;
submitTime: string;
ip?: string;
userAgent?: string;
}

🔧 开发工具配置

1. ESLint 配置

// .eslintrc.json
{
"extends": [
"react-app",
"react-app/jest",
"@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint", "react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn"
}
}

2. Prettier 配置

// .prettierrc
{
"semi": false,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false
}

3. TypeScript 配置

// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/pages/*": ["pages/*"],
"@/services/*": ["services/*"],
"@/types/*": ["types/*"],
"@/utils/*": ["utils/*"]
}
},
"include": ["src"]
}

🎨 样式管理

1. 全局样式

// app.less
@import '~antd/dist/antd.less';

// 主题变量
@primary-color: #1890ff;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #f5222d;

// 全局样式
.ant-layout {
min-height: 100vh;
}

.content-wrapper {
padding: 24px;
background: #fff;
min-height: 360px;
}

2. 组件样式

// components/Answer/styles.less
.answer-container {
max-width: 800px;
margin: 0 auto;
padding: 24px;

.question-item {
margin-bottom: 32px;

.question-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 12px;

&.required::after {
content: ' *';
color: @error-color;
}
}

.question-options {
.option-item {
margin-bottom: 8px;

&:last-child {
margin-bottom: 0;
}
}
}
}
}

📱 响应式设计

1. 断点设置

// 响应式断点
@screen-xs: 480px;
@screen-sm: 576px;
@screen-md: 768px;
@screen-lg: 992px;
@screen-xl: 1200px;
@screen-xxl: 1600px;

// 媒体查询 mixins
.responsive(@rules) {
@media (max-width: @screen-md) {
@rules();
}
}

2. 组件响应式

// hooks/useResponsive.ts
import { useEffect, useState } from 'react';

export const useResponsive = () => {
const [isMobile, setIsMobile] = useState(false);
const [isTablet, setIsTablet] = useState(false);

useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
setIsTablet(window.innerWidth >= 768 && window.innerWidth < 1024);
};

handleResize();
window.addEventListener('resize', handleResize);

return () => window.removeEventListener('resize', handleResize);
}, []);

return { isMobile, isTablet };
};

🔄 路由管理

1. 路由配置

// routes/index.tsx
import { createBrowserRouter } from 'react-router-dom';
import Layout from '@/layouts/Layout';
import SurveyList from '@/pages/Survey/List';
import SurveyEdit from '@/pages/Survey/Edit';
import SurveyAnswer from '@/pages/Survey/Answer';

export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: 'surveys',
element: <SurveyList />,
},
{
path: 'surveys/:id/edit',
element: <SurveyEdit />,
},
{
path: 'surveys/:id/answer',
element: <SurveyAnswer />,
},
],
},
]);

2. 权限路由

// components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { RootState } from '@/store';

interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
}

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requiredRole,
}) => {
const { isAuthenticated, user } = useSelector(
(state: RootState) => state.auth
);

if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}

if (requiredRole && user?.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}

return <>{children}</>;
};

下一步: 查看 后端代码结构 了解服务端架构设计。