乐于分享
好东西不私藏

用 WPS 多维表格收集学生做题数据,完整教程(附代码)

用 WPS 多维表格收集学生做题数据,完整教程(附代码)

PART 01
这个方案能干什么
举几个场景:
  • 课堂上学生做完习题,答案自动汇总到表格,课后直接分析错题分布
  • 发一份在线调查问卷,回收的数据实时进入 WPS,随时查看统计
  • 收集学生报名信息、作业提交记录,统一管理
核心思路就一句话:用 HTML 页面收集数据,通过脚本自动写入 WPS 多维表格。
PART 02
你需要准备什么
工具
用途
获取方式
WPS 在线文档
存储数据
[kdocs.cn](https://www.kdocs.cn) 免费注册
Node.js
运行本地服务器
[nodejs.org](https://nodejs.org) 下载安装
一份 HTML 文件
学生看到的页面
下文提供完整代码
一份 JS 文件
本地代理服务器
下文提供完整代码
不需要买服务器,不需要域名,不需要任何付费工具。你的电脑就是服务器。
PART 03
整体架构(先看全貌)
学生浏览器
↓ 填写答案,点提交
本地服务器 (server.js)
↓ 转发请求
WPS AirScript API
↓ 执行脚本
WPS 多维表格 ← 数据落库
一共三个环节,每个环节对应一个文件。下面逐一配置。
PART 04
配置 WPS 多维表格
1.1 创建多维表格
打开 [kdocs.cn](https://www.kdocs.cn),新建一个多维表格(不是普通表格)。
1.2 创建 AirScript 脚本
在多维表格中,点击「扩展」→「AirScript」→ 新建脚本。
把下面的代码完整粘贴进去:
  • /*** 字段值类型自动转换*/functionconvertFieldValue(value, fieldType{if (value === null || value === undefined || value === ""returnnull;switch (fieldType) {case'Number':case'Currency':case'Percentage':constnum = Number(value);returnisNaN(num) ? null : num;case'Date':case'CreatedTime':case'LastModifiedTime':try {constdate = newDate(value);returnisNaN(date.getTime()) ? null : date.toLocaleDateString('zh-CN');catch (e) { returnnull; }case'Time':returnString(value);case'Checkbox':case'Complete':returnBoolean(value);case'SingleSelect':returnString(value);case'MultipleSelect':if (Array.isArray(value)) {return value.map(item => String(item));}return [String(value)];case'Phone':case'Email':case'Url':case'MultiLineText':case'Note':case'ID':returnString(value);default:return value;}}/*** 按表名判断:存在则追加数据,不存在则新建表@param {Object} input 输入参数@param {string} input.sheetName 目标表名称(必填)@param {Array} [input.fields] 表字段定义(新建表时必填)@param {Array} input.records 要写入的记录数据(必填)@param {Object} [input.fieldMap] 字段映射@param {Boolean} [input.autoConvertType=true] 是否自动转换字段类型@return {Object} 执行结果*/functionwriteOrCreateSheet(input{if (!input.sheetName || !input.records || !Array.isArray(input.records)) {return { success: false, message: "参数错误:sheetName 和 records 为必填项" };}constfieldMap = input.fieldMap || {};constautoConvertType = input.autoConvertType !== false;try {constallSheets = Application.Sheet.GetSheets();let targetSheet = null;for (let i = 0; i < allSheets.length; i++) {if (allSheets[i].name === input.sheetName) {targetSheet = allSheets[i];break;}}if (targetSheet) {// 表已存在:追加数据constsheetId = targetSheet.id;constexistingFields = Application.Field.GetFields({ SheetId: sheetId });constfieldTypeMap = {};for (let i = 0; i < existingFields.length; i++) {fieldTypeMap[existingFields[i].name] = existingFields[i].type;}constexistingFieldNames = Object.keys(fieldTypeMap);constprocessedRecords = [];for (let i = 0; i < input.records.length; i++) {constrecord = input.records[i];constprocessedFields = {};constfieldKeys = Object.keys(record.fields);for (let j = 0; j < fieldKeys.length; j++) {constinputKey = fieldKeys[j];constvalue = record.fields[inputKey];constactualKey = fieldMap[inputKey] || inputKey;if (existingFieldNames.includes(actualKey)) {processedFields[actualKey] = autoConvertTypeconvertFieldValue(value, fieldTypeMap[actualKey]): value;}}processedRecords.push({ fields: processedFields });}constresult = Application.Record.CreateRecords({SheetId: sheetId,Records: processedRecords});return {success: true,operation: "追加数据",message: "成功向表「" + input.sheetName + "」追加" + result.length + "条数据",data: { sheetId, sheetName: input.sheetName, records: result }};else {// 表不存在:创建新表if (!input.fields || !Array.isArray(input.fields)) {return { success: false, message: "参数错误:表不存在时 fields 为必填项" };}constnewSheet = Application.Sheet.CreateSheet({Name: input.sheetName,Views: [{ name'表格视图'type'Grid' }],Fields: input.fields});constresult = Application.Record.CreateRecords({SheetId: newSheet.id,Records: input.records});return {success: true,operation: "创建新表",message: "成功创建表「" + input.sheetName + "」并写入" + result.length + "条数据",data: { sheetId: newSheet.id, sheetName: input.sheetName, records: result }};}catch (error) {return {success: false,message: "执行错误:" + error.message,error: error.stack};}}// ========== 主入口 ==========try {constinputParams = Context.argv;constexecutionResult = writeOrCreateSheet(inputParams);console.log("执行结果:", JSON.stringify(executionResult, null2));executionResult;catch (globalError) {console.error("脚本全局错误:", globalError);throw globalError;}}
1.3 获取两个关键 ID
保存脚本后,你需要记下两个值:

https://www.kdocs.cn/api/v3/ide/file/517338409857/script/V2-3XvxSAmmiMDu2igftMjoXY/sync_task

  • File ID:在你的 WPS 文件 URL 中能找到。那 File ID 就是 `517338409857`。也可以在 AirScript 编辑器中查看。
  • Script ID:在 AirScript 编辑器中,点击脚本名称旁边的设置图标可以看到”V2-3XvxSAmmiMDu2igftMjoXY“。
  • API Token:在 AirScript 编辑器中,点击「发布」→「API 调用」,会生成一个令牌。
把这三个值记下来,后面要用。
PART 05
创建本地服务器
在电脑上新建一个文件夹,比如叫 `data-collector`。在里面创建一个文件 `server.js`,粘贴以下代码:
//这个文件可以发给AI,然后让ai生成你所需要的。const http = require('http');const https = require('https');const fs = require('fs');const path = require('path');// ========================================// 配置区(只需修改这里)// ========================================const WPS_CONFIG = {fileId'你的File ID',        // ← 替换scriptId'你的Script ID',    // ← 替换apiToken'你的API Token',    // ← 替换baseUrl'https://www.kdocs.cn/api/v3'};const PORT = 3000;const HTML_FILE = path.join(__dirname, 'index.html');// ========================================// 服务器代码(不用改)// ========================================const server = http.createServer((req, res) => {res.setHeader('Access-Control-Allow-Origin''*');res.setHeader('Access-Control-Allow-Methods''GET, POST, OPTIONS');res.setHeader('Access-Control-Allow-Headers''Content-Type');if (req.method === 'OPTIONS') {res.writeHead(204);res.end();return;}// 提供网页if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {fs.readFile(HTML_FILE'utf-8'(err, content) => {if (err) {res.writeHead(500);res.end('HTML 文件未找到,请确保 index.html 与 server.js 在同一目录');return;}res.writeHead(200, { 'Content-Type''text/html; charset=utf-8' });res.end(content);});return;}// 代理 API 请求if (req.method === 'POST' && req.url === '/api/submit') {let body = '';req.on('data'chunk => { body += chunk; });req.on('end'() => {try {const data = JSON.parse(body);console.log('收到提交:', data.className'-', data.studentName);const wpsUrl = WPS_CONFIG.baseUrl'/ide/file/' + WPS_CONFIG.fileId'/script/' + WPS_CONFIG.scriptId'/sync_task';const wpsPayload = {Context: {argv: {sheetName: data.sheetName || '数据收集',records: [{fields: data.record}],fields: data.fields || null}}};const postData = JSON.stringify(wpsPayload);const urlObj = new URL(wpsUrl);const wpsReq = https.request({hostname: urlObj.hostname,port443,path: urlObj.pathname,method'POST',headers: {'Content-Type''application/json','AirScript-Token'WPS_CONFIG.apiToken,'Content-Length'Buffer.byteLength(postData)}}, (wpsRes) => {let wpsBody = '';wpsRes.on('data'chunk => { wpsBody += chunk; });wpsRes.on('end'() => {console.log('WPS 响应:', wpsRes.statusCode, wpsBody);res.writeHead(wpsRes.statusCode, { 'Content-Type''application/json' });res.end(wpsBody);});});wpsReq.on('error'(err) => {console.error('请求失败:', err.message);res.writeHead(500, { 'Content-Type''application/json' });res.end(JSON.stringify({ error: err.message }));});wpsReq.write(postData);wpsReq.end();catch (err) {res.writeHead(400, { 'Content-Type''application/json' });res.end(JSON.stringify({ error'请求数据格式错误' }));}});return;}res.writeHead(404);res.end('Not Found');});server.listen(PORT() => {console.log('');console.log('  服务已启动!');console.log('  打开浏览器访问: http://localhost:' + PORT);console.log('  按 Ctrl+C 停止');console.log('');});
只需要改配置区的三个值,其他代码不用动。
PART 06
创建前端页面
在同一个文件夹里创建 `index.html`。下面是一个习题测试的完整示例,你可以根据自己的需求修改题目内容和字段:这些也都可以让AI生成你所需的。不知道怎么写提示词,就把这部分内容复制给AI,再让AI生成你想要的内容
<!DOCTYPE html><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>在线习题测试</title><p>正在提交...</p></div></div><script>// ========================================// 提交数据到本地服务器(服务器再转发到 WPS)// ========================================function submitToBackend(data) {return new Promise(function(resolve, reject) {var xhr = new XMLHttpRequest();xhr.addEventListener('readystatechange'function() {if (this.readyState === this.DONE) {if (this.status >= 200 && this.status < 300) {try { resolve(JSON.parse(this.responseText)); }catch(e) { resolve(this.responseText); }else {reject(new Error('HTTP ' + this.status));}}});xhr.open('POST''/api/submit');xhr.setRequestHeader('Content-Type''application/json');xhr.send(JSON.stringify(data));});}// ========================================// 收集表单数据// ========================================function collectFormData() {var className = document.getElementById('className').value.trim();var studentName = document.getElementById('studentName').value.trim();if (!className || !studentName) {showToast('请填写班级和姓名''warning');return null;}var answers = {};for (var i = 1; i <= 8; i++) {var sel = document.querySelector('input[name="q' + i + '"]:checked');answers['q' + i] = sel ? sel.value : '未作答';}answers['q9'] = document.getElementById('q9').value.trim() || '未作答';answers['q10'] = document.getElementById('q10').value.trim() || '未作答';return { className: className, studentName: studentName, answers: answers };}// ========================================// 提交(带防重复提交锁)// ========================================var isSubmitting = false;async function handleSubmit() {if (isSubmitting) return;isSubmitting = true;var data = collectFormData();if (!data) { isSubmitting = falsereturn; }var unanswered = Object.values(data.answers).filter(function(a) { return a === '未作答'; }).length;if (unanswered > 0) {if (!confirm('你还有 ' + unanswered + ' 道题未作答,确定要提交吗?')) {isSubmitting = falsereturn;}}var btn = document.getElementById('submitBtn');btn.disabled = true;document.getElementById('loading').classList.add('show');// 构造发给后端的数据(后端会转换为 WPS 要求的格式)var payload = {sheetName'质量和密度测试',record: {'班级': data.className,'姓名': data.studentName,'第1题': data.answers.q1,'第2题': data.answers.q2,'第3题': data.answers.q3,'第4题': data.answers.q4,'第5题': data.answers.q5,'第6题': data.answers.q6,'第7题': data.answers.q7,'第8题': data.answers.q8,'第9题': data.answers.q9,'第10题': data.answers.q10,'提交时间'new Date().toLocaleString('zh-CN')},fields: [name'班级'type'MultiLineText' },name'姓名'type'MultiLineText' },name'第1题'type'MultiLineText' },name'第2题'type'MultiLineText' },name'第3题'type'MultiLineText' },name'第4题'type'MultiLineText' },name'第5题'type'MultiLineText' },name'第6题'type'MultiLineText' },name'第7题'type'MultiLineText' },name'第8题'type'MultiLineText' },name'第9题'type'MultiLineText' },name'第10题'type'MultiLineText' },name'提交时间'type'MultiLineText' }]};try {var result = await submitToBackend(payload);console.log('提交结果:', result);showToast('✅ 提交成功!''success');btn.textContent = '✅ 已提交';catch (error) {console.error('提交失败:', error);showToast('❌ 提交失败:' + error.message'error');btn.disabled = false;isSubmitting = false;finally {document.getElementById('loading').classList.remove('show');}}function showToast(message, type) {var toast = document.getElementById('toast');toast.textContent = message;toast.className = 'toast ' + type + ' show';setTimeout(function() { toast.classList.remove('show'); }, 3000);}</script></body></html>
PART 07
启动运行
确保文件夹里有这两个文件:
data-collector/
├── server.js      ← 后端(改好配置)
└── index.html     ← 前端(改好题目)
打开终端(Windows 按 `Win+R` 输入 `cmd`,Mac 打开「终端」),进入这个文件夹:
cd data-collector
node server.js
看到「服务已启动」后,浏览器打开http://localhost:3000
让学生在同一局域网内访问 `http://你的电脑IP:3000` 即可(比如 `http://192.168.1.100:3000`)。
PART 08
数据在哪里看
学生提交后,回到你的 WPS 多维表格,会自动多出一个叫「质量和密度测试」的数据表。每行就是一条提交记录。不同的测试,你都只要再前端改下就可以了,不需要再在WPS里修改了。不用每次都来回写。
你可以直接在 WPS 里做数据分析了——筛选、排序、统计,都是现成的功能。
这里你可以根据自己的需求,设置仪表盘,可以做成数据大屏那种。
PART 09
想改成自己的问卷/测试?
只需要改 `index.html` 里的两部分:
  1. 题目内容:把 `<div class=”question”>` 里的文字换成你的题目
  2. 字段定义:把 `payload.fields` 和 `payload.record` 里的字段名换成你需要的
`server.js` 和 WPS 脚本不用改,它们是通用的。
PART 10
常见问题
Q: 学生用自己的手机能访问吗?
A: 可以。确保手机和你的电脑在同一个 WiFi 下,用你电脑的局域网 IP 访问即可。在终端输入 `ipconfig`(Windows)或 `ifconfig`(Mac)查看 IP。
Q: 关掉终端后数据还在吗?
A: 在。数据已经写入 WPS 云端,关掉终端只影响网页访问,不影响已提交的数据。
Q: 能同时收集多个班级的数据吗?
A: 可以。每个学生提交时都带了班级字段,在 WPS 表格里按班级筛选即可。
Q: 提交出错怎么办?
A: 先检查 `server.js` 里的 File ID、Script ID、API Token 是否正确。再确认 WPS 多维表格里的脚本已保存并发布了 API。