diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..deed335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git a/index.html b/index.html new file mode 100644 index 0000000..1e793d5 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +
void main() => runApp(MyApp());"
+ },
+ {
+ "id": 5,
+ "lv": 1,
+ "q": "要在控制台输出文字,应调用哪个函数?",
+ "opts": [
+ "console.log()",
+ "System.out.println()",
+ "echo",
+ "print()"
+ ],
+ "ans": 3,
+ "exp": "Dart 用 print() 输出到控制台,比如 print(\"Hello Dart\");"
+ },
+ {
+ "id": 6,
+ "lv": 1,
+ "q": "int x = 10; 要打印出 x=10,以下哪种写法正确?",
+ "opts": [
+ "print(\"x=\" + x);",
+ "print(\"x=$x\");",
+ "print('x=${x}');",
+ "以上都可以"
+ ],
+ "ans": 3,
+ "exp": "Dart 中 + 拼接字符串、$x 插值、${x} 表达式插值都合法,三种写法均能输出 x=10。"
+ },
+ {
+ "id": 7,
+ "lv": 2,
+ "q": "以下哪个 不是 Dart 的循环写法?",
+ "opts": [
+ "for (var i=0; i<5; i++)",
+ "while (ok)",
+ "foreach (x in list)",
+ "do { } while (ok)"
+ ],
+ "ans": 2,
+ "exp": "Dart 没有 foreach(x in list) 语法,应写 for (var x in list) 或 list.forEach(...)。"
+ },
+ {
+ "id": 8,
+ "lv": 2,
+ "q": "执行 for (var i=0; i<5; i++) print(i); 会输出?",
+ "opts": [
+ "1 2 3 4 5",
+ "0 1 2 3 4",
+ "0 1 2 3 4 5",
+ "5 4 3 2 1"
+ ],
+ "ans": 1,
+ "exp": "i 从 0 开始,条件 i<5,所以输出 0 1 2 3 4。"
+ },
+ {
+ "id": 9,
+ "lv": 2,
+ "q": "Dart 的 switch 语句中,每个 case 结束必须以什么结尾(否则编译报错)?",
+ "opts": [
+ "end",
+ "return / break / throw",
+ "continue",
+ "逗号"
+ ],
+ "ans": 1,
+ "exp": "Dart 每个非空 case 必须以 break、return 或 throw 等结束,不能像 C 语言那样自动穿透。"
+ },
+ {
+ "id": 10,
+ "lv": 2,
+ "q": "定义一个返回两数之和的函数,哪个写法正确?",
+ "opts": [
+ "int add(int a,int b){ return a+b; }",
+ "add(int a,int b) => a+b;",
+ "void add(int a,int b){ return a+b; }",
+ "A 和 B 都可以"
+ ],
+ "ans": 3,
+ "exp": "A 是传统写法,B 是箭头函数简写(返回表达式),两者都合法;C 返回 void 却 return 值,编译错误。"
+ },
+ {
+ "id": 11,
+ "lv": 2,
+ "q": "以下哪个是可选命名参数的正确声明?",
+ "opts": [
+ "void f({String name}) {}",
+ "void f([String name]) {}",
+ "void f(String? name) {}",
+ "void f(String name=\"\") {}"
+ ],
+ "ans": 0,
+ "exp": "{} 包裹的是命名参数(调用用 f(name: x));[] 是可选位置参数;? 是空安全;最后一个是默认值写法但不属于命名参数。"
+ },
+ {
+ "id": 12,
+ "lv": 2,
+ "q": "函数 void f({int a = 1}) {},以下调用哪个正确?",
+ "opts": [
+ "f()",
+ "f(1)",
+ "f(a: 1)",
+ "A 和 C 都可以"
+ ],
+ "ans": 3,
+ "exp": "命名参数有默认值时可省略(A 合法);必须按名调用 f(a: 1)(C 合法);f(1) 用位置传参给命名参数会报错。"
+ },
+ {
+ "id": 13,
+ "lv": 3,
+ "q": "Dart 中表示\"数组/列表\"的类型是?",
+ "opts": [
+ "Array",
+ "ArrayList",
+ "List",
+ "Vector"
+ ],
+ "ans": 2,
+ "exp": "Dart 用 List(可增长数组),字面量写法 [1,2,3]。"
+ },
+ {
+ "id": 14,
+ "lv": 3,
+ "q": "创建包含 1、2、3 的列表,正确的是?",
+ "opts": [
+ "{1, 2, 3}",
+ "[1, 2, 3]",
+ "(1, 2, 3)",
+ "List(1,2,3)"
+ ],
+ "ans": 1,
+ "exp": "[] 是 List;{} 是 Set/Map;() 不是集合字面量。"
+ },
+ {
+ "id": 15,
+ "lv": 3,
+ "q": "var list = [10, 20, 30]; print(list[1]); 输出?",
+ "opts": [
+ "10",
+ "20",
+ "30",
+ "报错"
+ ],
+ "ans": 1,
+ "exp": "Dart 索引从 0 开始,list[1] 是第二个元素 20。"
+ },
+ {
+ "id": 16,
+ "lv": 3,
+ "q": "对 List 每个元素做处理并返回一个新集合的方法是?",
+ "opts": [
+ "forEach()",
+ "map()",
+ "where()",
+ "add()"
+ ],
+ "ans": 1,
+ "exp": "map() 对每个元素变换并返回 Iterable;forEach() 只遍历返回 void;where() 是过滤。"
+ },
+ {
+ "id": 17,
+ "lv": 3,
+ "q": "var a=[1,2,3]; var b=[0, ...a]; 那么 b.length 是?",
+ "opts": [
+ "3",
+ "4",
+ "7",
+ "报错"
+ ],
+ "ans": 1,
+ "exp": "...a 是扩展运算符,把 a 展开成 1,2,3,加上开头的 0,b = [0,1,2,3],长度 4。"
+ },
+ {
+ "id": 18,
+ "lv": 3,
+ "q": "以下哪种集合类型保证元素不重复?",
+ "opts": [
+ "List",
+ "Map",
+ "Set",
+ "Queue"
+ ],
+ "ans": 2,
+ "exp": "Set 存储唯一元素,重复添加会被忽略;Map 存储键值对,键也不重复。"
+ },
+ {
+ "id": 19,
+ "lv": 4,
+ "q": "定义类 class Dog { String name; } 后,创建一个实例哪个正确?",
+ "opts": [
+ "Dog d = Dog();",
+ "var d = new Dog;",
+ "Dog d = new Dog();",
+ "以上都可以"
+ ],
+ "ans": 3,
+ "exp": "Dart 2 之后 new 关键字可选,且省略括号的 Dog() 也合法,三种写法都能创建实例。"
+ },
+ {
+ "id": 20,
+ "lv": 4,
+ "q": "子类继承父类使用哪个关键字?",
+ "opts": [
+ "with",
+ "implements",
+ "extends",
+ "inherits"
+ ],
+ "ans": 2,
+ "exp": "继承用 extends;implements 是实现接口;with 用于混入 Mixin。"
+ },
+ {
+ "id": 21,
+ "lv": 4,
+ "q": "包含未实现方法、不能被直接实例化的类,用什么关键字声明?",
+ "opts": [
+ "abstract",
+ "interface",
+ "base",
+ "pure"
+ ],
+ "ans": 0,
+ "exp": "abstract 抽象类可含未实现方法,不能直接 new,需子类实现后实例化。"
+ },
+ {
+ "id": 22,
+ "lv": 4,
+ "q": "在构造函数/实例方法中,this 指向?",
+ "opts": [
+ "当前实例",
+ "父类对象",
+ "类本身",
+ "静态成员"
+ ],
+ "ans": 0,
+ "exp": "this 指向当前实例对象,用于区分参数与成员变量,如 this.name = name。"
+ },
+ {
+ "id": 23,
+ "lv": 4,
+ "q": "创建一个只能装 String 的 List,哪个写法正确?",
+ "opts": [
+ "List<String> names = [];",
+ "List names = <String>[];",
+ "var names = <String>[];",
+ "以上都是"
+ ],
+ "ans": 3,
+ "exp": "Dart 泛型语法多样:类型标注式、泛型字面量 <String>[] 都合法,三种写法结果相同。"
+ },
+ {
+ "id": 24,
+ "lv": 4,
+ "q": "抽象类 Animal 有抽象方法 void speak(),子类 Dog 必须?",
+ "opts": [
+ "重写 speak() 提供实现",
+ "调用父类构造",
+ "删除 speak",
+ "什么也不用做"
+ ],
+ "ans": 0,
+ "exp": "子类必须用 @override 实现所有抽象方法,否则 Dog 也是抽象类无法实例化。"
+ },
+ {
+ "id": 25,
+ "lv": 5,
+ "q": "Dart 空安全中,声明一个可能为 null 的字符串类型是?",
+ "opts": [
+ "String",
+ "String?",
+ "String!",
+ "nullable String"
+ ],
+ "ans": 1,
+ "exp": "在类型后加 ? 表示可空:String? s = null;;不加 ? 的 String 保证非空。"
+ },
+ {
+ "id": 26,
+ "lv": 5,
+ "q": "空值合并运算符 ?? 的作用是?",
+ "opts": [
+ "若左边为 null 则返回右边",
+ "强制把 null 变为非空",
+ "判断对象是否相等",
+ "抛出空异常"
+ ],
+ "ans": 0,
+ "exp": "a ?? b:a 不为 null 用 a,否则用 b。常配合 ??= 设置默认值。"
+ },
+ {
+ "id": 27,
+ "lv": 5,
+ "q": "使用 async/await 时,异步函数一般返回什么类型?",
+ "opts": [
+ "Stream<T>",
+ "Future<T>",
+ "void",
+ "T"
+ ],
+ "ans": 1,
+ "exp": "async 函数返回 Future<T>,await 会挂起等待该 Future 完成后再继续执行。"
+ },
+ {
+ "id": 28,
+ "lv": 5,
+ "q": "执行以下代码,输出顺序是?Future.delayed(Duration(seconds:1), () => print(\"A\"));
print(\"B\");",
+ "opts": [
+ "A 后 B",
+ "B 后 A",
+ "同时输出",
+ "只输出 A"
+ ],
+ "ans": 1,
+ "exp": "Future.delayed 是异步任务,先执行同步的 print(\"B\"),1 秒后再输出 A。"
+ },
+ {
+ "id": 29,
+ "lv": 6,
+ "q": "Flutter 中用于监听持续不断的数据流(如多次事件)的是?",
+ "opts": [
+ "Future",
+ "Stream",
+ "List",
+ "Map"
+ ],
+ "ans": 1,
+ "exp": "Stream 可监听多次异步事件;Future 只代表一次性的结果。"
+ },
+ {
+ "id": 30,
+ "lv": 6,
+ "q": "Flutter 中构建界面的基本单位是?",
+ "opts": [
+ "Activity",
+ "View",
+ "Widget",
+ "Component"
+ ],
+ "ans": 2,
+ "exp": "Flutter 一切皆 Widget,UI 由 Widget 树组合而成(如 Text、Container、Column)。"
+ },
+ {
+ "id": 31,
+ "lv": 6,
+ "q": "界面状态需要随数据变化而更新时,应使用哪类组件?",
+ "opts": [
+ "StatelessWidget",
+ "StatefulWidget",
+ "Container",
+ "Scaffold"
+ ],
+ "ans": 1,
+ "exp": "有动态状态用 StatefulWidget(配合 State 里的 setState 刷新);无状态用 StatelessWidget。"
+ }
+ ]
+ },
+ {
+ "id": 9,
+ "name": "Dart 开发实战",
+ "questions": [
+ {
+ "id": 98,
+ "lv": 1,
+ "q": "开发注册页时,用户输入的年龄是字符串,需要转成 int 保存。已知 String ageText = \"18\";,正确的转换写法是?",
+ "opts": [
+ "int(ageText)",
+ "toInt(ageText)",
+ "int.parse(ageText)",
+ "ageText.parseInt()"
+ ],
+ "ans": 2,
+ "exp": "int.parse() 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 int.tryParse()(失败返回 null)。注意 Dart 没有 \"18\".parseInt() 这种写法,int(...) 也不是转换函数。"
+ },
+ {
+ "id": 99,
+ "lv": 1,
+ "q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?",
+ "opts": [
+ "final String api = \"https://api.example.com\";",
+ "const String api = \"https://api.example.com\";",
+ "String api = \"https://api.example.com\";",
+ "var api = \"https://api.example.com\";"
+ ],
+ "ans": 1,
+ "exp": "const 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。final 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。"
+ },
+ {
+ "id": 100,
+ "lv": 1,
+ "q": "你在重构代码时写下:var x = 10; x = \"hello\"; 会发生什么?",
+ "opts": [
+ "正常运行,x 变成字符串",
+ "编译报错:x 已被推断为 int 类型",
+ "运行时报错",
+ "x 自动变为 null"
+ ],
+ "ans": 1,
+ "exp": "var 声明时由初始值自动推断类型,推断后类型就固定了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 dynamic 或 Object?。"
+ },
+ {
+ "id": 101,
+ "lv": 1,
+ "q": "给变量起名时,以下哪个不是合法的 Dart 标识符?",
+ "opts": [
+ "_score",
+ "2x",
+ "中文名",
+ "name_2"
+ ],
+ "ans": 1,
+ "exp": "Dart 标识符不能以数字开头。_score 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。"
+ },
+ {
+ "id": 102,
+ "lv": 1,
+ "q": "结算功能计算人均消费:int total = 10; int people = 4; double avg = total / people; 打印 avg 得到?",
+ "opts": [
+ "2",
+ "2.5",
+ "2.0",
+ "报错"
+ ],
+ "ans": 1,
+ "exp": "Dart 中 int 除以 int 的结果一定是 double:10 / 4 = 2.5。想得到整除结果用 ~/:10 ~/ 4 == 2,取余用 %。"
+ },
+ {
+ "id": 103,
+ "lv": 2,
+ "q": "写优惠逻辑:订单数 > 10 打 9 折,> 50 打 8 折。对 orders = 60,以下代码会得到什么折扣?double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }",
+ "opts": [
+ "0.8",
+ "0.9",
+ "1.0",
+ "0.7"
+ ],
+ "ans": 1,
+ "exp": "if-else 链从上到下匹配,命中即止:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。"
+ },
+ {
+ "id": 104,
+ "lv": 2,
+ "q": "接口返回 Map<String, dynamic> data,想遍历所有 key 打印出来,以下哪种写法正确?",
+ "opts": [
+ "for (var k in data) print(k);",
+ "for (var k in data.keys) print(k);",
+ "data.keys.forEach(print);",
+ "B 和 C 都可以"
+ ],
+ "ans": 3,
+ "exp": "Map 本身不能直接 for-in 遍历,要先取 keys / values / entries。B 是标准写法;C 中 forEach(print) 利用了函数引用,同样正确。两种都行,按团队规范选择。"
+ },
+ {
+ "id": 105,
+ "lv": 2,
+ "q": "写一个返回用户名的函数:String getUserName() { },忘了写 return,会怎样?",
+ "opts": [
+ "返回 null",
+ "编译报错",
+ "运行时崩溃",
+ "自动返回空字符串"
+ ],
+ "ans": 1,
+ "exp": "空安全下,声明了非空返回类型 String 的函数必须有 return,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 String?。"
+ },
+ {
+ "id": 106,
+ "lv": 2,
+ "q": "问候函数:void greet(String name, {String greeting = \"你好\"}),调用 greet(\"小明\", \"早上好\") 会发生什么?",
+ "opts": [
+ "正常执行,greeting 为 \"早上好\"",
+ "编译报错:命名参数不能按位置传参",
+ "greeting 变为 null",
+ "编译警告但能运行"
+ ],
+ "ans": 1,
+ "exp": "{} 包裹的是命名参数,必须写成 greet(\"小明\", greeting: \"早上好\");[] 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。"
+ },
+ {
+ "id": 107,
+ "lv": 2,
+ "q": "写\"重试发送验证码\"逻辑:要求至少发送一次,失败则继续重试。哪种循环语义最贴切?",
+ "opts": [
+ "while",
+ "do-while",
+ "for",
+ "repeat"
+ ],
+ "ans": 1,
+ "exp": "do-while 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。"
+ },
+ {
+ "id": 108,
+ "lv": 3,
+ "q": "实现\"删除待办\"功能,list.remove(3) 和 list.removeAt(3) 的区别是?",
+ "opts": [
+ "remove 按值删除,removeAt 按下标删除",
+ "两者完全相同",
+ "remove 删下标,removeAt 按值删除",
+ "remove 会删除所有等于 3 的元素"
+ ],
+ "ans": 0,
+ "exp": "remove(value) 删除第一个值相等的元素并返回是否成功;removeAt(index) 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。"
+ },
+ {
+ "id": 109,
+ "lv": 3,
+ "q": "解析接口数据:用 data[\"name\"] 取一个不存在的 key 时,返回什么?",
+ "opts": [
+ "抛异常",
+ "null",
+ "空字符串",
+ "抛出 NoSuchMethodError"
+ ],
+ "ans": 1,
+ "exp": "Map 用 [] 取值时,key 不存在返回 null 而不是抛异常。所以 data[\"name\"] 可能为 null,取值后判空再使用是常规操作(配合 ?? 兜底)。"
+ },
+ {
+ "id": 110,
+ "lv": 3,
+ "q": "配置列表按条件插入元素:var l = [1, 2, if (isVip) 3]; 当 isVip 为 false 时 l 是?",
+ "opts": [
+ "[1, 2]",
+ "[1, 2, 3]",
+ "[1, 2, false]",
+ "编译错误"
+ ],
+ "ans": 0,
+ "exp": "这是 collection-if 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 ... 展开和 collection-for 一起使用。"
+ },
+ {
+ "id": 111,
+ "lv": 3,
+ "q": "两个用户各自关注了一批标签,要找出共同关注的标签,用哪种写法?",
+ "opts": [
+ "a & b",
+ "a.intersect(b)",
+ "a.toSet().intersection(b.toSet())",
+ "a.union(b)"
+ ],
+ "ans": 2,
+ "exp": "List 没有交集方法,要先转 Set。intersection() 返回两个集合的共同元素;union() 是并集。注意方法名是 intersection(),不是其它语言的 intersect()。"
+ },
+ {
+ "id": 112,
+ "lv": 3,
+ "q": "计算购物车总价:List<double> prices = []; 空列表调用 prices.reduce((a, b) => a + b) 会?",
+ "opts": [
+ "返回 0",
+ "抛出 StateError 异常",
+ "返回 null",
+ "返回空列表"
+ ],
+ "ans": 1,
+ "exp": "reduce 要求列表至少有一个元素,空列表直接抛 \"Bad state: No element\"。更稳的写法是 fold(0, (a, b) => a + b):带初始值,空列表返回 0。"
+ },
+ {
+ "id": 113,
+ "lv": 4,
+ "q": "写日期工具类,希望 DateUtil.parse(\"2024-01-01\") 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?",
+ "opts": [
+ "factory 构造函数",
+ "普通构造函数",
+ "命名构造函数",
+ "const 构造函数"
+ ],
+ "ans": 0,
+ "exp": "factory 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。"
+ },
+ {
+ "id": 114,
+ "lv": 4,
+ "q": "日志打印、权限检查这类能力要被多个互不相关的类复用,又不想强制继承关系,Dart 的推荐做法是?",
+ "opts": [
+ "extends 继承",
+ "implements 实现",
+ "with 混入 Mixin",
+ "abstract 抽象类"
+ ],
+ "ans": 2,
+ "exp": "Mixin(with)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 class _X with RouteAware。"
+ },
+ {
+ "id": 115,
+ "lv": 4,
+ "q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?",
+ "opts": [
+ "private String _token;",
+ "String _token;",
+ "-String token;",
+ "#token"
+ ],
+ "ans": 1,
+ "exp": "Dart 没有 private 关键字,以下划线开头表示库内私有:_token。注意这是\"库\"级别私有(同文件可见),不是类级别。"
+ },
+ {
+ "id": 116,
+ "lv": 4,
+ "q": "用户类的 name 字段希望赋值时自动去掉首尾空格,最佳做法是?",
+ "opts": [
+ "声明 getter/setter,在 setter 里 trim",
+ "直接写公共字段,调用方自己 trim",
+ "用 final 禁止修改",
+ "用 static 修饰"
+ ],
+ "ans": 0,
+ "exp": "setter 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:String get name 与 set name(String v)。"
+ },
+ {
+ "id": 117,
+ "lv": 4,
+ "q": "class Dog implements Animal 与 extends 继承的关键区别是?",
+ "opts": [
+ "implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现",
+ "两者完全一样",
+ "implements 只能用于抽象类",
+ "extends 要求实现全部成员"
+ ],
+ "ans": 0,
+ "exp": "implements 把 Animal 当接口:所有成员都要自己重写,即使父类已有实现;extends 继承实现,只覆写需要改的部分。implements 用于\"规定能力\",extends 用于\"复用实现\"。"
+ },
+ {
+ "id": 118,
+ "lv": 5,
+ "q": "用户对象可能为 null:User? u = await fetchUser();,以下哪种使用方式更安全?",
+ "opts": [
+ "if (u != null) { print(u.name); } 判空后直接用 u",
+ "if (u != null) { print(u!.name); } 必须加 !",
+ "print(u?.name ?? \"\");",
+ "A 和 C 都可以"
+ ],
+ "ans": 3,
+ "exp": "A:局部变量判空后编译器会自动提升为非空类型,直接使用即可;C:?. 安全调用 + ?? 兜底,一行搞定。只有被闭包捕获的变量才需要 !。"
+ },
+ {
+ "id": 119,
+ "lv": 5,
+ "q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?",
+ "opts": [
+ "Future.wait([fetchUser(), fetchOrders()])",
+ "串行写两个 await",
+ "Future.any",
+ "Future.delayed 等待"
+ ],
+ "ans": 0,
+ "exp": "Future.wait 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。Future.any 只等第一个完成的。"
+ },
+ {
+ "id": 120,
+ "lv": 5,
+ "q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?",
+ "opts": [
+ "late String token;",
+ "String? token = \"\";",
+ "dynamic token;",
+ "final token = null;"
+ ],
+ "ans": 0,
+ "exp": "late 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。"
+ },
+ {
+ "id": 121,
+ "lv": 5,
+ "q": "把解析结果转成目标类型:var age = data[\"age\"] as int;,当 age 实际是 String 时会发生?",
+ "opts": [
+ "返回 null",
+ "抛出 TypeError(类型转换失败)",
+ "静默忽略",
+ "自动转成 int"
+ ],
+ "ans": 1,
+ "exp": "as 是强制类型转换,类型不符立即抛异常。安全做法:先用 is 判断类型,或先统一规范数据(如 num 一律 .toInt())。"
+ },
+ {
+ "id": 122,
+ "lv": 5,
+ "q": "对可能为 null 的 String? s 使用 s!,若 s 确实为 null 会?",
+ "opts": [
+ "返回 null",
+ "抛出 TypeError(空检查失败)",
+ "静默继续",
+ "编译错误"
+ ],
+ "ans": 1,
+ "exp": "! 是强制解包运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 ?? 的地方尽量别用 !,减少运行时崩溃点。"
+ },
+ {
+ "id": 123,
+ "lv": 6,
+ "q": "Flutter 中常见 ButtonStyle()..backgroundColor = ... ..shape = ... 写法,.. 级联操作符的核心行为是?",
+ "opts": [
+ "对同一对象连续操作且返回对象本身",
+ "链式调用并返回最后一次结果",
+ "只能用于 List",
+ "与 ... 展开等价"
+ ],
+ "ans": 0,
+ "exp": "级联操作符 .. 对同一对象连续执行多个操作,不改变返回值(始终是原对象),适合集中配置对象。. 返回的是方法调用的返回值,两者行为不同。"
+ },
+ {
+ "id": 124,
+ "lv": 6,
+ "q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?",
+ "opts": [
+ "Map<String, dynamic>",
+ "Record 记录类型:(String name, int age)",
+ "List<dynamic>",
+ "用全局变量"
+ ],
+ "ans": 1,
+ "exp": "Record 是 Dart 3 的轻量聚合类型:var r = (\"张三\", 18); 或命名字段 (name: \"张三\", age: 18),用 r.$1 / r.name 取值,还能配合模式匹配解构,比 Map 更类型安全。"
+ },
+ {
+ "id": 125,
+ "lv": 6,
+ "q": "Dart 3 的 switch 可以直接当表达式赋值:var s = switch (status) { \"ok\" => \"成功\", _ => \"未知\" }; 这种写法?",
+ "opts": [
+ "合法:switch 表达式返回一个值",
+ "非法:switch 不能作表达式",
+ "必须每个分支加 break",
+ "只能匹配字符串"
+ ],
+ "ans": 0,
+ "exp": "switch 表达式用 => 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。"
+ },
+ {
+ "id": 126,
+ "lv": 6,
+ "q": "想给系统类 String 加一个 isEmail() 方法,但 String 不能改源码,用?",
+ "opts": [
+ "extension 扩展:extension EmailCheck on String { bool isEmail() {...} }",
+ "继承 String",
+ "用 mixin 混入",
+ "写静态工具类"
+ ],
+ "ans": 0,
+ "exp": "extension 可以不修改原类给已有类型添加方法,之后 \"a@b.com\".isEmail() 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。"
+ },
+ {
+ "id": 127,
+ "lv": 6,
+ "q": "泛型容器 class Box<T extends num> {} 中 extends num 的含义是?",
+ "opts": [
+ "T 只能是 int、double 等数值类型",
+ "T 可以是任意类型",
+ "T 必须可空",
+ "T 必须是集合类型"
+ ],
+ "ans": 0,
+ "exp": "泛型约束限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 Object?。"
+ }
+ ],
+ "lvNames": [
+ "基础语法",
+ "控制流与函数",
+ "集合",
+ "面向对象",
+ "空安全与异步",
+ "Dart 高级特性"
+ ]
+ },
+ {
+ "id": 10,
+ "name": "Flutter 开发实战",
+ "questions": [
+ {
+ "id": 128,
+ "lv": 1,
+ "q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?",
+ "opts": [
+ "void main() => runApp(MyApp());",
+ "void main() { MyApp(); }",
+ "runApp() { MyApp }",
+ "main() { MaterialApp(); }"
+ ],
+ "ans": 0,
+ "exp": "Dart 程序入口是 main(),Flutter 通过 runApp(Widget) 把根组件挂载到引擎上。注意 MyApp() 是创建 Widget 实例,不能少了括号。"
+ },
+ {
+ "id": 129,
+ "lv": 1,
+ "q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?",
+ "opts": [
+ "StatelessWidget",
+ "StatefulWidget",
+ "Container",
+ "Scaffold"
+ ],
+ "ans": 1,
+ "exp": "有动态变化的数据(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 setState() 刷新。没有可变状态时应优先用 StatelessWidget。"
+ },
+ {
+ "id": 130,
+ "lv": 1,
+ "q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?",
+ "opts": [
+ "Text",
+ "Label",
+ "Paragraph",
+ "RichText"
+ ],
+ "ans": 0,
+ "exp": "Text 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。"
+ },
+ {
+ "id": 131,
+ "lv": 1,
+ "q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?",
+ "opts": [
+ "修改 State 字段后调用 setState()",
+ "直接修改字段就行",
+ "重启整个 App",
+ "修改 Widget 的构造参数"
+ ],
+ "ans": 0,
+ "exp": "State 里的字段只是普通数据,必须调用 setState() 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建当前 State 的子树,不是整棵树。"
+ },
+ {
+ "id": 132,
+ "lv": 1,
+ "q": "要展示 10000 条商品记录,性能最优的做法是?",
+ "opts": [
+ "ListView.builder(按需构建可见项)",
+ "Column 里循环放 10000 个组件",
+ "SingleChildScrollView 包 Column",
+ "GridView.count 全部构建"
+ ],
+ "ans": 0,
+ "exp": "ListView.builder 只构建当前可见的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。"
+ },
+ {
+ "id": 133,
+ "lv": 2,
+ "q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?",
+ "opts": [
+ "Column",
+ "Row",
+ "Stack",
+ "Wrap"
+ ],
+ "ans": 0,
+ "exp": "Column 垂直方向排列子组件,Row 水平方向,Stack 叠加并支持定位,Wrap 在空间不足时自动换行。"
+ },
+ {
+ "id": 134,
+ "lv": 2,
+ "q": "Row 里想让一个按钮占满剩余宽度(剩余空间全给它),按钮外层套?",
+ "opts": [
+ "Expanded",
+ "Padding",
+ "Center",
+ "Spacer"
+ ],
+ "ans": 0,
+ "exp": "Expanded 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。"
+ },
+ {
+ "id": 135,
+ "lv": 2,
+ "q": "Column 里有几个按钮,想让它们在水平方向居中,应该设置?",
+ "opts": [
+ "crossAxisAlignment: CrossAxisAlignment.center",
+ "mainAxisAlignment: MainAxisAlignment.center",
+ "alignment: Alignment.center",
+ "textAlign: TextAlign.center"
+ ],
+ "ans": 0,
+ "exp": "Column 的主轴是垂直方向、交叉轴是水平方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。"
+ },
+ {
+ "id": 136,
+ "lv": 2,
+ "q": "封面图要铺满整个区域且不拉伸变形(多出的部分裁掉),fit 参数应该用?",
+ "opts": [
+ "BoxFit.cover",
+ "BoxFit.fill",
+ "BoxFit.none",
+ "BoxFit.scaleDown"
+ ],
+ "ans": 0,
+ "exp": "cover:等比缩放填满、超出裁剪,常用于头像、封面;fill 会拉伸变形;contain 完整显示但可能留白;none 不缩放。"
+ },
+ {
+ "id": 137,
+ "lv": 2,
+ "q": "商品名过长时希望单行显示,超出部分显示省略号,Text 的配置是?",
+ "opts": [
+ "maxLines: 1, overflow: TextOverflow.ellipsis",
+ "只设 overflow: TextOverflow.clip",
+ "只设 maxLines: 3",
+ "softWrap: false 就够了"
+ ],
+ "ans": 0,
+ "exp": "maxLines 限制最大行数 + overflow: TextOverflow.ellipsis 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。"
+ },
+ {
+ "id": 138,
+ "lv": 3,
+ "q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?",
+ "opts": [
+ "TextEditingController",
+ "每次读 onChanged 参数",
+ "GlobalKey 临时查找",
+ "无法实现"
+ ],
+ "ans": 0,
+ "exp": "TextEditingController 是输入框的\"数据源\":TextEditingController(text: \"预填\") 初始化,controller.text 读取,controller.clear() 清空。用完后在 dispose 里释放。"
+ },
+ {
+ "id": 139,
+ "lv": 3,
+ "q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?",
+ "opts": [
+ "dispose()",
+ "initState()",
+ "build()",
+ "didChangeDependencies()"
+ ],
+ "ans": 0,
+ "exp": "dispose() 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。"
+ },
+ {
+ "id": 140,
+ "lv": 3,
+ "q": "登录状态、用户信息要被多个页面共享,又不想层层传构造参数,项目常用方案是?",
+ "opts": [
+ "Provider / InheritedWidget",
+ "定义全局变量直接读写",
+ "每个页面重新请求接口",
+ "把状态塞进路由参数"
+ ],
+ "ans": 0,
+ "exp": "Flutter 官方的底层方案是 InheritedWidget,Provider 是对它的优雅封装:共享数据放顶层,子组件 context.watch() 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。"
+ },
+ {
+ "id": 141,
+ "lv": 3,
+ "q": "商品列表页要支持下拉刷新,应该把 ListView 包进哪个组件?",
+ "opts": [
+ "RefreshIndicator",
+ "GestureDetector",
+ "AnimatedList",
+ "Scrollbar"
+ ],
+ "ans": 0,
+ "exp": "RefreshIndicator 包住可滚动组件,配合 onRefresh 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。"
+ },
+ {
+ "id": 142,
+ "lv": 3,
+ "q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?",
+ "opts": [
+ "WidgetsBindingObserver",
+ "RouteObserver",
+ "NavigatorObserver",
+ "MediaQuery"
+ ],
+ "ans": 0,
+ "exp": "给 State 混入 with WidgetsBindingObserver,在 didChangeAppLifecycleState() 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。"
+ },
+ {
+ "id": 143,
+ "lv": 4,
+ "q": "列表页点击商品跳转详情页,标准写法是?",
+ "opts": [
+ "Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))",
+ "Navigator.go(DetailPage(id: id))",
+ "Route.open(DetailPage())",
+ "DetailPage(id: id).show()"
+ ],
+ "ans": 0,
+ "exp": "Navigator.push 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 Navigator.pop(context)。"
+ },
+ {
+ "id": 144,
+ "lv": 4,
+ "q": "详情页编辑完要把结果回传给列表页,详情页返回时应该写?",
+ "opts": [
+ "Navigator.pop(context, result)",
+ "Navigator.back(result)",
+ "Navigator.pushResult(result)",
+ "context.popAndPush(result)"
+ ],
+ "ans": 0,
+ "exp": "pop(context, 返回值) 的第二个参数就是回传的数据,列表页 await push(...) 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。"
+ },
+ {
+ "id": 145,
+ "lv": 4,
+ "q": "跳转时需要传递商品 id、名称等参数,团队协作中最推荐的传参方式是?",
+ "opts": [
+ "DetailPage(id: 1, name: \"商品\") 构造参数传递",
+ "全局变量临时存",
+ "static 静态字段存",
+ "URL 字符串拼接"
+ ],
+ "ans": 0,
+ "exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。"
+ },
+ {
+ "id": 146,
+ "lv": 4,
+ "q": "在 App 里注册命名路由(如 /login、/home),配置位置是?",
+ "opts": [
+ "MaterialApp(routes: {...})",
+ "main() 函数里",
+ "runApp() 参数",
+ "每个页面里各自声明"
+ ],
+ "ans": 0,
+ "exp": "命名路由集中配置在 MaterialApp 的 routes 表,之后用 Navigator.pushNamed(context, \"/login\") 跳转;需要传参时配合 arguments 参数。"
+ },
+ {
+ "id": 147,
+ "lv": 4,
+ "q": "跳到选择页,用户选完返回后要根据选择结果继续处理,列表页怎么写?",
+ "opts": [
+ "final r = await Navigator.push(...); 拿到返回值再处理",
+ "无法等待结果",
+ "用 setState 轮询",
+ "跳转时把回调函数传过去"
+ ],
+ "ans": 0,
+ "exp": "push 返回 Future<T>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。"
+ },
+ {
+ "id": 148,
+ "lv": 5,
+ "q": "接口返回 JSON 字符串,执行 var data = jsonDecode(text); 后 data 的类型是?",
+ "opts": [
+ "dynamic(实际可能是 Map 或 List)",
+ "String",
+ "Map<String, dynamic>",
+ "Object"
+ ],
+ "ans": 0,
+ "exp": "jsonDecode 的返回类型是 dynamic:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:jsonDecode(text) as Map<String, dynamic>。"
+ },
+ {
+ "id": 149,
+ "lv": 5,
+ "q": "要把用户对象 User(id: 1, name: \"张三\") 转成 JSON 字符串发给服务器,正确流程是?",
+ "opts": [
+ "给 User 写 toJson(),再 jsonEncode(user.toJson())",
+ "jsonEncode(user) 直接序列化",
+ "user.toString()",
+ "JSON.stringify(user)"
+ ],
+ "ans": 0,
+ "exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 Map<String, dynamic> toJson(),再用 jsonEncode 转字符串;反序列化则对应 User.fromJson(Map) 工厂构造。"
+ },
+ {
+ "id": 150,
+ "lv": 5,
+ "q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?",
+ "opts": [
+ "用 try-catch 包裹 await 请求,捕获异常并提示用户",
+ "不做处理",
+ "打印日志后忽略",
+ "延迟 3 秒自动重试"
+ ],
+ "ans": 0,
+ "exp": "try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" } 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。"
+ },
+ {
+ "id": 151,
+ "lv": 5,
+ "q": "请求进行中需要展示\"加载中\"状态,最合适用?",
+ "opts": [
+ "CircularProgressIndicator",
+ "Text(\"加载中...\")",
+ "Opacity 半透明遮罩",
+ "AnimatedContainer 闪烁"
+ ],
+ "ans": 0,
+ "exp": "Material 标准的加载指示器是 CircularProgressIndicator(转圈)和 LinearProgressIndicator(条),配合 loading 状态变量做条件渲染。"
+ },
+ {
+ "id": 152,
+ "lv": 5,
+ "q": "http 请求要设置 10 秒超时,正确写法是?",
+ "opts": [
+ "await http.get(url).timeout(Duration(seconds: 10))",
+ "http.get(url, timeout: 10)",
+ "Timer(Duration(seconds: 10), ...)",
+ "Future.any 自动超时"
+ ],
+ "ans": 0,
+ "exp": "Future 自带 .timeout():超时抛 TimeoutException,可再接 onTimeout 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。"
+ },
+ {
+ "id": 153,
+ "lv": 6,
+ "q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?",
+ "opts": [
+ "Future.wait([...]) 并发等待",
+ "三个 await 串行",
+ "依次 catch 分别显示",
+ "用 Future.any"
+ ],
+ "ans": 0,
+ "exp": "并发场景用 Future.wait,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。"
+ },
+ {
+ "id": 154,
+ "lv": 6,
+ "q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?",
+ "opts": [
+ "ScrollController 监听 pixels 接近 maxScrollExtent 时触发",
+ "用 Timer 每 3 秒加载",
+ "下拉刷新代替",
+ "GestureDetector 监听手势"
+ ],
+ "ans": 0,
+ "exp": "给 ListView 挂 ScrollController,在 addListener 里判断 position.pixels >= position.maxScrollExtent - 200(预留阈值)就加载下一页,配合 loading 状态防止重复触发。"
+ },
+ {
+ "id": 155,
+ "lv": 6,
+ "q": "商品标题中要红色高亮\"热卖\"二字、其余部分灰色,用?",
+ "opts": [
+ "RichText + TextSpan 组合不同样式",
+ "拆成多个 Text 排一排",
+ "用换行符拼接字符串",
+ "TextStyle 做不到,只能整段同色"
+ ],
+ "ans": 0,
+ "exp": "RichText 的 TextSpan 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。"
+ },
+ {
+ "id": 156,
+ "lv": 6,
+ "q": "界面出现黄黑条纹溢出警告(RenderFlex overflowed),说明?",
+ "opts": [
+ "子组件超出了父组件给定的约束空间",
+ "主题颜色配置错误",
+ "内存泄漏",
+ "字体加载失败"
+ ],
+ "ans": 0,
+ "exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。"
+ },
+ {
+ "id": 157,
+ "lv": 6,
+ "q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?",
+ "opts": [
+ "cacheWidth / ResizeImage 限制解码尺寸",
+ "给图片加圆角",
+ "提高图片的清晰度",
+ "减少列表项数量"
+ ],
+ "ans": 0,
+ "exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 cacheWidth: 300 或 ResizeImage 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。"
+ }
+ ],
+ "lvNames": [
+ "Widget 基础",
+ "布局与组件",
+ "状态管理",
+ "路由与导航",
+ "数据与网络",
+ "实战综合"
+ ]
+ },
+ {
+ "id": 11,
+ "name": "Dart 基础巩固",
+ "lvNames": [
+ "基础语法",
+ "控制流与函数",
+ "集合",
+ "面向对象",
+ "空安全与异步",
+ "Dart 高级特性"
+ ],
+ "questions": [
+ {
+ "id": 158,
+ "lv": 1,
+ "q": "Dart 中布尔类型 bool 的字面量写法是?",
+ "opts": [
+ "TRUE",
+ "true",
+ "1",
+ "yes"
+ ],
+ "ans": 1,
+ "exp": "bool 只有两个值:true 和 false(全小写)。TRUE、1、yes 都不是 Dart 的合法写法。"
+ },
+ {
+ "id": 159,
+ "lv": 1,
+ "q": "要存储 3.14 这样的浮点数,应声明为什么类型?",
+ "opts": [
+ "float",
+ "decimal",
+ "big",
+ "double"
+ ],
+ "ans": 3,
+ "exp": "浮点数用 double,整数用 int,两者的共同父类是 num。Dart 没有 float、decimal 类型。"
+ },
+ {
+ "id": 160,
+ "lv": 1,
+ "q": "定义一个内容跨多行的字符串,应该用哪组引号包裹?",
+ "opts": [
+ "''' 三个单引号(或 \"\"\")",
+ "\" 单个双引号",
+ "' 单个单引号",
+ "` 反引号"
+ ],
+ "ans": 0,
+ "exp": "三个单引号 ''' 或三个双引号 \"\"\" 包裹的字符串可以直接跨行书写,换行会被保留;单引号/双引号字符串内不能直接换行。Dart 没有反引号模板字符串。"
+ },
+ {
+ "id": 161,
+ "lv": 1,
+ "q": "关于 Dart 字符串的特性,以下说法正确的是?",
+ "opts": [
+ "字符串创建后可随意修改内部字符",
+ "字符串只能用双引号包裹",
+ "字符串方法返回新字符串,原字符串不变",
+ "字符串本质是数字类型"
+ ],
+ "ans": 2,
+ "exp": "Dart 字符串不可变:toUpperCase() 等方法返回新字符串,原值不变。频繁拼接字符串时建议用 StringBuffer 提升性能。"
+ },
+ {
+ "id": 162,
+ "lv": 1,
+ "q": "String s = \"Hello\"; 要得到全大写的 \"HELLO\",应调用?",
+ "opts": [
+ "s.upper()",
+ "s.toUpperCase()",
+ "s.up()",
+ "String.upper(s)"
+ ],
+ "ans": 1,
+ "exp": "转大写用 s.toUpperCase(),转小写用 s.toLowerCase()。注意返回的是新字符串,原字符串不变。"
+ },
+ {
+ "id": 163,
+ "lv": 1,
+ "q": "把数字 42 转成字符串 \"42\",正确写法是?",
+ "opts": [
+ "(String) 42",
+ "int.stringify(42)",
+ "42.toString()",
+ "String.valueOf(42)"
+ ],
+ "ans": 2,
+ "exp": "调用 toString():42.toString()。注意 double 的 toString 会带小数:3.0.toString() 得到 \"3.0\"。(String) 强转、String.valueOf 是其它语言的习惯,Dart 没有。"
+ },
+ {
+ "id": 164,
+ "lv": 2,
+ "q": "取 a、b 中较大的值,用一行表达式应写?",
+ "opts": [
+ "a ?? b",
+ "a > b ? a : b",
+ "a || b",
+ "a && b"
+ ],
+ "ans": 1,
+ "exp": "三元运算符 条件 ? 表达式1 : 表达式2:条件成立取第一个,否则取第二个。a ?? b 是空值合并(a 为 null 才取 b),|| 和 && 是逻辑运算,都不能比较大小。"
+ },
+ {
+ "id": 165,
+ "lv": 2,
+ "q": "bool ok = false; print(ok && check()); 中 check() 会被执行吗?",
+ "opts": [
+ "会执行",
+ "报错",
+ "不会执行,&& 左侧为 false 时短路",
+ "执行两次"
+ ],
+ "ans": 2,
+ "exp": "短路求值:&& 左侧为 false 时结果已确定为 false,右侧不会执行;|| 左侧为 true 时同理。常用于安全判断:user != null && user.isVip。"
+ },
+ {
+ "id": 166,
+ "lv": 2,
+ "q": "循环里要跳过当前这一次迭代、继续下一次,用哪个关键字?",
+ "opts": [
+ "break",
+ "return",
+ "continue",
+ "skip"
+ ],
+ "ans": 2,
+ "exp": "continue 结束本次迭代、立即进入下一轮;break 直接退出整个循环;return 退出的是整个函数。"
+ },
+ {
+ "id": 167,
+ "lv": 2,
+ "q": "var f = (x) => x * 2; 这种没有名字的函数写法是?",
+ "opts": [
+ "命名函数",
+ "构造函数",
+ "静态函数",
+ "匿名函数(Lambda 表达式)"
+ ],
+ "ans": 3,
+ "exp": "箭头 => 配合无名参数列表就是匿名函数 / Lambda,常作为回调、比较器传递:list.sort((a, b) => a - b)。"
+ },
+ {
+ "id": 168,
+ "lv": 2,
+ "q": "void greet(String name, [String word = \"你好\"]),调用 greet(\"小明\") 时 word 的值是?",
+ "opts": [
+ "null",
+ "报错",
+ "\"你好\"",
+ "空字符串"
+ ],
+ "ans": 2,
+ "exp": "[ ] 包裹的是可选位置参数,不传时使用默认值 \"你好\"。它与命名参数({} 包裹、必须按名传)不同,是按顺序传参的:greet(\"小明\", \"早上好\")。"
+ },
+ {
+ "id": 169,
+ "lv": 2,
+ "q": "循环里要立即终止整个循环,用哪个关键字?",
+ "opts": [
+ "break",
+ "continue",
+ "exit",
+ "stop"
+ ],
+ "ans": 0,
+ "exp": "break 立即结束当前循环;continue 只跳过本次。break 也常用于 switch 的 case 结尾——Dart 要求非空 case 必须以 break、return、throw 等结束。"
+ },
+ {
+ "id": 170,
+ "lv": 3,
+ "q": "var data = {'name': '张三', 'age': 18}; data 的类型是?",
+ "opts": [
+ "Set",
+ "List",
+ "Map",
+ "Tuple"
+ ],
+ "ans": 2,
+ "exp": "{} 且含冒号键值对时创建 Map;无冒号的 {1, 2, 3} 是 Set;[] 是 List。Dart 没有 Tuple 类型。"
+ },
+ {
+ "id": 171,
+ "lv": 3,
+ "q": "var map = {'a': 1}; map['b'] = 2; 之后 map 的内容是?",
+ "opts": [
+ "{'a': 1}",
+ "{'b': 2}",
+ "报错",
+ "{'a': 1, 'b': 2}"
+ ],
+ "ans": 3,
+ "exp": "map[key] = value 写入时,key 已存在则更新值,不存在则新增条目,所以得到 {'a': 1, 'b': 2}。"
+ },
+ {
+ "id": 172,
+ "lv": 3,
+ "q": "['2024', '2025', '2026'].join('-') 的结果是?",
+ "opts": [
+ "'2024,2025,2026'",
+ "报错",
+ "'2024-2025-2026'",
+ "'202420252026'"
+ ],
+ "ans": 2,
+ "exp": "join(分隔符) 把列表元素按分隔符拼成一个字符串,得到 2024-2025-2026。注意不传参数时是无分隔符直接拼接。"
+ },
+ {
+ "id": 173,
+ "lv": 3,
+ "q": "从 [1, 2, 3, 4] 中挑出偶数(2、4),应调用哪个方法?",
+ "opts": [
+ "map",
+ "reduce",
+ "addAll",
+ "where"
+ ],
+ "ans": 3,
+ "exp": "where() 按条件过滤:[1,2,3,4].where((n) => n % 2 == 0) 得到 2、4。map() 是变换每个元素,reduce() 是聚合,addAll() 是批量添加。"
+ },
+ {
+ "id": 174,
+ "lv": 3,
+ "q": "var nums = [3, 1, 2]; nums.sort(); 排序后 nums 是?",
+ "opts": [
+ "[3, 1, 2]",
+ "[1, 2, 3]",
+ "报错",
+ "[2, 3, 1]"
+ ],
+ "ans": 1,
+ "exp": "sort() 原地升序排序,结果 [1, 2, 3]。注意它直接修改原列表并返回 void;要降序需传比较器:sort((a, b) => b - a)。"
+ },
+ {
+ "id": 175,
+ "lv": 3,
+ "q": "[1, 2, 3].contains(5) 返回?",
+ "opts": [
+ "true",
+ "5",
+ "null",
+ "false"
+ ],
+ "ans": 3,
+ "exp": "contains() 判断是否包含指定元素,包含返回 true、否则 false。判断 Map 的键用 containsKey,判断值用 containsValue。"
+ },
+ {
+ "id": 176,
+ "lv": 4,
+ "q": "class Point { Point(this.x, this.y); int x; int y; } 中 this.x 的作用是?",
+ "opts": [
+ "创建私有变量",
+ "引用父类构造函数",
+ "把构造参数自动赋给同名成员变量",
+ "声明静态字段"
+ ],
+ "ans": 2,
+ "exp": "构造参数写成 this.x 时,Dart 会把参数值自动赋给同名成员变量,省去手写 this.x = x; 的样板代码。"
+ },
+ {
+ "id": 177,
+ "lv": 4,
+ "q": "想给 Point 提供便捷构造 Point.origin()(x、y 初始化为 0),应该用?",
+ "opts": [
+ "静态方法",
+ "构造重载",
+ "默认构造函数",
+ "命名构造函数"
+ ],
+ "ans": 3,
+ "exp": "命名构造函数:Point.origin() : x = 0, y = 0;,调用 Point.origin()。Dart 不支持构造重载,用命名构造区分多种初始化方式。"
+ },
+ {
+ "id": 178,
+ "lv": 4,
+ "q": "统计 Point 类一共创建了多少个实例,计数变量应声明为?",
+ "opts": [
+ "final int count",
+ "static int count",
+ "int count",
+ "late int count"
+ ],
+ "ans": 1,
+ "exp": "static 成员属于类本身、所有实例共享一份,在构造函数里 count++ 即可统计,读取用 Point.count,不需要实例。"
+ },
+ {
+ "id": 179,
+ "lv": 4,
+ "q": "私有字段 _name 想对外只读(能读不能写),应该声明?",
+ "opts": [
+ "void name() {}",
+ "static String name;",
+ "String get name => _name;",
+ "set String name;"
+ ],
+ "ans": 2,
+ "exp": "getter 提供只读访问:String get name => _name;,外部用 p.name 读取;需要可写时再补 set name(String v)。"
+ },
+ {
+ "id": 180,
+ "lv": 4,
+ "q": "@override 注解放在重写方法上的作用是?",
+ "opts": [
+ "调用父类方法",
+ "声明重写父类方法,签名不符会编译报错",
+ "创建新方法",
+ "删除父类方法"
+ ],
+ "ans": 1,
+ "exp": "@override 声明\"此方法重写了父类/接口方法\",签名不一致时编译报错,防止手滑写错。调用父类实现用 super.xxx()。"
+ },
+ {
+ "id": 181,
+ "lv": 4,
+ "q": "子类构造函数里显式调用父类构造函数,用哪个关键字?",
+ "opts": [
+ "this",
+ "base",
+ "super",
+ "parent"
+ ],
+ "ans": 2,
+ "exp": "用 super:Dog(String n) : super(n);。父类有默认构造时可省略(自动调用);需要传参或指定父类命名构造时才显式写 super。"
+ },
+ {
+ "id": 182,
+ "lv": 5,
+ "q": "String? s;(可能为 null),直接写 print(s.length) 会?",
+ "opts": [
+ "输出 0",
+ "输出 null",
+ "编译报错:可空类型不能直接调用方法",
+ "正常运行"
+ ],
+ "ans": 2,
+ "exp": "空安全下 String? 可能为 null,不能直接调用方法,编译期直接报错——这是空安全的核心价值:把空指针问题拦在编译期。需先判空,或用 ? / !。"
+ },
+ {
+ "id": 183,
+ "lv": 5,
+ "q": "希望\"s 为 null 时整句返回 null,否则调用 length\",应该用?",
+ "opts": [
+ "s.length",
+ "s?.length",
+ "s!.length",
+ "s ?? length"
+ ],
+ "ans": 1,
+ "exp": "?. 安全调用:s 为 null 时结果为 null、不执行调用。常与 ?? 配合兜底:s?.length ?? 0。s!.length 是强制解包,s 为 null 时运行时报错。"
+ },
+ {
+ "id": 184,
+ "lv": 5,
+ "q": "int? x = null; x ??= 10; 的作用是?",
+ "opts": [
+ "把 x 强制转为非空",
+ "x 为 null 时才赋值为 10,否则保持原值",
+ "报错",
+ "x 永远等于 10"
+ ],
+ "ans": 1,
+ "exp": "??= 是\"若空则赋值\":x 为 null 时赋 10,若已有值则保持不变。等价于 x = x ?? 10,常用于给可空变量设置默认值。"
+ },
+ {
+ "id": 185,
+ "lv": 5,
+ "q": "执行 throw FormatException('格式错误'); 会?",
+ "opts": [
+ "打印日志后继续执行",
+ "抛出异常,未被捕获则程序中断",
+ "返回 null",
+ "编译错误"
+ ],
+ "ans": 1,
+ "exp": "throw 主动抛出异常并中断当前执行,外层 try-catch 能接住就继续运行,否则异常冒泡导致程序崩溃。适合校验不通过时立即终止。"
+ },
+ {
+ "id": 186,
+ "lv": 5,
+ "q": "处理可能出错的代码,正确的结构是?",
+ "opts": [
+ "catch { } try { }",
+ "if (error) { }",
+ "try { } catch (e) { } finally { }",
+ "except: { }"
+ ],
+ "ans": 2,
+ "exp": "try-catch-finally:try 放可能异常的代码,catch 捕获处理(可写 on FormatException 精确分类),finally 无论成败都会执行,适合释放资源。"
+ },
+ {
+ "id": 187,
+ "lv": 6,
+ "q": "定义星期、状态码这类固定的一组取值,应使用?",
+ "opts": [
+ "typedef",
+ "enum",
+ "const 列表",
+ "abstract 类"
+ ],
+ "ans": 1,
+ "exp": "enum 枚举:enum Status { pending, done, failed },用 Status.done 取值,配合 switch 模式匹配处理,比散落的字符串常量更类型安全。"
+ },
+ {
+ "id": 188,
+ "lv": 6,
+ "q": "写一个\"返回列表首个元素\"的通用函数,任意类型都能用,正确的签名是?",
+ "opts": [
+ "String first(List<String> list)",
+ "void first(List list)",
+ "<T> T first(List<T> list)",
+ "函数不能带类型参数"
+ ],
+ "ans": 2,
+ "exp": "泛型函数:<T> T first(List<T> list),T 是类型占位,调用时自动推断:first([1, 2]) 返回 int,first(['a']) 返回 String,既通用又保留类型信息。"
+ },
+ {
+ "id": 189,
+ "lv": 6,
+ "q": "typedef Compare = int Function(int a, int b); 的作用是?",
+ "opts": [
+ "创建了一个新函数",
+ "声明一个类",
+ "定义枚举",
+ "给函数类型起别名"
+ ],
+ "ans": 3,
+ "exp": "typedef 给函数类型起别名:之后 Compare cmp; 或作为参数类型 void sort(Compare cmp),签名一目了然。Dart 2.13+ 还能给普通类型起别名。"
+ },
+ {
+ "id": 190,
+ "lv": 6,
+ "q": "var (name, age) = ('张三', 18); 之后 name 和 age 的值是?",
+ "opts": [
+ "name=18,age='张三'",
+ "name='张三',age=18",
+ "报错:不能解构",
+ "两者都是 null"
+ ],
+ "ans": 1,
+ "exp": "这是 Dart 3 的解构:把 Record ('张三', 18) 按位置拆开,name='张三'、age=18。函数返回 Record 时配合解构取值非常简洁。"
+ }
+ ]
+ }
+ ],
+ "nextSetId": 12,
+ "nextQId": 191
+}
\ No newline at end of file
diff --git a/scripts/data/set-dart-dev.json b/scripts/data/set-dart-dev.json
new file mode 100644
index 0000000..5c4b216
--- /dev/null
+++ b/scripts/data/set-dart-dev.json
@@ -0,0 +1,216 @@
+{
+ "name": "Dart 开发实战",
+ "lvNames": ["基础语法", "控制流与函数", "集合", "面向对象", "空安全与异步", "Dart 高级特性"],
+ "questions": [
+ {
+ "lv": 1,
+ "q": "开发注册页时,用户输入的年龄是字符串,需要转成 int 保存。已知 String ageText = \"18\";,正确的转换写法是?",
+ "opts": ["int(ageText)", "toInt(ageText)", "int.parse(ageText)", "ageText.parseInt()"],
+ "ans": 2,
+ "exp": "int.parse() 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 int.tryParse()(失败返回 null)。注意 Dart 没有 \"18\".parseInt() 这种写法,int(...) 也不是转换函数。"
+ },
+ {
+ "lv": 1,
+ "q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?",
+ "opts": ["final String api = \"https://api.example.com\";", "const String api = \"https://api.example.com\";", "String api = \"https://api.example.com\";", "var api = \"https://api.example.com\";"],
+ "ans": 1,
+ "exp": "const 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。final 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。"
+ },
+ {
+ "lv": 1,
+ "q": "你在重构代码时写下:var x = 10; x = \"hello\"; 会发生什么?",
+ "opts": ["正常运行,x 变成字符串", "编译报错:x 已被推断为 int 类型", "运行时报错", "x 自动变为 null"],
+ "ans": 1,
+ "exp": "var 声明时由初始值自动推断类型,推断后类型就固定了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 dynamic 或 Object?。"
+ },
+ {
+ "lv": 1,
+ "q": "给变量起名时,以下哪个不是合法的 Dart 标识符?",
+ "opts": ["_score", "2x", "中文名", "name_2"],
+ "ans": 1,
+ "exp": "Dart 标识符不能以数字开头。_score 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。"
+ },
+ {
+ "lv": 1,
+ "q": "结算功能计算人均消费:int total = 10; int people = 4; double avg = total / people; 打印 avg 得到?",
+ "opts": ["2", "2.5", "2.0", "报错"],
+ "ans": 1,
+ "exp": "Dart 中 int 除以 int 的结果一定是 double:10 / 4 = 2.5。想得到整除结果用 ~/:10 ~/ 4 == 2,取余用 %。"
+ },
+ {
+ "lv": 2,
+ "q": "写优惠逻辑:订单数 > 10 打 9 折,> 50 打 8 折。对 orders = 60,以下代码会得到什么折扣?double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }",
+ "opts": ["0.8", "0.9", "1.0", "0.7"],
+ "ans": 1,
+ "exp": "if-else 链从上到下匹配,命中即止:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。"
+ },
+ {
+ "lv": 2,
+ "q": "接口返回 Map<String, dynamic> data,想遍历所有 key 打印出来,以下哪种写法正确?",
+ "opts": ["for (var k in data) print(k);", "for (var k in data.keys) print(k);", "data.keys.forEach(print);", "B 和 C 都可以"],
+ "ans": 3,
+ "exp": "Map 本身不能直接 for-in 遍历,要先取 keys / values / entries。B 是标准写法;C 中 forEach(print) 利用了函数引用,同样正确。两种都行,按团队规范选择。"
+ },
+ {
+ "lv": 2,
+ "q": "写一个返回用户名的函数:String getUserName() { },忘了写 return,会怎样?",
+ "opts": ["返回 null", "编译报错", "运行时崩溃", "自动返回空字符串"],
+ "ans": 1,
+ "exp": "空安全下,声明了非空返回类型 String 的函数必须有 return,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 String?。"
+ },
+ {
+ "lv": 2,
+ "q": "问候函数:void greet(String name, {String greeting = \"你好\"}),调用 greet(\"小明\", \"早上好\") 会发生什么?",
+ "opts": ["正常执行,greeting 为 \"早上好\"", "编译报错:命名参数不能按位置传参", "greeting 变为 null", "编译警告但能运行"],
+ "ans": 1,
+ "exp": "{} 包裹的是命名参数,必须写成 greet(\"小明\", greeting: \"早上好\");[] 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。"
+ },
+ {
+ "lv": 2,
+ "q": "写\"重试发送验证码\"逻辑:要求至少发送一次,失败则继续重试。哪种循环语义最贴切?",
+ "opts": ["while", "do-while", "for", "repeat"],
+ "ans": 1,
+ "exp": "do-while 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。"
+ },
+ {
+ "lv": 3,
+ "q": "实现\"删除待办\"功能,list.remove(3) 和 list.removeAt(3) 的区别是?",
+ "opts": ["remove 按值删除,removeAt 按下标删除", "两者完全相同", "remove 删下标,removeAt 按值删除", "remove 会删除所有等于 3 的元素"],
+ "ans": 0,
+ "exp": "remove(value) 删除第一个值相等的元素并返回是否成功;removeAt(index) 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。"
+ },
+ {
+ "lv": 3,
+ "q": "解析接口数据:用 data[\"name\"] 取一个不存在的 key 时,返回什么?",
+ "opts": ["抛异常", "null", "空字符串", "抛出 NoSuchMethodError"],
+ "ans": 1,
+ "exp": "Map 用 [] 取值时,key 不存在返回 null 而不是抛异常。所以 data[\"name\"] 可能为 null,取值后判空再使用是常规操作(配合 ?? 兜底)。"
+ },
+ {
+ "lv": 3,
+ "q": "配置列表按条件插入元素:var l = [1, 2, if (isVip) 3]; 当 isVip 为 false 时 l 是?",
+ "opts": ["[1, 2]", "[1, 2, 3]", "[1, 2, false]", "编译错误"],
+ "ans": 0,
+ "exp": "这是 collection-if 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 ... 展开和 collection-for 一起使用。"
+ },
+ {
+ "lv": 3,
+ "q": "两个用户各自关注了一批标签,要找出共同关注的标签,用哪种写法?",
+ "opts": ["a & b", "a.intersect(b)", "a.toSet().intersection(b.toSet())", "a.union(b)"],
+ "ans": 2,
+ "exp": "List 没有交集方法,要先转 Set。intersection() 返回两个集合的共同元素;union() 是并集。注意方法名是 intersection(),不是其它语言的 intersect()。"
+ },
+ {
+ "lv": 3,
+ "q": "计算购物车总价:List<double> prices = []; 空列表调用 prices.reduce((a, b) => a + b) 会?",
+ "opts": ["返回 0", "抛出 StateError 异常", "返回 null", "返回空列表"],
+ "ans": 1,
+ "exp": "reduce 要求列表至少有一个元素,空列表直接抛 \"Bad state: No element\"。更稳的写法是 fold(0, (a, b) => a + b):带初始值,空列表返回 0。"
+ },
+ {
+ "lv": 4,
+ "q": "写日期工具类,希望 DateUtil.parse(\"2024-01-01\") 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?",
+ "opts": ["factory 构造函数", "普通构造函数", "命名构造函数", "const 构造函数"],
+ "ans": 0,
+ "exp": "factory 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。"
+ },
+ {
+ "lv": 4,
+ "q": "日志打印、权限检查这类能力要被多个互不相关的类复用,又不想强制继承关系,Dart 的推荐做法是?",
+ "opts": ["extends 继承", "implements 实现", "with 混入 Mixin", "abstract 抽象类"],
+ "ans": 2,
+ "exp": "Mixin(with)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 class _X with RouteAware。"
+ },
+ {
+ "lv": 4,
+ "q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?",
+ "opts": ["private String _token;", "String _token;", "-String token;", "#token"],
+ "ans": 1,
+ "exp": "Dart 没有 private 关键字,以下划线开头表示库内私有:_token。注意这是\"库\"级别私有(同文件可见),不是类级别。"
+ },
+ {
+ "lv": 4,
+ "q": "用户类的 name 字段希望赋值时自动去掉首尾空格,最佳做法是?",
+ "opts": ["声明 getter/setter,在 setter 里 trim", "直接写公共字段,调用方自己 trim", "用 final 禁止修改", "用 static 修饰"],
+ "ans": 0,
+ "exp": "setter 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:String get name 与 set name(String v)。"
+ },
+ {
+ "lv": 4,
+ "q": "class Dog implements Animal 与 extends 继承的关键区别是?",
+ "opts": ["implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", "两者完全一样", "implements 只能用于抽象类", "extends 要求实现全部成员"],
+ "ans": 0,
+ "exp": "implements 把 Animal 当接口:所有成员都要自己重写,即使父类已有实现;extends 继承实现,只覆写需要改的部分。implements 用于\"规定能力\",extends 用于\"复用实现\"。"
+ },
+ {
+ "lv": 5,
+ "q": "用户对象可能为 null:User? u = await fetchUser();,以下哪种使用方式更安全?",
+ "opts": ["if (u != null) { print(u.name); } 判空后直接用 u", "if (u != null) { print(u!.name); } 必须加 !", "print(u?.name ?? \"\");", "A 和 C 都可以"],
+ "ans": 3,
+ "exp": "A:局部变量判空后编译器会自动提升为非空类型,直接使用即可;C:?. 安全调用 + ?? 兜底,一行搞定。只有被闭包捕获的变量才需要 !。"
+ },
+ {
+ "lv": 5,
+ "q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?",
+ "opts": ["Future.wait([fetchUser(), fetchOrders()])", "串行写两个 await", "Future.any", "Future.delayed 等待"],
+ "ans": 0,
+ "exp": "Future.wait 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。Future.any 只等第一个完成的。"
+ },
+ {
+ "lv": 5,
+ "q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?",
+ "opts": ["late String token;", "String? token = \"\";", "dynamic token;", "final token = null;"],
+ "ans": 0,
+ "exp": "late 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。"
+ },
+ {
+ "lv": 5,
+ "q": "把解析结果转成目标类型:var age = data[\"age\"] as int;,当 age 实际是 String 时会发生?",
+ "opts": ["返回 null", "抛出 TypeError(类型转换失败)", "静默忽略", "自动转成 int"],
+ "ans": 1,
+ "exp": "as 是强制类型转换,类型不符立即抛异常。安全做法:先用 is 判断类型,或先统一规范数据(如 num 一律 .toInt())。"
+ },
+ {
+ "lv": 5,
+ "q": "对可能为 null 的 String? s 使用 s!,若 s 确实为 null 会?",
+ "opts": ["返回 null", "抛出 TypeError(空检查失败)", "静默继续", "编译错误"],
+ "ans": 1,
+ "exp": "! 是强制解包运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 ?? 的地方尽量别用 !,减少运行时崩溃点。"
+ },
+ {
+ "lv": 6,
+ "q": "Flutter 中常见 ButtonStyle()..backgroundColor = ... ..shape = ... 写法,.. 级联操作符的核心行为是?",
+ "opts": ["对同一对象连续操作且返回对象本身", "链式调用并返回最后一次结果", "只能用于 List", "与 ... 展开等价"],
+ "ans": 0,
+ "exp": "级联操作符 .. 对同一对象连续执行多个操作,不改变返回值(始终是原对象),适合集中配置对象。. 返回的是方法调用的返回值,两者行为不同。"
+ },
+ {
+ "lv": 6,
+ "q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?",
+ "opts": ["Map<String, dynamic>", "Record 记录类型:(String name, int age)", "List<dynamic>", "用全局变量"],
+ "ans": 1,
+ "exp": "Record 是 Dart 3 的轻量聚合类型:var r = (\"张三\", 18); 或命名字段 (name: \"张三\", age: 18),用 r.$1 / r.name 取值,还能配合模式匹配解构,比 Map 更类型安全。"
+ },
+ {
+ "lv": 6,
+ "q": "Dart 3 的 switch 可以直接当表达式赋值:var s = switch (status) { \"ok\" => \"成功\", _ => \"未知\" }; 这种写法?",
+ "opts": ["合法:switch 表达式返回一个值", "非法:switch 不能作表达式", "必须每个分支加 break", "只能匹配字符串"],
+ "ans": 0,
+ "exp": "switch 表达式用 => 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。"
+ },
+ {
+ "lv": 6,
+ "q": "想给系统类 String 加一个 isEmail() 方法,但 String 不能改源码,用?",
+ "opts": ["extension 扩展:extension EmailCheck on String { bool isEmail() {...} }", "继承 String", "用 mixin 混入", "写静态工具类"],
+ "ans": 0,
+ "exp": "extension 可以不修改原类给已有类型添加方法,之后 \"a@b.com\".isEmail() 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。"
+ },
+ {
+ "lv": 6,
+ "q": "泛型容器 class Box<T extends num> {} 中 extends num 的含义是?",
+ "opts": ["T 只能是 int、double 等数值类型", "T 可以是任意类型", "T 必须可空", "T 必须是集合类型"],
+ "ans": 0,
+ "exp": "泛型约束限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 Object?。"
+ }
+ ]
+}
diff --git a/scripts/data/set-flutter-dev.json b/scripts/data/set-flutter-dev.json
new file mode 100644
index 0000000..97cfe15
--- /dev/null
+++ b/scripts/data/set-flutter-dev.json
@@ -0,0 +1,216 @@
+{
+ "name": "Flutter 开发实战",
+ "lvNames": ["Widget 基础", "布局与组件", "状态管理", "路由与导航", "数据与网络", "实战综合"],
+ "questions": [
+ {
+ "lv": 1,
+ "q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?",
+ "opts": ["void main() => runApp(MyApp());", "void main() { MyApp(); }", "runApp() { MyApp }", "main() { MaterialApp(); }"],
+ "ans": 0,
+ "exp": "Dart 程序入口是 main(),Flutter 通过 runApp(Widget) 把根组件挂载到引擎上。注意 MyApp() 是创建 Widget 实例,不能少了括号。"
+ },
+ {
+ "lv": 1,
+ "q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?",
+ "opts": ["StatelessWidget", "StatefulWidget", "Container", "Scaffold"],
+ "ans": 1,
+ "exp": "有动态变化的数据(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 setState() 刷新。没有可变状态时应优先用 StatelessWidget。"
+ },
+ {
+ "lv": 1,
+ "q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?",
+ "opts": ["Text", "Label", "Paragraph", "RichText"],
+ "ans": 0,
+ "exp": "Text 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。"
+ },
+ {
+ "lv": 1,
+ "q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?",
+ "opts": ["修改 State 字段后调用 setState()", "直接修改字段就行", "重启整个 App", "修改 Widget 的构造参数"],
+ "ans": 0,
+ "exp": "State 里的字段只是普通数据,必须调用 setState() 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建当前 State 的子树,不是整棵树。"
+ },
+ {
+ "lv": 1,
+ "q": "要展示 10000 条商品记录,性能最优的做法是?",
+ "opts": ["ListView.builder(按需构建可见项)", "Column 里循环放 10000 个组件", "SingleChildScrollView 包 Column", "GridView.count 全部构建"],
+ "ans": 0,
+ "exp": "ListView.builder 只构建当前可见的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。"
+ },
+ {
+ "lv": 2,
+ "q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?",
+ "opts": ["Column", "Row", "Stack", "Wrap"],
+ "ans": 0,
+ "exp": "Column 垂直方向排列子组件,Row 水平方向,Stack 叠加并支持定位,Wrap 在空间不足时自动换行。"
+ },
+ {
+ "lv": 2,
+ "q": "Row 里想让一个按钮占满剩余宽度(剩余空间全给它),按钮外层套?",
+ "opts": ["Expanded", "Padding", "Center", "Spacer"],
+ "ans": 0,
+ "exp": "Expanded 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。"
+ },
+ {
+ "lv": 2,
+ "q": "Column 里有几个按钮,想让它们在水平方向居中,应该设置?",
+ "opts": ["crossAxisAlignment: CrossAxisAlignment.center", "mainAxisAlignment: MainAxisAlignment.center", "alignment: Alignment.center", "textAlign: TextAlign.center"],
+ "ans": 0,
+ "exp": "Column 的主轴是垂直方向、交叉轴是水平方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。"
+ },
+ {
+ "lv": 2,
+ "q": "封面图要铺满整个区域且不拉伸变形(多出的部分裁掉),fit 参数应该用?",
+ "opts": ["BoxFit.cover", "BoxFit.fill", "BoxFit.none", "BoxFit.scaleDown"],
+ "ans": 0,
+ "exp": "cover:等比缩放填满、超出裁剪,常用于头像、封面;fill 会拉伸变形;contain 完整显示但可能留白;none 不缩放。"
+ },
+ {
+ "lv": 2,
+ "q": "商品名过长时希望单行显示,超出部分显示省略号,Text 的配置是?",
+ "opts": ["maxLines: 1, overflow: TextOverflow.ellipsis", "只设 overflow: TextOverflow.clip", "只设 maxLines: 3", "softWrap: false 就够了"],
+ "ans": 0,
+ "exp": "maxLines 限制最大行数 + overflow: TextOverflow.ellipsis 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。"
+ },
+ {
+ "lv": 3,
+ "q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?",
+ "opts": ["TextEditingController", "每次读 onChanged 参数", "GlobalKey 临时查找", "无法实现"],
+ "ans": 0,
+ "exp": "TextEditingController 是输入框的\"数据源\":TextEditingController(text: \"预填\") 初始化,controller.text 读取,controller.clear() 清空。用完后在 dispose 里释放。"
+ },
+ {
+ "lv": 3,
+ "q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?",
+ "opts": ["dispose()", "initState()", "build()", "didChangeDependencies()"],
+ "ans": 0,
+ "exp": "dispose() 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。"
+ },
+ {
+ "lv": 3,
+ "q": "登录状态、用户信息要被多个页面共享,又不想层层传构造参数,项目常用方案是?",
+ "opts": ["Provider / InheritedWidget", "定义全局变量直接读写", "每个页面重新请求接口", "把状态塞进路由参数"],
+ "ans": 0,
+ "exp": "Flutter 官方的底层方案是 InheritedWidget,Provider 是对它的优雅封装:共享数据放顶层,子组件 context.watch() 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。"
+ },
+ {
+ "lv": 3,
+ "q": "商品列表页要支持下拉刷新,应该把 ListView 包进哪个组件?",
+ "opts": ["RefreshIndicator", "GestureDetector", "AnimatedList", "Scrollbar"],
+ "ans": 0,
+ "exp": "RefreshIndicator 包住可滚动组件,配合 onRefresh 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。"
+ },
+ {
+ "lv": 3,
+ "q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?",
+ "opts": ["WidgetsBindingObserver", "RouteObserver", "NavigatorObserver", "MediaQuery"],
+ "ans": 0,
+ "exp": "给 State 混入 with WidgetsBindingObserver,在 didChangeAppLifecycleState() 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。"
+ },
+ {
+ "lv": 4,
+ "q": "列表页点击商品跳转详情页,标准写法是?",
+ "opts": ["Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))", "Navigator.go(DetailPage(id: id))", "Route.open(DetailPage())", "DetailPage(id: id).show()"],
+ "ans": 0,
+ "exp": "Navigator.push 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 Navigator.pop(context)。"
+ },
+ {
+ "lv": 4,
+ "q": "详情页编辑完要把结果回传给列表页,详情页返回时应该写?",
+ "opts": ["Navigator.pop(context, result)", "Navigator.back(result)", "Navigator.pushResult(result)", "context.popAndPush(result)"],
+ "ans": 0,
+ "exp": "pop(context, 返回值) 的第二个参数就是回传的数据,列表页 await push(...) 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。"
+ },
+ {
+ "lv": 4,
+ "q": "跳转时需要传递商品 id、名称等参数,团队协作中最推荐的传参方式是?",
+ "opts": ["DetailPage(id: 1, name: \"商品\") 构造参数传递", "全局变量临时存", "static 静态字段存", "URL 字符串拼接"],
+ "ans": 0,
+ "exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。"
+ },
+ {
+ "lv": 4,
+ "q": "在 App 里注册命名路由(如 /login、/home),配置位置是?",
+ "opts": ["MaterialApp(routes: {...})", "main() 函数里", "runApp() 参数", "每个页面里各自声明"],
+ "ans": 0,
+ "exp": "命名路由集中配置在 MaterialApp 的 routes 表,之后用 Navigator.pushNamed(context, \"/login\") 跳转;需要传参时配合 arguments 参数。"
+ },
+ {
+ "lv": 4,
+ "q": "跳到选择页,用户选完返回后要根据选择结果继续处理,列表页怎么写?",
+ "opts": ["final r = await Navigator.push(...); 拿到返回值再处理", "无法等待结果", "用 setState 轮询", "跳转时把回调函数传过去"],
+ "ans": 0,
+ "exp": "push 返回 Future<T>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。"
+ },
+ {
+ "lv": 5,
+ "q": "接口返回 JSON 字符串,执行 var data = jsonDecode(text); 后 data 的类型是?",
+ "opts": ["dynamic(实际可能是 Map 或 List)", "String", "Map<String, dynamic>", "Object"],
+ "ans": 0,
+ "exp": "jsonDecode 的返回类型是 dynamic:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:jsonDecode(text) as Map<String, dynamic>。"
+ },
+ {
+ "lv": 5,
+ "q": "要把用户对象 User(id: 1, name: \"张三\") 转成 JSON 字符串发给服务器,正确流程是?",
+ "opts": ["给 User 写 toJson(),再 jsonEncode(user.toJson())", "jsonEncode(user) 直接序列化", "user.toString()", "JSON.stringify(user)"],
+ "ans": 0,
+ "exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 Map<String, dynamic> toJson(),再用 jsonEncode 转字符串;反序列化则对应 User.fromJson(Map) 工厂构造。"
+ },
+ {
+ "lv": 5,
+ "q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?",
+ "opts": ["用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", "延迟 3 秒自动重试"],
+ "ans": 0,
+ "exp": "try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" } 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。"
+ },
+ {
+ "lv": 5,
+ "q": "请求进行中需要展示\"加载中\"状态,最合适用?",
+ "opts": ["CircularProgressIndicator", "Text(\"加载中...\")", "Opacity 半透明遮罩", "AnimatedContainer 闪烁"],
+ "ans": 0,
+ "exp": "Material 标准的加载指示器是 CircularProgressIndicator(转圈)和 LinearProgressIndicator(条),配合 loading 状态变量做条件渲染。"
+ },
+ {
+ "lv": 5,
+ "q": "http 请求要设置 10 秒超时,正确写法是?",
+ "opts": ["await http.get(url).timeout(Duration(seconds: 10))", "http.get(url, timeout: 10)", "Timer(Duration(seconds: 10), ...)", "Future.any 自动超时"],
+ "ans": 0,
+ "exp": "Future 自带 .timeout():超时抛 TimeoutException,可再接 onTimeout 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。"
+ },
+ {
+ "lv": 6,
+ "q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?",
+ "opts": ["Future.wait([...]) 并发等待", "三个 await 串行", "依次 catch 分别显示", "用 Future.any"],
+ "ans": 0,
+ "exp": "并发场景用 Future.wait,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。"
+ },
+ {
+ "lv": 6,
+ "q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?",
+ "opts": ["ScrollController 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", "GestureDetector 监听手势"],
+ "ans": 0,
+ "exp": "给 ListView 挂 ScrollController,在 addListener 里判断 position.pixels >= position.maxScrollExtent - 200(预留阈值)就加载下一页,配合 loading 状态防止重复触发。"
+ },
+ {
+ "lv": 6,
+ "q": "商品标题中要红色高亮\"热卖\"二字、其余部分灰色,用?",
+ "opts": ["RichText + TextSpan 组合不同样式", "拆成多个 Text 排一排", "用换行符拼接字符串", "TextStyle 做不到,只能整段同色"],
+ "ans": 0,
+ "exp": "RichText 的 TextSpan 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。"
+ },
+ {
+ "lv": 6,
+ "q": "界面出现黄黑条纹溢出警告(RenderFlex overflowed),说明?",
+ "opts": ["子组件超出了父组件给定的约束空间", "主题颜色配置错误", "内存泄漏", "字体加载失败"],
+ "ans": 0,
+ "exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。"
+ },
+ {
+ "lv": 6,
+ "q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?",
+ "opts": ["cacheWidth / ResizeImage 限制解码尺寸", "给图片加圆角", "提高图片的清晰度", "减少列表项数量"],
+ "ans": 0,
+ "exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 cacheWidth: 300 或 ResizeImage 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。"
+ }
+ ]
+}
diff --git a/scripts/extract-questions.js b/scripts/extract-questions.js
new file mode 100644
index 0000000..7ccd904
--- /dev/null
+++ b/scripts/extract-questions.js
@@ -0,0 +1,31 @@
+const fs = require('fs');
+const vm = require('vm');
+
+const html = fs.readFileSync('index.html', 'utf8');
+const start = html.indexOf('const questions');
+const arrStart = html.indexOf('[', start);
+let depth = 0, inStr = false, esc = false, end = -1;
+for (let i = arrStart; i < html.length; i++) {
+ const c = html[i];
+ if (inStr) {
+ if (esc) { esc = false; continue; }
+ if (c === '\\') { esc = true; continue; }
+ if (c === "'") inStr = false;
+ continue;
+ }
+ if (c === "'") { inStr = true; continue; }
+ if (c === '[') depth++;
+ else if (c === ']') { depth--; if (depth === 0) { end = i; break; } }
+}
+let src = html.slice(arrStart, end + 1);
+src = src.replace(/\/\/\s*=====[^']*?=====\s*/g, ' ');
+
+const questions = vm.runInNewContext(src);
+const withIds = questions.map((q, i) => ({ id: i + 1, ...q }));
+const data = {
+ sets: [{ id: 1, name: 'Dart & Flutter 学习测验', questions: withIds }],
+ nextSetId: 2,
+ nextQId: withIds.length + 1
+};
+fs.writeFileSync('questions.json', JSON.stringify(data, null, 2), 'utf8');
+console.log('已提取', withIds.length, '道题 -> questions.json');
diff --git a/scripts/fix-escaping.js b/scripts/fix-escaping.js
new file mode 100644
index 0000000..f83d213
--- /dev/null
+++ b/scripts/fix-escaping.js
@@ -0,0 +1,34 @@
+const fs = require('fs');
+const path = require('path');
+
+(async () => {
+ const { escapeSmartHtml, decodeEntities } = await import('../web/src/htmlUtil.mjs');
+
+ const file = path.join(__dirname, '..', 'questions.json');
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
+ let fixed = 0;
+
+ for (const set of data.sets) {
+ for (const q of set.questions) {
+ const nq = escapeSmartHtml(decodeEntities(q.q));
+ const nx = escapeSmartHtml(decodeEntities(q.exp || ''));
+ if (nq !== q.q) { q.q = nq; fixed++; }
+ if (nx !== q.exp) { q.exp = nx; fixed++; }
+ const no = q.opts.map(o => escapeSmartHtml(decodeEntities(o)));
+ if (JSON.stringify(no) !== JSON.stringify(q.opts)) { q.opts = no; fixed++; }
+ }
+ }
+
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
+ console.log('清洗完成,修正字段数:', fixed);
+ console.log('幂等检查(再跑一遍应无变化)...');
+ let again = 0;
+ for (const set of data.sets) {
+ for (const q of set.questions) {
+ if (escapeSmartHtml(decodeEntities(q.q)) !== q.q) again++;
+ if (escapeSmartHtml(decodeEntities(q.exp || '')) !== q.exp) again++;
+ if (JSON.stringify(q.opts.map(o => escapeSmartHtml(decodeEntities(o)))) !== JSON.stringify(q.opts)) again++;
+ }
+ }
+ console.log(again === 0 ? '幂等 ✓' : `仍会变化: ${again} 处 ✗`);
+})();
diff --git a/scripts/import-sets.js b/scripts/import-sets.js
new file mode 100644
index 0000000..2b5db1d
--- /dev/null
+++ b/scripts/import-sets.js
@@ -0,0 +1,38 @@
+const fs = require('fs');
+const path = require('path');
+
+const BASE = 'http://localhost:3030';
+const FILES = ['set-dart-dev.json', 'set-flutter-dev.json'];
+
+async function main() {
+ const loginRes = await fetch(BASE + '/api/admin/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username: 'admin', password: 'admin123' })
+ });
+ const login = await loginRes.json();
+ if (!loginRes.ok) throw new Error('登录失败: ' + login.error);
+
+ for (const file of FILES) {
+ const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', file), 'utf8'));
+ const res = await fetch(BASE + '/api/admin/import', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' + login.token
+ },
+ body: JSON.stringify(data)
+ });
+ const result = await res.json();
+ if (!res.ok) {
+ console.log(`✗ ${data.name} 导入失败: ${result.error}`);
+ } else {
+ console.log(`✓ ${data.name}: 导入 ${result.imported} 题, 套题 id=${result.set.id}`);
+ }
+ }
+}
+
+main().catch(e => {
+ console.error(e.message);
+ process.exit(1);
+});
diff --git a/scripts/verify-questions.js b/scripts/verify-questions.js
new file mode 100644
index 0000000..2217346
--- /dev/null
+++ b/scripts/verify-questions.js
@@ -0,0 +1,48 @@
+const d = require('../questions.json');
+let total = 0, bad = [];
+for (const set of d.sets) {
+ for (const q of set.questions) {
+ total++;
+ if (!q.q || !Array.isArray(q.opts) || q.opts.length < 2) { bad.push(`[${set.name} #${q.id}] 选项不足`); continue; }
+ if (!Number.isInteger(q.ans) || q.ans < 0 || q.ans >= q.opts.length) { bad.push(`[${set.name} #${q.id}] ans 越界`); continue; }
+ if (!q.exp) { bad.push(`[${set.name} #${q.id}] 缺解析`); continue; }
+ if (!Number.isInteger(q.lv) || q.lv < 1 || q.lv > 6) { bad.push(`[${set.name} #${q.id}] lv 非法`); continue; }
+ const dup = q.opts.findIndex((o, i) => q.opts.indexOf(o) !== i);
+ if (dup !== -1) { bad.push(`[${set.name} #${q.id}] 选项重复: "${q.opts[dup]}"`); }
+ }
+}
+console.log('总题数:', total);
+if (bad.length) { console.log('问题:'); bad.forEach(b => console.log(' -', b)); }
+else console.log('结构校验全部通过');
+
+const checks = [
+ ['Dart 开发实战', 'var x = 10', 1],
+ ['Dart 开发实战', 'orders = 60', 1],
+ ['Dart 开发实战', 'remove(3)', 0],
+ ['Dart 开发实战', '不存在的 key', 1],
+ ['Dart 开发实战', 'if (isVip) 3', 0],
+ ['Dart 开发实战', 'reduce((a, b)', 1],
+ ['Dart 开发实战', 'factory 构造函数', 0],
+ ['Dart 开发实战', 'mixin', 2],
+ ['Dart 开发实战', 's!', 1],
+ ['Dart 开发实战', 'late String token', 0],
+ ['Flutter 开发实战', 'runApp(MyApp())', 0],
+ ['Flutter 开发实战', 'crossAxisAlignment', 0],
+ ['Flutter 开发实战', 'TextOverflow.ellipsis', 0],
+ ['Flutter 开发实战', 'RefreshIndicator', 0],
+ ['Flutter 开发实战', 'Navigator.pop(context, result)', 0],
+ ['Flutter 开发实战', 'jsonDecode(text)', 0],
+ ['Flutter 开发实战', '.timeout(Duration', 0],
+ ['Flutter 开发实战', 'cacheWidth', 0],
+ ['Flutter 开发实战', 'RichText', 0],
+ ['Flutter 开发实战', 'dispose()', 0]
+];
+let fail = 0;
+for (const [setName, keyword, expect] of checks) {
+ const set = d.sets.find(s => s.name === setName);
+ const q = set.questions.find(x => (x.q + x.opts.join('') + x.exp).includes(keyword));
+ if (!q) { console.log('✗ 未找到题:', setName, keyword); fail++; continue; }
+ const ok = q.ans === expect;
+ if (!ok) { console.log(`✗ [${setName}] ${keyword} → ans=${q.ans} 期望 ${expect},正确选项内容: "${q.opts[q.ans]}"`); fail++; }
+}
+console.log(fail ? `抽查 ${checks.length} 题,${fail} 题不符` : `抽查 ${checks.length} 道关键题答案全部正确`);
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..f8b3c67
--- /dev/null
+++ b/server.js
@@ -0,0 +1,238 @@
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const PORT = process.env.PORT || 3030;
+const ADMIN_USER = process.env.ADMIN_USER || 'admin';
+const ADMIN_PASS = process.env.ADMIN_PASS || 'admin123';
+const DATA_FILE = path.join(__dirname, 'questions.json');
+
+const app = express();
+app.use(express.json({ limit: '2mb' }));
+
+const DIST = path.join(__dirname, 'dist');
+if (fs.existsSync(DIST)) {
+ app.use(express.static(DIST));
+ app.get(/^\/(?!api\/).*/, (req, res) => {
+ res.sendFile(path.join(DIST, 'index.html'));
+ });
+} else {
+ console.warn('未找到 dist/ 目录,请先执行 npm run build 再访问页面');
+}
+
+function loadData() {
+ if (!fs.existsSync(DATA_FILE)) return { sets: [], nextSetId: 1, nextQId: 1 };
+ try {
+ const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
+ if (Array.isArray(raw)) {
+ const qs = raw;
+ const migrated = {
+ sets: [{ id: 1, name: 'Dart & Flutter 学习测验', questions: qs }],
+ nextSetId: 2,
+ nextQId: qs.length ? Math.max(...qs.map(q => q.id)) + 1 : 1
+ };
+ fs.writeFileSync(DATA_FILE, JSON.stringify(migrated, null, 2), 'utf8');
+ return migrated;
+ }
+ if (raw && Array.isArray(raw.sets)) return raw;
+ return { sets: [], nextSetId: 1, nextQId: 1 };
+ } catch {
+ return { sets: [], nextSetId: 1, nextQId: 1 };
+ }
+}
+
+const data = loadData();
+
+function saveData() {
+ fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
+}
+
+const tokens = new Set();
+
+function findQuestion(qid) {
+ for (const set of data.sets) {
+ const q = set.questions.find(x => x.id === qid);
+ if (q) return { set, q };
+ }
+ return null;
+}
+
+function toPublic(q) {
+ return { id: q.id, lv: q.lv, q: q.q, opts: q.opts };
+}
+
+function validateBody(body) {
+ const { lv, q, opts, ans, exp } = body || {};
+ if (!q || typeof q !== 'string' || q.trim() === '') return '题目内容不能为空';
+ if (!Array.isArray(opts) || opts.length < 2) return '至少需要 2 个选项';
+ if (opts.some(o => typeof o !== 'string' || o.trim() === '')) return '选项不能为空';
+ if (typeof ans !== 'number' || !Number.isInteger(ans) || ans < 0 || ans >= opts.length) return '答案序号不正确';
+ if (lv !== undefined && (!Number.isInteger(lv) || lv < 1 || lv > 6)) return '难度需为 1-6 的整数';
+ return null;
+}
+
+function buildQuestion(body) {
+ const { lv, q, opts, ans, exp } = body;
+ return { id: data.nextQId++, lv: lv === undefined ? 1 : lv, q: q.trim(), opts, ans, exp: (exp || '').trim() };
+}
+
+function validateLvNames(v) {
+ if (v === undefined || v === null) return null;
+ if (!Array.isArray(v) || v.length !== 6 || v.some(x => typeof x !== 'string' || !x.trim())) {
+ return '难度名称必须为 6 个非空字符串';
+ }
+ return null;
+}
+
+app.get('/api/sets', (req, res) => {
+ res.json({ sets: data.sets.map(s => ({ id: s.id, name: s.name, count: s.questions.length, lvNames: s.lvNames || null })) });
+});
+
+app.get('/api/questions', (req, res) => {
+ const setId = req.query.set ? Number(req.query.set) : (data.sets[0] ? data.sets[0].id : null);
+ const set = data.sets.find(s => s.id === setId);
+ if (!set) return res.status(404).json({ error: '套题不存在' });
+ res.json({
+ set: { id: set.id, name: set.name, lvNames: set.lvNames || null },
+ count: set.questions.length,
+ questions: set.questions.map(toPublic)
+ });
+});
+
+app.post('/api/check', (req, res) => {
+ const { id, answer } = req.body || {};
+ const hit = findQuestion(id);
+ if (!hit) return res.status(404).json({ error: '题目不存在' });
+ if (!Number.isInteger(answer)) return res.status(400).json({ error: 'answer 必须为数字' });
+ res.json({ correct: hit.q.ans === answer, answer: hit.q.ans, exp: hit.q.exp });
+});
+
+app.post('/api/admin/login', (req, res) => {
+ const { username, password } = req.body || {};
+ if (username === ADMIN_USER && password === ADMIN_PASS) {
+ const token = crypto.randomBytes(24).toString('hex');
+ tokens.add(token);
+ res.json({ token });
+ } else {
+ res.status(401).json({ error: '用户名或密码错误' });
+ }
+});
+
+function requireAuth(req, res, next) {
+ const token = (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
+ if (token && tokens.has(token)) return next();
+ res.status(401).json({ error: '未登录或登录已过期' });
+}
+
+app.get('/api/admin/sets', requireAuth, (req, res) => {
+ res.json({ sets: data.sets });
+});
+
+app.post('/api/admin/sets', requireAuth, (req, res) => {
+ const { name } = req.body || {};
+ if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' });
+ const lvErr = validateLvNames(req.body.lvNames);
+ if (lvErr) return res.status(400).json({ error: lvErr });
+ const set = { id: data.nextSetId++, name: name.trim(), questions: [] };
+ if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames;
+ data.sets.push(set);
+ saveData();
+ res.json(set);
+});
+
+app.put('/api/admin/sets/:id', requireAuth, (req, res) => {
+ const id = Number(req.params.id);
+ const set = data.sets.find(s => s.id === id);
+ if (!set) return res.status(404).json({ error: '套题不存在' });
+ const { name } = req.body || {};
+ if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' });
+ const lvErr = validateLvNames(req.body.lvNames);
+ if (lvErr) return res.status(400).json({ error: lvErr });
+ set.name = name.trim();
+ if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames;
+ saveData();
+ res.json(set);
+});
+
+app.delete('/api/admin/sets/:id', requireAuth, (req, res) => {
+ const id = Number(req.params.id);
+ const idx = data.sets.findIndex(s => s.id === id);
+ if (idx === -1) return res.status(404).json({ error: '套题不存在' });
+ data.sets.splice(idx, 1);
+ saveData();
+ res.json({ ok: true });
+});
+
+app.post('/api/admin/import', requireAuth, (req, res) => {
+ const { name, questions } = req.body || {};
+ if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' });
+ if (!Array.isArray(questions)) return res.status(400).json({ error: 'questions 必须为数组' });
+ const lvErr = validateLvNames(req.body.lvNames);
+ if (lvErr) return res.status(400).json({ error: lvErr });
+ for (let i = 0; i < questions.length; i++) {
+ const err = validateBody(questions[i]);
+ if (err) return res.status(400).json({ error: '第 ' + (i + 1) + ' 题格式错误:' + err });
+ }
+ const set = { id: data.nextSetId++, name: name.trim(), questions: questions.map(buildQuestion) };
+ if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames;
+ data.sets.push(set);
+ saveData();
+ res.json({ set: { id: set.id, name: set.name, count: set.questions.length }, imported: set.questions.length });
+});
+
+app.get('/api/admin/sets/:id/export', requireAuth, (req, res) => {
+ const id = Number(req.params.id);
+ const set = data.sets.find(s => s.id === id);
+ if (!set) return res.status(404).json({ error: '套题不存在' });
+ res.json({
+ name: set.name,
+ lvNames: set.lvNames || null,
+ exportedAt: new Date().toISOString(),
+ questions: set.questions.map(q => ({ lv: q.lv, q: q.q, opts: q.opts, ans: q.ans, exp: q.exp }))
+ });
+});
+
+app.post('/api/admin/sets/:id/questions', requireAuth, (req, res) => {
+ const id = Number(req.params.id);
+ const set = data.sets.find(s => s.id === id);
+ if (!set) return res.status(404).json({ error: '套题不存在' });
+ const err = validateBody(req.body);
+ if (err) return res.status(400).json({ error: err });
+ const item = buildQuestion(req.body);
+ set.questions.push(item);
+ saveData();
+ res.json(item);
+});
+
+app.put('/api/admin/questions/:qid', requireAuth, (req, res) => {
+ const hit = findQuestion(Number(req.params.qid));
+ if (!hit) return res.status(404).json({ error: '题目不存在' });
+ const err = validateBody(req.body);
+ if (err) return res.status(400).json({ error: err });
+ const { lv, q, opts, ans, exp } = req.body;
+ const item = hit.q;
+ item.lv = lv === undefined ? item.lv : lv;
+ item.q = q.trim();
+ item.opts = opts;
+ item.ans = ans;
+ item.exp = (exp || '').trim();
+ saveData();
+ res.json(item);
+});
+
+app.delete('/api/admin/questions/:qid', requireAuth, (req, res) => {
+ const hit = findQuestion(Number(req.params.qid));
+ if (!hit) return res.status(404).json({ error: '题目不存在' });
+ const idx = hit.set.questions.indexOf(hit.q);
+ hit.set.questions.splice(idx, 1);
+ saveData();
+ res.json({ ok: true });
+});
+
+app.listen(PORT, '0.0.0.0', () => {
+ console.log('测验系统已启动: http://localhost:' + PORT);
+ console.log('答题页: http://localhost:' + PORT + '/');
+ console.log('管理后台: http://localhost:' + PORT + '/admin.html');
+ console.log('管理员账号: ' + ADMIN_USER + ' / ' + ADMIN_PASS);
+});
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..7dc24f5
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ root: 'web',
+ plugins: [vue()],
+ build: {
+ outDir: '../dist',
+ emptyOutDir: true
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': 'http://localhost:3030'
+ }
+ }
+})
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..b40a325
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ | ID | 难度 | 题干 | 操作 |
|---|---|---|---|
| {{ q.id }} | +Lv{{ q.lv || 1 }} | +{{ stripHtmlAndDecode(q.q).slice(0, 60) }} | ++ + + | +