vue2-client 1.22.39 → 1.22.46

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.
Files changed (39) hide show
  1. package/.env.iot +1 -0
  2. package/.idea/af-vue2-client.iml +2 -3
  3. package/.idea/inspectionProfiles/Project_Default.xml +17 -0
  4. package/1yarn.lock +11850 -0
  5. package/logs/afgit.config.log +12 -0
  6. package/logs/afgit.config.log.2026-02-27 +7 -0
  7. package/logs/afgit.config_error.log +6 -0
  8. package/logs/afgit.config_error.log.2026-02-27 +3 -0
  9. package/package.json +1 -1
  10. package/pnpm-workspace.yaml +4 -0
  11. package/public/his/editor/mock/bind_data_with_images.html +78 -0
  12. package/public/his/editor/mock/blank.html +645 -0
  13. package/src/assets/img/80359c35a5465167cb25ff87bab49035d041a65558b35-YJOr3x.png +0 -0
  14. package/src/assets/img/hisLogo.png +0 -0
  15. package/src/assets/svg/unknown-icon.svg +3 -3
  16. package/src/base-client/components/common/AfMap/InfoWindowComponent.vue +141 -141
  17. package/src/base-client/components/common/AfMap/demo.vue +492 -492
  18. package/src/base-client/components/common/HIS/HButtons/HButtons.vue +89 -1
  19. package/src/base-client/components/common/HIS/HButtons/HButtonsSuffixDemo.vue +141 -0
  20. package/src/base-client/components/common/Upload/Upload.vue +2 -2
  21. package/src/base-client/components/common/XInspectionDetailDrawer/components/DeviceStatus.vue +1 -1
  22. package/src/base-client/components/his/XHisEditor/ImageReportDemo.vue +156 -0
  23. package/src/base-client/components/his/XHisEditor/XDocTree.vue +529 -529
  24. package/src/base-client/plugins/GetLoginInfoService.js +26 -1
  25. package/src/components/ImagePreview/ImagePreview.vue +323 -323
  26. package/src/constants/crypto.js +9 -1
  27. package/src/pages/login/Login.vue +107 -3
  28. package/src/router/async/router.map.js +6 -1
  29. package/src/services/user.js +7 -3
  30. package/src/utils/EncryptUtil.js +7 -2
  31. package/src/utils/login.js +29 -14
  32. package/src/utils/request.js +14 -3
  33. package/src/utils/util.js +27 -4
  34. package/vue.config.js +10 -0
  35. package/.idea/MarsCodeWorkspaceAppSettings.xml +0 -7
  36. package/.idea/deployment.xml +0 -14
  37. package/.idea/encodings.xml +0 -6
  38. package/.idea/gradle.xml +0 -7
  39. package/.idea/misc.xml +0 -6
@@ -25,6 +25,7 @@
25
25
  autocomplete="autocomplete"
26
26
  placeholder="请输入账户名"
27
27
  size="large"
28
+ :disabled="smsStep"
28
29
  >
29
30
  <a-icon slot="prefix" type="user" />
30
31
  </a-input>
@@ -36,10 +37,29 @@
36
37
  placeholder="请输入密码"
37
38
  size="large"
38
39
  type="password"
40
+ :disabled="smsStep"
39
41
  >
40
42
  <a-icon slot="prefix" type="lock" />
41
43
  </a-input>
42
44
  </a-form-item>
45
+ <a-form-item v-if="smsStep">
46
+ <a-input
47
+ v-decorator="['smsCode', {rules: [{ required: true, message: '请输入验证码', whitespace: true}]}]"
48
+ autocomplete="autocomplete"
49
+ placeholder="请输入短信验证码"
50
+ size="large"
51
+ :maxLength="8"
52
+ >
53
+ <a-icon slot="prefix" type="safety" />
54
+ <a-button
55
+ slot="suffix"
56
+ type="link"
57
+ :disabled="countdown > 0"
58
+ @click="resendSms"
59
+ style="padding: 0 4px;"
60
+ >{{ countdown > 0 ? `${countdown}s 后重发` : '重新发送' }}</a-button>
61
+ </a-input>
62
+ </a-form-item>
43
63
  <a-form-item>
