You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js循环渲染MUI TextField 提交取值与状态问题修复

运行效果截图1
运行效果截图2

问题说明

基于Next.js + MUI开发的写作答题页存在两处功能异常:

  • 全局维护单一字数统计状态,切换题目后字数显示沿用上一题的值,无法做到每道题独立统计
  • 题目切换时非当前题的输入框被卸载,已输入内容丢失,且提交时无法聚合所有题目的答案
修复方案

核心改动是将单值的答案、字数状态改为以题目ID为键的对象结构存储,输入框改为受控组件绑定对应状态,既可以保留切换时的输入内容,也能实现单题字数独立统计,提交时直接遍历状态对象即可拿到所有答案。
同时优化了原来硬编码的按钮显隐逻辑,改为根据当前题目索引自动判断按钮展示状态,适配任意数量的题目场景。

修复后完整代码
import { Grid, Typography, Box, NoSsr, TextField } from '@mui/material';
import PersonIcon from '@mui/icons-material/Person';
import Timer from '../../../components/timer';
import Button from '@mui/material/Button';
import { useState } from 'react';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { Scrollbars } from 'react-custom-scrollbars';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import axios from '../../../lib/axios';
import { decode } from 'html-entities';
import { blueGrey } from '@mui/material/colors';

export default function Writing({ questions }) {
    const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
    // 存储所有题目答案,key为题目id,value为输入内容
    const [answers, setAnswers] = useState({});
    // 存储所有题目字数统计,key为题目id,value为对应字数
    const [wordCounts, setWordCounts] = useState({});

    // 输入变化处理
    const handleAnswerChange = (questionId, value) => {
        // 更新对应题目的答案
        setAnswers(prev => ({
            ...prev,
            [questionId]: value
        }));
        // 计算并更新对应题目的字数,空内容时字数为0
        const count = value.trim() ? value.trim().split(/\s+/).length : 0;
        setWordCounts(prev => ({
            ...prev,
            [questionId]: count
        }));
    };

    // 提交处理
    const handleSubmit = () => {
        // 此处可直接拿到所有题目的答案做接口提交
        console.log('所有作答内容:', answers);
        // 示例提交逻辑
        // axios.post('提交地址', { answers })
    };

    return (
        <>
            <Box sx={{ flexGrow: 1 }}>
                <AppBar position="fixed" style={{ background: blueGrey[900] }}>
                    <Toolbar>
                        <Grid container spacing={2} alignItems="center">
                            <Grid item xs={4} display="flex" alignItems="center">
                                <PersonIcon
                                    sx={{ background: '#f2f2f2', borderRadius: '50px' }}
                                />
                                <Typography variant="h6" color="#f2f2f2" ml={1}>
                                    xxxxx xxxxx-1234
                                </Typography>
                            </Grid>
                            <Grid item xs={4} container justifyContent="center">
                                <Timer timeValue={2400} />
                            </Grid>
                            <Grid item xs={4} container justifyContent={'right'}>
                                <Button
                                    variant="contained"
                                    style={{ background: 'white', color: 'black' }}
                                    size="small">
                                    Settings
                                </Button>
                                <Button
                                    variant="contained"
                                    style={{
                                        background: 'white',
                                        color: 'black',
                                        margin: '0px 10px',
                                    }}
                                    size="small">
                                    Hide
                                </Button>
                                <Button
                                    variant="contained"
                                    style={{ background: 'white', color: 'black' }}
                                    size="small">
                                    Help
                                </Button>
                            </Grid>
                        </Grid>
                    </Toolbar>
                </AppBar>
            </Box>

            <Box
                sx={{
                    background: blueGrey[50],
                    height: '100%',
                    width: '100%',
                    position: 'absolute',
                }}
                pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}>
                {questions.map((question, index) =>
                    index === currentQuestionIndex ? (
                        <Box
                            key={question.id}
                            px={3}
                            sx={{ background: '#f2f2f2', pb: 4 }}
                            position={{
                                xs: 'sticky',
                                sm: 'sticky',
                                lg: 'initial',
                                md: 'initial',
                                xl: 'initial',
                            }}>
                            <Box
                                style={{ background: '#f7fcff', borderRadius: '4px' }}
                                py={1}
                                px={2}>
                                <Box>
                                    <Typography variant="h6" component="h6" ml={1}>
                                        Part {question.id}
                                    </Typography>
                                    <Typography variant="subtitle2" component="div" ml={1} mt={1}>
                                        <NoSsr>
                                            <div
                                                dangerouslySetInnerHTML={{
                                                    __html: decode(question.questions[0].question, {
                                                        level: 'html5',
                                                    }),
                                                }}></div>
                                        </NoSsr>
                                    </Typography>
                                </Box>
                            </Box>

                            <Box
                                style={{
                                    background: '#f7fcff',
                                    borderRadius: '4px',
                                    marginBottom: '75px',
                                }}
                                pt={1}
                                px={3}
                                mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}>
                                <Grid container spacing={2}>
                                    <Grid item xs={12} sm={12} lg={6} md={6} xl={6}>
                                        <Box
                                            py={{ lg: 1, md: 1, xl: 1 }}
                                            style={{ height: '50vh' }}>
                                            <Scrollbars universal>
                                                <Typography
                                                    variant="body1"
                                                    component="div"
                                                    style={{ textAlign: 'justify' }}
                                                    mr={2}>
                                                    <NoSsr>
                                                        <div
                                                            dangerouslySetInnerHTML={{
                                                                __html: decode(question.question_text, {
                                                                    level: 'html5',
                                                                }),
                                                            }}></div>
                                                    </NoSsr>
                                                </Typography>
                                            </Scrollbars>
                                        </Box>
                                    </Grid>
                                    <Grid
                                        item
                                        xs={12}
                                        sm={12}
                                        lg={6}
                                        md={6}
                                        xl={6}
                                        mt={{ md: 4, lg: 4, xl: 4 }}>
                                        <TextField
                                            id={`${question.id}`}
                                            label="Type your answer here"
                                            multiline
                                            name={`answer_${question.id}`}
                                            rows={12}
                                            variant="outlined"
                                            fullWidth
                                            // 绑定受控值,从状态中取当前题目的答案
                                            value={answers[question.id] || ''}
                                            helperText={`Words Count: ${wordCounts[question.id] || 0}`}
                                            onChange={(e) => {
                                                handleAnswerChange(question.id, e.target.value);
                                            }}
                                        />
                                    </Grid>
                                </Grid>
                            </Box>
                        </Box>
                    ) : null
                )}

                <Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}>
                    <Grid
                        container
                        style={{ background: blueGrey[300], display: 'flex' }}
                        py={2}
                        px={3}>
                        <Grid
                            item
                            xs={3}
                            sm={3}
                            lg={6}
                            md={6}
                            xl={6}
                            container
                            justifyContent={'start'}>
                            <Button
                                variant="contained"
                                style={{ background: 'white', color: 'black' }}
                                size="small">
                                Save Draft
                            </Button>
                        </Grid>
                        <Grid
                            item
                            xs={9}
                            sm={9}
                            lg={6}
                            md={6}
                            xl={6}
                            container
                            justifyContent={'end'}
                            gap={1}
                        >
                            {/* 上一题按钮:不是第一题时显示 */}
                            {currentQuestionIndex > 0 && (
                                <Button
                                    variant="contained"
                                    style={{
                                        background: 'white',
                                        color: 'black',
                                    }}
                                    startIcon={<ArrowBackIosIcon />}
                                    size="small"
                                    onClick={() => {
                                        setCurrentQuestionIndex(prev => prev - 1);
                                    }}>
                                    Previous Part
                                </Button>
                            )}
                            {/* 下一题按钮:不是最后一题时显示 */}
                            {currentQuestionIndex < questions.length - 1 && (
                                <Button
                                    variant="contained"
                                    size="small"
                                    style={{
                                        background: 'white',
                                        color: 'black',
                                    }}
                                    endIcon={<ArrowForwardIosIcon />}
                                    onClick={() => {
                                        setCurrentQuestionIndex(prev => prev + 1);
                                    }}>
                                    Next Part
                                </Button>
                            )}
                            {/* 提交按钮:最后一题时显示 */}
                            {currentQuestionIndex === questions.length - 1 && (
                                <Button variant="contained" color="success" onClick={handleSubmit}>
                                    Submit Test
                                </Button>
                            )}
                        </Grid>
                    </Grid>
                </Box>
            </Box>
        </>
    );
}

export async function getServerSideProps(context) {
    const { id } = context.query;
    const token = context.req.cookies.token;

    if (!token) {
        context.res.writeHead(302, {
            Location: '/',
        });
        context.res.end();
    }

    const res = await axios.get(`test/${id}/questions`, {
        headers: {
            Authorization: `Bearer ${token}`,
        },
    });
    if (res.data.success) {
        return {
            props: {
                questions: res.data.data.questions,
            },
        };
    }
}
改动点说明
  • 删除了原来冗余的show、show1布尔显隐状态,统一通过currentQuestionIndex判断当前题目和按钮展示逻辑,适配任意题目数量
  • 新增answers对象存储所有题目输入内容,输入框改为受控组件,切换题目时内容持久化保存在状态中不会丢失
  • 新增wordCounts对象按题目ID单独存储字数,每个题目的字数统计独立互不干扰
  • 新增handleSubmit提交方法,可直接获取所有题目的答案用于接口请求
  • 修复了空输入时字数统计为1的边界问题,空内容时字数显示为0

内容的提问来源于stack exchange,提问作者DCodeMania

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 13:51:24