@zscreate/zhxy-app-component 1.0.213 → 1.0.215

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,128 +1,128 @@
1
- <script>
2
- import FullModal from "../../fullModal/fullModal.vue";
3
-
4
- export default {
5
- name: "responsibility",
6
- props: {
7
- value: {
8
- type: String,
9
- default: ''
10
- },
11
- widget: {
12
- type: Object,
13
- default: () => ({})
14
- },
15
- },
16
- components: {FullModal},
17
- data() {
18
- return {
19
- countDown: 0,
20
- force: false,
21
- clearTimeout: null,
22
- dataModel: false,
23
- isShow: false,
24
- }
25
- },
26
- watch: {
27
- value: {
28
- handler(val) {
29
- if (!val) return this.dataModel = false
30
- this.dataModel = true
31
- },
32
- immediate: true
33
- },
34
- dataModel(val) {
35
- this.$emit('input', val ? true : '')
36
- this.$emit('change', val ? true: '')
37
- },
38
- },
39
- methods: {
40
- handleClick() {
41
- if (this.widget.options.disabled) {
42
- this.isShow = true
43
- return
44
- }
45
- this.isShow = true
46
- this.countDown = 0
47
- clearInterval(this.clearTimeout)
48
- if (this.widget.options.force) {
49
- this.countDown = this.widget.options.countDown
50
- this.clearTimeout = setInterval(() => {
51
- if (this.countDown < 0) {
52
- clearInterval(this.clearTimeout)
53
- }
54
- this.countDown -= 1
55
- }, 1000)
56
- }
57
- },
58
- confirm() {
59
- if (this.countDown <= 0) {
60
- this.dataModel = true
61
- this.isShow = false
62
- }
63
- },
64
- cancel() {
65
- this.isShow = false
66
- this.dataModel = false
67
- clearInterval(this.clearTimeout)
68
- },
69
- handleCheck() {
70
- const { disabled, force} = this.widget.options
71
- if (disabled) return
72
- if (force && !disabled) {
73
- if (this.dataModel) return this.dataModel = false
74
- return this.handleClick()
75
- }
76
- this.dataModel = !this.dataModel
77
- },
78
- change(e) {
79
- console.log(e)
80
- },
81
- disabledScroll() {
82
- return
83
- }
84
- }
85
- }
86
- </script>
87
-
88
- <template>
89
- <view>
90
- <full-modal v-model="isShow">
91
- <view style="height: 100%; background-color: #fff" @touchmove.stop.prevent="disabledScroll">
92
- <scroll-view scroll-y="true" style="height: calc(100% - 100rpx); " >
93
- <view style="padding: 20rpx 40rpx;">
94
- <rich-text :nodes="widget.options.content" style="text-align: left"></rich-text>
95
- </view>
96
- <view v-if="!widget.options.disabled" style="display: flex; padding: 20rpx; margin-bottom: 20rpx; justify-content: center">
97
- <button @click="confirm" type="primary" style="margin-right: 10px; border-radius: 10rpx; padding: 0 20px; height: 60rpx; line-height: 60rpx; font-size: 26rpx" :disabled="widget.options.force && countDown > 0">我同意 <text v-if="widget.options.force && countDown > 0">({{ countDown }}s)</text></button>
98
- <button style="height: 60rpx; line-height: 60rpx; font-size: 26rpx; border-radius: 10rpx; color: #333" @click="cancel">不同意</button>
99
- </view>
100
- <view v-else style="display: flex; justify-content: center; align-items: center">
101
- <button @click="isShow = false" type="primary" style="margin-bottom: 20px; border-radius: 10rpx; padding: 0 20px; height: 60rpx; line-height: 60rpx; font-size: 26rpx; " >关闭</button>
102
- </view>
103
- </scroll-view>
104
- </view>
105
- </full-modal>
106
- <view class="responsibility_check" >
107
- <view @click.stop="handleCheck">
108
- <label>
109
- <checkbox :disabled="widget.options.disabled" :readonly="widget.options.disabled" v-if="!isShow" :checked="dataModel" color="#007AFF" style="transform: scale(0.8,0.8);"/>
110
- </label>
111
- </view>
112
- <view class="option-font-size" @click.stop="handleClick">{{ widget.options.title }}</view>
113
-
114
- </view>
115
- </view>
116
-
117
- </template>
118
-
119
- <style lang="less">
120
- .responsibility_check {
121
- display: flex;
122
- align-items: center;
123
- /deep/ .uni-checkbox-input {
124
- width: 18px;
125
- height: 18px;
126
- }
127
- }
1
+ <script>
2
+ import FullModal from "../../fullModal/fullModal.vue";
3
+
4
+ export default {
5
+ name: "responsibility",
6
+ props: {
7
+ value: {
8
+ type: String,
9
+ default: ''
10
+ },
11
+ widget: {
12
+ type: Object,
13
+ default: () => ({})
14
+ },
15
+ },
16
+ components: {FullModal},
17
+ data() {
18
+ return {
19
+ countDown: 0,
20
+ force: false,
21
+ clearTimeout: null,
22
+ dataModel: false,
23
+ isShow: false,
24
+ }
25
+ },
26
+ watch: {
27
+ value: {
28
+ handler(val) {
29
+ if (!val) return this.dataModel = false
30
+ this.dataModel = true
31
+ },
32
+ immediate: true
33
+ },
34
+ dataModel(val) {
35
+ this.$emit('input', val ? true : '')
36
+ this.$emit('change', val ? true: '')
37
+ },
38
+ },
39
+ methods: {
40
+ handleClick() {
41
+ if (this.widget.options.disabled) {
42
+ this.isShow = true
43
+ return
44
+ }
45
+ this.isShow = true
46
+ this.countDown = 0
47
+ clearInterval(this.clearTimeout)
48
+ if (this.widget.options.force) {
49
+ this.countDown = this.widget.options.countDown
50
+ this.clearTimeout = setInterval(() => {
51
+ if (this.countDown < 0) {
52
+ clearInterval(this.clearTimeout)
53
+ }
54
+ this.countDown -= 1
55
+ }, 1000)
56
+ }
57
+ },
58
+ confirm() {
59
+ if (this.countDown <= 0) {
60
+ this.dataModel = true
61
+ this.isShow = false
62
+ }
63
+ },
64
+ cancel() {
65
+ this.isShow = false
66
+ this.dataModel = false
67
+ clearInterval(this.clearTimeout)
68
+ },
69
+ handleCheck() {
70
+ const { disabled, force} = this.widget.options
71
+ if (disabled) return
72
+ if (force && !disabled) {
73
+ if (this.dataModel) return this.dataModel = false
74
+ return this.handleClick()
75
+ }
76
+ this.dataModel = !this.dataModel
77
+ },
78
+ change(e) {
79
+ console.log(e)
80
+ },
81
+ disabledScroll() {
82
+ return
83
+ }
84
+ }
85
+ }
86
+ </script>
87
+
88
+ <template>
89
+ <view>
90
+ <full-modal v-model="isShow">
91
+ <view style="height: 100%; background-color: #fff" @touchmove.stop.prevent="disabledScroll">
92
+ <scroll-view scroll-y="true" style="height: calc(100% - 100rpx); " >
93
+ <view style="padding: 20rpx 40rpx;">
94
+ <rich-text :nodes="widget.options.content" style="text-align: left"></rich-text>
95
+ </view>
96
+ <view v-if="!widget.options.disabled" style="display: flex; padding: 20rpx; margin-bottom: 20rpx; justify-content: center">
97
+ <button @click="confirm" type="primary" style="margin-right: 10px; border-radius: 10rpx; padding: 0 20px; height: 60rpx; line-height: 60rpx; font-size: 26rpx" :disabled="widget.options.force && countDown > 0">我同意 <text v-if="widget.options.force && countDown > 0">({{ countDown }}s)</text></button>
98
+ <button style="height: 60rpx; line-height: 60rpx; font-size: 26rpx; border-radius: 10rpx; color: #333" @click="cancel">不同意</button>
99
+ </view>
100
+ <view v-else style="display: flex; justify-content: center; align-items: center">
101
+ <button @click="isShow = false" type="primary" style="margin-bottom: 20px; border-radius: 10rpx; padding: 0 20px; height: 60rpx; line-height: 60rpx; font-size: 26rpx; " >关闭</button>
102
+ </view>
103
+ </scroll-view>
104
+ </view>
105
+ </full-modal>
106
+ <view class="responsibility_check" >
107
+ <view @click.stop="handleCheck">
108
+ <label>
109
+ <checkbox :disabled="widget.options.disabled" :readonly="widget.options.disabled" v-if="!isShow" :checked="dataModel" color="#007AFF" style="transform: scale(0.8,0.8);"/>
110
+ </label>
111
+ </view>
112
+ <view class="option-font-size" @click.stop="handleClick">{{ widget.options.title }}</view>
113
+
114
+ </view>
115
+ </view>
116
+
117
+ </template>
118
+
119
+ <style lang="less">
120
+ .responsibility_check {
121
+ display: flex;
122
+ align-items: center;
123
+ /deep/ .uni-checkbox-input {
124
+ width: 18px;
125
+ height: 18px;
126
+ }
127
+ }
128
128
  </style>
