博客 (47)

前言:本文源于前几天看到的一条微博:

微博

对于这种言论我并不赞同。我大学学的是化学,没有学习过计算机专业的课程,但我认为至少这个问题并不需要多么高端的计算机专业知识,只要中学数学没有全还给老师,就应该能给出至少一种解法。比如说,我就随便涂了一个多边形和一个点,现在我要找出一种通用的方法来判断这个点是不是在多边形内部(别告诉我用肉眼观察……)。

问题

首先想到的一个解法是从这个点做一条射线,计算它跟多边形边界的交点个数,如果交点个数为奇数,那么点在多边形内部,否则点在多边形外。

思路

这个结论很简单,那它是怎么来的?其实,对于平面内任意闭合曲线,我们都可以直观地认为,曲线把平面分割成了内、外两部分,其中“内”就是我们所谓的多边形区域。

多边形

基于这一认识,对于平面内任意一条直线,我们可以得出下面这些结论:

  1. 直线穿越多边形边界时,有且只有两种情况:进入多边形或穿出多边形。

  2. 在不考虑非欧空间的情况下,直线不可能从内部再次进入多边形,或从外部再次穿出多边形,即连续两次穿越边界的情况必然成对。

  3. 直线可以无限延伸,而闭合曲线包围的区域是有限的,因此最后一次穿越多边形边界,一定是穿出多边形,到达外部。

穿越

现在回到我们最初的题目。假如我们从一个给定的点做射线,还可以得出下面两条结论:

  1. 如果点在多边形内部,射线第一次穿越边界一定是穿出多边形。

  2. 如果点在多边形外部,射线第一次穿越边界一定是进入多边形。

首次穿越

把上面这些结论综合起来,我们可以归纳出:

  1. 当射线穿越多边形边界的次数为偶数时,所有第偶数次(包括最后一次)穿越都是穿出,因此所有第奇数次(包括第一次)穿越为穿入,由此可推断点在多边形外部。

    外部

  2. 当射线穿越多边形边界的次数为奇数时,所有第奇数次(包括第一次和最后一次)穿越都是穿出,由此可推断点在多边形内部。

    内部

到这里,我们已经了解这个解法的思路了,下面接着看算法实现的一些具体问题和边界条件的处理。

  1. 点在多边形的边上

    上面我们讲到,这个解法的主要思路就是计算射线穿越多边形边界的次数,那么对于点在多边形的边上这种特殊情况,射线出发的这一次,是否应该算作穿越呢?

    边

    看了上面的图就会发现,不管算不算穿越,都会陷入两难的境地——同样落在多边形边上的点,可能会得到相反的结果。这显然是不正确的,因此对这种特殊情况需要特殊处理。

  2. 点和多边形的顶点重合

    这其实是第一种情况的一个特例。

    顶点

  3. 射线经过多边形顶点

    射线刚好经过多边形顶点的时候,应该算一次还是两次穿越?这种情况比前两种复杂,也是实现中的难点。

    边

  4. 射线刚好经过多边形的一条边

    这是上一种情况的特例,也就是说,射线连续经过了多边形的两个相邻顶点。

    经过边

解决方案:

  1. 判断点是否在线上的方法有很多,比较简单直接的就是计算点与两个多边形顶点的连线斜率是否相等,中学数学都学过。

  2. 点和多边形顶点重合的情况更简单,直接比较点的坐标就行了。

  3. 顶点穿越看似棘手,其实我们换一个角度,思路会大不相同。先来回答一个问题,射线穿越一条线段需要什么前提条件?没错,就是线段两个端点分别在射线两侧。只要想通这一点,顶点穿越就迎刃而解了。这样一来,我们只需要规定被射线穿越的点都算作其中一侧。

    两侧

    如上图,假如我们规定射线经过的点都属于射线以上的一侧,显然点 D 和发生顶点穿越的点 C 都位于射线 Y 的同一侧,所以射线 Y 其实并没有穿越 CD 这条边。而点 C 和点 B 则分别位于射线 Y 的两侧,所以射线 Y 和 BC 发生了穿越,由此我们可以断定点 Y 在多边形内。同理,射线 X 分别与 AD 和 CD 都发生了穿越,因此点 X 在多边形外,而射线 Z 没有和多边形发生穿越,点 Z 位于多边形外。

  4. 解决了第三点,这一点就毫无难度了。根据上面的假设,射线连续经过的两个顶点显然都位于射线以上的一侧,因此这种情况看作没有发生穿越就可以了。由于第三点的解决方案实际上已经覆盖到这种特例,因此不需要再做特别的处理。

