diff --git a/src/BootstrapBlazor.Server/Components/Pages/GlobalOption.razor b/src/BootstrapBlazor.Server/Components/Pages/GlobalOption.razor index b8eedf08a9d..8d18fcab022 100644 --- a/src/BootstrapBlazor.Server/Components/Pages/GlobalOption.razor +++ b/src/BootstrapBlazor.Server/Components/Pages/GlobalOption.razor @@ -1,4 +1,4 @@ -@page "/global-option" +@page "/global-option" @inject IStringLocalizer Localizer

@Localizer["Title"]

@@ -65,6 +65,21 @@
  • TableExportOptions.ArrayDelimiter 数组类型合并操作时使用的分隔符
  • +

    DateTimeSettings 日期时间全局配置

    + +

    @((MarkupString)Localizer["DateTimeSettingsIntro"].Value)

    + + + +
    builder.Services.AddBootstrapBlazor(options =>
    +{
    +    options.DateTimeSettings.ParseDateTimeCallback = value => DateTime.ParseExact(value, "yyyyMMdd", CultureInfo.InvariantCulture);
    +    options.DateTimeSettings.ParseDateTimeOffsetCallback = value => DateTimeOffset.ParseExact(value, "yyyyMMdd zzz", CultureInfo.InvariantCulture);
    +});
    +

    StepSettings 步长全局统一配置各种数据类型的步长值

    ConnectionHubOptions 步长全局统一配置各种数据类型的步长值

    diff --git a/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor b/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor index b0cc8be8d60..fe9c06c237d 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor @@ -103,9 +103,13 @@ +
    + @((MarkupString)Localizer["IsEditableTip"].Value) +

    @((MarkupString)Localizer["IsEditableCallbackTip"].Value)

    +
    - +
    diff --git a/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor.cs index c28cd05715c..d7b42dc91d2 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/DateTimePickers.razor.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using System.Globalization; + namespace BootstrapBlazor.Server.Components.Samples; /// @@ -73,6 +75,8 @@ private static string FormatterSpanString(TimeSpan ts) private DateTime? BindValue { get; set; } = DateTime.Today; + private static DateTime ParseDateTime(string value) => DateTime.ParseExact(value, "yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture); + private string BindValueString { get => BindValue.HasValue ? BindValue.Value.ToString("yyyy-MM-dd") : ""; diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index a6b7a666ad4..f287afdb633 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -169,7 +169,12 @@ "BarcodeReader": "BarcodeReader", "Block": "Block", "Bluetooth": "IBluetooth", + "BootstrapBlazorIcon": "Icon", "BootstrapIcon": "Bootstrap Icons", + "BootstrapInput": "BootstrapInput", + "BootstrapInputGroup": "BootstrapInputGroup", + "BootstrapInputNumber": "BootstrapInputNumber", + "BootstrapLabel": "Labels", "Breadcrumb": "Breadcrumb", "Breakpoints": "Breakpoints", "BrowserFinger": "BrowserFingerService", @@ -278,15 +283,11 @@ "Html2Image": "IHtml2Image", "Html2Pdf": "IHtml2Pdf", "HtmlRenderer": "HtmlRenderer", - "BootstrapBlazorIcon": "Icon", "IconPark": "ByteDance IconPark", "Icons": "Icons", "IFrame": "IFrame", "ImageCropper": "ImageCropper", "ImageViewer": "ImageViewer", - "BootstrapInput": "BootstrapInput", - "BootstrapInputGroup": "BootstrapInputGroup", - "BootstrapInputNumber": "BootstrapInputNumber", "InputUpload": "InputUpload", "Install": "Install", "IntersectionObserver": "IntersectionObserver", @@ -294,7 +295,6 @@ "IpAddress": "IpAddress", "JitViewer": "Jit Viewer", "JSExtension": "JSRuntime Extensions", - "BootstrapLabel": "Labels", "Layout": "Layout", "LayoutComponents": "Layouts", "LayoutPage": "Admin Template", @@ -525,6 +525,9 @@ "Title": "Globalization" }, "BootstrapBlazor.Server.Components.Pages.GlobalOption": { + "DateTimeSettingsIntro": "Use this configuration to customize the text conversion logic of the DateTimePicker component when IsEditable=\"true\". The global configuration is used when no component-level callback is set; a component-level callback takes precedence over the global configuration.", + "DateTimeSettingsParseDateTimeCallback": "ParseDateTimeCallback Custom callback for parsing DateTime", + "DateTimeSettingsParseDateTimeOffsetCallback": "ParseDateTimeOffsetCallback Custom callback for parsing DateTimeOffset", "SubTitle": "Added component ErrorLogger Through this component, global logs and exceptions can be output uniformly; currently, the Blazor framework does not provide a MVC like Global exception The overall solution", "Title": "Global exception" }, @@ -1679,7 +1682,9 @@ "FeatureShowLunarIntro": "ShowLunar Whether to display the lunar calendar", "FeatureShowSolarTerm": "Solar Term", "FeatureShowSolarTermIntro": "ShowSolarTerm Whether to display the 24 solar terms", - "IsEditableIntro": "Enable manual input function by setting IsEditable=\"true\"", + "IsEditableCallbackTip": "To automatically convert entered text to DateTime/DateTimeOffset, use the component-level ParseDateTimeCallback and ParseDateTimeOffsetCallback parameters to customize the conversion logic. Corresponding options are also available through the DateTimeSettings property of the global BootstrapBlazorOptions configuration. The component callback takes precedence, followed by the global callback. If neither is configured, the default DateTimeHelper.ToDateTime conversion logic is used.", + "IsEditableIntro": "Enable manual date input by setting IsEditable=\"true\", and customize how the input text is converted to a date with ParseDateTimeCallback", + "IsEditableTip": "This example sets ParseDateTimeCallback to convert text in yyyyMMdd format to a date. Try entering 20250808. When binding to DateTimeOffset or DateTimeOffset?, use ParseDateTimeOffsetCallback to customize the conversion logic", "IsEditableTitle": "Editable", "MinValueIntro": "Set the MinValue property value to the MaxValue limit the range of optional values, in this case setting the range to 45days", "MinValueTitle": "Set the range of values", @@ -3345,6 +3350,54 @@ "Scale2ButtonText": "ZoomIn", "SetDataButtonText": "SetData" }, + "BootstrapBlazor.Server.Components.Samples.Modals": { + "ModalsBigPopup": "big popup", + "ModalsCenterVerticallyIntro": "Set the vertical centering of the popup component via IsCentered", + "ModalsCenterVerticallyTitle": "Center vertically", + "ModalsDescription": "Inform the user and host relevant actions while preserving the current page state", + "ModalsDialogResizeIntro": "Resize the modal box by setting the ShowResize property", + "ModalsDialogResizeTitle": "Resize", + "ModalsDialogSizeIntro": "Set the size of the popup component through Size", + "ModalsDialogSizeTitle": "Bullet size", + "ModalsFullScreenPopup": "Full screen popup", + "ModalsFullScreenPopup1200": "Full screen popup(<1200px)", + "ModalsFullScreenPopup1400": "Full screen popup(<1400px)", + "ModalsFullScreenPopup992": "Full screen popup(<992px)", + "ModalsFullScreenSizeIntro": "Just set the property FullScreenSize", + "ModalsFullScreenSizeTitle": "Full screen popup", + "ModalsIsBackdropIntro": "Click the area outside the pop-up window to close the pop-up window effect by default", + "ModalsIsBackdropTitle": "IsBackdrop background off mode", + "ModalsIsBackdropToClose": "Click on the background to close the popup", + "ModalsIsDraggableIntro": "Click on the title bar of the pop-up window to drag and drop the pop-up window", + "ModalsIsDraggableTitle": "Draggable popup", + "ModalsLargeFullScreenPopup": "Large full screen popup", + "ModalsLargeFullScreenPopupWindow": "Large full-screen pop-up window", + "ModalsLongContentIntro": "Use IsScrolling to set the scroll wheel sliding function of the pop-up frame component for the excess content", + "ModalsLongContentTitle": "Extra long content", + "ModalsMaximizeIntro": "Show the maximize button by setting the ShowMaximinzeButton popup", + "ModalsMaximizePopup": "Maximize popup", + "ModalsMaximizeTitle": "Maximize button", + "ModalsNormalDefaultPopup": "Default popup", + "ModalsNormalDefaultPopupText": "I am the text in the pop-up window", + "ModalsNormalIntro": "A dialog box pops up, suitable for scenarios that require more customization", + "ModalsNormalIsKeyboard": "by setting Modal component IsKeyboard:true parameter, whether to open the pop-up window is supported ESC", + "ModalsNormalPopups": "Pop-ups", + "ModalsNormalPopupText": "popup text", + "ModalsNormalPopupTitle": "Popup title", + "ModalsNormalTitle": "Basic usage", + "ModalsOverSizedPopup": "OverSized pop-up window", + "ModalsResize": "Resize", + "ModalsScrollBarPopup": "Built-in scroll bar popup", + "ModalsShownCallbackAsyncIntro": "By setting the ShowCallbackAsync callback delegate, this method will be recalled after the pop-up window is displayed", + "ModalsShownCallbackAsyncTitle": "Popup shows callback method", + "ModalsSmallPopup": "small popup", + "ModalsSuperLargeFullScreenPopupWindow": "Super large full-screen pop-up window", + "ModalsSuperLargePopup": "super large pop-up window", + "ModalsTitle": "Modal box", + "ModalsTitlePopupWindowText": "I am the text in the pop-up window", + "ModalsVerticallyCenteredPopup": "Vertically centered popup", + "ModalsVeryLongContent": "Pop-up window with very long content" + }, "BootstrapBlazor.Server.Components.Samples.Modbus.ModbusFactories": { "BenchmarkProjectIntro": "The project includes a Benchmark test project", "BenchmarkResultIntro": "The Benchmark results are shown below:", @@ -3366,9 +3419,9 @@ "FactoryMethodRtu": "Use GetOrCreateRtuMaster to get an IModbusRtuClient instance", "FactoryMethodRtuOverTcp": "Use GetOrCreateRtuOverTcpMaster to get an IModbusTcpClient instance", "FactoryMethodRtuOverUdp": "Use GetOrCreateRtuOverUdpMaster to get an IModbusTcpClient instance", + "FactoryMethodsTitle": "IModbusFactory instance methods", "FactoryMethodTcp": "Use GetOrCreateTcpMaster to get an IModbusTcpClient instance", "FactoryMethodUdp": "Use GetOrCreateUdpMaster to get an IModbusTcpClient instance", - "FactoryMethodsTitle": "IModbusFactory instance methods", "FloatCustomExtensionIntro": "By accessing the raw Buffer through IModbusResponse, you can easily build custom extensions for your own business data types. For example, two consecutive registers can be interpreted as a 32-bit floating-point value that follows the IEEE 754 standard.", "FloatEndiannessTips": "Note: when converting Buffer into custom types such as a 32-bit floating-point value, pay attention to endianness. Byte order changes how the data is interpreted, and the wrong byte order can produce incorrect results. Choose the proper conversion based on the actual device or protocol specification.", "FloatIndexIntro": "In the methods above, the index parameter represents the float value index. Each float occupies 2 registers. If multiple float values are read at once, adjust index to access them in sequence.", @@ -3399,54 +3452,6 @@ "TransportTcp": "Modbus TCP/IP: Runs over Ethernet using the TCP/IP protocol, with the default port 502. It adds an MBAP header on top of the Modbus RTU protocol and omits the CRC checksum because TCP itself already provides a reliable connection service.", "WasmTips": "Special note: this service does not support wasm mode" }, - "BootstrapBlazor.Server.Components.Samples.Modals": { - "ModalsBigPopup": "big popup", - "ModalsCenterVerticallyIntro": "Set the vertical centering of the popup component via IsCentered", - "ModalsCenterVerticallyTitle": "Center vertically", - "ModalsDescription": "Inform the user and host relevant actions while preserving the current page state", - "ModalsDialogResizeIntro": "Resize the modal box by setting the ShowResize property", - "ModalsDialogResizeTitle": "Resize", - "ModalsDialogSizeIntro": "Set the size of the popup component through Size", - "ModalsDialogSizeTitle": "Bullet size", - "ModalsFullScreenPopup": "Full screen popup", - "ModalsFullScreenPopup1200": "Full screen popup(<1200px)", - "ModalsFullScreenPopup1400": "Full screen popup(<1400px)", - "ModalsFullScreenPopup992": "Full screen popup(<992px)", - "ModalsFullScreenSizeIntro": "Just set the property FullScreenSize", - "ModalsFullScreenSizeTitle": "Full screen popup", - "ModalsIsBackdropIntro": "Click the area outside the pop-up window to close the pop-up window effect by default", - "ModalsIsBackdropTitle": "IsBackdrop background off mode", - "ModalsIsBackdropToClose": "Click on the background to close the popup", - "ModalsIsDraggableIntro": "Click on the title bar of the pop-up window to drag and drop the pop-up window", - "ModalsIsDraggableTitle": "Draggable popup", - "ModalsLargeFullScreenPopup": "Large full screen popup", - "ModalsLargeFullScreenPopupWindow": "Large full-screen pop-up window", - "ModalsLongContentIntro": "Use IsScrolling to set the scroll wheel sliding function of the pop-up frame component for the excess content", - "ModalsLongContentTitle": "Extra long content", - "ModalsMaximizeIntro": "Show the maximize button by setting the ShowMaximinzeButton popup", - "ModalsMaximizePopup": "Maximize popup", - "ModalsMaximizeTitle": "Maximize button", - "ModalsNormalDefaultPopup": "Default popup", - "ModalsNormalDefaultPopupText": "I am the text in the pop-up window", - "ModalsNormalIntro": "A dialog box pops up, suitable for scenarios that require more customization", - "ModalsNormalIsKeyboard": "by setting Modal component IsKeyboard:true parameter, whether to open the pop-up window is supported ESC", - "ModalsNormalPopups": "Pop-ups", - "ModalsNormalPopupText": "popup text", - "ModalsNormalPopupTitle": "Popup title", - "ModalsNormalTitle": "Basic usage", - "ModalsOverSizedPopup": "OverSized pop-up window", - "ModalsResize": "Resize", - "ModalsScrollBarPopup": "Built-in scroll bar popup", - "ModalsShownCallbackAsyncIntro": "By setting the ShowCallbackAsync callback delegate, this method will be recalled after the pop-up window is displayed", - "ModalsShownCallbackAsyncTitle": "Popup shows callback method", - "ModalsSmallPopup": "small popup", - "ModalsSuperLargeFullScreenPopupWindow": "Super large full-screen pop-up window", - "ModalsSuperLargePopup": "super large pop-up window", - "ModalsTitle": "Modal box", - "ModalsTitlePopupWindowText": "I am the text in the pop-up window", - "ModalsVerticallyCenteredPopup": "Vertically centered popup", - "ModalsVeryLongContent": "Pop-up window with very long content" - }, "BootstrapBlazor.Server.Components.Samples.MouseFollowers": { "MouseFollowerIconIntro": "If you use SVG spritesheet in your project and want to display them in the cursor, then you can use this method. In this case, you need to specify the path to the SVG sprite in the options and set class names.", "MouseFollowerIconTitle": "Icon mode", @@ -5777,10 +5782,10 @@ "TransferBindTitle": "Two-way binding", "TransferCustomerIntro": "You can customize the list title copy, button copy, rendering function of data items, check status copy at the bottom of the list, content area at the bottom of the list, and so on.", "TransferCustomerTitle": "Customizable", - "TransferDisableIntro": "When you set the IsDisabled property value to true, the component suppresses input", - "TransferDisableTitle": "Disable", "TransferDisabledCallbackIntro": "By setting the OnDisabledCallback callback method, you can control whether specific items are disabled. The first argument target identifies the current panel — left for the left panel and right for the right panel — so you can apply different disable rules to each side. The second argument is the SelectedItem, which is null for the header select-all option. In this example the left panel disables 2 4 6 and the right panel disables 5. Disabled items cannot be checked and are excluded from the select-all/deselect-all operations", "TransferDisabledCallbackTitle": "Disable specific items", + "TransferDisableIntro": "When you set the IsDisabled property value to true, the component suppresses input", + "TransferDisableTitle": "Disable", "TransferItemClassIntro": "By setting the OnSetItemClass callback method styles options based on the values of SelectedItem", "TransferItemClassTitle": "Set the Item style", "TransferMinMaxIntro": "By setting Min Max Parameter to limit the number of options", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 3182df86e80..67dbb377588 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -169,7 +169,12 @@ "BarcodeReader": "条码扫描 BarcodeReader", "Block": "条件块 Block", "Bluetooth": "蓝牙服务 IBluetoothService", + "BootstrapBlazorIcon": "图标 Icon", "BootstrapIcon": "Bootstrap Icons", + "BootstrapInput": "输入框 BootstrapInput", + "BootstrapInputGroup": "输入组 BootstrapInputGroup", + "BootstrapInputNumber": "数字框 BootstrapInputNumber", + "BootstrapLabel": "表单标签", "Breadcrumb": "面包屑 Breadcrumb", "Breakpoints": "断点阈值", "BrowserFinger": "浏览器指纹 BrowserFingerService", @@ -278,15 +283,11 @@ "Html2Image": "Html 转 Image IHtml2Image", "Html2Pdf": "Html 转 Pdf IHtml2Pdf", "HtmlRenderer": "Html 转换器 HtmlRenderer", - "BootstrapBlazorIcon": "图标 Icon", "IconPark": "字节跳动图标 IconPark", "Icons": "内置图标", "IFrame": "内嵌框架 IFrame", "ImageCropper": "图像裁剪 ImageCropper", "ImageViewer": "图片 ImageViewer", - "BootstrapInput": "输入框 BootstrapInput", - "BootstrapInputGroup": "输入组 BootstrapInputGroup", - "BootstrapInputNumber": "数字框 BootstrapInputNumber", "InputUpload": "上传组件 InputUpload", "Install": "安装", "IntersectionObserver": "交叉观察者 IntersectionObserver", @@ -294,7 +295,6 @@ "IpAddress": "IP 地址 IpAddress", "JitViewer": "文件预览器 JitViewer", "JSExtension": "JSRuntime 扩展", - "BootstrapLabel": "表单标签", "Layout": "布局组件 Layout", "LayoutComponents": "布局组件", "LayoutPage": "后台模拟器", @@ -525,6 +525,9 @@ "Title": "全球化" }, "BootstrapBlazor.Server.Components.Pages.GlobalOption": { + "DateTimeSettingsIntro": "通过此配置统一设置 DateTimePicker 组件在 IsEditable=\"true\" 时的文本转换逻辑。组件未单独设置回调时,使用此处的全局配置;组件回调的优先级高于全局配置。", + "DateTimeSettingsParseDateTimeCallback": "ParseDateTimeCallback 自定义解析 DateTime 的回调方法", + "DateTimeSettingsParseDateTimeOffsetCallback": "ParseDateTimeOffsetCallback 自定义解析 DateTimeOffset 的回调方法", "SubTitle": "组件库提供一种对当前应用程序中所有组件进行配置的方法,通过 BootstrapBlazorOptions 全局配置类实现此功能", "Title": "全局配置" }, @@ -1679,7 +1682,9 @@ "FeatureShowLunarIntro": "ShowLunar 是否显示农历", "FeatureShowSolarTerm": "24 节气", "FeatureShowSolarTermIntro": "ShowSolarTerm 是否显示 24 节气", - "IsEditableIntro": "通过设置 IsEditable=\"true\" 开启手工录入日期功能", + "IsEditableCallbackTip": "为了方便将录入的字符串自动转换为 DateTime/DateTimeOffset 数据类型,除了组件提供的 ParseDateTimeCallbackParseDateTimeOffsetCallback 参数可用于自定义转换逻辑外,全局配置 BootstrapBlazorOptionsDateTimeSettings 属性也提供了对应的配置项。组件会优先使用自身的回调函数,其次使用全局配置的回调函数;如果均未设置,则使用默认的 DateTimeHelper.ToDateTime 转换逻辑。", + "IsEditableIntro": "通过设置 IsEditable=\"true\" 开启手工录入日期功能,通过 ParseDateTimeCallback 自定义录入文本的日期格式转换逻辑", + "IsEditableTip": "本例设置 ParseDateTimeCallbackyyyyMMdd 格式的文本转换为日期,请尝试输入 20250808。绑定值为 DateTimeOffsetDateTimeOffset? 时,请使用 ParseDateTimeOffsetCallback 参数自定义转换逻辑", "IsEditableTitle": "手工录入", "MinValueIntro": "设置 MinValue 属性值与 MaxValue 限制可选值范围,本例中设置范围为 45 天", "MinValueTitle": "设置值范围", @@ -3345,6 +3350,54 @@ "Scale2ButtonText": "放大", "SetDataButtonText": "SetData" }, + "BootstrapBlazor.Server.Components.Samples.Modals": { + "ModalsBigPopup": "大弹窗", + "ModalsCenterVerticallyIntro": "通过 IsCentered 设置弹框组件的垂直居中", + "ModalsCenterVerticallyTitle": "垂直居中", + "ModalsDescription": "在保留当前页面状态的情况下,告知用户并承载相关操作", + "ModalsDialogResizeIntro": "通过设置 ShowResize=\"true\" 可以通过鼠标拉动弹窗右下角进行窗口大小调整", + "ModalsDialogResizeTitle": "调整大小弹窗", + "ModalsDialogSizeIntro": "通过 Size 设置弹框组件的大小", + "ModalsDialogSizeTitle": "弹框大小", + "ModalsFullScreenPopup": "全屏弹窗", + "ModalsFullScreenPopup1200": "全屏弹窗(<1200px)", + "ModalsFullScreenPopup1400": "全屏弹窗(<1400px)", + "ModalsFullScreenPopup992": "全屏弹窗(<992px)", + "ModalsFullScreenSizeIntro": "设置属性 FullScreenSize 即可", + "ModalsFullScreenSizeTitle": "全屏弹窗", + "ModalsIsBackdropIntro": "点击弹窗以外区域默认关闭弹窗效果", + "ModalsIsBackdropTitle": "IsBackdrop 背景关闭模式", + "ModalsIsBackdropToClose": "点击背景可关闭弹窗", + "ModalsIsDraggableIntro": "点击弹窗标题栏对弹窗进行拖拽", + "ModalsIsDraggableTitle": "可拖拽弹窗", + "ModalsLargeFullScreenPopup": "大全屏弹窗", + "ModalsLargeFullScreenPopupWindow": "超大全屏弹窗", + "ModalsLongContentIntro": "通过 IsScrolling 针对超出内容设置弹框组件滚轮滑动功能", + "ModalsLongContentTitle": "超长内容", + "ModalsMaximizeIntro": "通过设置 ShowMaximinzeButton 弹窗显示最大化按钮", + "ModalsMaximizePopup": "可最大化弹窗", + "ModalsMaximizeTitle": "最大化按钮", + "ModalsNormalDefaultPopup": "默认弹窗", + "ModalsNormalDefaultPopupText": "我是弹窗内正文", + "ModalsNormalIntro": "弹出一个对话框,适合需要定制性更大的场景", + "ModalsNormalIsKeyboard": "通过设置 Modal 组件的 IsKeyboard:true 参数,开启弹窗是否支持 ESC", + "ModalsNormalPopups": "弹窗", + "ModalsNormalPopupText": "弹窗正文", + "ModalsNormalPopupTitle": "弹窗标题", + "ModalsNormalTitle": "基本用法", + "ModalsOverSizedPopup": "超大弹窗", + "ModalsResize": "弹框大小", + "ModalsScrollBarPopup": "内置滚动条弹窗", + "ModalsShownCallbackAsyncIntro": "通过设置 ShownCallbackAsync 回调委托,弹窗显示后回调此方法", + "ModalsShownCallbackAsyncTitle": "弹窗已显示回调方法", + "ModalsSmallPopup": "小弹窗", + "ModalsSuperLargeFullScreenPopupWindow": "超超大全屏弹窗", + "ModalsSuperLargePopup": "超超大弹窗", + "ModalsTitle": "Modal 模态框", + "ModalsTitlePopupWindowText": "我是弹窗内正文", + "ModalsVerticallyCenteredPopup": "垂直居中的弹窗", + "ModalsVeryLongContent": "内容超长的弹窗" + }, "BootstrapBlazor.Server.Components.Samples.Modbus.ModbusFactories": { "BenchmarkProjectIntro": "项目包含 Benchmark 基准测试工程", "BenchmarkResultIntro": "Benchmark 结果如下:", @@ -3366,9 +3419,9 @@ "FactoryMethodRtu": "通过 GetOrCreateRtuMaster 方法得到 IModbusRtuClient 实例", "FactoryMethodRtuOverTcp": "通过 GetOrCreateRtuOverTcpMaster 方法得到 IModbusTcpClient 实例", "FactoryMethodRtuOverUdp": "通过 GetOrCreateRtuOverUdpMaster 方法得到 IModbusTcpClient 实例", + "FactoryMethodsTitle": "IModbusFactory 实例方法", "FactoryMethodTcp": "通过 GetOrCreateTcpMaster 方法得到 IModbusTcpClient 实例", "FactoryMethodUdp": "通过 GetOrCreateUdpMaster 方法得到 IModbusTcpClient 实例", - "FactoryMethodsTitle": "IModbusFactory 实例方法", "FloatCustomExtensionIntro": "通过接口 IModbusResponse 获得到其原始数据 Buffer 可以通过自定义扩展非常方便的扩展出符合自己业务的数据类型。如通过连续 2 个寄存器存储的数据,得到遵循 IEEE 754 标准的 32 位 浮点数", "FloatEndiannessTips": "注意:在将 Buffer 转换为自定义类型(如 32 位浮点数)时,需要注意字节序(Endianness)。字节序会影响数据的解释方式,错误的字节序可能导致解析结果不正确。请根据实际设备或协议规范选择合适的字节序进行转换。", "FloatIndexIntro": "以上方法中的参数 index 表示 float 值索引,每个 float 占用 2 个寄存器。若一次读取多个浮点数,可通过调整 index 依次访问。", @@ -3399,54 +3452,6 @@ "TransportTcp": "Modbus TCP/IP: 运行于以太网上,使用 TCP/IP 协议,默认端口 502。它在 Modbus RTU 协议基础上添加了 MBAP 报文头,并由于 TCP 本身是可靠连接的服务,因此去掉了 CRC 校验码。", "WasmTips": "特别注意:本服务不支持 wasm 模式" }, - "BootstrapBlazor.Server.Components.Samples.Modals": { - "ModalsBigPopup": "大弹窗", - "ModalsCenterVerticallyIntro": "通过 IsCentered 设置弹框组件的垂直居中", - "ModalsCenterVerticallyTitle": "垂直居中", - "ModalsDescription": "在保留当前页面状态的情况下,告知用户并承载相关操作", - "ModalsDialogResizeIntro": "通过设置 ShowResize=\"true\" 可以通过鼠标拉动弹窗右下角进行窗口大小调整", - "ModalsDialogResizeTitle": "调整大小弹窗", - "ModalsDialogSizeIntro": "通过 Size 设置弹框组件的大小", - "ModalsDialogSizeTitle": "弹框大小", - "ModalsFullScreenPopup": "全屏弹窗", - "ModalsFullScreenPopup1200": "全屏弹窗(<1200px)", - "ModalsFullScreenPopup1400": "全屏弹窗(<1400px)", - "ModalsFullScreenPopup992": "全屏弹窗(<992px)", - "ModalsFullScreenSizeIntro": "设置属性 FullScreenSize 即可", - "ModalsFullScreenSizeTitle": "全屏弹窗", - "ModalsIsBackdropIntro": "点击弹窗以外区域默认关闭弹窗效果", - "ModalsIsBackdropTitle": "IsBackdrop 背景关闭模式", - "ModalsIsBackdropToClose": "点击背景可关闭弹窗", - "ModalsIsDraggableIntro": "点击弹窗标题栏对弹窗进行拖拽", - "ModalsIsDraggableTitle": "可拖拽弹窗", - "ModalsLargeFullScreenPopup": "大全屏弹窗", - "ModalsLargeFullScreenPopupWindow": "超大全屏弹窗", - "ModalsLongContentIntro": "通过 IsScrolling 针对超出内容设置弹框组件滚轮滑动功能", - "ModalsLongContentTitle": "超长内容", - "ModalsMaximizeIntro": "通过设置 ShowMaximinzeButton 弹窗显示最大化按钮", - "ModalsMaximizePopup": "可最大化弹窗", - "ModalsMaximizeTitle": "最大化按钮", - "ModalsNormalDefaultPopup": "默认弹窗", - "ModalsNormalDefaultPopupText": "我是弹窗内正文", - "ModalsNormalIntro": "弹出一个对话框,适合需要定制性更大的场景", - "ModalsNormalIsKeyboard": "通过设置 Modal 组件的 IsKeyboard:true 参数,开启弹窗是否支持 ESC", - "ModalsNormalPopups": "弹窗", - "ModalsNormalPopupText": "弹窗正文", - "ModalsNormalPopupTitle": "弹窗标题", - "ModalsNormalTitle": "基本用法", - "ModalsOverSizedPopup": "超大弹窗", - "ModalsResize": "弹框大小", - "ModalsScrollBarPopup": "内置滚动条弹窗", - "ModalsShownCallbackAsyncIntro": "通过设置 ShownCallbackAsync 回调委托,弹窗显示后回调此方法", - "ModalsShownCallbackAsyncTitle": "弹窗已显示回调方法", - "ModalsSmallPopup": "小弹窗", - "ModalsSuperLargeFullScreenPopupWindow": "超超大全屏弹窗", - "ModalsSuperLargePopup": "超超大弹窗", - "ModalsTitle": "Modal 模态框", - "ModalsTitlePopupWindowText": "我是弹窗内正文", - "ModalsVerticallyCenteredPopup": "垂直居中的弹窗", - "ModalsVeryLongContent": "内容超长的弹窗" - }, "BootstrapBlazor.Server.Components.Samples.MouseFollowers": { "MouseFollowerIconIntro": "在光标中显示SVG图", "MouseFollowerIconTitle": "图标模式", @@ -5777,10 +5782,10 @@ "TransferBindTitle": "双向绑定", "TransferCustomerIntro": "可以对列表标题文案、按钮文案、数据项的渲染函数、列表底部的勾选状态文案、列表底部的内容区等进行自定义。", "TransferCustomerTitle": "可自定义", - "TransferDisableIntro": "设置 IsDisabled 属性值为 true 时,组件禁止输入", - "TransferDisableTitle": "禁用", "TransferDisabledCallbackIntro": "通过设置 OnDisabledCallback 回调方法控制指定选项是否禁用。回调首个参数 target 用于标识当前面板,左侧面板为 left、右侧面板为 right,可据此对左右两侧分别设置禁用规则;第二个参数为 SelectedItem,为 null 时代表头部全选项。本例左侧面板禁用 2 4 6,右侧面板禁用 5。被禁用的选项不可勾选,且不参与全选/取消全选操作", "TransferDisabledCallbackTitle": "禁用指定选项", + "TransferDisableIntro": "设置 IsDisabled 属性值为 true 时,组件禁止输入", + "TransferDisableTitle": "禁用", "TransferItemClassIntro": "通过设置 OnSetItemClass 回调方法根据 SelectedItem 值设置选项样式", "TransferItemClassTitle": "设置 Item 样式", "TransferMinMaxIntro": "通过设置 Min Max 参数来限制选项个数", diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index f7361db4ab6..099483c4ea6 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -342,7 +342,7 @@ public string? Format /// v10.9.1 /// [Parameter] - public Func? ParseDateTimeResolve { get; set; } + public Func? ParseDateTimeCallback { get; set; } /// /// 自定义解析日期时间偏移的方法 @@ -350,7 +350,7 @@ public string? Format /// v10.9.1 /// [Parameter] - public Func? ParseDateTimeOffsetResolve { get; set; } + public Func? ParseDateTimeOffsetCallback { get; set; } [Inject] [NotNull] @@ -601,7 +601,7 @@ protected override bool TryParseValueFromString(string value, [MaybeNullWhen(fal private bool TryParseDateTime(string value, out object val) { var op = Options.CurrentValue; - var resolve = ParseDateTimeResolve ?? op.DateTimeSettings.ParseDateTimeResolve; + var resolve = ParseDateTimeCallback ?? op.DateTimeSettings.ParseDateTimeCallback; if (resolve != null) { val = resolve(value); @@ -616,7 +616,7 @@ private bool TryParseDateTime(string value, out object val) private bool TryParseDateTimeOffset(string value, out object val) { var op = Options.CurrentValue; - var resolve = ParseDateTimeOffsetResolve ?? op.DateTimeSettings.ParseDateTimeOffsetResolve; + var resolve = ParseDateTimeOffsetCallback ?? op.DateTimeSettings.ParseDateTimeOffsetCallback; if (resolve != null) { val = resolve(value); diff --git a/src/BootstrapBlazor/Options/DateTimeSettings.cs b/src/BootstrapBlazor/Options/DateTimeSettings.cs index 6aa363b4b71..85bfe2f7219 100644 --- a/src/BootstrapBlazor/Options/DateTimeSettings.cs +++ b/src/BootstrapBlazor/Options/DateTimeSettings.cs @@ -13,10 +13,10 @@ public class DateTimeSettings /// /// 自定义解析日期时间的方法 /// - public Func? ParseDateTimeResolve { get; set; } + public Func? ParseDateTimeCallback { get; set; } /// /// 自定义解析日期时间偏移的方法 /// - public Func? ParseDateTimeOffsetResolve { get; set; } + public Func? ParseDateTimeOffsetCallback { get; set; } } diff --git a/src/BootstrapBlazor/Utils/DateTimeHelper.cs b/src/BootstrapBlazor/Utils/DateTimeHelper.cs index 1c5bd7e6976..21884b6e2aa 100644 --- a/src/BootstrapBlazor/Utils/DateTimeHelper.cs +++ b/src/BootstrapBlazor/Utils/DateTimeHelper.cs @@ -7,7 +7,11 @@ namespace BootstrapBlazor.Components; -static class DateTimeHelper +/// +/// 日期时间相关帮助类 +/// DateTime related helper class +/// +public static class DateTimeHelper { /// /// 无分隔符等标准解析无法识别的紧凑格式,需要显式列出 @@ -20,30 +24,47 @@ static class DateTimeHelper "yyyyMMddHHmmssfff", "yyyyMMdd HHmmss", "yyyyMMdd HH:mm:ss", - "yyyyMMdd HH:mm", - "yyyy-M-d", - "yyyy/M/d" + "yyyyMMdd HH:mm" ]; /// - /// 将字符串解析为 ,无法解析时返回 + /// 将字符串解析为 ,无法解析时返回 + /// Parse the string to , return if parsing fails /// + /// + /// 要解析的字符串 + /// The string to parse + /// public static DateTime? ToDateTime(string value) => TryToDateTime(value, out var result) ? result : null; /// - /// 将字符串解析为 ,无法解析时返回 + /// 将字符串解析为 ,无法解析时返回 + /// Parse the string to , return if parsing fails /// - /// 要解析的字符串 - /// 解析失败时返回的默认值 + /// + /// 要解析的字符串 + /// The string to parse + /// + /// + /// 解析失败时返回的默认值 + /// The default value to return if parsing fails + /// public static DateTime ToDateTime(string value, DateTime defaultValue) => TryToDateTime(value, out var result) ? result : defaultValue; /// - /// 尝试将字符串解析为 + /// 尝试将字符串解析为 + /// Try to parse the string to /// - /// 要解析的字符串 - /// 解析成功时的结果,失败时为 + /// + /// 要解析的字符串 + /// The string to parse + /// + /// + /// 解析成功时的结果,失败时为 + /// The result when parsing succeeds, otherwise + /// public static bool TryToDateTime(string value, out DateTime result) { result = DateTime.MinValue; @@ -65,14 +86,13 @@ public static bool TryToDateTime(string value, out DateTime result) } /// - /// 将字符串解析为 ,无法解析时返回 + /// 将字符串解析为 ,无法解析时返回 + /// Parse the string to , return if parsing fails /// - /// - /// 适用于 DateTimePicker 绑定 的场景; - /// 不含时区信息时按本地时区处理。 - /// - /// 要解析的字符串 - /// 解析成功返回对应的 ,否则返回 + /// + /// 要解析的字符串 + /// The string to parse + /// public static DateTimeOffset? ToDateTimeOffset(string value) { if (TryToDateTime(value, out var dateTime)) diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs index 4fd607b61b5..e8dbab390f6 100644 --- a/test/UnitTest/Components/DateTimePickerTest.cs +++ b/test/UnitTest/Components/DateTimePickerTest.cs @@ -1492,7 +1492,7 @@ public async Task ParseDateTimeResolve_Parameter_Ok() pb.Add(a => a.IsEditable, true); pb.Add(a => a.ViewMode, DatePickerViewMode.Date); pb.Add(a => a.DateFormat, "MM/dd/yyyy"); - pb.Add(a => a.ParseDateTimeResolve, v => new DateTime(2020, 1, 1)); + pb.Add(a => a.ParseDateTimeCallback, v => new DateTime(2020, 1, 1)); }); var input = cut.Find(".datetime-picker-input"); @@ -1506,7 +1506,7 @@ public async Task ParseDateTimeResolve_Options_Ok() { // 全局配置自定义解析方法 var options = Context.Services.GetRequiredService>(); - options.CurrentValue.DateTimeSettings.ParseDateTimeResolve = v => new DateTime(2021, 2, 2); + options.CurrentValue.DateTimeSettings.ParseDateTimeCallback = v => new DateTime(2021, 2, 2); var cut = Context.Render>(pb => { @@ -1521,7 +1521,7 @@ public async Task ParseDateTimeResolve_Options_Ok() // 组件参数优先级高于全局配置 cut.Render(pb => { - pb.Add(a => a.ParseDateTimeResolve, v => new DateTime(2022, 3, 3)); + pb.Add(a => a.ParseDateTimeCallback, v => new DateTime(2022, 3, 3)); }); await cut.InvokeAsync(() => input.Change("test")); Assert.Equal(new DateTime(2022, 3, 3), cut.Instance.Value); @@ -1536,7 +1536,7 @@ public async Task ParseDateTimeOffsetResolve_Parameter_Ok() pb.Add(a => a.IsEditable, true); pb.Add(a => a.ViewMode, DatePickerViewMode.Date); pb.Add(a => a.DateFormat, "MM/dd/yyyy"); - pb.Add(a => a.ParseDateTimeOffsetResolve, v => expected); + pb.Add(a => a.ParseDateTimeOffsetCallback, v => expected); }); var input = cut.Find(".datetime-picker-input"); @@ -1551,7 +1551,7 @@ public async Task ParseDateTimeOffsetResolve_Options_Ok() // 全局配置自定义解析方法 var expected = new DateTimeOffset(new DateTime(2021, 2, 2), TimeSpan.FromHours(8)); var options = Context.Services.GetRequiredService>(); - options.CurrentValue.DateTimeSettings.ParseDateTimeOffsetResolve = v => expected; + options.CurrentValue.DateTimeSettings.ParseDateTimeOffsetCallback = v => expected; var cut = Context.Render>(pb => { @@ -1567,7 +1567,7 @@ public async Task ParseDateTimeOffsetResolve_Options_Ok() var expectedParameter = new DateTimeOffset(new DateTime(2022, 3, 3), TimeSpan.FromHours(8)); cut.Render(pb => { - pb.Add(a => a.ParseDateTimeOffsetResolve, v => expectedParameter); + pb.Add(a => a.ParseDateTimeOffsetCallback, v => expectedParameter); }); await cut.InvokeAsync(() => input.Change("test")); Assert.Equal(expectedParameter, cut.Instance.Value); diff --git a/test/UnitTest/Utils/DateTimeHelperTest.cs b/test/UnitTest/Utils/DateTimeHelperTest.cs index a1db225c7c4..2a00e7ff94e 100644 --- a/test/UnitTest/Utils/DateTimeHelperTest.cs +++ b/test/UnitTest/Utils/DateTimeHelperTest.cs @@ -3,37 +3,29 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using System.Runtime.CompilerServices; - namespace UnitTest.Utils; /// -/// DateTimeHelper 为内部类,通过 配合 -/// 直接访问其静态方法进行测试 +/// 测试类 /// public class DateTimeHelperTest { - /// - /// 内部类型的程序集限定名 - /// - private const string HelperTypeName = "BootstrapBlazor.Components.DateTimeHelper, BootstrapBlazor"; - [Fact] public void ToDateTime_Ok() { // 紧凑格式解析成功 - Assert.Equal(new DateTime(2026, 5, 1), ToDateTime(null, "20260501")); + Assert.Equal(new DateTime(2026, 5, 1), DateTimeHelper.ToDateTime("20260501")); // 带分隔符格式解析成功 - Assert.Equal(new DateTime(2026, 5, 1, 13, 4, 5), ToDateTime(null, "2026-05-01 13:04:05")); + Assert.Equal(new DateTime(2026, 5, 1, 13, 4, 5), DateTimeHelper.ToDateTime("2026-05-01 13:04:05")); } [Fact] public void ToDateTime_Null() { // 解析失败返回 null - Assert.Null(ToDateTime(null, "test")); - Assert.Null(ToDateTime(null, null!)); + Assert.Null(DateTimeHelper.ToDateTime("test")); + Assert.Null(DateTimeHelper.ToDateTime(null!)); } [Fact] @@ -42,10 +34,10 @@ public void ToDateTime_DefaultValue_Ok() var defaultValue = new DateTime(2020, 1, 1); // 解析成功返回解析值 - Assert.Equal(new DateTime(2026, 5, 1), ToDateTimeWithDefaultValue(null, "20260501", defaultValue)); + Assert.Equal(new DateTime(2026, 5, 1), DateTimeHelper.ToDateTime("20260501", defaultValue)); // 解析失败返回默认值 - Assert.Equal(defaultValue, ToDateTimeWithDefaultValue(null, "test", defaultValue)); + Assert.Equal(defaultValue, DateTimeHelper.ToDateTime("test", defaultValue)); } [Theory] @@ -56,7 +48,7 @@ public void ToDateTime_DefaultValue_Ok() public void TryToDateTime_NullOrWhiteSpace(string? value) { // 空字符串直接返回 false 且结果为 DateTime.MinValue - Assert.False(TryToDateTime(null, value!, out var result)); + Assert.False(DateTimeHelper.TryToDateTime(value!, out var result)); Assert.Equal(DateTime.MinValue, result); } @@ -64,10 +56,10 @@ public void TryToDateTime_NullOrWhiteSpace(string? value) public void TryToDateTime_Invalid() { // 两种解析方式均失败 - Assert.False(TryToDateTime(null, "test", out var result)); + Assert.False(DateTimeHelper.TryToDateTime("test", out var result)); Assert.Equal(DateTime.MinValue, result); - Assert.False(TryToDateTime(null, "20261301", out _)); + Assert.False(DateTimeHelper.TryToDateTime("20261301", out _)); } [Theory] @@ -83,7 +75,7 @@ public void TryToDateTime_Invalid() public void TryToDateTime_CompactFormats(string value, int year, int month, int day, int hour, int minute, int second, int millisecond) { // 紧凑格式由 TryParseExact 分支解析 - Assert.True(TryToDateTime(null, value, out var result)); + Assert.True(DateTimeHelper.TryToDateTime(value, out var result)); Assert.Equal(new DateTime(year, month, day, hour, minute, second, millisecond), result); } @@ -93,7 +85,7 @@ public void TryToDateTime_CompactFormats(string value, int year, int month, int public void TryToDateTime_Fallback(string value) { // 标准格式由 TryParse 回退分支解析 - Assert.True(TryToDateTime(null, value, out var result)); + Assert.True(DateTimeHelper.TryToDateTime(value, out var result)); Assert.Equal(new DateTime(2026, 5, 1), result); } @@ -101,7 +93,7 @@ public void TryToDateTime_Fallback(string value) public void TryToDateTime_Trim() { // 前后空白被裁剪后可正常解析 - Assert.True(TryToDateTime(null, " 20260501 ", out var result)); + Assert.True(DateTimeHelper.TryToDateTime(" 20260501 ", out var result)); Assert.Equal(new DateTime(2026, 5, 1), result); } @@ -112,7 +104,7 @@ public void ToDateTimeOffset_Unspecified() var value = new DateTime(2026, 5, 1, 13, 4, 5); var expected = TimeZoneInfo.Local.GetUtcOffset(value); - var actual = ToDateTimeOffset(null, "20260501 130405"); + var actual = DateTimeHelper.ToDateTimeOffset("20260501 130405"); Assert.NotNull(actual); Assert.Equal(expected, actual.Value.Offset); Assert.Equal(value, actual.Value.DateTime); @@ -122,7 +114,7 @@ public void ToDateTimeOffset_Unspecified() public void ToDateTimeOffset_Local() { // 带时区信息的字符串解析出的 Kind 不是 Unspecified,直接构造 - var actual = ToDateTimeOffset(null, "2026-05-01T13:04:05Z"); + var actual = DateTimeHelper.ToDateTimeOffset("2026-05-01T13:04:05Z"); Assert.NotNull(actual); Assert.Equal(new DateTime(2026, 5, 1, 13, 4, 5, DateTimeKind.Utc), actual.Value.UtcDateTime); } @@ -131,18 +123,6 @@ public void ToDateTimeOffset_Local() public void ToDateTimeOffset_Null() { // 解析失败返回 null - Assert.Null(ToDateTimeOffset(null, "test")); + Assert.Null(DateTimeHelper.ToDateTimeOffset("test")); } - - [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ToDateTime")] - static extern DateTime? ToDateTime([UnsafeAccessorType(HelperTypeName)] object? @this, string value); - - [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ToDateTime")] - static extern DateTime ToDateTimeWithDefaultValue([UnsafeAccessorType(HelperTypeName)] object? @this, string value, DateTime defaultValue); - - [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "TryToDateTime")] - static extern bool TryToDateTime([UnsafeAccessorType(HelperTypeName)] object? @this, string value, out DateTime result); - - [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ToDateTimeOffset")] - static extern DateTimeOffset? ToDateTimeOffset([UnsafeAccessorType(HelperTypeName)] object? @this, string value); }