n20-project-component 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- (function(t,e){"object"===typeof exports&&"object"===typeof module?module.exports=e():"function"===typeof define&&define.amd?define([],e):"object"===typeof exports?exports["n20-project-component"]=e():t["n20-project-component"]=e()})("undefined"!==typeof self?self:this,(function(){return function(t){var e={};function n(o){if(e[o])return e[o].exports;var i=e[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(o,i,function(e){return t[e]}.bind(null,i));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="fb15")}({"00be":function(t,e,n){},"021e":function(t,e,n){"use strict";n("f14e")},d72c:function(t,e,n){"use strict";n("00be")},f14e:function(t,e,n){},fb15:function(t,e,n){"use strict";if(n.r(e),n.d(e,"DemoButton",(function(){return d})),n.d(e,"N20CopyText",(function(){return h})),"undefined"!==typeof window){var o=window.document.currentScript,i=o&&o.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);i&&(n.p=i[1])}var r=function(){var t=this,e=t._self._c;return e("el-button",{staticClass:"n20-demo-button",attrs:{type:t.type,size:t.size,disabled:t.disabled,loading:t.loading,icon:t.icon},on:{click:t.handleClick}},[t._t("default",(function(){return[t._v(t._s(t.text))]}))],2)},c=[],s={name:"N20DemoButton",props:{text:{type:String,default:"按钮"},type:{type:String,default:"primary"},size:{type:String,default:""},disabled:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},icon:{type:String,default:""}},methods:{handleClick(t){this.$emit("click",t)}}},a=s;n("021e");function l(t,e,n,o,i,r,c,s){var a,l="function"===typeof t?t.options:t;if(e&&(l.render=e,l.staticRenderFns=n,l._compiled=!0),o&&(l.functional=!0),r&&(l._scopeId="data-v-"+r),c?(a=function(t){t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,t||"undefined"===typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),i&&i.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(c)},l._ssrRegister=a):i&&(a=s?function(){i.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:i),a)if(l.functional){l._injectStyles=a;var u=l.render;l.render=function(t,e){return a.call(e),u(t,e)}}else{var d=l.beforeCreate;l.beforeCreate=d?[].concat(d,a):[a]}return{exports:t,options:l}}var u=l(a,r,c,!1,null,"1c0b7107",null),d=u.exports,f=function(){var t=this,e=t._self._c;return e("span",{staticClass:"n20-copy-text",on:{click:function(e){return e.stopPropagation(),t.handleCopy.apply(null,arguments)}}},[e("span",{ref:"content",staticClass:"n20-copy-text__content"},[t._t("default",(function(){return[t._v(t._s(t.text))]}))],2),t.showIcon?e("i",{class:["n20-copy-text__icon",t.copied?"el-icon-check":"el-icon-document-copy"],style:{order:"left"===t.iconPosition?-1:1}}):t._e(),e("transition",{attrs:{name:"n20-copy-fade"}},[t.copied?e("span",{staticClass:"n20-copy-text__tip"},[t._v(t._s(t.successTip))]):t._e()])],1)},p=[],y={name:"N20CopyText",props:{text:{type:[String,Number],default:""},successTip:{type:String,default:"已复制"},showIcon:{type:Boolean,default:!0},iconPosition:{type:String,default:"right",validator:t=>["left","right"].includes(t)},disabled:{type:Boolean,default:!1}},data(){return{copied:!1,timer:null}},beforeDestroy(){clearTimeout(this.timer)},methods:{async handleCopy(){if(this.disabled||this.copied)return;const t=this.text||this.$refs.content&&this.$refs.content.innerText||"";if(t)try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(String(t));else{const e=document.createElement("textarea");e.value=String(t),e.style.position="fixed",e.style.left="-9999px",document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}this.copied=!0,this.$emit("copy",String(t)),this.timer=setTimeout(()=>{this.copied=!1},2e3)}catch(e){this.$emit("error",e)}}}},m=y,_=(n("d72c"),l(m,f,p,!1,null,"540c418a",null)),h=_.exports;const b={DemoButton:d,N20CopyText:h},x=t=>{x.installed||(x.installed=!0,Object.values(b).forEach(e=>{t.component(e.name,e)}))};"undefined"!==typeof window&&window.Vue&&x(window.Vue);var v={install:x,...b};e["default"]=v}})}));
1
+ (function(t,e){"object"===typeof exports&&"object"===typeof module?module.exports=e():"function"===typeof define&&define.amd?define([],e):"object"===typeof exports?exports["n20-project-component"]=e():t["n20-project-component"]=e()})("undefined"!==typeof self?self:this,(function(){return function(t){var e={};function n(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(i,o,function(e){return t[e]}.bind(null,o));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="fb15")}({"021e":function(t,e,n){"use strict";n("f14e")},"141c":function(t,e,n){},"3f46":function(t,e,n){"use strict";n("c5db")},b006:function(t,e,n){"use strict";n("141c")},b553:function(t,e,n){},c5db:function(t,e,n){},e863:function(t,e,n){"use strict";n("b553")},f14e:function(t,e,n){},fb15:function(t,e,n){"use strict";if(n.r(e),n.d(e,"DemoButton",(function(){return u})),n.d(e,"N20CopyText",(function(){return y})),n.d(e,"N20FloatingToolbar",(function(){return w})),n.d(e,"N20BatchInput",(function(){return j})),"undefined"!==typeof window){var i=window.document.currentScript,o=i&&i.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);o&&(n.p=o[1])}var s=function(){var t=this,e=t._self._c;return e("el-button",{staticClass:"n20-demo-button",attrs:{type:t.type,size:t.size,disabled:t.disabled,loading:t.loading,icon:t.icon},on:{click:t.handleClick}},[t._t("default",(function(){return[t._v(t._s(t.text))]}))],2)},a=[],l={name:"N20DemoButton",props:{text:{type:String,default:"按钮"},type:{type:String,default:"primary"},size:{type:String,default:""},disabled:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},icon:{type:String,default:""}},methods:{handleClick(t){this.$emit("click",t)}}},r=l;n("021e");function c(t,e,n,i,o,s,a,l){var r,c="function"===typeof t?t.options:t;if(e&&(c.render=e,c.staticRenderFns=n,c._compiled=!0),i&&(c.functional=!0),s&&(c._scopeId="data-v-"+s),a?(r=function(t){t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,t||"undefined"===typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),o&&o.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},c._ssrRegister=r):o&&(r=l?function(){o.call(this,(c.functional?this.parent:this).$root.$options.shadowRoot)}:o),r)if(c.functional){c._injectStyles=r;var d=c.render;c.render=function(t,e){return r.call(e),d(t,e)}}else{var u=c.beforeCreate;c.beforeCreate=u?[].concat(u,r):[r]}return{exports:t,options:c}}var d=c(r,s,a,!1,null,"1c0b7107",null),u=d.exports,p=function(){var t=this,e=t._self._c;return e("span",{staticClass:"n20-copy-text",on:{click:function(e){return e.stopPropagation(),t.handleCopy.apply(null,arguments)}}},[e("span",{ref:"content",staticClass:"n20-copy-text__content"},[t._t("default",(function(){return[t._v(t._s(t.text))]}))],2),t.showIcon?e("i",{class:["n20-copy-text__icon",t.copied?"el-icon-check":"el-icon-document-copy"],style:{order:"left"===t.iconPosition?-1:1}}):t._e()])},f=[],h={name:"N20CopyText",props:{text:{type:[String,Number],default:""},successTip:{type:String,default:"已复制"},showIcon:{type:Boolean,default:!0},iconPosition:{type:String,default:"right",validator:t=>["left","right"].includes(t)},disabled:{type:Boolean,default:!1}},data(){return{copied:!1,timer:null}},beforeDestroy(){clearTimeout(this.timer)},methods:{async handleCopy(){if(this.disabled||this.copied)return;const t=this.text||this.$refs.content&&this.$refs.content.innerText||"";if(t)try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(String(t));else{const e=document.createElement("textarea");e.value=String(t),e.style.position="fixed",e.style.left="-9999px",document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}this.copied=!0,this.$message.success(this.successTip),this.$emit("copy",String(t)),this.timer=setTimeout(()=>{this.copied=!1},2e3)}catch(e){this.$message.error("复制失败"),this.$emit("error",e)}}}},m=h,b=(n("3f46"),c(m,p,f,!1,null,"35583176",null)),y=b.exports,_=function(){var t=this,e=t._self._c;return e("div",{ref:"wrapper",staticClass:"n20-floating-toolbar-wrapper",on:{mouseup:t.handleMouseUp}},[t._t("default"),e("transition",{attrs:{name:"n20-toolbar-fade"}},[t.visible?e("div",{ref:"toolbar",staticClass:"n20-floating-toolbar",style:t.toolbarStyle,on:{mousedown:function(t){t.preventDefault()}}},[e("div",{staticClass:"n20-floating-toolbar__arrow"}),e("div",{staticClass:"n20-floating-toolbar__body"},[t._l(t.actions,(function(n,i){return e("button",{key:i,staticClass:"n20-floating-toolbar__btn",attrs:{title:n.label},on:{click:function(e){return e.stopPropagation(),t.handleAction(n,i)}}},[n.icon?e("i",{class:n.icon}):t._e(),n.label&&t.showLabel?e("span",[t._v(t._s(n.label))]):t._e()])})),t._t("toolbar",null,{selectedText:t.selectedText})],2)]):t._e()])],2)},g=[],v={name:"N20FloatingToolbar",props:{actions:{type:Array,default:()=>[{label:"复制",icon:"el-icon-document-copy",command:"copy"},{label:"搜索",icon:"el-icon-search",command:"search"},{label:"高亮",icon:"el-icon-edit",command:"highlight"}]},showLabel:{type:Boolean,default:!0},offset:{type:Number,default:8},disabled:{type:Boolean,default:!1},minLength:{type:Number,default:1}},data(){return{visible:!1,selectedText:"",toolbarStyle:{top:"0px",left:"0px"}}},mounted(){this._onDocClick=t=>{this.visible&&this.$refs.toolbar&&!this.$refs.toolbar.contains(t.target)&&this.hide()},this._onScroll=()=>{this.visible&&this.hide()},document.addEventListener("mousedown",this._onDocClick,!0),window.addEventListener("scroll",this._onScroll,!0)},beforeDestroy(){document.removeEventListener("mousedown",this._onDocClick,!0),window.removeEventListener("scroll",this._onScroll,!0)},methods:{handleMouseUp(){this.disabled||this.$nextTick(()=>{setTimeout(()=>{this.checkSelection()},0)})},checkSelection(){const t=window.getSelection();if(!t||t.isCollapsed||!t.toString().trim())return void this.hide();const e=t.toString().trim();if(e.length<this.minLength)return void this.hide();const n=this.$refs.wrapper;if(!n)return;const i=t.getRangeAt(0);n.contains(i.commonAncestorContainer)?(this.selectedText=e,this.positionToolbar(i),this.visible=!0):this.hide()},positionToolbar(t){const e=t.getBoundingClientRect(),n=this.$refs.wrapper.getBoundingClientRect(),i=e.left+e.width/2-n.left,o=e.top-n.top-this.offset;this.toolbarStyle={left:i+"px",top:o+"px"}},handleAction(t,e){this.$emit("action",{command:t.command||t.label,label:t.label,index:e,text:this.selectedText}),"copy"===t.command&&this.doCopy(this.selectedText),this.hide()},async doCopy(t){try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(t);else{const e=document.createElement("textarea");e.value=t,e.style.position="fixed",e.style.left="-9999px",document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}this.$message&&this.$message.success("已复制")}catch(e){this.$message&&this.$message.error("复制失败")}},hide(){this.visible=!1,this.selectedText=""},show(){this.checkSelection()}}},x=v,C=(n("e863"),c(x,_,g,!1,null,"1b434c24",null)),w=C.exports,T=function(){var t=this,e=t._self._c;return e("div",{staticClass:"n20-batch-input"},[e("el-input",{staticClass:"n20-batch-input__trigger",attrs:{value:t.displayValue,placeholder:t.placeholder,disabled:t.disabled,readonly:""},nativeOn:{click:function(e){return t.handleOpen.apply(null,arguments)}}},[e("template",{slot:"suffix"},[e("span",{staticClass:"n20-batch-input__suffix"},[t.value&&!t.disabled?e("i",{staticClass:"el-icon-circle-close n20-batch-input__clear",on:{click:function(e){return e.stopPropagation(),t.handleClear.apply(null,arguments)}}}):t._e(),e("i",{staticClass:"el-icon-edit-outline n20-batch-input__edit",on:{click:function(e){return e.stopPropagation(),t.handleOpen.apply(null,arguments)}}})])])],2),e("el-dialog",{attrs:{title:t.dialogTitle,visible:t.dialogVisible,width:"520px","close-on-click-modal":!1},on:{"update:visible":function(e){t.dialogVisible=e},open:t.handleDialogOpen,closed:t.handleDialogClosed}},[e("div",{staticClass:"n20-batch-input__body"},[e("el-input",{attrs:{type:"textarea",rows:10,placeholder:t.textareaPlaceholder},on:{input:t.handleTextareaInput},model:{value:t.draftText,callback:function(e){t.draftText=e},expression:"draftText"}}),e("div",{staticClass:"n20-batch-input__counter",class:{"is-over":t.isOverLimit}},[t.isOverLimit?e("span",{staticClass:"n20-batch-input__error"},[t._v(" 已超出最大条数 "+t._s(t.max)+",请删减后提交 ")]):t._e(),e("span",{staticClass:"n20-batch-input__count"},[t._v(" 当前 "+t._s(t.parsedItems.length)+" 条 / 最大 "+t._s(t.max)+" 条 ")])])],1),e("div",{attrs:{slot:"footer"},slot:"footer"},[e("el-button",{on:{click:function(e){t.dialogVisible=!1}}},[t._v("取 消")]),e("el-button",{attrs:{type:"primary",disabled:t.isOverLimit},on:{click:t.handleConfirm}},[t._v(" 确 认 ")])],1)])],1)},S=[],$={name:"N20BatchInput",model:{prop:"value",event:"input"},props:{value:{type:String,default:""},max:{type:Number,default:100},placeholder:{type:String,default:"请输入"},dialogTitle:{type:String,default:"批量输入"},textareaPlaceholder:{type:String,default:"支持从 Excel 粘贴,每行 / 逗号 / Tab 自动识别为一条"},disabled:{type:Boolean,default:!1}},data(){return{dialogVisible:!1,draftText:"",parsedItems:[]}},computed:{displayValue(){return this.value||""},isOverLimit(){return this.parsedItems.length>this.max}},methods:{handleOpen(){this.disabled||(this.dialogVisible=!0)},handleDialogOpen(){if(this.value){const t=this.value.split(",").map(t=>t.trim()).filter(Boolean);this.draftText=t.join("\n"),this.parsedItems=t}else this.draftText="",this.parsedItems=[]},handleDialogClosed(){this.draftText="",this.parsedItems=[]},handleTextareaInput(t){const e=t.split(/[\n\r\t,,]+/).map(t=>t.trim()).filter(Boolean);this.parsedItems=e},handleConfirm(){if(this.isOverLimit)return;const t=this.parsedItems.join(",");this.$emit("input",t),this.$emit("change",t),this.dialogVisible=!1},handleClear(){this.$emit("input",""),this.$emit("change","")}}},k=$,O=(n("b006"),c(k,T,S,!1,null,"081f8b8a",null)),j=O.exports;const B={DemoButton:u,N20CopyText:y,N20FloatingToolbar:w,N20BatchInput:j},I=t=>{I.installed||(I.installed=!0,Object.values(B).forEach(e=>{t.component(e.name,e)}))};"undefined"!==typeof window&&window.Vue&&I(window.Vue);var N={install:I,...B};e["default"]=N}})}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n20-project-component",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "PC 端 Vue 2 + Element UI 组件库",
5
5
  "main": "dist/n20-project-component.umd.min.js",
