欢迎光临
我们一直在努力

【Xilem 0.4 基础语法学与练】第18课:map_state——派生数据的只读映射

一、本课目标

掌握 map_state 的用法——当子组件需要展示的数据不是状态中的某个字段,而是从状态计算出来的派生值时,lens 无能为力,map_state 才是正确的工具。

二、问题场景:lens 的边界在哪里?

回顾第17课的 lens,它的第二个参数(lens 闭包)必须返回父状态中某个字段的 &mut 引用:

lens(
|data: &mut i32| counter_view(data),
|data: &mut AppState| &mut data.count, // 只能返回字段的 &mut 引用
)

这在"子组件需要直接编辑某个字段"时完美工作。但考虑这个场景:

struct AppState {
celsius: f64,
}

你想在界面上同时显示摄氏度和华氏度。摄氏度是字段,可以直接用 lens 取。但华氏度不是字段——它是 celsius * 9.0 / 5.0 + 32.0 的计算结果。

你试着用 lens:

lens(
|data: &mut f64| label(format!("华氏: {:.1}°F", data)),
|data: &mut AppState| {
let fahrenheit = data.celsius * 9.0 / 5.0 + 32.0;
&mut fahrenheit // ❌ 编译错误!返回了局部变量的引用
},
)

编译器报错:returns a reference to data owned by the current function。这是第17课练习三已经讲过的问题——lens 闭包不能返回临时值的引用。

lens 的设计初衷是字段访问,不是数据变换。当子组件需要的数据需要经过计算才能得出时,lens 就到极限了。

map_state 就是为这个场景而生的。

三、map_state 的核心思想

map_state 和 lens 的根本区别在于映射方向:

lens——从父状态中取出一个字段的 &mut 引用,交给子组件。子组件可以直接修改这个字段。是双向的:读和写都通过同一个引用。

map_state——从父状态中计算出一个值,交给子组件。子组件只能读取这个值,不能修改它。是单向的:只能读,不能写。

用一句话概括:lens 用于"编辑字段",map_state 用于"展示派生数据"。

四、完整示例:温度转换器

4.1 Cargo.toml

[package]
name = "xilem-map-state-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
xilem = "0.4"
winit = "0.30"

4.2 main.rs

use winit::error::EventLoopError;
use xilem::view::{Axis, flex, label, text_button, lens, map_state};
use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};

#[derive(Default)]
struct AppState {
celsius: f64,
}

// —- 子组件 1:摄氏度编辑器(用 lens,编辑真实字段)—-
fn celsius_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("{:.1}°C", data)),
text_button("+1°C", |data: &mut f64| *data += 1.0),
text_button("-1°C", |data: &mut f64| *data -= 1.0),
))
}

// —- 子组件 2:华氏度显示器(用 map_state,只读派生值)—-
fn fahrenheit_display(data: &f64) -> impl WidgetView<()> + use<> {
label(format!("{:.1}°F", data))
}

// —- 根视图 —-
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
flex(Axis::Vertical, (
// lens:从 AppState 取出 &mut f64(celsius 字段),可编辑
lens(
|data: &mut f64| celsius_editor(data),
|data: &mut AppState| &mut data.celsius,
),

// map_state:从 AppState 计算出 f64(华氏度),只读
map_state(
|data: &f64| fahrenheit_display(data),
|data: &AppState| data.celsius * 9.0 / 5.0 + 32.0,
),

// 根视图自己的控件
text_button("设为 100°C", |data: &mut AppState| {
data.celsius = 100.0;
}),
text_button("设为 0°C", |data: &mut AppState| {
data.celsius = 0.0;
}),
))
}

fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(
AppState::default(),
app_logic,
WindowOptions::new("温度转换器"),
);
app.run_in(EventLoop::with_user_event())?;
Ok(())
}

五、逐行拆解

5.1 状态定义

#[derive(Default)]
struct AppState {
celsius: f64,
}

只有一个字段 celsius。华氏度不是字段,它是从 celsius 计算出来的。

5.2 摄氏度编辑器(lens 子组件)

