feat(i18n): 国际化支持 (#2698)

* wip(i18n): laydate 国际化

* wip(i18n): colorpicker 国际化

* wip(i18n): laypage 国际化

* fix

* update code

* wip(i18n): 修改国际化消息对象结构

* wip(i18n): update

* wip(i18n): code 国际化

* wip(i18n): dropdown 国际化

* wip(i18n): flow 国际化

* wip(i18n): form 国际化

* wip(i18n): layer 国际化

* wip(i18n): table 国际化

* wip(i18n): transfer 国际化

* wip(i18n): tree 国际化

* wip(i18n): treeTable 控制台提示统一为英文

* wip(i18n): upload 国际化

* wip(i18n): util 国际化

* wip(i18n): update code

* wip(i18n): update code

* wip(i18n): fix

* wip(i18n): 优化 $t 代码细节

* wip(i18n): 修复 laydate lang

* wip(i18n): 改进 upload i18n key

* wip(i18n): 修复打包后 laydate 和 layer 异常

* wip(i18n): 移除国际化消息中的 `lay` 命名空间

* refactor(i18n): 改进国际化支持

* wip(i18n): 修复 table text.none 切换 locale 无效问题

* style(laydate): 优化逗号格式

* chore(laydate): 优化部分提示

* chore(i18n): 优化演示中部分国际化消息

* refactor: 剔除 laydate 单独版的判断逻辑

为接下来全面支持国际化做铺垫

* wip(i18n): 为 laydate 重新添加完整国际化的支持

* i18n(laydate): 优化 lang() 方法中的逻辑

* chore(util): 删除未使用的代码

* chore(i18n): 优化注释

* docs(i18n): 新增国际化文档(beta)

note: 由于时间关系,本次提交仅为初版,该文档尚未完成

* wip(docs): 完善 i18n 文档

* fix(i18n): 修复 laypage 变量定义前使用

* wip(i18n): 转义翻译结果

* fix(i18n): 修复 table 排序 key 无效

* wip(i18n): 优化获取对象中指定路径值的性能

* wip(i18n): 删除 $t 可变长参数重载

* chore(i18n): 删除不必要的注释

* refactor(i18n): laydate 国际化方案迁移至 i18n.$t (#2745)

* wip(i18n): 改进 laydate i18nMsg key

* update code

* wip(i18n): 改进 laydate 面板中的日期格式处理

* wip(i18n): 改进 util.toDateString meridiem

遵循 CLDR day periods 标准

* update code

* wip(docs): 优化 i18n 文档示例

* docs(i18n): 优化正式文档

* docs(i18n): 优化部分文案

* docs(i18n): 优化示例

---------

Co-authored-by: 贤心 <3277200+sentsim@users.noreply.github.com>
This commit is contained in:
morning-star
2025-09-08 10:31:02 +08:00
committed by GitHub
parent a0a0ba4a1c
commit d96ad79960
26 changed files with 3040 additions and 397 deletions

786
docs/i18n/detail/demo.md Normal file
View File

@@ -0,0 +1,786 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>i18n 演示 - Layui</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="{{= d.layui[2].cdn.css }}" rel="stylesheet">
</head>
<body class="layui-padding-3">
<div id="root"></div>{{!
<template id="template">
{{ const i18n = layui.i18n; }}
<div class="layui-form">
<div class="layui-inline">
<strong>{{= i18n.$t('custom.switchLanguage') }}: </strong>
</div>
<div class="layui-inline">
<select id="change-locale" lay-filter="change-locale">
<option value="zh-CN">简体中文</option>
<option value="en">English</option>
<option value="zh-HK">繁體中文</option>
</select>
</div>
</div>
<br>
<fieldset class="layui-elem-field">
<legend>README</legend>
<div class="layui-field-box layui-text" id="tpl-test">
<p>{{= i18n.$t('custom.readme.description') }}</p>
<ul>
<li><strong>locale</strong>: <span style="color:red">{{= i18n.config.locale }}</span></li>
<li><strong>Date</strong>: {{= new Date().toLocaleDateString(i18n.config.locale) }}</li>
<li><strong>Hello</strong>: {{= i18n.$t('custom.readme.hello') }}</li>
</ul>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>code</legend>
<div class="layui-field-box">
<pre id="demo-code" class="layui-code" lay-options="{}">
code content
</pre>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>colorpicker</legend>
<div class="layui-field-box">
<div id="demo-colorpicker"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>dropdown</legend>
<div class="layui-field-box">
<button id="demo-dropdown" class="layui-btn demo-dropdown-base">
<span>Dropdown</span>
<i class="layui-icon layui-icon-down layui-font-12"></i>
</button>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>flow</legend>
<div class="layui-field-box">
<div class="flow-demo" id="demo-flow"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>form</legend>
<div class="layui-field-box">
<form class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label">{{= i18n.$t('custom.form.required') }}</label>
<div class="layui-input-block">
<input type="text" name="username" lay-verify="required" lay-vertype="alert" placeholder="{{= i18n.$t('custom.form.placeholder') }}" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{{= i18n.$t('custom.form.phone') }}</label>
<div class="layui-input-inline layui-input-wrap">
<input type="tel" name="phone" lay-verify="phone" autocomplete="off" value="123456" lay-affix="clear"
class="layui-input demo-phone">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{{= i18n.$t('custom.form.email') }}</label>
<div class="layui-input-inline">
<input type="text" name="email" value="123.com" lay-verify="email" autocomplete="off"
class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">{{= i18n.$t('custom.form.date') }}</label>
<div class="layui-input-inline layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-date"></i>
</div>
<input type="text" name="date" value="2077" id="date" lay-verify="date" placeholder="yyyy-MM-dd"
autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{{= i18n.$t('custom.form.select') }}</label>
<div class="layui-input-block">
<select name="interest" lay-filter="aihao" lay-search>
<option value=""></option>
<option value="0">AAA</option>
<option value="1" selected>BBB</option>
<option value="2">CCC</option>
<option value="3">DDD</option>
<option value="4">EEE</option>
</select>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="demo1">
{{= i18n.$t('custom.form.submit') }}
</button>
<button type="reset" class="layui-btn layui-btn-primary">
{{= i18n.$t('custom.form.reset') }}
</button>
</div>
</div>
</form>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>laydate</legend>
<div class="layui-field-box">
<div class="layui-inline">
<input class="layui-input" id="demo-laydate" />
</div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>layer</legend>
<div class="layui-field-box">
<button type="button" class="layui-btn layui-btn-primary" lay-on="alert">Alert</button>
<button type="button" class="layui-btn layui-btn-primary" lay-on="prompt">Prompt</button>
<button type="button" class="layui-btn layui-btn-primary" lay-on="photos">Photos</button>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>laypage</legend>
<div class="layui-field-box">
<div id="demo-laypage-all"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>table</legend>
<div class="layui-field-box">
<table class="layui-hide" id="demo-table" lay-filter="test"></table>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>transfer</legend>
<div class="layui-field-box">
<div id="demo-transfer"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>tree</legend>
<div class="layui-field-box">
<div id="demo-tree"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>upload</legend>
<div class="layui-field-box">
<button type="button" class="layui-btn" id="demo-upload">
<i class="layui-icon layui-icon-upload"></i> Upload
</button>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>utils</legend>
<div class="layui-field-box">
<label>
timeAgo: <input id="demo-time-ago-picker" type="datetime-local" /> <span id="demo-time-ago-display"></span>
</label>
<br>
<label>
toDateString: <div id="demo-toDateString"></div>
</label>
</div>
</fieldset>
</template>!}}
<script>
// 配置 Layui 组件语言包
window.LAYUI_GLOBAL = {
i18n: {
locale: localStorage.getItem('layui-i18n-local-test') || 'zh-CN', // 当前语言环境
messages: { // 扩展其他语言包
// English
'en': {
code: {
copy: 'Copy Code',
copied: 'Copied',
copyError: 'Copy Failed',
maximize: 'Maximize',
restore: 'Restore',
preview: 'Open Preview in New Window'
},
colorpicker: {
clear: 'Clear',
confirm: 'OK'
},
dropdown: {
noData: 'No Data'
},
flow: {
loadMore: 'Load More',
noMore: 'No More Data'
},
form: {
select: {
noData: 'No Data',
noMatch: 'No Matching Data',
placeholder: 'Please Select'
},
validateMessages: {
required: 'This field is required',
phone: 'Invalid phone number format',
email: 'Invalid email format',
url: 'Invalid URL format',
number: 'Numbers only',
date: 'Invalid date format',
identity: 'Invalid ID number format'
},
verifyErrorPromptTitle: 'Notice'
},
laydate: {
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
weeks: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
time: ['Hour', 'Minute', 'Second'],
literal: {
year: ''
},
selectDate: 'Select Date',
selectTime: 'Select Time',
startTime: 'Start Time',
endTime: 'End Time',
tools: {
confirm: 'Confirm',
clear: 'Clear',
now: 'Now',
reset: 'Reset'
},
rangeOrderPrompt: 'End time cannot be less than start Time\nPlease re-select',
invalidDatePrompt: 'Invalid date\n',
formatErrorPrompt: 'Date format is invalid\nMust follow the format:\n{format}\n',
autoResetPrompt: 'It has been reset',
preview: 'The selected result'
},
layer: {
confirm: 'OK',
cancel: 'Cancel',
defaultTitle: 'Info',
prompt: {
InputLengthPrompt: 'Maximum {length} characters'
},
photos: {
noData: 'No Image',
tools: {
rotate: 'Rotate',
scaleX: 'Flip Horizontally',
zoomIn: 'Zoom In',
zoomOut: 'Zoom Out',
reset: 'Reset',
close: 'Close'
},
viewPicture: 'View Picture',
urlError: {
prompt: 'Image URL is invalid, \nContinue to next one?',
confirm: 'Next',
cancel: 'Cancel'
}
}
},
laypage: {
prev: 'Prev',
next: 'Next',
first: 'First',
last: 'Last',
total: 'Total {total} items',
pagesize: '/page',
goto: 'Go to',
page: 'page',
confirm: 'Confirm'
},
table: {
sort: {
asc: 'Ascending',
desc: 'Descending'
},
noData: 'No Data',
tools: {
filter: {
title: 'Filter Columns'
},
export: {
title: 'Export',
noDataPrompt: 'No data in the table',
compatPrompt: 'Export is not supported in IE. Please use Chrome or another modern browser.',
csvText: 'Export CSV File'
},
print: {
title: 'Print',
noDataPrompt: 'No data in the table'
}
},
dataFormatError: 'Returned data is invalid. The correct success status code should be: "{statusName}": {statusCode}',
xhrError: 'Request Error: {msg}'
},
transfer: {
noData: 'No Data',
noMatch: 'No Match',
title: ['List 1', 'List 2'],
searchPlaceholder: 'Search by Keyword'
},
tree: {
defaultNodeName: 'Unnamed',
noData: 'No Data',
deleteNodePrompt: 'Are you sure you want to delete the node "{name}"?'
},
upload: {
fileType: {
file: 'File',
image: 'Image',
video: 'Video',
audio: 'Audio'
},
validateMessages: {
fileExtensionError: 'Unsupported format in selected {fileType}',
filesOverLengthLimit: 'Maximum {length} files allowed at once',
currentFilesLength: 'You have selected {length} files',
fileOverSizeLimit: 'File size must not exceed {size}'
},
chooseText: '{length} files'
},
util: {
timeAgo: {
days: '{days} days ago',
hours: '{hours} hours ago',
minutes: '{minutes} minutes ago',
future: 'In the future',
justNow: 'Just now'
},
toDateString: {
meridiem: function (hours, minutes) {
return hours < 12 ? 'AM' : 'PM';
}
}
}
},
// 繁體中文
'zh-HK': {
code: {
copy: '複製代碼',
copied: '已複製',
copyError: '複製失敗',
maximize: '最大化顯示',
restore: '還原顯示',
preview: '在新視窗預覽'
},
colorpicker: {
clear: '清除',
confirm: '確定'
},
dropdown: {
noData: '暫無資料'
},
flow: {
loadMore: '載入更多',
noMore: '沒有更多了'
},
form: {
select: {
noData: '暫無資料',
noMatch: '無匹配資料',
placeholder: '請選擇'
},
validateMessages: {
required: '必填項不能為空',
phone: '手機號碼格式不正確',
email: '電郵格式不正確',
url: '連結格式不正確',
number: '只能填寫數字',
date: '日期格式不正確',
identity: '身份證號碼格式不正確'
},
verifyErrorPromptTitle: '提示'
},
laydate: {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
weeks: ['日', '一', '二', '三', '四', '五', '六'],
time: ['時', '分', '秒'],
literal: {
year: '年'
},
selectDate: '選擇日期',
selectTime: '選擇時間',
startTime: '開始時間',
endTime: '結束時間',
tools: {
confirm: '確定',
clear: '清除',
now: '現在',
reset: '重設'
},
rangeOrderPrompt: '結束時間不能早於開始時間\n請重新選擇',
invalidDatePrompt: '不在有效日期或時間範圍內\n',
formatErrorPrompt: '日期格式不合法\n必須遵循\n{format}\n',
autoResetPrompt: '已自動重設',
preview: '當前選中的結果'
},
layer: {
confirm: '確定',
cancel: '取消',
defaultTitle: '資訊',
prompt: {
InputLengthPrompt: '最多輸入 {length} 個字符'
},
photos: {
noData: '沒有圖片',
tools:{
rotate: '旋轉',
scaleX: '水平變換',
zoomIn: '放大',
zoomOut: '縮小',
reset: '還原',
close: '關閉'
},
viewPicture: '查看原圖',
urlError: {
prompt: '當前圖片地址異常,\n是否繼續查看下一張',
confirm: '下一張',
cancel: '不看了'
}
}
},
laypage: {
prev: '上一頁',
next: '下一頁',
first: '首頁',
last: '尾頁',
total: '共 {total} 條',
pagesize: '條/頁',
goto: '到第',
page: '頁',
confirm: '確定'
},
table: {
sort: {
asc: '升序',
desc: '降序'
},
noData: '無資料',
tools:{
filter: {
title: '篩選列'
},
export: {
title: '匯出',
noDataPrompt: '當前表格無資料',
compatPrompt: '匯出功能不支援 IE請用 Chrome 等高級瀏覽器匯出',
csvText : '匯出 CSV 檔案'
},
print: {
title: '列印',
noDataPrompt: '當前表格無資料'
}
},
dataFormatError: '返回的資料不符合規範,正確的成功狀態碼應為:"{statusName}": {statusCode}',
xhrError: '請求異常,錯誤提示:{msg}'
},
transfer: {
noData: '無資料',
noMatch: '無匹配資料',
title: ['列表一', '列表二'],
searchPlaceholder: '關鍵詞搜尋'
},
tree: {
defaultNodeName: '未命名',
noData: '無資料',
deleteNodePrompt: '確認刪除"{name}"節點嗎?'
},
upload: {
fileType: {
file: '檔案',
image: '圖片',
video: '影片',
audio: '音訊'
},
validateMessages: {
fileExtensionError: '選擇的{fileType}中包含不支援的格式',
filesOverLengthLimit: '同時最多只能上傳: {length} 個檔案',
currentFilesLength: '您當前已經選擇了: {length} 個檔案',
fileOverSizeLimit: '檔案大小不能超過 {size}'
},
chooseText: '{length} 個檔案'
},
util: {
timeAgo: {
days: '{days} 天前',
hours: '{hours} 小時前',
minutes: '{minutes} 分鐘前',
future: '未來',
justNow: '剛剛'
},
toDateString: {
meridiem: function(hours, minutes){
var hm = hours * 100 + minutes;
if (hm < 500) {
return '凌晨';
} else if (hm < 800) {
return '早上';
} else if (hm < 1200) {
return '上午';
} else if (hm < 1300) {
return '中午';
} else if (hm < 1900) {
return '下午';
}
return '晚上';
}
}
}
},
}
}
}
</script>
<script src="{{= d.layui[2].cdn.js }}"></script>
<script>
layui.use(async function () {
const {
$, colorpicker, dropdown, flow, form,
i18n, laydate, laypage, laytpl, layer,
table, transfer, tree, upload, util
} = layui;
/**
* 业务 i18n 配置
* 说明此处仅为了让演示的「页面内容」与「Layui 组件」语言保持一致。实际使用时通常只需指定 Layui 组件语言环境,而页面内容的语言建议由您的项目本身进行管理。
*/
i18n.set({
messages: {
'zh-CN': {
custom: {
switchLanguage: '切换语言',
readme: {
description: 'layui.i18n.$t 是私有方法(未文档化),此处仅用于演示',
hello: '你好',
},
form: {
required: '验证必填项',
phone: '验证手机号',
email: '验证邮箱',
date: '验证日期',
select: '选择框',
submit: '立即提交',
reset: '重置',
placeholder: '请输入'
}
}
},
'en': {
custom: {
switchLanguage: 'Switch Language',
readme: {
description: 'layui.i18n.$t is a private method (undocumented), used here for demonstration only.',
hello: 'Hello',
},
form: {
required: 'Required',
phone: 'Telephone',
email: 'Email',
date: 'Date',
select: 'Select',
submit: 'Submit',
reset: 'Reset',
placeholder: 'Please enter'
}
}
},
'zh-HK': {
custom: {
switchLanguage: '切換語言',
readme: {
description: 'layui.i18n.$t 是私有方法(未文檔化),此處僅用於示範',
hello: '你好',
},
form: {
required: '驗證必填項',
phone: '驗證手機號',
email: '驗證郵箱',
date: '驗證日期',
select: '選擇框',
submit: '立即提交',
reset: '重置',
placeholder: '請輸入'
}
}
}
}
});
// 渲染页面模板
const template = $('#template').html();
const html = laytpl(template, { tagStyle: 'modern' }).render();
$('#root').html(html);
/**
* 组件示例
*/
// code
layui.code({
elem: "#demo-code",
preview: true,
tools: ['copy', 'full', 'window'],
header: true,
lang: 'html',
langMarker: true,
});
// colorpicker
colorpicker.render({
elem: "#demo-colorpicker",
});
// dropdown
dropdown.render({
elem: "#demo-dropdown",
});
// flow
flow.load({
elem: '#demo-flow',
scrollElem: '#demo-flow',
done: function (page, next) {
// 模拟数据插入
setTimeout(function () {
var lis = [];
for (var i = 0; i < 3; i++) {
lis.push('<li>' + ((page - 1) * 3 + i + 1) + '</li>')
}
next(lis.join(''), page < 2);
}, 200);
}
});
// form
form.on('submit(demo1)', function (data) {
var field = data.field;
// 显示填写结果,仅作演示用
layer.alert(JSON.stringify(field));
return false;
});
// laydate
laydate.render({
elem: "#demo-laydate",
type: "datetime",
calendar: true
});
// layer
util.on({
alert: function () {
layer.alert("Hello world");
},
prompt: function () {
layer.prompt({ formType: 1, maxlength: 3 }, function (value, index) {
layer.close(index);
});
},
photos: function () {
layer.photos({
photos: {
"title": "Photos Demo",
"start": 0,
"data": [
{
"alt": "layer",
"pid": 1,
"src": "https://unpkg.com/outeres@0.1.1/demo/layer.png",
},
{
"alt": "error",
"pid": 3,
"src": "error.png",
},
{
"alt": "universe",
"pid": 5,
"src": "https://unpkg.com/outeres@0.1.1/demo/outer-space.jpg",
}
]
}
});
}
})
// laypage
laypage.render({
elem: "demo-laypage-all",
count: 100,
layout: ["count", "prev", "page", "next", "limit", "refresh", "skip"],
});
// table
table.render({
elem: '#demo-table',
cols: [[{ field: 'test', title: '1', sort: true }, { field: 'test2', title: '2', sort: true }]],
data: new Array(0),
toolbar: 'default',
defaultToolbar: ['filter', 'exports', 'print'],
height: 'full',
page: true,
text: {
// none: 'none'
}
});
// transfer
transfer.render({
elem: '#demo-transfer',
data: [
{ "value": "1", "title": "Item 1" },
{ "value": "2", "title": "Item 2" },
{ "value": "3", "title": "Item 3" },
],
showSearch: true
});
// tree
tree.render({
elem: '#demo-tree',
data: [{ title: 'Item 1', id: 1, children: [{ title: 'Item 1-1', id: 2 }] }],
edit: ['add', 'update', 'del']
});
// upload
upload.render({
elem: '#demo-upload',
url: '', // 实际使用时改成您自己的上传接口即可。
multiple: true,
accept: 'file',
number: 1
});
// util
$('#demo-time-ago-picker').on('change', function(){
$('#demo-time-ago-display').html(
util.timeAgo(this.value)
);
})
$('#demo-toDateString').html(
util.toDateString('2023-01-01 11:35:25', 'yyyy-MM-dd HH:mm:ss A')
+ '<br>'
+ util.toDateString('2023-01-01 18:35:25', 'yyyy-MM-dd HH:mm:ss A')
)
// 演示:切换语言
$("#change-locale").val(i18n.config.locale);
form.render('select').on("select(change-locale)", function (elem) {
// 记录语言,并重载页面(推荐)
localStorage.setItem('layui-i18n-local-test', elem.value);
window.location.reload();
});
$("body").css("opacity", 1);
console.log(i18n.config)
});
</script>
</body>
</html>

183
docs/i18n/detail/options.md Normal file
View File

@@ -0,0 +1,183 @@
<pre class="layui-code" lay-options="{style: 'height: 525px;', layout: ['code'], tools: []}">
<textarea>
i18n.set({
locale: 'zh-CN', // 设置语言环境
messages: { // 语言包
'zh-CN': { // 简体中文语言包(内置)
code: {
copy: '复制代码',
copied: '已复制',
copyError: '复制失败',
maximize: '最大化显示',
restore: '还原显示',
preview: '在新窗口预览'
},
colorpicker: {
clear: '清除',
confirm: '确定'
},
dropdown: {
noData: '暂无数据'
},
flow: {
loadMore: '加载更多',
noMore: '没有更多了'
},
form: {
select: {
noData: '暂无数据',
noMatch: '无匹配数据',
placeholder: '请选择'
},
validateMessages: {
required: '必填项不能为空',
phone: '手机号格式不正确',
email: '邮箱格式不正确',
url: '链接格式不正确',
number: '只能填写数字',
date: '日期格式不正确',
identity: '身份证号格式不正确'
},
verifyErrorPromptTitle: '提示'
},
laydate: {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
weeks: ['日', '一', '二', '三', '四', '五', '六'],
time: ['时', '分', '秒'],
literal: {
year: '年'
},
selectDate: '选择日期',
selectTime: '选择时间',
startTime: '开始时间',
endTime: '结束时间',
tools: {
confirm: '确定',
clear: '清空',
now: '现在',
reset: '重置'
},
rangeOrderPrompt: '结束时间不能早于开始时间\n请重新选择',
invalidDatePrompt: '不在有效日期或时间范围内\n',
formatErrorPrompt: '日期格式不合法\n必须遵循\n{format}\n',
autoResetPrompt: '已自动重置',
preview: '当前选中的结果'
},
layer: {
confirm: '确定',
cancel: '取消',
defaultTitle: '信息',
prompt: {
InputLengthPrompt: '最多输入 {length} 个字符'
},
photos: {
noData: '没有图片',
tools:{
rotate: '旋转',
scaleX: '水平变换',
zoomIn: '放大',
zoomOut: '缩小',
reset: '还原',
close: '关闭'
},
viewPicture: '查看原图',
urlError: {
prompt: '当前图片地址异常,\n是否继续查看下一张',
confirm: '下一张',
cancel: '不看了'
}
}
},
laypage: {
prev: '上一页',
next: '下一页',
first: '首页',
last: '尾页',
total: '共 {total} 条',
pagesize: '条/页',
goto: '到第',
page: '页',
confirm: '确定'
},
table: {
sort: {
asc: '升序',
desc: '降序'
},
noData: '暂无数据',
tools:{
filter: {
title: '筛选列'
},
export: {
title: '导出',
noDataPrompt: '当前表格无数据',
compatPrompt: '导出功能不支持 IE请用 Chrome 等高级浏览器导出',
csvText : '导出 CSV 文件'
},
print: {
title: '打印',
noDataPrompt: '当前表格无数据'
}
},
dataFormatError: '返回的数据不符合规范,正确的成功状态码应为:"{statusName}": {statusCode}',
xhrError: '请求异常,错误提示:{msg}'
},
transfer: {
noData: '暂无数据',
noMatch: '无匹配数据',
title: ['列表一', '列表二'],
searchPlaceholder: '关键词搜索'
},
tree: {
defaultNodeName: '未命名',
noData: '暂无数据',
deleteNodePrompt: '确认删除"{name}"节点吗?'
},
upload: {
fileType: {
file: '文件',
image: '图片',
video: '视频',
audio: '音频'
},
validateMessages: {
fileExtensionError: '选择的{fileType}中包含不支持的格式',
filesOverLengthLimit: '同时最多只能上传: {length} 个文件',
currentFilesLength: '当前已经选择了: {length} 个文件',
fileOverSizeLimit: '文件大小不能超过 {size}'
},
chooseText: '{length} 个文件'
},
util: {
timeAgo: {
days: '{days} 天前',
hours: '{hours} 小时前',
minutes: '{minutes} 分钟前',
future: '未来',
justNow: '刚刚'
},
toDateString: {
// https://www.unicode.org/cldr/charts/47/supplemental/day_periods.html
meridiem: function(hours, minutes){
var hm = hours * 100 + minutes;
if (hm < 500) {
return '凌晨';
} else if (hm < 800) {
return '早上';
} else if (hm < 1200) {
return '上午';
} else if (hm < 1300) {
return '中午';
} else if (hm < 1900) {
return '下午';
}
return '晚上';
}
}
}
}
}
});
</textarea>
</pre>

114
docs/i18n/index.md Normal file
View File

@@ -0,0 +1,114 @@
---
title: 国际化 i18n
toc: true
---
# 国际化 <sup>2.12+</sup>
> `i18n` 是 2.12 版本新增的国际化模块,用于为 Layui 各组件实现多语言支持。
<h2 id="examples" lay-toc="{}" style="margin-bottom: 0;">完整演示</h2>
为了避免语言包配置冗长而影响示例源代码的查看,此处只演示「简体中文 / English / 繁體中文」语言环境,你可以点击该示例头部的「切换语言」选择框查看 Layui 组件在不同语言环境中的显示效果。
<div class="ws-docs-showcase"></div>
<pre class="layui-code" lay-options="{preview: 'iframe', text: {preview: 'Preview'}, style: 'height: 560px;', layout: ['preview', 'code'], tools: ['full','window']}">
<textarea>
{{- d.include("/i18n/detail/demo.md") }}
</textarea>
</pre>
<h2 id="api" lay-toc="{hot: true}">API</h2>
| API | 描述 |
| --- | --- |
| var i18n = layui.i18n | 获得 `i18n` 模块。|
| [i18n.set(options)](#set) | 设置语言环境及语言包。|
<h3 id="set" lay-toc="{level: 2}">配置方式</h3>
i18n 支持两种配置方式,你可以根据实际应用场景选择任一方式。
#### 1. 通过 `i18n.set()` 方法配置
`i18n.set(options)`
- 参数 `options` : 基础属性选项。[#详见语言包选项](#options)
```js
layui.use(function() {
var i18n = layui.i18n;
// 设置语言
i18n.set({
locale: 'zh-CN', // 当前语言环境。zh-CN 为内置简体中文语言包
messages: { // 扩展其他语言包
'en': {},
'zh-HK': {},
}
});
});
```
🔔 请注意:如果你的页面有用到 Layui 组件的自动渲染(如 table 模板配置渲染方式),因为执行顺序的问题,组件在自动渲染时可能无法读取到 `i18n.set()` 的配置信息,此时建议采用下述 `LAYUI_GLOBAL.i18n` 全局配置。
#### 2. 通过 `LAYUI_GLOBAL.i18n` 全局配置(推荐)
由于 i18n 配置与组件渲染存在执行顺序问题,为了确保 i18n 配置始终在组件渲染之前生效,更推荐采用该全局配置方式。
```html
<script>
// 全局配置应放在 layui.js 引入之前的位置
window.LAYUI_GLOBAL = {
i18n: { // 选项同 i18n.set(options)
locale: 'zh-CN', // 当前语言环境
messages: { // 扩展其他语言包
'en': {},
'zh-HK': {},
}
}
};
</script>
<script src="/layui/layui.js"></script>
```
<h3 id="options" lay-toc="{level: 2}">语言包选项</h3>
i18n 默认采用简体中文(`zh-CN`)语言环境,以下为各组件消息文本对应的选项:
<div>
{{- d.include("/i18n/detail/options.md") }}
</div>
基于上述选项,还可以扩展更多语言包,如:
```js
i18n.set({
locale: 'en', // 当前语言环境
messages: { // 扩展更多语言包
'en': { // 通用英语
code: {
copy: 'Copy Code',
copied: 'Copied',
// ……
},
// ……
},
'fr': {}, // 通用法语
'zh-HK': {}, // 繁体中文
// …… // 更多语言
}
});
```
为了节省时间,也可以借助「**第三方提供并维护**的 Layui 多语言 AI 翻译工具」直接生成不同语言的消息文本,如:
| 翻译工具 | 提供者 |
| --- | --- |
| <a href="https://gitee.com/mail_osc/translate/tree/master/extend/layui-i18n-object-translate" target="_blank">https://gitee.com/mail_osc/translate/tree/master/extend/layui-i18n-object-translate</a> | <a href="https://github.com/xnx3" target="_blank">@xnx3</a> |
## 💖 心语
i18n 模块是在众多开发者强烈的需求呼声中,由 Layui 核心 Contributor [@Sight-wcg](https://github.com/Sight-wcg) 完成,该模块通过简练的设计,为 Layui 组件实现了多语言的无缝接入并且兼容了一些原本自带简单多语言或消息配置的组件Layui 2.x 版本也因此具备国际化能力。

486
examples/i18n.html Normal file
View File

@@ -0,0 +1,486 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>i18n - Layui</title>
<link href="../src/css/layui.css" rel="stylesheet" />
<style>
body {
transition: opacity 0.8s ease-in-out;
opacity: 0;
}
</style>
</head>
<body class="layui-padding-3">
<div id="root"></div>
<template id="template">
{{ const i18n = layui.i18n; }}
<div class="layui-form">
<div class="layui-inline">
<strong>{{= i18n.$t('custom.switchLanguage') }}: </strong>
</div>
<div class="layui-inline">
<select id="change-locale" lay-filter="change-locale">
<option value="zh-CN">简体中文</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="zh-HK">繁體中文</option>
</select>
</div>
</div>
<br>
<fieldset class="layui-elem-field">
<legend>README</legend>
<div class="layui-field-box layui-text" id="tpl-test">
<p>{{= i18n.$t('custom.readme.description') }}</p>
<ul>
<li><strong>locale</strong>: <span style="color:red">{{= i18n.config.locale }}</span></li>
<li><strong>Date</strong>: {{= new Date().toLocaleDateString(i18n.config.locale) }}</li>
<li><strong>Hello</strong>: {{= i18n.$t('custom.readme.hello') }}</li>
</ul>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>code</legend>
<div class="layui-field-box">
<pre id="demo-code" class="layui-code" lay-options="{}">
<textarea>
code content
</textarea>
</pre>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>colorpicker</legend>
<div class="layui-field-box">
<div id="demo-colorpicker"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>dropdown</legend>
<div class="layui-field-box">
<button id="demo-dropdown" class="layui-btn demo-dropdown-base">
<span>Dropdown</span>
<i class="layui-icon layui-icon-down layui-font-12"></i>
</button>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>flow</legend>
<div class="layui-field-box">
<div class="flow-demo" id="demo-flow"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>form</legend>
<div class="layui-field-box">
<form class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label">{{= i18n.$t('custom.form.required') }}</label>
<div class="layui-input-block">
<input type="text" name="username" lay-verify="required" lay-vertype="alert" placeholder="{{= i18n.$t('custom.form.placeholder') }}" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{{= i18n.$t('custom.form.phone') }}</label>
<div class="layui-input-inline layui-input-wrap">
<input type="tel" name="phone" lay-verify="phone" autocomplete="off" value="123456" lay-affix="clear"
class="layui-input demo-phone">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{{= i18n.$t('custom.form.email') }}</label>
<div class="layui-input-inline">
<input type="text" name="email" value="123.com" lay-verify="email" autocomplete="off"
class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">{{= i18n.$t('custom.form.date') }}</label>
<div class="layui-input-inline layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-date"></i>
</div>
<input type="text" name="date" value="2077" id="date" lay-verify="date" placeholder="yyyy-MM-dd"
autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{{= i18n.$t('custom.form.select') }}</label>
<div class="layui-input-block">
<select name="interest" lay-filter="aihao" lay-search>
<option value=""></option>
<option value="0">AAA</option>
<option value="1" selected>BBB</option>
<option value="2">CCC</option>
<option value="3">DDD</option>
<option value="4">EEE</option>
</select>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="demo1">
{{= i18n.$t('custom.form.submit') }}
</button>
<button type="reset" class="layui-btn layui-btn-primary">
{{= i18n.$t('custom.form.reset') }}
</button>
</div>
</div>
</form>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>laydate</legend>
<div class="layui-field-box">
<div class="layui-inline">
<input class="layui-input" id="demo-laydate" />
</div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>layer</legend>
<div class="layui-field-box">
<button type="button" class="layui-btn layui-btn-primary" lay-on="alert">Alert</button>
<button type="button" class="layui-btn layui-btn-primary" lay-on="prompt">Prompt</button>
<button type="button" class="layui-btn layui-btn-primary" lay-on="photos">Photos</button>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>laypage</legend>
<div class="layui-field-box">
<div id="demo-laypage-all"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>table</legend>
<div class="layui-field-box">
<table class="layui-hide" id="demo-table" lay-filter="test"></table>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>transfer</legend>
<div class="layui-field-box">
<div id="demo-transfer"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>tree</legend>
<div class="layui-field-box">
<div id="demo-tree"></div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>upload</legend>
<div class="layui-field-box">
<button type="button" class="layui-btn" id="demo-upload">
<i class="layui-icon layui-icon-upload"></i> Upload
</button>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>utils</legend>
<div class="layui-field-box">
<label>
timeAgo: <input id="demo-time-ago-picker" type="datetime-local" /> <span id="demo-time-ago-display"></span>
</label>
<br>
<label>
toDateString: <div id="demo-toDateString"></div>
</label>
</div>
</fieldset>
</template>
<script src="../src/layui.js"></script>
<script>
layui.use(async function () {
const {
$, colorpicker, dropdown, flow, form,
i18n, laydate, laypage, laytpl, layer,
table, transfer, tree, upload, util
} = layui;
// 演示异步加载组件语言包
const { default: en } = await import('./i18n/en.js');
const { default: fr } = await import('./i18n/fr.js');
const { default: zhHK } = await import('./i18n/zh-HK.js');
// 组件 i18n 配置
i18n.set({
locale: layui.data('layui-i18n-test').locale || 'zh-CN', // 指定语言
messages: {
en, // 扩展英语
fr, // 扩展法语
'zh-HK': zhHK, // 扩展繁体中文
},
});
// 业务 i18n 配置
i18n.set({
messages: {
'zh-CN': {
custom: {
switchLanguage: '切换语言',
readme: {
description: 'layui.i18n.$t 是私有方法(未文档化),此处仅用于演示',
hello: '你好',
},
form: {
required: '验证必填项',
phone: '验证手机号',
email: '验证邮箱',
date: '验证日期',
select: '选择框',
submit: '立即提交',
reset: '重置',
placeholder: '请输入'
}
}
},
'en': {
custom: {
switchLanguage: 'Switch Language',
readme: {
description: 'layui.i18n.$t is a private method (undocumented), used here for demonstration only.',
hello: 'Hello',
},
form: {
required: 'Required',
phone: 'Telephone',
email: 'Email',
date: 'Date',
select: 'Select',
submit: 'Submit',
reset: 'Reset',
placeholder: 'Please enter'
}
}
},
'fr': {
custom: {
switchLanguage: 'Changer la langue',
readme: {
description: 'layui.i18n.$t est une méthode privée (non documentée), utilisée ici uniquement à des fins de démonstration.',
hello: 'Bonjour',
},
form: {
required: 'Requis',
phone: 'Téléphone',
email: 'Email',
date: 'Date',
select: 'Sélection',
submit: 'Soumettre maintenant',
reset: 'Réinitialiser',
placeholder: 'Veuillez entrer'
}
}
},
'zh-HK': {
custom: {
switchLanguage: '切換語言',
readme: {
description: 'layui.i18n.$t 是私有方法(未文檔化),此處僅用於示範',
hello: '你好',
},
form: {
required: '驗證必填項',
phone: '驗證手機號',
email: '驗證郵箱',
date: '驗證日期',
select: '選擇框',
submit: '立即提交',
reset: '重置',
placeholder: '請輸入'
}
}
}
}
});
// 渲染页面模板
const template = $('#template').html();
const html = laytpl(template, { tagStyle: 'modern' }).render();
$('#root').html(html);
/**
* 组件示例
*/
// code
layui.code({
elem: "#demo-code",
preview: true,
tools: ['copy', 'full', 'window'],
header: true,
lang: 'html',
langMarker: true,
});
// colorpicker
colorpicker.render({
elem: "#demo-colorpicker",
});
// dropdown
dropdown.render({
elem: "#demo-dropdown",
});
// flow
flow.load({
elem: '#demo-flow',
scrollElem: '#demo-flow',
done: function (page, next) {
// 模拟数据插入
setTimeout(function () {
var lis = [];
for (var i = 0; i < 3; i++) {
lis.push('<li>' + ((page - 1) * 3 + i + 1) + '</li>')
}
next(lis.join(''), page < 2);
}, 200);
}
});
// form
form.on('submit(demo1)', function (data) {
var field = data.field;
// 显示填写结果,仅作演示用
layer.alert(JSON.stringify(field));
return false;
});
//laydate
laydate.render({
elem: "#demo-laydate",
type: "datetime",
calendar: true
});
// layer
util.on({
alert: function () {
layer.alert("Hello world");
},
prompt: function () {
layer.prompt({ formType: 1, maxlength: 3 }, function (value, index) {
layer.close(index);
});
},
photos: function () {
layer.photos({
photos: {
"title": "Photos Demo",
"start": 0,
"data": [
{
"alt": "layer",
"pid": 1,
"src": "https://unpkg.com/outeres@0.1.1/demo/layer.png",
},
{
"alt": "error",
"pid": 3,
"src": "错误提示演示",
},
{
"alt": "universe",
"pid": 5,
"src": "https://unpkg.com/outeres@0.1.1/demo/outer-space.jpg",
}
]
}
});
}
})
// laypage
laypage.render({
elem: "demo-laypage-all",
count: 100,
layout: ["count", "prev", "page", "next", "limit", "refresh", "skip"],
});
// table
table.render({
elem: '#demo-table',
cols: [[{ field: 'test', title: '1', sort: true }, { field: 'test2', title: '2', sort: true }]],
data: new Array(0),
toolbar: 'default',
defaultToolbar: ['filter', 'exports', 'print'],
height: 'full',
page: true,
text: {
// none: 'none'
}
});
// transfer
transfer.render({
elem: '#demo-transfer',
data: [
{ "value": "1", "title": "Item 1" },
{ "value": "2", "title": "Item 2" },
{ "value": "3", "title": "Item 3" },
],
showSearch: true
});
// tree
tree.render({
elem: '#demo-tree',
//data: [],
data: [{ title: 'Item 1', id: 1, children: [{ title: 'Item 1-1', id: 2 }] }],
edit: ['add', 'update', 'del']
});
// upload
upload.render({
elem: '#demo-upload',
url: '', // 实际使用时改成您自己的上传接口即可。
multiple: true,
accept: 'file',
//exts: 'zip',
number: 1,
//size: 60,
});
// util
$('#demo-time-ago-picker').on('change', function(){
$('#demo-time-ago-display').html(
util.timeAgo(this.value)
);
})
$('#demo-toDateString').html(
util.toDateString('2023-01-01 11:35:25', 'yyyy-MM-dd HH:mm:ss A')
+ '<br>'
+ util.toDateString('2023-01-01 18:35:25', 'yyyy-MM-dd HH:mm:ss A')
)
// 演示:切换语言
$("#change-locale").val(i18n.config.locale);
form.render('select').on("select(change-locale)", function (elem) {
// 记录语言,并重载页面(推荐)
layui.data('layui-i18n-test', {
key: 'locale',
value: elem.value
});
window.location.reload();
});
$("body").css("opacity", 1);
layer.msg(layui.v);
console.log(i18n.config)
});
</script>
</body>
</html>

166
examples/i18n/en.js Normal file
View File

@@ -0,0 +1,166 @@
/**
* English (en)
*/
// Common English internationalization message object
export default {
code: {
copy: 'Copy Code',
copied: 'Copied',
copyError: 'Copy Failed',
maximize: 'Maximize',
restore: 'Restore',
preview: 'Preview in New Window'
},
colorpicker: {
clear: 'Clear',
confirm: 'OK'
},
dropdown: {
noData: 'No Data'
},
flow: {
loadMore: 'Load More',
noMore: 'No More Data'
},
form: {
select: {
noData: 'No Data',
noMatch: 'No Matching Data',
placeholder: 'Please Select'
},
validateMessages: {
required: 'This field is required',
phone: 'Invalid phone number format',
email: 'Invalid email format',
url: 'Invalid URL format',
number: 'Numbers only',
date: 'Invalid date format',
identity: 'Invalid ID number format'
},
verifyErrorPromptTitle: 'Notice'
},
laydate: {
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
weeks: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
time: ['Hour', 'Minute', 'Second'],
literal: {
year: ''
},
selectDate: 'Select Date',
selectTime: 'Select Time',
startTime: 'Start Time',
endTime: 'End Time',
tools: {
confirm: 'Confirm',
clear: 'Clear',
now: 'Now',
reset: 'Reset'
},
rangeOrderPrompt: 'End time cannot be less than start Time\nPlease re-select',
invalidDatePrompt: 'Invalid date\n',
formatErrorPrompt: 'Date format is invalid\nMust follow the format:\n{format}\n',
autoResetPrompt: 'It has been reset',
preview: 'The selected result'
},
layer: {
confirm: 'OK',
cancel: 'Cancel',
defaultTitle: 'Info',
prompt: {
InputLengthPrompt: 'Maximum {length} characters'
},
photos: {
noData: 'No Image',
tools: {
rotate: 'Rotate',
scaleX: 'Flip Horizontally',
zoomIn: 'Zoom In',
zoomOut: 'Zoom Out',
reset: 'Reset',
close: 'Close'
},
viewPicture: 'View Original',
urlError: {
prompt: 'Image URL is invalid, \nContinue to next one?',
confirm: 'Next',
cancel: 'Cancel'
}
}
},
laypage: {
prev: 'Prev',
next: 'Next',
first: 'First',
last: 'Last',
total: 'Total {total} items',
pagesize: 'items/page',
goto: 'Go to',
page: 'page',
confirm: 'Confirm'
},
table: {
sort: {
asc: 'Ascending',
desc: 'Descending'
},
noData: 'No Data',
tools: {
filter: {
title: 'Filter Columns'
},
export: {
title: 'Export',
noDataPrompt: 'No data in the table',
compatPrompt: 'Export is not supported in IE. Please use Chrome or another modern browser.',
csvText: 'Export CSV File'
},
print: {
title: 'Print',
noDataPrompt: 'No data in the table'
}
},
dataFormatError: 'Returned data is invalid. The correct success status code should be: "{statusName}": {statusCode}',
xhrError: 'Request Error: {msg}'
},
transfer: {
noData: 'No Data',
noMatch: 'No Match',
title: ['List One', 'List Two'],
searchPlaceholder: 'Search by Keyword'
},
tree: {
defaultNodeName: 'Unnamed',
noData: 'No Data',
deleteNodePrompt: 'Are you sure you want to delete the node "{name}"?'
},
upload: {
fileType: {
file: 'File',
image: 'Image',
video: 'Video',
audio: 'Audio'
},
validateMessages: {
fileExtensionError: 'Unsupported format in selected {fileType}',
filesOverLengthLimit: 'Maximum {length} files allowed at once',
currentFilesLength: 'You have selected {length} files',
fileOverSizeLimit: 'File size must not exceed {size}'
},
chooseText: '{length} files'
},
util: {
timeAgo: {
days: '{days} days ago',
hours: '{hours} hours ago',
minutes: '{minutes} minutes ago',
future: 'In the future',
justNow: 'Just now'
},
toDateString: {
meridiem: function (hours, minutes) {
return hours < 12 ? 'AM' : 'PM';
}
}
}
};

166
examples/i18n/fr.js Normal file
View File

@@ -0,0 +1,166 @@
/**
* Français (fr)
*/
export default {
code: {
copy: 'Copier le code',
copied: 'Copié',
copyError: 'Échec de la copie',
maximize: 'Afficher en plein écran',
restore: 'Restaurer laffichage',
preview: 'Aperçu dans une nouvelle fenêtre'
},
colorpicker: {
clear: 'Effacer',
confirm: 'OK'
},
dropdown: {
noData: 'Aucune donnée disponible'
},
flow: {
loadMore: 'Charger plus',
noMore: 'Plus de données'
},
form: {
select: {
noData: 'Aucune donnée disponible',
noMatch: 'Aucune correspondance',
placeholder: 'Veuillez sélectionner'
},
validateMessages: {
required: 'Ce champ est obligatoire',
phone: 'Numéro de téléphone invalide',
email: 'Adresse e-mail invalide',
url: 'URL invalide',
number: 'Uniquement des chiffres',
date: 'Format de date invalide',
identity: 'Numéro didentification invalide'
},
verifyErrorPromptTitle: 'Avertissement'
},
laydate: {
months: ['Janv', 'Févr', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc'],
weeks: ['Di', 'Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa'],
time: ['Heure', 'Minute', 'Seconde'],
literal: {
year: ''
},
selectDate: 'Sélec. date',
selectTime: 'Sélec. heure',
startTime: 'Heure de début',
endTime: 'Heure de fin',
// Recommandé en version abrégée en raison de lespace limité du composant
tools: {
confirm: 'OK',
clear: 'Eff.',
now: 'Ajd.',
reset: 'Réinit.'
},
rangeOrderPrompt: 'Lheure de fin ne peut pas être antérieure à lheure de début\nVeuillez recommencer',
invalidDatePrompt: 'Date ou heure hors plage valide\n',
formatErrorPrompt: 'Format de date invalide\nLe format attendu est :\n{format}\n',
autoResetPrompt: 'Il a été réinitialisé pour vous',
preview: 'Résultat sélectionné actuel'
},
layer: {
confirm: 'Confirmer',
cancel: 'Annuler',
defaultTitle: 'Information',
prompt: {
InputLengthPrompt: 'Maximum {length} caractères'
},
photos: {
noData: 'Aucune image',
tools: {
rotate: 'Faire pivoter',
scaleX: 'Inverser horizontalement',
zoomIn: 'Agrandir',
zoomOut: 'Réduire',
reset: 'Réinitialiser',
close: 'Fermer'
},
viewPicture: 'Voir limage originale',
urlError: {
prompt: 'Ladresse de limage est invalide,\nContinuer avec la suivante ?',
confirm: 'Suivante',
cancel: 'Annuler'
}
}
},
laypage: {
prev: 'Page précédente',
next: 'Page suivante',
first: 'Première',
last: 'Dernière',
total: 'Total {total} éléments',
pagesize: 'éléments/page',
goto: 'Aller à',
page: 'page',
confirm: 'Confirmer'
},
table: {
sort: {
asc: 'Croissant',
desc: 'Décroissant'
},
noData: 'Aucune donnée',
tools: {
filter: {
title: 'Filtrer les colonnes'
},
export: {
title: 'Exporter',
noDataPrompt: 'Aucune donnée à exporter',
compatPrompt: 'Lexportation nest pas supportée par IE, veuillez utiliser Chrome ou un autre navigateur moderne',
csvText: 'Exporter au format CSV'
},
print: {
title: 'Imprimer',
noDataPrompt: 'Aucune donnée à imprimer'
}
},
dataFormatError: 'Les données retournées sont invalides. Le code de réussite attendu est : "{statusName}": {statusCode}',
xhrError: 'Erreur de requête : {msg}'
},
transfer: {
noData: 'Aucune donnée',
noMatch: 'Aucune correspondance',
title: ['Liste 1', 'Liste 2'],
searchPlaceholder: 'Recherche par mot-clé'
},
tree: {
defaultNodeName: 'Sans nom',
noData: 'Aucune donnée',
deleteNodePrompt: 'Confirmer la suppression du nœud "{name}" ?'
},
upload: {
fileType: {
file: 'Fichier',
image: 'Image',
video: 'Vidéo',
audio: 'Audio'
},
validateMessages: {
fileExtensionError: 'Le {fileType} sélectionné contient un format non supporté',
filesOverLengthLimit: 'Nombre maximum de fichiers : {length}',
currentFilesLength: 'Vous avez sélectionné {length} fichiers',
fileOverSizeLimit: 'La taille du fichier ne doit pas dépasser {size}'
},
chooseText: '{length} fichiers'
},
util: {
timeAgo: {
days: 'il y a {days} jours',
hours: 'il y a {hours} heures',
minutes: 'il y a {minutes} minutes',
future: 'Futur',
justNow: 'À linstant'
},
toDateString: {
meridiem: function(hours, minutes){
return hours < 12 ? 'AM' : 'PM';
}
}
}
};

177
examples/i18n/zh-HK.js Normal file
View File

@@ -0,0 +1,177 @@
/**
* 繁體中文 (zh-HK)
*/
export default {
code: {
copy: '複製代碼',
copied: '已複製',
copyError: '複製失敗',
maximize: '最大化顯示',
restore: '還原顯示',
preview: '在新視窗預覽'
},
colorpicker: {
clear: '清除',
confirm: '確定'
},
dropdown: {
noData: '暫無資料'
},
flow: {
loadMore: '載入更多',
noMore: '沒有更多了'
},
form: {
select: {
noData: '暫無資料',
noMatch: '無匹配資料',
placeholder: '請選擇'
},
validateMessages: {
required: '必填項不能為空',
phone: '手機號碼格式不正確',
email: '電郵格式不正確',
url: '連結格式不正確',
number: '只能填寫數字',
date: '日期格式不正確',
identity: '身份證號碼格式不正確'
},
verifyErrorPromptTitle: '提示'
},
laydate: {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
weeks: ['日', '一', '二', '三', '四', '五', '六'],
time: ['時', '分', '秒'],
literal: {
year: '年'
},
selectDate: '選擇日期',
selectTime: '選擇時間',
startTime: '開始時間',
endTime: '結束時間',
tools: {
confirm: '確定',
clear: '清除',
now: '現在',
reset: '重設'
},
rangeOrderPrompt: '結束時間不能早於開始時間\n請重新選擇',
invalidDatePrompt: '不在有效日期或時間範圍內\n',
formatErrorPrompt: '日期格式不合法\n必須遵循\n{format}\n',
autoResetPrompt: '已自動重設',
preview: '當前選中的結果'
},
layer: {
confirm: '確定',
cancel: '取消',
defaultTitle: '資訊',
prompt: {
InputLengthPrompt: '最多輸入 {length} 個字符'
},
photos: {
noData: '沒有圖片',
tools:{
rotate: '旋轉',
scaleX: '水平變換',
zoomIn: '放大',
zoomOut: '縮小',
reset: '還原',
close: '關閉'
},
viewPicture: '查看原圖',
urlError: {
prompt: '當前圖片地址異常,\n是否繼續查看下一張',
confirm: '下一張',
cancel: '不看了'
}
}
},
laypage: {
prev: '上一頁',
next: '下一頁',
first: '首頁',
last: '尾頁',
total: '共 {total} 條',
pagesize: '條/頁',
goto: '到第',
page: '頁',
confirm: '確定'
},
table: {
sort: {
asc: '升序',
desc: '降序'
},
noData: '無資料',
tools:{
filter: {
title: '篩選列'
},
export: {
title: '匯出',
noDataPrompt: '當前表格無資料',
compatPrompt: '匯出功能不支援 IE請用 Chrome 等高級瀏覽器匯出',
csvText : '匯出 CSV 檔案'
},
print: {
title: '列印',
noDataPrompt: '當前表格無資料'
}
},
dataFormatError: '返回的資料不符合規範,正確的成功狀態碼應為:"{statusName}": {statusCode}',
xhrError: '請求異常,錯誤提示:{msg}'
},
transfer: {
noData: '無資料',
noMatch: '無匹配資料',
title: ['列表一', '列表二'],
searchPlaceholder: '關鍵詞搜尋'
},
tree: {
defaultNodeName: '未命名',
noData: '無資料',
deleteNodePrompt: '確認刪除"{name}"節點嗎?'
},
upload: {
fileType: {
file: '檔案',
image: '圖片',
video: '影片',
audio: '音訊'
},
validateMessages: {
fileExtensionError: '選擇的{fileType}中包含不支援的格式',
filesOverLengthLimit: '同時最多只能上傳: {length} 個檔案',
currentFilesLength: '您當前已經選擇了: {length} 個檔案',
fileOverSizeLimit: '檔案大小不能超過 {size}'
},
chooseText: '{length} 個檔案'
},
util: {
timeAgo: {
days: '{days} 天前',
hours: '{hours} 小時前',
minutes: '{minutes} 分鐘前',
future: '未來',
justNow: '剛剛'
},
toDateString: {
meridiem: function(hours, minutes){
var hm = hours * 100 + minutes;
if (hm < 500) {
return '凌晨';
} else if (hm < 800) {
return '早上';
} else if (hm < 1200) {
return '上午';
} else if (hm < 1300) {
return '中午';
} else if (hm < 1900) {
return '下午';
}
return '晚上';
}
}
}
};

View File

@@ -20,7 +20,7 @@ const config = {
{pkg: pkg, js: ';'}
],
// 全部模块
modules: 'lay,laytpl,laypage,laydate,jquery,component,layer,util,dropdown,slider,colorpicker,element,upload,form,table,treeTable,tabs,tree,transfer,carousel,rate,flow,code'
modules: 'lay,i18n,laytpl,laypage,laydate,jquery,component,layer,util,dropdown,slider,colorpicker,element,upload,form,table,treeTable,tabs,tree,transfer,carousel,rate,flow,code'
};
// 获取参数

View File

@@ -46,6 +46,7 @@ html #layuicss-laydate{display: none; position: absolute; width: 1989px;}
.layui-laydate-header i.laydate-prev-m{left: 45px;}
.layui-laydate-header i.laydate-next-y{right: 15px;}
.layui-laydate-header i.laydate-next-m{right: 45px;}
.laydate-time-show .layui-laydate-header{padding-left: 10px; padding-right: 10px;}
.laydate-set-ym{width: 100%; text-align: center; box-sizing: border-box; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;}
.laydate-set-ym span{padding: 0 10px; cursor: pointer;}
.laydate-time-text{cursor: default !important;}
@@ -95,7 +96,7 @@ html #layuicss-laydate{display: none; position: absolute; width: 1989px;}
.layui-laydate .laydate-time-list-hide-2 ol li{padding-left: 117px;}
/* 提示 */
.layui-laydate-hint{position: absolute; top: 115px; left: 50%; width: 250px; margin-left: -125px; line-height: 20px; padding: 15px; text-align: center; font-size: 12px; color: #FF5722;}
.layui-laydate-hint{position: absolute; top: 115px; left: 50%; width: 250px; margin-left: -125px; line-height: 20px; padding: 15px; text-align: center; font-size: 12px; color: #FF5722;white-space: pre-line;}
/* 双日历 */

View File

@@ -55,8 +55,25 @@
// 异常提示
var error = function(msg, type) {
type = type || 'log';
window.console && console[type] && console[type]('layui error hint: ' + msg);
msg = '[Layui warn]: ' + msg;
if (window.console) {
console[type] ? console[type](msg) : console.log(msg);
}
};
var warned = Object.create(null);
var errorOnce = function (msg, type) {
if(warned._size && warned._size > 100){
warned = Object.create(null);
warned._size = 0;
}
if (!warned[msg]) {
warned[msg] = true;
warned._size = (warned._size || 0) + 1;
error(msg, type)
}
}
// 内置模块
var builtinModules = config.builtin = {
@@ -83,6 +100,7 @@
code: 'code', // 代码修饰器
jquery: 'jquery', // DOM 库(第三方)
component: 'component', // 组件构建器
i18n: 'i18n', // 国际化
all: 'all',
'layui.all': 'layui.all' // 聚合标识(功能性的,非真实模块)
@@ -161,7 +179,7 @@
/**
* 全局配置
* @param {Object} options
* @param {Object} options - 配置对象
*/
Class.prototype.config = function(options) {
Object.assign(config, options);
@@ -710,7 +728,8 @@
// 提示
Class.prototype.hint = function() {
return {
error: error
error: error,
errorOnce: errorOnce
};
};

View File

@@ -3,7 +3,7 @@
* Code 预览组件
*/
layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
layui.define(['lay', 'i18n', 'util', 'element', 'tabs', 'form'], function(exports) {
"use strict";
var $ = layui.$;
@@ -13,6 +13,7 @@ layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
var form = layui.form;
var layer = layui.layer;
var hint = layui.hint();
var i18n = layui.i18n;
// 常量
var CONST = {
@@ -201,7 +202,7 @@ layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
var tools = {
copy: {
className: 'file-b',
title: ['复制代码'],
title: [i18n.$t('code.copy')],
event: function(obj){
var code = util.unescape(finalCode(options.code));
var hasOnCopy = typeof options.onCopy === 'function';
@@ -215,14 +216,14 @@ layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
if(ret === false) return;
}
layer.msg('已复制', {icon: 1});
layer.msg(i18n.$t('code.copied'), {icon: 1});
},
error: function() {
if(hasOnCopy){
var ret = options.onCopy(code, false);
if(ret === false) return;
}
layer.msg('复制失败', {icon: 2});
layer.msg(i18n.$t('code.copyError'), {icon: 2});
}
});
}
@@ -277,7 +278,7 @@ layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
$.extend(tools, {
'full': {
className: 'screen-full',
title: ['最大化显示', '还原显示'],
title: [i18n.$t('code.maximize'), i18n.$t('code.restore')],
event: function(obj){
var el = obj.elem;
var elemView = el.closest('.'+ CONST.ELEM_PREVIEW);
@@ -302,7 +303,7 @@ layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
},
'window': {
className: 'release',
title: ['在新窗口预览'],
title: [i18n.$t('code.preview')],
event: function(obj){
util.openWin({
content: finalCode(options.code)
@@ -561,9 +562,11 @@ layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
// 若开启复制,且未开启预览,则单独生成复制图标
if(options.copy && !options.preview){
var copyElem = $(['<span class="layui-code-copy">',
'<i class="layui-icon layui-icon-file-b" title="复制"></i>',
'</span>'].join(''));
var copyElem = $([
'<span class="layui-code-copy">',
'<i class="layui-icon layui-icon-file-b" title="' + i18n.$t('code.copy') + '"></i>',
'</span>'
].join(''));
// 点击复制
copyElem.on('click', function(){

View File

@@ -3,12 +3,13 @@
* 颜色选择组件
*/
layui.define(['jquery', 'lay'], function(exports) {
layui.define(['i18n', 'jquery', 'lay'], function(exports) {
"use strict";
var $ = layui.$;
var lay = layui.lay;
var hint = layui.hint();
var i18n = layui.i18n;
var device = layui.device();
var clickOrMousedown = (device.mobile ? 'click' : 'mousedown');
@@ -284,9 +285,9 @@ layui.define(['jquery', 'lay'], function(exports) {
,'<div class="layui-inline">'
,'<input type="text" class="layui-input">'
,'</div>'
,'<div class="layui-btn-container">'
,'<button class="layui-btn layui-btn-primary layui-btn-sm" colorpicker-events="clear">清空</button>'
,'<button class="layui-btn layui-btn-sm" colorpicker-events="confirm">确定</button>'
,'<div class="layui-btn-group">'
,'<button style="border-radius: 0" class="layui-btn layui-btn-primary layui-btn-sm" colorpicker-events="clear">' + i18n.$t('colorpicker.clear') + '</button>'
,'<button style="border-radius: 0; border-left: 0" class="layui-btn layui-btn-primary layui-btn-sm" colorpicker-events="confirm">' + i18n.$t('colorpicker.confirm') + '</button>'
,'</div'
,'</div>'
,'</div>'].join(''))

View File

@@ -3,13 +3,14 @@
* 下拉菜单组件
*/
layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports) {
layui.define(['i18n', 'jquery', 'laytpl', 'lay', 'util'], function(exports) {
"use strict";
var $ = layui.$;
var laytpl = layui.laytpl;
var util = layui.util;
var hint = layui.hint();
var i18n = layui.i18n;
var device = layui.device();
var clickOrMousedown = (device.mobile ? 'touchstart' : 'mousedown');
@@ -181,7 +182,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports) {
if(options.data.length > 0 ){
eachItemView(elemUl, options.data)
} else {
elemUl.html('<li class="layui-menu-item-none">暂无数据</li>');
elemUl.html('<li class="layui-menu-item-none">' + i18n.$t('dropdown.noData') + '</li>');
}
return elemUl;
};
@@ -569,7 +570,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports) {
that.remove();
}, lay.passiveSupported ? { passive: false} : false);
// onClickOutside 检测 iframe
// onClickOutside 检测 iframe
_WIN.on('blur', function(e){
if(!dropdown.thisId) return;
var that = thisModule.getThis(dropdown.thisId)

View File

@@ -3,10 +3,11 @@
*/
layui.define('jquery', function(exports) {
layui.define(['i18n', 'jquery'], function(exports) {
"use strict";
var $ = layui.$;
var i18n = layui.i18n;
var Flow = function(options) {};
var ELEM_MORE = 'layui-flow-more';
var ELEM_LOAD = '<i class="layui-anim layui-anim-rotate layui-anim-loop layui-icon ">&#xe63e;</i>';
@@ -20,8 +21,8 @@ layui.define('jquery', function(exports) {
var scrollElem = $(options.scrollElem || document); // 滚动条所在元素
var threshold = 'mb' in options ? options.mb : 50; // 临界距离
var isAuto = 'isAuto' in options ? options.isAuto : true; // 否自动滚动加载
var moreText = options.moreText || "加载更多"; // 手动加载时,加载更多按钮文案
var end = options.end || '没有更多了'; // “末页”显示文案
var moreText = options.moreText || i18n.$t('flow.loadMore'); // 手动加载时,加载更多按钮文案
var end = options.end || i18n.$t('flow.noMore'); // “末页”显示文案
var direction = options.direction || 'bottom';
var isTop = direction === 'top';

View File

@@ -2,7 +2,7 @@
* form 表单组件
*/
layui.define(['lay', 'layer', 'util'], function(exports){
layui.define(['lay', 'i18n', 'layer', 'util'], function(exports){
"use strict";
var $ = layui.$;
@@ -10,6 +10,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
var util = layui.util;
var hint = layui.hint();
var device = layui.device();
var i18n = layui.i18n;
var MOD_NAME = 'form';
var ELEM = '.layui-form';
@@ -31,42 +32,42 @@ layui.define(['lay', 'layer', 'util'], function(exports){
verify: {
required: function(value) {
if (!/[\S]+/.test(value) || value === undefined || value === null) {
return '必填项不能为空';
return i18n.$t('form.validateMessages.required');
}
},
phone: function(value) {
var EXP = /^1\d{10}$/;
if (value && !EXP.test(value)) {
return '手机号格式不正确';
return i18n.$t('form.validateMessages.phone');
}
},
email: function(value) {
var EXP = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
if (value && !EXP.test(value)) {
return '邮箱格式不正确';
return i18n.$t('form.validateMessages.email');
}
},
url: function(value) {
var EXP = /^(#|(http(s?)):\/\/|\/\/)[^\s]+\.[^\s]+$/;
if (value && !EXP.test(value)) {
return '链接格式不正确';
return i18n.$t('form.validateMessages.url');
}
},
number: function(value){
if (value && isNaN(value)) {
return '只能填写数字';
return i18n.$t('form.validateMessages.number');
}
},
date: function(value){
var EXP = /^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/;
if (value && !EXP.test(value)) {
return '日期格式不正确';
return i18n.$t('form.validateMessages.date');
}
},
identity: function(value) {
var EXP = /(^\d{15}$)|(^\d{17}(x|X|\d)$)/;
if (value && !EXP.test(value)) {
return '身份证号格式不正确';
return i18n.$t('form.validateMessages.identity');
}
}
},
@@ -466,7 +467,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
// 下拉选择框
,select: function(elem){
var TIPS = '请选择';
var TIPS = i18n.$t('form.select.placeholder');
var CLASS = 'layui-form-select';
var TITLE = 'layui-select-title';
var NONE = 'layui-select-none';
@@ -765,7 +766,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
}
}else{
if(none){
dl.find('.'+NONE)[0] || dl.append('<p class="'+ NONE +'">无匹配项</p>');
dl.find('.'+NONE)[0] || dl.append('<p class="'+ NONE + '">' + i18n.$t('form.select.noMatch') + '</p>');
} else {
dl.find('.'+NONE).remove();
}
@@ -956,7 +957,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
}
});
if (arr.length === 0) {
arr.push('<dd lay-value="" class="'+ DISABLED +'">None</dd>');
arr.push('<dd lay-value="" class="'+ DISABLED + '">' + i18n.$t('form.select.noData') + '</dd>');
}
return arr.join('');
}();
@@ -1258,7 +1259,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
}
} else {
type ? (
items[type] ? items[type]() : hint.error('不支持的 "'+ type + '" 表单渲染')
items[type] ? items[type]() : hint.error('[form] "' + type + '" is an unsupported form element type')
) : renderItem();
}
return that;
@@ -1384,7 +1385,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
return othis;
}(), {tips: 1});
} else if(verType === 'alert') {
layer.alert(errorText, {title: '提示', shadeClose: true});
layer.alert(errorText, {title: i18n.$t('form.verifyErrorPromptTitle'), shadeClose: true});
}
// 若返回的为字符或数字,则自动弹出默认提示框;否则由 verify 方法中处理提示
else if(/\b(string|number)\b/.test(typeof errorText)) {

357
src/modules/i18n.js Normal file
View File

@@ -0,0 +1,357 @@
/**
* i18n
* 国际化
*/
layui.define('lay', function(exports) {
'use strict';
var lay = layui.lay;
var hint = layui.hint();
var MOD_NAME = 'i18n';
// 识别预先可能定义的指定全局对象
var GLOBAL = window.LAYUI_GLOBAL || {};
// 简体中文
var zhCN = {
code: {
copy: '复制代码',
copied: '已复制',
copyError: '复制失败',
maximize: '最大化显示',
restore: '还原显示',
preview: '在新窗口预览'
},
colorpicker: {
clear: '清除',
confirm: '确定'
},
dropdown: {
noData: '暂无数据'
},
flow: {
loadMore: '加载更多',
noMore: '没有更多了'
},
form: {
select: {
noData: '暂无数据',
noMatch: '无匹配数据',
placeholder: '请选择'
},
validateMessages: {
required: '必填项不能为空',
phone: '手机号格式不正确',
email: '邮箱格式不正确',
url: '链接格式不正确',
number: '只能填写数字',
date: '日期格式不正确',
identity: '身份证号格式不正确'
},
verifyErrorPromptTitle: '提示'
},
laydate: {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
weeks: ['日', '一', '二', '三', '四', '五', '六'],
time: ['时', '分', '秒'],
literal: {
year: '年'
},
selectDate: '选择日期',
selectTime: '选择时间',
startTime: '开始时间',
endTime: '结束时间',
tools: {
confirm: '确定',
clear: '清空',
now: '现在',
reset: '重置'
},
rangeOrderPrompt: '结束时间不能早于开始时间\n请重新选择',
invalidDatePrompt: '不在有效日期或时间范围内\n',
formatErrorPrompt: '日期格式不合法\n必须遵循\n{format}\n',
autoResetPrompt: '已自动重置',
preview: '当前选中的结果'
},
layer: {
confirm: '确定',
cancel: '取消',
defaultTitle: '信息',
prompt: {
InputLengthPrompt: '最多输入 {length} 个字符'
},
photos: {
noData: '没有图片',
tools:{
rotate: '旋转',
scaleX: '水平变换',
zoomIn: '放大',
zoomOut: '缩小',
reset: '还原',
close: '关闭'
},
viewPicture: '查看原图',
urlError: {
prompt: '当前图片地址异常,\n是否继续查看下一张',
confirm: '下一张',
cancel: '不看了'
}
}
},
laypage: {
prev: '上一页',
next: '下一页',
first: '首页',
last: '尾页',
total: '共 {total} 条',
pagesize: '条/页',
goto: '到第',
page: '页',
confirm: '确定'
},
table: {
sort: {
asc: '升序',
desc: '降序'
},
noData: '暂无数据',
tools:{
filter: {
title: '筛选列'
},
export: {
title: '导出',
noDataPrompt: '当前表格无数据',
compatPrompt: '导出功能不支持 IE请用 Chrome 等高级浏览器导出',
csvText : '导出 CSV 文件'
},
print: {
title: '打印',
noDataPrompt: '当前表格无数据'
}
},
dataFormatError: '返回的数据不符合规范,正确的成功状态码应为:"{statusName}": {statusCode}',
xhrError: '请求异常,错误提示:{msg}'
},
transfer: {
noData: '暂无数据',
noMatch: '无匹配数据',
title: ['列表一', '列表二'],
searchPlaceholder: '关键词搜索'
},
tree: {
defaultNodeName: '未命名',
noData: '暂无数据',
deleteNodePrompt: '确认删除"{name}"节点吗?'
},
upload: {
fileType: {
file: '文件',
image: '图片',
video: '视频',
audio: '音频'
},
validateMessages: {
fileExtensionError: '选择的{fileType}中包含不支持的格式',
filesOverLengthLimit: '同时最多只能上传: {length} 个文件',
currentFilesLength: '当前已经选择了: {length} 个文件',
fileOverSizeLimit: '文件大小不能超过 {size}'
},
chooseText: '{length} 个文件'
},
util: {
timeAgo: {
days: '{days} 天前',
hours: '{hours} 小时前',
minutes: '{minutes} 分钟前',
future: '未来',
justNow: '刚刚'
},
toDateString: {
// https://www.unicode.org/cldr/charts/47/supplemental/day_periods.html
meridiem: function(hours, minutes){
var hm = hours * 100 + minutes;
if (hm < 500) {
return '凌晨';
} else if (hm < 800) {
return '早上';
} else if (hm < 1200) {
return '上午';
} else if (hm < 1300) {
return '中午';
} else if (hm < 1900) {
return '下午';
}
return '晚上';
}
}
}
};
// 默认配置
var config = lay.extend({
locale: 'zh-CN', // 全局内置语言
messages: { // 全局国际化消息对象
'zh-CN': zhCN
}
}, GLOBAL.i18n); // 读取全局预设配置,确保打包后的版本初始调用时机
var OBJECT_REPLACE_REGEX = /\{(\w+)\}/g;
/**
* 获取对象中指定路径的值,类似于 lodash 的 _.get 方法(简易版)
* @param {Record<string, any>} obj - 要查找的对象
* @param {string} path - 要查找的路径,支持类似 'a[0].b.c' 的格式
* @param {any} defaultValue - 若未找到对应值时返回的默认值
* @returns {any} - 找到的值或默认值
*/
function get(obj, path, defaultValue) {
// 'a[0].b.c' ==> ['a', '0', 'b', 'c']
var casePath = path.replace(/\[(\d+)\]/g, '.$1').split('.');
var result = obj;
for(var i = 0; i < casePath.length; i++) {
result = result && result[casePath[i]];
if(result === null || result === undefined){
return defaultValue;
}
}
return result;
}
/**
* 为纯函数创建具有缓存功能的版本,类似于 lodash 的 _.memoize 方法(简易版)
* @template T
* @param {(key: string, ...args) => T} fn - 需要缓存的函数,第一个参数为键
* @returns {{(key: string, ...args): T, cleanup: () => void}} - 带有缓存的函数
*/
function memoize(fn){
/** @type Record<string, T> */
var cache = Object.create(null);
function cachedFn(key) {
var hit = cache[key];
return hit || (cache[key] = fn.apply(cache, arguments));
}
cachedFn.cleanup = function() {
cache = Object.create(null);
}
return cachedFn;
}
/**
* 对传入的值进行转义处理
* 若值为字符串,直接进行转义;若为函数,对函数返回的字符串进行转义;若为数组,对数组中的字符串元素进行转义
* @param {any} value - 需要进行转义处理的值
* @returns {any} - 转义后的结果
*/
function escape(value) {
if(typeof value === 'string'){
value = lay.escape(value);
}else if(typeof value === 'function'){
var origFn = value;
value = function(){
var val = origFn.apply(this, arguments)
return typeof val === 'string' ? lay.escape(val) : val;
}
}else if(layui.type(value) === 'array'){
value = value.map(function(v){
return typeof v === 'string' ? lay.escape(v) : v;
});
}
return value
}
function isDef(value) {
return value !== null && value !== undefined;
}
var resolveValue = memoize(function(path, obj, defaultValue){
var pathParts = path.split(':');
var locale = pathParts[0];
path = pathParts[1];
var value = get(obj, path, defaultValue);
if (layui.cache.debug) {
var isFallback = defaultValue === value || value === path;
var isNotFound = !isDef(value) || isFallback;
if (isNotFound) {
hint.errorOnce("Not found '" + path + "' key in '" + locale + "' locale messages.", 'warn');
}
if (isFallback) {
hint.errorOnce("Fallback to default message for key: '" + path + "'", 'warn');
}
}
return isDef(value) ? value : path;
});
var i18n = {
config: config,
set: function(options) {
lay.extend(config, options);
resolveValue.cleanup();
}
};
/**
* 根据给定的键从国际化消息中获取翻译后的内容
* 未文档化的私有方法,仅限内部使用
*
* @internal
* @param {string} keypath 要翻译的键路径
* @param {Record<string, any> | any[]} [parameters] 可选的占位符替换参数:
* - 对象形式:用于替换 `{key}` 形式的占位符;
* - 数组形式:用于替换 `{0}`, `{1}` 等占位符;
* @param {{locale: string, default: string}} [options] 翻译选项
* @returns {string} 翻译后的文本
*
* @example 使用对象替换命名占位符
* message: {
* hello: '{msg} world'
* }
* i18n.$t('message.hello', { msg: 'Hello' })
*
* @example 使用数组替换索引占位符
* message: {
* hello: '{0} world'
* }
* i18n.$t('message.hello', ['Hello'])
*/
i18n.translation = function(keypath, parameters, options) {
var locale = (options && options.locale) || config.locale;
var i18nMessages = config.messages[locale];
var namespace = locale + ':';
var fallbackMessage = (options && lay.hasOwn(options, 'default')) ? options.default : undefined;
if (!i18nMessages) {
hint.errorOnce("Locale '" + locale + "' not found. Please add i18n messages for this locale first.", 'error');
}
var result = resolveValue(namespace + keypath, i18nMessages, fallbackMessage);
// 替换占位符
if (typeof result === 'string' && parameters) {
// 第二个参数为对象或数组,替换占位符 {key} 或 {0}, {1}...
result = result.replace(OBJECT_REPLACE_REGEX, function(match, key) {
return parameters[key] !== undefined ? parameters[key] : match;
});
}
return escape(result);
};
/**
* i18n.translation 的别名,用于简化代码书写,未文档化仅限内部使用
*/
i18n.$t = i18n.translation;
exports(MOD_NAME, i18n);
});

View File

@@ -796,10 +796,46 @@
};
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* 检查对象是否具有指定的属性
* @param {Record<string, any>} obj 要检查的对象
* @param {string} prop 要检查的属性名
* @returns {boolean} 如果对象具有指定的属性,则为 true否则为 false
*/
lay.hasOwn = function(obj, prop){
return hasOwnProperty.call(obj, prop);
};
/**
* 转义 HTML 字符串中的特殊字符
* @param {string} html 要转义的 HTML 字符串
* @returns {string} 转义后的 HTML 字符串
*/
lay.escape = function (html) {
var exp = /[<"'>]|&(?=#?[a-zA-Z0-9]+)/g;
if (html === undefined || html === null) return '';
html += '';
if (!exp.test(html)) return html;
return html.replace(/&(?=#?[a-zA-Z0-9]+;?)/g, '&amp;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/'/g, '&#39;').replace(/"/g, '&quot;');
};
/**
* 还原转义的 HTML 字符串中的特殊字符
* @param {string} html 要还原转义的 HTML 字符串
* @returns {string} 还原转义后的 HTML 字符串
*/
lay.unescape = function (html) {
if (html === undefined || html === null) return '';
return String(html).replace(/\&quot;/g, '"').replace(/\&#39;/g, '\'')
.replace(/\&gt;/g, '>').replace(/\&lt;/g, '<')
.replace(/\&amp;/g, '&');
};
/*
* lay 元素操作

View File

@@ -1,39 +1,59 @@
/** laydate 日期与时间控件 | MIT Licensed */
// @ts-expect-error
(function(window, document) {
/**
* laydate
* 日期与时间组件
*/
layui.define(['lay', 'i18n'], function(exports) {
"use strict";
var isLayui = window.layui && layui.define;
var ready = {
getPath: window.lay && lay.getPath ? lay.getPath : '',
// 载入 CSS 依赖
link: function (href, fn, cssname) {
// 未设置路径,则不主动加载 css
if (!laydate.path) return;
// 加载 css
if (window.lay && lay.layui) {
lay.layui.link(laydate.path + href, fn, cssname);
}
}
};
// 识别预先可能定义的指定全局对象
var GLOBAL = window.LAYUI_GLOBAL || {};
var lay = layui.lay;
var i18n = layui.i18n;
// 模块名
var MOD_NAME = 'laydate';
var MOD_ID = 'lay-' + MOD_NAME + '-id'; // 已渲染过的索引标记名
var zhCN = 'zh-CN'; // 简体中文语言码
var YearBeforeMonthLocale = ['eu-ES', 'ja-JP', 'km-KH', 'ko-KR', 'pt-BR', 'si-LK', 'ms-MY', 'ug-CN', 'zh-CN', 'zh-HK', 'zh-TW']; // 年份在前的语言
function addSpaceBetweenChars(str) {
if (typeof str !== 'string' || str.length <= 1) {
return str;
}
var result = '';
for (var i = 0; i < str.length - 1; i++) {
var char = str[i];
var nextChar = str[i + 1];
result += char;
// 判断当前字符和下一个字符的类型
var isCharDigit = isDigit(char);
var isNextCharDigit = isDigit(nextChar);
// 在数字和非数字(非空格)之间添加空格
if (
(isCharDigit && !isNextCharDigit && nextChar !== ' ') || // 数字 → 非数字(非空格)
(char !== ' ' && !isCharDigit && isNextCharDigit) // 非空格非数字 → 数字
) {
result += ' ';
}
}
result += str[str.length - 1]; // 添加最后一个字符
return result;
}
function isDigit(char) {
var code = char.charCodeAt(0);
return code >= 48 && code <= 57; // '0' 到 '9' 的 ASCII 码范围
}
// 外部调用
var laydate = {
v: '5.6.0', // layDate 版本号
v: '5.7.0', // layDate 版本号
config: {
weekStart: 0 // 默认周日一周的开始
}, // 全局配置项
index: window.laydate && window.laydate.v ? 100000 : 0,
path: GLOBAL.laydate_dir || ready.getPath,
// 设置全局项
set: function (options) {
@@ -45,14 +65,14 @@
// 主体 CSS 等待事件
ready: function (callback) {
var cssname = 'laydate';
var ver = '';
var path = (isLayui ? 'modules/' : '') + 'laydate.css?v=' + laydate.v + ver;
var path = 'modules/laydate.css?v=' + laydate.v;
isLayui ? (
layui['layui.all'] ?
(typeof callback === 'function' && callback()) :
layui.addcss(path, callback, cssname)
) : ready.link(path, callback, cssname);
// 打包版直接执行回调函数
if (layui['layui.all']) {
typeof callback === 'function' && callback();
} else {
layui.addcss(path, callback, cssname);
}
return this;
}
@@ -124,6 +144,27 @@
// 初始化属性
options = lay.extend(that.config, lay.options(elem[0])); // 继承节点上的属性
// 更新 i18n 消息对象
that.i18nMessages = that.getI18nMessages();
// 处理日期面板顶部年月顺序
// 这是一个变通的方法,因为 i18nMessages.monthBeforeYear 不存在
if(typeof that.i18nMessages.monthBeforeYear !== 'boolean'){
if(!window.Intl){
that.i18nMessages.monthBeforeYear = !(YearBeforeMonthLocale.indexOf(options.lang) > -1);
}else{
var formatter = new Intl.DateTimeFormat(options.lang, { year: 'numeric', month: 'short' });
var parts = formatter.formatToParts(new Date(1970, 0));
var order = [];
parts.map(function(part) {
if (part.type === 'year' || part.type === 'month') {
order.push(part.type);
}
})
that.i18nMessages.monthBeforeYear = order[0] === 'month';
}
}
// 若重复执行 render则视为 reload 处理
if(elem[0] && elem.attr(MOD_ID)){
var newThat = thisModule.getThis(elem.attr(MOD_ID));
@@ -164,76 +205,127 @@
// 默认配置
Class.prototype.config = {
type: 'date' //控件类型支持year/month/date/time/datetime
,range: false //是否开启范围选择,即双控件
,format: 'yyyy-MM-dd' //默认日期格式
,value: null //默认日期支持传入new Date()或者符合format参数设定的日期格式字符
,isInitValue: true //用于控制是否自动向元素填充初始值(需配合 value 参数使用)
,min: '1900-1-1' //有效最小日期,年月日必须用“-”分割,时分秒必须用“:”分割。注意:它并不是遵循 format 设定的格式。
,max: '2099-12-31' //有效最大日期,同上
,trigger: 'click' //呼出控件的事件
,show: false //是否直接显示,如果设置 true则默认直接显示控件
,showBottom: true //是否显示底部栏
,isPreview: true //是否显示值预览
,btns: ['clear', 'now', 'confirm'] //右下角显示的按钮,会按照数组顺序排列
,lang: 'cn' //语言只支持cn/en即中文和英文
,theme: 'default' //主题
,position: null //控件定位方式定位, 默认absolute支持fixed/absolute/static
,calendar: false //是否开启公历重要节日,仅支持中文版
,mark: {} //日期备注,如重要事件或活动标记
,holidays: null // 标注法定节假日或补假上班
,zIndex: null //控件层叠顺序
,done: null //控件选择完毕后的回调,点击清空/现在/确定也均会触发
,change: null //日期时间改变后的回调
,autoConfirm: true //是否自动确认(日期|年份|月份选择器非range下是否自动确认
,shade: 0
type: 'date', // 控件类型支持year/month/date/time/datetime
range: false, // 是否开启范围选择,即双控件
format: 'yyyy-MM-dd', // 默认日期格式
value: null, // 默认日期支持传入new Date()或者符合format参数设定的日期格式字符
isInitValue: true, // 用于控制是否自动向元素填充初始值(需配合 value 参数使用)
min: '1900-1-1', // 有效最小日期,年月日必须用“-”分割,时分秒必须用“:”分割。注意:它并不是遵循 format 设定的格式。
max: '2099-12-31', // 有效最大日期,同上
trigger: 'click', // 呼出控件的事件
show: false, // 是否直接显示,如果设置 true则默认直接显示控件
showBottom: true, // 是否显示底部栏
isPreview: true, // 是否显示值预览
btns: ['clear', 'now', 'confirm'], // 右下角显示的按钮,会按照数组顺序排列
// 为实现 lang 选项就近生效,去除此处的默认值,$t 设置了英文回退值
lang: '', // 语言,只支持 cn/en即中文和英文
theme: 'default', // 主题
position: null, // 控件定位方式定位, 默认absolute支持fixed/absolute/static
calendar: false, // 是否开启公历重要节日,仅支持中文版
mark: {}, // 日期备注,如重要事件或活动标记
holidays: null, // 标注法定节假日或补假上班
zIndex: null, // 控件层叠顺序
done: null, // 控件选择完毕后的回调,点击清空/现在/确定也均会触发
change: null, // 日期时间改变后的回调
autoConfirm: true, // 是否自动确认(日期|年份|月份选择器非range下是否自动确认
shade: 0
};
//多语言
Class.prototype.lang = function(){
var that = this
,options = that.config
,text = {
cn: {
weeks: ['日', '一', '二', '三', '四', '五', '六']
,time: ['时', '分', '秒']
,timeTips: '选择时间'
,startTime: '开始时间'
,endTime: '结束时间'
,dateTips: '返回日期'
,month: ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
,tools: {
confirm: '确定'
,clear: '清空'
,now: '现在'
}
,timeout: '结束时间不能早于开始时间<br>请重新选择'
,invalidDate: '不在有效日期或时间范围内'
,formatError: ['日期格式不合法<br>必须遵循下述格式:<br>', '<br>已为你重置']
,preview: '当前选中的结果'
}
,en: {
weeks: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
,time: ['Hours', 'Minutes', 'Seconds']
,timeTips: 'Select Time'
,startTime: 'Start Time'
,endTime: 'End Time'
,dateTips: 'Select Date'
,month: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
,tools: {
confirm: 'Confirm'
,clear: 'Clear'
,now: 'Now'
}
,timeout: 'End time cannot be less than start Time<br>Please re-select'
,invalidDate: 'Invalid date'
,formatError: ['The date format error<br>Must be followed<br>', '<br>It has been reset']
,preview: 'The selected result'
}
};
return text[options.lang] || text['cn'];
};
Class.prototype.getI18nMessages = function () {
var that = this;
var options = that.config;
var locale = i18n.config.locale;
// 纠正旧版「简体中文」语言码
if (options.lang === 'cn') {
options.lang = zhCN;
}else if(!options.lang){
options.lang = i18n.config.locale;
}
locale = options.lang;
return {
months: i18n.$t('laydate.months', null, {
locale: locale,
default: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}),
weeks: i18n.$t('laydate.weeks', null, {
locale: locale,
default: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
}),
time: i18n.$t('laydate.time', null, {
locale: locale,
default: ['Hour', 'Minute', 'Second']
}),
literal: {
year: i18n.$t('laydate.literal.year', null, {
locale: locale,
default: ''
})
},
monthBeforeYear: i18n.$t('laydate.monthBeforeYear', null, {
locale: locale
}),
selectDate: i18n.$t('laydate.selectDate', null, {
locale: locale,
default: 'Select Date'
}),
selectTime: i18n.$t('laydate.selectTime', null, {
locale: locale,
default: 'Select Time'
}),
startTime: i18n.$t('laydate.startTime', null, {
locale: locale,
default: 'Start Time'
}),
endTime: i18n.$t('laydate.endTime', null, {
locale: locale,
default: 'End Time'
}),
tools: {
confirm: i18n.$t('laydate.tools.confirm', null, {
locale: locale,
default: 'Confirm'
}),
clear: i18n.$t('laydate.tools.clear', null, {
locale: locale,
default: 'Clear'
}),
now: i18n.$t('laydate.tools.now', null, {
locale: locale,
default: 'Now'
}),
reset: i18n.$t('laydate.tools.reset', null, {
locale: locale,
default: 'Reset'
})
},
rangeOrderPrompt: i18n.$t('laydate.rangeOrderPrompt', null, {
locale: locale,
default: 'End time cannot be less than start Time\nPlease re-select'
}),
invalidDatePrompt: i18n.$t('laydate.invalidDatePrompt', null, {
locale: locale,
default: 'Invalid date\n'
}),
formatErrorPrompt: function (format) {
return i18n.$t('laydate.formatErrorPrompt', {format: format}, {
locale: locale,
default: 'Date format is invalid\nMust follow the format:\n{format}\n'
});
},
autoResetPrompt: i18n.$t('laydate.autoResetPrompt', null, {
locale: locale,
default: 'It has been reset'
}),
preview: i18n.$t('laydate.preview', null, {
locale: locale,
default: 'The selected result'
})
}
}
// 仅简体中文生效,不做国际化
Class.prototype.markerOfChineseFestivals = {
'0-1-1': '元旦',
'0-2-14': '情人' ,
@@ -246,7 +338,7 @@
'0-9-10': '教师',
'0-10-1': '国庆',
'0-12-25': '圣诞'
}
};
// 重载实例
Class.prototype.reload = function(options){
@@ -327,7 +419,7 @@
// 设置了一周的开始是周几,此处做一个控制
if (options.weekStart) {
if (!/^[0-6]$/.test(options.weekStart)) {
var lang = that.lang();
var lang = that.i18nMessages;
options.weekStart = lang.weeks.indexOf(options.weekStart);
if (options.weekStart === -1) options.weekStart = 0;
}
@@ -444,7 +536,7 @@
Class.prototype.render = function(){
var that = this
,options = that.config
,lang = that.lang()
,lang = that.i18nMessages
,isStatic = options.position === 'static'
//主面板
@@ -576,7 +668,7 @@
lay(divFooter).html(function(){
var html = [], btns = [];
if(options.type === 'datetime'){
html.push('<span lay-type="datetime" class="'+ ELEM_TIME_BTN +'">'+ lang.timeTips +'</span>');
html.push('<span lay-type="datetime" class="'+ ELEM_TIME_BTN +'">'+ lang.selectTime +'</span>');
}
if(!(!options.range && options.type === 'datetime') || options.fullPanel){
html.push('<span class="'+ ELEM_PREVIEW +'" title="'+ lang.preview +'"></span>')
@@ -584,8 +676,10 @@
lay.each(options.btns, function(i, item){
var title = lang.tools[item] || 'btn';
if(options.range && item === 'now') return;
if(isStatic && item === 'clear') title = options.lang === 'cn' ? '重置' : 'Reset';
if (options.range && item === 'now') return;
if (isStatic && item === 'clear') {
title = lang.tools.reset;
}
btns.push('<span lay-type="'+ item +'" class="laydate-btns-'+ item +'">'+ title +'</span>');
});
html.push('<div class="laydate-footer-btns">'+ btns.join('') +'</div>');
@@ -838,7 +932,7 @@
var that = this
,thisDate = new Date()
,options = that.config
,lang = that.lang()
,lang = that.i18nMessages
,dateTime = options.dateTime = options.dateTime || that.systemDate()
,thisMaxDate, error
@@ -970,9 +1064,12 @@
}
} else {
//格式不合法
that.hint(lang.formatError[0] + (
options.range ? (options.format + ' '+ that.rangeStr +' ' + options.format) : options.format
) + lang.formatError[1]);
that.hint(
lang.formatErrorPrompt(
options.range ? (options.format + ' '+ that.rangeStr +' ' + options.format) : options.format
) +
lang.autoResetPrompt
);
error = true;
}
} else if(value && layui.type(value) === 'date'){ //若值为日期对象
@@ -1050,7 +1147,7 @@
// 初始值不在最大最小范围内
if(minMaxError && value){
that.setValue(that.parse());
that.hint('value ' + lang.invalidDate + lang.formatError[1]);
that.hint('value ' + lang.invalidDatePrompt + lang.autoResetPrompt);
}
// 初始赋值 startDate,endState
@@ -1101,8 +1198,11 @@
that.markRender(td, YMD, markers);
}
if(options.calendar && options.lang === 'cn'){
render(that.markerOfChineseFestivals);
// chineseFestivals 仅简体中文生效
if (options.calendar) {
if (options.lang === zhCN) {
render(that.markerOfChineseFestivals);
}
}
if(typeof options.mark === 'function'){
@@ -1436,14 +1536,14 @@
,options = that.config
,dateTime = value || that.thisDateTime(index)
,thisDate = new Date(), startWeek, prevMaxDate, thisMaxDate
,lang = that.lang()
,lang = that.i18nMessages
,isAlone = options.type !== 'date' && options.type !== 'datetime'
,tds = lay(that.table[index]).find('td')
,elemYM = lay(that.elemHeader[index][2]).find('span');
if(dateTime.year < LIMIT_YEAR[0]) dateTime.year = LIMIT_YEAR[0], that.hint(lang.invalidDate);
if(dateTime.year > LIMIT_YEAR[1]) dateTime.year = LIMIT_YEAR[1], that.hint(lang.invalidDate);
if(dateTime.year < LIMIT_YEAR[0]) dateTime.year = LIMIT_YEAR[0], that.hint(lang.invalidDatePrompt);
if(dateTime.year > LIMIT_YEAR[1]) dateTime.year = LIMIT_YEAR[1], that.hint(lang.invalidDatePrompt);
//记录初始值
if(!that.firstDate){
@@ -1499,12 +1599,14 @@
if(!that.panelYM) that.panelYM = {};
that.panelYM[index] = {year: dateTime.year, month: dateTime.month};
if(options.lang === 'cn'){
lay(elemYM[0]).attr('lay-type', 'year').html(dateTime.year + ' 年')
lay(elemYM[1]).attr('lay-type', 'month').html((dateTime.month + 1) + ' 月');
var normalizedYearStr = addSpaceBetweenChars(dateTime.year + lang.literal.year);
var normalizedMonthStr = addSpaceBetweenChars(lang.months[dateTime.month]);
if(!lang.monthBeforeYear){
lay(elemYM[0]).attr('lay-type', 'year').html(normalizedYearStr);
lay(elemYM[1]).attr('lay-type', 'month').html(normalizedMonthStr);
} else {
lay(elemYM[0]).attr('lay-type', 'month').html(lang.month[dateTime.month]);
lay(elemYM[1]).attr('lay-type', 'year').html(dateTime.year);
lay(elemYM[0]).attr('lay-type', 'month').html(normalizedMonthStr);
lay(elemYM[1]).attr('lay-type', 'year').html(normalizedYearStr);
}
//初始默认选择器
@@ -1518,9 +1620,10 @@
that.list(options.type, 0).list(options.type, 1);
//同步按钮可点状态
options.type === 'time' ? that.setBtnStatus('时间'
,lay.extend({}, that.systemDate(), that.startTime)
,lay.extend({}, that.systemDate(), that.endTime)
options.type === 'time' ? that.setBtnStatus(
true,
lay.extend({}, that.systemDate(), that.startTime),
lay.extend({}, that.systemDate(), that.endTime)
) : that.setBtnStatus(true);
}
} else {
@@ -1581,7 +1684,7 @@
var that = this
,options = that.config
,dateTime = that.rangeLinked ? options.dateTime : [options.dateTime, that.endDate][index]
,lang = that.lang()
,lang = that.i18nMessages
,isAlone = options.range && options.type !== 'date' && options.type !== 'datetime' //独立范围选择器
,ul = lay.elem('ul', {
@@ -1595,8 +1698,8 @@
,elemYM = lay(elemHeader[2]).find('span')
,elemCont = that.elemCont[index || 0]
,haveList = lay(elemCont).find('.'+ ELEM_LIST)[0]
,isCN = options.lang === 'cn'
,text = isCN ? '年' : ''
,isMonthBeforeYear = lang.monthBeforeYear
,text = lang.literal.year
,listYM = that.listYM[index] || {}
,hms = ['hours', 'minutes', 'seconds']
@@ -1644,7 +1747,7 @@
yearNum++;
});
lay(elemYM[isCN ? 0 : 1]).attr('lay-ym', (yearNum - 8) + '-' + listYM[1])
lay(elemYM[!isMonthBeforeYear ? 0 : 1]).attr('lay-ym', (yearNum - 8) + '-' + listYM[1])
.html((startY + text) + ' - ' + (yearNum - 1 + text));
}
@@ -1661,7 +1764,7 @@
};
i + 1 == listYM[1] && lay(li).addClass(THIS);
li.innerHTML = lang.month[i] + (isCN ? '月' : '');
li.innerHTML = lang.months[i];
ul.appendChild(li);
/*
@@ -1683,7 +1786,7 @@
that.cellRender(li, {year: listYM[0], month: i + 1, date: 1}, 'month');
});
lay(elemYM[isCN ? 0 : 1]).attr('lay-ym', listYM[0] + '-' + listYM[1])
lay(elemYM[!isMonthBeforeYear ? 0 : 1]).attr('lay-ym', listYM[0] + '-' + listYM[1])
.html(listYM[0] + text);
}
@@ -1852,7 +1955,7 @@
,haveSpan = lay(elemHeader[2]).find('.'+ ELEM_TIME_TEXT);
scroll();
span.innerHTML = options.range ? [lang.startTime,lang.endTime][index] : lang.timeTips;
span.innerHTML = options.range ? [lang.startTime,lang.endTime][index] : lang.selectTime;
lay(that.elemMain[index]).addClass('laydate-time-show');
if(haveSpan[0]) haveSpan.remove();
@@ -1913,7 +2016,7 @@
Class.prototype.setBtnStatus = function(tips, start, end){
var that = this
,options = that.config
,lang = that.lang()
,lang = that.i18nMessages
,isOut
,elemBtn = lay(that.footer).find(ELEM_CONFIRM)
,timeParams = options.type === 'datetime' || options.type === 'time' ? ['hours', 'minutes', 'seconds'] : undefined;
@@ -1937,10 +2040,10 @@
? elemBtn.addClass(DISABLED)
: elemBtn[isOut ? 'addClass' : 'removeClass'](DISABLED);
//是否异常提示
if(tips && isOut) that.hint(
typeof tips === 'string' ? lang.timeout.replace(/日期/g, tips) : lang.timeout
);
// 是否异常提示
if (tips && isOut) {
that.hint(lang.rangeOrderPrompt);
}
}
};
@@ -2320,7 +2423,7 @@
Class.prototype.tool = function(btn, type){
var that = this
,options = that.config
,lang = that.lang()
,lang = that.i18nMessages
,dateTime = options.dateTime
,isStatic = options.position === 'static'
,active = {
@@ -2329,13 +2432,13 @@
if(lay(btn).hasClass(DISABLED)) return;
that.list('time', 0);
options.range && that.list('time', 1);
lay(btn).attr('lay-type', 'date').html(that.lang().dateTips);
lay(btn).attr('lay-type', 'date').html(that.i18nMessages.selectDate);
}
//选择日期
,date: function(){
that.closeList();
lay(btn).attr('lay-type', 'datetime').html(that.lang().timeTips);
lay(btn).attr('lay-type', 'datetime').html(that.i18nMessages.selectTime);
}
//清空、重置
@@ -2360,7 +2463,7 @@
// 当前系统时间未在 min/max 范围内,则不可点击
if(lay(btn).hasClass(DISABLED)){
return that.hint(lang.tools.now +', '+ lang.invalidDate);
return that.hint(lang.tools.now +', '+ lang.invalidDatePrompt);
}
lay.extend(dateTime, that.systemDate(), {
@@ -2382,12 +2485,10 @@
? that.startTime && that.endTime && that.newDate(that.startTime) > that.newDate(that.endTime)
: that.startDate && that.endDate && that.newDate(lay.extend({},that.startDate, that.startTime || {})) > that.newDate(lay.extend({},that.endDate, that.endTime || {}));
return isTimeout
? that.hint(options.type === 'time' ? lang.timeout.replace(/日期/g, '时间') : lang.timeout)
: that.hint(lang.invalidDate);
return that.hint(isTimeout ? lang.rangeOrderPrompt : lang.invalidDatePrompt);
}
} else {
if(lay(btn).hasClass(DISABLED)) return that.hint(lang.invalidDate);
if(lay(btn).hasClass(DISABLED)) return that.hint(lang.invalidDatePrompt);
}
that.setValue(that.parse());
@@ -2604,64 +2705,63 @@
};
};
//记录所有实例
// 绑定关闭控件事件
lay(document).on('mousedown', function(e){
if(!laydate.thisId) return;
var that = thisModule.getThis(laydate.thisId);
if(!that) return;
var options = that.config;
if(
e.target === options.elem[0] ||
e.target === options.eventElem[0] ||
e.target === lay(options.closeStop)[0] ||
(options.elem[0] && options.elem[0].contains(e.target))
) return;
that.remove();
}).on('keydown', function(e){
if(!laydate.thisId) return;
var that = thisModule.getThis(laydate.thisId);
if(!that) return;
// 回车触发确认
if(that.config.position === 'static') return;
if(e.keyCode === 13){
if(lay('#'+ that.elemID)[0] && that.elemID === Class.thisElemDate){
e.preventDefault();
lay(that.footer).find(ELEM_CONFIRM)[0].click();
}
}
});
//自适应定位
lay(window).on('resize', function(){
if(!laydate.thisId) return;
var that = thisModule.getThis(laydate.thisId);
if(!that) return;
if(!that.elem || !lay(ELEM)[0]){
return false;
}
that.position();
});
// 记录所有实例
thisModule.that = {}; //记录所有实例对象
//获取当前实例对象
thisModule.getThis = function(id){
// 获取当前实例对象
thisModule.getThis = function(id) {
var that = thisModule.that[id];
if(!that && isLayui) layui.hint().error(id ? (MOD_NAME +' instance with ID \''+ id +'\' not found') : 'ID argument required');
if (!that) {
layui.hint().error(id ? (MOD_NAME +' instance with ID \''+ id +'\' not found') : 'ID argument required');
}
return that;
};
// 初始执行
ready.run = function(lay){
// 绑定关闭控件事件
lay(document).on('mousedown', function(e){
if(!laydate.thisId) return;
var that = thisModule.getThis(laydate.thisId);
if(!that) return;
var options = that.config;
if(
e.target === options.elem[0] ||
e.target === options.eventElem[0] ||
e.target === lay(options.closeStop)[0] ||
(options.elem[0] && options.elem[0].contains(e.target))
) return;
that.remove();
}).on('keydown', function(e){
if(!laydate.thisId) return;
var that = thisModule.getThis(laydate.thisId);
if(!that) return;
// 回车触发确认
if(that.config.position === 'static') return;
if(e.keyCode === 13){
if(lay('#'+ that.elemID)[0] && that.elemID === Class.thisElemDate){
e.preventDefault();
lay(that.footer).find(ELEM_CONFIRM)[0].click();
}
}
});
//自适应定位
lay(window).on('resize', function(){
if(!laydate.thisId) return;
var that = thisModule.getThis(laydate.thisId);
if(!that) return;
if(!that.elem || !lay(ELEM)[0]){
return false;
}
that.position();
});
};
// 渲染 - 核心接口
laydate.render = function(options){
var inst = new Class(options);
@@ -2747,23 +2847,5 @@
return new Date(thisDate.getTime() - 1000*60*60*24).getDate();
};
//加载方式
isLayui ? (
laydate.ready()
,layui.define('lay', function(exports){ //layui 加载
laydate.path = layui.cache.dir;
ready.run(lay);
exports(MOD_NAME, laydate);
})
) : (
(typeof define === 'function' && define.amd) ? define(function(){ //requirejs 加载
ready.run(lay);
return laydate;
}) : function(){ //普通 script 标签加载
laydate.ready();
ready.run(window.lay);
window.laydate = laydate;
}()
);
})(window, window.document);
exports(MOD_NAME, laydate);
});

View File

@@ -9,6 +9,9 @@
var isLayui = window.layui && layui.define;
var $;
var win;
var i18n = {};
var OBJECT_REPLACE_REGEX = /\{(\w+)\}/g;
var ready = {
getPath: function(){
var jsPath = (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') ? document.currentScript.src : function(){
@@ -27,15 +30,38 @@ var ready = {
return GLOBAL.layer_dir || jsPath.substring(0, jsPath.lastIndexOf('/') + 1);
}(),
config: {
removeFocus: true
removeFocus: true,
lang: {
confirm: '确定',
cancel: '取消',
defaultTitle: '信息',
prompt: {
InputLengthPrompt: '最多输入 {length} 个字符'
},
photos: {
noData: '没有图片',
tools: {
rotate: '旋转',
scaleX: '水平变换',
zoomIn: '放大',
zoomOut: '缩小',
reset: '还原',
close: '关闭'
},
viewPicture: '查看原图',
urlError: {
prompt: '当前图片地址异常,\n是否继续查看下一张',
confirm: '下一张',
cancel: '不看了'
}
}
},
},
end: {},
beforeEnd: {},
events: {resize: {}},
minStackIndex: 0,
minStackArr: [],
btn: ['确定', '取消'],
// 五种原始层模式
type: ['dialog', 'page', 'iframe', 'loading', 'tips'],
@@ -99,6 +125,43 @@ var ready = {
}
};
/**
* 获取对象中的值lodash _.get 简易版
* @param {Record<string, any>} obj
* @param {string} path
* @param {any} defaultValue
*/
var get = function (obj, path, defaultValue) {
// 'a[0].b.c' ==> ['a', '0', 'b', 'c']
var casePath = path.replace(/\[(\d+)\]/g, '.$1').split('.');
var result = obj;
for (var i = 0; i < casePath.length; i++) {
result = result && result[casePath[i]];
if (result === null || result === undefined) {
return defaultValue;
}
}
return result;
}
// 根据给定的键从国际化消息中获取翻译后的内容
i18n.$t = function(key) {
// 非 layui 环境去除命名空间前缀,从 config.lang 中取值
// TODO 简化版,暂不文档化,不支持配置多个 locale
key = key.replace(/^layer\./, '');
var result = get(ready.config.lang, key, key);
if(typeof result === 'string' && arguments.length > 1) {
var opts = arguments[1];
return result.replace(OBJECT_REPLACE_REGEX, function(match, key) {
return opts[key] !== undefined ? opts[key] : match;
})
}
return result;
};
// 默认内置方法。
var layer = {
v: '3.7.0',
@@ -162,7 +225,7 @@ var layer = {
}
return layer.open($.extend({
content: content,
btn: ready.btn,
btn: [i18n.$t('layer.confirm'), i18n.$t('layer.cancel')],
yes: yes,
btn2: cancel
}, type ? {} : options));
@@ -225,6 +288,8 @@ var Class = function(setings){
var that = this, creat = function(){
that.creat();
};
// TODO 临时的同步方案
ready.config.title = i18n.$t('layer.defaultTitle');
that.index = ++layer.index;
that.config.maxWidth = $(win).width() - 15*2; // 初始最大宽度:当前屏幕宽,左右留 15px 边距
that.config = $.extend({}, that.config, ready.config, setings);
@@ -268,7 +333,7 @@ Class.pt.config = {
shade: 0.3,
fixed: true,
move: doms[1],
title: '信息',
title: i18n.$t('layer.defaultTitle'),
offset: 'auto',
area: 'auto',
closeBtn: 1,
@@ -456,7 +521,7 @@ Class.pt.creat = function(){
switch(config.type){
case 0:
config.btn = ('btn' in config) ? config.btn : ready.btn[0];
config.btn = ('btn' in config) ? config.btn : i18n.$t('layer.confirm');
layer.closeAll('dialog');
break;
case 2:
@@ -1444,7 +1509,7 @@ layer.prompt = function(options, yes){
return layer.open($.extend({
type: 1,
btn: ['确定','取消'],
btn: [i18n.$t('layer.confirm'),i18n.$t('layer.cancel')],
content: content,
skin: 'layui-layer-prompt' + skin('prompt'),
maxWidth: win.width(),
@@ -1457,7 +1522,7 @@ layer.prompt = function(options, yes){
yes: function(index){
var value = prompt.val();
if(value.length > (options.maxlength||500)) {
layer.tips('最多输入'+ (options.maxlength || 500) +'个字符', prompt, {tips: 1});
layer.tips(i18n.$t('layer.prompt.InputLengthPrompt', {length: (options.maxlength || 500)}), prompt, {tips: 1});
} else {
yes && yes(value, index, prompt);
}
@@ -1571,7 +1636,7 @@ layer.photos = function(options, loop, key){
// 不直接弹出
if (!loop) return;
} else if (data.length === 0){
return layer.msg('没有图片');
return layer.msg(i18n.$t('layer.photos.noData'));
}
// 上一张
@@ -1716,7 +1781,7 @@ layer.photos = function(options, loop, key){
});
// 滑动切换图片事件,仅限 layui 中
if(window.layui || window.lay){
if(isLayui || (window.lay && typeof window.lay === 'function')){
var lay = window.layui.lay || window.lay;
var touchEndCallback = function(e, state){
var duration = Date.now() - state.timeStart;
@@ -1819,12 +1884,12 @@ layer.photos = function(options, loop, key){
if (options.toolbar) {
arr.push([
'<div class="layui-layer-photos-toolbar layui-layer-photos-header">',
'<span toolbar-event="rotate" data-option="90" title="旋转"><i class="layui-icon layui-icon-refresh"></i></span>',
'<span toolbar-event="scalex" title="变换"><i class="layui-icon layui-icon-slider"></i></span>',
'<span toolbar-event="zoom" data-option="0.1" title="放大"><i class="layui-icon layui-icon-add-circle"></i></span>',
'<span toolbar-event="zoom" data-option="-0.1" title="缩小"><i class="layui-icon layui-icon-reduce-circle"></i></span>',
'<span toolbar-event="reset" title="还原"><i class="layui-icon layui-icon-refresh-1"></i></span>',
'<span toolbar-event="close" title="关闭"><i class="layui-icon layui-icon-close"></i></span>',
'<span toolbar-event="rotate" data-option="90" title="'+ i18n.$t('layer.photos.tools.rotate') +'"><i class="layui-icon layui-icon-refresh"></i></span>',
'<span toolbar-event="scalex" title="'+ i18n.$t('layer.photos.tools.scaleX') +'"><i class="layui-icon layui-icon-slider"></i></span>',
'<span toolbar-event="zoom" data-option="0.1" title="'+ i18n.$t('layer.photos.tools.zoomIn') +'"><i class="layui-icon layui-icon-add-circle"></i></span>',
'<span toolbar-event="zoom" data-option="-0.1" title="'+ i18n.$t('layer.photos.tools.zoomOut') +'"><i class="layui-icon layui-icon-reduce-circle"></i></span>',
'<span toolbar-event="reset" title="'+ i18n.$t('layer.photos.tools.reset') +'"><i class="layui-icon layui-icon-refresh-1"></i></span>',
'<span toolbar-event="close" title="'+ i18n.$t('layer.photos.tools.close') +'"><i class="layui-icon layui-icon-close"></i></span>',
'</div>'
].join(''));
}
@@ -1834,7 +1899,7 @@ layer.photos = function(options, loop, key){
arr.push(['<div class="layui-layer-photos-toolbar layui-layer-photos-footer">',
'<h3>'+ alt +'</h3>',
'<em>'+ dict.imgIndex +' / '+ data.length +'</em>',
'<a href="'+ data[start].src +'" target="_blank">查看原图</a>',
'<a href="'+ data[start].src + '" target="_blank">'+ i18n.$t('layer.photos.viewPicture') +'</a>',
'</div>'].join(''));
}
@@ -1856,9 +1921,9 @@ layer.photos = function(options, loop, key){
}, options));
}, function(){
layer.close(dict.loadi);
layer.msg('当前图片地址异常,<br>是否继续查看下一张?', {
layer.msg('<span style="white-space: pre-line;">' + i18n.$t('layer.photos.urlError.prompt') + '</span>', {
time: 30000,
btn: ['下一张', '不看了'],
btn: [i18n.$t('layer.photos.urlError.confirm'), i18n.$t('layer.photos.urlError.cancel')],
yes: function(){
data.length > 1 && dict.imgnext(true,true);
}
@@ -1897,7 +1962,8 @@ ready.run = function(_$){
// 加载方式
window.layui && layui.define ? (
layer.ready(),
layui.define(['jquery','lay'], function(exports){ // layui
layui.define(['jquery', 'lay', 'i18n'], function(exports) { // layui
i18n.$t = layui.i18n.$t;
layer.path = layui.cache.dir;
ready.run(layui.$);

View File

@@ -2,9 +2,11 @@
* laypage 分页组件
*/
layui.define(function(exports) {
layui.define('i18n', function(exports) {
"use strict";
var i18n = layui.i18n;
var doc = document;
var id = 'getElementById';
var tag = 'getElementsByTagName';
@@ -72,8 +74,8 @@ layui.define(function(exports) {
groups = config.pages;
}
config.prev = 'prev' in config ? config.prev : '上一页'; // 上一页文本
config.next = 'next' in config ? config.next : '下一页'; // 下一页文本
config.prev = 'prev' in config ? config.prev : i18n.$t('laypage.prev'); // 上一页文本
config.next = 'next' in config ? config.next : i18n.$t('laypage.next'); // 下一页文本
// 计算当前组
var index = config.pages > groups
@@ -100,7 +102,7 @@ layui.define(function(exports) {
// 首页
if(index > 1 && config.first !== false && groups !== 0){
pager.push('<a class="layui-laypage-first" data-page="1" title="首页">'+ (config.first || 1) +'</a>');
pager.push('<a class="layui-laypage-first" data-page="1" title="' + i18n.$t('laypage.first') + '">' + (config.first || 1) +'</a>');
}
// 计算当前页码组的起始页
@@ -118,7 +120,7 @@ layui.define(function(exports) {
// 输出左分割符
if(config.first !== false && start > 2){
pager.push('<span class="layui-laypage-spr">...</span>')
pager.push('<span class="layui-laypage-spr">...</span>');
}
// 输出连续页码
@@ -137,7 +139,7 @@ layui.define(function(exports) {
pager.push('<span class="layui-laypage-spr">...</span>');
}
if(groups !== 0){
pager.push('<a class="layui-laypage-last" title="尾页" data-page="'+ config.pages +'">'+ (config.last || config.pages) +'</a>');
pager.push('<a class="layui-laypage-last" title="' + i18n.$t('laypage.last') + '" data-page="'+ config.pages +'">'+ (config.last || config.pages) +'</a>');
}
}
@@ -153,15 +155,18 @@ layui.define(function(exports) {
// 数据总数
count: function(){
var countText = typeof config.countText === 'object' ? config.countText : ['共 ', ' 条'];
return '<span class="layui-laypage-count">'+ countText[0] + config.count + countText[1] +'</span>'
var countText = typeof config.countText === 'object'
? config.countText[0] + config.count + config.countText[1]
: i18n.$t('laypage.total', {total: config.count});
return '<span class="layui-laypage-count">'+ countText +'</span>'
}(),
// 每页条数
limit: function(){
var elemArr = ['<span class="layui-laypage-limits"><select lay-ignore>'];
var template = function(item) {
var def = item +' 条/页';
var def = item + ' ' + i18n.$t('laypage.pagesize');
return typeof config.limitTemplet === 'function'
? (config.limitTemplet(item) || def)
: def;
@@ -189,9 +194,9 @@ layui.define(function(exports) {
// 跳页区域
skip: function(){
var skipText = typeof config.skipText === 'object' ? config.skipText : [
'到第',
'页',
'确定'
i18n.$t('laypage.goto'),
i18n.$t('laypage.page'),
i18n.$t('laypage.confirm')
];
return [
'<span class="layui-laypage-skip">'+ skipText[0],

View File

@@ -3,7 +3,7 @@
* 表格组件
*/
layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
layui.define(['lay', 'i18n', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
"use strict";
var $ = layui.$;
@@ -15,6 +15,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
var util = layui.util;
var hint = layui.hint();
var device = layui.device();
var i18n = layui.i18n;
// api
var table = {
@@ -192,7 +193,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
,'{{# } else { }}'
,'<span>{{-item2.title||""}}</span>'
,'{{# if(isSort){ }}'
,'<span class="layui-table-sort layui-inline"><i class="layui-edge layui-table-sort-asc" title="升序"></i><i class="layui-edge layui-table-sort-desc" title="降序"></i></span>'
,'<span class="layui-table-sort layui-inline"><i class="layui-edge layui-table-sort-asc" title="{{= d.i18nMessages.table_sort_asc }}"></i><i class="layui-edge layui-table-sort-desc" title="{{= d.i18nMessages.table_sort_desc }}"></i></span>'
,'{{# } }}'
,'{{# } }}'
,'</div>'
@@ -301,9 +302,6 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
defaultToolbar: ['filter', 'exports', 'print'], // 工具栏右侧图标
defaultContextmenu: true, // 显示默认上下文菜单
autoSort: true, // 是否前端自动排序。如果否,则需自主排序(通常为服务端处理好排序)
text: {
none: '无数据'
},
cols: []
};
@@ -323,7 +321,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
thisTable.that[id] = that; // 记录当前实例对象
thisTable.config[id] = options; // 记录当前实例配置项
//请求参数的自定义格式
// 请求参数的自定义格式
options.request = $.extend({
pageName: 'page',
limitName: 'limit'
@@ -339,7 +337,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
countName: 'count'
}, options.response);
//如果 page 传入 laypage 对象
// 如果 page 传入 laypage 对象
if(options.page !== null && typeof options.page === 'object'){
options.limit = options.page.limit || options.limit;
options.limits = options.page.limits || options.limits;
@@ -348,7 +346,12 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
delete options.page.jump;
}
if(!options.elem[0]) return that;
// 加载 i18n 自定义文本
options.text = $.extend(true, {
none: i18n.$t('table.noData')
}, options.text);
if (!options.elem[0]) return that;
// 若元素未设 lay-filter 属性,则取实例 id 值
if(!options.elem.attr('lay-filter')){
@@ -419,7 +422,11 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
tagStyle: 'legacy'
}).render({
data: options,
index: that.index //索引
index: that.index, //索引
i18nMessages: {
'table_sort_asc': i18n.$t('table.sort.asc'),
'table_sort_desc': i18n.$t('table.sort.desc')
}
}));
// 初始化样式
@@ -650,7 +657,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
// 头部工具栏右上角默认工具
var defaultConfig = {
filter: {
title: '筛选列',
title: i18n.$t('table.tools.filter.title'),
layEvent: 'LAYTABLE_COLS',
icon: 'layui-icon-cols',
onClick: function(obj) {
@@ -703,7 +710,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
}
},
exports: {
title: '导出',
title: i18n.$t('table.tools.export.title'),
layEvent: 'LAYTABLE_EXPORT',
icon: 'layui-icon-export',
onClick: function(obj) { // 自带导出
@@ -712,16 +719,16 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
var openPanel = obj.openPanel;
var elem = obj.elem;
if (!data.length) return layer.tips('当前表格无数据', elem, {tips: 3});
if (!data.length) return layer.tips(i18n.$t('table.tools.export.noDataPrompt'), elem, {tips: 3});
if(device.ie){
layer.tips('导出功能不支持 IE请用 Chrome 等高级浏览器导出', elem, {
layer.tips(i18n.$t('table.tools.export.compatPrompt'), elem, {
tips: 3
});
} else {
openPanel({
list: function(){
return [
'<li data-type="csv">导出 CSV 文件</li>'
'<li data-type="csv">'+ i18n.$t('table.tools.export.csvText') +'</li>'
].join('')
}(),
done: function(panel, list){
@@ -735,7 +742,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
}
},
print: {
title: '打印',
title: i18n.$t('table.tools.print.title'),
layEvent: 'LAYTABLE_PRINT',
icon: 'layui-icon-print',
onClick: function(obj) {
@@ -743,7 +750,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
var options = obj.config;
var elem = obj.elem;
if (!data.length) return layer.tips('当前表格无数据', elem, {tips: 3});
if (!data.length) return layer.tips(i18n.$t('table.tools.print.noDataPrompt'), elem, {tips: 3});
var printWin = window.open('about:blank', '_blank');
var style = ['<style>',
'body{font-size: 12px; color: #5F5F5F;}',
@@ -1219,7 +1226,10 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
if(res[response.statusName] != response.statusCode){
that.errorView(
res[response.msgName] ||
('返回的数据不符合规范,正确的成功状态码应为:"'+ response.statusName +'": '+ response.statusCode)
i18n.$t('table.dataFormatError', {
statusName: response.statusName,
statusCode: response.statusCode
})
);
} else {
// 当前页不能超过总页数
@@ -1246,7 +1256,7 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports) {
that._xhrAbort = false;
return;
}
that.errorView('请求异常,错误提示:'+ msg);
that.errorView(i18n.$t('table.xhrError', {msg: msg}));
typeof options.error === 'function' && options.error(e, msg);
}
});

View File

@@ -2,12 +2,13 @@
* transfer 穿梭框组件
*/
layui.define(['laytpl', 'form'], function(exports) {
layui.define(['i18n', 'laytpl', 'form'], function(exports) {
"use strict";
var $ = layui.$;
var laytpl = layui.laytpl;
var form = layui.form;
var i18n = layui.i18n;
// 模块名
var MOD_NAME = 'transfer';
@@ -71,7 +72,7 @@ layui.define(['laytpl', 'form'], function(exports) {
var ELEM_DATA = 'layui-transfer-data';
// 穿梭框模板
var TPL_BOX = function(obj){
var TPL_BOX = function(obj) {
obj = obj || {};
return ['<div class="layui-transfer-box" data-index="'+ obj.index +'">',
'<div class="layui-transfer-header">',
@@ -80,54 +81,35 @@ layui.define(['laytpl', 'form'], function(exports) {
'{{# if(d.data.showSearch){ }}',
'<div class="layui-transfer-search">',
'<i class="layui-icon layui-icon-search"></i>',
'<input type="text" class="layui-input" placeholder="关键词搜索">',
'<input type="text" class="layui-input" placeholder="'+ i18n.$t('transfer.searchPlaceholder') +'">',
'</div>',
'{{# } }}',
'<ul class="layui-transfer-data"></ul>',
'</div>'].join('');
};
// 主模板
var TPL_MAIN = ['<div class="layui-transfer layui-form layui-border-box" lay-filter="LAY-transfer-{{= d.index }}">',
TPL_BOX({
index: 0,
checkAllName: 'layTransferLeftCheckAll'
}),
'<div class="layui-transfer-active">',
'<button type="button" class="layui-btn layui-btn-sm layui-btn-primary layui-btn-disabled" data-index="0">',
'<i class="layui-icon layui-icon-next"></i>',
'</button>',
'<button type="button" class="layui-btn layui-btn-sm layui-btn-primary layui-btn-disabled" data-index="1">',
'<i class="layui-icon layui-icon-prev"></i>',
'</button>',
'</div>',
TPL_BOX({
index: 1,
checkAllName: 'layTransferRightCheckAll'
}),
'</div>'].join('');
// 构造器
var Class = function(options){
var Class = function(options) {
var that = this;
that.index = ++transfer.index;
that.config = $.extend({}, that.config, transfer.config, options);
that.config = $.extend({
title: i18n.$t('transfer.title'),
text: {
none: i18n.$t('transfer.noData'),
searchNone: i18n.$t('transfer.noMatch')
}
}, that.config, transfer.config, options);
that.render();
};
// 默认配置
Class.prototype.config = {
title: ['列表一', '列表二'],
width: 200,
height: 360,
data: [], // 数据源
value: [], // 选中的数据
showSearch: false, // 是否开启搜索
id: '', // 唯一索引,默认自增 index
text: {
none: '无数据',
searchNone: '无匹配数据'
}
};
// 重载实例
@@ -142,6 +124,26 @@ layui.define(['laytpl', 'form'], function(exports) {
var that = this;
var options = that.config;
// 主模板
var TPL_MAIN = ['<div class="layui-transfer layui-form layui-border-box" lay-filter="LAY-transfer-{{= d.index }}">',
TPL_BOX({
index: 0,
checkAllName: 'layTransferLeftCheckAll'
}),
'<div class="layui-transfer-active">',
'<button type="button" class="layui-btn layui-btn-sm layui-btn-primary layui-btn-disabled" data-index="0">',
'<i class="layui-icon layui-icon-next"></i>',
'</button>',
'<button type="button" class="layui-btn layui-btn-sm layui-btn-primary layui-btn-disabled" data-index="1">',
'<i class="layui-icon layui-icon-prev"></i>',
'</button>',
'</div>',
TPL_BOX({
index: 1,
checkAllName: 'layTransferRightCheckAll'
}),
'</div>'].join('');
// 解析模板
var thisElem = that.elem = $(laytpl(TPL_MAIN, {
open: '{{', // 标签符前缀

View File

@@ -2,13 +2,14 @@
* tree 树组件
*/
layui.define(['form','util'], function(exports) {
layui.define(['i18n', 'form', 'util'], function(exports) {
"use strict";
var $ = layui.$;
var form = layui.form;
var layer = layui.layer;
var util = layui.util;
var i18n = layui.i18n;
// 模块名
var MOD_NAME = 'tree';
@@ -92,7 +93,12 @@ layui.define(['form','util'], function(exports) {
var Class = function(options){
var that = this;
that.index = ++tree.index;
that.config = $.extend({}, that.config, tree.config, options);
that.config = $.extend({
text: {
defaultNodeName: i18n.$t('tree.defaultNodeName'), // 节点默认名称
none: i18n.$t('tree.noData') // 数据为空时的文本提示
}
}, that.config, tree.config, options);
that.render();
};
@@ -106,11 +112,6 @@ layui.define(['form','util'], function(exports) {
onlyIconControl: false, // 是否仅允许节点左侧图标控制展开收缩
isJump: false, // 是否允许点击节点时弹出新窗口跳转
edit: false, // 是否开启节点的操作图标
text: {
defaultNodeName: '未命名', // 节点默认名称
none: '无数据' // 数据为空时的文本提示
}
};
// 重载实例
@@ -576,7 +577,10 @@ layui.define(['form','util'], function(exports) {
// 删除
} else {
layer.confirm('确认删除该节点 "<span style="color: #999;">'+ (item[customName.title] || '') +'</span>" 吗?', function(index){
var i18nText = i18n.$t('tree.deleteNodePrompt', {
name: item[customName.title] || ''
});
layer.confirm(i18nText, function(index){
options.operate && options.operate(returnObj); // 节点删除的回调
returnObj.status = 'remove'; // 标注节点删除

View File

@@ -400,7 +400,7 @@ layui.define(['table'], function (exports) {
Class.prototype.getTreeNode = function (data) {
var that = this;
if (!data) {
return hint.error('找不到节点数据');
return hint.error('Node data not found');
}
var options = that.getOptions();
var treeOptions = options.tree;
@@ -422,7 +422,7 @@ layui.define(['table'], function (exports) {
var that = this;
var treeNodeData = that.getNodeDataByIndex(index);
if (!treeNodeData) {
return hint.error('找不到节点数据');
return hint.error('Node data not found by index: ' + index);
}
var options = that.getOptions();
var treeOptions = options.tree;
@@ -886,7 +886,7 @@ layui.define(['table'], function (exports) {
* */
treeTable.expandAll = function (id, expandFlag) {
if (layui.type(expandFlag) !== 'boolean') {
return hint.error('expandAll 的展开状态参数只接收true/false')
return hint.error('treeTable.expandAll param "expandFlag" must be a boolean value.')
}
var that = getThisTable(id);

View File

@@ -3,13 +3,14 @@
* 上传组件
*/
layui.define(['lay', 'layer'], function(exports) {
layui.define(['lay', 'i18n', 'layer'], function(exports) {
"use strict";
var $ = layui.$;
var lay = layui.lay;
var layer = layui.layer;
var device = layui.device();
var i18n = layui.i18n;
// 模块名
var MOD_NAME = 'upload';
@@ -524,11 +525,11 @@ layui.define(['lay', 'layer'], function(exports) {
// 文件类型名称
var typeName = ({
file: '文件',
images: '图片',
video: '视频',
audio: '音频'
})[options.accept] || '文件';
file: i18n.$t('upload.fileType.file'),
images: i18n.$t('upload.fileType.image'),
video: i18n.$t('upload.fileType.video'),
audio: i18n.$t('upload.fileType.audio')
})[options.accept] || i18n.$t('upload.fileType.file');
// 校验文件格式
value = value.length === 0
@@ -572,7 +573,7 @@ layui.define(['lay', 'layer'], function(exports) {
// 校验失败提示
if(check){
that.msg(text['check-error'] || ('选择的'+ typeName +'中包含不支持的格式'));
that.msg(text['check-error'] || i18n.$t('upload.validateMessages.fileExtensionError', {fileType: typeName}));
return elemFile.value = '';
}
@@ -598,8 +599,9 @@ layui.define(['lay', 'layer'], function(exports) {
return that.msg(typeof text['limit-number'] === 'function'
? text['limit-number'](options, that.fileLength)
: (
'同时最多只能上传: '+ options.number + ' 个文件'
+'<br>您当前已经选择了: '+ that.fileLength +' 个文件'
i18n.$t('upload.validateMessages.filesOverLengthLimit', {length: options.number})
+ '<br/>'
+ i18n.$t('upload.validateMessages.currentFilesLength', {length: that.fileLength})
));
}
@@ -615,9 +617,10 @@ layui.define(['lay', 'layer'], function(exports) {
limitSize = size;
}
});
if(limitSize) return that.msg(typeof text['limit-size'] === 'function'
? text['limit-size'](options, limitSize)
: '文件大小不能超过 '+ limitSize);
if(limitSize) return that.msg(
typeof text['limit-size'] === 'function'
? text['limit-size'](options, limitSize)
: i18n.$t('upload.validateMessages.fileOverSizeLimit', {size: limitSize}));
}
send();
@@ -642,7 +645,7 @@ layui.define(['lay', 'layer'], function(exports) {
var elemFile = that.elemFile;
var item = options.item ? options.item : options.elem;
var value = files.length > 1
? files.length + '个文件'
? i18n.$t('upload.chooseText', {length: files.length})
: ((files[0] || {}).name || (elemFile[0].value.match(/[^\/\\]+\..+/g)||[]) || '');
if(elemFile.next().hasClass(ELEM_CHOOSE)){

View File

@@ -2,11 +2,18 @@
* util 工具组件
*/
layui.define('jquery', function(exports) {
layui.define(['lay', 'i18n', 'jquery'], function(exports) {
"use strict";
var $ = layui.$;
var hint = layui.hint();
var i18n = layui.i18n;
var lay = layui.lay;
// 引用自 dayjs
// https://github.com/iamkun/dayjs/blob/v1.11.9/src/constant.js#L30
var REGEX_FORMAT = /\[([^\]]+)]|y{1,4}|M{1,2}|d{1,2}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|SSS/g;
var REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[T\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/i;
// 外部接口
var util = {
@@ -230,15 +237,15 @@ layui.define('jquery', function(exports) {
// 30 天以内,返回「多久前」
if(stamp >= 1000*60*60*24){
return ((stamp/1000/60/60/24)|0) + ' 天前';
return i18n.$t('util.timeAgo.days', {days: (stamp/1000/60/60/24)|0});
} else if(stamp >= 1000*60*60){
return ((stamp/1000/60/60)|0) + ' 小时前';
return i18n.$t('util.timeAgo.hours', {hours: (stamp/1000/60/60)|0});
} else if(stamp >= 1000*60*3){ // 3 分钟以内为:刚刚
return ((stamp/1000/60)|0) + ' 分钟前';
return i18n.$t('util.timeAgo.minutes', {minutes: (stamp/1000/60)|0});
} else if(stamp < 0){
return '未来';
return i18n.$t('util.timeAgo.future');
} else {
return '刚刚';
return i18n.$t('util.timeAgo.justNow');
}
},
@@ -258,10 +265,6 @@ layui.define('jquery', function(exports) {
// 若 null 或空字符,则返回空字符
if(time === null || time === '') return '';
// 引用自 dayjs
// https://github.com/iamkun/dayjs/blob/v1.11.9/src/constant.js#L30
var REGEX_FORMAT = /\[([^\]]+)]|y{1,4}|M{1,2}|d{1,2}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|SSS/g;
var REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[T\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/i;
var that = this;
var normalizeDate = function(date) {
@@ -302,21 +305,7 @@ layui.define('jquery', function(exports) {
var seconds = date.getSeconds();
var milliseconds = date.getMilliseconds();
var defaultMeridiem = function(hours, minutes){
var hm = hours * 100 + minutes;
if (hm < 600) {
return '凌晨';
} else if (hm < 900) {
return '早上';
} else if (hm < 1100) {
return '上午';
} else if (hm < 1300) {
return '中午';
} else if (hm < 1800) {
return '下午';
}
return '晚上';
};
var defaultMeridiem = i18n.$t('util.toDateString.meridiem');
var meridiem = (options && options.customMeridiem) || defaultMeridiem;
@@ -347,26 +336,10 @@ layui.define('jquery', function(exports) {
},
// 转义 html
escape: function(html){
var exp = /[<"'>]|&(?=#?[a-zA-Z0-9]+)/g;
if (html === undefined || html === null) return '';
html += '';
if (!exp.test(html)) return html;
return html.replace(/&(?=#?[a-zA-Z0-9]+;?)/g, '&amp;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/'/g, '&#39;').replace(/"/g, '&quot;');
},
escape: lay.escape,
// 还原转义的 html
unescape: function(html){
if (html === undefined || html === null) return '';
return String(html).replace(/\&quot;/g, '"').replace(/\&#39;/g, '\'')
.replace(/\&gt;/g, '>').replace(/\&lt;/g, '<')
.replace(/\&amp;/g, '&');
},
unescape: lay.unescape,
// 打开新窗口
openWin: function(options){