Ver Fonte

入库&出库&下架

Alex há 11 meses atrás
pai
commit
e5d1b88d54

+ 10 - 0
src/api/recycleService/stockIn.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+
+// 获取入库详情
+export function getStockInDetail(params) {
+  return request({
+    url: '/baseinfo/godownstock/inputDetail',
+    method: 'get',
+    params
+  });
+} 

+ 78 - 94
src/components/CommonPage/SimpleFormModal.vue

@@ -1,56 +1,35 @@
 <!-- 编辑弹窗 -->
 <template>
-  <ele-modal
-    form
-    :width="width"
-    v-model="visible"
-    :title="title"
-    position="center"
-    :body-style="{ maxHeight: '84vh', position: 'relative', ...bodyStyle }"
-  >
-    <slot name="other"></slot>
+    <ele-modal form :width="width" v-model="visible" :title="title" position="center"
+        :body-style="{ maxHeight: '84vh', position: 'relative', ...bodyStyle }">
+        <slot name="other"></slot>
 
-    <SimpleForm
-      :items="items"
-      :labelWidth="labelWidth"
-      ref="formRef"
-      v-bind="formProps"
-      :disabled="type === 'detail'"
-    >
-      <template
-        v-for="(val, key) in slotArray"
-        v-slot:[key]="{ item, model, updateValue }"
-      >
-        <slot
-          :name="key"
-          :item="item"
-          :model="model"
-          :updateValue="updateValue"
-        ></slot>
-      </template>
-    </SimpleForm>
+        <SimpleForm :items="items" :labelWidth="labelWidth" ref="formRef" v-bind="formProps"
+            :disabled="type === 'detail'">
+            <template v-for="(val, key) in slotArray" v-slot:[key]="{ item, model, updateValue }">
+                <slot :name="key" :item="item" :model="model" :updateValue="updateValue"></slot>
+            </template>
+        </SimpleForm>
 
-    <template #footer>
-      <el-button @click="handleCancel">关闭</el-button>
-      <el-button type="primary" @click="handleSumbit" v-if="type !== 'detail'"
-        >确定</el-button
-      >
-    </template>
-  </ele-modal>
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+            <el-button type="primary" @click="handleSumbit" v-if="type !== 'detail'">确定</el-button>
+        </template>
+    </ele-modal>
 </template>
 
 <script setup>
-  import { ref, reactive, nextTick, getCurrentInstance, useSlots } from 'vue';
-  import { Flag, ChatDotSquare } from '@element-plus/icons-vue';
-  import orderTimeline from '@/views/recycleOrder/components/order-timeline.vue';
-  import SimpleForm from '@/components/CommonPage/SimpleForm.vue';
-  import validators from '@/utils/validators';
-  import { EleMessage } from 'ele-admin-plus/es';
-  let { proxy } = getCurrentInstance();
+import { ref, reactive, nextTick, getCurrentInstance, useSlots } from 'vue';
+import { Flag, ChatDotSquare } from '@element-plus/icons-vue';
+import orderTimeline from '@/views/recycleOrder/components/order-timeline.vue';
+import SimpleForm from '@/components/CommonPage/SimpleForm.vue';
+import validators from '@/utils/validators';
+import { EleMessage } from 'ele-admin-plus/es';
+let { proxy } = getCurrentInstance();
 
-  const slotArray = useSlots();
+const slotArray = useSlots();
 
-  const props = defineProps({
+const props = defineProps({
     items: { type: Array, default: () => [] },
     title: { type: String, default: '编辑' },
     type: { type: String },
@@ -62,75 +41,80 @@
     //是否需要合并打开时传入的参数
     isMerge: { type: Boolean, default: false },
     formatData: {
-      type: Function,
-      default: (data) => data
+        type: Function,
+        default: (data) => data
     }, //格式化提交数据
     fallbackData: { type: Function, default: (data) => data } //回填数据
-  });
-  const emit = defineEmits(['success']);
-  /** 弹窗是否打开 */
-  const visible = defineModel({ type: Boolean });
+});
+const emit = defineEmits(['success']);
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
 
-  /** 关闭弹窗 */
-  const handleCancel = () => {
+/** 关闭弹窗 */
+const handleCancel = () => {
     visible.value = false;
-  };
+};
 
-  const form = ref({});
-  const mergeData = ref({});
-  /** 弹窗打开事件 */
-  const handleOpen = async (data) => {
+const form = ref({});
+const mergeData = ref({});
+/** 弹窗打开事件 */
+const handleOpen = async (data) => {
     visible.value = true;
     nextTick(() => {
-      formRef.value?.resetForm();
-      mergeData.value = data;
-      if (!props.baseUrl.detail) {
-        form.value = data;
-        formRef.value?.setData(data);
-      } else {
-        getDetail(data.id).then((res) => {
-          form.value = res.data;
-          let data = props.fallbackData(res.data);
-          formRef.value?.setData(data);
-        });
-      }
+        formRef.value?.resetForm();
+        mergeData.value = data;
+        if (!props.baseUrl.detail) {
+            form.value = data;
+            formRef.value?.setData(data);
+        } else {
+            getDetail(data.id).then((res) => {
+                form.value = res.data;
+                let data = props.fallbackData(res.data);
+                formRef.value?.setData(data);
+            });
+        }
     });
-  };
+};
 
