借库逻辑实现
This commit is contained in:
@ -92,20 +92,19 @@
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
||||
<span class="stock-num">{{ scope.row.qty_stock }}</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_available'">
|
||||
<span class="avail-num">{{ scope.row.qty_available }}</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||
<el-tag :type="getStatusType(scope.row.status)" effect="light" round>
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
||||
<span class="stock-num">{{ scope.row.qty_stock }}</span>
|
||||
</template>
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_available'">
|
||||
<span class="avail-num">{{ scope.row.qty_available }}</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['arrival_photo', 'inspection_report'].includes(col.prop)">
|
||||
<div v-if="getImagesOnly(scope.row[col.prop]).length > 0" style="display: flex; align-items: center; justify-content: center;">
|
||||
<el-image
|
||||
@ -128,12 +127,8 @@
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop.includes('link')">
|
||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank"
|
||||
:underline="false">
|
||||
<el-icon>
|
||||
<Link/>
|
||||
</el-icon>
|
||||
查看
|
||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
|
||||
<el-icon><Link/></el-icon> 查看
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
@ -146,10 +141,7 @@
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||
<el-icon>
|
||||
<Printer/>
|
||||
</el-icon>
|
||||
打印
|
||||
<el-icon><Printer/></el-icon> 打印
|
||||
</el-button>
|
||||
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
|
||||
@ -234,10 +226,8 @@
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
|
||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
@ -273,7 +263,7 @@
|
||||
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
||||
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock/></el-icon> 历史锁定</span>
|
||||
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock/></el-icon> 历史锁定 (同物料遵循历史模式)</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
@ -385,7 +375,9 @@
|
||||
</el-dialog>
|
||||
|
||||
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/>
|
||||
|
||||
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
|
||||
|
||||
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
||||
<div style="text-align: center;">
|
||||
<div v-loading="printLoading" class="preview-box">
|
||||
@ -455,23 +447,20 @@ const inspection_report_url = ref('')
|
||||
// 基础列
|
||||
const baseColumns = [
|
||||
{prop: 'material_name', label: '名称'},
|
||||
{prop: 'material_type', label: '类型'}, // 移到类别前面
|
||||
{prop: 'material_type', label: '类型'},
|
||||
{prop: 'category', label: '类别'},
|
||||
{prop: 'spec_model', label: '规格型号'},
|
||||
{prop: 'unit', label: '单位'},
|
||||
]
|
||||
|
||||
// [修改] 库存与商务列配置:将序列号/批号改为 "序列号/批号"
|
||||
// 库存与商务列
|
||||
const stockColumns = [
|
||||
{prop: 'id', label: 'ID', minWidth: '60'},
|
||||
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
||||
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
||||
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
||||
{prop: 'barcode', label: '条码', minWidth: '120'},
|
||||
|
||||
// 新的合并列,修改 label 为 "序列号/批号"
|
||||
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160'},
|
||||
|
||||
{prop: 'status', label: '状态', minWidth: '100'},
|
||||
{prop: 'inspection_status', label: '到检', minWidth: '100'},
|
||||
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
||||
@ -492,11 +481,8 @@ const stockColumns = [
|
||||
]
|
||||
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
// [修改] 更新 key 以强制用户获取新默认值
|
||||
const STORAGE_KEY_COLS = 'stock_buy_visible_columns_v2'
|
||||
|
||||
// [修改] 默认列配置:加入 'sn_bn' 和 'warehouse_loc'
|
||||
// 同时这里也要对应上方的顺序变化,先 material_type 后 category
|
||||
const defaultColumns = [
|
||||
'material_name', 'material_type', 'category', 'spec_model', 'unit',
|
||||
'inbound_date', 'sn_bn', 'warehouse_loc', 'status', 'inspection_status',
|
||||
@ -516,7 +502,7 @@ const form = reactive({
|
||||
arrival_photo: [] as string[], inspection_report: [] as string[]
|
||||
})
|
||||
|
||||
// ... (以下逻辑保持不变)
|
||||
// 历史记录辅助函数
|
||||
const HISTORY_KEYS = { SUPPLIER: 'history_suppliers', PURCHASER: 'history_purchasers', EMAIL: 'history_emails', MATERIAL: 'history_materials' }
|
||||
const saveToHistory = (key: string, value: string) => {
|
||||
if (!value) return
|
||||
@ -595,15 +581,19 @@ const onMaterialSelected = (val: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 校验规则
|
||||
// ------------------------------------
|
||||
const validateUnique = (rule: any, value: string, callback: any) => {
|
||||
if (!value) return callback()
|
||||
// 前端仅做当前页面的简单重复提示,真正的校验在后端
|
||||
const isDuplicate = tableData.value.some((row: any) => {
|
||||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||||
if (rule.field === 'batch_number' && row.batch_number === value) return true
|
||||
if (rule.field === 'batch_number' && row.batch_number === value && row.base_id === form.base_id) return true
|
||||
return false
|
||||
})
|
||||
if (isDuplicate) callback(new Error('编号重复'))
|
||||
if (isDuplicate) callback(new Error('当前列表页存在相同编号(后端将进行全局校验)'))
|
||||
else callback()
|
||||
}
|
||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
@ -618,26 +608,54 @@ const rules = {
|
||||
batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}]
|
||||
}
|
||||
|
||||
// 自动计算批号逻辑
|
||||
const checkHistoryAndSetMode = async (baseId: number) => {
|
||||
try {
|
||||
const res: any = await getBuyList({page: 1, pageSize: 1000})
|
||||
const res: any = await getBuyList({page: 1, pageSize: 1000}) // 获取最近数据
|
||||
const historyItems = (res.data.items || []).filter((item: any) => item.base_id === baseId)
|
||||
|
||||
if (historyItems.length > 0) {
|
||||
modeLocked.value = true
|
||||
// 找最新的那条记录
|
||||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||||
if (latest.serial_number) { entryMode.value = 'serial'; form.serial_number = ''; form.batch_number = '' }
|
||||
else { entryMode.value = 'batch'; form.serial_number = ''; form.batch_number = incrementBatchNumber(latest.batch_number || '000000') }
|
||||
} else { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' }
|
||||
if (formRef.value) { formRef.value.clearValidate('serial_number'); formRef.value.clearValidate('batch_number') }
|
||||
} catch (e) { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' }
|
||||
if (latest.serial_number) {
|
||||
entryMode.value = 'serial'
|
||||
form.serial_number = ''
|
||||
form.batch_number = ''
|
||||
} else {
|
||||
entryMode.value = 'batch'
|
||||
form.serial_number = ''
|
||||
// 自动递增批号
|
||||
form.batch_number = incrementBatchNumber(latest.batch_number || '000000')
|
||||
}
|
||||
} else {
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate('serial_number')
|
||||
formRef.value.clearValidate('batch_number')
|
||||
}
|
||||
} catch (e) {
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
}
|
||||
const incrementBatchNumber = (batchStr: string) => {
|
||||
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
||||
return (parseInt(batchStr, 10) + 1).toString().padStart(6, '0')
|
||||
}
|
||||
const handleEntryModeChange = (val: string) => {
|
||||
if (val === 'batch') { form.serial_number = ''; form.batch_number = '000001'; if(formRef.value) formRef.value.clearValidate('serial_number') }
|
||||
else { form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') }
|
||||
if (val === 'batch') {
|
||||
form.serial_number = ''
|
||||
form.batch_number = '000001'
|
||||
if(formRef.value) formRef.value.clearValidate('serial_number')
|
||||
} else {
|
||||
form.batch_number = ''
|
||||
if(formRef.value) formRef.value.clearValidate('batch_number')
|
||||
}
|
||||
}
|
||||
watch(() => [form.in_quantity, form.unit_price], () => { form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4)) })
|
||||
|
||||
@ -688,6 +706,46 @@ const handleUpdate = (row: any) => {
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 提交逻辑
|
||||
// ------------------------------------
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
const finalReportList = [...form.inspection_report]
|
||||
if (inspection_report_url.value && !finalReportList.includes(inspection_report_url.value)) finalReportList.push(inspection_report_url.value)
|
||||
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||||
if (inspection_report_url.value) onlyImages.push(inspection_report_url.value)
|
||||
const payload = { ...form, inspection_report: onlyImages, in_quantity: Number(form.in_quantity), unit_price: Number(form.unit_price) }
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
const res: any = await createBuyInbound(payload)
|
||||
ElMessage.success('入库成功')
|
||||
if (res.data) {
|
||||
ElMessage.info('发送打印指令...')
|
||||
try { await executePrint(res.data); ElMessage.success('打印指令已发送') }
|
||||
catch (printErr: any) { ElMessage.warning('打印失败:' + (printErr.msg || '未知错误')) }
|
||||
}
|
||||
} else { await updateBuyInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
|
||||
// 成功后保存历史记录
|
||||
saveToHistory(HISTORY_KEYS.SUPPLIER, form.supplier_name)
|
||||
saveToHistory(HISTORY_KEYS.PURCHASER, form.purchaser)
|
||||
saveToHistory(HISTORY_KEYS.EMAIL, form.purchaser_email)
|
||||
|
||||
await fetchData()
|
||||
visible.value = false
|
||||
} catch (e: any) {
|
||||
// 重点:捕获后端唯一性校验错误
|
||||
ElMessage.error(e.msg || '操作失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 其他辅助函数保持不变
|
||||
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
|
||||
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
|
||||
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
|
||||
@ -740,33 +798,6 @@ const handleCameraFile = async (event: Event) => {
|
||||
} catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' }
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
const finalReportList = [...form.inspection_report]
|
||||
if (inspection_report_url.value && !finalReportList.includes(inspection_report_url.value)) finalReportList.push(inspection_report_url.value)
|
||||
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||||
if (inspection_report_url.value) onlyImages.push(inspection_report_url.value)
|
||||
const payload = { ...form, inspection_report: onlyImages, in_quantity: Number(form.in_quantity), unit_price: Number(form.unit_price) }
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
const res: any = await createBuyInbound(payload)
|
||||
ElMessage.success('入库成功')
|
||||
if (res.data) {
|
||||
ElMessage.info('发送打印指令...')
|
||||
try { await executePrint(res.data); ElMessage.success('打印指令已发送') }
|
||||
catch (printErr: any) { ElMessage.warning('打印失败:' + (printErr.msg || '未知错误')) }
|
||||
}
|
||||
} else { await updateBuyInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
saveToHistory(HISTORY_KEYS.SUPPLIER, form.supplier_name); saveToHistory(HISTORY_KEYS.PURCHASER, form.purchaser); saveToHistory(HISTORY_KEYS.EMAIL, form.purchaser_email)
|
||||
await fetchData(); visible.value = false
|
||||
} catch (e: any) { ElMessage.error(e.msg || '操作失败') } finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleDelete = async (row: any) => { try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } }
|
||||
const handlePrint = async (row: any) => {
|
||||
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
||||
@ -786,6 +817,7 @@ onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式部分保持不变,直接复用原有代码 */
|
||||
.buy-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
||||
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }
|
||||
|
||||
@ -425,9 +425,24 @@ const form = reactive({
|
||||
quality_report_link: [] as string[], inspection_report_link: [] as string[], product_photo: [] as string[], detail_link: ''
|
||||
})
|
||||
|
||||
// ------------------------------------
|
||||
// 校验规则 (前端 pre-check)
|
||||
// ------------------------------------
|
||||
const validateUnique = (rule: any, value: string, callback: any) => {
|
||||
if (!value) return callback()
|
||||
// 简单的列表前端查重
|
||||
const isDuplicate = tableData.value.some((row: any) => {
|
||||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||||
return false
|
||||
})
|
||||
if (isDuplicate) callback(new Error('当前列表页存在相同SN(后端将进行全局校验)'))
|
||||
else callback()
|
||||
}
|
||||
|
||||
const rules = {
|
||||
base_id: [{ required: true, message: '必选', trigger: 'change' }],
|
||||
serial_number: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
serial_number: [{ required: true, message: '必填', trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
|
||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
@ -585,7 +600,10 @@ const submitForm = async () => {
|
||||
} else { await updateProductInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
||||
visible.value = false; fetchData()
|
||||
} catch(e:any) { ElMessage.error(e.msg || '失败') } finally { submitting.value = false }
|
||||
} catch(e:any) {
|
||||
// 捕获后端报错
|
||||
ElMessage.error(e.msg || '操作失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -611,10 +611,11 @@ const validateUnique = (rule: any, value: string, callback: any) => {
|
||||
const isDuplicate = tableData.value.some((row: any) => {
|
||||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||||
if (rule.field === 'batch_number' && row.batch_number === value) return true
|
||||
// 批号校验需要同时匹配物料
|
||||
if (rule.field === 'batch_number' && row.batch_number === value && row.base_id === form.base_id) return true
|
||||
return false
|
||||
})
|
||||
if (isDuplicate) callback(new Error('编号重复'))
|
||||
if (isDuplicate) callback(new Error('当前列表页存在相同编号(后端将进行全局校验)'))
|
||||
else callback()
|
||||
}
|
||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
@ -778,7 +779,10 @@ const submitForm = async () => {
|
||||
} else { await updateSemiInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
||||
await fetchData(); visible.value = false
|
||||
} catch (e: any) { ElMessage.error(e.msg || '操作失败') } finally { submitting.value = false }
|
||||
} catch (e: any) {
|
||||
// 捕获后端报错
|
||||
ElMessage.error(e.msg || '操作失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -822,12 +826,8 @@ onMounted(() => fetchData())
|
||||
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
|
||||
.card-content { padding: 20px; }
|
||||
.basic-card { border-left: 4px solid #409EFF; }
|
||||
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
|
||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background-color: #f5f7fa; border-bottom: 1px solid #dcdfe6; border-radius: 0; padding-left: 0; }
|
||||
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
|
||||
.inbound-card { border-left: 4px solid #67C23A; }
|
||||
.production-card { border-left: 4px solid #E6A23C; }
|
||||
.production-card .card-title .icon { color: #E6A23C; }
|
||||
.production-card { border-left: 4px solid #E6A23C; } .production-card .card-title .icon { color: #E6A23C; }
|
||||
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; border-radius: 6px; padding: 15px; margin-bottom: 20px; }
|
||||
.custom-radio-group { margin-bottom: 10px; }
|
||||
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
|
||||
@ -839,16 +839,12 @@ onMounted(() => fetchData())
|
||||
.divider-text::before { margin-right: 15px; }
|
||||
.divider-text::after { margin-left: 15px; }
|
||||
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; padding: 20px; border-top: 1px solid #ebeef5; }
|
||||
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center;}
|
||||
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center; }
|
||||
.opt-name { font-weight: bold; }
|
||||
.opt-spec { color: #8492a6; font-size: 13px; margin-right: 10px; }
|
||||
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
|
||||
.preview-box { min-height: 150px; display: flex; justify-content: center; align-items: center; background: #f5f7fa; border-radius: 4px; }
|
||||
.empty-preview { color: #909399; }
|
||||
.more-images-badge { margin-left: 5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 6px; font-size: 12px; }
|
||||
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
||||
.clickable-text:hover { color: #66b1ff; }
|
||||
/* Scroll container specific to Product style */
|
||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background-color: #f5f7fa; border-bottom: 1px solid #dcdfe6; border-radius: 0; padding-left: 0; }
|
||||
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
|
||||
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
|
||||
.dialog-scroll-container { padding: 20px; max-height: 70vh; overflow-y: auto; }
|
||||
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
|
||||
|
||||
@ -1 +1,507 @@
|
||||
<template><div style="padding:20px;"><h2>借库申请</h2></div></template>
|
||||
<template>
|
||||
<div class="app-container mobile-optimized">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="title-box">
|
||||
<span>借库作业 (领用人签字)</span>
|
||||
<el-tag v-if="cartItems.length > 0" type="warning" size="small" effect="dark">
|
||||
已选 {{ cartItems.length }} 项
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="scan-section">
|
||||
<div v-if="showCamera" class="camera-wrapper">
|
||||
<QrScanner @decode="onScanSuccess" />
|
||||
<div class="scan-overlay">
|
||||
<el-button type="info" size="small" bg text @click="showCamera = false" icon="Close">
|
||||
关闭摄像头
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="camera-placeholder" @click="showCamera = true">
|
||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||
<span class="text">点击开启扫码</span>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-input
|
||||
v-model="barcodeInput"
|
||||
placeholder="扫描或输入条码回车"
|
||||
@keyup.enter="handleManualInput"
|
||||
clearable
|
||||
ref="barcodeRef"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Scissor /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="handleManualInput">添加</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-section">
|
||||
<div v-if="cartItems.length > 0">
|
||||
<el-table :data="cartItems" border stripe style="width: 100%">
|
||||
<el-table-column prop="name" label="物品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="可用库存" width="90" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag type="info">{{ parseFloat(row.available_quantity) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="借用数" width="130" align="center">
|
||||
<template #default="{row}">
|
||||
<el-input-number
|
||||
v-model="row.out_quantity"
|
||||
:min="1"
|
||||
:max="parseFloat(row.available_quantity)"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||
<template #default="{$index}">
|
||||
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-empty v-else description="暂无物品,请扫码借出" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<div v-if="cartItems.length > 0" class="form-section">
|
||||
<el-divider content-position="left">借用登记信息</el-divider>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-position="top">
|
||||
<el-row :gutter="15">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="领用人/借用人" prop="borrower_name">
|
||||
<el-input v-model="form.borrower_name" placeholder="请输入姓名" size="large" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item label="预计归还日期" prop="expected_return_time">
|
||||
<el-date-picker
|
||||
v-model="form.expected_return_time"
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
style="width: 100%"
|
||||
size="large"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled-date="disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注说明" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="用途说明..." />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="领用人签名确认" required>
|
||||
<div class="signature-box" @click="openSignatureDialog">
|
||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||
<img :src="signaturePreviewUrl" alt="签名" />
|
||||
<span class="re-sign-tip">点击重签</span>
|
||||
</div>
|
||||
<div v-else class="unsigned-placeholder">
|
||||
<el-icon :size="24"><EditPen /></el-icon>
|
||||
<span>点击此处进行全屏签名</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="bottom-actions">
|
||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||
确认借出
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="showSignatureDialog"
|
||||
fullscreen
|
||||
destroy-on-close
|
||||
:show-close="false"
|
||||
class="fullscreen-signature-dialog"
|
||||
@opened="initCanvas"
|
||||
>
|
||||
<div class="signature-wrapper">
|
||||
<div class="signature-canvas-container" ref="canvasContainerRef">
|
||||
<canvas
|
||||
ref="nativeCanvasRef"
|
||||
class="native-canvas"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@mouseleave="stopDrawing"
|
||||
@touchstart="startDrawing"
|
||||
@touchmove="draw"
|
||||
@touchend="stopDrawing"
|
||||
></canvas>
|
||||
<div class="canvas-tip">请在此区域横屏书写</div>
|
||||
</div>
|
||||
|
||||
<div class="signature-sidebar">
|
||||
<div class="sidebar-title">电子签名</div>
|
||||
<div class="sidebar-actions">
|
||||
<el-button type="warning" @click="clearCanvas">重写</el-button>
|
||||
<el-button @click="handleSignCancel">取消</el-button>
|
||||
<el-button type="success" class="confirm-btn" @click="handleSignConfirm">确认使用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
||||
import QrScanner from '@/components/QrScanner/index.vue'
|
||||
import { getStockByBarcode } from '@/api/outbound'
|
||||
import request from '@/utils/request'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
|
||||
// --- 状态定义 ---
|
||||
const barcodeInput = ref('')
|
||||
const cartItems = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
const barcodeRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
// 签名相关
|
||||
const showSignatureDialog = ref(false)
|
||||
const signaturePreviewUrl = ref('')
|
||||
const signatureFile = ref<File | null>(null)
|
||||
const nativeCanvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const canvasContainerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = ref<CanvasRenderingContext2D | null>(null)
|
||||
const isDrawing = ref(false)
|
||||
const lastX = ref(0)
|
||||
const lastY = ref(0)
|
||||
|
||||
const form = reactive({
|
||||
borrower_name: '',
|
||||
expected_return_time: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// ★ 修改点:增强校验规则
|
||||
const rules = {
|
||||
borrower_name: [
|
||||
{ required: true, message: '请输入借用人姓名', trigger: 'blur' }
|
||||
],
|
||||
expected_return_time: [
|
||||
{ required: true, message: '请选择预计归还日期', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// ★ 新增:禁止选择今天之前的日期
|
||||
const disabledDate = (time: Date) => {
|
||||
return time.getTime() < Date.now() - 8.64e7 // 禁止选择昨天及之前
|
||||
}
|
||||
|
||||
// --- 核心扫码逻辑 ---
|
||||
const onScanSuccess = (code: string) => {
|
||||
if (!code) return
|
||||
const trimCode = code.trim()
|
||||
|
||||
const validPattern = /^[A-Za-z0-9\-\.]+$/
|
||||
if (!validPattern.test(trimCode)) {
|
||||
ElMessage.warning(`识别到异常符号,已忽略:${trimCode}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (trimCode.length < 3) {
|
||||
ElMessage.warning('扫描结果过短,请对准重试')
|
||||
return
|
||||
}
|
||||
|
||||
if (loading.value) return
|
||||
|
||||
barcodeInput.value = trimCode
|
||||
handleManualInput()
|
||||
}
|
||||
|
||||
const handleManualInput = async () => {
|
||||
const code = barcodeInput.value.trim()
|
||||
if (!code) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 查重
|
||||
const existIndex = cartItems.value.findIndex(item => item.barcode === code || item.sku === code)
|
||||
if (existIndex > -1) {
|
||||
const item = cartItems.value[existIndex]
|
||||
const maxQty = parseFloat(item.available_quantity)
|
||||
if (item.out_quantity < maxQty) {
|
||||
item.out_quantity++
|
||||
ElMessage.success(`数量+1 (当前: ${item.out_quantity})`)
|
||||
if (navigator.vibrate) navigator.vibrate(50)
|
||||
} else {
|
||||
ElMessage.warning(`库存不足 (余: ${maxQty})`)
|
||||
}
|
||||
barcodeInput.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 查库
|
||||
const res = await getStockByBarcode(code)
|
||||
if (res.data) {
|
||||
const item = res.data
|
||||
const availQty = parseFloat(item.available_quantity || 0)
|
||||
|
||||
if (availQty <= 0) {
|
||||
ElMessage.warning(`库存不足 (余: ${availQty})`)
|
||||
} else {
|
||||
cartItems.value.push({
|
||||
...item,
|
||||
out_quantity: 1,
|
||||
price: 0
|
||||
})
|
||||
ElMessage.success(`添加成功: ${item.name}`)
|
||||
if (navigator.vibrate) navigator.vibrate(100)
|
||||
}
|
||||
barcodeInput.value = ''
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.status === 404) {
|
||||
ElMessage.error(`未找到条码: ${code}`)
|
||||
} else {
|
||||
ElMessage.error('查询出错')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
nextTick(() => { barcodeRef.value?.focus() })
|
||||
}
|
||||
}
|
||||
|
||||
const removeFromCart = (index: number) => {
|
||||
cartItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
ElMessageBox.confirm('确定清空所有已选物品吗?', '提示', { type: 'warning' })
|
||||
.then(() => {
|
||||
cartItems.value = []
|
||||
form.borrower_name = ''
|
||||
form.remark = ''
|
||||
form.expected_return_time = ''
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
barcodeInput.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
// --- 提交逻辑 ---
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
if (cartItems.value.length === 0) return ElMessage.warning('请先添加物品')
|
||||
|
||||
// ★ 核心修改:等待校验通过后再提交,否则报错会被拦截在前端
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请填写完整的必填项(姓名、归还日期)')
|
||||
return
|
||||
}
|
||||
|
||||
if (!signatureFile.value) {
|
||||
ElMessage.error('请领用人进行电子签名')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 上传签名
|
||||
const uploadRes = await uploadFile(signatureFile.value)
|
||||
const signatureUrl = uploadRes.data.url
|
||||
|
||||
await request({
|
||||
url: '/v1/transactions/borrow',
|
||||
method: 'post',
|
||||
data: {
|
||||
items: cartItems.value,
|
||||
...form, // 此时 form.expected_return_time 已经是 YYYY-MM-DD 格式
|
||||
signature_path: signatureUrl
|
||||
}
|
||||
})
|
||||
|
||||
ElMessage.success('借用成功')
|
||||
cartItems.value = []
|
||||
form.borrower_name = ''
|
||||
form.expected_return_time = ''
|
||||
form.remark = ''
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
showCamera.value = false
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
ElMessage.error(error.response?.data?.msg || '提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- 签名逻辑 ---
|
||||
const openSignatureDialog = () => { showSignatureDialog.value = true }
|
||||
|
||||
const initCanvas = async () => {
|
||||
await nextTick()
|
||||
const canvas = nativeCanvasRef.value
|
||||
const container = canvasContainerRef.value
|
||||
|
||||
if (canvas && container) {
|
||||
canvas.width = container.clientWidth
|
||||
canvas.height = container.clientHeight
|
||||
ctx.value = canvas.getContext('2d')
|
||||
if (ctx.value) {
|
||||
ctx.value.lineWidth = 4
|
||||
ctx.value.lineCap = 'round'
|
||||
ctx.value.lineJoin = 'round'
|
||||
ctx.value.strokeStyle = '#000000'
|
||||
ctx.value.fillStyle = '#ffffff'
|
||||
ctx.value.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getPos = (e: MouseEvent | TouchEvent) => {
|
||||
if (!nativeCanvasRef.value) return { x: 0, y: 0 }
|
||||
const rect = nativeCanvasRef.value.getBoundingClientRect()
|
||||
const clientX = e.type.startsWith('touch') ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX
|
||||
const clientY = e.type.startsWith('touch') ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
||||
return { x: clientX - rect.left, y: clientY - rect.top }
|
||||
}
|
||||
|
||||
const startDrawing = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault()
|
||||
isDrawing.value = true
|
||||
const { x, y } = getPos(e)
|
||||
lastX.value = x; lastY.value = y
|
||||
ctx.value?.beginPath()
|
||||
ctx.value?.moveTo(x, y)
|
||||
}
|
||||
|
||||
const draw = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault()
|
||||
if (!isDrawing.value || !ctx.value) return
|
||||
const { x, y } = getPos(e)
|
||||
ctx.value.lineTo(x, y)
|
||||
ctx.value.stroke()
|
||||
}
|
||||
|
||||
const stopDrawing = () => { isDrawing.value = false }
|
||||
|
||||
const clearCanvas = () => {
|
||||
if (!ctx.value || !nativeCanvasRef.value) return
|
||||
ctx.value.clearRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
|
||||
ctx.value.fillStyle = '#ffffff'
|
||||
ctx.value.fillRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
|
||||
}
|
||||
|
||||
const handleSignConfirm = () => {
|
||||
nativeCanvasRef.value?.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const file = new File([blob], `sign_${Date.now()}.png`, { type: 'image/png' })
|
||||
signatureFile.value = file
|
||||
signaturePreviewUrl.value = URL.createObjectURL(file)
|
||||
showSignatureDialog.value = false
|
||||
}
|
||||
}, 'image/png')
|
||||
}
|
||||
|
||||
const handleSignCancel = () => { showSignatureDialog.value = false }
|
||||
|
||||
onUnmounted(() => {
|
||||
if (signaturePreviewUrl.value) URL.revokeObjectURL(signaturePreviewUrl.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container.mobile-optimized {
|
||||
padding: 10px; max-width: 600px; margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.title-box { font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* 扫码区 */
|
||||
.scan-section { margin-bottom: 20px; }
|
||||
.camera-wrapper {
|
||||
height: 25vh; background: #000; border-radius: 12px; overflow: hidden; position: relative; margin-bottom: 10px;
|
||||
}
|
||||
.scan-overlay {
|
||||
position: absolute; bottom: 10px; right: 10px; z-index: 10;
|
||||
}
|
||||
.camera-placeholder {
|
||||
height: 120px; background: #f5f7fa; border: 1px dashed #dcdfe6; border-radius: 8px;
|
||||
display: flex; flex-direction: column; justify-content: center; align-items: center;
|
||||
color: #909399; margin-bottom: 10px; cursor: pointer;
|
||||
}
|
||||
.camera-placeholder .text { margin-top: 5px; font-size: 13px; }
|
||||
|
||||
/* 表单与购物车 */
|
||||
.cart-section { margin-bottom: 20px; }
|
||||
.form-section { background: #fff; }
|
||||
.signature-box {
|
||||
border: 1px dashed #dcdfe6; border-radius: 6px; height: 100px;
|
||||
background: #fcfcfc; display: flex; justify-content: center; align-items: center; cursor: pointer;
|
||||
}
|
||||
.unsigned-placeholder { display: flex; flex-direction: column; align-items: center; color: #909399; font-size: 13px; }
|
||||
.signed-img img { max-height: 90px; }
|
||||
.re-sign-tip { display: block; text-align: center; font-size: 12px; color: #409EFF; margin-top: 2px; }
|
||||
|
||||
.bottom-actions { display: flex; justify-content: space-between; margin-top: 30px; }
|
||||
.bottom-actions .el-button { width: 48%; }
|
||||
|
||||
/* 全屏签名弹窗 */
|
||||
:deep(.fullscreen-signature-dialog .el-dialog__body) { padding: 0; height: 100%; display: flex; }
|
||||
.signature-wrapper { display: flex; width: 100%; height: 100%; }
|
||||
.signature-canvas-container { flex: 1; position: relative; background: #fff; overflow: hidden; }
|
||||
.native-canvas { display: block; width: 100%; height: 100%; touch-action: none; }
|
||||
.canvas-tip {
|
||||
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: #ccc; font-size: 20px; pointer-events: none; opacity: 0.5; writing-mode: vertical-lr;
|
||||
}
|
||||
.signature-sidebar {
|
||||
width: 120px; background: #333; color: #fff;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px 10px;
|
||||
}
|
||||
.sidebar-title { writing-mode: vertical-rl; font-size: 18px; letter-spacing: 5px; margin-bottom: 30px; font-weight: bold; }
|
||||
.sidebar-actions { display: flex; flex-direction: column; gap: 20px; width: 100%; }
|
||||
.sidebar-actions .el-button { width: 100%; margin: 0; height: 50px; }
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.signature-wrapper { flex-direction: column; }
|
||||
.signature-canvas-container { flex: 1; }
|
||||
.canvas-tip { writing-mode: horizontal-tb; bottom: 50%; }
|
||||
.signature-sidebar { width: 100%; height: auto; flex-direction: row; padding: 10px; justify-content: space-between; }
|
||||
.sidebar-title { display: none; }
|
||||
.sidebar-actions { flex-direction: row; width: 100%; gap: 10px; }
|
||||
.sidebar-actions .el-button { flex: 1; height: 40px; }
|
||||
}
|
||||
</style>
|
||||
198
inventory-web/src/views/transaction/records.vue
Normal file
198
inventory-web/src/views/transaction/records.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-radio-group v-model="status" @change="fetchData" style="margin-right: 20px">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button label="borrowed">未归还</el-radio-button>
|
||||
<el-radio-button label="returned">已归还</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-input v-model="keyword" placeholder="搜索借用人/SKU" style="width: 200px" @keyup.enter="fetchData" />
|
||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="list"
|
||||
border
|
||||
stripe
|
||||
style="margin-top:20px"
|
||||
v-loading="loading"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="borrower_name" label="借用人" width="100" />
|
||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="borrow_time" label="借出时间" width="160" sortable />
|
||||
|
||||
<el-table-column label="归还时间 / 预计" min-width="200">
|
||||
<template #default="{row}">
|
||||
<div v-if="row.status === 'returned'">
|
||||
<el-tag type="success" size="small">实际</el-tag>
|
||||
{{ row.return_time || '-' }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="info" size="small">预计</el-tag>
|
||||
{{ formatExpectedTime(row.expected_return_time).text }}
|
||||
<span :class="formatExpectedTime(row.expected_return_time).cssClass">
|
||||
{{ formatExpectedTime(row.expected_return_time).diffText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status==='returned'?'success':'warning'">
|
||||
{{ row.status==='returned'?'已还':'借出中' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="归还库位" min-width="120">
|
||||
<template #default="{row}">
|
||||
<span v-if="row.return_location">{{ row.return_location }}</span>
|
||||
<span v-else style="color:#ccc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="电子签名" width="140" align="center">
|
||||
<template #default="{row}">
|
||||
<div style="display:flex; justify-content: center; gap:10px">
|
||||
<el-popover trigger="hover" placement="top" v-if="row.borrow_signature" width="220">
|
||||
<template #reference><el-tag size="small">借</el-tag></template>
|
||||
<img :src="row.borrow_signature" style="width:200px; border:1px solid #eee" />
|
||||
</el-popover>
|
||||
|
||||
<el-popover trigger="hover" placement="top" v-if="row.return_signature" width="220">
|
||||
<template #reference><el-tag type="success" size="small">还</el-tag></template>
|
||||
<img :src="row.return_signature" style="width:200px; border:1px solid #eee" />
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="total"
|
||||
@current-change="handlePage"
|
||||
style="margin-top:10px; text-align:right"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
||||
import 'dayjs/locale/zh-cn' // 导入中文包
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
// ★ 修改点:默认状态改为 'borrowed' (未归还)
|
||||
const status = ref('borrowed')
|
||||
const keyword = ref('')
|
||||
const page = ref(1)
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/v1/transactions/records',
|
||||
method: 'get',
|
||||
params: {
|
||||
page: page.value,
|
||||
status: status.value,
|
||||
keyword: keyword.value
|
||||
}
|
||||
})
|
||||
list.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handlePage = (val: number) => {
|
||||
page.value = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// ★ 新增:格式化预计归还时间及倒计时逻辑
|
||||
const formatExpectedTime = (timeStr: string) => {
|
||||
if (!timeStr) return { text: '-', diffText: '', cssClass: '' }
|
||||
|
||||
// 后端返回的可能是 YYYY-MM-DD HH:mm:ss,我们只取日期部分比较
|
||||
const expected = dayjs(timeStr).startOf('day')
|
||||
const today = dayjs().startOf('day')
|
||||
const diffDays = expected.diff(today, 'day')
|
||||
|
||||
let diffText = ''
|
||||
let cssClass = ''
|
||||
|
||||
// 这里的 timeStr 只展示前10位 (日期),或者展示完整
|
||||
// 需求说单号规则是日期,预计归还也主要看日期
|
||||
const displayTime = timeStr.substring(0, 10)
|
||||
|
||||
if (diffDays < 0) {
|
||||
// 逾期
|
||||
diffText = ` (逾期 ${Math.abs(diffDays)} 天)`
|
||||
cssClass = 'text-danger'
|
||||
} else if (diffDays === 0) {
|
||||
// 今天到期
|
||||
diffText = ` (今天到期)`
|
||||
cssClass = 'text-warning'
|
||||
} else {
|
||||
// 剩余
|
||||
diffText = ` (剩 ${diffDays} 天)`
|
||||
cssClass = 'text-normal' // 正常,或者灰色
|
||||
}
|
||||
|
||||
return { text: displayTime, diffText, cssClass }
|
||||
}
|
||||
|
||||
// ★ 新增:表格行样式逻辑
|
||||
const tableRowClassName = ({ row }: { row: any }) => {
|
||||
// 如果已归还,不标颜色
|
||||
if (row.status === 'returned') return ''
|
||||
if (!row.expected_return_time) return ''
|
||||
|
||||
const expected = dayjs(row.expected_return_time).startOf('day')
|
||||
const today = dayjs().startOf('day')
|
||||
const diffDays = expected.diff(today, 'day')
|
||||
|
||||
if (diffDays < 0) {
|
||||
return 'danger-row' // 逾期标红
|
||||
} else if (diffDays === 0) {
|
||||
return 'warning-row' // 当天标黄
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 注意:Element Plus Table 的 row-class-name 样式通常不能放在 scoped 中 */
|
||||
.el-table .warning-row {
|
||||
--el-table-tr-bg-color: #fdf6ec !important; /* 浅橙色/黄色 */
|
||||
}
|
||||
.el-table .danger-row {
|
||||
--el-table-tr-bg-color: #fef0f0 !important; /* 浅红色 */
|
||||
color: #F56C6C; /* 文字变红增强警示 */
|
||||
}
|
||||
|
||||
/* 文字颜色辅助类 */
|
||||
.text-danger {
|
||||
color: #F56C6C;
|
||||
font-weight: bold;
|
||||
}
|
||||
.text-warning {
|
||||
color: #E6A23C;
|
||||
font-weight: bold;
|
||||
}
|
||||
.text-normal {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@ -1 +1,447 @@
|
||||
<template><div style="padding:20px;"><h2>维修登记</h2></div></template>
|
||||
<template>
|
||||
<div class="app-container mobile-optimized">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="title-box">
|
||||
<span>还库作业 (库管签字)</span>
|
||||
<el-tag v-if="returnList.length > 0" type="success" size="small" effect="dark">
|
||||
待还 {{ returnList.length }} 项
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="scan-section">
|
||||
<div v-if="showCamera" class="camera-wrapper">
|
||||
<QrScanner @decode="onScanSuccess" />
|
||||
<div class="scan-overlay">
|
||||
<el-button type="info" size="small" bg text @click="showCamera = false" icon="Close">
|
||||
关闭摄像头
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="camera-placeholder" @click="showCamera = true">
|
||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||
<span class="text">点击开启扫码</span>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-input
|
||||
v-model="barcode"
|
||||
placeholder="扫描已借出物品条码"
|
||||
@keyup.enter="scanItem"
|
||||
clearable
|
||||
ref="barcodeRef"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Scissor /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="scanItem">识别</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-section">
|
||||
<div v-if="returnList.length > 0">
|
||||
<el-table :data="returnList" border stripe style="width: 100%">
|
||||
<el-table-column prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
|
||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="归还库位(可改)" min-width="160">
|
||||
<template #default="{row}">
|
||||
<el-input
|
||||
v-model="row.return_location"
|
||||
:placeholder="`原: ${row.current_location || '无'}`"
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<template #append v-if="row.return_location !== row.current_location">
|
||||
<span style="color: #E6A23C; font-size: 12px;">变更</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||
<template #default="{$index}">
|
||||
<el-button type="danger" icon="Delete" circle size="small" @click="returnList.splice($index, 1)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-empty v-else description="请扫描已借出的条码" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<div v-if="returnList.length > 0" class="form-section">
|
||||
<el-divider content-position="left">还库确认</el-divider>
|
||||
|
||||
<div style="margin-bottom: 10px; font-size: 14px; color: #606266; padding: 0 10px;">
|
||||
请库管员在此签字确认入库:
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item required>
|
||||
<div class="signature-box" @click="openSignatureDialog">
|
||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||
<img :src="signaturePreviewUrl" alt="签名" />
|
||||
<span class="re-sign-tip">点击重签</span>
|
||||
</div>
|
||||
<div v-else class="unsigned-placeholder">
|
||||
<el-icon :size="24"><EditPen /></el-icon>
|
||||
<span>点击此处进行库管签名</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="bottom-actions">
|
||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button type="success" size="large" :loading="loading" @click="preSubmitCheck" icon="Select">
|
||||
确认归还
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="showSignatureDialog"
|
||||
fullscreen
|
||||
destroy-on-close
|
||||
:show-close="false"
|
||||
class="fullscreen-signature-dialog"
|
||||
@opened="initCanvas"
|
||||
>
|
||||
<div class="signature-wrapper">
|
||||
<div class="signature-canvas-container" ref="canvasContainerRef">
|
||||
<canvas
|
||||
ref="nativeCanvasRef"
|
||||
class="native-canvas"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@mouseleave="stopDrawing"
|
||||
@touchstart="startDrawing"
|
||||
@touchmove="draw"
|
||||
@touchend="stopDrawing"
|
||||
></canvas>
|
||||
<div class="canvas-tip">请在此区域横屏书写</div>
|
||||
</div>
|
||||
|
||||
<div class="signature-sidebar">
|
||||
<div class="sidebar-title">电子签名</div>
|
||||
<div class="sidebar-actions">
|
||||
<el-button type="warning" @click="clearCanvas">重写</el-button>
|
||||
<el-button @click="handleSignCancel">取消</el-button>
|
||||
<el-button type="success" class="confirm-btn" @click="handleSignConfirm">确认使用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onUnmounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
||||
import QrScanner from '@/components/QrScanner/index.vue'
|
||||
|
||||
// --- 状态 ---
|
||||
const barcode = ref('')
|
||||
const returnList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
const barcodeRef = ref()
|
||||
|
||||
// 签名状态
|
||||
const showSignatureDialog = ref(false)
|
||||
const signaturePreviewUrl = ref('')
|
||||
const signatureFile = ref<File | null>(null)
|
||||
const nativeCanvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const canvasContainerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = ref<CanvasRenderingContext2D | null>(null)
|
||||
const isDrawing = ref(false)
|
||||
const lastX = ref(0)
|
||||
const lastY = ref(0)
|
||||
|
||||
// --- 扫码回调 (复刻) ---
|
||||
const onScanSuccess = (code: string) => {
|
||||
if (!code) return
|
||||
const trimCode = code.trim()
|
||||
|
||||
const validPattern = /^[A-Za-z0-9\-\.]+$/
|
||||
if (!validPattern.test(trimCode)) {
|
||||
ElMessage.warning(`识别到异常符号,已忽略:${trimCode}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (trimCode.length < 3) {
|
||||
ElMessage.warning('扫描结果过短,请对准重试')
|
||||
return
|
||||
}
|
||||
|
||||
if (loading.value) return
|
||||
|
||||
barcode.value = trimCode
|
||||
scanItem()
|
||||
}
|
||||
|
||||
const scanItem = async () => {
|
||||
const code = barcode.value.trim()
|
||||
if(!code) return
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await request({
|
||||
url: '/v1/transactions/return/scan',
|
||||
method: 'get',
|
||||
params: { barcode: code }
|
||||
})
|
||||
|
||||
if(returnList.value.some(i => i.id === res.data.id)) {
|
||||
ElMessage.warning('已在清单中')
|
||||
barcode.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const item = res.data
|
||||
// 默认将归还库位填为当前库位
|
||||
item.return_location = item.current_location || ''
|
||||
|
||||
returnList.value.push(item)
|
||||
barcode.value = ''
|
||||
ElMessage.success('识别成功')
|
||||
} catch(e) {
|
||||
ElMessage.error('未找到该物品的未还记录')
|
||||
} finally {
|
||||
loading.value = false
|
||||
nextTick(() => { barcodeRef.value?.focus() })
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
ElMessageBox.confirm('确定清空所有待还物品吗?', '提示', { type: 'warning' })
|
||||
.then(() => {
|
||||
returnList.value = []
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
barcode.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
// --- 提交前检查 (库位变更警告) ---
|
||||
const preSubmitCheck = async () => {
|
||||
if (returnList.value.length === 0) return
|
||||
|
||||
if (!signatureFile.value) {
|
||||
ElMessage.error('库管必须签字确认')
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 处理空输入:默认为原库位
|
||||
returnList.value.forEach(item => {
|
||||
if (!item.return_location || item.return_location.trim() === '') {
|
||||
item.return_location = item.current_location
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 检测变更
|
||||
const changedItems = returnList.value.filter(
|
||||
item => item.return_location !== item.current_location
|
||||
)
|
||||
|
||||
// 3. 弹窗确认
|
||||
if (changedItems.length > 0) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`检测到 ${changedItems.length} 个物品的库位发生变更。\n\n请务必打印新的标签并贴在物品上!\n\n是否确认继续?`,
|
||||
'库位变更提醒',
|
||||
{
|
||||
confirmButtonText: '已知晓,确认归还',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
center: true
|
||||
}
|
||||
)
|
||||
submitReturn()
|
||||
} catch {
|
||||
// 用户取消
|
||||
return
|
||||
}
|
||||
} else {
|
||||
submitReturn()
|
||||
}
|
||||
}
|
||||
|
||||
const submitReturn = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const upRes = await uploadFile(signatureFile.value!)
|
||||
|
||||
await request({
|
||||
url: '/v1/transactions/return',
|
||||
method: 'post',
|
||||
data: {
|
||||
items: returnList.value,
|
||||
signature_path: upRes.data.url
|
||||
}
|
||||
})
|
||||
ElMessage.success('还库成功')
|
||||
returnList.value = []
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
showCamera.value = false // 关闭摄像头
|
||||
} catch(e: any) {
|
||||
ElMessage.error(e.response?.data?.msg || '提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- 签名逻辑 (复刻) ---
|
||||
const openSignatureDialog = () => { showSignatureDialog.value = true }
|
||||
|
||||
const initCanvas = async () => {
|
||||
await nextTick()
|
||||
const canvas = nativeCanvasRef.value
|
||||
const container = canvasContainerRef.value
|
||||
|
||||
if (canvas && container) {
|
||||
canvas.width = container.clientWidth
|
||||
canvas.height = container.clientHeight
|
||||
ctx.value = canvas.getContext('2d')
|
||||
if (ctx.value) {
|
||||
ctx.value.lineWidth = 4
|
||||
ctx.value.lineCap = 'round'
|
||||
ctx.value.lineJoin = 'round'
|
||||
ctx.value.strokeStyle = '#000000'
|
||||
ctx.value.fillStyle = '#ffffff'
|
||||
ctx.value.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getPos = (e: MouseEvent | TouchEvent) => {
|
||||
if (!nativeCanvasRef.value) return { x: 0, y: 0 }
|
||||
const rect = nativeCanvasRef.value.getBoundingClientRect()
|
||||
const clientX = e.type.startsWith('touch') ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX
|
||||
const clientY = e.type.startsWith('touch') ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
||||
return { x: clientX - rect.left, y: clientY - rect.top }
|
||||
}
|
||||
|
||||
const startDrawing = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault()
|
||||
isDrawing.value = true
|
||||
const { x, y } = getPos(e)
|
||||
lastX.value = x; lastY.value = y
|
||||
ctx.value?.beginPath()
|
||||
ctx.value?.moveTo(x, y)
|
||||
}
|
||||
|
||||
const draw = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault()
|
||||
if (!isDrawing.value || !ctx.value) return
|
||||
const { x, y } = getPos(e)
|
||||
ctx.value.lineTo(x, y)
|
||||
ctx.value.stroke()
|
||||
}
|
||||
|
||||
const stopDrawing = () => { isDrawing.value = false }
|
||||
|
||||
const clearCanvas = () => {
|
||||
if (!ctx.value || !nativeCanvasRef.value) return
|
||||
ctx.value.clearRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
|
||||
ctx.value.fillStyle = '#ffffff'
|
||||
ctx.value.fillRect(0, 0, nativeCanvasRef.value.width, nativeCanvasRef.value.height)
|
||||
}
|
||||
|
||||
const handleSignConfirm = () => {
|
||||
nativeCanvasRef.value?.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const file = new File([blob], `sign_${Date.now()}.png`, { type: 'image/png' })
|
||||
signatureFile.value = file
|
||||
signaturePreviewUrl.value = URL.createObjectURL(file)
|
||||
showSignatureDialog.value = false
|
||||
}
|
||||
}, 'image/png')
|
||||
}
|
||||
|
||||
const handleSignCancel = () => { showSignatureDialog.value = false }
|
||||
|
||||
onUnmounted(() => {
|
||||
if (signaturePreviewUrl.value) URL.revokeObjectURL(signaturePreviewUrl.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container.mobile-optimized {
|
||||
padding: 10px; max-width: 600px; margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.title-box { font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* 扫码区 */
|
||||
.scan-section { margin-bottom: 20px; }
|
||||
.camera-wrapper {
|
||||
height: 25vh; background: #000; border-radius: 12px; overflow: hidden; position: relative; margin-bottom: 10px;
|
||||
}
|
||||
.scan-overlay {
|
||||
position: absolute; bottom: 10px; right: 10px; z-index: 10;
|
||||
}
|
||||
.camera-placeholder {
|
||||
height: 120px; background: #f5f7fa; border: 1px dashed #dcdfe6; border-radius: 8px;
|
||||
display: flex; flex-direction: column; justify-content: center; align-items: center;
|
||||
color: #909399; margin-bottom: 10px; cursor: pointer;
|
||||
}
|
||||
.camera-placeholder .text { margin-top: 5px; font-size: 13px; }
|
||||
|
||||
/* 表单与购物车 */
|
||||
.cart-section { margin-bottom: 20px; }
|
||||
.form-section { background: #fff; }
|
||||
.signature-box {
|
||||
border: 1px dashed #dcdfe6; border-radius: 6px; height: 100px;
|
||||
background: #fcfcfc; display: flex; justify-content: center; align-items: center; cursor: pointer;
|
||||
}
|
||||
.unsigned-placeholder { display: flex; flex-direction: column; align-items: center; color: #909399; font-size: 13px; }
|
||||
.signed-img img { max-height: 90px; }
|
||||
.re-sign-tip { display: block; text-align: center; font-size: 12px; color: #409EFF; margin-top: 2px; }
|
||||
|
||||
.bottom-actions { display: flex; justify-content: space-between; margin-top: 30px; }
|
||||
.bottom-actions .el-button { width: 48%; }
|
||||
|
||||
/* 全屏签名弹窗 */
|
||||
:deep(.fullscreen-signature-dialog .el-dialog__body) { padding: 0; height: 100%; display: flex; }
|
||||
.signature-wrapper { display: flex; width: 100%; height: 100%; }
|
||||
.signature-canvas-container { flex: 1; position: relative; background: #fff; overflow: hidden; }
|
||||
.native-canvas { display: block; width: 100%; height: 100%; touch-action: none; }
|
||||
.canvas-tip {
|
||||
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: #ccc; font-size: 20px; pointer-events: none; opacity: 0.5; writing-mode: vertical-lr;
|
||||
}
|
||||
.signature-sidebar {
|
||||
width: 120px; background: #333; color: #fff;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px 10px;
|
||||
}
|
||||
.sidebar-title { writing-mode: vertical-rl; font-size: 18px; letter-spacing: 5px; margin-bottom: 30px; font-weight: bold; }
|
||||
.sidebar-actions { display: flex; flex-direction: column; gap: 20px; width: 100%; }
|
||||
.sidebar-actions .el-button { width: 100%; margin: 0; height: 50px; }
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.signature-wrapper { flex-direction: column; }
|
||||
.signature-canvas-container { flex: 1; }
|
||||
.canvas-tip { writing-mode: horizontal-tb; bottom: 50%; }
|
||||
.signature-sidebar { width: 100%; height: auto; flex-direction: row; padding: 10px; justify-content: space-between; }
|
||||
.sidebar-title { display: none; }
|
||||
.sidebar-actions { flex-direction: row; width: 100%; gap: 10px; }
|
||||
.sidebar-actions .el-button { flex: 1; height: 40px; }
|
||||
}
|
||||
</style>
|
||||
@ -1 +0,0 @@
|
||||
<template><div style="padding:20px;"><h2>报废处理</h2></div></template>
|
||||
Reference in New Issue
Block a user