< 返回版块

jasper2007111 发表于 2023-07-01 09:12

Tags:Yew

之前写过按钮组件算是简单尝试,决定试下稍微复杂一点的评分(Rate)组件。实现部分依旧搬照Element UI的实现,主要是熟悉Rust以及Yew这个框架的使用。

组件的属性定义在YewButtonProps这个结构体,里面的属性的默认值的定义使用了#[prop_or_default]#[prop_or(0.0)]这样的属性宏。属性宏有点像Java里面的注解,很奇怪的是如同Java书籍很少讲注解一样,实际中大量使用注解。Rust也是,手头的《Rust程序设计》和《Rust权威指南》对于宏的讲解很少,《Rust程序设计》根本没有属性宏,《Rust权威指南》有提到属性宏,只是举个使用的例子,根本没有讲怎么实现。实际中我发现Rust这些框架中也是大量的使用这些东西。

#[derive(Clone, PartialEq, Properties)]
pub struct YewButtonProps {
    #[prop_or_default]
    pub disabled: bool,

    #[prop_or_default]
    pub on_change: Callback<f64>,

    #[prop_or(0.0)]
    pub value:f64,

    #[prop_or(5)]
    pub max:i32,

    #[prop_or(vec!["el-icon-star-on".to_string(); 3])]
    pub icon_classes:Vec<String>,

    #[prop_or(vec!["#F7BA2A".to_string(), "#F7BA2A".to_string(), "#F7BA2A".to_string()])]
    pub colors:Vec<String>,
    
    // ...
}

视图渲染方面,Yew跟Vue都使用内嵌HTML这样的方式,只不过Vue是单独一个Template,所以这块代码变化不大。

    fn view(&self, ctx: &Context<Self>) -> Html {
        let show_text = ctx.props().show_text.clone();
        let max = ctx.props().max.clone();
        let show_score = ctx.props().show_score.clone();
        let text_color = ctx.props().text_color.clone();

        let span_style = if self.is_rate_disabled() {
            "cursor: auto"
        } else {
            "cursor: pointer"
        };

        let mut span_vec = vec![];
        for i in 0..max  {
            span_vec.push(html!{
                <span class={"el-rate__item"} style={span_style}>
                <i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseMove((i, e))
                })} onmouseleave={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseLeave
                })} onclick={ctx.link().callback(move |_e: MouseEvent| { 
                    Msg::OnSelectValue(i)
                })}>
                if self.is_show_decimal_icon(i+1) {
                    <i class={self.get_decimal_icon_class()} style={self.get_decimal_style()}/>
                }
                </i>
                </span>
            });
        }
        html! {
            <div onkeydown={ctx.link().callback(move|e:KeyboardEvent|{
                Msg::OnKeydown(e)
            })} class="el-rate" role="slider" tabindex="0" aria-valuemin="0" aria-valuemax={max.to_string()} aria-valuenow={self.current_value.to_string()} >
            {span_vec}
            if show_score || show_text {
                <span class="el-rate__text" style={format!("color: {}", text_color)}>{self.get_text()}</span>
            }
            </div>
        }
    }

列表渲染

分开来看下,Yew中对于列表渲染方式,我使用的传统for循环式,当然也可以用函数式的。

        let mut span_vec = vec![];
        for i in 0..max  {
            span_vec.push(html!{
                <span class={"el-rate__item"} style={span_style}>
                <i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseMove((i, e))
                })} onmouseleave={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseLeave
                })} onclick={ctx.link().callback(move |_e: MouseEvent| { 
                    Msg::OnSelectValue(i)
                })}>
                if self.is_show_decimal_icon(i+1) {
                    <i class={self.get_decimal_icon_class()} style={self.get_decimal_style()}/>
                }
                </i>
                </span>
            });
        }

条件渲染

在Yew中比较简单。

            if show_score || show_text {
                <span class="el-rate__text" style={format!("color: {}", text_color)}>{self.get_text()}</span>
            }

