Browse Source

fix: scan组件

hella_vue3
zhang_li 2 months ago
parent
commit
e17df7b0d8
  1. 213
      src/mycomponents/qty/numbeIntegerrBox.vue
  2. 17
      src/mycomponents/qty/recommendQty.vue
  3. 21
      src/mycomponents/qty/recommendQtyEdit.vue
  4. 35
      src/mycomponents/record/recordComDetailCard.vue
  5. 71
      src/mycomponents/scan/winComScan.vue
  6. 9
      src/mycomponents/scan/winScanPack.vue
  7. 64
      src/mycomponents/scan/winScanPackAndLocation.vue

213
src/mycomponents/qty/numbeIntegerrBox.vue

@ -0,0 +1,213 @@
<template>
<view class="uni-numbox">
<view @click="_calcValue('minus')" class="uni-numbox__minus uni-numbox-btns" :style="{ background }">
<text class="uni-numbox--text" :class="{ 'uni-numbox--disabled': inputValue <= min || disabled }">-</text>
</view>
<input :disabled="disabled" @input="onKeyInput" @blur="_onBlur" class="uni-numbox__value" type="number" v-model="inputValue" />
<view @click="_calcValue('plus')" class="uni-numbox__plus uni-numbox-btns" :style="{ background }">
<text class="uni-numbox--text" :class="{ 'uni-numbox--disabled': inputValue >= max || disabled }">+</text>
</view>
</view>
</template>
<script setup lang="ts">
import { onMounted, nextTick, watch } from 'vue'
// name: "UniNumberBox"
const props = defineProps({
value: {
type: [Number, String],
default: 1
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 1
},
background: {
type: String,
default: '#f5f5f5'
},
disabled: {
type: Boolean,
default: false
}
})
const inputValue = ref(0)
watch(
() => inputValue.value,
(newVal, oldVa) => {
if (+newVal !== +oldVal) {
emit('change', newVal)
}
},
{
immediate: true,
deep: true
}
)
onMounted(() => {
inputValue.value = +props.value
})
const setValue = (val) => {
inputValue.value = val
}
//
const onKeyInput = (event) => {
// console.log( event.detail.value)
const hint = event.detail.value
if (!Number.isInteger(hint)) {
//
const temp = hint.toString().match(/^\d+/)
if (Array.isArray(temp)) {
inputValue.value = temp[0]
} else {
inputValue.value = temp
}
// this.inputValue = hint.toString().match(/^\d+/) || 0;
} else {
//
inputValue.value = hint
}
}
// (minus:;plus:)
const _calcValue = (type) => {
if (this.disabled) {
return
}
const scale = _getDecimalScale()
let value = inputValue.value * scale
const step = props.step * scale
if (type === 'minus') {
value -= step
if (value < props.min * scale) {
return
}
if (value > props.max * scale) {
value = props.max * scale
}
} else if (type === 'plus') {
value += step
if (value > props.max * scale) {
return
}
if (value < props.min * scale) {
value = props.min * scale
}
}
if (`${value}`.length > 5) {
value = value.toFixed(0) // toFixed
}
inputValue.value = String(value / scale)
}
const _getDecimalScale = () => {
const scale = 1
//
// if (~~this.step !== this.step) {
// scale = Math.pow(10, (this.step + "").split(".")[1].length);
// }
return scale
}
const onBlur = (event) => {
let { value } = event.detail
if (!value) {
return
}
value = +value
if (value > props.max) {
value = props.max
} else if (value < props.min) {
value = props.min
}
inputValue.value = value
/* 小数点后保留四位 */
// if (value > 0) {
// event.detail.value =parseFloat(event.detail.value).toFixed(0)
// } else {
// event.detail.value =-parseFloat(event.detail.value).toFixed(0)
// }
// input
nextTick(() => {
inputValue.value = event.detail.value
})
}
//
const emit = defineEmits(['change', 'confirm'])
</script>
<style lang="scss" scoped>
$box-height: 26px;
$bg: #f5f5f5;
$br: 2px;
$color: #333;
.uni-numbox {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-numbox-btns {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 8px;
background-color: $bg;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-numbox__value {
margin: 0 2px;
background-color: $bg;
width: 100px;
height: $box-height;
text-align: center;
font-size: 14px;
border-left-width: 0;
border-right-width: 0;
color: $color;
}
.uni-numbox__minus {
border-top-left-radius: $br;
border-bottom-left-radius: $br;
width: 64rpx;
}
.uni-numbox__plus {
border-top-right-radius: $br;
border-bottom-right-radius: $br;
width: 64rpx;
}
.uni-numbox--text {
// fix nvue
line-height: 20px;
font-size: 20px;
font-weight: 300;
color: $color;
}
.uni-numbox .uni-numbox--disabled {
color: #c0c0c0 !important;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
</style>

17
src/mycomponents/qty/recommendQty.vue

@ -1,19 +1,19 @@
<template>
<view>
<status v-if="isShowStatus" :status="dataContent.inventoryStatus"></status>
<status v-if="isShowStatus && dataContent.inventoryStatus" :status="dataContent.inventoryStatus"></status>
<view class="u-flex u-row center" v-if="isShowCount">
<view class="text_recommend">
{{ Number(dataContent.qty) }}
{{ Number(dataContent.qty) ? Number(dataContent.qty) : '' }}
</view>
<uom :uom="dataContent.uom"></uom>
<view v-if="isShowPackCount && dataContent.packQty != undefined" class="uni-flex uni-row">
<!-- <view v-if="isShowPackCount && dataContent.packQty != undefined" class="uni-flex uni-row">
<view class="uom">/</view>
<view class="text_packQty">{{ calc(dataContent.qty, dataContent.packQty) }} </view>
</view>
</view> -->
</view>
<view>
<pack-unit :dataContent="dataContent"></pack-unit>
<!-- <pack-unit :dataContent="dataContent"></pack-unit> -->
</view>
</view>
</template>
@ -65,11 +65,10 @@ const props = defineProps({
}
})
const calc = (qty, packQty) => {
if(qty&&packQty){
return Math.ceil(Number(qty) / Number(packQty));
}else{
return 0
if (qty && packQty) {
return Math.ceil(Number(qty) / Number(packQty))
}
return 0
}
</script>

21
src/mycomponents/qty/recommendQtyEdit.vue

@ -40,8 +40,8 @@
<view class="split_line"></view>
<view class="uni-flex uni-row space-between padding title u-col-center" v-if="showBalanceQty">
<text>库存数量 : </text>
<view class="uni-flex uni-row uni-center" style="align-items: center;">
<text class="text_recommend">{{Number(dataContent.balanceQty)}}</text>
<view class="uni-flex uni-row uni-center" style="align-items: center">
<text class="text_recommend">{{ Number(dataContent.balanceQty) }}</text>
<uom :uom="dataContent.uom"></uom>
</view>
</view>
@ -86,10 +86,15 @@ const props = defineProps({
type: Boolean,
default: true
},
showBalanceQty: {
//
isNumPackTips: {
type: Boolean,
default: false
},
showBalanceQty: {
type: Boolean,
default: false
}
})
const allQty = ref(0)
const stdCount = ref(0)
@ -130,7 +135,7 @@ const openTaskEditPopup = (recommendQtyParams, handleQty, labelQtyParams) => {
allQty.value = Number(handleQty)
setTimeout((res) => {
show.value = true
stdCount.value = Math.ceil(allQty.value / dataContent.value.packQty);
stdCount.value = Math.ceil(allQty.value / dataContent.value.packQty)
}, 500)
}
const openRecordEditPopup = (labelQtyParams, item) => {
@ -159,6 +164,14 @@ const calcQty = (e) => {
const setValue = () => {
// var recommendQty = Number(this.dataContent.qty);
// var labelQty = Number(this.dataContent.record.label.qty);
// isNumPackTips
if (allQty.value > parseFloat(dataContent.value.packQty) && props.isNumPackTips) {
comMessageRef.value.showErrorMessage(`数量[${allQty.value}]不允许大于包装数量[${dataContent.value.packQty}]`, (res) => {
allQty.value = dataContent.value.packQty
})
return
}
//
if (allQty.value > labelQty.value && props.isNumTips) {
comMessageRef.value.showErrorMessage(`数量[${allQty.value}]不允许大于标签数量[${labelQty.value}]`, (res) => {
allQty.value = labelQty.value

35
src/mycomponents/record/recordComDetailCard.vue

@ -4,20 +4,15 @@
<u-collapse-item :open="true">
<template v-slot:title>
<u-swipe-action :show="false" style="width: 90%" :options="scanOptions" bg-color="rgba(255,255,255,0)" @click="(...event) => removeItem(event, dataContent)">
<item-qty :dataContent="dataContent" :isShowBalance="true"
:isShowBalanceQty="isShowBalanceQty" :isShowRecommendQty="false">
</item-qty>
<view style="margin-left: 10px; margin-top: 5px;">
<pack title='父包装' v-if="dataContent.containerNumber" :packingCode='dataContent.containerNumber'></pack>
<location v-if="isShowParentToLocation" title='目标库位' :locationCode='dataContent.toLocationCode'></location>
<item-qty :dataContent="dataContent" :isShowBalance="true" :isShowBalanceQty="isShowBalanceQty" :isShowRecommendQty="true"> </item-qty>
<view style="margin-left: 10px; margin-top: 5px">
<pack title="父包装" v-if="dataContent.containerNumber" :packingCode="dataContent.containerNumber"></pack>
<location v-if="isShowParentToLocation" title="目标库位" :locationCode="dataContent.toLocationCode"></location>
</view>
</u-swipe-action>
</template>
<u-swipe-action :show="detail.show" :index="index" v-for="(detail, index) in dataContent.subList" :key="index" :options="detail.scaned ? scanOptions : detailOptions" bg-color="rgba(255,255,255,0)" class="u-m-b-20" @click="(...event) => swipeClick(event, detail)">
<balance :dataContent="detail" :isShowStdPack="false"
:isShowStatus="isShowStatus" :isShowPack="true"
:isShowFromLocation="isShowFromLocation" :isShowParentPack="isShowParentPack"
:isShowToLocation="isShowToLocation"> </balance>
<balance :dataContent="detail" :isShowStdPack="false" :isShowStatus="isShowStatus" :isShowPack="true" :isShowPackingNumberProps="isShowPackingNumberProps" isShowFromLocation="isShowFromLocation" :isShowToLocation="isShowToLocation" :isShowParentPack="isShowParentPack"> </balance>
</u-swipe-action>
</u-collapse-item>
</u-collapse>
@ -36,7 +31,7 @@ import location from '@/mycomponents/balance/location.vue'
import recordDetailPopup from '@/mycomponents/detail/recordDetailPopup.vue'
import pack from '@/mycomponents/balance/pack.vue'
import PackageAndItemCard from '@/mycomponents/package/PackageAndItemCard.vue'
import { getDetailOption, getDetailEditRemoveOption, getClearOption,getEditLocationRemoveOption ,getRecordOption} from '@/common/array.js'
import { getDetailOption, getDetailEditRemoveOption, getClearOption, getEditLocationRemoveOption, getRecordOption } from '@/common/array.js'
const props = defineProps({
dataContent: {
@ -94,6 +89,10 @@ const props = defineProps({
type: Boolean,
default: true
},
isShowPackingNumberProps: {
type: Boolean,
default: false
}
})
const collapse1 = ref()
//
@ -131,18 +130,18 @@ dataContent.value.subList.forEach((item) => {
})
onMounted(() => {
detailOptions.value = getDetailOption()
scanOptions.value = getRecordOption(props.allowModifyQty,props.allowModifyLocation);
removeOptions.value = props.isShowModifedLocation ? getEditLocationRemoveOption():getClearOption();
scanOptions.value = getRecordOption(props.allowModifyQty, props.allowModifyLocation)
removeOptions.value = props.isShowModifedLocation ? getEditLocationRemoveOption() : getClearOption()
})
const removeItem = (params, dataContent) => {
const { text } = removeOptions.value[params[1]]
if (text == '移除') {
comMessageRef.value.showQuestionMessage('确定清空物料及箱码信息?', (res) => {
if (res) {
emit('removeItem',dataContent)
emit('removeItem', dataContent)
}
})
}else {
} else {
editLocation(dataContent)
}
}
@ -181,12 +180,12 @@ const confirm = (qty) => {
editItem.value.handleQty = qty
emit('updateData')
}
const editLocation = (item)=> {
editItem.value = item;
const editLocation = (item) => {
editItem.value = item
emit('editLocation', item)
}
//
const emit = defineEmits(['updateData', 'removePack','editLocation'])
const emit = defineEmits(['updateData', 'removePack', 'editLocation'])
</script>
<style>

71
src/mycomponents/scan/winComScan.vue

@ -4,10 +4,7 @@
<view class="pop_tab">
<view class="tab_info">
<view class="conbox">
<textarea v-model="scanMsg" trim="all" maxlength="1000" style="margin-left: 5px; width: 90%"
:focus="boxfocus" :placeholder="placeholderValue" @input="handelScanMsg"
@blur="handleBlur" @focus="handleFocus"
:cursor="cursorIndex"></textarea>
<textarea v-model="scanMsg" trim="all" maxlength="1000" style="margin-left: 5px; width: 90%" :focus="boxfocus" :placeholder="placeholderValue" @input="handelScanMsg" @blur="handleBlur" @focus="handleFocus" :cursor="cursorIndex"></textarea>
</view>
<view class="uni-flex uni-row space-between u-col-center">
@ -52,9 +49,8 @@
<script setup lang="ts">
import { ref, getCurrentInstance, onMounted, nextTick, watch } from 'vue'
import { getLabelInfo } from '@/common/label.js'
import {
getManagementPrecisions
} from '@/common/balance.js';
import { getManagementPrecisions } from '@/common/balance.js'
const props = defineProps({
placeholder: {
type: String,
@ -76,9 +72,14 @@ const props = defineProps({
type: String,
default: 'HPQ' // HLB HMQ HCQ HPQ
},
locationCode:{
locationCode: {
type: String,
default: ''
},
//
isHavePackNumber: {
type: Boolean,
default: false
}
})
@ -92,9 +93,7 @@ const expendIcon = ref('arrow-down')
const cursorIndex = ref(0)
const comMessageRef = ref(null)
placeholderValue.value = `请扫描${props.placeholder}`
onMounted(() => {
})
onMounted(() => {})
//
watch(
() => props.placeholder,
@ -151,30 +150,36 @@ const handelScanMsg = () => {
if (props.isShowHistory) {
scanList.value.unshift(content)
}
getLabelInfo(content, props.headerType, (callback) => {
// uni.hideLoading();
const scanResult = callback
scanResult.scanMessage = content
if (scanResult.success) {
clear()
// that.getfocus();//
emit('getResult', scanResult)
} else {
clear()
losefocus()
comMessageRef.value.showErrorMessage(scanResult.message, (res) => {
if (res) {
getfocus()
}
})
}
},props.locationCode)
getLabelInfo(
content,
props.headerType,
(callback) => {
// uni.hideLoading();
const scanResult = callback
scanResult.scanMessage = content
if (scanResult.success) {
clear()
// that.getfocus();//
emit('getResult', scanResult)
} else {
clear()
losefocus()
comMessageRef.value.showErrorMessage(scanResult.message, (res) => {
if (res) {
getfocus()
}
})
}
},
props.locationCode,
props.isHavePackNumber
)
}, 200)
}
}
const handleFocus = ()=> {}
const handleFocus = () => {}
const handleBlur = ()=> {
const handleBlur = () => {
// setTimeout(res=>{
// uni.hideKeyboard();
// },200)
@ -192,7 +197,7 @@ const losefocus = () => {
})
}
const clear = () => {
if(props.clearResult){
if (props.clearResult) {
cursorIndex.value = 0
scanMsg.value = ''
}
@ -241,7 +246,7 @@ defineExpose({
clear,
clickScanMsg,
losefocus,
setItemCodeSimulate,
setItemCodeSimulate
})
</script>
<script module="textarea" lang="renderjs">

9
src/mycomponents/scan/winScanPack.vue

@ -11,10 +11,7 @@
</view>
<view class="">
<view class="">
<win-com-scan ref="comscan" :placeholder="title"
@getResult="getScanResult" :headerType="headerType"
:isShowHistory="isShowHistory" :clearResult="true"
:locationCode='locationCode'></win-com-scan>
<win-com-scan ref="comscan" :placeholder="title" @getResult="getScanResult" :headerType="headerType" :isShowHistory="isShowHistory" :clearResult="true" :locationCode="locationCode" :isHavePackNumber="isHavePackNumber"></win-com-scan>
</view>
</view>
</view>
@ -47,6 +44,7 @@ const show = ref(false)
const comMessageRef = ref()
const comscan = ref()
const locationCode = ref('')
const isHavePackNumber = ref(false) //
//
const simulateScan = (scanMessage) => {
getLabelInfo(scanMessage, props.headerType, (callback) => {
@ -57,10 +55,11 @@ const simulateScan = (scanMessage) => {
}
})
}
const openScanPopup = (locationCode1) => {
const openScanPopup = (locationCode1, isHavePackNumberParams) => {
setTimeout((res) => {
show.value = true
locationCode.value = locationCode1
isHavePackNumber.value = isHavePackNumberParams
getfocus()
}, 500)
}

64
src/mycomponents/scan/winScanPackAndLocation.vue

@ -27,7 +27,7 @@
</view>
<view class="">
<view class="">
<win-com-scan ref="comscan" :placeholder="title" @getResult="getScanResult" :isShowHistory="isShowHistory" :clearResult="true" :headerType="headerType"></win-com-scan>
<win-com-scan ref="comscan" :placeholder="title" @getResult="getScanResult" :isShowHistory="isShowHistory" :clearResult="true" :headerType="headerType" :locationCode="fromLocationCode"></win-com-scan>
</view>
</view>
</view>
@ -92,8 +92,13 @@ const props = defineProps({
},
toLocationCode: {
type: String,
default: ""
default: ''
},
//
isJustReplay: {
type: Boolean,
default: false
}
})
const scanResult = ref({})
const show = ref(false)
@ -116,6 +121,7 @@ const comscan = ref(null)
const comMessageRef = ref(null)
const location = ref()
const chooseWhich = ref('1')
const fromWarehouseCode = ref('')
const handleConfirm = () => {
emit('confirm', fromLocationCode.value)
}
@ -154,7 +160,6 @@ const openScanPopupForJob = (fromLocationCodePrams, fromLocationListPrams, jobCo
fromLocationCode.value = fromLocationList.value[0]
}
}, 500)
}
//
const openScanPopupForJobSimulate = (fromLocationCodeParams, fromLocationListParams, jobContent, item) => {
@ -173,7 +178,7 @@ const openScanPopupForJobSimulate = (fromLocationCodeParams, fromLocationListPar
inventoryStatus.value = getDirectoryItemArray(jobContent.outInventoryStatuses) // 出库库存状态; //出库库存状态
fromLocationAreaTypeList.value = getDirectoryItemArray(jobContent.fromAreaTypes) //
uni.showLoading({
title: "获取标签信息",
title: '获取标签信息',
mask: true
})
getLabelInfo(scanMessage, headerType.value, (callback) => {
@ -191,7 +196,7 @@ const closeScanPopup = (content) => {
emit('close', '')
}
const scanLocation = (scanResult) => {
if(scanResult.fromLocationCode){
if (scanResult.fromLocationCode) {
fromLocationCode.value = scanResult.fromLocationCode
}
const isCheck = false
@ -225,7 +230,14 @@ const scanLocation = (scanResult) => {
if (available == 'TRUE') {
if (checkDirectoryItemExist(fromLocationAreaTypeList.value, type)) {
location.value = result
fromWarehouseCode.value = result.warehouseCode
// this.packGetFocus();
if (isJustReplay.value) {
emit('getResult', scanResult, managementPrecision.value)
uni.hideLoading()
} else {
checkPackage(scanResult)
}
checkPackage(scanResult)
} else {
uni.hideLoading()
@ -253,13 +265,21 @@ const scanLocation = (scanResult) => {
})
})
}
const getScanResult = (result) => {
const getScanResult = (result, businessTypeParams) => {
// if (this.fromLocationCode == '' || this.fromLocationCode == null) {
// this.showMessage('', callback => {
// this.locationGetFocus();
// })
// return;
// } else
//
if (businessTypeParams) {
businessType.value = businessTypeParams
fromInventoryStatuses.value = getDirectoryItemArray(businessTypeParams.outInventoryStatuses)
inventoryStatus.value = getDirectoryItemArray(businessTypeParams.outInventoryStatuses) //
fromLocationAreaTypeList.value = getDirectoryItemArray(businessTypeParams.outAreaTypes) //
}
scanLocation(result)
// debugger
// if(this.isCheck){
@ -279,20 +299,20 @@ const checkPackage = async (result) => {
if (res.success) {
managementPrecision.value = res.managementPrecision
if (managementPrecision.value == 'BY_BATCH') {
res.data.list.forEach(item => {
res.data.list.forEach((item) => {
item.packingNumber = ''
})
}
chooseWhich.value = '2'
if(!result.label.itemCode){
if (!result.label.itemCode) {
showErrorMessage('扫描标签不对,请重新扫描')
emit('clearItemCode',result.label)
emit('clearItemCode', result.label)
return
}
if (res.data && res.data.list && res.data.list.length > 1) {
showBalanceSelect(res.data.list);
showBalanceSelect(res.data.list)
} else {
afterQueryBalance(res.data.list);
afterQueryBalance(res.data.list)
}
} else {
showErrorMessage(res.message, (res) => {
@ -338,7 +358,7 @@ const allowNoneBalance = async (datas) => {
const mustHavaBalance = (datas) => {
if (datas.length == 0) {
showErrorMessage(`${getQueryCondition()}\n未查找到库存记录`, (res) => {
emit('clearItemCode',scanResult.value)
emit('clearItemCode', scanResult.value)
packGetFocus()
})
} else if (datas.length == 1) {
@ -365,9 +385,9 @@ const showBalanceSelect = (items) => {
}
const selectBalanceItem = (balance) => {
if (chooseWhich.value == 1) {
packCallBack(balance);
packCallBack(balance)
} else {
countCallBack(balance);
countCallBack(balance)
}
// 20231228
// if (balance.qty > 0) {
@ -390,15 +410,16 @@ const countCallBack = (datas) => {
const data = {
label: scanResult.value.label,
package: scanResult.value.package,
fromWarehouseCode: fromWarehouseCode.value,
balance: datas,
fromLocationCode: fromLocationCode.value
}
// this.packGetFocus();
//
if (managementPrecision.value == 'BY_BATCH' || managementPrecision.value == 'BY_QUANTITY') {
emit("getResult", data, managementPrecision.value);
emit('getResult', data, managementPrecision.value)
} else {
emit("getResult", data);
emit('getResult', data)
}
emit('getCountScanResult', data)
}
@ -411,15 +432,16 @@ const packCallBack = async (item) => {
const data = {
label: scanResult.value.label,
package: scanResult.value.package,
fromWarehouseCode: fromWarehouseCode.value,
balance: item,
fromLocationCode: fromLocationCode.value
}
packGetFocus()
//
if (managementPrecision.value == 'BY_BATCH' || managementPrecision.value == 'BY_QUANTITY') {
emit("getResult", data, managementPrecision.value);
emit('getResult', data, managementPrecision.value)
} else {
emit("getResult", data);
emit('getResult', data)
}
}
const packGetFocus = () => {
@ -439,10 +461,10 @@ const locationGetFocus = () => {
const getQueryCondition = () => {
let condition = '按照以下条件:\n'
const { label } = scanResult.value
let isShowStatus = props.balanceFromInventoryStatuses ? fromInventoryStatuses.value : undefined
const isShowStatus = props.balanceFromInventoryStatuses ? fromInventoryStatuses.value : undefined
let status = ''
if (isShowStatus) {
status = getInventoryStatusDesc(isShowStatus);
status = getInventoryStatusDesc(isShowStatus)
}
switch (managementPrecision.value) {
case 'BY_PACKAGING':
@ -490,7 +512,7 @@ const addLocationCode = (code) => {
}
}
//
const emit = defineEmits(['close', 'getCountScanResult', 'getResult', 'confirm','clearItemCode'])
const emit = defineEmits(['close', 'getCountScanResult', 'getResult', 'confirm', 'clearItemCode'])
defineExpose({ openScanPopupForType, openScanPopupForJob, packGetFocus, packLoseFocus, closeScanPopup })
</script>

Loading…
Cancel
Save