这种简单直观的算法通常叫做射线法奇偶法,下面给出 JavaScript 的算法实现。

/**
 * @description 射线法判断点是否在多边形内部
 * @param {Object} p 待判断的点,格式:{ x: X 坐标, y: Y 坐标 }
 * @param {Array} poly 多边形顶点,数组成员的格式同 p
 * @return {String} 点 p 和多边形 poly 的几何关系
 */function rayCasting(p, poly) {
  var px = p.x,
      py = p.y,
      flag = false

  for(var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
    var sx = poly[i].x,
        sy = poly[i].y,
        tx = poly[j].x,
        ty = poly[j].y

    // 点与多边形顶点重合
    if((sx === px && sy === py) || (tx === px && ty === py)) {
      return 'on'
    }

    // 判断线段两端点是否在射线两侧
    if((sy < py && ty >= py) || (sy >= py && ty < py)) {
      // 线段上与射线 Y 坐标相同的点的 X 坐标
      var x = sx + (py - sy) * (tx - sx) / (ty - sy)

      // 点在多边形的边上
      if(x === px) {
        return 'on'
      }

      // 射线穿过多边形的边界
      if(x > px) {
        flag = !flag
      }
    }
  }

  // 射线穿过多边形边界的次数为奇数时点在多边形内
  return flag ? 'in' : 'out'}

除了射线法还有很多其他的方法,下面再介绍一种回转数法

平面中的闭合曲线关于一个点的回转数(又叫卷绕数),代表了曲线绕过该点的总次数。下面这张图动态演示了回转数的概念:图中红色曲线关于点(人所在位置)的回转数为 2。

回转数

回转数是拓扑学中的一个基本概念,具有很重要的性质和用途。本文并不打算在这一点上展开论述,这需要具备相当的数学知识,否则会非常乏味和难以理解。我们暂时只需要记住回转数的一个特性就行了:当回转数为 0 时,点在闭合曲线外部(回转数大于 0 时所代表的含义,大家可以自己想一想,还是很有趣的)。

对于给定的点和多边形,回转数应该怎么计算呢?

  1. 用线段分别连接点和多边形的全部顶点。

    顶点连线

  2. 计算所有点与相邻顶点连线的夹角。

    内部

  3. 计算所有夹角和。注意每个夹角都是有方向的,所以有可能是负值。

    外部

  4. 最后根据角度累加值计算回转数。看过前面的介绍,很容易理解 360°(2π)相当于一次回转。

思路介绍完了,下面两点是实现中需要留意的问题。

  1. JavaScript 的数只有 64 位双精度浮点这一种。对于三角函数产生的无理数,浮点数计算不可避免会造成一些误差,因此在最后计算回转数时需要做取整操作。

  2. 通常情况下,平面直角坐标系内一个角的取值范围是 -π 到 π 这个区间,这也是 JavaScript 三角函数 Math.atan2() 返回值的范围。但 JavaScript 并不能直接计算任意两条线的夹角,我们只能先计算两条线与 X 正轴夹角,再取两者差值。这个差值的结果就有可能超出 -π 到 π 这个区间,因此我们还需要处理差值超出取值区间的情况。

这里也给出回转数法的 JavaScript 实现。

/**
 * @description 回转数法判断点是否在多边形内部
 * @param {Object} p 待判断的点,格式:{ x: X 坐标, y: Y 坐标 }
 * @param {Array} poly 多边形顶点,数组成员的格式同 p
 * @return {String} 点 p 和多边形 poly 的几何关系
 */function windingNumber(p, poly) {
  var px = p.x,
      py = p.y,
      sum = 0

  for(var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
    var sx = poly[i].x,
        sy = poly[i].y,
        tx = poly[j].x,
        ty = poly[j].y

    // 点与多边形顶点重合或在多边形的边上
    if((sx - px) * (px - tx) >= 0 && (sy - py) * (py - ty) >= 0 && (px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
      return 'on'
    }

    // 点与相邻顶点连线的夹角
    var angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)

    // 确保夹角不超出取值范围(-π 到 π)
    if(angle >= Math.PI) {
      angle = angle - Math.PI * 2
    } else if(angle <= -Math.PI) {
      angle = angle + Math.PI * 2
    }

    sum += angle
  }

  // 计算回转数并判断点和多边形的几何关系
  return Math.round(sum / Math.PI) === 0 ? 'out' : 'in'}

也有人问到像下面这种复杂多边形有没有办法?答案是肯定的。至于为什么,就留给大家思考吧。

Complex Polygon

本文示例:https://xoyozo.net/Demo/IsThePointInsideThePolygon