-  //获取详情数据
-  const getDetail = (id) => {
+//获取详情数据
+const getDetail = (id) => {
     if (!props.baseUrl.detail || !id) return;
     let url = `${props.baseUrl.detail}/${id}`;
     return new Promise((resolve, reject) => {
-      proxy.$http.get(url).then((res) => {
-        if (res.data.code !== 200) return EleMessage.error(res.data.msg);
-        resolve(res.data || {});
-      });
+        proxy.$http.get(url).then((res) => {
+            if (res.data.code !== 200) return EleMessage.error(res.data.msg);
+            resolve(res.data || {});
+        });
     });
-  };
+};
 
-  const formRef = ref();
-  const handleSumbit = () => {
+const formRef = ref();
+const handleSumbit = () => {
     console.log(props.formatData, '格式化数据');
 
     formRef.value?.submitForm().then((data) => {
-      console.log(data, '格式化数据data');
+        console.log(data, '格式化数据data');
 
-      data.id = form.value?.id;
-      let url = data.id ? props.baseUrl.update : props.baseUrl.add;
-      data = props.isMerge ? { ...mergeData.value, ...data } : data;
-      let format = props.formatData(data);
-      proxy.$http.post(url, format).then((res) => {
-        if (res.data.code !== 200) return EleMessage.error(res.data.msg);
-        visible.value = false;
-        emit('success', data);
-        EleMessage.success(format.id ? '编辑成功' : '操作成功');
-      });
+        data.id = form.value?.id;
+        let url = data.id ? props.baseUrl.update : props.baseUrl.add;
+        data = props.isMerge ? { ...mergeData.value, ...data } : data;
+        let format = props.formatData(data);
+        proxy.$http.post(url, format).then((res) => {
+            if (res.data.code !== 200) return EleMessage.error(res.data.msg);
+            visible.value = false;
+            emit('success', data);
+            EleMessage.success(format.id ? '编辑成功' : '操作成功');
+        });
     });
-  };
+};
+
+const setData = (data) => {
+    formRef.value?.setData(data);
+};
 
-  defineExpose({
+defineExpose({
     handleOpen,
-    handleSumbit
-  });
+    handleSumbit,
+    setData
+});
 </script>

+ 78 - 0
src/views/recycleService/stockIn/components/stock-in-detail.vue

@@ -0,0 +1,78 @@
+<template>
+    <el-dialog title="入库单详情" v-model="visible" width="1020px" destroy-on-close>
+        <div class="detail-content">
+            <el-descriptions :column="2" border label-width="100px">
+                <el-descriptions-item label="入库单号">{{ detailData.inputCode }}</el-descriptions-item>
+                <el-descriptions-item label="入库库位">{{ detailData.positionCode }}</el-descriptions-item>
+                <el-descriptions-item label="入库仓库">{{ detailData.godownName }}</el-descriptions-item>
+                <el-descriptions-item label="入库时间">{{ formatTime(detailData.inputTime) }}</el-descriptions-item>
+                <el-descriptions-item label="备注">{{ detailData.inputRemark }}</el-descriptions-item>
+                <el-descriptions-item label="操作员">{{ detailData.inputName }}</el-descriptions-item>
+            </el-descriptions>
+
+            <el-table :data="dataList" style="width: 100%;margin-top: 20px;" border>
+                <el-table-column type="index" label="序号" width="70" />
+                <el-table-column prop="orderId" label="订单编号" width="180" />
+                <el-table-column prop="waybillCode" label="物流单号" width="160" />
+                <el-table-column prop="orderNum" label="数量" />
+                <el-table-column prop="bookNum" label="不良数量" />
+                <el-table-column prop="checkTime" label="验货时间" width="180">
+                    <template #default="scope">
+                        {{ formatTime(scope.row.checkTime) }}
+                    </template>
+                </el-table-column>
+
+                <el-table-column prop="inputRemark" label="订单备注" width="180" />
+            </el-table>
+        </div>
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="visible = false">关闭</el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import dayjs from 'dayjs';
+import { getStockInDetail } from '@/api/recycleService/stockIn';
+
+const visible = ref(false);
+const detailData = ref({
+    inputCode: '',
+    positionCode: '',
+    orderId: '',
+    waybillCode: '',
+    orderNum: 0,
+    bookNum: 0,
+    checkTime: '',
+    inputRemark: ''
+});
+
+const formatTime = (time) => {
+    return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-';
+};
+
+const dataList = ref([])
+const open = async (row) => {
+    visible.value = true;
+    detailData.value = row
+    try {
+        const res = await getStockInDetail({ inputCode: row.inputCode });
+        dataList.value = res.data
+    } catch (error) {
+        console.error('获取入库详情失败:', error);
+    }
+};
+
+defineExpose({
+    open
+});
+</script>
+
+<style scoped>
+.detail-content {
+    padding: 20px;
+}
+</style>

