@@ -937,3 +937,104 @@ function formatEditorCode(editor) {
937937 // 移动光标到目标行末尾(Ace 的 gotoLine 行号是 1-based)
938938 editor . gotoLine ( targetRow + 1 , lineEndColumn , false ) ;
939939}
940+
941+ /**
942+ * 在页面右下角显示一条临时消息提示(Toast)
943+ *
944+ * @param {string } message - 要显示的消息文本,必填
945+ * @param {Object } [options] - 可选配置项
946+ * @param {number } [options.duration=3000] - 消息自动消失的毫秒数,默认 3000ms
947+ * @param {string } [options.type='info'] - 消息类型,可选 'info' | 'success' | 'warning' | 'error'
948+ * @param {boolean } [options.allowMultiple=false] - 是否允许多条消息同时存在
949+ * @returns {void }
950+ * @throws {TypeError } 当 message 不是字符串时抛出错误
951+ */
952+ function showToast ( message , options = { } ) {
953+ // 参数校验
954+ if ( typeof message !== 'string' ) {
955+ throw new TypeError ( 'Parameter "message" must be a string.' ) ;
956+ }
957+ try {
958+ // 默认配置
959+ const config = {
960+ duration : 3000 ,
961+ type : 'info' ,
962+ allowMultiple : false ,
963+ ...options ,
964+ } ;
965+ const validTypes = [ 'info' , 'success' , 'warning' , 'error' ] ;
966+ if ( ! validTypes . includes ( config . type ) ) {
967+ console . warn ( `不存在的消息类型"${ config . type } "` ) ;
968+ config . type = 'info' ;
969+ }
970+ let toastContainer = document . getElementById ( '__toast-container' ) ;
971+ if ( ! toastContainer ) {
972+ toastContainer = document . createElement ( 'div' ) ;
973+ toastContainer . id = '__toast-container' ;
974+ toastContainer . style . cssText = `
975+ position: fixed;
976+ bottom: 20px;
977+ right: 20px;
978+ z-index: 10000;
979+ max-width: 80vw;
980+ pointer-events: none;
981+ ` ;
982+ document . body . appendChild ( toastContainer ) ;
983+ }
984+ if ( ! config . allowMultiple ) {
985+ toastContainer . innerHTML = '' ;
986+ }
987+ const toastEl = document . createElement ( 'div' ) ;
988+ toastEl . textContent = message ;
989+ toastEl . style . cssText = `
990+ background: ${
991+ config . type === 'success' ? '#4caf50' : config . type === 'warning' ? '#ff9800' : config . type === 'error' ? '#f44336' : '#333'
992+ } ;
993+ color: white;
994+ padding: 12px 16px;
995+ border-radius: 6px;
996+ margin-top: 8px;
997+ font-size: 14px;
998+ line-height: 1.4;
999+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1000+ opacity: 0;
1001+ transform: translateY(20px);
1002+ transition: opacity 0.3s ease, transform 0.3s ease;
1003+ animation: __toast-fade-in 0.3s forwards;
1004+ pointer-events: auto;
1005+ word-break: break-word;
1006+ ` ;
1007+ if ( ! document . getElementById ( '__toast-styles' ) ) {
1008+ const styleEl = document . createElement ( 'style' ) ;
1009+ styleEl . id = '__toast-styles' ;
1010+ styleEl . textContent = `
1011+ @keyframes __toast-fade-in {
1012+ to { opacity: 1; transform: translateY(0); }
1013+ }
1014+ @keyframes __toast-fade-out {
1015+ to { opacity: 0; transform: translateY(20px); }
1016+ }
1017+ ` ;
1018+ document . head . appendChild ( styleEl ) ;
1019+ }
1020+ toastContainer . appendChild ( toastEl ) ;
1021+ requestAnimationFrame ( ( ) => {
1022+ toastEl . style . animation = '__toast-fade-in 0.3s forwards' ;
1023+ } ) ;
1024+ const removeToast = ( ) => {
1025+ if ( ! toastEl . parentNode ) return ;
1026+ toastEl . style . animation = '__toast-fade-out 0.3s forwards' ;
1027+ setTimeout ( ( ) => {
1028+ if ( toastEl . parentNode ) {
1029+ toastEl . parentNode . removeChild ( toastEl ) ;
1030+ }
1031+ } , 300 ) ;
1032+ } ;
1033+ if ( config . duration > 0 ) {
1034+ setTimeout ( removeToast , config . duration ) ;
1035+ }
1036+ toastEl . addEventListener ( 'click' , removeToast , { once : true } ) ;
1037+ } catch ( error ) {
1038+ console . error ( '显示消息异常' , error ) ;
1039+ }
1040+ }
0 commit comments