fn celsius_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("{:.1}°C", data)),
text_button("+1°C", |data: &mut f64| *data += 1.0),
text_button("-1°C", |data: &mut f64| *data -= 1.0),
))
}

参数是 &mut f64——它可以读取和修改摄氏度。按钮的闭包通过 *data += 1.0 直接修改字段值。这是标准的 lens 用法:子组件拥有对真实字段的可变访问权。

5.3 华氏度显示器(map_state 子组件)

fn fahrenheit_display(data: &f64) -> impl WidgetView<()> + use<> {
label(format!("{:.1}°F", data))
}

注意参数类型:&f64,不是 &mut f64。它是不可变引用——只能读,不能写。这个组件的职责就是"显示一个数值",它不需要也不应该修改这个值。

5.4 根视图:lens 和 map_state 的配合

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
flex(Axis::Vertical, (
// lens:取出 &mut f64,可编辑
lens(
|data: &mut f64| celsius_editor(data),
|data: &mut AppState| &mut data.celsius,
),

// map_state:计算出 f64,只读
map_state(
|data: &f64| fahrenheit_display(data),
|data: &AppState| data.celsius * 9.0 / 5.0 + 32.0,
),

text_button("设为 100°C", |data: &mut AppState| {
data.celsius = 100.0;
}),
text_button("设为 0°C", |data: &mut AppState| {
data.celsius = 0.0;
}),
))
}

map_state 接收两个参数:

第一个参数:子 View 函数。它接收 &子状态类型(不可变引用),返回一个 WidgetView。

第二个参数:映射闭包。它接收 &父状态(不可变引用),返回一个owned 值(注意不是引用)。这个闭包可以包含任意计算逻辑——乘法、除法、字符串拼接、条件判断,什么都行。

对比 lens 的第二个参数:

// lens:返回 &mut 引用,只能指向已有字段
|data: &mut AppState| &mut data.celsius

// map_state:返回 owned 值,可以是任意计算结果
|data: &AppState| data.celsius * 9.0 / 5.0 + 32.0