+ 104 - 0
src/views/recycleService/stockIn/components/stock-in-search.vue

@@ -0,0 +1,104 @@
+<!-- 搜索表单 -->
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch :items="formItems" ref="searchRef" @search="search" :initKeys="initKeys"></ProSearch>
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive, ref, computed, getCurrentInstance } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+import request from '@/utils/request';
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(['search']);
+
+// 仓库列表
+const godownList = ref([]);
+function getStoreList(name = '') {
+    return proxy.$http.post(`/baseinfo/godown/searchGodown?name=${name}`);
+}
+getStoreList().then((res) => {
+    godownList.value = res.data.data;
+});
+
+// 用户列表
+const userList = ref([]);
+function getUserList(query = '') {
+    return proxy.$http.get(`/system/user/list?username=${query}`);
+}
+
+const formItems = computed(() => {
+    return [
+        {
+            type: 'select',
+            label: '仓库名称',
+            prop: 'godownId',
+            options: godownList.value.map((item) => ({
+                label: item.name,
+                value: item.id
+            }))
+        },
+        { type: 'input', label: '入库库位', prop: 'positionCode' },
+        { type: 'input', label: '物流单号', prop: 'waybillCode' },
+        { type: 'input', label: '订单编号', prop: 'orderId' },
+        { type: 'input', label: '入库单号', prop: 'inputCode' },
+        {
+            type: 'select',
+            label: '操作员',
+            prop: 'userId',
+            options: userList.value.map((item) => ({
+                label: item.nickName,
+                value: item.userId
+            })),
+            props: {
+                filterable: true,
+                remote: true,
+                reserveKeyword: true,
+                remoteMethod: (query) => {
+                    getUserList(query).then((res) => {
+                        userList.value = res.data.rows;
+                    });
+                }
+            }
+        },
+        { type: 'input', label: '入库备注', prop: 'inputRemark' },
+        {
+            type: 'datetimerange',
+            label: '入库时间',
+            prop: 'dateRange',
+            startProp: 'startTime',
+            endProp: 'endTime',
+            colProps: { span: 6 },
+            props: {
+                format: 'YYYY-MM-DD HH:mm:ss',
+                valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                rangeSeparator: '至',
+                onChange: (val) => {
+                    searchRef.value?.setData({ startTime: val?.[0] || '', endTime: val?.[1] || '' });
+                }
+            }
+        }
+    ];
+});
+
+const initKeys = reactive({
+    godownId: '',
+    positionCode: '',
+    waybillCode: '',
+    orderId: '',
+    inputCode: '',
+    userId: '',
+    inputRemark: '',
+    startTime: '',
+    endTime: '',
+    dateRange: []
+});
+
+const searchRef = ref(null);
+
+/** 搜索 */
+const search = (data) => {
+    emit('search', { ...data });
+};
+</script> 

+ 79 - 4
src/views/recycleService/stockIn/index.vue

@@ -1,5 +1,80 @@
 <template>
-    <div>
-        入库
-    </div>
-</template>
+    <ele-page flex-table>
+        <stock-in-search @search="reload"></stock-in-search>
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <template #toolbar>
+                <el-button type="success" plain v-permission="'recycleService:stockIn:export'"
+                    @click="handleExportExcel" :icon="DownloadOutlined">
+                    导出
+                </el-button>
+            </template>
+
+            <template #action="{ row }">
+                <el-button type="primary" link @click="handleDetail(row)" v-permission="'recycleService:stockIn:detail'">
+                    详情
+                </el-button>
+            </template>
+        </common-table>
+
+        <stock-in-detail ref="detailRef" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { DownloadOutlined } from '@/components/icons';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import stockInSearch from './components/stock-in-search.vue';
+import stockInDetail from './components/stock-in-detail.vue';
+import dayjs from 'dayjs';
+
+defineOptions({ name: 'stockInList' });
+
+/** 表格列配置 */
+const columns = ref([
+    { type: 'selection', columnKey: 'selection', width: 50, align: 'center', fixed: 'left' },
+    { label: '入库单号', prop: 'inputCode', align: 'center', minWidth: 174 },
+    { label: '入库时间', prop: 'inputTime', align: 'center', formatter: (row) => dayjs(row.inputTime).format('YYYY-MM-DD HH:mm:ss'), width: 160 },
+    { label: '仓库', prop: 'godownName', align: 'center', width: 120 },
+    { label: '入库库位', prop: 'positionCode', align: 'center', minWidth: 120 },
+    { label: '订单数量', prop: 'orderNum', align: 'center', width: 100 },
+    { label: '反馈', prop: 'inputRemark', align: 'center', minWidth: 200 },
+    { label: '操作员', prop: 'inputName', align: 'center', width: 120 },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 120,
+      align: 'center',
+      slot: 'action'
+    }
+]);
+
+/** 页面组件实例 */
+const pageRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/baseinfo/godownstock/inputList',
+    exportUrl: '/baseinfo/godownstock/exportInputList',
+    fileName: '入库管理',
+    cacheKey: 'stockInTable'
+});
+
+/** 详情组件实例 */
+const detailRef = ref(null);
+
+//刷新表格
+function reload(where) {
+    pageRef.value?.reload(where);
+}
+
+//导出excel
+function handleExportExcel() {
+    pageRef.value?.exportData('入库管理');
+}
+
+// 查看详情
+function handleDetail(row) {
+    detailRef.value?.open(row);
+}
+</script>