6
6
  "module": "dist/n20-project-component.common.js",
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <div class="n20-batch-input">
3
+ <el-input
4
+ :value="displayValue"
5
+ :placeholder="placeholder"
6
+ :disabled="disabled"
7
+ readonly
8
+ class="n20-batch-input__trigger"
9
+ @click.native="handleOpen"
10
+ >
11
+ <template slot="suffix">
12
+ <span class="n20-batch-input__suffix">
13
+ <i
14
+ v-if="value && !disabled"
15
+ class="el-icon-circle-close n20-batch-input__clear"
16
+ @click.stop="handleClear"
17
+ />
18
+ <i
19
+ class="el-icon-edit-outline n20-batch-input__edit"
20
+ @click.stop="handleOpen"
21
+ />
22
+ </span>
23
+ </template>
24
+ </el-input>
25
+
26
+ <el-dialog
27
+ :title="dialogTitle"
28
+ :visible.sync="dialogVisible"
29
+ width="520px"
30
+ :close-on-click-modal="false"
31
+ @open="handleDialogOpen"
32
+ @closed="handleDialogClosed"
33
+ >
34
+ <div class="n20-batch-input__body">
35
+ <el-input
36
+ v-model="draftText"
37
+ type="textarea"
38
+ :rows="10"
39
+ :placeholder="textareaPlaceholder"
40
+ @input="handleTextareaInput"
41
+ />
42
+ <div
43
+ class="n20-batch-input__counter"
44
+ :class="{ 'is-over': isOverLimit }"
45
+ >
46
+ <span v-if="isOverLimit" class="n20-batch-input__error">
47
+ 已超出最大条数 {{ max }},请删减后提交
48
+ </span>
49
+ <span class="n20-batch-input__count">
50
+ 当前 {{ parsedItems.length }} 条 / 最大 {{ max }} 条
51
+ </span>
52
+ </div>
53
+ </div>
54
+ <div slot="footer">
55
+ <el-button @click="dialogVisible = false">取 消</el-button>
56
+ <el-button
57
+ type="primary"
58
+ :disabled="isOverLimit"
59
+ @click="handleConfirm"
60
+ >
61
+ 确 认
62
+ </el-button>
63
+ </div>
64
+ </el-dialog>
65
+ </div>
66
+ </template>
67
+
68
+ <script>
69
+ /**
70
+ * N20BatchInput - 批量输入组件
71
+ *
72
+ * 点击输入框打开弹窗,支持从 Excel 粘贴内容,自动按换行/制表符/逗号拆分为多个条目。
73
+ * v-model 绑定逗号分隔的字符串。
74
+ *
75
+ * @example
76
+ * <N20BatchInput v-model="batchValue" :max="50" />
77
+ */
78
+ export default {
79
+ name: 'N20BatchInput',
80
+ model: {
81
+ prop: 'value',
82
+ event: 'input',
83
+ },
84
+ props: {
85
+ /** v-model 绑定值,逗号分隔字符串 */
86
+ value: {
87
+ type: String,
88
+ default: '',
89
+ },
90
+ /** 最大条数限制 */
91
+ max: {
92
+ type: Number,
93
+ default: 100,
94
+ },
95
+ /** 输入框占位符 */
96
+ placeholder: {
97
+ type: String,
98
+ default: '请输入',
99
+ },
100
+ /** 弹窗标题 */
101
+ dialogTitle: {
102
+ type: String,
103
+ default: '批量输入',
104
+ },
105
+ /** 文本域占位符 */
106
+ textareaPlaceholder: {
107
+ type: String,
108
+ default: '支持从 Excel 粘贴,每行 / 逗号 / Tab 自动识别为一条',
109
+ },
110
+ /** 是否禁用 */
111
+ disabled: {
112
+ type: Boolean,
113
+ default: false,
114
+ },
115
+ },
116
+ data() {
117
+ return {
118
+ dialogVisible: false,
119
+ draftText: '',
120
+ parsedItems: [],
121
+ }
122
+ },
123
+ computed: {
124
+ displayValue() {
125
+ return this.value || ''
126
+ },
127
+ isOverLimit() {
128
+ return this.parsedItems.length > this.max
129
+ },
130
+ },
131
+ methods: {
132
+ handleOpen() {
133
+ if (this.disabled) return
134
+ this.dialogVisible = true
135
+ },
136
+ handleDialogOpen() {
137
+ // 将当前 value 还原为每行一条
138
+ if (this.value) {
139
+ const items = this.value.split(',').map(s => s.trim()).filter(Boolean)
140
+ this.draftText = items.join('\n')
141
+ this.parsedItems = items
142
+ } else {
143
+ this.draftText = ''
144
+ this.parsedItems = []
145
+ }
146
+ },
147
+ handleDialogClosed() {
148
+ this.draftText = ''
149
+ this.parsedItems = []
150
+ },
151
+ handleTextareaInput(val) {
152
+ const items = val
153
+ .split(/[\n\r\t,,]+/)
154
+ .map(s => s.trim())
155
+ .filter(Boolean)
156
+ this.parsedItems = items
157
+ },
158
+ handleConfirm() {
159
+ if (this.isOverLimit) return
160
+ const result = this.parsedItems.join(',')
161
+ this.$emit('input', result)
162
+ this.$emit('change', result)
163
+ this.dialogVisible = false
164
+ },
165
+ handleClear() {
166
+ this.$emit('input', '')
167
+ this.$emit('change', '')
168
+ },
169
+ },
170
+ }
171
+ </script>
172
+
173
+ <style scoped>
174
+ .n20-batch-input {
175
+ display: inline-block;
176
+ width: 100%;
177
+ }
178
+
179
+ .n20-batch-input__trigger {
180
+ cursor: pointer;
181
+ }
182
+
183
+ .n20-batch-input__trigger /deep/ .el-input__inner {
184
+ cursor: pointer;
185
+ overflow: hidden;
186
+ text-overflow: ellipsis;
187
+ white-space: nowrap;
188
+ padding-right: 52px;
189
+ }
190
+
191
+ .n20-batch-input__suffix {
192
+ display: inline-flex;
193
+ align-items: center;
194
+ height: 100%;
195
+ gap: 4px;
196
+ }
197
+
198
+ .n20-batch-input__clear:hover {
199
+ color: #909399;
200
+ }
201
+
202
+ .n20-batch-input__edit {
203
+ color: #c0c4cc;
204
+ cursor: pointer;
205
+ }
206
+
207
+ .n20-batch-input__edit:hover {
208
+ color: #409eff;
209
+ }
210
+
211
+ .n20-batch-input__body {
212
+ padding: 0 4px;
213
+ }
214
+
215
+ .n20-batch-input__counter {
216
+ display: flex;
217
+ justify-content: flex-end;
218
+ align-items: center;
219
+ margin-top: 8px;
220
+ font-size: 13px;
221
+ color: #909399;
222
+ gap: 12px;
223
+ }
224
+
225
+ .n20-batch-input__counter.is-over {
226
+ color: #f56c6c;
227
+ }
228
+
229
+ .n20-batch-input__error {
230
+ flex: 1;
231
+ color: #f56c6c;
232
+ }
233
+
234
+ .n20-batch-input__count {
235
+ white-space: nowrap;
236
+ }
237
+ </style>
@@ -8,9 +8,6 @@
8
8
  :class="['n20-copy-text__icon', copied ? 'el-icon-check' : 'el-icon-document-copy']"
