|
|
@@ -0,0 +1,373 @@
|
|
|
+<template>
|
|
|
+ <SidebarLayout
|
|
|
+ :menuItems="menuItems"
|
|
|
+ :userName="authStore.user?.name"
|
|
|
+ :activeKey="'cut-batches'"
|
|
|
+ @logout="handleLogout"
|
|
|
+ >
|
|
|
+ <div class="cut-batches-page">
|
|
|
+ <a-card>
|
|
|
+ <a-space>
|
|
|
+ <a-input-search v-model:value="searchKeyword" placeholder="搜索批次号" style="width: 300px" @search="loadBatches" />
|
|
|
+ <a-button type="primary" @click="showCreateModal">创建批次</a-button>
|
|
|
+ </a-space>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="mt-4" title="裁床批次列表">
|
|
|
+ <a-table :columns="columns" :dataSource="batches" :loading="loading" rowKey="id">
|
|
|
+ <template #status="{ record }">
|
|
|
+ <a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
|
|
+ </template>
|
|
|
+ <template #action="{ record }">
|
|
|
+ <a-space>
|
|
|
+ <a-button size="small" @click="showBundlesModal(record)">查看扎号</a-button>
|
|
|
+ <a-button size="small" @click="showEditModal(record)">编辑</a-button>
|
|
|
+ <a-popconfirm title="确定删除此批次吗?" @confirm="deleteBatch(record.id)">
|
|
|
+ <a-button size="small" danger>删除</a-button>
|
|
|
+ </a-popconfirm>
|
|
|
+ </a-space>
|
|
|
+ </template>
|
|
|
+ </a-table>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- 创建/编辑批次模态框 -->
|
|
|
+ <a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleSubmit" :confirmLoading="submitting" width="600px">
|
|
|
+ <a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
|
|
|
+ <a-form-item label="工单" name="work_order_id" v-if="!isEdit">
|
|
|
+ <a-select v-model:value="formState.work_order_id" placeholder="请选择工单">
|
|
|
+ <a-select-option v-for="wo in workOrders" :key="wo.id" :value="wo.id">
|
|
|
+ {{ wo.order_no }} - {{ wo.style_name }}
|
|
|
+ </a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="批次号" name="batch_no" v-if="!isEdit">
|
|
|
+ <a-input v-model:value="formState.batch_no" placeholder="请输入批次号" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="颜色" name="color">
|
|
|
+ <a-input v-model:value="formState.color" placeholder="请输入颜色" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="总数量" name="total_qty">
|
|
|
+ <a-input-number v-model:value="formState.total_qty" :min="1" :max="100000" style="width: 100%" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-modal>
|
|
|
+
|
|
|
+ <!-- 扎号管理模态框 -->
|
|
|
+ <a-modal v-model:open="bundlesModalVisible" :title="`扎号管理 - ${currentBatch?.batch_no}`" width="900px" :footer="null">
|
|
|
+ <a-space class="mb-4">
|
|
|
+ <a-button type="primary" @click="showAddBundleModal">添加扎号</a-button>
|
|
|
+ <a-button @click="generateAllQRCodes">批量生成二维码</a-button>
|
|
|
+ </a-space>
|
|
|
+ <a-table :columns="bundleColumns" :dataSource="bundles" rowKey="id" size="small">
|
|
|
+ <template #action="{ record }">
|
|
|
+ <a-space>
|
|
|
+ <a-button size="small" @click="showBundleQRCode(record)">二维码</a-button>
|
|
|
+ <a-popconfirm title="删除此扎号?" @confirm="deleteBundle(record.id)">
|
|
|
+ <a-button size="small" danger>删除</a-button>
|
|
|
+ </a-popconfirm>
|
|
|
+ </a-space>
|
|
|
+ </template>
|
|
|
+ </a-table>
|
|
|
+ </a-modal>
|
|
|
+
|
|
|
+ <!-- 添加扎号模态框 -->
|
|
|
+ <a-modal v-model:open="addBundleModalVisible" title="添加扎号" @ok="handleAddBundle" :confirmLoading="addingBundle">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="尺码" required>
|
|
|
+ <a-input v-model:value="newBundle.size" placeholder="如:S/M/L/XL" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="数量" required>
|
|
|
+ <a-input-number v-model:value="newBundle.quantity" :min="1" :max="10000" style="width: 100%" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-modal>
|
|
|
+
|
|
|
+ <!-- 单个扎号二维码展示 -->
|
|
|
+ <a-modal v-model:open="bundleQRVisible" title="扎号二维码" :footer="null" width="400px">
|
|
|
+ <div class="qr-code-container">
|
|
|
+ <img :src="bundleQRUrl" alt="扎号二维码" style="width: 100%" />
|
|
|
+ <p class="qr-code-tip">扎号: {{ currentBundle?.bundle_no }}</p>
|
|
|
+ </div>
|
|
|
+ </a-modal>
|
|
|
+ </div>
|
|
|
+ </SidebarLayout>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted } from 'vue';
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
+import { message } from 'ant-design-vue';
|
|
|
+import { useAuthStore } from '@/stores/auth';
|
|
|
+import { SidebarLayout } from '@smartcut/shared-components';
|
|
|
+import { WorkOrderApi, createApiClient } from '@smartcut/api-client';
|
|
|
+import { STORAGE_KEYS } from '@smartcut/shared-utils';
|
|
|
+import type { WorkOrder, CutBatch, CutBundle } from '@smartcut/types';
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
+const authStore = useAuthStore();
|
|
|
+
|
|
|
+const menuItems = [
|
|
|
+ { key: 'dashboard', label: '主页看板', icon: 'DashboardOutlined', path: '/' },
|
|
|
+ { key: 'work-orders', label: '工单管理', icon: 'FileOutlined', path: '/work-orders' },
|
|
|
+ { key: 'processes', label: '工序管理', icon: 'AppstoreOutlined', path: '/processes' },
|
|
|
+ { key: 'records', label: '计件记录', icon: 'UnorderedListOutlined', path: '/records' },
|
|
|
+ { key: 'salary', label: '工资管理', icon: 'PayCircleOutlined', path: '/salary' },
|
|
|
+ { key: 'users', label: '用户管理', icon: 'UserOutlined', path: '/users' }
|
|
|
+];
|
|
|
+
|
|
|
+const apiClient = createApiClient({
|
|
|
+ baseURL: window.location.origin,
|
|
|
+ tokenKey: STORAGE_KEYS.FACTORY_TOKEN
|
|
|
+});
|
|
|
+
|
|
|
+const workOrderApi = new WorkOrderApi(apiClient);
|
|
|
+
|
|
|
+const batches = ref<CutBatch[]>([]);
|
|
|
+const workOrders = ref<WorkOrder[]>([]);
|
|
|
+const bundles = ref<CutBundle[]>([]);
|
|
|
+const loading = ref(false);
|
|
|
+const searchKeyword = ref('');
|
|
|
+
|
|
|
+const columns = [
|
|
|
+ { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
|
|
+ { title: '批次号', dataIndex: 'batch_no', key: 'batch_no', width: 150 },
|
|
|
+ { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
|
|
|
+ { title: '颜色', dataIndex: 'color', key: 'color', width: 100 },
|
|
|
+ { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
|
|
|
+ { title: '完成数量', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
|
|
|
+ { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
|
|
|
+ { title: '操作', key: 'action', width: 250, slots: { customRender: 'action' } }
|
|
|
+];
|
|
|
+
|
|
|
+const bundleColumns = [
|
|
|
+ { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
|
|
+ { title: '扎号', dataIndex: 'bundle_no', key: 'bundle_no', width: 120 },
|
|
|
+ { title: '尺码', dataIndex: 'size', key: 'size', width: 100 },
|
|
|
+ { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 100 },
|
|
|
+ { title: '操作', key: 'action', width: 150, slots: { customRender: 'action' } }
|
|
|
+];
|
|
|
+
|
|
|
+const modalVisible = ref(false);
|
|
|
+const modalTitle = ref('创建批次');
|
|
|
+const isEdit = ref(false);
|
|
|
+const submitting = ref(false);
|
|
|
+const formRef = ref();
|
|
|
+const editingId = ref<number | null>(null);
|
|
|
+
|
|
|
+const formState = reactive({
|
|
|
+ work_order_id: null as number | null,
|
|
|
+ batch_no: '',
|
|
|
+ color: '',
|
|
|
+ total_qty: 1
|
|
|
+});
|
|
|
+
|
|
|
+const formRules = {
|
|
|
+ work_order_id: [{ required: true, message: '请选择工单', trigger: 'change' }],
|
|
|
+ batch_no: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
|
|
|
+ total_qty: [{ required: true, message: '请输入总数量', trigger: 'blur' }]
|
|
|
+};
|
|
|
+
|
|
|
+const bundlesModalVisible = ref(false);
|
|
|
+const currentBatch = ref<CutBatch | null>(null);
|
|
|
+const addBundleModalVisible = ref(false);
|
|
|
+const addingBundle = ref(false);
|
|
|
+const newBundle = reactive({ size: '', quantity: 1 });
|
|
|
+
|
|
|
+const bundleQRVisible = ref(false);
|
|
|
+const bundleQRUrl = ref('');
|
|
|
+const currentBundle = ref<CutBundle | null>(null);
|
|
|
+
|
|
|
+function getStatusColor(status: string) {
|
|
|
+ const colors: Record<string, string> = {
|
|
|
+ pending: 'default',
|
|
|
+ in_progress: 'blue',
|
|
|
+ completed: 'green'
|
|
|
+ };
|
|
|
+ return colors[status] || 'default';
|
|
|
+}
|
|
|
+
|
|
|
+function getStatusText(status: string) {
|
|
|
+ const texts: Record<string, string> = {
|
|
|
+ pending: '待开始',
|
|
|
+ in_progress: '进行中',
|
|
|
+ completed: '已完成'
|
|
|
+ };
|
|
|
+ return texts[status] || status;
|
|
|
+}
|
|
|
+
|
|
|
+async function loadBatches() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const response = await apiClient.get('/cut-batches', { params: { keyword: searchKeyword.value } });
|
|
|
+ batches.value = response.data.data.list || [];
|
|
|
+ } catch (error) {
|
|
|
+ message.error('加载批次列表失败');
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadWorkOrders() {
|
|
|
+ try {
|
|
|
+ const result = await workOrderApi.getWorkOrders({ page: 1, page_size: 100 });
|
|
|
+ workOrders.value = result.list;
|
|
|
+ } catch (error) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function showCreateModal() {
|
|
|
+ modalTitle.value = '创建批次';
|
|
|
+ isEdit.value = false;
|
|
|
+ modalVisible.value = true;
|
|
|
+ formRef.value?.resetFields();
|
|
|
+ formState.work_order_id = null;
|
|
|
+ formState.batch_no = '';
|
|
|
+ formState.color = '';
|
|
|
+ formState.total_qty = 1;
|
|
|
+}
|
|
|
+
|
|
|
+function showEditModal(record: CutBatch) {
|
|
|
+ modalTitle.value = '编辑批次';
|
|
|
+ isEdit.value = true;
|
|
|
+ editingId.value = record.id;
|
|
|
+ modalVisible.value = true;
|
|
|
+ formState.work_order_id = record.work_order_id;
|
|
|
+ formState.batch_no = record.batch_no;
|
|
|
+ formState.color = record.color;
|
|
|
+ formState.total_qty = record.total_qty;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubmit() {
|
|
|
+ try {
|
|
|
+ await formRef.value.validate();
|
|
|
+ submitting.value = true;
|
|
|
+ if (isEdit.value && editingId.value) {
|
|
|
+ await apiClient.put(`/cut-batches/${editingId.value}`, {
|
|
|
+ color: formState.color,
|
|
|
+ total_qty: formState.total_qty
|
|
|
+ });
|
|
|
+ message.success('批次已更新');
|
|
|
+ } else {
|
|
|
+ await apiClient.post('/cut-batches', {
|
|
|
+ work_order_id: formState.work_order_id,
|
|
|
+ batch_no: formState.batch_no,
|
|
|
+ color: formState.color,
|
|
|
+ total_qty: formState.total_qty
|
|
|
+ });
|
|
|
+ message.success('批次已创建');
|
|
|
+ }
|
|
|
+ modalVisible.value = false;
|
|
|
+ loadBatches();
|
|
|
+ } catch (error: any) {
|
|
|
+ message.error(error.response?.data?.msg || '操作失败');
|
|
|
+ } finally {
|
|
|
+ submitting.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteBatch(id: number) {
|
|
|
+ try {
|
|
|
+ await apiClient.delete(`/cut-batches/${id}`, { headers: { 'X-Confirm': 'true' } });
|
|
|
+ message.success('批次已删除');
|
|
|
+ loadBatches();
|
|
|
+ } catch (error) {
|
|
|
+ message.error('删除失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function showBundlesModal(record: CutBatch) {
|
|
|
+ currentBatch.value = record;
|
|
|
+ bundlesModalVisible.value = true;
|
|
|
+ try {
|
|
|
+ const response = await apiClient.get(`/cut-batches/${record.id}/bundles`);
|
|
|
+ bundles.value = response.data.data || [];
|
|
|
+ } catch (error) {
|
|
|
+ message.error('加载扎号列表失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function showAddBundleModal() {
|
|
|
+ addBundleModalVisible.value = true;
|
|
|
+ newBundle.size = '';
|
|
|
+ newBundle.quantity = 1;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleAddBundle() {
|
|
|
+ if (!currentBatch.value) return;
|
|
|
+ if (!newBundle.size) {
|
|
|
+ message.error('请输入尺码');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ addingBundle.value = true;
|
|
|
+ try {
|
|
|
+ await apiClient.post(`/cut-batches/${currentBatch.value.id}/bundles`, {
|
|
|
+ size: newBundle.size,
|
|
|
+ quantity: newBundle.quantity
|
|
|
+ });
|
|
|
+ message.success('扎号已添加');
|
|
|
+ addBundleModalVisible.value = false;
|
|
|
+ showBundlesModal(currentBatch.value);
|
|
|
+ } catch (error: any) {
|
|
|
+ message.error(error.response?.data?.msg || '添加失败');
|
|
|
+ } finally {
|
|
|
+ addingBundle.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteBundle(id: number) {
|
|
|
+ try {
|
|
|
+ await apiClient.delete(`/cut-batches/bundles/${id}`);
|
|
|
+ message.success('扎号已删除');
|
|
|
+ if (currentBatch.value) showBundlesModal(currentBatch.value);
|
|
|
+ } catch (error) {
|
|
|
+ message.error('删除失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function showBundleQRCode(bundle: CutBundle) {
|
|
|
+ currentBundle.value = bundle;
|
|
|
+ try {
|
|
|
+ const response = await apiClient.get(`/cut-batches/bundles/${bundle.id}/qrcode`, { responseType: 'blob' });
|
|
|
+ bundleQRUrl.value = URL.createObjectURL(response.data);
|
|
|
+ bundleQRVisible.value = true;
|
|
|
+ } catch (error) {
|
|
|
+ message.error('生成二维码失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function generateAllQRCodes() {
|
|
|
+ if (!currentBatch.value) return;
|
|
|
+ try {
|
|
|
+ const response = await apiClient.get(`/cut-batches/${currentBatch.value.id}/qrcodes`, { responseType: 'blob' });
|
|
|
+ const url = URL.createObjectURL(response.data);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = `batch-${currentBatch.value.batch_no}-qrcodes.zip`;
|
|
|
+ a.click();
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ message.success('二维码已下载');
|
|
|
+ } catch (error) {
|
|
|
+ message.error('批量生成二维码失败');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleLogout() {
|
|
|
+ authStore.logout();
|
|
|
+ message.success('已退出登录');
|
|
|
+ router.push('/login');
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadBatches();
|
|
|
+ loadWorkOrders();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="postcss">
|
|
|
+.cut-batches-page { padding: 0; }
|
|
|
+.mt-4 { margin-top: 16px; }
|
|
|
+.mb-4 { margin-bottom: 16px; }
|
|
|
+.qr-code-container { text-align: center; }
|
|
|
+.qr-code-tip { margin-top: 16px; color: #666; font-size: 14px; }
|
|
|
+</style>
|