@kyfe/fms-vue2-components 0.0.11 → 0.0.15

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.
@@ -0,0 +1,353 @@
1
+ <template>
2
+ <div>
3
+ <div class="fms-text-more"
4
+ :style="`--expandLine:${expandLine}`">
5
+ <div ref="content"
6
+ class="fms-text-more-content"
7
+ :class="{ expanded: isExpand }"
8
+ :style="`font-size: ${fontSize}; font-family: ${fontFamily}`"
9
+ v-html="renderedContent"></div>
10
+ <div v-if="showExpand || showViewAll"
11
+ class="fms-text-more-icon"
12
+ @click="toggleIconClick">
13
+ <van-icon :name="isExpand || isShowPopup? 'arrow-up' : 'arrow-down'"
14
+ size="12"
15
+ color="#7158FF" />
16
+ </div>
17
+ </div>
18
+
19
+ <van-popup v-model="isShowPopup"
20
+ position="bottom"
21
+ round
22
+ close-on-click-overlay
23
+ :get-container="getContainer"
24
+ @click-overlay="closePopup">
25
+ <div class="fms-text-more-popup">
26
+ <div class="fms-text-more-popup-title">
27
+ <strong>{{ popupTitle }}</strong>
28
+ <van-icon name="cross"
29
+ color="#9E9CB0"
30
+ size="16"
31
+ @click="closePopup" />
32
+ </div>
33
+ <div class="fms-text-more-popup-content"
34
+ :style="{ height: popupHeight, fontSize, fontFamily}"
35
+ v-html="popupContent"></div>
36
+ </div>
37
+ </van-popup>
38
+ </div>
39
+ </template>
40
+
41
+ <script>
42
+ import { Icon, Popup } from 'vant'
43
+
44
+ const FONT_FAMILY = '-apple-system,BlinkMacSystemFont,PingFang SC,Helvetica Neue,STHeiti,Microsoft Yahei,Tahoma,Simsun,sans-serif'
45
+
46
+ /**
47
+ * 转义 HTML 特殊字符,防止 XSS 注入
48
+ * @param {String} str 原始文本
49
+ * @returns {String} 转义后的文本
50
+ */
51
+ function escapeHtml (str) {
52
+ return String(str == null ? '' : str)
53
+ .replace(/&/g, '&amp;')
54
+ .replace(/</g, '&lt;')
55
+ .replace(/>/g, '&gt;')
56
+ .replace(/"/g, '&quot;')
57
+ .replace(/'/g, '&#39;')
58
+ }
59
+
60
+ /**
61
+ * 计算文本在指定宽度下渲染的行数
62
+ * @param {String} text 原始文本(\n 表示换行)
63
+ * @param {Number} maxWidth 内容区域宽度
64
+ * @param {String} fontSize 字体大小
65
+ * @returns {Number} 行数
66
+ */
67
+ function getTextLine (text, maxWidth, fontSize = '12px') {
68
+ const canvas = document.createElement('canvas')
69
+ const context = canvas.getContext('2d')
70
+ context.font = `${fontSize} ${FONT_FAMILY}`
71
+
72
+ let currentWidth = 0
73
+ let lines = 1
74
+ let i = 0
75
+
76
+ while (i < text.length) {
77
+ const char = text[i]
78
+ if (char === '\n') {
79
+ // 遇到换行符,增加行数并重置当前行宽
80
+ lines++
81
+ currentWidth = 0
82
+ i++
83
+ continue
84
+ }
85
+ const charWidth = context.measureText(char).width
86
+ if (currentWidth + charWidth > maxWidth) {
87
+ lines++
88
+ currentWidth = charWidth
89
+ } else {
90
+ currentWidth += charWidth
91
+ }
92
+ i++
93
+ }
94
+
95
+ return lines
96
+ }
97
+
98
+ /**
99
+ * 创建宽度变化监听器,仅在内容宽度变化时触发回调
100
+ * @param {Element} el 需要监听的元素
101
+ * @param {Function} callback 宽度变化回调
102
+ * @returns {ResizeObserver|null} 监听器实例
103
+ */
104
+ function createResizeObserver (el, callback) {
105
+ if (typeof ResizeObserver === 'undefined' || !el) return null
106
+ let lastWidth = el.offsetWidth
107
+ const observer = new ResizeObserver(entries => {
108
+ const width = entries[0] && entries[0].contentRect && entries[0].contentRect.width
109
+ if (width && width !== lastWidth) {
110
+ lastWidth = width
111
+ callback()
112
+ }
113
+ })
114
+ observer.observe(el)
115
+ return observer
116
+ }
117
+
118
+ export default {
119
+ name: 'FmsTextMore',
120
+ components: {
121
+ VanIcon: Icon,
122
+ VanPopup: Popup
123
+ },
124
+ props: {
125
+ // 事由内容,支持 \n 换行
126
+ content: {
127
+ type: String,
128
+ default: ''
129
+ },
130
+ // 显示展开行数, 默认 3 行
131
+ expandLine: {
132
+ type: Number,
133
+ default: 3
134
+ },
135
+ // 显示查看全文行数,默认6行
136
+ viewAllLine: {
137
+ type: Number,
138
+ default: 6
139
+ },
140
+ // 字体大小
141
+ fontSize: {
142
+ type: String,
143
+ default: '12px'
144
+ },
145
+ // 字体
146
+ fontFamily: {
147
+ type: String,
148
+ default: FONT_FAMILY
149
+ },
150
+ // 弹窗标题
151
+ popupTitle: {
152
+ type: String,
153
+ default: ''
154
+ },
155
+ },
156
+ data () {
157
+ return {
158
+ isExpand: false,
159
+ isShowPopup: false,
160
+ lineCount: 0,
161
+ contentWidth: 0,
162
+ resizeObserver: null,
163
+ }
164
+ },
165
+ computed: {
166
+ // 规范化换行符(\r\n -> \n),用于行数计算
167
+ normalizedContent () {
168
+ return String(this.content || '').replace(/\r\n/g, '\n')
169
+ },
170
+ // 内容区展示(转义后换行转为 <br/>)
171
+ renderedContent () {
172
+ return escapeHtml(this.normalizedContent).replace(/\n/g, '<br/>')
173
+ },
174
+ // 弹窗内容(每个换行拆分为独立段落)
175
+ popupContent () {
176
+ return `<p>${escapeHtml(this.normalizedContent).replace(/\n/g, '</p><p>')}</p>`
177
+ },
178
+ // 是否显示展开/收起(超过 3 行且不超过 6 行)
179
+ showExpand () {
180
+ return this.lineCount > this.expandLine && this.lineCount <= this.viewAllLine
181
+ },
182
+ // 是否显示查看全文(超过 6 行)
183
+ showViewAll () {
184
+ return this.lineCount > this.viewAllLine
185
+ },
186
+ // 弹窗内容区高度
187
+ popupHeight () {
188
+ const maxHeight = document.body.clientHeight * 0.7
189
+ return `${Math.ceil(Math.min(this.lineCount * 24 + 56, maxHeight))}px`
190
+ }
191
+ },
192
+ watch: {
193
+ content () {
194
+ this.$nextTick(this.recompute)
195
+ }
196
+ },
197
+ mounted () {
198
+ this.recompute()
199
+ window.addEventListener('resize', this.handleResize)
200
+ this.resizeObserver = createResizeObserver(this.$refs.content, this.handleResize)
201
+ },
202
+ beforeDestroy () {
203
+ window.removeEventListener('resize', this.handleResize)
204
+ if (this.resizeObserver) {
205
+ this.resizeObserver.disconnect()
206
+ this.resizeObserver = null
207
+ }
208
+ },
209
+ methods: {
210
+ toggleIconClick () {
211
+ if (this.showExpand) {
212
+ this.toggleExpand()
213
+ }
214
+ if (this.showViewAll) {
215
+ this.openPopup()
216
+ }
217
+ },
218
+ /**
219
+ * 展开 / 收起切换
220
+ */
221
+ toggleExpand () {
222
+ this.isExpand = !this.isExpand
223
+ },
224
+ /**
225
+ * 打开查看全文弹窗
226
+ */
227
+ openPopup () {
228
+ this.isShowPopup = true
229
+ },
230
+ /**
231
+ * 关闭查看全文弹窗
232
+ */
233
+ closePopup () {
234
+ this.isShowPopup = false
235
+ },
236
+ /**
237
+ * 弹窗挂载容器
238
+ * @returns {Element} 挂载的 DOM 节点
239
+ */
240
+ getContainer () {
241
+ return document.body
242
+ },
243
+ /**
244
+ * 窗口尺寸变化处理:重置状态并重新测量
245
+ */
246
+ handleResize () {
247
+ this.isExpand = false
248
+ this.isShowPopup = false
249
+ this.recompute()
250
+ },
251
+ /**
252
+ * 动态获取内容区域真实宽度并计算行数
253
+ */
254
+ recompute () {
255
+ const el = this.$refs.content
256
+ if (!el) return
257
+ const width = el.offsetWidth
258
+ if (!width) {
259
+ // 组件尚未完成布局(可能被隐藏),延迟到下一帧重试一次
260
+ if (!this.measureRetried) {
261
+ this.measureRetried = true
262
+ this.$nextTick(this.recompute)
263
+ }
264
+ return
265
+ }
266
+ this.measureRetried = false
267
+ this.contentWidth = width
268
+ this.computeLineCount()
269
+ },
270
+ /**
271
+ * 根据内容与宽度计算文本行数
272
+ */
273
+ computeLineCount () {
274
+ this.lineCount = getTextLine(this.normalizedContent, this.contentWidth, this.fontSize)
275
+ }
276
+ }
277
+ }
278
+ </script>
279
+
280
+ <style lang="less" scoped>
281
+ .fms-text-more {
282
+ display: flex;
283
+
284
+ .fms-text-more-content {
285
+ flex: 1;
286
+ overflow: hidden;
287
+ text-overflow: ellipsis;
288
+ display: -webkit-box;
289
+ -webkit-line-clamp: var(--expandLine);
290
+ -webkit-box-orient: vertical;
291
+ word-break: break-all;
292
+
293
+ &.expanded {
294
+ -webkit-line-clamp: unset;
295
+ }
296
+ }
297
+ .fms-text-more-icon {
298
+ margin-left: 4px;
299
+ flex-shrink: 0;
300
+ flex-grow: 0;
301
+ }
302
+ }
303
+
304
+ .fms-text-more-expand {
305
+ margin-top: 12px;
306
+ display: flex;
307
+ align-items: center;
308
+ width: 100%;
309
+ justify-content: center;
310
+ color: #9e9cb0;
311
+ font-size: 12px;
312
+ height: 16px;
313
+ cursor: pointer;
314
+
315
+ .van-icon {
316
+ margin-left: 4px;
317
+ }
318
+ }
319
+
320
+ .fms-text-more-more {
321
+ display: flex;
322
+ margin-top: 12px;
323
+ color: #9e9cb0;
324
+ align-items: center;
325
+ justify-content: center;
326
+ cursor: pointer;
327
+
328
+ .fms-text-more-more-text {
329
+ margin-right: 4px;
330
+ }
331
+ }
332
+
333
+ .fms-text-more-popup {
334
+ .fms-text-more-popup-title {
335
+ height: 40px;
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: space-between;
339
+ font-size: 18px;
340
+ padding: 16px;
341
+ border-radius: 16px 16px 0 0;
342
+ }
343
+
344
+ .fms-text-more-popup-content {
345
+ line-height: 24px;
346
+ font-size: 15px;
347
+ padding: 0 16px 16px 16px;
348
+ overflow: auto;
349
+ color: #18153a;
350
+ word-break: break-word;
351
+ }
352
+ }
353
+ </style>
@@ -0,0 +1,7 @@
1
+ import FmsTextMore from './fms-text-more.vue'
2
+
3
+ FmsTextMore.install = function(Vue) {
4
+ Vue.component(FmsTextMore.name, FmsTextMore)
5
+ }
6
+
7
+ export default FmsTextMore
package/src/index.js CHANGED
@@ -19,6 +19,8 @@ import FmsKsUpload from './components/fms-ks-upload/index'
19
19
  import FmsKsPicker from './components/fms-ks-picker/index'
20
20
  import FmsKsUploader from './components/fms-ks-uploader/index'
21
21
  import FmsApprovalHeader from './components/fms-approval-header/index'
22
+ import FmsMoneySplit from './components/fms-money-split/index'
23
+ import FmsTextMore from './components/fms-text-more/index'
22
24
 
23
25
  const components = [
24
26
  FmsCopyBox,
@@ -40,6 +42,8 @@ const components = [
40
42
  FmsKsSearchTipsNew,
41
43
  FmsKsPicker,
42
44
  FmsApprovalHeader,
45
+ FmsMoneySplit,
46
+ FmsTextMore,
43
47
  ]
44
48
 
45
49
  const install = function(Vue) {
@@ -70,6 +74,8 @@ export {
70
74
  FmsKsPicker,
71
75
  FmsKsUploader,
72
76
  FmsApprovalHeader,
77
+ FmsMoneySplit,
78
+ FmsTextMore,
73
79
  }
74
80
  // 全量导出
75
81
  export default {
@@ -1,61 +0,0 @@
1
- import { n as i } from "./plugin-vue2_normalizer.ed7092a9.js";
2
- const l = {
3
- name: "FmsKsPicker",
4
- props: {
5
- value: {
6
- type: String,
7
- default: ""
8
- },
9
- label: {
10
- type: String,
11
- default: ""
12
- },
13
- placeholder: {
14
- type: String,
15
- default: ""
16
- },
17
- columns: {
18
- type: Array,
19
- default: () => []
20
- },
21
- required: {
22
- type: Boolean,
23
- default: !1
24
- }
25
- },
26
- data() {
27
- return {
28
- showPicker: !1
29
- };
30
- },
31
- methods: {
32
- showPickerFn() {
33
- this.showPicker = !0;
34
- },
35
- onConfirm(s) {
36
- const { code: e = "" } = s;
37
- this.$emit("input", e, s), this.showPicker = !1;
38
- }
39
- }
40
- };
41
- var t = function() {
42
- var e = this, r = e._self._c;
43
- return r("div", { staticClass: "pick-box" }, [r("div", { staticClass: "pick-field-box", on: { click: e.showPickerFn } }, [r("ks-field", { attrs: { readonly: "", clickable: "", "input-align": "right", "error-message-align": "right", required: e.required, name: "picker", value: e.value, label: e.label, "right-icon": "arrow", rules: [{ required: e.required, message: `\u8BF7\u9009\u62E9${e.label}` }], placeholder: e.placeholder } })], 1), r("ks-popup", { attrs: { position: "bottom" }, model: { value: e.showPicker, callback: function(o) {
44
- e.showPicker = o;
45
- }, expression: "showPicker" } }, [r("ks-picker", { attrs: { "show-toolbar": "", columns: e.columns }, on: { confirm: e.onConfirm, cancel: function(o) {
46
- e.showPicker = !1;
47
- } } })], 1)], 1);
48
- }, n = [], a = /* @__PURE__ */ i(
49
- l,
50
- t,
51
- n,
52
- !1,
53
- null,
54
- null,
55
- null,
56
- null
57
- );
58
- const u = a.exports;
59
- export {
60
- u as F
61
- };