feat: 完成了物料总表,生产发料,入料,以及部分采购计划。

This commit is contained in:
c
2026-03-06 15:04:57 +08:00
parent 219eef4729
commit b12b758be2
27 changed files with 3681 additions and 685 deletions

View File

@@ -69,7 +69,6 @@ defineExpose({
tableMainRef.value?.toggleRowExpansion(row, expanded);
},
sort: (field: string, order: string) => {
console.log(tableMainRef.value?.tableRef);
tableMainRef.value?.tableRef?.sort(field, order);
},
});
@@ -78,6 +77,7 @@ onMounted(loadData);
</script>
<template>
<TableHeader
v-if="searchers !== undefined"
:searchers="searchers"
:tool-buttons="toolButtons"
v-model:searcher-params="searcherParams"

View File

@@ -0,0 +1,188 @@
<script lang="ts" setup>
import QRCode from "qrcode";
const props = defineProps({
title: {
type: String,
default: "",
},
qrCodeContent: {
type: String,
default: "",
},
qrCodeSize: {
type: Number,
default: 200,
},
showLabel: {
type: Boolean,
default: true,
},
label: {
type: String,
default: "",
},
});
const visible = defineModel<boolean>("visible");
const qrCodeDataUrl = ref("");
const loading = ref(false);
const generateQrCode = async () => {
if (!props.qrCodeContent) {
return;
}
loading.value = true;
try {
qrCodeDataUrl.value = await QRCode.toDataURL(props.qrCodeContent, {
width: props.qrCodeSize,
margin: 2,
color: {
dark: "#000000",
light: "#ffffff",
},
});
} catch (error) {
console.error("Generate QR code failed:", error);
} finally {
loading.value = false;
}
};
const handlePrint = () => {
const printWindow = window.open("", "_blank");
if (!printWindow) {
return;
}
const imgHtml = `
<!DOCTYPE html>
<html>
<head>
<title>${props.title || "QR Code"}</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
img {
max-width: 100%;
}
.label {
margin-top: 10px;
font-size: 16px;
font-weight: bold;
text-align: center;
}
@media print {
body {
padding: 0;
}
}
</style>
</head>
<body>
<img src="${qrCodeDataUrl.value}" alt="QR Code" />
${props.showLabel ? `<div class="label">${props.label || props.qrCodeContent}</div>` : ""}
</body>
</html>
`;
printWindow.document.write(imgHtml);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
};
const handleDownload = () => {
const link = document.createElement("a");
link.download = `${props.label || props.qrCodeContent || "qrcode"}.png`;
link.href = qrCodeDataUrl.value;
link.click();
};
watch(
() => props.qrCodeContent,
() => {
if (visible.value && props.qrCodeContent) {
generateQrCode();
}
}
);
watch(visible, newVal => {
if (newVal && props.qrCodeContent) {
generateQrCode();
}
});
</script>
<template>
<el-dialog
:title="title"
v-model="visible"
width="400px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div v-loading="loading" class="qrcode-container">
<div v-if="qrCodeDataUrl" class="qrcode-display">
<img :src="qrCodeDataUrl" alt="QR Code" />
<div v-if="showLabel" class="qrcode-label">{{ label || qrCodeContent }}</div>
</div>
<div v-else class="qrcode-placeholder">
{{ $t("_message.warehouse.warehouse_item.no_qrcode_content") }}
</div>
</div>
<template #footer>
<el-button @click="visible = false">{{ $t("_button.cancel") }}</el-button>
<el-button type="primary" @click="handleDownload" :disabled="!qrCodeDataUrl">
{{ $t("_button.download") }}
</el-button>
<el-button type="success" @click="handlePrint" :disabled="!qrCodeDataUrl">
{{ $t("_button.print") }}
</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.qrcode-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
}
.qrcode-display {
display: flex;
flex-direction: column;
align-items: center;
}
.qrcode-display img {
max-width: 100%;
height: auto;
}
.qrcode-label {
margin-top: 10px;
font-size: 14px;
font-weight: bold;
text-align: center;
word-break: break-all;
}
.qrcode-placeholder {
font-size: 14px;
color: #909399;
}
</style>

View File