事件处理

这里主要用到了鼠标以及键盘相关的事件

<i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseMove((i, e))
                })} onmouseleave={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseLeave
                })} onclick={ctx.link().callback(move |_e: MouseEvent| { 
                    Msg::OnSelectValue(i)
                })}>

Yew中每个事件处理结尾都需要返回发送一个Msg,所以事件的处理我都放在update里面了。

fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::OnMouseMove((index, e))=> {
                if self.is_rate_disabled() {
                    return false;
                }
                if self.props.allow_half {
                    let element: Element = e.target_unchecked_into();
                    let mut target = None;
                    if target.is_none() {
                        target = Some(element);
                    }
                    if target.is_some() {
                        let offset_x = e.offset_x()*2;
                        let client_width = target.clone().unwrap().client_width();
                        self.pointer_at_left_half = offset_x <= client_width;
                        self.current_value = if self.pointer_at_left_half {
                            (index+1) as f64 - 0.5
                        } else {
                            (index+1) as f64
                        };
                    } 
                } else {
                    self.current_value = (index+1) as f64;
                }
                self.hover_index = index+1;
                true
            },
        // ... 
    }

在处理鼠标事件时遇到了一个坑,我按照Element UI的实现把事件放在<span/>标签上,结果发现获取到的e.offset_x()的值一直有问题,后来在Vue代码上测试发现是没有穿透的问题,就是Yew的这个设置鼠标事件只会在当前这个标签上,不会传递到下面的元素,这应该是Yew自己的设置。

其他问题

依赖问题

主要跟web-sys的有关系。

let element: Element = e.target_unchecked_into();
let mut target = None;
if element.class_list().contains("el-rate__item") {
    target = element.query_selector(".el-rate__icon").unwrap();
}

以上代码中用到了Element这个类型,然后我就在Cargo.toml中加入。

[dependencies.web-sys]
version = "0.3.9"
# We need to enable the `DomRect` feature in order to use the
# `get_bounding_client_rect` method.
features = [
    "console",
    "Element",
    # ...
]

结果在调用class_list这个方法时,老提示没有这个方法,但是我点击文档进去是有这个这个方法的,当时有点懵住,后来想起之前写记事本时也遇到这个问题,class_list这个方法会返回DomTokenList这个类型,不起作用就是因为我没有把这个类型也加入的问题。

[dependencies.web-sys]
version = "0.3.9"
# We need to enable the `DomRect` feature in order to use the
# `get_bounding_client_rect` method.
features = [
    "console",
    "Element",
    "DomTokenList",
    # ...
]

其他

里面用到了一些数学的方法,我本来想着用Rust自带的来着,后来发现js_sys提供了这些。

总结

基本上一天多时间,感觉再这么下去可以弄一个Element UI的Yew版了,主要是个时间的问题了。

评分组件完整代码,yew_rate.rs

use std::collections::HashMap;

use web_sys::Element;
use yew::prelude::*;

pub enum Msg {
    OnMouseMove((i32, MouseEvent)),
    OnSelectValue(i32),
    OnMouseLeave,
    OnKeydown(KeyboardEvent)
}
pub struct YewRate {
    pointer_at_left_half:bool,
    current_value:f64,
    hover_index:i32,
    props:YewButtonProps
}

struct ClassMapVaule  {
    pub value:String,
    pub excluded: bool
}

#[derive(Clone, PartialEq, Properties)]
pub struct YewButtonProps {
    #[prop_or_default]
    pub disabled: bool,

    #[prop_or_default]
    pub on_change: Callback<f64>,

    #[prop_or(0.0)]
    pub value:f64,

    #[prop_or(5)]
    pub max:i32,

    #[prop_or(vec!["el-icon-star-on".to_string(); 3])]
    pub icon_classes:Vec<String>,

    #[prop_or(vec!["#F7BA2A".to_string(), "#F7BA2A".to_string(), "#F7BA2A".to_string()])]
    pub colors:Vec<String>,