+ 197 - 0
src/views/recycleService/stockOff/components/stock-off-add.vue

@@ -0,0 +1,197 @@
+<template>
+    <simple-form-modal title="新增下架任务" labelWidth="120px" :items="formItems" ref="editRef" :baseUrl="baseUrl"
+        @success="(data) => emit('success', data)" :formatData="formatData">
+        <template #maxOrderNum>
+            时拆分任务
+        </template>
+        <template #customSelect="{ item, model, updateValue }">
+            <div class="flex items-center gap-2 w-full">
+                <el-select class="w-[100px] flex-1" v-model="model.timeType" :placeholder="item.label" @change="updateValue">
+                    <el-option v-for="option in item.options" :key="option.value" :label="option.label"
+                        :value="option.value" />
+                </el-select>
+                <el-radio-group v-model="formData.timeRangeType" @change="handleTimeRangeChange">
+                    <el-radio label="今天" :value="1"></el-radio>
+                    <el-radio label="近7天" :value="2"></el-radio>
+                    <el-radio label="近30天" :value="3"></el-radio>
+                </el-radio-group>
+            </div>
+
+
+        </template>
+    </simple-form-modal>
+</template>
+
+<script setup>
+import { reactive, ref, defineEmits, getCurrentInstance, watch } from 'vue';
+import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
+import dayjs from 'dayjs';
+const { proxy } = getCurrentInstance();
+
+//获取仓库
+const godownList = ref([]);
+const emit = defineEmits(['success']);
+
+const formItems = computed(() => {
+    return [
+        {
+            type: 'select',
+            label: '仓库名称',
+            prop: 'godownId',
+            required: true,
+            options: godownList.value.map((d) => {
+                return { label: d.godownName, value: d.id };
+            }),
+            props: {
+                placeholder: '请选择或输入搜索',
+                filterable: true
+            }
+        },
+        {
+            type: 'select',
+            label: '作业人员',
+            prop: 'taskUser',
+            options: [],
+            props: {
+                placeholder: '请输入搜索',
+                filterable: true,
+                remote: true,
+                multiple: true,
+                remoteMethod: (query) => {
+                    proxy.$http.get('/system/user/list', {
+                        params: { nickName: query || ' ' }
+                    }).then(res => {
+                        const users = res.data.rows || [];
+                        formItems.value[1].options = users.map(user => ({
+                            label: user.nickName,
+                            value: user.userId
+                        }));
+                    });
+                },
+                reserveKeyword: true
+            }
+        },
+        {
+            type: 'input',
+            label: '任务明细大于',
+            prop: 'maxOrderNum',
+            required: true,
+            slots: {
+                append: 'maxOrderNum'
+            },
+            props: {
+                min: 1,
+                placeholder: '请输入',
+            }
+        },
+        {
+            type: 'radio',
+            label: '是否按人员拆分',
+            prop: 'splitByUser',
+            required: true,
+            options: [
+                { label: '是', value: 1 },
+                { label: '否', value: 0 }
+            ]
+        },
+        {
+            type: 'customSelect',
+            label: '时间类型',
+            prop: 'timeType',
+            required: true,
+            options: [
+                { label: '验货时间', value: 1 },
+                { label: '入库时间', value: 2 },
+                { label: '打款时间', value: 3 }
+            ]
+        },
+        {
+            type: 'daterange',
+            label: '时间范围',
+            prop: 'timeRange',
+            required: true,
+            props: {
+                valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                format: 'YYYY-MM-DD HH:mm:ss',
+                rangeSeparator: '至',
+                onChange: (value) => {
+                    formData.value.startTime = value?.[0] || '';
+                    formData.value.endTime = value?.[1] || '';
+                }
+            },
+        }
+    ];
+});
+
+//默认值
+const baseUrl = reactive({
+    add: '/baseinfo/stocktask/add'
+});
+const formData = ref({
+    splitByUser: 1,
+    timeType: 1,
+    taskUser: [],
+    timeRangeType: 1,
+    timeRange: []
+});
+
+function handleTimeRangeChange(value) {
+    console.log(value);
+    const now = dayjs();
+    const end = now.endOf('day').format('YYYY-MM-DD HH:mm:ss');
+
+    let start;
+    switch (value) {
+        case 1: // 今天
+            start = now.startOf('day').format('YYYY-MM-DD HH:mm:ss');
+            break;
+        case 2: // 近7天
+            start = now.subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
+            break;
+        case 3: // 近30天
+            start = now.subtract(29, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
+            break;
+    }
+
+    formData.value.startTime = start;
+    formData.value.endTime = end;
+    formData.value.timeRange = [start, end];
+    editRef.value?.setData(formData.value);
+}
+
+const editRef = ref(null);
+
+function handleOpen() {
+    getStoreList().then((res) => {
+        godownList.value = res.data.data;
+    });
+    // 加载初始用户数据
+    getUserList();
+    handleTimeRangeChange(1)
+    editRef.value?.handleOpen(formData.value);
+}
+
+function getStoreList(name = '') {
+    return proxy.$http.post(`/baseinfo/godown/searchGodown?name=${name}`);
+}
+
+const formatData = (data) => {
+    let dataCopy = JSON.parse(JSON.stringify(data));
+    dataCopy.startTime = data.timeRange[0];
+    dataCopy.endTime = data.timeRange[1];
+    delete dataCopy.timeRange;
+    return dataCopy;
+}
+
+function getUserList() {
+    proxy.$http.get('/system/user/list').then(res => {
+        const users = res.data.rows || [];
+        formItems.value[1].options = users.map(user => ({
+            label: user.nickName,
+            value: user.userId
+        }));
+    });
+}
+
+defineExpose({ handleOpen });
+</script>

+ 94 - 0
src/views/recycleService/stockOff/components/stock-off-detail.vue

@@ -0,0 +1,94 @@
+<template>
+    <el-dialog v-model="visible" title="任务详情" width="80%" :close-on-click-modal="false" destroy-on-close>
+        <el-tabs v-model="activeTab">
+            <el-tab-pane label="任务详情" name="detail">
+                <common-table :pageConfig="detailConfig" :columns="detailColumns" :tool-bar="false">
+                    <template #taskStatus="{ row }">
+                        <dict-data code="task_status" type="tag" :model-value="row.taskStatus" />
+                    </template>
+
+                    <template #action="{ row }">
+                        <div>
+                            <el-button type="danger" link v-permission="'recycleService:stockOff:close'"
+                                @click="handleOperate(row, 3)" v-if="row.taskStatus == 0">
+                                关闭
+                            </el-button>
+                        </div>
+                    </template>
+                </common-table>
+            </el-tab-pane>
+            <el-tab-pane label="操作记录" name="log">
+                <common-table :pageConfig="logConfig" :columns="logColumns" :tool-bar="false">
+                </common-table>
+            </el-tab-pane>
+        </el-tabs>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import { useDictData } from '@/utils/use-dict-data';
+
+const visible = ref(false);
+const activeTab = ref('detail');
+const currentTask = ref(null);
+
+const detailColumns = ref([
+    { label: '操作', prop: 'operation', align: 'center', slot: 'action', width: 100 },
+    {
+        label: '作业状态', prop: 'taskStatus', align: 'center', width: 120, formatter: (row) =>
+            userDicts.value.find((d) => d.dictValue == row.taskStatus)
+                ?.dictLabel
+    },
+    { label: '仓库', prop: 'godownName', align: 'center', width: 100 },
+    { label: '下架库位', prop: 'positionCode', align: 'center' },
+    { label: '订单编号', prop: 'orderId', align: 'center' },
+    { label: '物流单号', prop: 'waybillCode', align: 'center' },
+    { label: '不良本数', prop: 'badNum', align: 'center', width: 100 },
+    { label: '计划数量', prop: 'planNum', align: 'center', width: 100 },
+    { label: '已下数量', prop: 'finishNum', align: 'center', width: 100 }
+]);
+
+const logColumns = ref([
+    { label: '操作时间', prop: 'createTime', align: 'center', width: 160 },
+    { label: '操作人', prop: 'createName', align: 'center', width: 100 },
+    { label: '操作内容', prop: 'content', align: 'center' }
+]);
+
+const detailConfig = reactive({
+    pageUrl: '/baseinfo/stocktask/stockDetail',
+    params: computed(() => ({
+        taskCode: currentTask.value?.taskCode
+    }))
+});
+
+const logConfig = reactive({
+    pageUrl: '/baseinfo/stocktask/taskLog',
+    params: computed(() => ({
+        taskCode: currentTask.value?.taskCode
+    }))
+});
+
+const userDicts = ref([]);
+function handleOpen(row) {
+    currentTask.value = row;
+    visible.value = true;
+    const [UseStatusDicts] = useDictData(['task_status']);
+    userDicts.value = UseStatusDicts.value;
+}
+
+//关闭
+function handleOperate(row, type) {
+    let data = {
+        taskCode: row.taskCode,
+        operateType: type
+    }
+    pageRef.value?.messageBoxConfirm({
+        message:'当前作业单据状态将变更为已关闭,是否继续?',
+        fetch: () => request.post('/baseinfo/stocktask/operate', data)
+    });
+}
+
+defineExpose({ handleOpen });
+</script>

+ 135 - 0
src/views/recycleService/stockOff/components/stock-off-search.vue

@@ -0,0 +1,135 @@
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <div class="flex items-start">
+            <div class="flex flex-col gap-[12px]">
+                <div class="flex items-center gap-2 w-full">
+                    <el-select class="w-[120px] flex-1" v-model="initKeys.timeType" @change="handleTimeTypeChange">
+                        <el-option label="创建时间" :value="1"></el-option>
+                        <el-option label="完成时间" :value="2"></el-option>
+                    </el-select>
+                    <el-radio-group v-model="initKeys.timeRangeType" @change="handleTimeRangeTypeChange">
+                        <el-radio label="今天" :value="1"></el-radio>
+                        <el-radio label="近7天" :value="2"></el-radio>
+                        <el-radio label="近30天" :value="3"></el-radio>
+                    </el-radio-group>
+                </div>
+                <el-date-picker format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" v-model="timeRange" type="datetimerange" range-separator="至" start-placeholder="开始日期"
+                    @change="handleTimeRangeChange" end-placeholder="结束日期" />
+            </div>
+
+            <ProSearch class="flex-1 ml-8" :items="formItems" ref="searchRef" @search="search" :initKeys="initKeys">
+            </ProSearch>
+        </div>
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive, ref, defineEmits, getCurrentInstance, computed, onMounted } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+import dayjs from 'dayjs';
+
+let { proxy } = getCurrentInstance();
+const emit = defineEmits(['search']);
+const godownList = ref([]);
+
+const searchRef = ref(null);
+const initKeys = reactive({
+    godownId: '',
+    positionCode: '',
+    waybillCode: '',
+    orderId: '',
+    taskCode: '',
+    operator: '',
+    remark: '',
+    taskStatus: '',
+    startTime: '',
+    endTime: '',
+    timeType: 1,
+    timeRangeType: 1
+});
+
+/** 搜索 */
+const search = (data) => {
+    emit('search', { ...data });
+};
+
+const timeRange = ref([]);
+const handleTimeRangeChange = (val) => {
+    timeRange.value = val;
+    searchRef.value?.setData({
+        startTime: val?.[0] || '',
+        endTime: val?.[1] || ''
+    });
+}
+const handleTimeTypeChange = (val) => {
+    initKeys.timeType = val;
+    searchRef.value?.setData({
+        timeType: val
+    });
+}
+
+const handleTimeRangeTypeChange = (val) => {
+    initKeys.timeRangeType = val;
+    const now = dayjs();
+    const end = now.endOf('day').format('YYYY-MM-DD HH:mm:ss');
+
+    let start;
+    switch (val) {
+        case 1: // 今天
+            start = now.startOf('day').format('YYYY-MM-DD HH:mm:ss');
+            break;
+        case 2: // 近7天
+            start = now.subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
+            break;
+        case 3: // 近30天
+            start = now.subtract(29, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
+            break;
+    }
+
+    initKeys.startTime = start;
+    initKeys.endTime = end;
+    timeRange.value = [start, end];
+    searchRef.value?.setData(initKeys);
+
+    search(initKeys);
+}
+onMounted(() => {
+    handleTimeRangeTypeChange(1)
+})
+
+const formItems = computed(() => {
+    return [
+        {
+            type: 'select',
+            label: '仓库名称',
+            prop: 'godownId',
+            options: godownList.value.map((d) => {
+                return { label: d.godownName, value: d.id };
+            }),
+            props: {
+                placeholder: '请选择或输入搜索',
+                filterable: true
+            },
+            colProps: {
+                span: 6
+            }
+        },
+        { type: 'input', label: '下架库位', prop: 'positionCode', colProps: { span: 6 } },
+        { type: 'input', label: '物流单号', prop: 'waybillCode', colProps: { span: 6 } },
+        { type: 'input', label: '订单编号', prop: 'orderId', colProps: { span: 6 } },
+        { type: 'input', label: '作业编号', prop: 'taskCode', colProps: { span: 6 } },
+        { type: 'input', label: '操作人', prop: 'operator', colProps: { span: 6 } },
+        { type: 'input', label: '备注', prop: 'remark', colProps: { span: 6 } },
+        { type: 'dictSelect', label: '作业状态', prop: 'taskStatus', props: { code: 'task_status' }, colProps: { span: 6 } },
+    ];
+});
+
+function getStoreList(name = '') {
+    return proxy.$http.post(`/baseinfo/godown/searchGodown?name=${name}`);
+}
+getStoreList().then((res) => {
+    godownList.value = res.data.data;
+});
+
+
+</script>

+ 149 - 4
src/views/recycleService/stockOff/index.vue

@@ -1,5 +1,150 @@
 <template>
-    <div>
-        下架
-    </div>
-</template>
+    <ele-page flex-table>
+        <stock-off-search @search="reload"></stock-off-search>
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <template #toolbar>
+                <el-button type="primary" plain :icon="PlusOutlined" v-permission="'recycleService:stockOff:add'"
+                    @click="handleAdd()">
+                    新增下架
+                </el-button>
+                <el-button type="danger" plain v-permission="'recycleService:stockOff:batchClose'"
+                    @click="handleBatchClose()">
+                    关闭作业
+                </el-button>
+                <el-button type="warning" plain :icon="DownloadOutlined" v-permission="'recycleService:stockOff:export'"
+                    @click="handleExportExcel">
+                    导出
+                </el-button>
+            </template>
+
+            <template #taskStatus="{ row }">
+                <dict-data code="task_status" type="tag" :model-value="row.taskStatus" />
+            </template>
+
+            <template #action="{ row }">
+                <div>
+                    <el-button type="primary" link v-permission="'recycleService:stockOff:detail'"
+                        @click="handleDetail(row)">
+                        详情
+                    </el-button>
+                    <el-button type="primary" link v-permission="'recycleService:stockOff:offline'"
+                        @click="handleOperate(row, 1)" v-if="row.taskStatus == 1">
+                        确认下架
+                    </el-button>
+                    <el-button type="primary" link v-permission="'recycleService:stockOff:end'"
+                        @click="handleOperate(row, 2)" v-if="row.taskStatus == 1">
+                        结束
+                    </el-button>
+                    <el-button type="danger" link v-permission="'recycleService:stockOff:close'"
+                        @click="handleOperate(row, 3)" v-if="row.taskStatus == 0">
+                        关闭
+                    </el-button>
+                </div>
+            </template>
+        </common-table>
+
+        <stock-off-add ref="addRef" @success="reload()"></stock-off-add>
+        <stock-off-detail ref="detailRef"></stock-off-detail>
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessageBox } from 'element-plus/es';
+import { EleMessage } from 'ele-admin-plus/es';
+import { DownloadOutlined, PlusOutlined } from '@/components/icons';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import StockOffSearch from './components/stock-off-search.vue';
+import StockOffAdd from './components/stock-off-add.vue';
+import StockOffDetail from './components/stock-off-detail.vue';
+import request from '@/utils/request';
+
+defineOptions({ name: 'stockOffList' });
+
+/** 表格列配置 */
+const columns = ref([
+    {
+        type: 'selection',
+        columnKey: 'selection',
+        width: 50,
+        align: 'center',
+        fixed: 'left'
+    },
+    { label: '操作', prop: 'operation', align: 'center', slot: 'action', width: 200 },
+    { label: '作业状态', prop: 'taskStatus', slot: 'taskStatus', align: 'center', width: 100 },
+    { label: '仓库名称', prop: 'godownName', align: 'center' },
+    { label: '作业编号', prop: 'taskCode', align: 'center', width: 160 },
+    { label: '创建时间', prop: 'createTime', align: 'center', width: 160 },
+    { label: '订单总数', prop: 'orderNum', align: 'center', width: 100 },
+    { label: '完成时间', prop: 'finishTime', align: 'center', width: 160 },
+    { label: '操作员', prop: 'taskUserName', align: 'center' },
+    { label: '备注', prop: 'remark', align: 'center' }
+]);
+
+/** 页面组件实例 */
+const pageRef = ref(null);
+const addRef = ref(null);
+const detailRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/baseinfo/stocktask/pagelist',
+    exportUrl: '/baseinfo/stocktask/export',
+    fileName: '下架管理',
+    cacheKey: 'stockOffTable'
+});
+
+//导出excel
+function handleExportExcel() {
+    pageRef.value?.exportData('下架列表');
+}
+
+//刷新表格
+function reload(where) {
+    pageRef.value?.reload(where);
+}
+
+//新增
+function handleAdd() {
+    addRef.value?.handleOpen();
+}
+
+//详情
+function handleDetail(row) {
+    detailRef.value?.handleOpen(row);
+}
+
+//操作
+function handleOperate(row, type) {
+    let message = {
+        1: '作业单据状态将变更为已完成,未"下架"数量将完成,是否继续?',
+        2: '作业单据状态将变更为已完成,未"下架"数量将忽略,是否继续?',
+        3: '作业单据状态将变更为已关闭,是否继续?'
+    }[type]
+    let title = `结束单据:${row.taskCode}`
+
+    ElMessageBox.confirm(message, title, {
+        type: 'warning'
+    }).then(() => {
+        request.post('/baseinfo/stocktask/operate', {
+            taskCode: row.taskCode,
+            operateType: type
+        }).then(() => {
+            EleMessage.success(`操作成功`);
+            reload();
+        });
+    });
+}
+
+//批量关闭 
+function handleBatchClose() {
+    let selections = pageRef.value?.getSelections();
+    let ids = selections.map((item) => item.id);
+    pageRef.value?.operatBatch({
+        title: '当前选择的 作业单据状态将变更为已关闭,是否继续?',
+        method: 'post',
+        url: '/baseinfo/stocktask/closeBatch',
+        data: { idList: ids }
+    });
+}
+</script>