@@ -2,11 +2,11 @@
2
2
  <view class="form-item"
3
3
  :class="{ showFalse: showFalse, isScroll: isScroll, isCorrected: widget.options.isCorrected ? true : false }"
4
4
  @click="resetShowFalse">
5
- <view v-if="widget.options.isCorrected" class="correctedWrap">
6
- 原值是: {{ widget.options.oldValue }}
5
+ <view v-if="widget.options.isCorrected" class="editCorrent correctedWrap" >
6
+ <view @click.native="CorrentItem">原值是: {{ correctOldValue(widget.options.oldValue) }}</view>
7
7
  </view>
8
8
  <view class="editCorrent"
9
- v-if="isCorrect && widget.options.canCorrect"
9
+ v-else-if="isCorrect && widget.options.canCorrect"
10
10
  @click.stop="CorrentItem">
11
11
  <!-- ===undefined兼容老版本表单 -->
12
12
  <text style="color: #4480E3">纠错</text>
@@ -65,7 +65,7 @@
65
65
  </view>
66
66
  <textarea v-else maxlength="9999999" class="form-input cover-text" v-model="dataModel"
67
67
  :style="{ height: (widget.options.height || 100) * 2 + 'px'}"
68
- placeholder-style="line-height:60rpx" placeholder-class="form-input-placeholder"
68
+ placeholder-class="form-input-placeholder"
69
69
  :placeholder="widget.options.placeholder" :disabled="widget.options.disabled">