    #[prop_or("#C6D1DE".to_string())]
    pub void_color:String,

    #[prop_or("#EFF2F7".to_string())]
    pub disabled_void_color:String,

    #[prop_or("el-icon-star-on".to_string())]
    pub disabled_void_icon_class:String,

    #[prop_or("el-icon-star-off".to_string())]
    pub void_icon_class:String,

    #[prop_or("#1f2d3d".to_string())]
    pub text_color:String,

    #[prop_or(2)]
    pub low_threshold:i32,

    #[prop_or(4)]
    pub high_threshold:i32,

    #[prop_or(false)]
    pub allow_half:bool,

    #[prop_or(false)]
    pub show_score:bool,

    #[prop_or(false)]
    pub show_text:bool,

    #[prop_or(vec!["极差".to_string(), "失望".to_string(), "一般".to_string(), "满意".to_string(), "惊喜".to_string()])]
    pub texts:Vec<String>
}

impl Component for YewRate {
    type Message = Msg;
    type Properties = YewButtonProps;

    fn create(ctx: &Context<Self>) -> Self {
        Self {
            pointer_at_left_half:true,
            current_value: ctx.props().value,
            hover_index:-1,
            props: ctx.props().clone()
        }
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::OnMouseMove((index, e))=> {
                if self.is_rate_disabled() {
                    return false;
                }
                if self.props.allow_half {
                    let element: Element = e.target_unchecked_into();
                    let mut target = None;
                    // 这段代码原本是Element UI的实现,但是Yew的鼠标移动事件并不能穿透,所以这段代码弃用
                    // if element.class_list().contains("el-rate__item") {
                    //     target = element.query_selector(".el-rate__icon").unwrap();
                    // }
                    // if target.is_some()&& target.clone().unwrap().class_list().contains("el-rate__decimal") {
                    //     target = target.clone().unwrap().parent_element();
                    // } else if element.class_list().contains("el-rate__decimal") {
                    //     target = element.parent_element();
                    // }
                    if target.is_none() {
                        target = Some(element);
                    }
                    if target.is_some() {
                        let offset_x = e.offset_x()*2;
                        let client_width = target.clone().unwrap().client_width();
                        self.pointer_at_left_half = offset_x <= client_width;
                        self.current_value = if self.pointer_at_left_half {
                            (index+1) as f64 - 0.5
                        } else {
                            (index+1) as f64
                        };
                    } 
                } else {
                    self.current_value = (index+1) as f64;
                }
                self.hover_index = index+1;
                true
            },
            Msg::OnMouseLeave=>{
                if self.is_rate_disabled() {
                    return false;
                }
                if self.props.allow_half {
                    self.pointer_at_left_half = self.props.value != js_sys::Math::floor(self.props.value);
                }
                self.current_value = self.props.value;
                true
            },
            Msg::OnSelectValue(i)=>{
                if self.is_rate_disabled() {
                    return false;
                }
                if self.props.allow_half && self.pointer_at_left_half {
                    self.props.value = self.current_value;
                } else {
                    self.current_value = (i+1) as f64;
                    self.props.value = self.current_value;
                }
                self.props.on_change.emit(self.props.value);
                self.hover_index = -1;
                true
            },
            Msg::OnKeydown(event) => {
                if self.is_rate_disabled() {
                    return false;
                }
                let mut current_value = self.current_value;
                let keycode = event.key_code();
                if keycode == 38 ||keycode == 39 {
                    if self.props.allow_half {
                        current_value += 0.5;
                    } else {
                        current_value += 1.0;
                    }
                    event.stop_propagation();
                    event.prevent_default();
                } else if keycode == 37 || keycode == 40 {
                    if self.props.allow_half {
                        current_value -= 0.5;
                    } else {
                        current_value -= 1.0;
                    }
                    event.stop_propagation();
                    event.prevent_default();
                }
                current_value = if current_value < 0.0 {
                    0.0
                } else if current_value > self.props.max as f64{
                    self.props.max as f64
                } else {
                    current_value
                };

                if current_value != self.current_value {
                    self.props.value = current_value;
                    self.current_value = self.props.value;
                    self.props.on_change.emit(self.props.value);
                    true
                } else {
                    false
                }
            }
        }
    }
    fn view(&self, ctx: &Context<Self>) -> Html {
        let show_text = ctx.props().show_text.clone();
        let max = ctx.props().max.clone();
        let show_score = ctx.props().show_score.clone();
        let text_color = ctx.props().text_color.clone();

        let span_style = if self.is_rate_disabled() {
            "cursor: auto"
        } else {
            "cursor: pointer"
        };

        let mut span_vec = vec![];
        for i in 0..max  {
            span_vec.push(html!{
                <span class={"el-rate__item"} style={span_style}>
                <i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseMove((i, e))
                })} onmouseleave={ctx.link().callback(move |e: MouseEvent| { 
                    Msg::OnMouseLeave
                })} onclick={ctx.link().callback(move |_e: MouseEvent| { 
                    Msg::OnSelectValue(i)
                })}>
                if self.is_show_decimal_icon(i+1) {
                    <i class={self.get_decimal_icon_class()} style={self.get_decimal_style()}/>
                }
                </i>
                </span>
            });
        }
        html! {
            <div onkeydown={ctx.link().callback(move|e:KeyboardEvent|{
                Msg::OnKeydown(e)
            })} class="el-rate" role="slider" tabindex="0" aria-valuemin="0" aria-valuemax={max.to_string()} aria-valuenow={self.current_value.to_string()} >
            {span_vec}
            if show_score || show_text {
                <span class="el-rate__text" style={format!("color: {}", text_color)}>{self.get_text()}</span>
            }
            </div>
        }
    }
}