44
64
  <a-button :loading="loading" htmlType="submit" size="large" class="btn" type="primary">登录</a-button>
45
65
  </a-form-item>
@@ -74,14 +94,68 @@ export default {
74
94
  return {
75
95
  loading: false,
76
96
  error: '',
77
- form: this.$form.createForm(this)
97
+ form: this.$form.createForm(this),
98
+ // V4 短信两阶段:smsStep=false 是 Step1 输入用户名+密码,true 是 Step2 输入短信验证码
99
+ smsStep: false,
100
+ maskedPhone: '',
101
+ countdown: 0,
102
+ countdownTimer: null,
103
+ // Step1 进入 Step2 后字段被 disabled 不可改,仍需提交 Step1 时的原始凭据
104
+ stage1Name: '',
105
+ stage1Password: ''
78
106
  }
79
107
  },
80
108
  computed: {
81
109
  ...mapState('setting', ['systemName', 'systemDesc', 'homePage', 'ticketPage', 'compatible', 'routeName', 'defaultAvatarUrl'])
82
110
  },
111
+ beforeDestroy () {
112
+ this.clearCountdown()
113
+ },
83
114
  methods: {
84
115
  ...mapMutations('account', ['setUser', 'setPermissions', 'setRoles']),
116
+ startCountdown (seconds) {
117
+ this.clearCountdown()
118
+ this.countdown = seconds
119
+ this.countdownTimer = setInterval(() => {
120
+ this.countdown -= 1
121
+ if (this.countdown <= 0) {
122
+ this.clearCountdown()
123
+ }
124
+ }, 1000)
125
+ },
126
+ clearCountdown () {
127
+ if (this.countdownTimer) {
128
+ clearInterval(this.countdownTimer)
129
+ this.countdownTimer = null
130
+ }
131
+ this.countdown = 0
132
+ },
133
+ exitSmsStep () {
134
+ this.smsStep = false
135
+ this.maskedPhone = ''
136
+ this.clearCountdown()
137
+ },
138
+ enterSmsStep (resp) {
139
+ this.$message.info(resp.states)
140
+ this.smsStep = true
141
+ this.maskedPhone = resp.maskedPhone || ''
142
+ this.startCountdown(resp.expireSeconds || 60)
143
+ },
144
+ resendSms () {
145
+ // 重新触发 Step1:不带 smsCode,后端会复用 userId 下发新的验证码并返回 smsPending
146
+ this.loading = true
147
+ V4Login(this.stage1Name, this.stage1Password, this.routeName).then(res => {
148
+ if (res && res.needsSmsCode) {
149
+ this.enterSmsStep(res)
150
+ } else {
151
+ this.$message.info('重发失败,请稍后重试')
152
+ }
153
+ }).catch(msg => {
154
+ this.$message.info(msg || '重发失败')
155
+ }).finally(() => {
156
+ this.loading = false
157
+ })
158
+ },
85
159
  onSubmit (e) {
86
160
  e.preventDefault()
87
161
  this.form.validateFields((err) => {
@@ -106,7 +180,14 @@ export default {
106
180
  break
107
181
  }
108
182
  case 'V4' : {
109
- V4Login(name, password, this.routeName).then(this.afterLoginV4).catch(msg => {
183
+ // V4 短信两阶段:Step1 不带 smsCode(密码正确→下发短信);Step2 带 smsCode(验证通过→转发上游拿 token)
184
+ const smsCode = this.smsStep ? this.form.getFieldValue('smsCode') : undefined
185
+ // 把 Step1 凭据缓存到 data.Step1,确保 Step2 时字段 disabled 也能取出原值
186
+ if (!this.smsStep) {
187
+ this.stage1Name = name
188
+ this.stage1Password = password
189
+ }
190
+ V4Login(name, password, this.routeName, smsCode).then(res => this.afterLoginV4(res)).catch(msg => {
110
191
  this.error = msg
111
192
  this.loading = false
112
193
  })
@@ -132,12 +213,35 @@ export default {
132
213
  }
133
214
  },
134
215
  afterLoginV4 (res) {
216
+ // V4 短信两阶段 Step1:密码正确 -> 后端下发短信并返回 smsPending
217
+ // 响应字段:{ states, needsSmsCode: true, maskedPhone, expireSeconds }
218
+ if (res && res.needsSmsCode) {
219
+ this.enterSmsStep(res)
220
+ this.loading = false
221
+ return
222
+ }
223
+ // 仅当 states 含"登录成功"或响应含 access_token 时才视为成功。
224
+ const isSuccess = res && res.access_token
225
+ if (!isSuccess) {
226
+ this.loading = false
227
+ const errMsg = (res && res.states) || '登录失败'
228
+ this.$message.info(errMsg)
229
+ return
230
+ }
135
231
  setV4AccessToken(res)
136
232
  let data = res.resources
137
- if (data.data) {
233
+ if (data && data.data) {
138
234
  data = data.data
139
235
  }
236
+ // 若上游 Step2 返回的是 token 而不是 resources,说明上游不是 V4 业务服务器:
237
+ // 此时前端没有 routes/permissions 信息,提示用户业务侧需要实现 V4 风格返回。
238
+ if (!data) {
239
+ this.loading = false
240
+ this.$message.info('登录成功但上游未返回路由信息,请联系管理员')
241
+ return
242
+ }
140
243
  this.$login.login(data).then(() => {
244
+ this.exitSmsStep()
141
245
  this.afterGeneral(data)
142
246
  if (data.deps === '用户工单登记') {
143
247
  this.$router.push(this.ticketPage).catch(() => {
@@ -57,7 +57,7 @@ routerResource.example = {
57
57
  path: 'example',
58
58
  name: '示例主页面',
59
59
  // component: () => import('@vue2-client/pages/LayoutTest/index.vue')
60
- component: () => import('@vue2-client/base-client/components/common/XDescriptions/demo.vue')
60
+ // component: () => import('@vue2-client/base-client/components/common/XDescriptions/demo.vue')
61
61
  // component: () => import('@vue2-client/base-client/components/his/HChart/demo.vue'),
62
62
  // component: () => import('@vue2-client/pages/WorkflowDetail/WorkFlowDemo.vue'),
63
63
  // component: () => import('@vue2-client/base-client/components/common/XFormTable/demo.vue')
@@ -94,6 +94,11 @@ routerResource.example = {
94
94
  // component: () => import('@vue2-client/pages/ReportGrid/index.vue'),
95
95
  // component: () => import('@vue2-client/base-client/components/common/HIS/demo.vue'),
96
96
  // component: () => import('@vue2-client/components/MarkdownEditor/demo.vue'),
97
+ // component: () => import('@vue2-client/base-client/components/his/XHisEditor/ImageReportDemo.vue')
98
+ // component: () => import('@vue2-client/components/MarkdownEditor/demo.vue'),
99
+ component: () => import('@vue2-client/base-client/components/common/HIS/HButtons/HButtonsSuffixDemo.vue')
100
+
101
+
97
102
  }
98
103
  // routerResource.example = () =>
99
104
  // import('@vue2-client/pages/Example')
@@ -38,16 +38,20 @@ export async function modifyPassword (name, password, newPassword) {
38
38
  })
39
39
  }
40
40
 
41
- export async function V4Login (name, password, routeName) {
41
+ export async function V4Login (name, password, routeName, smsCode) {
42
42
  // 适配未接入基座的单应用项目登录请求加密,如af-iot-web
43
43
  if (process.env.VUE_APP_SINGLE_PAPER === 'FALSE') {
44
44
  password = bcrypt.hashSync(password, 10)
45
45
  }
46
- return request(V4_LOGIN, METHOD.POST, {
46
+ const payload = {
47
47
  username: name,
48
48
  password: password,
49
49
  resourceName: routeName
50
- })
50
+ }
51
+ if (smsCode) {
52
+ payload.smsCode = smsCode
53
+ }
54
+ return request(V4_LOGIN, METHOD.POST, payload)
51
55
  }
52
56
 
53
57
  export async function V4RefreshToken () {
@@ -68,14 +68,19 @@ export default {
68
68
  return null
69
69
  }
70
70
  },
71
-
71
+ normalizeEncryptData (data) {
72
+ if (typeof data === 'string') {
73
+ return data
74
+ }
75
+ return JSON.stringify(data)
76
+ },
72
77
  AESEncryptCBC(data, CRYPTO_KEY) {
73
78
  // 生成随机IV(每次加密不同)
74
79
  const iv = CryptoJS.lib.WordArray.random(16)
75
80
 
76
81
  // 使用 AES CBC 模式进行加密
77
82
  const encrypted = CryptoJS.AES.encrypt(
78
- JSON.stringify(data),
83
+ this.normalizeEncryptData(data),
79
84
  CryptoJS.enc.Hex.parse(CRYPTO_KEY),
80
85
  {
81
86
  iv: iv, // IV
@@ -13,7 +13,7 @@ import {
13
13
  } from '@vue2-client/store/mutation-types'
14
14
  import { setAuthorization } from '@vue2-client/utils/request'
15
15
  import { loginGen } from '@vue2-client/base-client/plugins/GetLoginInfoService'
16
- import { RSA_PRIVATE_KEY } from '@vue2-client/constants/crypto'
16
+ import { RSA_PRIVATE_KEY, V3_RSA_PRIVATE_KEY } from '@vue2-client/constants/crypto'
17
17
  import EncryptUtil from '@vue2-client/utils/EncryptUtil'
18
18
  // 认证类型
19
19
  const AUTH_TYPE = {
@@ -44,14 +44,13 @@ export function checkSingleAuthorization(authType = AUTH_TYPE.BEARER, token) {
44
44
 
45
45
  function setAccessToken(data, jwt) {
46
46
  const token = jwt || data
47
- if (token) {
48
- localStorage.setItem(ACCESS_TOKEN, token)
49
- let timestamp = new Date().getTime() // 当前的时间戳
50
- timestamp = timestamp + 12 * 60 * 60 * 1000
51
- // 格式化时间获取年月日, 登陆过期时间
52
- const dateAfter = new Date(timestamp)
53
- setAuthorization({ token: token, expireAt: dateAfter })
54
- }
47
+ if (!token) return
48
+ localStorage.setItem(ACCESS_TOKEN, token)
49
+ let timestamp = new Date().getTime() // 当前的时间戳
50
+ timestamp = timestamp + 12 * 60 * 60 * 1000
51
+ // 格式化时间获取年月日, 登陆过期时间
52
+ const dateAfter = new Date(timestamp)
53
+ setAuthorization({ token, expireAt: dateAfter })
55
54
  }
56
55
 
57
56
  function afterGeneral(result, options) {
@@ -97,10 +96,12 @@ function afterLogin(res, options) {
97
96
  JSON.stringify({ username: name, password: password })
98
97
  )
99
98
  const jwt = res.jwtNew
100
- // 如果返回字段有s说明是走的V3XuanJian网关
101
- if(res.s || res.session) {
102
- setV4SessionKey({session: res.s || res.session})
99
+ const isV3Gateway = !!(res.s || res.session)
100
+ // V3 网关:响应含 s/session 表示启用请求体 AES 加密,需先解密会话密钥
101
+ if (isV3Gateway) {
102
+ setSessionKey({ session: res.s || res.session })
103
103
  }
104
+ setAccessToken(data, jwt)
104
105
  // 获取路由配置
105
106
  getRoutesConfig(data, setting.routeName)
106
107
  .then((result) => {
@@ -116,7 +117,6 @@ function afterLogin(res, options) {
116
117
  }
117
118
  result.functions.unshift(resourceManageMain)
118
119
  afterGeneral(result, options)
119
- setAccessToken(data, jwt)
120
120
  if (result.deps === '用户工单登记') {
121
121
  router.push(setting.ticketPage).catch(() => {
122
122
  })
@@ -173,7 +173,11 @@ function getWebConfigFromStorage() {
173
173
  }
174
174
  }
175
175
 
176
- /** 解析登录接口返回的 session 并写入 v4-session-key(受 webConfig.requestEncrypt 控制) */
176
+ /**
177
+ * V4 登录:解析 session 并写入 v4-session-key。
178
+ * 受 webConfig.setting.requestEncrypt 开关控制,仅当配置开启且响应含 session 时才解密。
179
+ * 使用 RSA_PRIVATE_KEY(V4 产品密钥)。
180
+ */
177
181
  export function setV4SessionKey(res) {
178
182
  const webConfig = getWebConfigFromStorage()
179
183
  if (!res?.session || !webConfig?.setting?.requestEncrypt) return
@@ -181,6 +185,17 @@ export function setV4SessionKey(res) {
181
185
  if (sessionKey) localStorage.setItem(V4_SESSION_KEY, sessionKey)
182
186
  }
183
187
 
188
+ /**
189
+ * V3 玄简网关:解析 session 并写入 v4-session-key。
190
+ * 登录响应含 s/session 时无条件解密(不读 requestEncrypt 开关),表示后续请求走 AES 加密。
191
+ * 使用 V3_RSA_PRIVATE_KEY(玄简网关密钥),与 V4 的 setV4SessionKey 密钥和触发条件均不同。
192
+ */
193
+ export function setSessionKey(res) {
194
+ if (!res?.session) return
195
+ const sessionKey = EncryptUtil.RSADecryptRaw(res.session, V3_RSA_PRIVATE_KEY)
196
+ if (sessionKey) localStorage.setItem(V4_SESSION_KEY, sessionKey)
197
+ }
198
+
184
199
  export function setV4AccessToken(res) {
185
200
  const payload = unwrapV4LoginPayload(res)
186
201
  if (!payload?.access_token) return
@@ -100,6 +100,16 @@ async function request(url, method, params, config) {
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * 规范 Authorization 头的 Bearer 前缀,避免重复拼接
105
+ * @param {string} token
106
+ * @returns {string}
107
+ */
108
+ export function formatBearerToken(token) {
109
+ if (!token) return token
110
+ return token.startsWith('Bearer ') ? token : `Bearer ${token}`
111
+ }
112
+
103
113
  /**
104
114
  * 设置认证信息
105
115
  * @param auth {Object}
@@ -232,8 +242,9 @@ function loadInterceptors() {
232
242
  // 让每个请求携带自定义 token 请根据实际情况自行修改
233
243
  if (token) {
234
244
  if (config.url !== V4_LOGIN) {
235
- // V4 环境则添加 V4请求头
236
- config.headers[V4_ACCESS_TOKEN] = token
245
+ const isV3 = ['V2', 'V3'].includes(getSystemVersion())
246
+ // V3 网关:Authorization 需 Bearer 前缀;Access-Token 始终传原始 token
247
+ config.headers[V4_ACCESS_TOKEN] = isV3 ? formatBearerToken(token) : token
237
248
  config.headers[ACCESS_TOKEN] = token
238
249
  }
239
250
  }
@@ -249,7 +260,7 @@ function loadInterceptors() {
249
260
  !config.url.includes('/logic/openapi/') &&
250
261
  !config.url.includes('devApi') &&
251
262
  !config.url.includes('auth/login') &&
252
- config.url.includes('api/af-')
263
+ (config.url.includes('api/af-') || config.url.includes('rs/user/userLogin'))
253
264
  ) {
254
265
  if (config.data && !(config.data instanceof FormData)) {
255
266
  if (v4SessionKey) {
package/src/utils/util.js CHANGED
@@ -336,10 +336,33 @@ export function _moment(...args) {
336
336
  return moment(...args)
337
337
  }
338
338
 
339
- export function base64ToFile(base64Data, filename = 'image.png') {
340
- // 将 base64 转换为 blob
339
+ function getBase64Mime(base64Data) {
341
340
  const arr = base64Data.split(',')
342
- const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
341
+ return arr[0].match(/:(.*?);/)?.[1] || 'image/png'
342
+ }
343
+
344
+ function getImageExtensionFromMime(mime) {
345
+ const extMap = {
346
+ 'image/jpeg': 'jpg',
347
+ 'image/jpg': 'jpg',
348
+ 'image/png': 'png',
349
+ 'image/gif': 'gif',
350
+ 'image/bmp': 'bmp',
351
+ 'image/webp': 'webp'
352
+ }
353
+ return extMap[mime] || mime.replace('image/', '') || 'png'
354
+ }
355
+
356
+ function resolveBase64FileName(filename, mime) {
357
+ const ext = getImageExtensionFromMime(mime)
358
+ const baseName = filename.replace(/\.[^.]+$/, '')
359
+ return `${baseName}.${ext}`
360
+ }
361
+
362
+ export function base64ToFile(base64Data, filename = 'image') {
363
+ const arr = base64Data.split(',')
364
+ const mime = getBase64Mime(base64Data)
365
+ const finalName = resolveBase64FileName(filename, mime)
343
366
  const bstr = atob(arr[1])
344
367
  let n = bstr.length
345
368
  const u8arr = new Uint8Array(n)
@@ -352,5 +375,5 @@ export function base64ToFile(base64Data, filename = 'image.png') {
352
375
  const blob = new Blob([u8arr], { type: mime })
353
376
 
354
377
  // 创建 File 对象
355
- return new File([blob], filename, { type: mime })
378
+ return new File([blob], finalName, { type: mime })
356
379
  }
package/vue.config.js CHANGED
@@ -12,6 +12,7 @@ const isProd = process.env.NODE_ENV === 'production'
12
12
 
13
13
  // v4 产品演示
14
14
  const liuli = 'http://192.168.50.67:31567'
15
+ // const v3Server = 'http://127.0.0.1:8077'
15
16
  const v3Server = 'http://192.168.50.67:31567'
16
17
  const geoserver = 'http://192.168.50.67:31567'
17
18
  // const gateway = 'http://192.168.50.67:31467'
@@ -36,6 +37,15 @@ module.exports = {
36
37
  port: 8020,
37
38
  // If you want to turn on the proxy, please remove the mockjs /src/main.jsL11
38
39
  proxy: {
40
+ '/api/af-auth/login': {
41
+ target: v3Server,
42
+ },
43
+ '/rs/logic/getLogin': {
44
+ pathRewrite: {
45
+ '^/rs/logic/getLogin': '/getLogin'
46
+ },
47
+ target: v3Server,
48
+ },
39
49
  // '/apply-image': {
40
50
  // target: revenue,
41
51
  // },
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
4
- <option name="ckgOperationStatus" value="SUCCESS" />
5
- <option name="progress" value="1.0" />
6
- </component>
7
- </project>
@@ -1,14 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
4
- <serverData>
5
- <paths name="50.4">
6
- <serverdata>
7
- <mappings>
8
- <mapping local="$PROJECT_DIR$" web="/" />
9
- </mappings>
10
- </serverdata>
11
- </paths>
12
- </serverData>
13
- </component>
14
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="UTF-8">
4
- <file url="PROJECT" charset="UTF-8" />
5
- </component>
6
- </project>
package/.idea/gradle.xml DELETED
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="GradleMigrationSettings" migrationVersion="1" />
4
- <component name="GradleSettings">
5
- <option name="parallelModelFetch" value="true" />
6
- </component>
7
- </project>
package/.idea/misc.xml DELETED
@@ -1,6 +0,0 @@
1
- <project version="4">
2
- <component name="ExternalStorageConfigurationManager" enabled="true" />
3
- <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
4
- <output url="file://$PROJECT_DIR$/out" />
5
- </component>
6
- </project>