d
转自 dg 4 年前
11,659

百度地图称之为地面叠加层(GroundOverlay)

类参考:https://mapopen-pub-jsapi.bj.bcebos.com/jsapi/reference/jsapi_reference_3_0.html#a3b18

示例DEMO:http://lbsyun.baidu.com/jsdemo.htm#gImageGround

var map = new BMapGL.Map('container');
map.centerAndZoom(new BMapGL.Point(117.200, 36.2437), 18);
map.enableScrollWheelZoom(true);
map.setTilt(45);
map.setDisplayOptions({
    poiText: false,  // 隐藏poi标注
    poiIcon: false,  // 隐藏poi图标
    building: false  // 隐藏楼块
});


var pStart = new BMapGL.Point(117.19635, 36.24093);
var pEnd = new BMapGL.Point(117.20350, 36.24764);
var bounds = new BMapGL.Bounds(new BMapGL.Point(pStart.lng, pEnd.lat), new BMapGL.Point(pEnd.lng, pStart.lat));
var imgOverlay = new BMapGL.GroundOverlay(bounds, {
    type: 'image',
    url: '/jsdemo/img/shouhuimap.png',
    opacity: 1
});
map.addOverlay(imgOverlay);


xoyozo 4 年前
7,357

核心文件路径:/theme/html/demo*/src/js/components/core.datatable.js

所有参数的默认值见该文件 3369 行起。

data:

属性 功能
type 数据源类型 local / remote
source 数据源 链接或对象(见下方)
pageSize 每页项数 默认 10
saveState 刷新、重新打开、返回时仍保持状态 默认 true
serverPaging 是否在服务端实现分页 默认 false
serverFiltering 是否在服务端实现筛选 默认 false
serverSorting 是否在服务端实现排序 默认 false
autoColumns 为远程数据源启用自动列功能 默认 false
attr

data.source:

属性 功能
url 数据源地址
params 请求参数
query: {
}
headers 自定义请求的头
{
    'x-my-custom-header': 'some value',
    'x-test-header': 'the value'
}
map 数据地图,作用是对返回的数据进行整理和定位
function (raw) {
    console.log(raw)
    // sample data mapping
    var dataSet = raw;
    if (typeof raw.data !== 'undefined') {
        dataSet = raw.data;
    }
    return dataSet;
}

layout:

属性 功能
theme 主题 默认 default
class 包裹的 CSS 样式
scroll 在需要时显示横向或纵向滚动条 默认 false
height 表格高度 默认 null
minHeight 表格最小高度 默认 null
footer 是否显示表格底部 默认 false
header 是否显示表头 默认 true
customScrollbar 自定义的滚动条 默认 true
spinner Loading 样式
{
	overlayColor: '#000000',
	opacity: 0,
	type: 'loader',
	state: 'primary',
	message: true,
}
icons 表格中的 icon
{
	sort: {
	    asc: 'flaticon2-arrow-up', 
	    desc: 'flaticon2-arrow-down'
	},
	pagination: {
		next: 'flaticon2-next',
		prev: 'flaticon2-back',
		first: 'flaticon2-fast-back',
		last: 'flaticon2-fast-next',
		more: 'flaticon-more-1',
	},
	rowDetail: {
	    expand: 'fa fa-caret-down', 
	    collapse: 'fa fa-caret-right'
	},
}
sortable 是否支持按列排序 默认 true
resizable
是否支持鼠标拖动改变列宽 默认 false
filterable 在列中过滤 默认 false
pagination
显示分页信息 默认 true
editable
行内编辑 默认 false
columns
见本文下方
search
搜索
{
	// 按回车键确认
	onEnter: false,
	// 文本内容
	input: null,
	// 搜索延时
	delay: 400,
	// 键名
	key: null
}

layout.columns:

属性 功能 解释
field 字段名 对应 JSON 的属性名,点击表头时作为排序字段名
title 表头名 显示在表格头部
sortable 默认排序方式 可选:'asc' / 'desc'
width 单元格最小宽度 值与 CSS 值一致,填数字时默认单位 px
type 数据类型 'number' / 'date' 等,与本地排序有关
format 数据格式化 例格式化日期:'YYYY-MM-DD'
selector 是否显示选择框 布尔值或对象,如:{ class: '' }
textAlign 文字对齐方式 'center'
overflow 内容超过单元格宽度时是否显示 'visible':永远显示
autoHide 自适应显示/隐藏 布尔值
template 用于显示内容的 HTML 模板 function(row) { return row.Id; }
sortCallback 排序回调 自定义排序方式,参 local-sort.js

其它:

属性 功能 解释
translate 翻译

