@studio-kit/utils-browser 1.0.1 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @studio-kit/utils-browser
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - test
8
+ - Updated dependencies
9
+ - @studio-kit/utils-common@2.0.1
10
+
3
11
  ## 1.0.1
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studio-kit/utils-browser",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -11,10 +11,13 @@
11
11
  "author": "",
12
12
  "license": "ISC",
13
13
  "dependencies": {
14
- "@studio-kit/utils-common": "^2.0.0",
14
+ "@studio-kit/utils-common": "^2.0.1",
15
15
  "axios": "^1.7.9",
16
16
  "chroma-js": "^3.1.2",
17
- "nanoid": "^5.1.2"
17
+ "clipboardy": "^5.3.1",
18
+ "file-saver": "^2.0.5",
19
+ "nanoid": "^5.1.2",
20
+ "reconnecting-websocket": "^4.4.0"
18
21
  },
19
22
  "exports": {
20
23
  ".": "./src/index.js",
package/src/APP.js ADDED
@@ -0,0 +1,159 @@
1
+ import Data from "./Data";
2
+ import HTTP from "./HTTP.js";
3
+ import clipboard from 'clipboardy';
4
+ export default class APP {
5
+ static USER_INFO_KEY = '$userInfo'
6
+ static clipboard = clipboard
7
+ /**
8
+ * 以 Vue Router hash 模式在新标签页打开路由(#/path?query)
9
+ * @param {string} path 路由路径,如 /workflow-menu
10
+ * @param {Record<string, unknown>} [query] hash 查询参数
11
+ * @param {string} [target] window.open 的 target,默认 _blank
12
+ * @returns {Window | null}
13
+ */
14
+ static openHashUrl({ path, query, target = '_blank', name = '新页面' } = {}) {
15
+ let routePath = path || '/'
16
+ if (routePath.startsWith('#')) {
17
+ routePath = routePath.slice(1)
18
+ }
19
+ if (!routePath.startsWith('/')) {
20
+ routePath = `/${routePath}`
21
+ }
22
+ const queryStr = query && Object.keys(query).length
23
+ ? HTTP.stringifyUrlQuery({ query })
24
+ : ''
25
+ const hash = queryStr ? `${routePath}?${queryStr}` : routePath
26
+ const base = location.href.replace(/#.*$/, '')
27
+ const url = `${base}#${hash}`
28
+ const openPayload = {
29
+ target: "3",
30
+ id: new Date().getTime(),
31
+ openType: "iframe",
32
+ name: name,
33
+ accessPath: url
34
+ }
35
+ let opened = false
36
+ try {
37
+ if (window.parent && window.parent !== window && window.parent.openNewPage) {
38
+ window.parent.openNewPage(openPayload)
39
+ opened = true
40
+ }
41
+ } catch (_) {}
42
+ if (!opened) {
43
+ const openNewPage = APP.getTopProp('openNewPage')
44
+ if (openNewPage) {
45
+ openNewPage(openPayload)
46
+ } else {
47
+ window.open(url, "_blank");
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * 安全读取当前窗或 top 上的命名属性,跨域时返回 fallback,不抛错
53
+ * @param {string} key 属性名,如 'electronAPI' | 'openNewPage' | 'STD_REALTIME'
54
+ * @param {{ fallback?: any }} [options]
55
+ * @returns {any}
56
+ */
57
+ static getTopProp(key, { fallback } = {}) {
58
+ if (typeof window === 'undefined' || !key) {
59
+ return fallback
60
+ }
61
+ try {
62
+ const local = window[key]
63
+ if (local != null) {
64
+ return local
65
+ }
66
+ } catch (_) {}
67
+ try {
68
+ const top = window.top
69
+ if (top && top !== window) {
70
+ const value = top[key]
71
+ if (value != null) {
72
+ return value
73
+ }
74
+ }
75
+ } catch (_) {
76
+ // Blocked a frame with origin "..." from accessing a cross-origin frame
77
+ }
78
+ return fallback
79
+ }
80
+ static openStdWin(params) {
81
+ APP.$bus.$emit("openStdWin", params)
82
+ }
83
+ /** 通过全局 $bus 触发 toast.loading */
84
+ static showLoadingToast(msg) {
85
+ APP.$bus?.$emit('showLoadingToast', msg)
86
+ }
87
+ /** 通过全局 $bus 触发 toast.success(字符串或参数对象) */
88
+ static showSuccessToast(params) {
89
+ APP.$bus?.$emit('showSuccessToast', params)
90
+ }
91
+ /** 通过全局 $bus 触发 toast.error(字符串或含 message 的对象) */
92
+ static showErrorToast(params) {
93
+ APP.$bus?.$emit('showErrorToast', params)
94
+ }
95
+ /** 通过全局 $bus 触发 toast.warning */
96
+ static showWarningToast(params) {
97
+ APP.$bus?.$emit('showErrorToast', params)
98
+ // APP.$bus?.$emit('showWarningToast', params)
99
+ }
100
+ /** 通过全局 $bus 触发 toast.confirm */
101
+ static showConfirm(config) {
102
+ APP.$bus?.$emit('showConfirm', config)
103
+ }
104
+ static async sleep(ms) {
105
+ return new Promise((resolve, reject) => {
106
+ setTimeout(resolve, ms)
107
+ })
108
+ }
109
+ static copyText(text) {
110
+ return clipboard.write(text)
111
+ }
112
+ static emitParentIframe({ data, origin = '*', topic, payload }) {
113
+ if (topic && payload) {
114
+ data = {
115
+ topic,
116
+ payload,
117
+ }
118
+ }
119
+ console.log('emitParentIframe', data)
120
+ try {
121
+ if (window.parent) window.parent.postMessage(data, origin);
122
+ else console.error('对应父级iframe不存在')
123
+ } catch (error) {
124
+ console.error(error)
125
+ }
126
+ }
127
+ static setUserInfo({ userInfo }) {
128
+ APP.userInfo = userInfo
129
+ Data.setCache({
130
+ key: APP.USER_INFO_KEY,
131
+ value: userInfo,
132
+ })
133
+ if (userInfo?.token) {
134
+ HTTP.setToken({ token: userInfo.token })
135
+ } else {
136
+ HTTP.clearToken()
137
+ }
138
+ }
139
+ static getUserInfo() {
140
+ if (APP.userInfo) {
141
+ return APP.userInfo
142
+ } else {
143
+ return Data.getCache({
144
+ key: APP.USER_INFO_KEY,
145
+ })
146
+ }
147
+ }
148
+ static getApiBaseUrl() {
149
+ return window.LV_API_BASE_URL || window.$std_api_base_url || '/studio-server'
150
+ }
151
+ static runCode({ code, args }) {
152
+ try {
153
+ const fn = new Function(code)
154
+ return fn.call(args)
155
+ } catch (error) {
156
+ console.error('run code error:', error)
157
+ }
158
+ }
159
+ }
package/src/DOM.js CHANGED
@@ -1,19 +1,36 @@
1
+ import Perf from './Perf.js';
2
+
1
3
  export default class DOM {
2
4
  // 获取坐标在元素中的相对位置
3
5
  static getOffsetPositionInElement({
4
6
  targetElement, // 对应DOM元素
5
7
  absolutePosition, // 相对于屏幕左上角的坐标 { x:..., y:.. }
6
8
  }) {
7
- // 获取元素在视口中的位置信息
9
+ // // 获取元素在视口中的位置信息
8
10
  const rect = targetElement.getBoundingClientRect();
9
- // 计算坐标相对于元素左上角的位置
10
- const x = absolutePosition.x - rect.left;
11
- const y = absolutePosition.y - rect.top;
12
- let isOutside = false
13
- if (x < 0 || y < 0 || x > rect.right || y > rect.bottom) {
14
- isOutside = true
15
- }
16
- return { x, y, isOutside };
11
+ // // 计算坐标相对于元素左上角的位置
12
+ // const x = absolutePosition.x - rect.left;
13
+ // const y = absolutePosition.y - rect.top;
14
+ // let isOutside = false
15
+ // if (x < 0 || y < 0 || x > rect.right || y > rect.bottom) {
16
+ // isOutside = true
17
+ // }
18
+ // return { x, y, isOutside };
19
+ // 计算视口坐标系下的相对位置
20
+ const viewportX = absolutePosition.x - rect.left;
21
+ const viewportY = absolutePosition.y - rect.top;
22
+
23
+ // 判断是否在可视区域外(基于元素布局尺寸)
24
+ const isOutside = viewportX < 0 || viewportY < 0 ||
25
+ viewportX > rect.width ||
26
+ viewportY > rect.height;
27
+
28
+ // 转换为包含元素自身滚动的内容坐标系
29
+ return {
30
+ x: viewportX + targetElement.scrollLeft,
31
+ y: viewportY + targetElement.scrollTop,
32
+ isOutside
33
+ };
17
34
  }
18
35
  static getBoundingClientRect({ targetEl, wrapperEl, needAutoDetectParent }) {
19
36
  if (needAutoDetectParent) {
@@ -24,7 +41,8 @@ export default class DOM {
24
41
  return {
25
42
  left: left - wrapperBoundingClientRect.left,
26
43
  top: top - wrapperBoundingClientRect.top,
27
- right, bottom,
44
+ right: right - wrapperBoundingClientRect.left,
45
+ bottom: bottom - wrapperBoundingClientRect.top,
28
46
  width: targetEl.offsetWidth,
29
47
  height: targetEl.offsetHeight
30
48
  }
@@ -48,9 +66,39 @@ export default class DOM {
48
66
  const style = window.getComputedStyle(element);
49
67
  return style.overflow === 'scroll' || style.overflow === 'auto';
50
68
  }
51
- static watchSizeChange(element, onChange) {
52
- if (window.ResizeObserver)
53
- return new ResizeObserver(onChange).observe(element)
69
+ /**
70
+ * @param {Element} element
71
+ * @param {(entries: ResizeObserverEntry[]) => void} onChange
72
+ * @param {{ debounce?: boolean, debounceDelay?: number }} [options]
73
+ * debounce 默认 true;为 false 时每次尺寸回调立即执行 onChange
74
+ */
75
+ static watchSizeChange(element, onChange, options = {}) {
76
+ if (!window.ResizeObserver || !element || typeof onChange !== 'function') {
77
+ return null;
78
+ }
79
+ const debounceEnabled = options.debounce !== false;
80
+ const debounceDelay = options.debounceDelay ?? 100;
81
+ let notify = onChange;
82
+ if (debounceEnabled) {
83
+ notify = Perf.debounce({ callback: onChange, delay: debounceDelay });
84
+ }
85
+ const ro = new ResizeObserver((entries) => {
86
+ notify(entries);
87
+ });
88
+ if (typeof notify.cancel === 'function') {
89
+ const disconnectOrig = ro.disconnect.bind(ro);
90
+ const unobserveOrig = ro.unobserve.bind(ro);
91
+ ro.disconnect = () => {
92
+ notify.cancel();
93
+ disconnectOrig();
94
+ };
95
+ ro.unobserve = (target) => {
96
+ notify.cancel();
97
+ unobserveOrig(target);
98
+ };
99
+ }
100
+ ro.observe(element);
101
+ return ro;
54
102
  }
55
103
  static unwatchSizeChange(watcher, element) {
56
104
  if (window.ResizeObserver) {
@@ -65,4 +113,43 @@ export default class DOM {
65
113
  styleDom.innerHTML = code
66
114
  document.head.appendChild(styleDom)
67
115
  }
116
+ static getSizeUnit({ size }) {
117
+ if (typeof size === 'string') {
118
+ return size[size.length - 1]
119
+ }
120
+ }
121
+ static getRelativePositionedParent({ element }) {
122
+ let parent = element.parentElement;
123
+ while (parent) {
124
+ const style = window.getComputedStyle(parent);
125
+ if (style.position !== 'static') {
126
+ return parent;
127
+ }
128
+ if (document.body === parent) {
129
+ return parent
130
+ }
131
+ parent = parent.parentElement;
132
+ }
133
+ return parent
134
+ }
135
+ // 去除元素flex属性
136
+ static removeFlexStyle({ element }) {
137
+ if (!element) return;
138
+ if (DOM.checkHasFlexStyle({ element })) {
139
+ element.style.flex = '';
140
+ }
141
+ }
142
+ static findSiblingWithFlexStyle({ element }) {
143
+ const siblings = Array.from(element.parentElement.children);
144
+ for (const sibling of siblings) {
145
+ if (sibling !== element && DOM.checkHasFlexStyle({ element: sibling })) {
146
+ return sibling;
147
+ }
148
+ }
149
+ }
150
+ static checkHasFlexStyle({ element }) {
151
+ const computedStyle = window.getComputedStyle(element);
152
+ const flexValue = computedStyle.flex;
153
+ return flexValue === '1 1 0%' || flexValue === '1';
154
+ }
68
155
  }
package/src/Data.js CHANGED
@@ -1,2 +1,18 @@
1
- import Data from '@studio-kit/utils-common/Data.js'
2
- export default Data
1
+ import CommonData from '@studio-kit/utils-common/Data.js'
2
+ export default class Data extends CommonData {
3
+ static getCache({ key, defaultValue }) {
4
+ let value = localStorage.getItem(key)
5
+ value = this.maybeJSON({ value })
6
+ if (!value) {
7
+ return defaultValue
8
+ }
9
+ return value
10
+ }
11
+ static setCache({ key, value }) {
12
+ if (typeof value === 'object' && value) {
13
+ value = JSON.stringify(value)
14
+ }
15
+ localStorage.setItem(key, value)
16
+ return value
17
+ }
18
+ }
@@ -0,0 +1,197 @@
1
+ import DateTime from './DateTime.js'
2
+
3
+ const DATE_UNIT = {
4
+ YEAR: 'year',
5
+ MONTH: 'month',
6
+ DAY: 'day',
7
+ HOUR: 'hour',
8
+ MINUTE: 'minute',
9
+ SECOND: 'second',
10
+ }
11
+
12
+ const DATE_CONFIG_TYPE = {
13
+ RELATIVE: 'diff',
14
+ FIXED: 'fixed',
15
+ }
16
+
17
+ const UNITS_ORDER = [
18
+ DATE_UNIT.YEAR,
19
+ DATE_UNIT.MONTH,
20
+ DATE_UNIT.DAY,
21
+ DATE_UNIT.HOUR,
22
+ DATE_UNIT.MINUTE,
23
+ DATE_UNIT.SECOND,
24
+ ]
25
+
26
+ const MOMENT_UNIT_MAP = {
27
+ [DATE_UNIT.YEAR]: 'year',
28
+ [DATE_UNIT.MONTH]: 'month',
29
+ [DATE_UNIT.DAY]: 'day',
30
+ [DATE_UNIT.HOUR]: 'hour',
31
+ [DATE_UNIT.MINUTE]: 'minute',
32
+ [DATE_UNIT.SECOND]: 'second',
33
+ }
34
+
35
+ const DEFAULT_DATE_VALUE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
36
+
37
+ /** 与 editor EdtParamTypeEnum 中带 dateFormat 的项保持一致 */
38
+ const DATE_PARAM_FORMAT_MAP = {
39
+ datetime: 'YYYY-MM-DD HH:mm:ss',
40
+ date: 'YYYY-MM-DD',
41
+ 'year-month': 'YYYY-MM',
42
+ year: 'YYYY',
43
+ month: 'MM',
44
+ day: 'DD',
45
+ hour: 'HH',
46
+ minute: 'mm',
47
+ }
48
+
49
+ export function getDateParamFormatMap() {
50
+ return { ...DATE_PARAM_FORMAT_MAP }
51
+ }
52
+
53
+ export function getDateConfigParam({ dateConfig, paramList = [] } = {}) {
54
+ if (dateConfig?.baseType !== 'param') return null
55
+ return (paramList || []).find((item) => {
56
+ return item && (
57
+ (dateConfig.paramId && item.id === dateConfig.paramId)
58
+ || (dateConfig.paramKey && item.key === dateConfig.paramKey)
59
+ )
60
+ }) || null
61
+ }
62
+
63
+ export function getDateConfigParamFormat({ dateConfig, paramList = [] } = {}) {
64
+ const param = getDateConfigParam({ dateConfig, paramList })
65
+ return param ? DATE_PARAM_FORMAT_MAP[param.valueType] || null : null
66
+ }
67
+
68
+ export function getDateConfigValueFormat({
69
+ dateConfig,
70
+ paramList = [],
71
+ fieldType,
72
+ dateType,
73
+ defaultFormat = DEFAULT_DATE_VALUE_FORMAT,
74
+ } = {}) {
75
+ return getDateConfigParamFormat({ dateConfig, paramList })
76
+ || (fieldType === 'date' && dateType ? DATE_PARAM_FORMAT_MAP[dateType] : null)
77
+ || defaultFormat
78
+ }
79
+
80
+ export function parseDateValueByFormat(value, format) {
81
+ if (value === undefined || value === null || value === '') return null
82
+ if (value instanceof Date) return value
83
+ const text = String(value).trim()
84
+ if (!text) return null
85
+ if (format === 'YYYY' && /^\d{4}$/.test(text)) {
86
+ return new Date(Number(text), 0, 1)
87
+ }
88
+ if (format === 'MM' && /^\d{1,2}$/.test(text)) {
89
+ return new Date(new Date().getFullYear(), Number(text) - 1, 1)
90
+ }
91
+ if (format === 'DD' && /^\d{1,2}$/.test(text)) {
92
+ return new Date(new Date().getFullYear(), new Date().getMonth(), Number(text))
93
+ }
94
+ if (format === 'HH' && /^\d{1,2}$/.test(text)) {
95
+ const date = new Date()
96
+ date.setHours(Number(text), 0, 0, 0)
97
+ return date
98
+ }
99
+ if (format === 'mm' && /^\d{1,2}$/.test(text)) {
100
+ const date = new Date()
101
+ date.setMinutes(Number(text), 0, 0)
102
+ return date
103
+ }
104
+ if (format === 'YYYY-MM' && /^(\d{4})-(\d{1,2})$/.test(text)) {
105
+ const matched = text.match(/^(\d{4})-(\d{1,2})$/)
106
+ if (matched) {
107
+ return new Date(Number(matched[1]), Number(matched[2]) - 1, 1)
108
+ }
109
+ }
110
+ const date = new Date(text.replace(/-/g, '/'))
111
+ return isNaN(date.getTime()) ? null : date
112
+ }
113
+
114
+ export function getDateConfigBaseDate({ dateConfig, paramList = [] } = {}) {
115
+ const param = getDateConfigParam({ dateConfig, paramList })
116
+ if (!param?.defaultDateConfig) return null
117
+ const paramFormat = getDateConfigParamFormat({ dateConfig, paramList })
118
+ const paramDate = computeDateFromConfig(param.defaultDateConfig)
119
+ if (!paramDate) return null
120
+ const paramValue = DateTime.format({
121
+ value: paramDate,
122
+ format: paramFormat || DEFAULT_DATE_VALUE_FORMAT,
123
+ })
124
+ return parseDateValueByFormat(paramValue, paramFormat)
125
+ }
126
+
127
+ export function getDateConfigValue({
128
+ dateConfig,
129
+ paramList = [],
130
+ fieldType,
131
+ dateType,
132
+ defaultFormat = DEFAULT_DATE_VALUE_FORMAT,
133
+ } = {}) {
134
+ if (!dateConfig) return ''
135
+ const baseDate = dateConfig.baseType === 'param'
136
+ ? getDateConfigBaseDate({ dateConfig, paramList })
137
+ : null
138
+ if (dateConfig.baseType === 'param' && !baseDate) return ''
139
+ const date = computeDateFromConfig(dateConfig, baseDate || undefined)
140
+ if (!date) return ''
141
+ return DateTime.format({
142
+ value: date,
143
+ format: getDateConfigValueFormat({
144
+ dateConfig,
145
+ paramList,
146
+ fieldType,
147
+ dateType,
148
+ defaultFormat,
149
+ }),
150
+ }) || ''
151
+ }
152
+
153
+ /**
154
+ * 根据单日期配置(相对/固定)计算相对于当前时间的日期,用于展示
155
+ * @param {{ year?, month?, day?, hour?, minute?, second? }} config
156
+ * @returns {Date | null}
157
+ */
158
+ export function computeDateFromConfig(config, baseDate) {
159
+ if (!config) return null
160
+ const m = baseDate ? DateTime.moment(baseDate) : DateTime.moment()
161
+ if (typeof m.isValid === 'function' && !m.isValid()) return null
162
+ for (const unitKey of UNITS_ORDER) {
163
+ const item = config[unitKey]
164
+ if (!item) continue
165
+ const momUnit = MOMENT_UNIT_MAP[unitKey]
166
+ if (item.type === DATE_CONFIG_TYPE.RELATIVE) {
167
+ const diff = item.diff ?? 0
168
+ if (diff !== 0) {
169
+ if (momUnit === 'day') {
170
+ const before = m.clone()
171
+ m.add(diff, momUnit)
172
+ if (m.month() !== before.month() || m.year() !== before.year()) {
173
+ // m.set(before).endOf('month');
174
+ } else {
175
+ const lastDay = DateTime.getLastDayOfMonth(m.toDate())
176
+ m.date(Math.min(m.date(), lastDay))
177
+ }
178
+ } else {
179
+ m.add(diff, momUnit)
180
+ }
181
+ }
182
+ } else if (item.type === DATE_CONFIG_TYPE.FIXED) {
183
+ const v = item.fixed
184
+ if (v != null) {
185
+ if (momUnit === 'month') {
186
+ m.month(Number(v) - 1)
187
+ } else if (momUnit === 'day') {
188
+ const lastDay = DateTime.getLastDayOfMonth(m.toDate())
189
+ m.date(Math.min(Number(v), lastDay))
190
+ } else {
191
+ m.set(momUnit, Number(v))
192
+ }
193
+ }
194
+ }
195
+ }
196
+ return m.toDate()
197
+ }
@@ -0,0 +1,2 @@
1
+ import DateTime from '@studio-kit/utils-common/DateTime.js'
2
+ export default DateTime
package/src/Effect.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import cta from "./cta";
2
2
  export default class Effect {
3
- static morph({ sourceEl, targetEl, duration }) {
3
+ static morph({ sourceEl, targetEl, duration, onFinish }) {
4
+ if (onFinish) {
5
+ setTimeout(onFinish, duration * 1e3)
6
+ }
4
7
  return cta(sourceEl, targetEl, {
5
8
  duration, // seconds
6
9
  });
@@ -0,0 +1,2 @@
1
+ import EventBus from '@studio-kit/utils-common/EventBus.js'
2
+ export default EventBus