70
70
  </textarea>
71
71
  </view>
@@ -261,7 +261,7 @@
261
261
  {{ widget.name }}
262
262
  </view>
263
263
  <view class="evan-form-item-container__main" :style="mContentStyle">
264
- <unploadFile :widget="widget" :action="getUploadFileUrl(widget)" @filePost="filePost" @fileDelete="fileDelete"
264
+ <unploadFile :widget="widget" ref="imgUpload" :action="getUploadFileUrl(widget)" @filePost="filePost" @fileDelete="fileDelete"
265
265
  :defaultImg="dataModel"></unploadFile>
266
266
  </view>
267
267
  </view>
@@ -301,7 +301,7 @@
301
301
  {{ widget.name }}
302
302
  </view>
303
303
  <view class="evan-form-item-container__main" :style="mContentStyle">
304
- <lb-picker style="width: 100%" :placeholder="widget.options.placeholder" ref="picker" v-model="dataModel"
304
+ <lb-picker style="width: 100%" :placeholder="widget.options.placeholder" ref="lb_picker" v-model="dataModel"
305
305
  @change="cityChecked" :value="widget.options.defaultValue" :disabled="widget.options.disabled">
306
306
  </lb-picker>
307
307
  </view>
@@ -722,6 +722,7 @@ export default {
722
722
  checkboxAndRadioOptions: [],
723
723
  selectedData: [],
724
724
  locationInfo: {},
725
+ correctObj: undefined
725
726
  }
726
727
  },