六、数据流走一遍

  • 应用启动 → AppState { celsius: 0.0 }
  • app_logic 执行:
    • lens 取出 &mut data.celsius(值为 0.0)→ celsius_editor 显示 “0.0°C” + 两个按钮
    • map_state 计算 0.0 * 9.0 / 5.0 + 32.0 = 32.0 → fahrenheit_display 显示 “32.0°F”
  • 用户点击"+1°C" → celsius_editor 内部闭包执行 *data += 1.0 → celsius 变为 1.0
  • 框架检测到 AppState 变化 → 重新调用 app_logic
  • lens 取出 &mut data.celsius(值为 1.0)→ 显示 “1.0°C”
  • map_state 计算 1.0 * 9.0 / 5.0 + 32.0 = 33.8 → 显示 “33.8°F”
  • 摄氏度和华氏度同步更新
  • 关键点:华氏度的更新不需要任何额外代码。只要你修改了 celsius,map_state 的映射闭包就会在下一次 app_logic 执行时自动重新计算。

    七、运行

    cargo run

    弹出一个窗口,上方显示摄氏度数值和"+1°C""-1°C"按钮,中间显示华氏度数值(自动换算),下方是"设为 100°C"和"设为 0°C"快捷按钮。

    点击"+1°C",摄氏度加一,华氏度自动更新。点击"设为 100°C",摄氏度变为 100.0,华氏度自动变为 212.0。

    八、map_state 的底层原理

    map_state 在 xilem_core 中对应的 View 类型是 MapState

    pub struct MapState<C, F> {
    child: C,
    map_fn: F,
    }

    • C 是子 View 的类型
    • F 是映射闭包的类型

    当框架调用 build 或 rebuild 时,MapState View 会:

  • 用 map_fn 闭包从父状态中计算出子值(owned value)
  • 把这个值的 & 引用传给子 View 的 build/rebuild
  • 子 View 只能读取这个值,不能修改它(因为引用是不可变的)
  • 对比 Lens 的行为:

  • 用 lens 闭包从父状态中取出子字段的 &mut 引用
  • 把这个 &mut 引用传给子 View 的 build/rebuild
  • 子 View 可以读取和修改这个值(因为引用是可变的)
  • 核心差异在于引用的可变性:Lens 给子 View 的是 &mut,MapState 给子 View 的是 &。这决定了子 View 能不能修改数据。

    九、lens vs map_state 选型指南

    用 lens 的场景——子组件需要编辑状态中的某个字段。典型例子:输入框绑定到 name 字段、计数器绑定到 count 字段、开关绑定到 is_enabled 字段。

    用 map_state 的场景——子组件需要展示一个从状态计算出来的值。典型例子:摄氏度→华氏度换算、总价=单价×数量、进度百分比=已完成/总数×100。

    判断标准:问自己一个问题——"子组件需要修改这个值吗?"如果需要,用 lens;如果只需要展示,用 map_state。

    能不能用 map_state 替代 lens? 不能。map_state 给子组件的是 &(不可变引用),子组件无法修改数据。如果你的子组件有按钮、输入框等交互控件需要修改状态,必须用 lens。

    能不能用 lens 替代 map_state? 不能。lens 闭包必须返回字段的 &mut 引用,不能返回计算结果。如果你需要的值不是某个字段的直接引用,而是经过计算的,lens 做不到。

    十、练习

    练习一:填空题——补全 map_state 调用

    以下代码中有一个购物车状态,需要在界面上显示商品数量和小计金额。小计金额是派生数据(price × quantity)。请补全 app_logic 中的 map_state 调用。

    use xilem::view::{Axis, flex, label, text_button, lens, map_state};
    use xilem::{WidgetView};

    #[derive(Default)]
    struct Cart {
    price: f64,
    quantity: u32,
    }

    fn quantity_editor(data: &mut u32) -> impl WidgetView<u32> + use<> {
    flex(Axis::Horizontal, (
    label(format!("数量: {}", data)),
    text_button("+1", |data: &mut u32| *data += 1),
    text_button("-1", |data: &mut u32| {
    if *data > 0 { *data -= 1; }
    }),
    ))
    }

    fn subtotal_display(data: &f64) -> impl WidgetView<()> + use<> {
    label(format!("小计: {:.2} 元", data))
    }

    fn app_logic(data: &mut Cart) -> impl WidgetView<Cart> + use<> {
    flex(Axis::Vertical, (
    // 填空①:用 lens 把 quantity_editor 连接到 data.quantity
    lens(______, ______),

    // 填空②:用 map_state 计算小计(price × quantity),传给 subtotal_display
    map_state(______, ______),
    ))
    }

    要求:

    • 填空①:写出 lens 的两个参数
    • 填空②:写出 map_state 的两个参数(第二个参数是一个计算表达式)

    练习二:改造题——BMI 计算器

    要求:

  • 定义状态结构体:
  • struct BmiState {
    height_cm: f64, // 身高(厘米)
    weight_kg: f64, // 体重(公斤)
    }

  • 实现 Default,height_cm 默认为 170.0,weight_kg 默认为 65.0
  • 写一个 height_editor(data: &mut f64) 子组件,显示当前身高,提供"+1"和"-1"按钮
  • 写一个 weight_editor(data: &mut f64) 子组件,显示当前体重,提供"+0.5"和"-0.5"按钮
  • 写一个 bmi_display(data: &f64) 子组件,显示 BMI 值和分类(<18.5 偏瘦、18.5-24 正常、24-28 偏胖、≥28 肥胖)
  • 在 app_logic 中,用 lens 连接 height_editor 和 weight_editor,用 map_state 计算 BMI(公式:weight / (height_m * height_m),注意身高要从厘米转换为米)传给 bmi_display
  • // 你的代码:实现 Default、height_editor、weight_editor、bmi_display、app_logic、main

    练习三:思考题——map_state 的子组件能不能有按钮?

    假设你写了这样的代码:

    fn editable_display(data: &f64) -> impl WidgetView<()> + use<> {
    flex(Axis::Horizontal, (
    label(format!("值: {}", data)),
    text_button("+1", |data: &mut f64| *data += 1.0), // ❌
    ))
    }

    问题:

  • 这段代码能编译吗?
  • 如果不能,编译器会报什么错?
  • 为什么 map_state 的子组件不能包含修改数据的按钮?如果你确实需要一个"能编辑派生值"的组件,应该怎么做?
  • 练习四:改造题——多派生值 + 嵌套计算

    要求:

  • 定义状态结构体:
  • struct OrderState {
    unit_price: f64,
    quantity: u32,
    discount_rate: f64, // 折扣率,0.0 到 1.0
    }

  • 实现 Default,unit_price 默认为 100.0,quantity 默认为 1,discount_rate 默认为 1.0(无折扣)
  • 用 lens 连接三个编辑器:unit_price(提供"+10"和"-10"按钮)、quantity(提供"+1"和"-1"按钮)、discount_rate(提供"9折"和"8折"按钮,分别设为 0.9 和 0.8,以及一个"恢复原价"按钮设为 1.0)
  • 用 map_state 计算并显示三个派生值:
    • 原价小计 = unit_price × quantity
    • 折扣金额 = 原价小计 × (1.0 – discount_rate)
    • 实付金额 = 原价小计 × discount_rate
  • 提示:你需要三个 map_state 调用,每个计算一个派生值。

    // 你的代码:实现 Default、各子组件、app_logic、main

    练习答案与知识要点

    练习一答案

    fn app_logic(data: &mut Cart) -> impl WidgetView<Cart> + use<> {
    flex(Axis::Vertical, (
    // 填空①
    lens(
    |data: &mut u32| quantity_editor(data),
    |data: &mut Cart| &mut data.quantity,
    ),

    // 填空②
    map_state(
    |data: &f64| subtotal_display(data),
    |data: &Cart| data.price * data.quantity as f64,
    ),
    ))
    }

    解读:lens 的第二个参数返回 &mut data.quantity——这是字段的直接引用,子组件可以编辑它。map_state 的第二个参数返回 data.price * data.quantity as f64——这是一个计算结果(owned f64),不是任何字段的引用。注意 data.quantity as f64 的类型转换:quantity 是 u32,price 是 f64,相乘前需要统一类型。map_state 的映射闭包接收 &Cart(不可变引用),所以它只能读取字段,不能修改。

    练习二答案

    use winit::error::EventLoopError;
    use xilem::view::{Axis, flex, label, text_button, lens, map_state};
    use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};

    struct BmiState {
    height_cm: f64,
    weight_kg: f64,
    }

    impl Default for BmiState {
    fn default() -> Self {
    BmiState {
    height_cm: 170.0,
    weight_kg: 65.0,
    }
    }
    }

    fn height_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
    flex(Axis::Horizontal, (
    label(format!("身高: {:.1} cm", data)),
    text_button("+1", |data: &mut f64| *data += 1.0),
    text_button("-1", |data: &mut f64| *data -= 1.0),
    ))
    }

    fn weight_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
    flex(Axis::Horizontal, (
    label(format!("体重: {:.1} kg", data)),
    text_button("+0.5", |data: &mut f64| *data += 0.5),
    text_button("-0.5", |data: &mut f64| *data -= 0.5),
    ))
    }

    fn bmi_display(data: &f64) -> impl WidgetView<()> + use<> {
    let category = if *data < 18.5 {
    "偏瘦"
    } else if *data < 24.0 {
    "正常"
    } else if *data < 28.0 {
    "偏胖"
    } else {
    "肥胖"
    };
    label(format!("BMI: {:.1} ({})", data, category))
    }

    fn app_logic(data: &mut BmiState) -> impl WidgetView<BmiState> + use<> {
    flex(Axis::Vertical, (
    lens(
    |data: &mut f64| height_editor(data),
    |data: &mut BmiState| &mut data.height_cm,
    ),
    lens(
    |data: &mut f64| weight_editor(data),
    |data: &mut BmiState| &mut data.weight_kg,
    ),
    map_state(
    |data: &f64| bmi_display(data),
    |data: &BmiState| {
    let height_m = data.height_cm / 100.0;
    data.weight_kg / (height_m * height_m)
    },
    ),
    ))
    }

    fn main() -> Result<(), EventLoopError> {
    let app = Xilem::new_simple(
    BmiState::default(),
    app_logic,
    WindowOptions::new("BMI 计算器"),
    );
    app.run_in(EventLoop::with_user_event())?;
    Ok(())
    }

    解读:这道题考察的是"在 map_state 映射闭包中做多步计算"。BMI 的计算需要先把身高从厘米转换为米(data.height_cm / 100.0),再套用公式 weight / (height_m²)。这个计算过程涉及多步运算,但 map_state 的映射闭包可以包含任意复杂的逻辑——它不受"只能返回字段引用"的限制。

    bmi_display 子组件内部根据 BMI 值判断分类(偏瘦/正常/偏胖/肥胖),这个判断是在子组件内部完成的,不是在映射闭包中。映射闭包只负责计算 BMI 数值,分类逻辑属于视图层的展示职责。

    练习三答案

    不能编译。

    编译器会报错,大意是:text_button 的闭包要求 FnMut(&mut f64),但 data 的类型是 &f64(不可变引用),不能隐转换为 &mut f64。

    原因:map_state 给子组件传递的是 &f64(不可变引用)。子组件的所有闭包都只能拿到 &f64,不可能拿到 &mut f64。而 text_button 的回调需要 FnMut(&mut State) 类型的闭包——它需要可变引用来修改状态。&f64 无法提供 &mut f64,所以编译失败。

    这是设计上的刻意限制:map_state 的语义就是"只读映射"。如果你需要一个能编辑的值,应该用 lens 把它绑定到真实字段上。

    如果你确实需要一个"能编辑派生值"的组件,正确的做法是:不要编辑派生值本身,而是编辑产生这个派生值的源字段。比如在温度转换器的例子中,不要试图直接编辑华氏度,而是编辑摄氏度——华氏度会自动跟着变。这就是单向数据流的核心思想:派生数据永远是源数据的"影子",你只需要修改源头。

    练习四答案

    use winit::error::EventLoopError;
    use xilem::view::{Axis, flex, label, text_button, lens, map_state};
    use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};

    struct OrderState {
    unit_price: f64,
    quantity: u32,
    discount_rate: f64,
    }

    impl Default for OrderState {
    fn default() -> Self {
    OrderState {
    unit_price: 100.0,
    quantity: 1,
    discount_rate: 1.0,
    }
    }
    }

    fn price_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
    flex(Axis::Horizontal, (
    label(format!("单价: {:.0} 元", data)),
    text_button("+10", |data: &mut f64| *data += 10.0),
    text_button("-10", |data: &mut f64| {
    if *data >= 10.0 { *data -= 10.0; }
    }),
    ))
    }

    fn quantity_editor(data: &mut u32) -> impl WidgetView<u32> + use<> {
    flex(Axis::Horizontal, (
    label(format!("数量: {}", data)),
    text_button("+1", |data: &mut u32| *data += 1),
    text_button("-1", |data: &mut u32| {
    if *data > 1 { *data -= 1; }
    }),
    ))
    }

    fn discount_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
    let rate_text = format!("{:.0}折", data * 10.0);
    flex(Axis::Horizontal, (
    label(format!("折扣: {}", rate_text)),
    text_button("9折", |data: &mut f64| *data = 0.9),
    text_button("8折", |data: &mut f64| *data = 0.8),
    text_button("恢复原价", |data: &mut f64| *data = 1.0),
    ))
    }

    fn money_display(data: &f64) -> impl WidgetView<()> + use<> {
    label(format!("{:.2} 元", data))
    }

    fn app_logic(data: &mut OrderState) -> impl WidgetView<OrderState> + use<> {
    flex(Axis::Vertical, (
    // 三个 lens:编辑真实字段
    lens(
    |data: &mut f64| price_editor(data),
    |data: &mut OrderState| &mut data.unit_price,
    ),
    lens(
    |data: &mut u32| quantity_editor(data),
    |data: &mut OrderState| &mut data.quantity,
    ),
    lens(
    |data: &mut f64| discount_editor(data),
    |data: &mut OrderState| &mut data.discount_rate,
    ),

    // 三个 map_state:展示派生值
    label("—- 计算结果 —-"),

    map_state(
    |data: &f64| {
    flex(Axis::Horizontal, (
    label("原价小计: "),
    label(format!("{:.2} 元", data)),
    ))
    },
    |data: &OrderState| data.unit_price * data.quantity as f64,
    ),

    map_state(
    |data: &f64| {
    flex(Axis::Horizontal, (
    label("折扣金额: "),
    label(format!("{:.2} 元", data)),
    ))
    },
    |data: &OrderState| {
    let subtotal = data.unit_price * data.quantity as f64;
    subtotal * (1.0 data.discount_rate)
    },
    ),

    map_state(
    |data: &f64| {
    flex(Axis::Horizontal, (
    label("实付金额: "),
    label(format!("{:.2} 元", data)),
    ))
    },
    |data: &OrderState| {
    let subtotal = data.unit_price * data.quantity as f64;
    subtotal * data.discount_rate
    },
    ),
    ))
    }

    fn main() -> Result<(), EventLoopError> {
    let app = Xilem::new_simple(
    OrderState::default(),
    app_logic,
    WindowOptions::new("订单计算器"),
    );
    app.run_in(EventLoop::with_user_event())?;
    Ok(())
    }

    解读:这道题考察的是"多个 lens + 多个 map_state 混合使用"的模式。三个字段(unit_price、quantity、discount_rate)各用一个 lens 连接编辑器,三个派生值(原价小计、折扣金额、实付金额)各用一个 map_state 连接显示器。

    注意三个 map_state 的映射闭包都读取了多个字段(unit_price、quantity、discount_rate),但它们都是只读的(&OrderState),不会互相冲突。这是 map_state 的优势:多个 map_state 可以同时读取同一个父状态的不同部分,甚至同一部分,因为它们拿到的都是不可变引用,不存在借用冲突。

    如果你用 lens 来做同样的事,就会遇到借用检查器的问题——你不能同时从 &mut OrderState 中取出多个 &mut 引用给不同的 lens。但 map_state 只需要 &,多个 & 可以共存,所以没有这个问题。

    知识要点

    要点一:lens 和 map_state 的分工

    lens 用于"编辑字段"——子组件需要 &mut 引用来修改状态中的某个字段。map_state 用于"展示派生数据"——子组件只需要 & 引用来读取一个计算结果。两者不是替代关系,而是互补关系:一个处理写入,一个处理读取。

    要点二:map_state 映射闭包返回 owned 值

    map_state 的第二个参数(映射闭包)接收 &父状态,返回一个owned 值(如 f64、String、bool)。这个值可以是任意计算的结果——乘法、除法、条件判断、字符串拼接,什么都行。这是它和 lens 的根本区别:lens 闭包必须返回字段的 &mut 引用,map_state 闭包返回的是计算出来的 owned 值。

    要点三:map_state 的子组件只能读不能写

    map_state 给子组件传递的是 &子状态类型(不可变引用)。子组件内部的所有闭包都只能拿到不可变引用,无法修改数据。这是设计上的刻意限制——派生数据不应该被直接编辑,而应该通过修改源字段来间接更新。

    要点四:多个 map_state 可以共存

    因为 map_state 的映射闭包只需要 &父状态(不可变引用),多个 map_state 可以同时读取同一个父状态,不存在借用冲突。而多个 lens 同时从 &mut 父状态 中取出 &mut 引用时,需要确保它们取的是不同的字段(否则会产生多个可变借用,编译器会报错)。

    要点五:单向数据流的核心思想

    派生数据永远是源数据的"影子"。你不应该直接编辑派生值(也编辑不了——map_state 禁止了这种操作),而应该编辑产生派生值的源字段。源字段一变,app_logic 重新执行,map_state 的映射闭包自动重新计算,派生值自动更新。整个过程不需要任何手动同步代码。

    下一课可以讲 memoize——当状态很大、重建 View Tree 很昂贵时,用 memoize 跳过不必要的重建,只在实际变化时才重新计算。要继续吗?

    赞(0)
    未经允许不得转载:171主机测评 » 【Xilem 0.4 基础语法学与练】第18课:map_state——派生数据的只读映射
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址