+ 103 - 0
src/views/recycleService/stockOut/components/stock-out-search.vue

@@ -0,0 +1,103 @@
+<!-- 搜索表单 -->
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch :items="formItems" ref="searchRef" @search="search" :initKeys="initKeys"></ProSearch>
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive, ref, computed, getCurrentInstance } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+import request from '@/utils/request';
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(['search']);
+
+// 仓库列表
+const godownList = ref([]);
+function getStoreList(name = '') {
+    return proxy.$http.post(`/baseinfo/godown/searchGodown?name=${name}`);
+}
+getStoreList().then((res) => {
+    godownList.value = res.data.data;
+});
+// 用户列表
+const userList = ref([]);
+function getUserList(query = '') {
+    return proxy.$http.get(`/system/user/list?username=${query}`);
+}
+
+const formItems = computed(() => {
+    return [
+        {
+            type: 'select',
+            label: '仓库名称',
+            prop: 'godownId',
+            options: godownList.value.map((item) => ({
+                label: item.name,
+                value: item.id
+            }))
+        },
+        { type: 'input', label: '出库库位', prop: 'positionCode' },
+        { type: 'input', label: '物流单号', prop: 'waybillCode' },
+        { type: 'input', label: '订单编号', prop: 'orderId' },
+        { type: 'input', label: '出库单号', prop: 'stockCode' },
+        {
+            type: 'select',
+            label: '操作员',
+            prop: 'userId',
+            options: userList.value.map((item) => ({
+                label: item.nickName,
+                value: item.userId
+            })),
+            props: {
+                filterable: true,
+                remote: true,
+                reserveKeyword: true,
+                remoteMethod: (query) => {
+                    getUserList(query).then((res) => {
+                        userList.value = res.data.rows;
+                    });
+                }
+            }
+        },
+        { type: 'input', label: '反馈', prop: 'remark' },
+        {
+            type: 'datetimerange',
+            label: '出库时间',
+            prop: 'dateRange',
+            startProp: 'startTime',
+            endProp: 'endTime',
+            colProps: { span: 6 },
+            props: {
+                format: 'YYYY-MM-DD HH:mm:ss',
+                valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                rangeSeparator: '至',
+                onChange: (val) => {
+                    searchRef.value?.setData({ startTime: val?.[0] || '', endTime: val?.[1] || '' });
+                }
+            }
+        }
+    ];
+});
+
+const initKeys = reactive({
+    godownId: '',
+    positionCode: '',
+    waybillCode: '',
+    orderId: '',
+    stockCode: '',
+    userId: '',
+    remark: '',
+    startTime: '',
+    endTime: '',
+    dateRange: []
+});
+
+const searchRef = ref(null);
+
+/** 搜索 */
+const search = (data) => {
+    emit('search', { ...data });
+};
+</script>