727
728
  mounted() {
@@ -781,6 +782,15 @@ export default {
781
782
  uni.$off(this.widget.model);
782
783
  },
783
784
  methods: {
785
+ correctOldValue(oldValue) {
786
+ const type = this.widget.type
787
+ if (Array.isArray(oldValue) && oldValue.length === 0) return '【空】'
788
+ if (type === 'switch') return Boolean(oldValue) ? 'true' : 'false'
789
+ if (type === 'slider') return Number(oldValue || 0)
790
+
791
+ const res = ['', undefined, null].includes(oldValue) ? '【空】': oldValue
792
+ return res.length > 10 ? res.substring(0, 10) + '...' : res
793
+ },
784
794
  numberBoxPlus(e) {
785
795
  console.log(e, this.dataModel);
786
796
  this.dataModel = this.widget.options.step ? e.value + this.widget.options.step : e.value + 1
@@ -1067,13 +1077,105 @@ export default {
1067
1077
  getApp().globalData.correntFormData
1068
1078
  getApp().globalData.correntWidget = {
1069
1079
  ...this.widget,
1070
- dataModel: this.dataModel
1080
+ dataModel: this.dataModel,
1081
+ correctInfo: {
1082
+ isSubTable: this.tableKey ? 1 : 0, //是否纠错子表单 0否 1是
1083
+ subTableCode: this.tableKey, //新增纠错子表名
1084
+ correctIndex: this.tableIndex, //新增纠错索引
1085
+ },
1086
+ setCorrectData: (correctObj) => {
1087
+ const { oldValue: oldVal, newDataObj: newVal } = correctObj
1088
+ this.correctObj = correctObj
1089
+ console.log('setCorrectData', correctObj)
1090
+ if (['checkbox', 'radio'].includes(this.widget.type)) {
1091
+ correctObj.dictOptions = this.checkboxAndRadioOptions
1092
+ }
1093
+ this.setCorrectStatus(correctObj)
1094
+
1095
+ // const options = this.widget.options
1096
+ this.dataModel = newVal
1097
+ // if (!options.isCorrected) {
1098
+ // this.$set(this.widget.options, 'isCorrected', true)
1099
+ // this.widget.options.oldValue = oldVal
1100
+ // }
1101
+ setTimeout(() => {
1102
+ if (this.widget.type === 'imgupload') this.$refs.imgUpload.showImage()
1103
+ }, 0)
1104
+ },
1071
1105
  }
1072
1106
  console.log(getApp().globalData.correntWidget)
1073
- Pubsub.subscribe('corrent_item', (msg) => {
1074
- console.log(msg)
1107
+ Pubsub.publish('correct_item', {
1108
+ originData: getApp().globalData.correntWidget,
1109
+ correctObj: this.correctObj
1075
1110
  })
1076
- Pubsub.publish('corrent_item', getApp().globalData.correntWidget)
1111
+ },
1112
+ setCorrectStatus(correctObj) {
1113
+ try {
1114
+ if (this.widget.options.oldValue === undefined) {
1115
+ // 如果当前oldValue为undefined,说明是第一次纠错,需要保存一下oldValue;之后oldValue值不变
1116
+ let oldValue = this.tableKey
1117
+ ? this.models[this.tableKey][this.tableIndex][this.widget.model]
1118
+ : this.models[this.widget.model];
1119
+
1120
+ if (correctObj.oldValue) oldValue = correctObj.oldValue
1121
+ // 获取select radio select 字典信息
1122
+ const dictOptions = correctObj.dictOptions
1123
+
1124
+ const getOldValue = (oldValue, keyName) => {
1125
+ if (!Array.isArray(oldValue)) return ""
1126
+ return Array.isArray(oldValue) ? oldValue.reduce((i, j) => j[keyName] + "," + i, "").slice(0, -1) : ""
1127
+ }
1128
+
1129
+ const getDictValue = (oldValue) => {
1130
+ if (!Array.isArray(oldValue)) {
1131
+ const findItem = dictOptions.find( item => item.value === oldValue)
1132
+ if (findItem) {
1133
+ return findItem.text || findItem.label
1134
+ }
1135
+ }
1136
+ if (!Array.isArray(oldValue)) return ""
1137
+ return oldValue.reduce((i, j) => {
1138
+ const findItem = dictOptions.find( item => item.value === j)
1139
+ if (findItem) return (findItem['text'] || findItem['label']) + "," + i
1140
+ }, "").slice(0, -1)
1141
+ }
1142
+ if (oldValue) {
1143
+ switch (this.widget.type) {
1144
+ case "cascader":
1145
+ oldValue = correctObj.cascaderLabel
1146
+ break;
1147
+ case "userSelectorByRole":
1148
+ case "userSelector":
1149
+ oldValue = Array.isArray(oldValue) ? oldValue.reduce((i, j) => i + " " + (j.realname || j.username), "") : ""
1150
+ break
1151
+ case 'time':
1152
+ oldValue = Array.isArray(oldValue) ? oldValue.reduce((i, j) => i + " " + j, '') : oldValue
1153
+ break
1154
+ case 'radio':
1155
+ case 'checkbox':
1156
+ case "select":
1157
+ oldValue = getDictValue(oldValue)
1158
+ break
1159
+ case "imgupload":
1160
+ case "fileupload":
1161
+ oldValue = getOldValue(correctObj['oldValue'], 'name')
1162
+ break
1163
+ case "deptSelector":
1164
+ oldValue = getOldValue(oldValue, 'departName')
1165
+ break
1166
+ }
1167
+ }
1168
+ console.log(oldValue)
1169
+ this.$set(this.widget.options, "oldValue", oldValue);
1170
+ }
1171
+ this.$set(this.widget.options, "isCorrected", true);
1172
+ // this.updateModels(correctObj.newDataObj);
1173
+ } catch (err) {
1174
+ console.log(err);
1175
+ } finally {
1176
+ correctObj.dictOptions = []
1177
+ correctObj.cascaderLabel = undefined
1178
+ }
1077
1179
  },
1078
1180
  /****更新*userSelector**/
1079
1181
  updateUserSelectorModal() {
@@ -1681,6 +1783,12 @@ export default {
1681
1783
  if (this.widget.type === 'deptSelector') {
1682
1784
  this.updatedeptSelectorModal()
1683
1785
  }
1786
+ if (['select', 'radio', 'checkbox'].includes(this.widget.type) ) {
1787
+ this.updatedSelectModal()
1788
+ }
1789
+ if (this.widget.type === 'userSelectorByRole' ) {
1790
+ this.handleSelectRoleUserOk(v)
1791
+ }
1684
1792
  if (this.widget.type === 'number') {
1685
1793
  const { max, min } = this.widget.options
1686
1794
  if ((max || max === 0) && v > max) {
@@ -2061,5 +2169,9 @@ checkbox-group label {
2061
2169
  .uni-list-cell::after {
2062
2170
  z-index: 0 !important;
2063
2171
  }
2172
+
2173
+ ::v-deep .uni-slider-thumb {
2174
+ z-index: 1;
2175
+ }
2064
2176
  </style>
2065
2177
 
@@ -0,0 +1,15 @@
1
+ import {toAwait} from "../../../utils/util";
2
+ import Vue from 'vue'
3
+ export default {
4
+ methods: {
5
+ async getOpenId() {
6
+ let openId = Vue.prototype._openId
7
+ if (openId) return Promise.resolve(openId)
8
+
9
+ const [ err , res ] = await toAwait(this.$u.get('/sys/user/getUserPublicOpenId'))
10
+ openId = await this.$async_encryption(res.result)
11
+ Vue.prototype._openId = openId
12
+ return openId
13
+ },
14
+ }
15
+ }
@@ -87,14 +87,17 @@
87
87
  this.getEcho()
88
88
  },
89
89
  watch: {
90
- value(v) {
91
- console.log('vvv', v)
90
+ value(oldVal,newVal) {
91
+ if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
92
+ this.getdata(this.list, 0)
93
+ }
94
+ // this.bindChange({ detail: {value: v}})
95
+ // this.handleOk()
92
96
  }
93
97
  },
94
98
  methods: {
95
99
  bindChange: function(e) {
96
100
  const val = e.detail.value
97
- console.log(1)
98
101
  if (val[0] != this.indexList[0]) {
99
102
  this.indexList[0] = val[0];
100
103
  this.indexList[1] = 0;
@@ -129,6 +132,7 @@
129
132
  * @param {Number} xindex 第几级数据
130
133
  * **/
131
134
  async getdata(list, xindex) {
135
+ if (xindex === 0) this.cityLabel = []
132
136
  if (xindex > this.value.length-1) return;
133
137
  for (var j = 0; j < list.length; j++) {
134
138
  if (list[j].value == this.value[xindex]) {
@@ -149,13 +153,24 @@
149
153
  },
150
154
  //获得一级
151
155
  init() {
152
- return this.$u.get('/sys/dict/listProvince')
156
+ return new Promise(resolve => {
157
+ if (Vue.prototype.data_listProvince) return resolve(Vue.prototype.data_listProvince)
158
+ this.$u.get('/sys/dict/listProvince').then( res => {
159
+ Vue.prototype.data_listProvince = res
160
+ resolve(res)
161
+ })
162
+ })
153
163
  },
154
164
  //请求下级
155
165
  getnext(id) {
156
- return this.$u.get('/sys/dict/listAreaByCode', {
157
- code: id
158
- })
166
+ if (!Vue.prototype.data_listAreaByCode) Vue.prototype.data_listAreaByCode = {}
167
+ return new Promise(resolve => {
168
+ if (Vue.prototype.data_listAreaByCode[id]) return resolve(Vue.prototype.data_listAreaByCode[id])
169
+ this.$u.get('/sys/dict/listAreaByCode', {code: id}).then( res => {
170
+ Vue.prototype.data_listAreaByCode[id] = res
171
+ resolve(res)
172
+ })
173
+ })
159
174
  },
160
175
  handleclick() {
161
176
  if (this.disabled) return false;
@@ -85,7 +85,6 @@ export default {
85
85
  },
86
86
 
87
87
  methods: {
88
-
89
88
  handleClick() {
90
89
  if (this.uploadIng) {
91
90
  return uni.showToast({
@@ -196,23 +195,27 @@ export default {
196
195
  });
197
196
  },
198
197
  showImage() {
199
- this.emitList = cloneObj(this.defaultImg || [])
200
- this.showProgress = false;
201
- if (this.defaultImg && this.defaultImg.length && this.defaultImg.length > 0) {
202
- this.fileList = this.defaultImg
203
- .map((item, index) => {
204
- let tempItem = {
205
- ...item,
206
- };
207
- tempItem.url = this.uniEnv.imgUrl + tempItem.url;
208
- if (tempItem.fileId) {
209
- tempItem.url = (this.uniEnv.BASE_API || this.uniEnv.apiUrl) + `/sys/common/downloadZip?fileId=${tempItem.fileId}&openId=${this.openId}&token=${this.header['X-Access-Token']}`
210
- }
211
- return tempItem;
212
- })
198
+ this.$nextTick(() => {
199
+ this.$refs.uUpload.lists = []
200
+ this.fileList = []
201
+ this.emitList = cloneObj(this.defaultImg || [])
202
+ this.showProgress = false;
203
+ if (this.emitList && this.emitList.length && this.emitList.length > 0) {
213
204
 
214
- console.log(this.fileList)
215
- }
205
+ this.fileList = this.emitList
206
+ .map((item, index) => {
207
+ let tempItem = {
208
+ ...item,
209
+ };
210
+ tempItem.url = this.uniEnv.imgUrl + tempItem.url;
211
+ if (tempItem.fileId) {
212
+ tempItem.url = (this.uniEnv.BASE_API || this.uniEnv.apiUrl) + `/sys/common/downloadZip?fileId=${tempItem.fileId}&openId=${this.openId}&token=${this.header['X-Access-Token']}`
213
+ }
214
+ return tempItem;
215
+ })
216
+ console.log('fileList:', this.fileList)
217
+ }
218
+ })
216
219
  },
217
220
  initWidget() {
218
221
  //action url
@@ -313,4 +316,7 @@ export default {
313
316
  .u-add-wrap__hover {
314
317
  background-color: rgb(235, 236, 238);
315
318
  }
319
+ /deep/ .u-delete-icon {
320
+ z-index: 1;
321
+ }
316
322
  </style>
@@ -109,6 +109,7 @@ export default {
109
109
  this.rolesList = res.result.records.filter((item) => {
110
110
  return this.role.includes(item.id);
111
111
  })
112
+ this.roleChange('')
112
113
  }).finally(() => {
113
114
  this.isLoading = false
114
115
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zscreate/zhxy-app-component",
3
- "version": "1.0.213",
3
+ "version": "1.0.215",
4
4
  "private": false,
5
5
  "description": "zhxy-app-component",
6
6
  "main": "index.js",