2025-08-06 15:16:18 +03:30
|
|
|
|
<template>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<v-dialog v-model="dialog" max-width="640px" persistent>
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-card>
|
|
|
|
|
|
<v-card-title class="d-flex align-center p-3 gap-2">
|
|
|
|
|
|
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
|
|
|
|
|
<span>{{ isEdit ? 'ویرایش سریال گارانتی' : 'افزودن سریال گارانتی جدید' }}</span>
|
|
|
|
|
|
</v-card-title>
|
|
|
|
|
|
|
|
|
|
|
|
<v-card-text>
|
|
|
|
|
|
<v-form ref="form" v-model="valid">
|
|
|
|
|
|
<v-row>
|
|
|
|
|
|
<v-col cols="12" md="6">
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
|
|
|
|
|
|
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
|
|
|
|
|
|
<template #append>
|
|
|
|
|
|
<v-btn icon small @click="openScanner" :disabled="isEdit" color="primary" variant="text">
|
|
|
|
|
|
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</v-text-field>
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-col>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-col cols="12" md="6">
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<Hcommoditysearch
|
|
|
|
|
|
:model-value="formData.commodity_id ?? undefined"
|
|
|
|
|
|
@update:modelValue="(val: number | Record<string, any>) => { formData.commodity_id = typeof val === 'number' ? val : (val as any)?.id ?? null }"
|
|
|
|
|
|
:return-object="false"
|
2025-08-06 15:16:18 +03:30
|
|
|
|
label="محصول *"
|
|
|
|
|
|
:rules="[rules.commodity]"
|
|
|
|
|
|
required
|
2025-08-18 22:05:33 +03:30
|
|
|
|
class="serial-commodity"
|
|
|
|
|
|
/>
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-col>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-col cols="12" md="6">
|
2025-08-20 00:21:48 +03:30
|
|
|
|
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]" :ignore-year-range="true" dense
|
2025-08-18 22:05:33 +03:30
|
|
|
|
outlined hide-details="auto" />
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-col>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-col cols="12" md="6">
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
|
2025-08-20 00:21:48 +03:30
|
|
|
|
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" :ignore-year-range="true" dense outlined
|
2025-08-18 22:05:33 +03:30
|
|
|
|
hide-details="auto" />
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-col>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-col cols="12" md="6">
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<v-select v-model="formData.status" label="وضعیت" :items="statusOptions" item-title="title"
|
|
|
|
|
|
item-value="value" variant="outlined" density="comfortable" hide-details="auto" />
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
|
|
<v-col v-if="isEdit" cols="12" md="6">
|
|
|
|
|
|
<v-select v-model="formData.activation" label="وضعیت فعالسازی" :items="activationOptions"
|
|
|
|
|
|
item-title="title" item-value="value" variant="outlined" density="comfortable" hide-details="auto" />
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-col>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-col cols="12">
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<v-textarea v-model="formData.description" label="توضیحات" rows="3" auto-grow variant="outlined"
|
|
|
|
|
|
density="comfortable" hide-details="auto" />
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
</v-form>
|
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
|
|
|
|
|
|
<v-card-actions>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<v-spacer />
|
2025-08-06 15:16:18 +03:30
|
|
|
|
<v-btn color="grey" @click="close">انصراف</v-btn>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
<v-btn color="primary" @click="save" :loading="loading" :disabled="!valid">
|
2025-08-06 15:16:18 +03:30
|
|
|
|
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
|
</v-card>
|
2025-08-18 22:05:33 +03:30
|
|
|
|
|
|
|
|
|
|
<v-dialog v-model="showQrScanner" :max-width="isMobile ? '95vw' : 560" persistent>
|
|
|
|
|
|
<v-card class="qr-card">
|
|
|
|
|
|
<v-card-title class="qr-title">
|
|
|
|
|
|
<v-icon left color="primary">mdi-qrcode-scan</v-icon>
|
|
|
|
|
|
اسکن کد QR/بارکد
|
|
|
|
|
|
</v-card-title>
|
|
|
|
|
|
|
|
|
|
|
|
<v-card-text>
|
|
|
|
|
|
<div class="qr-wrap">
|
|
|
|
|
|
<div id="reader" ref="readerRef" class="qr-reader"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="qr-status">
|
|
|
|
|
|
<v-alert v-if="scanError" type="error" variant="tonal" density="comfortable">
|
|
|
|
|
|
{{ scanError }}
|
|
|
|
|
|
</v-alert>
|
|
|
|
|
|
<v-progress-circular v-if="loadingScan" indeterminate size="28" class="mt-3" color="primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
|
|
|
|
|
|
<v-card-actions class="qr-actions">
|
|
|
|
|
|
<v-btn :disabled="loadingScan" variant="outlined" color="primary" prepend-icon="mdi-camera-switch"
|
|
|
|
|
|
@click="switchCamera">
|
|
|
|
|
|
تغییر دوربین
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
<v-spacer />
|
|
|
|
|
|
<v-btn variant="text" @click="closeScanner">انصراف</v-btn>
|
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
</v-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<v-snackbar v-model="showNotification" :color="notificationColor" :timeout="3000" location="top">
|
|
|
|
|
|
{{ notificationText }}
|
|
|
|
|
|
<template #actions>
|
|
|
|
|
|
<v-btn color="white" text @click="showNotification = false">بستن</v-btn>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</v-snackbar>
|
2025-08-06 15:16:18 +03:30
|
|
|
|
</v-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-08-18 22:05:33 +03:30
|
|
|
|
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
|
|
|
|
|
import { Html5Qrcode, Html5QrcodeSupportedFormats, Html5QrcodeScannerState } from 'html5-qrcode'
|
|
|
|
|
|
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
modelValue: boolean
|
|
|
|
|
|
serial?: any
|
|
|
|
|
|
commodities: any[]
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:modelValue': [value: boolean]
|
|
|
|
|
|
save: [data: any]
|
|
|
|
|
|
close: []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const valid = ref(false)
|
|
|
|
|
|
const form = ref()
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const commodityModel = ref<any>(null)
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
|
|
|
|
|
const formData = ref({
|
|
|
|
|
|
serialNumber: '',
|
2025-08-18 22:05:33 +03:30
|
|
|
|
commodity_id: null as number | null,
|
2025-08-06 15:16:18 +03:30
|
|
|
|
description: '',
|
|
|
|
|
|
warrantyStartDate: '',
|
|
|
|
|
|
warrantyEndDate: '',
|
2025-08-18 22:05:33 +03:30
|
|
|
|
status: 'available',
|
|
|
|
|
|
activation: 'deactive',
|
2025-08-06 15:16:18 +03:30
|
|
|
|
notes: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const rules = {
|
2025-08-18 22:05:33 +03:30
|
|
|
|
required: (v: any) => !!v || 'این فیلد الزامی است',
|
|
|
|
|
|
serialNumber: (v: any) => {
|
|
|
|
|
|
if (!v) return 'شماره سریال الزامی است'
|
|
|
|
|
|
if (!/^[A-Za-z0-9\-_.]+$/.test(v)) return 'شماره سریال نامعتبر است'
|
|
|
|
|
|
if (v.length < 3) return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
|
|
|
|
|
if (v.length > 50) return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
2025-08-06 15:16:18 +03:30
|
|
|
|
return true
|
|
|
|
|
|
},
|
2025-08-18 22:05:33 +03:30
|
|
|
|
commodity: (v: any) => !!v || 'انتخاب محصول الزامی است',
|
|
|
|
|
|
date: (v: any) => {
|
|
|
|
|
|
if (!v) return true
|
|
|
|
|
|
const d = new Date(v)
|
|
|
|
|
|
return !isNaN(d.getTime()) || 'تاریخ نامعتبر است'
|
2025-08-06 15:16:18 +03:30
|
|
|
|
},
|
2025-08-18 22:05:33 +03:30
|
|
|
|
endDate: (v: any, s: any) => {
|
|
|
|
|
|
if (!v || !s) return true
|
|
|
|
|
|
const end = new Date(v); const start = new Date(s)
|
|
|
|
|
|
return end > start || 'تاریخ پایان باید بعد از تاریخ شروع باشد'
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const statusOptions = [
|
2025-08-18 22:05:33 +03:30
|
|
|
|
{ title: 'آزاد', value: 'available' },
|
|
|
|
|
|
{ title: 'تخصیص یافته', value: 'allocated' },
|
|
|
|
|
|
{ title: 'تأیید شده', value: 'verified' },
|
|
|
|
|
|
{ title: 'متصل', value: 'bound' },
|
|
|
|
|
|
{ title: 'مصرف شده', value: 'consumed' },
|
|
|
|
|
|
{ title: 'باطل', value: 'void' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const activationOptions = [
|
|
|
|
|
|
{ title: 'غیرفعال', value: 'deactive' },
|
|
|
|
|
|
{ title: 'فعال', value: 'active' }
|
2025-08-06 15:16:18 +03:30
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const dialog = computed({
|
|
|
|
|
|
get: () => props.modelValue,
|
2025-08-18 22:05:33 +03:30
|
|
|
|
set: (v) => emit('update:modelValue', v)
|
2025-08-06 15:16:18 +03:30
|
|
|
|
})
|
|
|
|
|
|
const isEdit = computed(() => !!props.serial)
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const isMobile = computed(() => window.innerWidth <= 768)
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const showNotification = ref(false)
|
|
|
|
|
|
const notificationText = ref('')
|
|
|
|
|
|
const notificationColor = ref<'success' | 'error' | 'warning' | 'info'>('success')
|
|
|
|
|
|
const showNotify = (t: string, c: 'success' | 'error' | 'warning' | 'info' = 'success') => {
|
|
|
|
|
|
notificationText.value = t
|
|
|
|
|
|
notificationColor.value = c
|
|
|
|
|
|
showNotification.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const showQrScanner = ref(false)
|
|
|
|
|
|
const readerRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
let qr: Html5Qrcode | null = null
|
|
|
|
|
|
const loadingScan = ref(false)
|
|
|
|
|
|
const scanError = ref('')
|
|
|
|
|
|
const cameras = ref<{ id: string; label: string }[]>([])
|
|
|
|
|
|
const currentCamIndex = ref(0)
|
|
|
|
|
|
const qrboxSize = ref(240)
|
|
|
|
|
|
|
|
|
|
|
|
const computeQrBox = () => {
|
|
|
|
|
|
const el = readerRef.value
|
|
|
|
|
|
if (!el) return 260
|
|
|
|
|
|
const w = Math.max(320, Math.floor(el.clientWidth))
|
|
|
|
|
|
const size = Math.max(220, Math.min(380, Math.floor(w * 0.66)))
|
|
|
|
|
|
return size
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openScanner = async () => {
|
|
|
|
|
|
showQrScanner.value = true
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
await startScanner()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const startScanner = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
loadingScan.value = true
|
|
|
|
|
|
scanError.value = ''
|
|
|
|
|
|
qrboxSize.value = computeQrBox()
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const devices = await Html5Qrcode.getCameras()
|
|
|
|
|
|
if (!devices.length) { throw new Error('دوربین یافت نشد') }
|
|
|
|
|
|
cameras.value = devices.map(d => ({ id: d.id, label: d.label }))
|
|
|
|
|
|
if (currentCamIndex.value >= cameras.value.length) currentCamIndex.value = 0
|
|
|
|
|
|
|
|
|
|
|
|
if (qr && (qr.getState?.() === Html5QrcodeScannerState.SCANNING)) await stopScanner()
|
|
|
|
|
|
if (!qr) qr = new Html5Qrcode('reader', {
|
|
|
|
|
|
verbose: false,
|
|
|
|
|
|
formatsToSupport: [
|
|
|
|
|
|
Html5QrcodeSupportedFormats.QR_CODE,
|
|
|
|
|
|
Html5QrcodeSupportedFormats.CODE_128,
|
|
|
|
|
|
Html5QrcodeSupportedFormats.CODE_39,
|
|
|
|
|
|
Html5QrcodeSupportedFormats.EAN_13,
|
|
|
|
|
|
Html5QrcodeSupportedFormats.UPC_A,
|
|
|
|
|
|
Html5QrcodeSupportedFormats.DATA_MATRIX
|
|
|
|
|
|
],
|
|
|
|
|
|
experimentalFeatures: { useBarCodeDetectorIfSupported: true }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await qr.start(
|
|
|
|
|
|
{ deviceId: { exact: cameras.value[currentCamIndex.value].id } },
|
|
|
|
|
|
{ fps: 12, qrbox: { width: qrboxSize.value, height: qrboxSize.value }, aspectRatio: 1.333 },
|
|
|
|
|
|
(decodedText: string) => {
|
|
|
|
|
|
if (decodedText) {
|
|
|
|
|
|
formData.value.serialNumber = decodedText.trim()
|
|
|
|
|
|
showNotify('کد با موفقیت اسکن شد', 'success')
|
|
|
|
|
|
closeScanner()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
(_err: string) => { }
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
scanError.value = e?.message || 'خطا در راهاندازی دوربین'
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingScan.value = false
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
2025-08-18 22:05:33 +03:30
|
|
|
|
}
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const stopScanner = async () => {
|
|
|
|
|
|
if (!qr) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const state = qr.getState?.()
|
|
|
|
|
|
if (state === Html5QrcodeScannerState.SCANNING) await qr.stop()
|
|
|
|
|
|
await qr.clear()
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const closeScanner = async () => {
|
|
|
|
|
|
await stopScanner()
|
|
|
|
|
|
showQrScanner.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const switchCamera = async () => {
|
|
|
|
|
|
if (!cameras.value.length) return
|
|
|
|
|
|
currentCamIndex.value = (currentCamIndex.value + 1) % cameras.value.length
|
|
|
|
|
|
await stopScanner()
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
await startScanner()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const save = async () => {
|
|
|
|
|
|
const res = await form.value?.validate()
|
|
|
|
|
|
const ok = typeof res === 'object' ? res.valid : !!res
|
|
|
|
|
|
if (!ok) return
|
|
|
|
|
|
if (!formData.value.serialNumber || !formData.value.commodity_id) return
|
2025-08-06 15:16:18 +03:30
|
|
|
|
try {
|
|
|
|
|
|
loading.value = true
|
2025-08-18 22:05:33 +03:30
|
|
|
|
emit('save', { ...formData.value })
|
2025-08-06 15:16:18 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
2025-08-18 22:05:33 +03:30
|
|
|
|
closeScanner()
|
2025-08-06 15:16:18 +03:30
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
formData.value = {
|
|
|
|
|
|
serialNumber: '',
|
2025-08-18 22:05:33 +03:30
|
|
|
|
commodity_id: null,
|
2025-08-06 15:16:18 +03:30
|
|
|
|
description: '',
|
|
|
|
|
|
warrantyStartDate: '',
|
|
|
|
|
|
warrantyEndDate: '',
|
2025-08-18 22:05:33 +03:30
|
|
|
|
status: 'available',
|
|
|
|
|
|
activation: 'deactive',
|
2025-08-06 15:16:18 +03:30
|
|
|
|
notes: ''
|
|
|
|
|
|
}
|
2025-08-18 22:05:33 +03:30
|
|
|
|
commodityModel.value = null
|
|
|
|
|
|
form.value?.resetValidation()
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const loadSerialData = () => {
|
|
|
|
|
|
if (props.serial) {
|
|
|
|
|
|
formData.value = {
|
|
|
|
|
|
serialNumber: props.serial.serialNumber || '',
|
2025-08-18 22:05:33 +03:30
|
|
|
|
commodity_id: Number(props.serial.commodity?.id) || null,
|
2025-08-06 15:16:18 +03:30
|
|
|
|
description: props.serial.description || '',
|
|
|
|
|
|
warrantyStartDate: props.serial.warrantyStartDate || '',
|
|
|
|
|
|
warrantyEndDate: props.serial.warrantyEndDate || '',
|
2025-08-18 22:05:33 +03:30
|
|
|
|
status: props.serial.status || 'available',
|
|
|
|
|
|
activation: props.serial.activation || 'deactive',
|
2025-08-06 15:16:18 +03:30
|
|
|
|
notes: props.serial.notes || ''
|
|
|
|
|
|
}
|
2025-08-18 22:05:33 +03:30
|
|
|
|
commodityModel.value = props.commodities.find(c => c.id === formData.value.commodity_id) || null
|
2025-08-06 15:16:18 +03:30
|
|
|
|
} else {
|
|
|
|
|
|
resetForm()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const customFilter = (item: any, q: string) => {
|
|
|
|
|
|
const t = String(item?.name || '').toLowerCase()
|
|
|
|
|
|
const s = String(q || '').toLowerCase()
|
|
|
|
|
|
return t.indexOf(s) > -1
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
const handleCommoditySelect = (c: any) => {
|
|
|
|
|
|
formData.value.commodity_id = c?.id ? Number(c.id) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
|
|
|
|
|
|
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
|
|
|
|
|
|
onBeforeUnmount(() => { closeScanner() })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
/* normalize Hcommoditysearch height with other inputs */
|
|
|
|
|
|
.serial-commodity :deep(.v-field) { min-height: 56px; }
|
|
|
|
|
|
.serial-commodity :deep(.v-field__input) { padding-top: 14px; padding-bottom: 14px; }
|
|
|
|
|
|
#qr-shaded-region {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
video {
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.v-dialog {
|
|
|
|
|
|
direction: rtl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-card {
|
|
|
|
|
|
max-width: 95vw;
|
|
|
|
|
|
border-radius: 16px
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-title {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 14px 16px
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-wrap {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 560px;
|
|
|
|
|
|
margin: 0 auto
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-reader {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ویدئو تولیدی کتابخانه */
|
|
|
|
|
|
:deep(#reader video) {
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
height: auto !important;
|
|
|
|
|
|
display: block !important;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
min-height: 240px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* کانتینر داخلی کتابخانه راست به چپ نشه */
|
|
|
|
|
|
:deep(#reader div) {
|
|
|
|
|
|
direction: ltr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* وضعیتها */
|
|
|
|
|
|
.qr-status {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 12px
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-actions {
|
|
|
|
|
|
padding: 12px 16px
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ریسپانسیو */
|
|
|
|
|
|
@media (max-width:1024px) {
|
|
|
|
|
|
.qr-reader {
|
|
|
|
|
|
min-height: 300px
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(#reader video) {
|
|
|
|
|
|
min-height: 220px
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width:768px) {
|
|
|
|
|
|
.qr-card {
|
|
|
|
|
|
max-width: 95vw
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-reader {
|
|
|
|
|
|
min-height: 260px
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(#reader video) {
|
|
|
|
|
|
min-height: 200px
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
@media (max-width:480px) {
|
|
|
|
|
|
.qr-reader {
|
|
|
|
|
|
min-height: 220px
|
|
|
|
|
|
}
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
:deep(#reader video) {
|
|
|
|
|
|
min-height: 180px
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
2025-08-18 22:05:33 +03:30
|
|
|
|
}
|
2025-08-06 15:16:18 +03:30
|
|
|
|
|
2025-08-18 22:05:33 +03:30
|
|
|
|
.v-input.v-input--horizontal.v-input--center-affix.v-input--density-compact.v-theme--light.v-locale--is-rtl.v-input--error.v-text-field.my-0 {
|
|
|
|
|
|
height: 3rem;
|
2025-08-06 15:16:18 +03:30
|
|
|
|
}
|
2025-08-18 22:05:33 +03:30
|
|
|
|
</style>
|