+ 60 - 4
src/views/recycleService/stockOut/index.vue

@@ -1,5 +1,61 @@
 <template>
-    <div>
-        出库
-    </div>
-</template>
+    <ele-page flex-table>
+        <stock-out-search @search="reload"></stock-out-search>
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <template #toolbar>
+                <el-button type="success" plain v-permission="'recycleService:stockOut:export'"
+                    @click="handleExportExcel" :icon="DownloadOutlined">
+                    导出
+                </el-button>
+            </template>
+        </common-table>
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { DownloadOutlined } from '@/components/icons';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import stockOutSearch from './components/stock-out-search.vue';
+import dayjs from 'dayjs';
+
+defineOptions({ name: 'stockOutList' });
+
+/** 表格列配置 */
+const columns = ref([
+    { type: 'selection', columnKey: 'selection', width: 50, align: 'center', fixed: 'left' },
+    { label: '出库单号', prop: 'outputCode', align: 'center', minWidth: 174 },
+    { label: '出库时间', prop: 'outputTime', align: 'center', formatter: (row) => dayjs(row.outputTime).format('YYYY-MM-DD HH:mm:ss'), width: 160 },
+    { label: '仓库', prop: 'godownName', align: 'center', width: 120 },
+    { label: '出库库位', prop: 'positionCode', align: 'center', minWidth: 120 },
+    { label: '订单编号', prop: 'orderId', align: 'center', width: 176 },
+    { label: '物流单号', prop: 'waybillCode', align: 'center', width: 146 },
+    { label: '退回物流', prop: 'returnLogistics', align: 'center', width: 120 },
+    { label: '不良数量', prop: 'bookNum', align: 'center', width: 100 },
+    { label: '数量', prop: 'num', align: 'center' },
+    { label: '验收时间', prop: 'checkTime', align: 'center', formatter: (row) => dayjs(row.checkTime).format('YYYY-MM-DD HH:mm:ss'), width: 160 },
+    { label: '反馈', prop: 'outputRemark', align: 'center', minWidth: 200 },
+    { label: '操作员', prop: 'outputName', align: 'center', width: 120 }
+]);
+
+/** 页面组件实例 */
+const pageRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/baseinfo/godownstock/outputList',
+    exportUrl: '/baseinfo/godownstock/exportOutputList',
+    fileName: '出库管理',
+    cacheKey: 'stockOutTable'
+});
+
+//刷新表格
+function reload(where) {
+    pageRef.value?.reload(where);
+}
+
+//导出excel
+function handleExportExcel() {
+    pageRef.value?.exportData('出库管理');
+}
+</script>