11 #13

Merged
pamj3bxuk merged 2 commits from branc_yz into main 1 year ago

@ -1,80 +1,150 @@
// 引入 `once` 模块,`once` 函数通常用于确保一个回调函数只会被调用一次,
// 在后续代码中可能用于避免多次触发相同的回调逻辑,保证事件处理的唯一性。
var once = require('once');
// 定义一个空函数 `noop`,它不执行任何实际操作,在代码中可能作为默认的回调占位符使用,
// 当需要一个空的函数回调,但又不想传入 `null` 或 `undefined` 时可以用它。
var noop = function() {};
// 定义一个函数 `isRequest`,用于判断传入的 `stream` 参数是否代表一个请求相关的流对象。
// 判断依据是流对象是否具有 `setHeader` 方法并且 `abort` 属性是一个函数,
// 通常符合这种特征的流对象可能是与 HTTP 请求等相关的可操作流。
var isRequest = function(stream) {
return stream.setHeader && typeof stream.abort === 'function';
};
// 定义一个函数 `isChildProcess`,用于判断传入的 `stream` 参数是否代表一个子进程相关的流对象。
// 判断条件是流对象具有 `stdio` 属性,且 `stdio` 是一个长度为3的数组
// 这符合 Node.js 中子进程标准输入、输出、错误流的一种常见表示形式。
var isChildProcess = function(stream) {
return stream.stdio && Array.isArray(stream.stdio) && stream.stdio.length === 3
return stream.stdio && Array.isArray(stream.stdio) && stream.stdio.length === 3;
};
// 定义 `eos` 函数,它主要用于处理流(`stream`)的结束相关逻辑,比如流在可读、可写结束,
// 或者出现错误、关闭等情况时执行相应的回调函数,并提供了取消监听等相关功能。
var eos = function(stream, opts, callback) {
// 如果传入的 `opts` 参数是一个函数,说明可能只传入了回调函数而省略了 `opts` 对象,
// 则将当前的 `opts` 当作 `null`,并把传入的函数当作回调函数重新调用 `eos` 函数进行处理。
if (typeof opts === 'function') return eos(stream, null, opts);
// 如果没有传入 `opts` 参数,则初始化它为空对象,确保后续使用时有默认值可参考。
if (!opts) opts = {};
// 使用 `once` 函数包装传入的回调函数(如果没有传入回调函数,则使用之前定义的 `noop` 空函数作为默认),
// 保证回调函数只会被执行一次,避免重复触发带来的问题。
callback = once(callback || noop);
// 获取流对象的可写状态对象,通常包含了流可写相关的一些属性,比如缓冲区状态等,
// 后续可根据这个状态对象判断流的可写情况以及进行相关逻辑处理。
var ws = stream._writableState;
// 获取流对象的可读状态对象,类似地,它包含了流可读相关的属性,如已读取的数据量、是否结束等信息,
// 用于判断流的可读状态及后续逻辑处理。
var rs = stream._readableState;
var readable = opts.readable || (opts.readable !== false && stream.readable);
var writable = opts.writable || (opts.writable !== false && stream.writable);
var cancelled = false;
// 根据传入的 `opts.readable` 属性或者流本身的可读属性(如果 `opts.readable` 未明确设置为 `false`)来确定 `readable` 的值,
// 表示流是否处于可读状态,用于后续判断流结束等相关逻辑。
var readable = opts.readable || (opts.readable!== false && stream.readable);
// 按照类似的规则确定 `writable` 的值,即根据 `opts.writable` 属性或者流本身的可写属性(若 `opts.writable` 未设为 `false`)来判断流是否可写,
// 用于后续相关逻辑判断。
var writable = opts.writable || (opts.writable!== false && stream.writable);
// 定义一个变量 `cancelled`,用于标记是否取消了相关的事件监听,初始值为 `false`
// 后续在取消操作时会将其设置为 `true`。
// 定义 `onlegacyfinish` 函数,它作为一个事件处理回调函数,用于处理旧版本流(`legacy streams`)的 `end` 或 `close` 事件。
// 如果流已经不可写了,就调用 `onfinish` 函数,进行后续的结束相关处理。
var onlegacyfinish = function() {
if (!stream.writable) onfinish();
};
// 定义 `onfinish` 函数,用于处理流可写部分结束的情况。
// 首先将 `writable` 设置为 `false`,表示可写状态结束,然后如果流已经不可读了,就调用传入的 `callback` 回调函数,
// 传递 `stream` 作为 `this` 上下文,触发相应的业务逻辑(比如通知外部流的可写部分已完成等)。
var onfinish = function() {
writable = false;
if (!readable) callback.call(stream);
};
// 定义 `onend` 函数,用于处理流可读部分结束的情况。
// 将 `readable` 设置为 `false`,表示可读状态结束,接着如果流已经不可写了,就调用 `callback` 回调函数,
// 同样传递 `stream` 作为 `this` 上下文,执行相应的逻辑(例如通知外部流的可读部分已完成等)。
var onend = function() {
readable = false;
if (!writable) callback.call(stream);
};
// 定义 `onexit` 函数,用于处理子进程流(通过 `isChildProcess` 判断)退出的情况。
// 根据子进程的退出码(`exitCode`)来决定回调函数 `callback` 的参数,如果退出码存在(表示有错误),
// 则传入一个带有错误信息的 `Error` 对象作为回调函数的参数,否则传入 `null`,以此来通知外部子进程的退出状态。
var onexit = function(exitCode) {
callback.call(stream, exitCode ? new Error('exited with error code: ' + exitCode) : null);
callback.call(stream, exitCode? new Error('exited with error code: ' + exitCode) : null);
};
// 定义 `onerror` 函数,用于处理流发生错误的情况,直接将错误对象 `err` 作为参数传递给 `callback` 回调函数,
// 使得外部可以知晓流出现了错误并进行相应的处理(比如记录日志、进行错误提示等)。
var onerror = function(err) {
callback.call(stream, err);
};
// 定义 `onclose` 函数,用于处理流关闭的情况。
// 它会在下一个事件循环(`process.nextTick`)中调用 `onclosenexttick` 函数,这样做可以确保一些异步操作的顺序和时机控制,
// 避免在关闭事件触发时立即执行可能还未准备好的逻辑。
var onclose = function() {
process.nextTick(onclosenexttick);
};
// 定义 `onclosenexttick` 函数,用于在流关闭后的下一个事件循环中进行更细致的判断和处理。
// 如果已经取消了相关操作(`cancelled` 为 `true`),则直接返回,不进行后续处理。
// 如果流处于可读状态且可读状态对象存在,并且可读状态不是正常结束(`rs.ended` 为 `false` 或者 `rs.destroyed` 为 `true`
// 则调用 `callback` 回调函数并传入一个表示过早关闭的 `Error` 对象,提示外部流关闭不符合预期(可读部分还未正常结束)。
// 同理,如果流处于可写状态且可写状态对象存在,并且可写状态不是正常结束(`ws.ended` 为 `false` 或者 `ws.destroyed` 为 `true`
// 也调用 `callback` 回调函数并传入相应的错误对象,提示可写部分过早关闭。
var onclosenexttick = function() {
if (cancelled) return;
if (readable && !(rs && (rs.ended && !rs.destroyed))) return callback.call(stream, new Error('premature close'));
if (writable && !(ws && (ws.ended && !ws.destroyed))) return callback.call(stream, new Error('premature close'));
if (readable &&!(rs && (rs.ended &&!rs.destroyed))) return callback.call(stream, new Error('premature close'));
if (writable &&!(ws && (ws.ended &&!ws.destroyed))) return callback.call(stream, new Error('premature close'));
};
// 定义 `onrequest` 函数,用于在请求相关流(通过 `isRequest` 判断)的请求对象(`stream.req`)上监听 `finish` 事件,
// 当请求完成时调用 `onfinish` 函数进行后续处理。
var onrequest = function() {
stream.req.on('finish', onfinish);
};
// 根据流对象的类型进行不同的事件监听设置。
// 如果流对象是请求相关的流(通过 `isRequest` 判断为真),则进行以下事件监听:
// 监听 `complete` 事件,当请求完成时调用 `onfinish` 函数;
// 监听 `abort` 事件,当请求被中止时调用 `onclose` 函数;
// 如果流对象有请求对象(`stream.req`),则调用 `onrequest` 函数进行 `finish` 事件监听,
// 否则监听 `request` 事件,当有请求时调用 `onrequest` 函数进行后续处理。
if (isRequest(stream)) {
stream.on('complete', onfinish);
stream.on('abort', onclose);
if (stream.req) onrequest();
else stream.on('request', onrequest);
} else if (writable && !ws) { // legacy streams
} else if (writable &&!ws) { // legacy streams
// 如果流对象是旧版本的可写流(可写但没有可写状态对象,通过 `writable &&!ws` 判断),
// 则监听 `end` 和 `close` 事件,当这些事件触发时调用 `onlegacyfinish` 函数进行处理。
stream.on('end', onlegacyfinish);
stream.on('close', onlegacyfinish);
}
// 如果流对象是子进程相关的流(通过 `isChildProcess` 判断为真),则监听 `exit` 事件,
// 当子进程退出时调用 `onexit` 函数进行相应的处理,通知外部子进程的退出状态。
if (isChildProcess(stream)) stream.on('exit', onexit);
// 无论流对象属于哪种类型,都统一监听以下一些通用的事件:
// 监听 `end` 事件,当可读部分结束时调用 `onend` 函数进行处理;
// 监听 `finish` 事件,当可写部分结束时调用 `onfinish` 函数进行处理;
// 如果 `opts.error` 没有明确设置为 `false`(默认情况下),则监听 `error` 事件,
// 当流出现错误时调用 `onerror` 函数传递错误信息给回调函数;
// 监听 `close` 事件,当流关闭时调用 `onclose` 函数进行后续处理。
stream.on('end', onend);
stream.on('finish', onfinish);
if (opts.error !== false) stream.on('error', onerror);
if (opts.error!== false) stream.on('error', onerror);
stream.on('close', onclose);
// 返回一个函数,这个函数用于取消之前对各种事件的监听操作。
// 当调用这个返回的函数时,会将 `cancelled` 标记设置为 `true`,表示已取消监听,
// 然后依次移除之前添加的各个事件监听器,避免后续不必要的事件触发和逻辑执行,
// 比如移除对 `complete`、`abort`、`request`、`finish`、`exit`、`end`、`error`、`close` 等事件的监听器。
return function() {
cancelled = true;
stream.removeListener('complete', onfinish);
@ -91,4 +161,5 @@ var eos = function(stream, opts, callback) {
};
};
module.exports = eos;
// 将 `eos` 函数作为模块的导出内容,使得其他模块可以通过引入该模块来使用 `eos` 函数处理流的结束相关逻辑。
module.exports = eos;

@ -1,57 +1,110 @@
// 启用严格模式在严格模式下JavaScript 代码会遵循更严格的语法和行为规范,有助于发现更多潜在的错误并提升代码质量。
"use strict";
// 引入 `es5-ext/array/#/clear` 模块,可能用于清空数组相关操作,从模块路径推测它与扩展的数组功能相关。
// 这里将引入的模块赋值给 `clear` 变量,以便后续在代码中使用对应的功能。
var clear = require("es5-ext/array/#/clear")
, assign = require("es5-ext/object/assign")
, callable = require("es5-ext/object/valid-callable")
, value = require("es5-ext/object/valid-value")
, d = require("d")
, autoBind = require("d/auto-bind")
, Symbol = require("es6-symbol");
// 引入 `es5-ext/object/assign` 模块,通常用于将多个对象的属性合并到一个目标对象上,类似 `Object.assign` 的功能,
// 但可能是经过扩展或具有特定兼容性处理的版本,赋值给 `assign` 变量供后续使用。
, assign = require("es5-ext/object/assign")
// 引入 `es5-ext/object/valid-callable` 模块,可能用于验证某个值是否为可调用的(比如函数),保障后续代码中对可调用对象的使用安全,
// 并将其赋值给 `callable` 变量。
, callable = require("es5-ext/object/valid-callable")
// 引入 `es5-ext/object/valid-value` 模块,大概是用于验证某个值是否符合特定的有效性要求,具体取决于该模块内部实现,
// 赋值为 `value` 变量供代码中进行值的有效性判断使用。
, value = require("es5-ext/object/valid-value")
// 引入 `d` 模块,从代码使用情况看,它可能是一个用于创建具有特定属性描述符的对象属性的工具函数(类似 `Object.defineProperty` 的简化或扩展用法),
// 具体功能需看其内部实现细节,赋值给 `d` 变量以便后续使用。
, d = require("d")
// 引入 `d/auto-bind` 模块,推测是用于自动绑定函数上下文(比如将函数的 `this` 指向特定对象)的功能模块,
// 赋值给 `autoBind` 变量用于后续相关操作。
, autoBind = require("d/auto-bind")
// 引入 `es6-symbol` 模块,用于在 JavaScript 中支持 `Symbol` 类型,`Symbol` 常用于创建唯一的、不可变的标识符,
// 在这里用于定义一些特殊的属性键等情况,赋值给 `Symbol` 变量供代码使用。
, Symbol = require("es6-symbol");
// 获取 `Object.defineProperty` 和 `Object.defineProperties` 方法的引用,分别用于定义单个对象属性和多个对象属性,
// 后续会使用它们来创建具有特定行为和属性描述符的对象属性,确保对象属性的正确设置和访问控制等。
var defineProperty = Object.defineProperty, defineProperties = Object.defineProperties, Iterator;
// 将 `Iterator` 函数作为模块的导出内容,同时在当前模块内也使用 `Iterator` 变量来指代这个函数,
// 该函数用于创建一个迭代器对象,用于遍历某个列表(`list`)并处理相关的更新操作(如添加、删除、清空元素等情况)。
module.exports = Iterator = function (list, context) {
// 检查是否使用 `new` 关键字来调用 `Iterator` 构造函数,如果不是,则抛出一个 `TypeError` 异常,
// 这是遵循构造函数的常规使用规范,确保对象实例化的正确方式。
if (!(this instanceof Iterator)) throw new TypeError("Constructor requires 'new'");
// 使用 `Object.defineProperties` 方法为当前实例(`this`)定义三个属性:`__list__`、`__context__` 和 `__nextIndex__`。
// 通过 `d` 函数(具体功能由其模块实现决定)来设置属性的描述符,使其具有特定的可写性(`"w"` 表示可写)等特征。
// `__list__` 属性存储传入的要迭代的列表数据(通过 `value` 函数验证其有效性后赋值),
// `__context__` 属性存储传入的上下文对象(如果有的话),`__nextIndex__` 属性初始化为 `0`,用于记录迭代的位置索引。
defineProperties(this, {
__list__: d("w", value(list)),
__context__: d("w", context),
__nextIndex__: d("w", 0)
});
// 如果没有传入上下文对象(`context` 为 `null` 或 `undefined`),则直接返回,不进行后续与上下文相关的操作(比如监听上下文事件等)。
if (!context) return;
// 使用 `callable` 函数验证上下文对象的 `on` 方法是否是可调用的(即是否为函数),确保后续可以安全地进行事件监听绑定操作。
callable(context.on);
// 在上下文对象上监听 `_add` 事件,当该事件触发时,调用当前迭代器实例的 `_onAdd` 方法进行处理,
// 这样迭代器可以根据列表元素添加的情况更新自身的迭代状态等信息。
context.on("_add", this._onAdd);
// 类似地,监听 `_delete` 事件,触发时调用 `_onDelete` 方法,用于处理列表元素删除时迭代器的相应调整。
context.on("_delete", this._onDelete);
// 监听 `_clear` 事件,触发时调用 `_onClear` 方法,以应对列表被清空的情况对迭代器状态进行处理。
context.on("_clear", this._onClear);
};
// Internal %IteratorPrototype% doesn't expose its constructor
// 删除 `Iterator` 函数原型对象上的 `constructor` 属性,这样在使用迭代器实例时,不会默认暴露其构造函数引用,
// 可能是出于封装或避免不必要的外部访问构造函数的考虑。
delete Iterator.prototype.constructor;
// 使用 `Object.defineProperties` 方法为 `Iterator` 函数的原型对象(`Iterator.prototype`)定义多个属性和方法,
// 通过 `assign` 函数将多个属性描述对象合并后进行定义,实现了迭代器的核心功能以及相关的辅助方法。
defineProperties(
Iterator.prototype,
assign(
{
// `_next` 方法是迭代器内部用于获取下一个元素索引的私有方法(以下划线开头通常表示私有方法,虽然 JavaScript 并没有真正的私有方法概念,但这是一种约定)。
// 它的逻辑如下:
// 如果 `__list__` 属性不存在(可能表示列表已经被清空或者未正确初始化等情况),则返回 `undefined`,表示迭代结束。
// 如果存在 `__redo__` 属性(这个属性可能用于记录需要重新处理的索引情况,具体看其使用场景),则从 `__redo__` 数组中取出第一个元素作为索引 `i`
// 如果取出的元素不为 `undefined`,则返回该索引值,表示下一个要处理的元素索引;否则继续后续判断。
// 如果当前的 `__nextIndex__` 小于列表的长度(即还有未迭代的元素),则返回当前 `__nextIndex__` 的值,并将其自增 `1`,用于指向下一个待迭代元素的索引。
// 如果以上条件都不满足,说明迭代已经完成,调用 `_unBind` 方法进行一些清理和解除绑定操作,然后返回 `undefined`,表示迭代结束。
_next: d(function () {
var i;
if (!this.__list__) return undefined;
if (this.__redo__) {
i = this.__redo__.shift();
if (i !== undefined) return i;
if (i!== undefined) return i;
}
if (this.__nextIndex__ < this.__list__.length) return this.__nextIndex__++;
this._unBind();
return undefined;
}),
// `next` 方法是符合迭代器协议的公开方法,用于获取下一个迭代元素的结果对象。
// 它调用 `_next` 方法获取下一个元素的索引,然后将索引传递给 `_createResult` 方法来创建并返回包含 `done`(表示是否迭代完成)和 `value`(迭代元素的值)属性的结果对象。
next: d(function () {
return this._createResult(this._next());
}),
// `_createResult` 方法用于根据传入的索引值创建迭代结果对象。
// 如果索引值为 `undefined`,表示迭代已经完成,返回一个 `{ done: true, value: undefined }` 的对象,符合迭代器协议中迭代结束的表示形式。
// 如果索引值不为 `undefined`,则表示还有元素可迭代,通过调用 `_resolve` 方法获取对应索引位置的元素值,并返回一个 `{ done: false, value: 元素值 }` 的对象,
// 表示迭代未结束且包含当前迭代元素的值。
_createResult: d(function (i) {
if (i === undefined) return { done: true, value: undefined };
return { done: false, value: this._resolve(i) };
}),
// `_resolve` 方法用于根据索引值从 `__list__` 列表中获取对应的元素,简单地返回 `__list__` 数组中指定索引位置的元素,
// 是获取迭代元素实际值的一个辅助方法。
_resolve: d(function (i) {
return this.__list__[i];
}),
// `_unBind` 方法用于解除迭代器与相关对象的绑定关系以及进行一些清理操作。
// 首先将 `__list__` 属性设置为 `null`,表示不再关联列表数据,然后删除 `__redo__` 属性(如果存在)。
// 如果存在 `__context__` 属性(即有绑定的上下文对象),则在上下文对象上移除对 `_add`、`_delete`、`_clear` 事件的监听(通过 `off` 方法,与之前的 `on` 方法对应),
// 最后将 `__context__` 属性也设置为 `null`,彻底解除与上下文的关联,完成清理和解除绑定的过程。
_unBind: d(function () {
this.__list__ = null;
delete this.__redo__;
@ -61,11 +114,22 @@ defineProperties(
this.__context__.off("_clear", this._onClear);
this.__context__ = null;
}),
// `toString` 方法用于返回迭代器对象的字符串表示形式,遵循 `[object 类型名称]` 的格式,
// 如果对象定义了 `Symbol.toStringTag` 属性,则使用该属性的值作为类型名称,否则使用 `Object` 作为默认类型名称。
toString: d(function () {
return "[object " + (this[Symbol.toStringTag] || "Object") + "]";
})
},
// 使用 `autoBind` 函数(来自 `d/auto-bind` 模块)对 `_onAdd`、`_onDelete`、`_onClear` 这几个方法进行自动绑定操作,
// 使得这些方法内部的 `this` 指针始终指向当前迭代器实例,方便在事件触发等场景下正确访问和操作迭代器的属性和方法。
autoBind({
// `_onAdd` 方法用于处理列表中元素添加的情况,它接收添加元素的索引 `index` 作为参数,逻辑如下:
// 如果添加元素的索引大于等于当前迭代器的 `__nextIndex__`(表示添加的元素在当前迭代位置之后,不会影响当前迭代过程),则直接返回,不做处理。
// 如果添加元素的索引小于 `__nextIndex__`,说明添加的元素影响了当前的迭代顺序,需要调整迭代器的状态,将 `__nextIndex__` 自增 `1`,表示下一个迭代元素的索引往后移一位。
// 如果不存在 `__redo__` 属性(可能之前没有需要重新处理的索引情况),则创建一个新的 `__redo__` 属性,它是一个数组,并将添加元素的索引 `index` 放入数组中,
// 这样后续迭代时可以根据这个数组来重新处理添加元素后的情况。
// 如果已经存在 `__redo__` 属性,则遍历 `__redo__` 数组,对于每个元素 `redo`,如果它大于等于添加元素的索引 `index`,则将其自增 `1`
// 表示这些需要重新处理的索引位置也需要往后移一位,以适应新添加的元素,最后将添加元素的索引 `index` 也添加到 `__redo__` 数组末尾。
_onAdd: d(function (index) {
if (index >= this.__nextIndex__) return;
++this.__nextIndex__;
@ -78,17 +142,27 @@ defineProperties(
}, this);
this.__redo__.push(index);
}),
// `_onDelete` 方法用于处理列表中元素删除的情况,接收要删除元素的索引 `index` 作为参数,逻辑如下:
// 如果要删除元素的索引大于等于当前迭代器的 `__nextIndex__`(表示删除的元素在当前迭代位置之后,不影响当前迭代过程),则直接返回,不做处理。
// 如果要删除元素的索引小于 `__nextIndex__`,说明删除的元素影响了当前的迭代顺序,需要调整迭代器的状态,将 `__nextIndex__` 自减 `1`,表示下一个迭代元素的索引往前移一位。
// 如果不存在 `__redo__` 属性(即之前没有需要重新处理的索引情况),则直接返回,因为没有需要调整的额外索引记录。
// 如果存在 `__redo__` 属性,则先查找要删除元素的索引 `index` 在 `__redo__` 数组中的位置 `i`,如果找到(`i` 不为 `-1`),则从数组中删除该元素(通过 `splice` 方法)。
// 接着遍历 `__redo__` 数组,对于每个元素 `redo`,如果它大于要删除元素的索引 `index`,则将其自减 `1`
// 表示这些需要重新处理的索引位置需要往前移一位,以适应元素被删除后的情况。
_onDelete: d(function (index) {
var i;
if (index >= this.__nextIndex__) return;
--this.__nextIndex__;
if (!this.__redo__) return;
i = this.__redo__.indexOf(index);
if (i !== -1) this.__redo__.splice(i, 1);
if (i!== -1) this.__redo__.splice(i, 1);
this.__redo__.forEach(function (redo, j) {
if (redo > index) this.__redo__[j] = --redo;
}, this);
}),
// `_onClear` 方法用于处理列表被清空的情况,逻辑如下:
// 如果存在 `__redo__` 属性(表示之前有需要重新处理的索引情况),则调用 `clear` 函数(来自 `es5-ext/array/#/clear` 模块,用于清空数组)清空 `__redo__` 数组。
// 最后将 `__nextIndex__` 属性重置为 `0`,表示迭代器回到初始状态,等待新的列表数据进行迭代。
_onClear: d(function () {
if (this.__redo__) clear.call(this.__redo__);
this.__nextIndex__ = 0;
@ -97,6 +171,8 @@ defineProperties(
)
);
// 使用 `Object.defineProperty` 方法在 `Iterator` 函数的原型对象上定义一个 `Symbol.iterator` 属性,
// 其值是一个函数,该函数直接返回当前迭代器对象本身,这符合迭代器协议的要求,使得该迭代器对象可以被用于 `for...of` 循环等支持迭代器的语法结构中。
defineProperty(
Iterator.prototype,
Symbol.iterator,

@ -1,235 +1,282 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- 定义页面的字符编码为 UTF-8确保能正确显示各种字符 -->
<meta charset="UTF-8">
<!-- 设置页面视口,使其在移动设备上能自适应宽度,初始缩放比例为 1.0,方便页面在不同设备上的展示效果优化 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 设置页面标题为“创建新的投票项目”,该标题会显示在浏览器标签页上 -->
<title>创建新的投票项目</title>
<!-- 页面内部样式定义,用于设置页面元素的外观样式 -->
<style>
body {
/* 设置页面默认字体为 Arial 或无衬线字体sans-serif增强字体的通用性和兼容性 */
font-family: Arial, sans-serif;
/* 设置页面背景颜色为浅灰色(#f4f4f4营造柔和的视觉效果 */
background-color: #f4f4f4;
/* 清除页面默认的外边距,便于后续进行精确的页面布局 */
margin: 0;
/* 清除页面默认的内边距,同样利于布局控制 */
padding: 0;
}
.container {
/* 设置容器的最大宽度为 600px避免在大屏幕上内容过度拉伸保持合适的布局宽度 */
max-width: 600px;
/* 使容器在水平方向上居中显示,通过设置左右外边距为 auto 实现 */
margin: 50px auto;
/* 设置容器的背景颜色为白色(#fff与页面背景区分开来 */
background-color: #fff;
/* 设置容器内部的内边距为 20px增加内容与容器边框的间距 */
padding: 20px;
/* 设置容器的边框圆角为 8px使容器外观更圆润美观 */
border-radius: 8px;
/* 为容器添加一个淡淡的阴影效果,增加立体感,参数表示水平和垂直方向阴影偏移量为 0模糊半径为 10px阴影颜色为半透明黑色rgba(0, 0, 0, 0.1) */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
/* 使标题文本在水平方向上居中对齐 */
text-align: center;
/* 设置标题文本颜色为深灰色(#333增强可读性和视觉对比度 */
color: #333;
}
label {
/* 将 label 标签设置为块级元素,使其独占一行,方便布局和样式控制 */
display: block;
/* 设置 label 标签下方的外边距为 5px增加与下方元素的间距 */
margin-bottom: 5px;
/* 设置 label 标签文本颜色为深灰色(#333保持整体文本颜色一致性 */
color: #333;
}
input[type="text"],
select,
input[type="date"],
button {
/* 设置这些表单元素的宽度为 100%,使其占满父容器宽度,便于在不同屏幕尺寸下保持布局一致性 */
width: 100%;
/* 设置表单元素内部的内边距为 10px增加输入内容与边框的间距 */
padding: 10px;
/* 设置表单元素下方的外边距为 20px增加元素之间的垂直间距 */
margin-bottom: 20px;
/* 设置表单元素的边框为 1px 实线,颜色为浅灰色(#ccc统一外观样式 */
border: 1px solid #ccc;
/* 设置边框圆角为 5px使元素外观更圆润 */
border-radius: 5px;
/* 设置盒模型为 border-box这样元素设置的内边距和边框不会增加元素的实际宽度方便布局计算 */
box-sizing: border-box;
}
button {
/* 设置按钮的背景颜色为蓝色(#007bff通常用于表示可操作的主要按钮颜色 */
background-color: #007bff;
/* 设置按钮文本颜色为白色(#fff与背景颜色形成鲜明对比增强可读性 */
color: #fff;
/* 将鼠标指针样式设置为手型,提示用户该元素可点击操作 */
cursor: pointer;
/* 设置按钮背景颜色在鼠标悬停时的过渡效果,过渡时间为 0.3 秒过渡动画为缓动效果ease使交互体验更平滑 */
transition: background-color 0.3s ease;
}
button:hover {
/* 定义鼠标悬停在按钮上时的背景颜色变化,变为更深的蓝色(#0056b3增强交互反馈效果 */
background-color: #0056b3;
}
.message {
/* 使消息文本在水平方向上居中对齐 */
text-align: center;
/* 设置消息文本上方的外边距为 20px增加与上方元素的间距 */
margin-top: 20px;
}
.success {
/* 设置表示成功的消息文本颜色为绿色,直观地传达操作成功的信息 */
color: green;
}
.error {
/* 设置表示错误的消息文本颜色为红色,醒目地提示出现错误情况 */
color: red;
}
</style>
</head>
<body>
<div class="container">
<h1>创建新的投票项目</h1>
<form id="createVoteForm">
<label for="voteTitle">投票标题:</label>
<input type="text" id="voteTitle" name="voteTitle" required>
<label for="numOptions">候选项数量:</label>
<select id="numOptions" name="numOptions" onchange="updateOptions()" required>
<option value="">选择候选项数量</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
<div id="optionsContainer"></div>
<label for="deadline">截止日期:</label>
<input type="date" id="deadline" name="deadline" required>
<button type="submit">创建投票</button>
</form>
<div id="message" class="message"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/web3@1.5.3/dist/web3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@metamask/detect-provider@1.2.0/dist/detect-provider.min.js"></script>
<script>
function updateOptions() {
const numOptions = document.getElementById('numOptions').value;
const optionsContainer = document.getElementById('optionsContainer');
optionsContainer.innerHTML = ''; // 清空之前的选项
if (numOptions !== '') {
const optionValues = new Set(); // 用于存储已添加的候选项值
for (let i = 1; i <= numOptions; i++) {
const label = document.createElement('label');
label.textContent = `候选项 ${i}`;
optionsContainer.appendChild(label);
const input = document.createElement('input');
input.type = 'text';
input.name = `option${i}`;
input.required = true;
input.addEventListener('input', function(event) {
const value = event.target.value.trim(); // 移除首尾空格
if (value !== '' && optionValues.has(value)) {
event.target.setCustomValidity('候选项的值不能重复');
} else {
event.target.setCustomValidity('');
}
});
optionsContainer.appendChild(input);
optionValues.add(''); // 添加初始值,避免首次输入为空时的重复检查
<!-- 创建一个类名为 container 的 div 容器,用于包裹整个投票项目创建的表单内容,实现布局和样式的统一管理 -->
<div class="container">
<!-- 页面标题,显示“创建新的投票项目”,通过 h1 标签强调其重要性,并且在样式中设置了居中对齐 -->
<h1>创建新的投票项目</h1>
<!-- 创建一个表单id 为 createVoteForm用于收集创建投票项目所需的各种信息后续通过 JavaScript 来处理表单提交等操作 -->
<form id="createVoteForm">
<!-- 创建一个 label 标签,用于提示输入投票标题,通过 for 属性与对应的 input 元素关联,增强可访问性 -->
<label for="voteTitle">投票标题:</label>
<!-- 创建一个文本输入框id 为 voteTitlename 也为 voteTitle并且设置为必填项required用于用户输入投票项目的标题 -->
<input type="text" id="voteTitle" name="voteTitle" required>
optionsContainer.appendChild(document.createElement('br'));
}
}
}
<!-- 创建一个 label 标签,用于提示选择候选项数量 -->
<label for="numOptions">候选项数量:</label>
<!-- 创建一个下拉选择框id 为 numOptionsname 为 numOptions并且添加了 onchange 事件监听器,当选项改变时会调用 updateOptions 函数来动态更新候选项的输入框,同时设置为必填项 -->
<select id="numOptions" name="numOptions" onchange="updateOptions()" required>
<!-- 下拉框的默认提示选项,显示“选择候选项数量”,其 value 值为空 -->
<option value="">选择候选项数量</option>
<!-- 可供选择的候选项数量选项,分别提供了从 2 到 10 的数值选项 -->
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
// 设置截止日期的最小值为明天的日期
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1); // 设置日期为明天
const tomorrowISO = tomorrow.toISOString().split('T')[0];
document.getElementById('deadline').setAttribute('min', tomorrowISO);
window.addEventListener('DOMContentLoaded', async () => {
// 检查 MetaMask 是否安装
const provider = await detectEthereumProvider();
if (!provider) {
console.error('请安装 MetaMask 扩展程序');
return;
}
<!-- 创建一个空的 div 容器id 为 optionsContainer后续通过 JavaScript 动态生成候选项的输入框等元素会添加到这个容器内 -->
<div id="optionsContainer"></div>
// 创建 Web3 实例
const web3 = new Web3(provider);
try {
// 请求 MetaMask 授权
await ethereum.request({ method: 'eth_requestAccounts' });
// 获取 MetaMask 用户地址
const accounts = await web3.eth.getAccounts();
const metaMaskUser = accounts[0];
// 将 MetaMask 用户地址添加到表单数据中
document.getElementById('createVoteForm').addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(this); // 获取表单数据
const data = {};
formData.forEach(function(value, key) {
data[key] = value;
});
// 添加 MetaMask 用户地址到数据中
data.metaMaskUser = metaMaskUser;
// 检查候选项是否重复
const optionValues = new Set();
let hasDuplicate = false;
for (let i = 1; i <= data.numOptions; i++) {
const option = data[`option${i}`].trim(); // 移除首尾空格
if (option !== '') {
if (optionValues.has(option)) {
hasDuplicate = true;
break;
} else {
optionValues.add(option);
}
}
}
<!-- 创建一个 label 标签,用于提示输入投票截止日期 -->
<label for="deadline">截止日期:</label>
<!-- 创建一个日期选择输入框id 为 deadlinename 为 deadline并且设置为必填项用于用户选择投票的截止时间 -->
<input type="date" id="deadline" name="deadline" required>
if (hasDuplicate) {
alert('候选项的值不能重复');
<!-- 创建一个按钮,类型为 submit文本为“创建投票”用于提交表单数据触发创建投票的相关操作按钮的样式在前面的 CSS 中已定义,有鼠标悬停等交互效果 -->
<button type="submit">创建投票</button>
</form>
<!-- 创建一个空的 div 容器id 为 message类名为 message用于后续通过 JavaScript 根据操作结果显示相应的提示消息,如投票创建成功或失败的提示 -->
<div id="message" class="message"></div>
</div>
<!-- 通过 CDN 引入 Web3.js 库的最小化版本,版本号为 1.5.3Web3.js 常用于与以太坊区块链进行交互,在本页面中可能用于处理投票相关的区块链操作 -->
<script src="https://cdn.jsdelivr.net/npm/web3@1.5.3/dist/web3.min.js"></script>
<!-- 通过 CDN 引入 MetaMask 检测提供器的最小化版本,版本号为 1.2.0,用于检测用户浏览器是否安装了 MetaMask 扩展程序,这在基于区块链的应用中很常用,因为 MetaMask 常用于管理以太坊账户等操作 -->
<script src="https://cdn.jsdelivr.net/npm/@metamask/detect-provider@1.2.0/dist/detect-provider.min.js"></script>
<script>
// 以下是 JavaScript 脚本部分的注释,用于实现页面的交互逻辑和功能
// 定义 updateOptions 函数,用于根据用户在下拉框中选择的候选项数量动态更新候选项输入框的显示情况
function updateOptions() {
// 获取下拉框id 为 numOptions中用户选择的值即候选项的数量
const numOptions = document.getElementById('numOptions').value;
// 获取用于存放候选项输入框的容器元素id 为 optionsContainer
const optionsContainer = document.getElementById('optionsContainer');
// 清空 optionsContainer 内之前可能存在的候选项输入框等元素,避免重复添加造成混乱
optionsContainer.innerHTML = '';
// 如果用户选择了具体的候选项数量(即 numOptions 不为空字符串)
if (numOptions!== '') {
// 创建一个 Set 数据结构,用于存储已添加的候选项值,利用 Set 的特性可以方便地检查值是否重复
const optionValues = new Set();
// 循环生成对应数量的候选项输入框及相关标签元素
for (let i = 1; i <= numOptions; i++) {
// 创建一个 label 标签,用于显示候选项的序号提示,如“候选项 1
const label = document.createElement('label');
label.textContent = `候选项 ${i}`;
// 将创建好的 label 标签添加到 optionsContainer 容器内
optionsContainer.appendChild(label);
// 创建一个文本输入框,用于用户输入具体的候选项内容
const input = document.createElement('input');
input.type = 'text';
input.name = `option${i}`;
input.required = true;
// 为输入框添加 input 事件监听器,用于实时检查输入的值是否重复
input.addEventListener('input', function(event) {
// 获取输入框输入的值,并移除首尾空格,确保比较时的准确性
const value = event.target.value.trim();
// 如果输入的值不为空且在 optionValues 集合中已存在(表示重复)
if (value!== '' && optionValues.has(value)) {
// 设置输入框的自定义验证提示信息,提示用户候选项的值不能重复
event.target.setCustomValidity('候选项的值不能重复');
} else {
// 候选项没有重复,提交表单
fetch('/createVote', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误');
}
return response.json();
})
.then(data => {
const messageElement = document.getElementById('message');
if (data.success) {
messageElement.textContent = `投票项目创建成功!合约地址:${data.contractAddress}`;
messageElement.classList.add('success');
// 将合约地址复制到剪贴板
navigator.clipboard.writeText(data.contractAddress)
.then(() => console.log('合约地址已复制到剪贴板'))
.catch(err => console.error('复制到剪贴板失败:', err));
// Redirect to index.html on successful creation
setTimeout(() => {
window.location.href = '/index.html';
}, 1000); // Redirect after 2 seconds
} else {
messageElement.textContent = '投票项目创建失败,请稍后重试。';
messageElement.classList.add('error');
}
})
.catch(error => {
console.error('发生错误:', error);
const messageElement = document.getElementById('message');
messageElement.textContent = '发生错误,请稍后重试。';
messageElement.classList.add('error');
});
// 如果值不重复或为空,则清除自定义验证提示信息,允许输入框正常提交
event.target.setCustomValidity('');
}
});
} catch (error) {
console.error('授权失败:', error);
// 将创建好的输入框添加到 optionsContainer 容器内
optionsContainer.appendChild(input);
// 将一个空字符串添加到 optionValues 集合中,避免首次输入为空值时误判为重复(因为后续会检查是否已存在该值)
optionValues.add('');
// 在每个候选项输入框后添加一个换行元素br使页面布局更清晰
optionsContainer.appendChild(document.createElement('br'));
}
});
</script>
</body>
</html>
}
}
// 设置截止日期输入框id 为 deadline的最小值为明天的日期确保用户不能选择早于明天的截止日期
// 获取当前日期对象
const today = new Date();
// 创建一个新的日期对象,并赋值为今天的日期,用于后续修改为明天的日期
const tomorrow = new Date(today);
// 通过设置日期对象的日期值为当前日期加 1将其修改为明天的日期
tomorrow.setDate(tomorrow.getDate() + 1);
// 将明天的日期对象转换为 ISO 8601 格式的字符串,并截取日期部分(去除时间部分),得到适合设置为 input 类型为 date 的 min 属性的值
const tomorrowISO = tomorrow.toISOString().split('T')[0];
// 将计算得到的明天日期字符串设置为截止日期输入框的 min 属性值
document.getElementById('deadline').setAttribute('min', tomorrowISO);
// 当页面的 DOM 内容加载完成后(即页面结构和元素都已加载完毕),执行以下异步函数
window.addEventListener('DOMContentLoaded', async () => {
// 检查 MetaMask 是否安装,调用 detectEthereumProvider 函数(由引入的 @metamask/detect-provider 库提供)进行检测
const provider = await detectEthereumProvider();
// 如果没有检测到 MetaMask 提供器(即未安装 MetaMask 扩展程序)
if (!provider) {
// 在控制台输出错误信息,提示用户安装 MetaMask 扩展程序
console.error('请安装 MetaMask 扩展程序');
return;
}
// 创建一个 Web3 实例传入检测到的以太坊提供器provider用于后续与以太坊区块链进行交互
const web3 = new Web3(provider);
try {
// 请求 MetaMask 授权,通过调用 ethereum.request 方法并传入指定的授权方法名eth_requestAccounts来向 MetaMask 申请使用用户账户权限
await ethereum.request({ method: 'eth_requestAccounts' });
// 获取 MetaMask 用户的账户地址,通过调用 web3.eth.getAccounts 方法,该方法返回一个包含用户账户地址的数组,通常第一个地址就是常用的主地址
const accounts = await web3.eth.getAccounts();
const metaMaskUser = accounts[0];
// 为表单id 为 createVoteForm添加 submit 事件监听器,用于处理表单提交操作,阻止表单默认的提交行为(避免页面刷新等默认操作),然后进行后续的数据处理和提交逻辑
document.getElementById('createVoteForm').addEventListener('submit', function(event) {
event.preventDefault();
// 创建一个 FormData 对象,用于获取表单中用户输入的所有数据,方便后续进行处理和发送
const formData = new FormData(this);
const data = {};
// 遍历 FormData 对象中的每一项数据,将键值对添加到 data 对象中,方便后续以 JSON 格式发送数据
formData.forEach(function(value, key) {
data[key] = value;
});
// 将获取到的 MetaMask 用户地址添加到 data 对象中,以便后续将该信息一起发送到服务器,可能用于标识投票创建者等用途
data.metaMaskUser = metaMaskUser;
// 检查候选项是否存在重复值,创建一个新的 Set 数据结构用于存储已检查的候选项值
const optionValues = new Set();
let hasDuplicate = false;
for (let i = 1; i <= data.numOptions; i++) {
// 获取每个候选项的值,并移除首尾空格,确保准确比较
const option = data[`option${i}`].trim();
// 如果候选项值不为空
if (option!== '') {
// 如果该值已经在 optionValues 集合中存在(表示重复)
if (optionValues.has(option)) {
hasDuplicate = true;
break;
} else {
// 如果值不重复,则将其添加到 optionValues 集合中
optionValues.add(option);
}
}
}
// 如果候选项存在重复值
if (hasDuplicate) {
// 弹出警告框,提示用户候选项的值不能重复
alert('候选项的值不能重复');
} else {
// 如果候选项没有重复,进行表单数据的提交操作
// 使用 fetch API 发送一个 POST 请求到服务器的 /createVote 路径,用于创建投票项目
fetch('/createVote', {
method: 'POST',
// 设置请求头的 Content-Type 为 application/json表示发送的数据是 JSON 格式
headers: {
'Content-Type': 'application/json'
},
// 将 data 对象转换为 JSON 字符串
Loading…
Cancel
Save