impl YewRate {
    pub fn get_active_color(&self)->String {
        let s = self.get_value_from_map(js_sys::Math::ceil(self.current_value) as i32, self.get_color_map());
        s
    }
    fn get_color_map(&self)->HashMap<i32, ClassMapVaule> {
        let mut map = HashMap::new();

        map.insert(self.props.low_threshold, ClassMapVaule{
            value: self.props.colors[0].clone(),
            excluded: false
        });

        map.insert(self.props.high_threshold, ClassMapVaule{
            value: self.props.colors[1].clone(),
            excluded: true
        });

        map.insert(self.props.max, ClassMapVaule{
            value: self.props.colors[2].clone(),
            excluded: false
        });

        map
    }

    fn get_class_map(&self)->HashMap<i32, ClassMapVaule> {
        let mut map = HashMap::new();

        map.insert(self.props.low_threshold, ClassMapVaule{
            value: self.props.icon_classes[0].clone(),
            excluded: false
        });

        map.insert(self.props.high_threshold, ClassMapVaule{
            value: self.props.icon_classes[1].clone(),
            excluded: true
        });

        map.insert(self.props.max, ClassMapVaule{
            value: self.props.icon_classes[2].clone(),
            excluded: false
        });

        map
    }
    fn get_value_from_map(&self, value: i32, map:HashMap<i32, ClassMapVaule>) -> String{
        for (key, item) in &map {
            if item.excluded {
                if value>*key {
                    return item.value.clone();
                } 
            } else {
                if value<=*key {
                    return item.value.clone();
                }
            }
        }

        "".to_string()
    }
    pub fn get_icon_style(&self, item:i32) -> String {
        let void_color = if self.is_rate_disabled() {
            self.props.disabled_void_color.clone()
        } else {
            self.props.void_color.clone()
        };
        if item<self.current_value as i32 {
            format!("color: {}; hover: {}", self.get_active_color(), self.hover_index==item)
        } else {
            format!("color: {}; hover: {}", void_color, self.hover_index==item)
        }
    }
    pub fn get_active_class(&self) -> String  {
        let s = self.get_value_from_map(js_sys::Math::ceil(self.current_value) as i32, self.get_class_map());
        s
    }
    pub fn get_void_class(&self) -> String {
        if self.is_rate_disabled() {
            self.props.disabled_void_icon_class.clone()
        } else {
            self.props.void_icon_class.clone()
        }
    }
    pub fn get_classes(&self, item:i32)->Vec<String> {
        let mut result = vec![];
        let mut threshold = js_sys::Math::ceil(self.current_value) as i32;

        if self.props.allow_half && self.current_value !=js_sys::Math::floor(self.current_value) {
            threshold -=1;
        }
        for _ in (0..threshold) {
            let get_active_class =  self.get_active_class();
            result.push(get_active_class);
        }

        for _ in (threshold..self.props.max) {
            result.push(self.get_void_class());
        }

        vec!["el-rate__icon".to_string(), result[item as usize].clone()]
    }