@@ -0,0 +1,263 @@
<script lang="ts" setup>
import { $t } from "@/common/languages";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { get, post } from "@/common/http/request";
import { Delete, Edit, Plus } from "@element-plus/icons-vue";
const props = defineProps({
title: {
type: String,
default: "",
},
warehouseItemId: {
type: Number,
default: null,
},
selectedVendorUrl: {
type: String,
default: "",
},
saveVendorUrl: {
type: String,
default: "",
},
});
const emit = defineEmits<{
"update:visible": [value: boolean];
saved: [];
}>();
const visible = defineModel<boolean>("visible");
const loading = ref(false);
const saving = ref(false);
const formRef = ref<FormInstance>();
const vendorList = ref<any[]>([]);
const allVendors = ref<any[]>([]);
const rules = reactive<FormRules>({
vendorId: [{ required: true, message: $t("_message.warehouse.warehouse_item.select_vendor"), trigger: "change" }],
costPrice: [{ required: true, message: $t("_message.warehouse.warehouse_item.input_costPrice"), trigger: "blur" }],
});
const loadVendorList = async () => {
if (!props.warehouseItemId) {
return;
}
loading.value = true;
try {
const response = await get(props.selectedVendorUrl, { warehouseItemId: props.warehouseItemId });
vendorList.value = response.data || [];
} catch (error) {
console.error("Load vendor list failed:", error);
} finally {
loading.value = false;
}
};
const loadAllVendors = async () => {
try {
const response = await get("/systemset/vendor/getVendorPage", { current: 1, size: 1000 });
allVendors.value = response.data.records || [];
} catch (error) {
console.error("Load all vendors failed:", error);
}
};
const addRow = () => {
vendorList.value.push({
id: null,
vendorId: null,
vendorName: "",
costPrice: null,
procureDate: null,
});
};
const editRow = (row: any) => {
row.isEditing = true;
};
const deleteRow = (index: number) => {
vendorList.value.splice(index, 1);
};
const saveRow = (row: any) => {
row.isEditing = false;
};
const cancelEdit = (row: any, originalRow: any) => {
row.vendorId = originalRow.vendorId;
row.costPrice = originalRow.costPrice;
row.procureDate = originalRow.procureDate;
row.isEditing = false;
};
const handleSave = async () => {
if (!formRef.value) {
return;
}
try {
await formRef.value.validate();
} catch (error) {
return;
}
if (vendorList.value.length === 0) {
ElMessage.warning($t("_message.warehouse.warehouse_item.select_vendor"));
return;
}
saving.value = true;
try {
const vendorListData = vendorList.value.map(item => ({
vendorId: item.vendorId,
costPrice: item.costPrice,
procureDate: item.procureDate,
}));
await post(props.saveVendorUrl, {
warehouseItemId: props.warehouseItemId,
vendorList: vendorListData,
});
ElMessage.success($t("_message.common.edit_success"));
visible.value = false;
emit("saved");
} catch (error) {
console.error("Save vendor list failed:", error);
} finally {
saving.value = false;
}
};
const getVendorName = (vendorId: number) => {
const vendor = allVendors.value.find(v => v.id === vendorId);
return vendor ? vendor.vendorName : "";
};
watch(
() => visible.value,
val => {
if (val) {
loadVendorList();
loadAllVendors();
}
}
);
</script>
<template>
<el-dialog
:title="title"
v-model="visible"
width="900px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form ref="formRef" :model="{ vendorList }" label-width="120px">
<el-table :data="vendorList" border style="width: 100%" max-height="50vh" v-loading="loading">
<el-table-column :label="$t('_prop.warehouse.warehouse_item.vendorName')" width="200">
<template #default="{ row, $index }">
<el-form-item v-if="row.isEditing" :prop="`vendorList.${$index}.vendorId`" :rules="rules.vendorId">
<el-select
v-model="row.vendorId"
:placeholder="$t('_message.warehouse.warehouse_item.select_vendor')"
filterable
style="width: 100%"
>
<el-option
v-for="vendor in allVendors"
:key="vendor.id"
:label="vendor.vendorName"
:value="vendor.id"
/>
</el-select>
</el-form-item>
<span v-else>{{ getVendorName(row.vendorId) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.warehouse.warehouse_item.costPrice')" width="150">
<template #default="{ row, $index }">
<el-form-item v-if="row.isEditing" :prop="`vendorList.${$index}.costPrice`" :rules="rules.costPrice">
<el-input-number
v-model="row.costPrice"
:min="0"
:precision="2"
:placeholder="$t('_message.warehouse.warehouse_item.input_costPrice')"
style="width: 100%"
/>
</el-form-item>
<span v-else>{{ row.costPrice }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.warehouse.warehouse_item.procureDate')" width="200">
<template #default="{ row, $index }">
<el-form-item v-if="row.isEditing" :prop="`vendorList.${$index}.procureDate`">
<el-date-picker
v-model="row.procureDate"
type="datetime"
:placeholder="$t('_message.warehouse.warehouse_item.select_procureDate')"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<span v-else>{{ row.procureDate }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.common.operate')" width="150" align="center">
<template #default="{ row, $index }">
<template v-if="row.isEditing">
<el-button type="primary" link size="small" @click="saveRow(row)">
{{ $t("_button.confirm") }}
</el-button>
<el-button link size="small" @click="cancelEdit(row, { ...row })">
{{ $t("_button.cancel") }}
</el-button>
</template>
<template v-else>
<el-button type="primary" link size="small" @click="editRow(row)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button type="danger" link size="small" @click="deleteRow($index)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-form>
<div class="table-actions">
<el-button type="primary" @click="addRow">
<el-icon><Plus /></el-icon>
{{ $t("_button.add") }}
</el-button>
</div>
<template #footer>
<el-button @click="visible = false">{{ $t("_button.cancel") }}</el-button>
<el-button type="primary" @click="handleSave" :loading="saving">
{{ $t("_button.confirm") }}
</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.table-actions {
margin-top: 10px;
text-align: right;
}
.el-form-item {
margin-bottom: 0;
}
</style>

View File

@@ -1,22 +1,34 @@
<script lang="ts" setup>
import { $t } from "@/common/languages";
const props = defineProps({
authShowFunc: Function,
});
const emit = defineEmits<{ "operate-button-click": [eventName: string, row: any] }>();
const buttonList = useRoute().meta.toolButtonAuth ?? [];
const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
if (props.authShowFunc !== undefined) {
return props.authShowFunc(row, button);
}
return true;
};
</script>
<template>
<el-table-column :label="$t('_prop.common.operate')" v-if="buttonList?.length !== 0">
<template #default="scope">
<el-button
v-for="button of buttonList"
:key="button.buttonName"
:type="button.colorType || 'primary'"
@click="emit('operate-button-click', button.eventName, scope.row)"
link
>
{{ $t("_button." + button.eventName) }}
</el-button>
<template #default="{ row }">
<template v-for="button of buttonList" :key="button.buttonName">
<el-button
v-if="authShowFunc(row, button)"
:type="button.colorType || 'primary'"
@click="emit('operate-button-click', button.eventName, row)"
link
>
{{ $t("_button." + button.eventName) }}
</el-button>
</template>
</template>
</el-table-column>
</template>

View File

@@ -1,37 +1,36 @@
<script lang="ts" setup>
import { post } from "@/common/http/request";
import StatusSwitch from "../base-switch/StatusSwitch.vue";
import { ElMessage } from "element-plus";
import { $t } from "@/common/languages";
import { useStatus } from "@/common/languages/mapping/base-info-mapping";
const props = defineProps({
url: String,
tableEl: ref,
statusLabelMapping: Function,
statusParamName: {
type: String,
default: "status",
},
switchOnValue: {
type: Number,
default: 0,
},
switchOffValue: {
type: Number,
default: 1,
},
});
const emit = defineEmits<{
change: [val: any, row: any];
}>();
const { getCommonStatusLabel } = useStatus();
const buttonList = useRoute().meta.statusButtonAuth ?? [];
const change = (val: any, row: any) => {
if (props.url !== undefined)
post(props.url, { id: row.id, status: val })
.then(res => {
if (res.code === 0) {
ElMessage.success((val === 1 ? $t("_button.enable") : $t("_button.disable")) + "成功");
} else {
ElMessage.error((val === 1 ? $t("_button.enable") : $t("_button.disable")) + "失败,信息为:" + res.msg);
}
})
.catch(err => {
ElMessage.error((val === 1 ? $t("_button.enable") : $t("_button.disable")) + "失败,信息为:" + err);
});
emit("change", val, row);
};
const getLabel = (code: number | null) => {
@@ -49,7 +48,10 @@ const getLabel = (code: number | null) => {
<StatusSwitch
v-model="scope.row[statusParamName]"
@change="val => change(val, scope.row)"
v-if="button.buttonType === 'switch'"
v-if="
(button.eventName === 'enable' && scope.row[statusParamName] === switchOffValue) ||
(button.eventName === 'disable' && scope.row[statusParamName] === switchOnValue)
"
/>
</template>
</template>

View File

@@ -186,7 +186,7 @@ const wrappedParse = (rawData: any) => {
defineExpose({
reload: () => {
baseTableRef.value?.reload;
baseTableRef.value?.reload();
},
});
</script>
@@ -236,18 +236,19 @@ defineExpose({
<style scoped>
.item-container {
min-height: 50px;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
min-height: 50px;
}
.loading-state,
.empty-state {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
gap: 8px;
color: #909399;
}
</style>