参 core.datatable.js 3512 行,简体中文示例:

translate: {
    records: {
        processing: '加载中...',
        noRecords: '没有找到相关内容',
    },
    toolbar: {
        pagination: {
            items: {
                default: {
                    first: '首页',
                    prev: '前一页',
                    next: '后一页',
                    last: '末页',
                    more: '更多',
                    input: '请输入跳转页码',
                    select: '设置每页显示项数',
                },
                info: '当前第 {{start}} - {{end}} 项 共 {{total}} 项',
            },
        },
    },
},


extensions

暂时没有找到对字符串内容进行自动 HTML 编码的属性,这可能带来 XSS 攻击风险,在 remote 方式中必须在服务端预先 HtmlEncode。即使在 layout.columns.template 中进行处理也是无济于事,恶意代码会在 ajax 加载完成后立即执行。


方法和事件:待完善。


更多信息请查询官方文档:https://keenthemes.com/keen/?page=docs&section=html-components-datatable

xoyozo 4 年前
5,344

使用 JS 根据屏幕宽度来计算页面尺寸和字体大小并不是一个好办法。

rem 单位是跟随网页根元素的 font-size 改变而自动改变的,是一个相对的长度单位,非常适合在不同手机界面上自适应屏幕大小。 


一般的手机浏览器的默认字体大小为 16px。即:

:root { font-size: 16px; } /* 在 HTML 网页中,:root 选择器相当于 html */

也就是说,如果我定义一个宽度为 1rem,那么它的实际宽度为 16px。反过来说,如果设计稿宽度为 1000px,那么相当于 1000÷16=62.5‬rem。因此,我们设置一个 62.5rem 的宽度,即可正常显示 1000px 的设计效果。


为了适应不同屏幕尺寸的手机,只需按比例换算更改 :root 的 font-size 即可。这个比例是由“窗口宽度 ÷ 设计稿宽度”得到的,姑且称它为“窗设比”。

以 iPhone X 逻辑宽度 375px 为例,设计稿宽度 1000px 时,窗设比为:0.375。:root 的 font-size 将会被重置为 16 * 0.375 = 6px。上面的 62.5rem 宽度将对应 iPhone X 宽度为 62.5 * 6 = 375px,正好是屏幕逻辑宽度。


根据这个思路,理论上,我们只需要使用 CSS 将 :root 的 font-size 设置为 1px,然后使用 JS 重置为与“窗设比”相乘的积(本例中将被重置为 0.375px)。这样,我们可以不通过换算,只需要更改单位,将设计稿中的 500px 直接写入到 CSS 为 500rem,即可实现在 iPhone X 上显示逻辑宽度为(500 * 0.375 =)187.5px,即设计稿中的 1/2 宽度对应到屏幕上 1/2 的宽度。


实际上,部分安卓手机(如华为某旗舰机)不支持 :root 的 font-size 小于 12px,其它一些浏览器也会出现小于 12px 的文字以 12px 显示。


为了避免在任何环节出现小于 12px 的 font-size 值,且不增加设计稿尺寸到 CSS 数值的换算难度,我们设计了以下思路:

  1. 约定“设样比(设计稿的 px 值与 CSS 的 rem 值的比)”为 100;

  2. 使用 JS 将 :root 的 font-size 重置为“设样比”与“窗设比(窗口宽度 ÷ 设计稿宽度)”的乘积;

  3. 计算 CSS 时的 rem 值只需从设计稿中获取 px 值再除以“设样比”100 即可。 


这样做的另一个好处是,不用要求设计师必须按多少尺寸来设计,当然按主流尺寸来建议是可行的。


