@epxing/utils 0.0.0-stage → 5.3.0-beta.1
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/API.md +602 -0
- package/README.md +22 -2
- package/dist/_vendor/.pnpm/jsencrypt@3.5.4/_vendor/jsencrypt/bin/jsencrypt.min.js +1691 -0
- package/dist/_virtual/_commonjsHelpers.js +6 -0
- package/dist/_virtual/jsencrypt.min.js +31 -0
- package/dist/_virtual/jsencrypt.min2.js +4 -0
- package/dist/_virtual/preload-helper.js +88 -0
- package/dist/assets/ai-BBFn3QRy.png +0 -0
- package/dist/assets/ai_import-BxQ7Ohn6.png +0 -0
- package/dist/assets/app-DBNIokDP.png +0 -0
- package/dist/assets/approval-DGcBv-re.png +0 -0
- package/dist/assets/create_market_system-BH__UVbt.png +0 -0
- package/dist/assets/create_system-irg--ePh.png +0 -0
- package/dist/assets/custom-C-ujlHz1.png +0 -0
- package/dist/assets/default-icon-2193FMjv.png +0 -0
- package/dist/assets/ex_link-4fYPd9nh.png +0 -0
- package/dist/assets/excel-BYqRm2Y8.png +0 -0
- package/dist/assets/folder-CFVsNbjj.png +0 -0
- package/dist/assets/form-DD9HV-jN.png +0 -0
- package/dist/assets/import-tfKWbWVS.png +0 -0
- package/dist/assets/meta-Qorpi70B.png +0 -0
- package/dist/assets/module-DQrs7VAS.png +0 -0
- package/dist/assets/null-B2An29h4.png +0 -0
- package/dist/assets/option-CG02tIgh.svg +1 -0
- package/dist/assets/photo-uscRzRXF.png +0 -0
- package/dist/assets/process-Dcndu9m2.png +0 -0
- package/dist/assets/rule-DcYhYse0.png +0 -0
- package/dist/assets/service-CLYLpPl3.png +0 -0
- package/dist/assets/single_file-B3TJtJli.png +0 -0
- package/dist/assets/snapshot-history-BsVY9cVj.png +0 -0
- package/dist/assets/snapshot-manual-DBRfFHYI.png +0 -0
- package/dist/assets/snapshot-realtime-DjHnJI6C.png +0 -0
- package/dist/assets/staff-Bn_-_00l.png +0 -0
- package/dist/assets/sync_append-Bm8mtvZs.png +0 -0
- package/dist/assets/sync_append_update-DzjxqJ_U.png +0 -0
- package/dist/assets/sync_loading-D7LoNPFP.png +0 -0
- package/dist/assets/sync_replace-Cbbzz327.png +0 -0
- package/dist/assets/sync_update-Birao6Cr.png +0 -0
- package/dist/assets/userRole-D1LAdM6N.png +0 -0
- package/dist/assets/user_bg-BFHtaZjz.png +0 -0
- package/dist/src/Config.js +25 -0
- package/dist/src/asset-images.js +125 -0
- package/dist/src/assets/images/add.png.js +4 -0
- package/dist/src/assets/images/ai-button.png.js +4 -0
- package/dist/src/assets/images/ai.png.js +4 -0
- package/dist/src/assets/images/ai_import.png.js +4 -0
- package/dist/src/assets/images/app.png.js +4 -0
- package/dist/src/assets/images/approval.png.js +4 -0
- package/dist/src/assets/images/book.png.js +4 -0
- package/dist/src/assets/images/canvas-editor/alignment.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/arrow-left.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/arrow-right.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/block.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/bold.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/catalog.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/center.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/checkbox.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/close.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/codeblock.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/color.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/control.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/date.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/exit-fullscreen.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/format.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/highlight.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/hyperlink.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/image.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/italic.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/justify.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/latex.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/left.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-dash-dot-dot.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-dash-dot.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-dash-large-gap.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-dash-small-gap.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-dot.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-double.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-single.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/line-wavy.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/list.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/option.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/page-break.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/page-mode.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/page-scale-add.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/page-scale-minus.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/painter.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/paper-direction.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/paper-margin.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/paper-size.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/print.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/radio.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/redo.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/request-fullscreen.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/right.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/row-margin.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/search.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/separator.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/signature-undo.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/signature.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/size-add.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/size-minus.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/strikeout.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/subscript.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/superscript.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/table.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/title.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/trash.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/underline.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/undo.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/watermark.svg.js +4 -0
- package/dist/src/assets/images/canvas-editor/word-tool.svg.js +4 -0
- package/dist/src/assets/images/clear.png.js +4 -0
- package/dist/src/assets/images/create_market_system.png.js +4 -0
- package/dist/src/assets/images/create_system.png.js +4 -0
- package/dist/src/assets/images/custom.png.js +4 -0
- package/dist/src/assets/images/default-icon.png.js +4 -0
- package/dist/src/assets/images/ex_link.png.js +4 -0
- package/dist/src/assets/images/excel.png.js +4 -0
- package/dist/src/assets/images/file.png.js +4 -0
- package/dist/src/assets/images/folder.png.js +4 -0
- package/dist/src/assets/images/hall/form.png.js +4 -0
- package/dist/src/assets/images/hall/meta.png.js +4 -0
- package/dist/src/assets/images/hall/process.png.js +4 -0
- package/dist/src/assets/images/hall/rule.png.js +4 -0
- package/dist/src/assets/images/hall/service.png.js +4 -0
- package/dist/src/assets/images/hall/userRole.png.js +4 -0
- package/dist/src/assets/images/hr.png.js +4 -0
- package/dist/src/assets/images/import.png.js +4 -0
- package/dist/src/assets/images/jian.png.js +4 -0
- package/dist/src/assets/images/module.png.js +4 -0
- package/dist/src/assets/images/null.png.js +4 -0
- package/dist/src/assets/images/photo.png.js +4 -0
- package/dist/src/assets/images/qq.png.js +4 -0
- package/dist/src/assets/images/robot.png.js +4 -0
- package/dist/src/assets/images/single_file.png.js +4 -0
- package/dist/src/assets/images/snapshot-history.png.js +4 -0
- package/dist/src/assets/images/snapshot-manual.png.js +4 -0
- package/dist/src/assets/images/snapshot-realtime.png.js +4 -0
- package/dist/src/assets/images/staff.png.js +4 -0
- package/dist/src/assets/images/switch_app.png.js +4 -0
- package/dist/src/assets/images/sync_append.png.js +4 -0
- package/dist/src/assets/images/sync_append_update.png.js +4 -0
- package/dist/src/assets/images/sync_loading.png.js +4 -0
- package/dist/src/assets/images/sync_replace.png.js +4 -0
- package/dist/src/assets/images/sync_update.png.js +4 -0
- package/dist/src/assets/images/user_bg.png.js +4 -0
- package/dist/src/assets/images/weixin.png.js +4 -0
- package/dist/src/chart-config-update.js +17 -0
- package/dist/src/chart-data-adapters.js +112 -0
- package/dist/src/chart-option-override.js +128 -0
- package/dist/src/chart-option-paths.js +64 -0
- package/dist/src/chart-registry.js +46 -0
- package/dist/src/chart-render-host.js +57 -0
- package/dist/src/chart-request-broker.js +176 -0
- package/dist/src/chartTheme.js +683 -0
- package/dist/src/collection.js +25 -0
- package/dist/src/date-helper.js +60 -0
- package/dist/src/dom-helper.js +22 -0
- package/dist/src/eval-with.js +14 -0
- package/dist/src/fieldFactory.js +5 -0
- package/dist/src/formula.js +365 -0
- package/dist/src/hljs-lite.js +29 -0
- package/dist/src/index.js +24 -0
- package/dist/src/lazy-jsencrypt.js +13 -0
- package/dist/src/legacy.js +25 -0
- package/dist/src/locale.js +88 -0
- package/dist/src/menu-open.js +43 -0
- package/dist/src/mermaid.js +79 -0
- package/dist/src/meta.js +4 -0
- package/dist/src/mobile-chart-appearance.js +148 -0
- package/dist/src/moment-compat.js +558 -0
- package/dist/src/nav-history.js +184 -0
- package/dist/src/page.js +116 -0
- package/dist/src/pdf.viewer.js +4 -0
- package/dist/src/placeholder.js +64 -0
- package/dist/src/public.js +49 -0
- package/dist/src/security.js +26 -0
- package/dist/src/storage.js +107 -0
- package/dist/src/token-storage.js +108 -0
- package/dist/src/util.js +4 -0
- package/package.json +109 -3
- package/src/Config.js +25 -0
- package/src/asset-images.js +16 -0
- package/src/chart-config-update.js +16 -0
- package/src/chart-data-adapters.js +106 -0
- package/src/chart-option-override.js +126 -0
- package/src/chart-option-paths.js +63 -0
- package/src/chart-registry.js +41 -0
- package/src/chart-render-host.js +56 -0
- package/src/chart-request-broker.js +171 -0
- package/src/chartTheme.js +711 -0
- package/src/collection.js +28 -0
- package/src/date-helper.js +72 -0
- package/src/dom-helper.js +26 -0
- package/src/eval-with.js +21 -0
- package/src/fieldFactory.js +3 -0
- package/src/formula.js +362 -0
- package/src/hljs-lite.js +41 -0
- package/src/index.js +2 -0
- package/src/lazy-jsencrypt.js +36 -0
- package/src/legacy.js +29 -0
- package/src/locale.js +104 -0
- package/src/menu-open.js +72 -0
- package/src/mermaid.js +115 -0
- package/src/meta.js +2 -0
- package/src/mobile-chart-appearance.js +147 -0
- package/src/moment-compat.d.ts +91 -0
- package/src/moment-compat.js +744 -0
- package/src/nav-history.js +274 -0
- package/src/page.js +129 -0
- package/src/pdf.viewer.js +2 -0
- package/src/placeholder.js +107 -0
- package/src/public.js +11 -0
- package/src/security.js +29 -0
- package/src/storage.js +135 -0
- package/src/token-storage.js +120 -0
- package/src/util.js +2 -0
- package/types/Config.d.ts +4 -0
- package/types/collection.d.ts +40 -0
- package/types/date-helper.d.ts +11 -0
- package/types/date-time.d.ts +2 -0
- package/types/date.d.ts +1 -0
- package/types/dom-helper.d.ts +3 -0
- package/types/dom.d.ts +1 -0
- package/types/index.d.ts +1 -0
- package/types/moment-compat.d.ts +91 -0
- package/types/page.d.ts +56 -0
- package/types/placeholder.d.ts +42 -0
- package/types/public.d.ts +10 -0
- package/types/storage.d.ts +31 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导航历史记账器:让浏览器历史与内部页面栈/弹窗严格一一对应
|
|
3
|
+
*
|
|
4
|
+
* 背景:本框架的页面栈是内存栈(epx-router-view 的 routes 数组),与浏览器
|
|
5
|
+
* history 天然脱钩。旧实现打开页面时 pushState 哑条目、应用内返回却从不回退
|
|
6
|
+
* 历史,且每个页面组件各挂一个 popstate 监听——导致移动端系统返回手势要按
|
|
7
|
+
* 很多次才能退出应用、根页面按返回没反应等问题。
|
|
8
|
+
*
|
|
9
|
+
* 记账规则(每个「可关闭物」对应恰好一条浏览器历史):
|
|
10
|
+
* - 打开页面 / 新标签 / 弹窗 / 抽屉时 pushEntry() 压入一条哑历史
|
|
11
|
+
* - 系统返回(popstate):全局唯一监听器按文档序找到最顶层可见的
|
|
12
|
+
* routerView,让其栈顶页面执行一次 backPage(弹窗页会走弹窗分支)
|
|
13
|
+
* - 应用内主动关闭(返回按钮 / 弹窗 X / 遮罩等):consume() 对称回退等量历史
|
|
14
|
+
* 由此系统返回在任意时刻的语义都是「关一层」;条目耗尽后再按返回即可
|
|
15
|
+
* 直接回到上一个应用/页面。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const state = {
|
|
19
|
+
// 当前由本应用压入且未被消耗的哑历史条数
|
|
20
|
+
entryCount: 0,
|
|
21
|
+
// 待吞掉的、由 consume/leaveApp 自身 history.go 触发的 popstate 数
|
|
22
|
+
suppressCount: 0,
|
|
23
|
+
suppressDeadline: 0,
|
|
24
|
+
// 正在处理系统返回:此期间联动触发的关闭动作不再回退历史
|
|
25
|
+
handlingPop: false,
|
|
26
|
+
// 冷启动退出守卫是否生效(见 install/markAppJump)
|
|
27
|
+
guardMode: false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 所有存活的 epx-router-view 实例(含弹窗/抽屉内的)
|
|
31
|
+
const routerViews = []
|
|
32
|
+
|
|
33
|
+
// 待合并回退的历史条数(微任务内一次 history.go,避免与 pushState 时序交错)
|
|
34
|
+
let pendingGo = 0
|
|
35
|
+
let flushScheduled = false
|
|
36
|
+
|
|
37
|
+
// 当前应用上下文标识(路由路径前 4 段,容忍 role 等尾段差异)
|
|
38
|
+
// 大厅→应用 的正向跳转是 pushState+reload 整页重启(见 AppList.vue),
|
|
39
|
+
// 反向回退若不重启:hall 与 app 路由共用 App 组件,vue-router 复用实例导致白屏
|
|
40
|
+
let bootIdentity = ''
|
|
41
|
+
|
|
42
|
+
// 冷启动退出守卫(state.guardMode):应用不是从大厅/上个应用跳转而来时
|
|
43
|
+
// (无 epx-nav-prev 标记),移动端启动即压一条守卫条目,返回键打到守卫上就
|
|
44
|
+
// 原地补一条(视觉无操作),恢复旧版「防止移动端返回键直接退出网页」的语义;
|
|
45
|
+
// 从大厅跳转而来的应用不设守卫,根页面一次返回即回大厅
|
|
46
|
+
const PREV_CONTEXT_KEY = 'epx-nav-prev'
|
|
47
|
+
|
|
48
|
+
function pushGuard () {
|
|
49
|
+
window.history.pushState({ epxGuard: true }, '')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 跨应用跳转(大厅点应用 / 返回大厅 / 打开其他应用)前调用:
|
|
54
|
+
* 在 sessionStorage 留下标记,目标应用启动时据此关闭退出守卫,使返回键可以回到本页
|
|
55
|
+
*/
|
|
56
|
+
export function markAppJump () {
|
|
57
|
+
try {
|
|
58
|
+
sessionStorage.setItem(PREV_CONTEXT_KEY, window.location.href)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// sessionStorage 不可用(隐私模式等)时退化为守卫模式
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function routeIdentity () {
|
|
65
|
+
const hash = window.location.hash || ''
|
|
66
|
+
let path = hash.charAt(0) === '#' ? hash.slice(1) : window.location.pathname
|
|
67
|
+
path = path.split('?')[0]
|
|
68
|
+
return path.split('/').filter(Boolean).slice(0, 4).join('/')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 真实回退落到了本应用上下文之外(返回服务大厅/上一个应用)时整页重启目标路由,
|
|
73
|
+
* 与正向 pushState+reload 对称;同应用内的回退不受影响
|
|
74
|
+
*/
|
|
75
|
+
function reloadIfLeftContext () {
|
|
76
|
+
if (bootIdentity && routeIdentity() !== bootIdentity) {
|
|
77
|
+
window.location.reload()
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 未拦截的回退(含 leaveApp/consume 自发的)落点处理:
|
|
85
|
+
* 离开应用上下文 → 整页重启目标路由;仍在应用内且已无可关闭物 → 守卫模式下补回守卫条目
|
|
86
|
+
*/
|
|
87
|
+
function handleLanding () {
|
|
88
|
+
if (reloadIfLeftContext()) return
|
|
89
|
+
if (state.guardMode && state.entryCount <= 0 && pendingGo <= 0) {
|
|
90
|
+
pushGuard()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function registerRouterView (rv) {
|
|
95
|
+
if (routerViews.indexOf(rv) === -1) routerViews.push(rv)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function unregisterRouterView (rv) {
|
|
99
|
+
const index = routerViews.indexOf(rv)
|
|
100
|
+
if (index > -1) routerViews.splice(index, 1)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 打开一个可关闭物(页面/新标签/弹窗/抽屉)时调用,压入一条哑历史。
|
|
105
|
+
* state 里记录当前深度,供整页刷新后 install() 恢复计数。
|
|
106
|
+
*/
|
|
107
|
+
export function pushEntry () {
|
|
108
|
+
// 应用上下文可能在启动后被路由守卫补全(如追加 role 段),压条目时刷新基准
|
|
109
|
+
bootIdentity = routeIdentity()
|
|
110
|
+
if (pendingGo > 0) {
|
|
111
|
+
// 有待回退的条目未冲销:直接复用栈顶现有哑条目,一进一出相抵
|
|
112
|
+
pendingGo--
|
|
113
|
+
state.entryCount++
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
state.entryCount++
|
|
117
|
+
window.history.pushState({ epxNav: state.entryCount }, '')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 应用内主动关闭 delta 个可关闭物后调用,对称回退等量浏览器历史。
|
|
122
|
+
* 系统返回(popstate)联动的关闭不需要也不允许再回退——由 handlingPop 拦截。
|
|
123
|
+
*/
|
|
124
|
+
export function consume (delta = 1) {
|
|
125
|
+
if (state.handlingPop) return
|
|
126
|
+
const n = Math.min(delta, state.entryCount)
|
|
127
|
+
if (n <= 0) return
|
|
128
|
+
state.entryCount -= n
|
|
129
|
+
pendingGo += n
|
|
130
|
+
scheduleFlush()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 弹窗/抽屉关闭时的历史冲销(幂等:只对压过历史且未冲销的弹窗生效)
|
|
135
|
+
*/
|
|
136
|
+
export function consumeForPopup (popupObj) {
|
|
137
|
+
if (!popupObj || !popupObj._pushedHistory) return
|
|
138
|
+
popupObj._pushedHistory = false
|
|
139
|
+
consume(1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 吞掉下一次 popstate(自己发起 history 回退前调用)。
|
|
144
|
+
* 带超时:history.go 越过栈底或被 iframe 条目吃掉时 popstate 不会来,防止泄漏。
|
|
145
|
+
*/
|
|
146
|
+
export function suppressNextPop () {
|
|
147
|
+
state.suppressCount++
|
|
148
|
+
state.suppressDeadline = Date.now() + 1500
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 是否处于应用根页面:没有打开的弹窗/抽屉,且所有可见 routerView 的页面栈都只剩 1 页。
|
|
153
|
+
* 供 $backPage 判断「返回」语义应为离开应用(回上个应用/大厅)而非销毁页面
|
|
154
|
+
*/
|
|
155
|
+
export function isAtRootPage () {
|
|
156
|
+
const opened = window.openedPopup
|
|
157
|
+
if (Array.isArray(opened) && opened.some(popup => popup && !popup.closed)) return false
|
|
158
|
+
for (const rv of routerViews) {
|
|
159
|
+
if (!rv.routes || rv.routes.length === 0) continue
|
|
160
|
+
if (!rv.$el || !isVisible(rv.$el)) continue
|
|
161
|
+
if (rv.routes.length > 1) return false
|
|
162
|
+
}
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 把控制权交还浏览器/宿主(原 Tabs 的 history.go(-1)):
|
|
168
|
+
* 越过所有残留哑条目直接离开应用。
|
|
169
|
+
*/
|
|
170
|
+
export function leaveApp () {
|
|
171
|
+
const n = state.entryCount + pendingGo + 1
|
|
172
|
+
state.entryCount = 0
|
|
173
|
+
pendingGo = 0
|
|
174
|
+
suppressNextPop()
|
|
175
|
+
window.history.go(-n)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function scheduleFlush () {
|
|
179
|
+
if (flushScheduled) return
|
|
180
|
+
flushScheduled = true
|
|
181
|
+
Promise.resolve().then(() => {
|
|
182
|
+
flushScheduled = false
|
|
183
|
+
if (pendingGo <= 0) return
|
|
184
|
+
const n = pendingGo
|
|
185
|
+
pendingGo = 0
|
|
186
|
+
suppressNextPop()
|
|
187
|
+
window.history.go(-n)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isVisible (el) {
|
|
192
|
+
return !!el && el.isConnected && el.offsetParent !== null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 找到当前应响应系统返回的页面组件:
|
|
197
|
+
* 可见且栈非空的 routerView 中取文档序最靠后的一个(嵌套内层在外层之后、
|
|
198
|
+
* 弹窗/抽屉挂在 body 尾部在主界面之后),其栈顶页面即当前用户看到的最顶层页。
|
|
199
|
+
*/
|
|
200
|
+
function findTopPage () {
|
|
201
|
+
let top = null
|
|
202
|
+
for (const rv of routerViews) {
|
|
203
|
+
if (!rv.routes || rv.routes.length === 0) continue
|
|
204
|
+
if (!rv.$el || !isVisible(rv.$el)) continue
|
|
205
|
+
if (!top) {
|
|
206
|
+
top = rv
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
const pos = top.$el.compareDocumentPosition(rv.$el)
|
|
210
|
+
// FOLLOWING 含「在其后」与「被其包含」两种情况,都视为更顶层
|
|
211
|
+
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) top = rv
|
|
212
|
+
}
|
|
213
|
+
if (!top) return null
|
|
214
|
+
const pageWrap = top.$refs && top.$refs.page
|
|
215
|
+
// Vue3 无 $children(原 Vue2 实现用 pageWrap.$children[0] 取 epx-page 唯一的渲染子节点):
|
|
216
|
+
// epx-page(packages/components/src/page/Page.vue)内部动态渲染实际页面组件的
|
|
217
|
+
// `<component :is="componentName">` 标了 ref="component",直接取同一个实例,语义等价。
|
|
218
|
+
return (pageWrap && pageWrap.$refs && pageWrap.$refs.component) || null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function onPopstate (event) {
|
|
222
|
+
if (state.suppressCount > 0) {
|
|
223
|
+
if (Date.now() <= state.suppressDeadline) {
|
|
224
|
+
state.suppressCount--
|
|
225
|
+
// leaveApp 的回退会越出应用上下文,同样需要整页重启目标路由
|
|
226
|
+
handleLanding()
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
state.suppressCount = 0
|
|
230
|
+
}
|
|
231
|
+
// 推迟到微任务:让 AiChatPage 等自管理 popstate 的组件先行处理并标记
|
|
232
|
+
Promise.resolve().then(() => {
|
|
233
|
+
if (event && event._epxNavHandled) return
|
|
234
|
+
if (state.entryCount <= 0) {
|
|
235
|
+
// 条目耗尽后的真实回退:离开本应用(返回大厅/上一个应用)或撞上退出守卫
|
|
236
|
+
handleLanding()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
state.entryCount--
|
|
240
|
+
const page = findTopPage()
|
|
241
|
+
if (!page || typeof page.backPage !== 'function') return
|
|
242
|
+
state.handlingPop = true
|
|
243
|
+
try {
|
|
244
|
+
page.backPage({ fromPopstate: true })
|
|
245
|
+
} finally {
|
|
246
|
+
state.handlingPop = false
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function install () {
|
|
252
|
+
if (window.__epxNavHistoryInstalled) return
|
|
253
|
+
window.__epxNavHistoryInstalled = true
|
|
254
|
+
// 调试句柄:控制台可查 entryCount/suppressCount 排查返回问题
|
|
255
|
+
window.__epxNavHistory = state
|
|
256
|
+
bootIdentity = routeIdentity()
|
|
257
|
+
// 冷启动(非跨应用跳转而来)的移动端:压守卫条目防止返回键直接退出网页
|
|
258
|
+
let cameFromApp = false
|
|
259
|
+
try {
|
|
260
|
+
cameFromApp = !!sessionStorage.getItem(PREV_CONTEXT_KEY)
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// 读不到时按冷启动处理
|
|
263
|
+
}
|
|
264
|
+
if (!cameFromApp && window.innerWidth <= 992) {
|
|
265
|
+
state.guardMode = true
|
|
266
|
+
pushGuard()
|
|
267
|
+
}
|
|
268
|
+
// 整页刷新恢复:当前条目若带 epxNav 深度标记,说明其下方还有等量哑条目
|
|
269
|
+
const s = window.history.state
|
|
270
|
+
if (s && typeof s.epxNav === 'number' && s.epxNav > 0) {
|
|
271
|
+
state.entryCount = s.epxNav
|
|
272
|
+
}
|
|
273
|
+
window.addEventListener('popstate', onPopstate)
|
|
274
|
+
}
|
package/src/page.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
class Page {
|
|
2
|
+
constructor (props) {
|
|
3
|
+
const { label, linkEntitys, relatedEntitys, sysId, data } = props
|
|
4
|
+
const { CONTENT, FLAG, ID, META_ID, META_CODE, PAGE_ID, PAGE_CODE, PAGE_PATH, VISUAL_CONFIG_PATH, TITLE, HELP, SYSTEM, SYSTEM_CODE, RIGHT_CONTROL_MODE } = data
|
|
5
|
+
this.label = label
|
|
6
|
+
this.title = TITLE
|
|
7
|
+
this.linkEntitys = linkEntitys
|
|
8
|
+
this.relatedEntitys = relatedEntitys
|
|
9
|
+
this.sysId = sysId
|
|
10
|
+
// eslint-disable-next-line no-eval
|
|
11
|
+
this.config = eval('(' + (CONTENT || '{}') + ')')
|
|
12
|
+
this.flag = FLAG
|
|
13
|
+
this.pageId = ID
|
|
14
|
+
this.templateId = PAGE_ID
|
|
15
|
+
this.metaId = META_ID
|
|
16
|
+
this.metaCode = META_CODE
|
|
17
|
+
this.pagePath = PAGE_PATH
|
|
18
|
+
this.pageCode = PAGE_CODE
|
|
19
|
+
this.system = SYSTEM
|
|
20
|
+
this.systemCode = SYSTEM_CODE
|
|
21
|
+
this.help = HELP
|
|
22
|
+
this.rightControlMode = RIGHT_CONTROL_MODE
|
|
23
|
+
this.visualConfigPath = VISUAL_CONFIG_PATH
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getPageId () {
|
|
28
|
+
return this.pageId
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getLabel () {
|
|
32
|
+
return this.label
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getMetaId () {
|
|
36
|
+
return this.metaId
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getFlag () {
|
|
40
|
+
return this.flag
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getPagePath () {
|
|
44
|
+
return this.pagePath
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getConfig () {
|
|
48
|
+
return this.config
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 主键字段
|
|
53
|
+
* @return {}
|
|
54
|
+
*/
|
|
55
|
+
getKey () {
|
|
56
|
+
// var me=this;
|
|
57
|
+
return this.keyNames[0]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取所有属性
|
|
62
|
+
* @return {}
|
|
63
|
+
*/
|
|
64
|
+
getAttris () {
|
|
65
|
+
const data = []
|
|
66
|
+
const m = this
|
|
67
|
+
const attris = m.attributes
|
|
68
|
+
attris.forEach((name, index) => {
|
|
69
|
+
const a = attris[name]
|
|
70
|
+
data[index] = { ID: a.name, NAME: a.label }
|
|
71
|
+
})
|
|
72
|
+
return data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ID对应LAVEL
|
|
77
|
+
*/
|
|
78
|
+
getIdLabelAttris () {
|
|
79
|
+
const data = []
|
|
80
|
+
const m = this
|
|
81
|
+
const attris = m.attributes
|
|
82
|
+
attris.forEach((name, index) => {
|
|
83
|
+
const a = attris[name]
|
|
84
|
+
data[index] = { ID: a.id, NAME: a.label }
|
|
85
|
+
})
|
|
86
|
+
return data
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* NAME对应LAVEL
|
|
91
|
+
*/
|
|
92
|
+
getNameLabelAttris () {
|
|
93
|
+
const data = []
|
|
94
|
+
const m = this
|
|
95
|
+
const attris = m.attributes
|
|
96
|
+
attris.forEach((name, index) => {
|
|
97
|
+
const a = attris[name]
|
|
98
|
+
data[index] = { ID: a.name, NAME: a.label }
|
|
99
|
+
})
|
|
100
|
+
return data
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 根据字段ID获取字段Name
|
|
105
|
+
* @param {} attriId
|
|
106
|
+
* @return {}
|
|
107
|
+
*/
|
|
108
|
+
getAttriNameById (attriId) {
|
|
109
|
+
const me = this
|
|
110
|
+
if (me.attriIds) {
|
|
111
|
+
return me.attriIds[attriId]
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 根据字段key获取label
|
|
118
|
+
*/
|
|
119
|
+
getLabelByField (field) {
|
|
120
|
+
const m = this
|
|
121
|
+
const attribute = m.getAttributes()[field]
|
|
122
|
+
if (attribute) {
|
|
123
|
+
return attribute.label
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default Page
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// 统一的 `#{path}` 占位符解析。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有这个文件:同一个「按 key 从当前上下文取值」的操作,全仓被复制粘贴出
|
|
4
|
+
// **8 种不同语义**(105 行 / 51 个文件),归一化后是这样:
|
|
5
|
+
//
|
|
6
|
+
// 30 次 $query → $tab ← 根本不查组件自身
|
|
7
|
+
// 16 次 $query → $tab → this ← 组件自身排最后
|
|
8
|
+
// 12 次 this → $query ← 少了 $tab
|
|
9
|
+
// 8 次 this → $query → $tab → $route.query ← 多一个来源
|
|
10
|
+
// 6 次 this → $query → $tab
|
|
11
|
+
// 3 次 this
|
|
12
|
+
// …
|
|
13
|
+
//
|
|
14
|
+
// 也就是说同一个 `#{userId}` 写在不同组件里,解析结果不一样。本文件不试图替所有调用点
|
|
15
|
+
// 决定「该查哪些来源」——那是各组件的语境,强行统一等于拿 51 个文件的行为去赌。
|
|
16
|
+
// 它统一的是**取值语义**,把三个各处都错的地方一次修掉:
|
|
17
|
+
//
|
|
18
|
+
// 1. 命中判据是「这个来源**有没有**这个路径」,不是「取出来真不真」。
|
|
19
|
+
// 存量用 `eval(a.k) || eval(b.k) || ''` 串联,于是合法的 `0` / `''` / `false`
|
|
20
|
+
// 会静默穿透到下一个来源去取同名值——这是个真 bug,不是风格问题。
|
|
21
|
+
// 2. **全局**替换。存量写的是 `.replace(/#\{…\}/, fn)`,没有 `g`,
|
|
22
|
+
// 一个字符串里有多个占位符时只有第一个被替换。
|
|
23
|
+
// 3. 不用 eval。存量靠 `eval(\`that.${key}\`)` 做属性取值;这里就是纯属性路径遍历。
|
|
24
|
+
// (顺带说明:存量那个 eval 的注入风险其实很低——正则字符集里没有括号,
|
|
25
|
+
// 写不进函数调用。所以这次改动的理由是**语义一致性**,不是安全。)
|
|
26
|
+
//
|
|
27
|
+
// 迁移策略:新代码一律用这里;存量按「哪天碰到哪个文件就顺手迁哪个」推进,
|
|
28
|
+
// 不做一次性全量替换。
|
|
29
|
+
|
|
30
|
+
// 与存量 `#{}` 正则的字符集保持一致(含 `|`:`busi|NAME` 这类字段名里带竖线),
|
|
31
|
+
// 但补上 g —— 见上面第 2 点。
|
|
32
|
+
export const PLACEHOLDER_RE = /#\{([\w\-.|$#]+)\}/g
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 按属性路径在**一个**来源里取值。
|
|
36
|
+
* 只按 `.` 和 `[...]` 分段;`|` 当普通字符(字段名可能长这样:`busi|NAME`)。
|
|
37
|
+
* @returns {{ found: boolean, value: any }} found 区分「没有这个路径」与「值恰好是 undefined」
|
|
38
|
+
*/
|
|
39
|
+
export function lookup (path, source) {
|
|
40
|
+
if (source == null || typeof path !== 'string' || !path) return { found: false, value: undefined }
|
|
41
|
+
const keys = []
|
|
42
|
+
let cur = ''
|
|
43
|
+
let inBracket = false
|
|
44
|
+
for (const ch of path) {
|
|
45
|
+
if (ch === '[') {
|
|
46
|
+
inBracket = true
|
|
47
|
+
if (cur) { keys.push(cur); cur = '' }
|
|
48
|
+
} else if (ch === ']') {
|
|
49
|
+
inBracket = false
|
|
50
|
+
if (cur) { keys.push(cur.replace(/^['"](.*)['"]$/, '$1')); cur = '' }
|
|
51
|
+
} else if (ch === '.' && !inBracket) {
|
|
52
|
+
if (cur) { keys.push(cur); cur = '' }
|
|
53
|
+
} else {
|
|
54
|
+
cur += ch
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (cur) keys.push(cur)
|
|
58
|
+
if (!keys.length) return { found: false, value: undefined }
|
|
59
|
+
|
|
60
|
+
let obj = source
|
|
61
|
+
for (let i = 0; i < keys.length; i++) {
|
|
62
|
+
if (obj == null) return { found: false, value: undefined }
|
|
63
|
+
const k = keys[i]
|
|
64
|
+
// 用 in / hasOwnProperty 判存在,而不是看取出来的值真不真
|
|
65
|
+
const has = typeof obj === 'object' ? (k in obj) : Object.prototype.hasOwnProperty.call(Object(obj), k)
|
|
66
|
+
if (!has) return { found: false, value: undefined }
|
|
67
|
+
obj = obj[k]
|
|
68
|
+
}
|
|
69
|
+
return { found: true, value: obj }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 按给定顺序在多个来源里找第一个**拥有**该路径的。
|
|
74
|
+
* @param {string} path
|
|
75
|
+
* @param {Array<object>} sources 有序来源,如 [this, this.$query, this.$tab]
|
|
76
|
+
* @returns {{ found: boolean, value: any, index: number }} index 为命中的来源下标,未命中为 -1
|
|
77
|
+
*/
|
|
78
|
+
export function resolveKey (path, sources = []) {
|
|
79
|
+
for (let i = 0; i < sources.length; i++) {
|
|
80
|
+
const r = lookup(path, sources[i])
|
|
81
|
+
if (r.found) return { found: true, value: r.value, index: i }
|
|
82
|
+
}
|
|
83
|
+
return { found: false, value: undefined, index: -1 }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 替换字符串里的全部 `#{path}`。
|
|
88
|
+
* @param {string} text
|
|
89
|
+
* @param {Array<object>} sources 有序来源
|
|
90
|
+
* @param {object} [options]
|
|
91
|
+
* @param {'empty'|'keep'} [options.onMissing='empty']
|
|
92
|
+
* 'empty' —— 取不到就换成空串(与存量 `… || ''` 的收尾一致,是默认)
|
|
93
|
+
* 'keep' —— 原样保留 `#{path}`。⚠️ 保留下来的是**非空字符串**,
|
|
94
|
+
* 也就是真值,`a || b` 形式的兜底会失效——选它之前先确认调用方判得了。
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function resolvePlaceholders (text, sources = [], options = {}) {
|
|
98
|
+
if (typeof text !== 'string' || text.indexOf('#{') === -1) return text
|
|
99
|
+
const onMissing = options.onMissing || 'empty'
|
|
100
|
+
return text.replace(PLACEHOLDER_RE, (match, path) => {
|
|
101
|
+
const r = resolveKey(path, sources)
|
|
102
|
+
if (!r.found) return onMissing === 'keep' ? match : ''
|
|
103
|
+
return r.value == null ? '' : String(r.value)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default { PLACEHOLDER_RE, lookup, resolveKey, resolvePlaceholders }
|
package/src/public.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as Config } from './Config'
|
|
2
|
+
export { default as Page } from './page'
|
|
3
|
+
export { default as placeholder } from './placeholder'
|
|
4
|
+
export * from './placeholder'
|
|
5
|
+
export * from './date-helper'
|
|
6
|
+
export * from './dom-helper'
|
|
7
|
+
export { default as storage, getXsrfToken, setXsrfToken } from './storage'
|
|
8
|
+
export { default as cookie } from './storage'
|
|
9
|
+
export { default as moment, default as dateTime, createMoment } from './moment-compat'
|
|
10
|
+
export { default as lodash } from './collection'
|
|
11
|
+
export * from './collection'
|
package/src/security.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DANGEROUS_PATTERNS = [
|
|
2
|
+
/\beval\s*\(/,
|
|
3
|
+
/(?:^|[^$])\balert\s*\(/,
|
|
4
|
+
/(?:^|[^$])\bconfirm\s*\(/,
|
|
5
|
+
/(?:^|[^$])\bprompt\s*\(/,
|
|
6
|
+
/\bdocument\s*\.\s*write\s*\(/,
|
|
7
|
+
/\bdocument\s*\.\s*writeln\s*\(/,
|
|
8
|
+
/\blocation\s*\.\s*(href|replace|assign)\s*[=(]/,
|
|
9
|
+
/\bsetTimeout\s*\(\s*['"`]/,
|
|
10
|
+
/\bsetInterval\s*\(\s*['"`]/,
|
|
11
|
+
/\bnew\s+Function\s*\(/,
|
|
12
|
+
/\bimportScripts\s*\(/,
|
|
13
|
+
/\bfetch\s*\(/,
|
|
14
|
+
/\bXMLHttpRequest\b/
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 检查函数是否包含高危调用
|
|
19
|
+
* @param {Function} fn
|
|
20
|
+
* @returns {boolean} true 表示安全,false 表示包含高危调用
|
|
21
|
+
*/
|
|
22
|
+
export function isSafeFn (fn) {
|
|
23
|
+
const hit = DANGEROUS_PATTERNS.find(pattern => pattern.test(fn.toString()))
|
|
24
|
+
if (hit) {
|
|
25
|
+
console.warn('[security] 函数包含高危调用,已阻止执行。', hit)
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
return true
|
|
29
|
+
}
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Storage 兼容层:提供与 js-cookie 同形的 get / set / remove API,
|
|
3
|
+
* 但底层只读写 sessionStorage / localStorage,完全不接触浏览器 Cookie。
|
|
4
|
+
*
|
|
5
|
+
* 背景:安全扫描要求站点不使用 Cookie。前端历史上只用 cookie 读过后端下发的
|
|
6
|
+
* xsrf_token;现在 `$cookie` / `Frame.cookie` 都指向本模块,老代码
|
|
7
|
+
* `this.$cookie.get(...)` 不用改,只是再也读写不到浏览器 Cookie。
|
|
8
|
+
*
|
|
9
|
+
* 语义约定:
|
|
10
|
+
* - get(key):先查 sessionStorage,再查 localStorage;已过期的值当不存在处理(顺手清掉)
|
|
11
|
+
* - set(key, value, options):默认写 localStorage;options.session = true 写 sessionStorage;
|
|
12
|
+
* options.expires 支持「天数」或 Date(对齐 js-cookie),过期时间另存在 `${key}.expires`;
|
|
13
|
+
* 同名键只保留一份(写入一个存储区时清掉另一个存储区的同名键)
|
|
14
|
+
* - remove(key):两个存储区连同过期标记一起清掉
|
|
15
|
+
* 所有读写都包在 try/catch 里:隐私模式 / 禁用存储时退化为 no-op,不抛错。
|
|
16
|
+
*/
|
|
17
|
+
const EXPIRES_SUFFIX = '.expires'
|
|
18
|
+
|
|
19
|
+
const getArea = (session) => {
|
|
20
|
+
try {
|
|
21
|
+
return session ? window.sessionStorage : window.localStorage
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 查找顺序:sessionStorage 优先于 localStorage
|
|
28
|
+
const getAreas = () => [getArea(true), getArea(false)].filter(Boolean)
|
|
29
|
+
|
|
30
|
+
const toExpiresAt = (expires) => {
|
|
31
|
+
if (expires === undefined || expires === null || expires === '') return null
|
|
32
|
+
if (expires instanceof Date) return expires.getTime()
|
|
33
|
+
const days = Number(expires)
|
|
34
|
+
if (Number.isNaN(days)) return null
|
|
35
|
+
return Date.now() + days * 864e5
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const toText = (value) => {
|
|
39
|
+
if (typeof value === 'string') return value
|
|
40
|
+
if (value === undefined || value === null) return ''
|
|
41
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
42
|
+
return String(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const readFrom = (area, key) => {
|
|
46
|
+
try {
|
|
47
|
+
const value = area.getItem(key)
|
|
48
|
+
if (value === null) return undefined
|
|
49
|
+
const expiresAt = Number(area.getItem(key + EXPIRES_SUFFIX))
|
|
50
|
+
if (expiresAt && Date.now() > expiresAt) {
|
|
51
|
+
area.removeItem(key)
|
|
52
|
+
area.removeItem(key + EXPIRES_SUFFIX)
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
return value
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const removeFrom = (area, key) => {
|
|
62
|
+
try {
|
|
63
|
+
area.removeItem(key)
|
|
64
|
+
area.removeItem(key + EXPIRES_SUFFIX)
|
|
65
|
+
} catch (e) {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const storage = {
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} key
|
|
71
|
+
* @returns {string | undefined} 不存在或已过期返回 undefined(与 js-cookie 一致)
|
|
72
|
+
*/
|
|
73
|
+
get (key) {
|
|
74
|
+
if (key === undefined || key === null) return undefined
|
|
75
|
+
key = String(key)
|
|
76
|
+
const areas = getAreas()
|
|
77
|
+
for (let i = 0; i < areas.length; i++) {
|
|
78
|
+
const value = readFrom(areas[i], key)
|
|
79
|
+
if (value !== undefined) return value
|
|
80
|
+
}
|
|
81
|
+
return undefined
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} key
|
|
85
|
+
* @param {*} value 非字符串会被转成字符串(对象走 JSON.stringify)
|
|
86
|
+
* @param {{ expires?: number | Date, session?: boolean }} [options]
|
|
87
|
+
* @returns {string | undefined} 实际写入的字符串
|
|
88
|
+
*/
|
|
89
|
+
set (key, value, options) {
|
|
90
|
+
if (key === undefined || key === null) return undefined
|
|
91
|
+
key = String(key)
|
|
92
|
+
const { session = false, expires } = options || {}
|
|
93
|
+
const target = getArea(session)
|
|
94
|
+
if (!target) return undefined
|
|
95
|
+
const text = toText(value)
|
|
96
|
+
const expiresAt = toExpiresAt(expires)
|
|
97
|
+
try {
|
|
98
|
+
target.setItem(key, text)
|
|
99
|
+
if (expiresAt) target.setItem(key + EXPIRES_SUFFIX, String(expiresAt))
|
|
100
|
+
else target.removeItem(key + EXPIRES_SUFFIX)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
const other = getArea(!session)
|
|
105
|
+
if (other) removeFrom(other, key)
|
|
106
|
+
return text
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} key
|
|
110
|
+
*/
|
|
111
|
+
remove (key) {
|
|
112
|
+
if (key === undefined || key === null) return
|
|
113
|
+
key = String(key)
|
|
114
|
+
getAreas().forEach(area => removeFrom(area, key))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 防 CSRF 令牌:由后端通过响应头 `xsrf_token`(或 `X-XSRF-TOKEN`)下发,
|
|
120
|
+
* axios 响应拦截器写入 sessionStorage(随标签页关闭失效),请求拦截器再回填到
|
|
121
|
+
* `xsrf_token` 请求头 / 下载类 URL 的 `xsrf_token` 查询参数。
|
|
122
|
+
*/
|
|
123
|
+
export const XSRF_TOKEN_KEY = 'xsrf_token'
|
|
124
|
+
|
|
125
|
+
/** @returns {string} 没有令牌时返回空串,方便直接拼接到 URL / 请求头 */
|
|
126
|
+
export const getXsrfToken = () => storage.get(XSRF_TOKEN_KEY) || ''
|
|
127
|
+
|
|
128
|
+
export const setXsrfToken = (token) => {
|
|
129
|
+
if (!token) return
|
|
130
|
+
storage.set(XSRF_TOKEN_KEY, token, { session: true })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const clearXsrfToken = () => storage.remove(XSRF_TOKEN_KEY)
|
|
134
|
+
|
|
135
|
+
export default storage
|