    pub fn get_text(&self) ->String {
        if self.props.show_score {
            if self.is_rate_disabled() {
                return format!("{}", self.props.value);
            }
            return format!("{}", self.current_value);
        } else if self.props.show_text && self.current_value as i32 >0 {
            let s = self.props.texts[(js_sys::Math::ceil(self.current_value) as usize)-1].clone();
            return s;
        }
        "".to_string()
    }

    pub fn get_decimal_style(&self) -> String {
        let width;
        if self.is_rate_disabled() {
            width = format!("{}%", self.get_value_decimal());
        } else if self.props.allow_half {
            width = "50%".to_string();
        } else {
            width = "".to_string();
        }
        format!("color: {}; width: {}", self.get_active_color(), width)
    }

    pub fn get_value_decimal(&self) -> f64 {
        return self.props.value * 100.0 - js_sys::Math::floor(self.props.value) * 100.0;
    }

    pub fn is_show_decimal_icon(&self, item:i32) -> bool {
        let show_when_disabled = self.is_rate_disabled() && self.get_value_decimal()>0.0 && (item as f64)-1.0<self.props.value && (item as f64) >self.props.value;
        let show_when_allow_half = self.props.allow_half && self.pointer_at_left_half && (item as f64)-0.5<=self.current_value && (item as f64)>self.current_value;
        return  show_when_disabled || show_when_allow_half;
    }

    pub fn get_decimal_icon_class(&self) ->String {
        let s = self.get_value_from_map(self.props.value as i32, self.get_class_map());
        format!("el-rate__decimal {}", s)
    }

    pub fn is_rate_disabled(&self) -> bool {
        // TODO 原始实现会有elForm的判断,目前没有实现
        // return this.disabled || (this.elForm || {}).disabled;
        return self.props.disabled;
    }
}

使用方式代码,components_test.rs

use super::yew_button::YewButton;
use super::yew_rate::YewRate;
use gloo_console::log;
use yew::prelude::*;

pub enum Msg {
    BtnClick,
    OnRateValueChanged(f64)
}

pub struct ComponentsTest {}

impl Component for ComponentsTest {
    type Message = Msg;
    type Properties = ();

    fn create(_ctx: &Context<Self>) -> Self {
        Self {}
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::BtnClick => {
                log!("按钮点击");
                false
            },
            Msg::OnRateValueChanged(v) =>{
                log!("v: ", v);
                false
            }
        }
    }
    fn view(&self, ctx: &Context<Self>) -> Html {
        let on_clicked = ctx.link().callback(move |_e: MouseEvent| Msg::BtnClick);
        html! {
            <div style="margin-left: 100px;">
                <h1>{ "组件测试" }</h1>
                // <YewButton style="primary" title="按钮测试" on_clicked={on_clicked.clone()} />
                // <br/>
                <YewRate value={0.0} on_change={ctx.link().callback(|v| {
                    Msg::OnRateValueChanged(v)
                })} show_text={true} allow_half={true}/>
            </div>
        }
    }
}


完整工程工程见yew-lab

评论区

写评论

还没有评论

1 共 0 条评论, 1 页