完整代码(jQuery 版):

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>Index</title>
    <style>
        body { margin: 0; text-align: center; }
        #app { margin: 0 auto; background-color: #EEE; display: none; }
        /* 长度计算方法:从设计稿获得长度(例如 160px),除以 px2rem 值(本例为 100),得到 1.6rem */
        .whole { background-color: #D84C40; width: 10rem; }
        .half { background-color: #3296FA; width: 5rem; }
        .half2 { background-color: #3296FA; width: 5rem; margin-left: 5rem; }
    </style>
</head>
<body>
    <div id="app">
        <div class="whole">100%</div>
        <div class="half">50%</div>
        <div class="half2">50%</div>
    </div>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script>
        // [函数] 重置尺寸
        function fn_resize() {
            // 设计稿宽度(px)
            var designsWidth = 1000;
            // 约定的“设计稿 px 值与 CSS rem 值的比”,为方便计算,一般无需改动
            var px2rem = 100;
            // 限制 #app 的最大宽度为设计稿宽度
            $('#app').css('max-width', designsWidth).css('min-height', $(window).height());
            // 重置 :root 的 font-size
            var appWidth = Math.min($(window).width(), designsWidth);
            $(':root').css('font-size', px2rem * appWidth / designsWidth);
            $('#app').show();
        }
        // 页面加载完成后重置尺寸
        $(fn_resize);
        // 改变窗口大小时重置尺寸
        $(window).resize(fn_resize);
    </script>
</body>
</html>

完整代码(Vue2 版):

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>Index</title>
    <style>
        body { margin: 0; text-align: center; }
        #app { margin: 0 auto; background-color: #EEE; }
        /* 长度计算方法:从设计稿获得长度(例如 160px),除以 px2rem 值(本例为 100),得到 1.6rem */
        .whole { background-color: #D84C40; width: 10rem; }
        .half { background-color: #3296FA; width: 5rem; }
        .half2 { background-color: #3296FA; width: 5rem; margin-left: 5rem; }
    </style>
</head>
<body>
    <div id="app" v-bind:style="{ maxWidth: designsWidth + 'px', minHeight: appMinHeight + 'px' }" style="display: none;" v-show="appShow">
        <div class="whole">100%</div>
        <div class="half">50%</div>
        <div class="half2">50%</div>
        <div>appWidth: {{ appWidth }}</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                // 设计稿宽度(px)
                designsWidth: 1000,
                // 约定的“设计稿 px 值与 CSS rem 值的比”,为方便计算,一般无需改动
                px2rem: 100,
                // #app 的 width
                appWidth: 0, // 勿改
                appMinHeight: 0, // 勿改
                appShow: false, // 勿改
            },
            mounted: function () {
                // 页面加载完成后重置尺寸
                this.fn_resize();
                const that = this;
                // 改变窗口大小时重置尺寸
                window.onresize = () => {
                    return (() => {
                        console.log('RUN window.onresize()');
                        that.fn_resize();
                    })();
                };
            },
            watch: {
                // 侦听 appWidth 更改 root 的 font-size
                appWidth: function () {
                    console.log('RUN watch: appWidth');
                    var root = document.getElementsByTagName("html")[0];
                    root.style.fontSize = (this.px2rem * this.appWidth / this.designsWidth) + 'px';
                    this.appShow = true;
                }
            },
            methods: {
                fn_resize: function () {
                    console.log('RUN methods: fn_resize()');
                    this.appWidth = Math.min(document.body.clientWidth, this.designsWidth);
                    this.appMinHeight = document.documentElement.clientHeight;
                }
            }
        });
    </script>
</body>
</html>

完整代码(Vue3 版):

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>Index</title>
    <style>
        body { margin: 0; text-align: center; }
        #app > div { display: none; margin: 0 auto; background-color: #EEE; }
        /* 长度计算方法:从设计稿获得长度(例如 160px),除以 px2rem 值(本例为 100),得到 1.6rem */
        .whole { background-color: #D84C40; width: 10rem; }
        .half { background-color: #3296FA; width: 5rem; }
        .half2 { background-color: #3296FA; width: 5rem; margin-left: 5rem; }
    </style>
</head>
<body>
    <div id="app">
        <div style="display: none;" v-bind:style="{ maxWidth: designsWidth + 'px', minHeight: appMinHeight + 'px', display: appShow ? 'block' : 'none' }">
            <div class="whole">100%</div>
            <div class="half">50%</div>
            <div class="half2">50%</div>
            <div>appWidth: {{ appWidth }}</div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
    <script>
        const app = Vue.createApp({
            data: function () {
                return {
                    // 设计稿宽度(px)
                    designsWidth: 1000,
                    // 约定的“设计稿 px 值与 CSS rem 值的比”,为方便计算,一般无需改动
                    px2rem: 100,
                    // #app 的 width
                    appWidth: 0, // 勿改
                    appMinHeight: 0, // 勿改
                    appShow: false, // 勿改
                }
            },
            mounted: function () {
                // 页面加载完成后重置尺寸
                this.fn_resize();
                const that = this;
                // 改变窗口大小时重置尺寸
                window.onresize = () => {
                    return (() => {
                        console.log('RUN window.onresize()');
                        that.fn_resize();
                    })();
                };
            },
            watch: {
                // 侦听 appWidth 更改 root 的 font-size
                appWidth: function () {
                    console.log('RUN watch: appWidth');
                    var root = document.getElementsByTagName("html")[0];
                    root.style.fontSize = (this.px2rem * this.appWidth / this.designsWidth) + 'px';
                    this.appShow = true;
                }
            },
            methods: {
                fn_resize: function () {
                    console.log('RUN methods: fn_resize()');
                    this.appWidth = Math.min(document.body.clientWidth, this.designsWidth);
                    this.appMinHeight = document.documentElement.clientHeight;
                }
            }
        });
        const vm = app.mount('#app');
    </script>
</body>
</html>


示例中 CSS 初始 :root 的 font-size 为 16px(一个较小值,防止页面加载时瞬间出现大号文字影响用户体验),经过 fn_resize 后,:root 的 font-size 被设置为(100 * 375 / 1000 =)37.5px(iPhone X 中),那么宽度为 1000px 的设计稿中的 500px 换算到 CSS 中为 5rem,也即 37.5 * 5 = 187.5px,就是 iPhone X 的屏幕宽度的一半。

示例中 id 为 app 的 div 是用来在 PC 浏览器中限制页面内容最大宽度的(类似微信公众号发布的文章),如果网页不需要在 PC 端显示,jQuery 版代码中的 $('#app').width() 可以用 $(window).width() 来代替。

这个 div#app 一般设计有背景色,用于在宽度超过设计稿的设备上显示时区别于 body 的背景。但是当网页内容不超过一屏时,div#app 高度小于窗口,示例中与 appMinHeight 相关的代码就是为了解决这个问题。

示例中 div#app 隐藏/显示相关代码用于解决在页面加载初期由于 font-size 值变化引起的一闪而过的排版错乱。

image.png


最后补充一点,如果改变窗口大小时涉及执行耗时的操作,为避免页面卡顿,可以参考这篇文章添加函数防抖:https://xoyozo.net/Blog/Details/js-function-anti-shake

xoyozo 5 年前
7,130


引用

jQuery、moment.js、daterangepicker


例子

$('.x_dates').daterangepicker({
    "timePicker": false, // 是否显示时间
    //"dateLimit": {
    //    "days": 7 // 可选中的最大区间(天)
    //},
    "ranges": { // 快捷栏
        "今天": [moment(), moment()],
        "昨天": [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
        "最近 7 天": [moment().subtract(6, 'days'), moment()],
        "最近 30 天": [moment().subtract(29, 'days'), moment()],
        "本月": [moment().startOf('month'), moment().endOf('month')],
        "上个月": [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
    },
    startDate: daterangepicker_startDate, // moment(),
    endDate: daterangepicker_endDate, // moment(),
    autoUpdateInput: true,
    "locale": {
        "direction": "ltr",
        "format": "YYYY-MM-DD", // YYYY-MM-DD HH:mm
        "separator": " 至 ",
        "applyLabel": "确定",
        "cancelLabel": "取消",
        "fromLabel": "From",
        "toLabel": "To",
        "customRangeLabel": "自定义",
        "daysOfWeek": ["日", "一", "二", "三", "四", "五", "六"],
        "monthNames": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
        "firstDay": 1
    }
}, function (start, end, label) {
    //console.log('New date range selected: ' + start.format('YYYY-MM-DD') + ' to ' + end.format('YYYY-MM-DD') + ' (predefined range: ' + label + ')');
    fn_daterangepicker_changed(start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD'))
});


官网(含配置工具)

http://www.daterangepicker.com/


GitHub

https://github.com/dangrossman/daterangepicker


配置工具

下载的包中的 demo.html


Demo

https://awio.iljmp.com/5/drpdemo


xoyozo 5 年前
4,837

将 closeOnConfirm 设置为 false,需要关闭时调用 this.close();

例:

var $promt = $('#my-prompt').modal({
    relatedTarget: this,
    closeOnConfirm: false,
    closeOnCancel: false,
    onConfirm: function (e) {
        if (!e.data) {
            alert('输入点东西吧!')
        } else {
            alert('输入了');
            this.close();
        }
    },
    onCancel: function (e) {
        alert('不想说!');
    }
});


xoyozo 5 年前
4,190

在数据库连接字符串可设置是否将 tinyint(1) 映射为 bool,否则为 sbyte:

TreatTinyAsBoolean=false/true

tinyint(1) 一般用于表示 bool 型字段,存储内容为 0 或 1,但有时候也用来存储其它数字。

以 Discuz! 的表 pre_forum_post 为例,字段 first 和 invisible 都是 tinyint(1),但 first 只储存 0 和 1,invisible 却有 -1、-5 之类的值。

因此我们一般设置 TreatTinyAsBoolean=false,程序中 first 与 invisible 均以 sbyte 处理。


设置 TreatTinyAsBoolean=true 后,EF 或 EF Core 自动将该类型映射为 bool,方便在程序中作进一步处理。

一旦设置 TreatTinyAsBoolean=true,那么所有查询结果中 tinyint(1) 字段的返回值永远只有 1 和 0(即 True/False),即使真实值为 -1,也返回 1。

因为我们必须在确保所有的 tinyint(1) 类型字段都仅表示布尔值是才设置 TreatTinyAsBoolean=true。

一旦部分 tinyint(1) 类型字段用于存放 0 和 1 以外的数,那么就应该设置 TreatTinyAsBoolean=false。


在数据库优先的项目中,以 TreatTinyAsBoolean=false 生成数据模型后,可将明确为布尔类型的字段改为 bool。列出 MySQL 数据库中所有表所有字段中类型为 tinyint(1) 的字段值


以 .edmx 为例:

在项目中搜索该字段名,在搜索结果中找到 .edmx 文件中的两处。

.edmx 文件中的注释已经表明其包含 SSDL、CSDL、C-S mapping 三块内容,

在 SSDL content 下方找到该字段:

<Property Name="字段名" Type="tinyint" Nullable="***" />

改为

<Property Name="字段名" Type="bool" Nullable="***" />


在 CSDL content 下方找到该属性:

<Property Name="属性名" Type="SByte" Nullable="***" />

改为

<Property Name="属性名" Type="Boolean" Nullable="***" />

在解决方案管理器中展开 .edmx ->库名.tt -> 表名.cs 文件,

将模型类中的属性

public sbyte invisible { get; set; }

改为

public bool invisible { get; set; }

或 sbyte? 改为 bool?。


在 EF Core 中,直接打开对应数据表的 .cs 文件,更改属性类型即可。


相关报错:

错误: 指定的成员映射无效。类型中的成员的类型“Edm.SByte[Nullable=False,DefaultValue=]”与类型中的成员的“MySql.bool[Nullable=False,DefaultValue=]”不兼容。

InvalidOperationException: The property '***' is of type 'sbyte' which is not supported by the current database provider. Either change the property CLR type, or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

尝试先连接一次能解决此问题(概率),非常的莫名其妙:

using Data.Discuz.db_bbs2021Context dbd = new();
var conn = dbd.Database.GetDbConnection();
conn.Open();
conn.Close();


参考:

https://mysqlconnector.net/connection-options/

https://stackoverflow.com/questions/6656511/treat-tiny-as-boolean-and-entity-framework-4


2023年1月注:本文适用于 Pomelo.EntityFrameworkCore.MySql 6.0,升级到 7.0 后会出现:

System.InvalidOperationException:“The 'sbyte' property could not be mapped to the database type 'tinyint(1)' because the database provider does not support mapping 'sbyte' properties to 'tinyint(1)' columns. Consider mapping to a different database type or converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.”

解决方法:https://xoyozo.net/Blog/Details/the-sbyte-property-could-not-be-mapped-to-the-database-type-tinyint-1

xoyozo 5 年前
7,536

* 若无特殊注明,本文中的“手机”包含平板电脑


判断网页是否在移动设备中打开


Web Form:

public static bool IsMobileDevice
{
    get
    {
        return (HttpContext.Current.Request.Browser?.IsMobileDevice ?? false)
            || (HttpContext.Current.Request.UserAgent?.Contains("Mobile") ?? false);
    }
}


ASP.NET Core:

好像还没有找到,可能是因为随着响应式布局的流行以及电脑和手机之间的界线越来越不明朗,取消了这个判断?


判断网页是否在微信浏览器中打开(宽松)


Web Form:

public bool IsInWechat
{
    get
    {
        return HttpContext.Current.Request.UserAgent?.Contains("MicroMessenger") ?? false;
    }
}


ASP.NET Core:

需注入 IHttpContextAccessor

public bool IsInWechat
{
    get
    {
        string ua = httpContextAccessor.HttpContext.Request.Headers[HeaderNames.UserAgent];
        return ua?.Contains("MicroMessenger") ?? false;
    }
}


判断网页是否在微信浏览器中打开(严谨)


使用 JS-SDK 网页授权来判断




xoyozo 5 年前
4,440

Nuget 安装:X.PagedList.Mvc.Core


控制器:

using X.PagedList;

public IActionResult Index(int page = 1)
{
    ……
    return View(q.ToPagedList(page, size));
}


视图:

@using X.PagedList
@using X.PagedList.Mvc.Core
@model IPagedList<xxx>

@Html.PagedListPager(Model, page => Url.Action("Index", new { page }))


自定义(options 默认值):

@Html.PagedListPager(
    Model, 
    page => Url.Action("Index", new { page }), 
    new X.PagedList.Mvc.Common.PagedListRenderOptionsBase
    {
        HtmlEncoder = HtmlEncoder.get_Default(),
        DisplayLinkToFirstPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToLastPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToPreviousPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToNextPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToIndividualPages = true,
        DisplayPageCountAndCurrentLocation = false, // 显示总页数和当前页码
        MaximumPageNumbersToDisplay = 10, // 最多显示页码数
        DisplayEllipsesWhenNotShowingAllPageNumbers = true,
        EllipsesFormat = "&#8230;",
        LinkToFirstPageFormat = "<<",
        LinkToPreviousPageFormat = "<",
        LinkToIndividualPageFormat = "{0}",
        LinkToNextPageFormat = ">",
        LinkToLastPageFormat = ">>",
        PageCountAndCurrentLocationFormat = "Page {0} of {1}.",
        ItemSliceAndTotalFormat = "Showing items {0} through {1} of {2}.",
        FunctionToDisplayEachPageNumber = null,
        ClassToApplyToFirstListItemInPager = null,
        ClassToApplyToLastListItemInPager = null,
        ContainerDivClasses = new string[1]
        {
            "pagination-container"
        },
        UlElementClasses = new string[1]
        {
            "pagination"
        },
        LiElementClasses = Enumerable.Empty<string>(),
        PageClasses = Enumerable.Empty<string>(),
        UlElementattributes = null,
        ActiveLiElementClass = "active",
        EllipsesElementClass = "PagedList-ellipses",
        PreviousElementClass = "PagedList-skipToPrevious",
        NextElementClass = "PagedList-skipToNext",
    })


保留地址栏参数:

@{ 
    string query = Context.Request.QueryString.Value;
}
@Html.PagedListPager(Model, page => Regex.IsMatch(query, @"[?&]page=\d+")
    ? Regex.Replace(query, @"([?&])page=\d+", $"$1page={page}")
    : (query.StartsWith("?") ? $"{query}&page={page}" : $"{query}?page={page}"),
    new X.PagedList.Web.Common.PagedListRenderOptionsBase
    {
        DisplayPageCountAndCurrentLocation = true,
        MaximumPageNumbersToDisplay = 5,
    })

这里从查询字符串中判断并替换 page 值,如果有更简单的方法敬请告知。比如 Webdiyer 的分页组件会自动携带所有参数。


更多使用方式参官方文档:https://github.com/dncuug/X.PagedList


附应用于 Unify Template(一款基于 Bootstrap 的 HTML 模板)中的配置:

<style>
    .u-pagination-v1-1--active .u-pagination-v1-1 { color: #fff; border-color: #72c02c; }
    .PagedList-pageCountAndLocation { float: right !important; }
</style>
@{
    string query = Context.Request.QueryString.Value;
}
@Html.PagedListPager(Model, page => Regex.IsMatch(query, @"[?&]page=\d+")
    ? Regex.Replace(query, @"([?&])page=\d+", $"$1page={page}")
    : (query.StartsWith("?") ? $"{query}&page={page}" : $"{query}?page={page}"),
    new X.PagedList.Web.Common.PagedListRenderOptionsBase
    {
        DisplayPageCountAndCurrentLocation = true,
        MaximumPageNumbersToDisplay = 5,
        UlElementClasses = new string[] { "list-inline" },
        LiElementClasses = new string[] { "list-inline-item" },
        PageClasses = new string[] { "u-pagination-v1__item", "u-pagination-v1-1", "g-pa-7-14" },
        ActiveLiElementClass = "u-pagination-v1-1--active",
        EllipsesElementClass = "g-pa-7-14",
    })


xoyozo 5 年前
10,201

安装 Nuget 包:

BuildBundlerMinifier


项目根目录创建绑定配置文件:bundleconfig.json

示例:

[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/css/site.css",
      "wwwroot/css/custom.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  }
]

配置文件中所有路径都相对于项目根目录(而非静态文件根目录 wwwroot),因此配置的路径都要以“wwwroot”开头。

outputFileName 是压缩合并后的文件,inputFiles 是被压缩合并的原始文件集合。

对于 js 配置部分,minify.enabled 配置是否缩小,renameLocals 配置是否修改变量名,sourceMap 配置是否生成 map 映射文件。


引用示例:

<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />

asp-append-version 表示是否在引用路径添加版本参数,可实现在文件有修改时及时在客户端浏览器中生效。


* 注意:有时候“生成”不一定生效,“重新生成”肯定会生效。


更多高级用法请参考官方文档

xoyozo 5 年前
7,501