9
9
  :style="{ order: iconPosition === 'left' ? -1 : 1 }"
10
10
  />
11
- <transition name="n20-copy-fade">
12
- <span v-if="copied" class="n20-copy-text__tip">{{ successTip }}</span>
13
- </transition>
14
11
  </span>
15
12
  </template>
16
13
 
@@ -18,7 +15,7 @@
18
15
  /**
19
16
  * N20CopyText - 点击复制文本组件
20
17
  *
21
- * 点击后将指定文本复制到剪贴板,显示成功反馈。
18
+ * 点击后将指定文本复制到剪贴板,通过 Element Message 显示成功反馈。
22
19
  *
23
20
  * @example
24
21
  * <N20CopyText :text="row.billNo">{{ row.billNo }}</N20CopyText>
@@ -89,12 +86,14 @@ export default {
89
86
  }
90
87
 
91
88
  this.copied = true
89
+ this.$message.success(this.successTip)
92
90
  this.$emit('copy', String(copyText))
93
91
 
94
92
  this.timer = setTimeout(() => {
95
93
  this.copied = false
96
94
  }, 2000)
97
95
  } catch (err) {
96
+ this.$message.error('复制失败')
98
97
  this.$emit('error', err)
99
98
  }
100
99
  }
@@ -108,7 +107,6 @@ export default {
108
107
  align-items: center;
109
108
  gap: 4px;
110
109
  cursor: pointer;
111
- position: relative;
112
110
 
113
111
  &:hover {
114
112
  .n20-copy-text__icon {
@@ -131,41 +129,5 @@ export default {
131
129
  opacity: 1;
132
130
  }
133
131
  }
134
-
135
- &__tip {
136
- position: absolute;
137
- top: -28px;
138
- left: 50%;
139
- transform: translateX(-50%);
140
- background: #67C23A;
141
- color: #fff;
142
- font-size: 12px;
143
- padding: 2px 8px;
144
- border-radius: 4px;
145
- white-space: nowrap;
146
- pointer-events: none;
147
-
148
- &::after {
149
- content: '';
150
- position: absolute;
151
- bottom: -4px;
152
- left: 50%;
153
- transform: translateX(-50%);
154
- border-left: 4px solid transparent;
155
- border-right: 4px solid transparent;
156
- border-top: 4px solid #67C23A;
157
- }
158
- }
159
- }
160
-
161
- .n20-copy-fade-enter-active,
162
- .n20-copy-fade-leave-active {
163
- transition: opacity 0.3s ease, transform 0.3s ease;
164
- }
165
-
166
- .n20-copy-fade-enter,
167
- .n20-copy-fade-leave-to {
168
- opacity: 0;
169
- transform: translateX(-50%) translateY(4px);
170
132
  }
171
133
  </style>
@@ -0,0 +1,318 @@
1
+ <template>
2
+ <div
3
+ class="n20-floating-toolbar-wrapper"
4
+ ref="wrapper"
5
+ @mouseup="handleMouseUp"
6
+ >
7
+ <!-- 包裹的内容区域 -->
8
+ <slot></slot>
9
+
10
+ <!-- 浮现工具条 -->
11
+ <transition name="n20-toolbar-fade">
12
+ <div
13
+ v-if="visible"
14
+ ref="toolbar"
15
+ class="n20-floating-toolbar"
16
+ :style="toolbarStyle"
17
+ @mousedown.prevent
18
+ >
19
+ <div class="n20-floating-toolbar__arrow"></div>
20
+ <div class="n20-floating-toolbar__body">
21
+ <button
22
+ v-for="(btn, idx) in actions"
23
+ :key="idx"
24
+ class="n20-floating-toolbar__btn"
25
+ :title="btn.label"
26
+ @click.stop="handleAction(btn, idx)"
27
+ >
28
+ <i v-if="btn.icon" :class="btn.icon"></i>
29
+ <span v-if="btn.label && showLabel">{{ btn.label }}</span>
30
+ </button>
31
+ <!-- 允许通过 slot 自定义工具栏内容 -->
32
+ <slot name="toolbar" :selectedText="selectedText"></slot>
33
+ </div>
34
+ </div>
35
+ </transition>
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ /**
41
+ * N20FloatingToolbar — 选中浮现工具栏
42
+ *
43
+ * 包裹内容区域,用户选中文字后在选区上方浮现操作按钮条。
44
+ * 鼠标拖选文字松开后,选区上方浮现深色工具条,
45
+ * 点击按钮触发事件,点其他地方消失。
46
+ *
47
+ * @example
48
+ * <N20FloatingToolbar
49
+ * :actions="[
50
+ * { label: '复制', icon: 'el-icon-document-copy' },
51
+ * { label: '搜索', icon: 'el-icon-search' },
52
+ * { label: '高亮', icon: 'el-icon-edit' },
53
+ * ]"
54
+ * @action="handleToolbarAction"
55
+ * >
56
+ * <p>这里是一段可以被选中的文本内容...</p>
57
+ * </N20FloatingToolbar>
58
+ */
59
+ export default {
60
+ name: 'N20FloatingToolbar',
61
+ props: {
62
+ /**
63
+ * 工具栏按钮列表
64
+ * 每项 { label: string, icon?: string, command?: string }
65
+ */
66
+ actions: {
67
+ type: Array,
68
+ default: () => [
69
+ { label: '复制', icon: 'el-icon-document-copy', command: 'copy' },
70
+ { label: '搜索', icon: 'el-icon-search', command: 'search' },
71
+ { label: '高亮', icon: 'el-icon-edit', command: 'highlight' },
72
+ ]
73
+ },
74
+ /** 是否在按钮中显示文字标签 */
75
+ showLabel: {
76
+ type: Boolean,
77
+ default: true
78
+ },
79
+ /** 工具栏与选区的垂直间距(px) */
80
+ offset: {
81
+ type: Number,
82
+ default: 8
83
+ },
84
+ /** 是否禁用 */
85
+ disabled: {
86
+ type: Boolean,
87
+ default: false
88
+ },
89
+ /** 选中最少字符数才触发 */
90
+ minLength: {
91
+ type: Number,
92
+ default: 1
93
+ }
94
+ },
95
+ data() {
96
+ return {
97
+ visible: false,
98
+ selectedText: '',
99
+ toolbarStyle: {
100
+ top: '0px',
101
+ left: '0px'
102
+ }
103
+ }
104
+ },
105
+ mounted() {
106
+ // 监听全局点击 → 隐藏工具栏
107
+ this._onDocClick = (e) => {
108
+ if (this.visible && this.$refs.toolbar && !this.$refs.toolbar.contains(e.target)) {
109
+ this.hide()
110
+ }
111
+ }
112
+ // 监听滚动 → 隐藏工具栏
113
+ this._onScroll = () => {
114
+ if (this.visible) {
115
+ this.hide()
116
+ }
117
+ }
118
+ document.addEventListener('mousedown', this._onDocClick, true)
119
+ window.addEventListener('scroll', this._onScroll, true)
120
+ },
121
+ beforeDestroy() {
122
+ document.removeEventListener('mousedown', this._onDocClick, true)
123
+ window.removeEventListener('scroll', this._onScroll, true)
124
+ },
125
+ methods: {
126
+ /** 鼠标松开时判断是否有选区 */
127
+ handleMouseUp() {
128
+ if (this.disabled) return
129
+
130
+ // 延迟一帧让浏览器完成选区更新
131
+ this.$nextTick(() => {
132
+ setTimeout(() => {
133
+ this.checkSelection()
134
+ }, 0)
135
+ })
136
+ },
137
+
138
+ /** 检测当前选区 */
139
+ checkSelection() {
140
+ const selection = window.getSelection()
141
+ if (!selection || selection.isCollapsed || !selection.toString().trim()) {
142
+ this.hide()
143
+ return
144
+ }
145
+
146
+ const text = selection.toString().trim()
147
+ if (text.length < this.minLength) {
148
+ this.hide()
149
+ return
150
+ }
151
+
152
+ // 确保选区在 wrapper 内
153
+ const wrapper = this.$refs.wrapper
154
+ if (!wrapper) return
155
+
156
+ const range = selection.getRangeAt(0)
157
+ if (!wrapper.contains(range.commonAncestorContainer)) {
158
+ this.hide()
159
+ return
160
+ }
161
+
162
+ this.selectedText = text
163
+ this.positionToolbar(range)
164
+ this.visible = true
165
+ },
166
+
167
+ /** 根据选区 Range 定位工具栏 */
168
+ positionToolbar(range) {
169
+ const rect = range.getBoundingClientRect()
170
+ const wrapperRect = this.$refs.wrapper.getBoundingClientRect()
171
+
172
+ // 工具栏居中于选区上方
173
+ const left = rect.left + rect.width / 2 - wrapperRect.left
174
+ const top = rect.top - wrapperRect.top - this.offset
175
+
176
+ this.toolbarStyle = {
177
+ left: `${left}px`,
178
+ top: `${top}px`
179
+ }
180
+ },
181
+
182
+ /** 点击按钮 */
183
+ handleAction(btn, idx) {
184
+ this.$emit('action', {
185
+ command: btn.command || btn.label,
186
+ label: btn.label,
187
+ index: idx,
188
+ text: this.selectedText,
189
+ })
190
+
191
+ // 内置 copy 行为
192
+ if (btn.command === 'copy') {
193
+ this.doCopy(this.selectedText)
194
+ }
195
+
196
+ this.hide()
197
+ },
198
+
199
+ /** 复制文本到剪贴板 */
200
+ async doCopy(text) {
201
+ try {
202
+ if (navigator.clipboard && window.isSecureContext) {
203
+ await navigator.clipboard.writeText(text)
204
+ } else {
205
+ const textarea = document.createElement('textarea')
206
+ textarea.value = text
207
+ textarea.style.position = 'fixed'
208
+ textarea.style.left = '-9999px'
209
+ document.body.appendChild(textarea)
210
+ textarea.select()
211
+ document.execCommand('copy')
212
+ document.body.removeChild(textarea)
213
+ }
214
+ this.$message && this.$message.success('已复制')
215
+ } catch (err) {
216
+ this.$message && this.$message.error('复制失败')
217
+ }
218
+ },
219
+
220
+ /** 隐藏工具栏 */
221
+ hide() {
222
+ this.visible = false
223
+ this.selectedText = ''
224
+ },
225
+
226
+ /** 外部可调用:手动显示 */
227
+ show() {
228
+ this.checkSelection()
229
+ }
230
+ }
231
+ }
232
+ </script>
233
+
234
+ <style lang="scss" scoped>
235
+ .n20-floating-toolbar-wrapper {
236
+ position: relative;
237
+ display: inline-block;
238
+ width: 100%;
239
+ }
240
+
241
+ .n20-floating-toolbar {
242
+ position: absolute;
243
+ z-index: 9999;
244
+ transform: translateX(-50%) translateY(-100%);
245
+ pointer-events: auto;
246
+
247
+ // 箭头
248
+ &__arrow {
249
+ position: absolute;
250
+ left: 50%;
251
+ bottom: -5px;
252
+ transform: translateX(-50%) rotate(45deg);
253
+ width: 10px;
254
+ height: 10px;
255
+ background: #1f2937;
256
+ border-radius: 0 0 2px 0;
257
+ }
258
+
259
+ // 主体
260
+ &__body {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: 2px;
264
+ padding: 4px 6px;
265
+ background: #1f2937;
266
+ border-radius: 6px;
267
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(255, 255, 255, 0.06) inset;
268
+ white-space: nowrap;
269
+ }
270
+
271
+ // 按钮
272
+ &__btn {
273
+ display: inline-flex;
274
+ align-items: center;
275
+ gap: 4px;
276
+ padding: 5px 10px;
277
+ border: none;
278
+ border-radius: 4px;
279
+ background: transparent;
280
+ color: #e5e7eb;
281
+ font-size: 13px;
282
+ line-height: 1;
283
+ cursor: pointer;
284
+ transition: background 0.15s ease, color 0.15s ease;
285
+ outline: none;
286
+
287
+ i {
288
+ font-size: 14px;
289
+ }
290
+
291
+ &:hover {
292
+ background: rgba(255, 255, 255, 0.12);
293
+ color: #fff;
294
+ }
295
+
296
+ &:active {
297
+ background: rgba(255, 255, 255, 0.18);
298
+ }
299
+
300
+ & + & {
301
+ margin-left: 1px;
302
+ }
303
+ }
304
+ }
305
+
306
+ // 出现动画
307
+ .n20-toolbar-fade-enter-active {
308
+ transition: opacity 0.18s ease, transform 0.18s ease;
309
+ }
310
+ .n20-toolbar-fade-leave-active {
311
+ transition: opacity 0.12s ease, transform 0.12s ease;
312
+ }
313
+ .n20-toolbar-fade-enter,
314
+ .n20-toolbar-fade-leave-to {
315
+ opacity: 0;
316
+ transform: translateX(-50%) translateY(-100%) scale(0.92);
317
+ }
318
+ </style>