gaoluyang
2026-05-09 d4bc592b5d0c42bc69abd5bd6334c191205dc244
浪潮
1.日志前端页面开发与联调
2.仓库盘点页面开发与联调
已添加4个文件
已修改1个文件
1496 ■■■■■ 文件已修改
src/api/inventoryManagement/stockCheck.js 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockProfitLoss.js 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockCheckPlan/index.vue 720 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockProfitLoss/index.vue 565 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockCheck.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢ç›˜ç‚¹è®¡åˆ’列表
export const getStockCheckPlanPage = (params) => {
  return request({
    url: '/stockInventoryCheckPlan/listPage',
    method: 'get',
    params,
  })
}
// æ–°å¢žç›˜ç‚¹è®¡åˆ’
export const addStockCheckPlan = (data) => {
  return request({
    url: '/stockInventoryCheckPlan/add',
    method: 'post',
    data,
  })
}
// ä¿®æ”¹ç›˜ç‚¹è®¡åˆ’
export const updateStockCheckPlan = (data) => {
  return request({
    url: '/stockInventoryCheckPlan/update',
    method: 'put',
    data,
  })
}
// åˆ é™¤ç›˜ç‚¹è®¡åˆ’
export const deleteStockCheckPlan = (ids) => {
  return request({
    url: '/stockInventoryCheckPlan/delete',
    method: 'delete',
    data: ids,
  })
}
// æäº¤å®¡æ‰¹
export const submitApproval = (id) => {
  return request({
    url: '/stockCheckPlan/submitApproval/' + id,
    method: 'post',
  })
}
// å®¡æ‰¹é€šè¿‡
export const approvePlan = (id) => {
  return request({
    url: '/stockCheckPlan/approve/' + id,
    method: 'post',
  })
}
// å®¡æ‰¹æ‹’绝
export const rejectPlan = (id, reason) => {
  return request({
    url: '/stockCheckPlan/reject/' + id,
    method: 'post',
    data: { reason },
  })
}
// å¼€å§‹ç›˜ç‚¹
export const startCheck = (id) => {
  return request({
    url: '/stockInventoryCheckPlan/start/' + id,
    method: 'post',
  })
}
// ç»“束盘点
export const completeCheck = (data) => {
  return request({
    url: '/stockInventoryCheckPlan/end',
    method: 'post',
    data: data
  })
}
// èŽ·å–ç›˜ç‚¹å•†å“åˆ—è¡¨
export const getCheckItems = (planId) => {
  return request({
    url: '/stockInventoryCheckPlan/detail/' + planId,
    method: 'get',
  })
}
// ä¿å­˜ç›˜ç‚¹æ•°æ®
export const saveCheckData = (data) => {
  return request({
    url: '/stockInventoryCheckPlanItem/batchUpdate',
    method: 'post',
    data,
  })
}
// èŽ·å–å·®å¼‚æ±‡æ€»
export const getDiffSummary = (planId) => {
  return request({
    url: '/stockCheckPlan/diffSummary/' + planId,
    method: 'get',
  })
}
// ç”Ÿæˆç›ˆäºå¤„理单
export const generateProfitLoss = (planId) => {
  return request({
    url: '/stockCheckPlan/generateProfitLoss/' + planId,
    method: 'post',
  })
}
src/api/inventoryManagement/stockProfitLoss.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢ç›ˆäºè®°å½•列表
export const getStockProfitLossPage = (params) => {
  return request({
    url: '/stockProfitLoss/page',
    method: 'get',
    params,
  })
}
// æŸ¥è¯¢ç›ˆäºè®°å½•详情
export const getStockProfitLossDetail = (id) => {
  return request({
    url: '/stockProfitLoss/' + id,
    method: 'get',
  })
}
// æ–°å¢žç›ˆäºè®°å½•(手动创建)
export const addStockProfitLoss = (data) => {
  return request({
    url: '/stockProfitLoss/add',
    method: 'post',
    data,
  })
}
// ä¿®æ”¹ç›ˆäºè®°å½•
export const updateStockProfitLoss = (data) => {
  return request({
    url: '/stockProfitLoss/update',
    method: 'put',
    data,
  })
}
// åˆ é™¤ç›ˆäºè®°å½•
export const deleteStockProfitLoss = (id) => {
  return request({
    url: '/stockProfitLoss/delete/' + id,
    method: 'delete',
  })
}
// æäº¤å®¡æ‰¹
export const submitApproval = (id) => {
  return request({
    url: '/stockProfitLoss/submitApproval/' + id,
    method: 'post',
  })
}
// å®¡æ‰¹é€šè¿‡
export const approveProfitLoss = (id) => {
  return request({
    url: '/stockProfitLoss/approve/' + id,
    method: 'post',
  })
}
// å®¡æ‰¹æ‹’绝
export const rejectProfitLoss = (id, reason) => {
  return request({
    url: '/stockProfitLoss/reject/' + id,
    method: 'post',
    data: { reason },
  })
}
// æ‰§è¡Œç›ˆäºå¤„理(更新库存)
export const executeProfitLoss = (id) => {
  return request({
    url: '/stockProfitLoss/execute/' + id,
    method: 'post',
  })
}
// æ ¹æ®ç›˜ç‚¹è®¡åˆ’ID查询差异商品(用于生成盈亏单)
export const getDiffItemsByPlanId = (planId) => {
  return request({
    url: '/stockProfitLoss/diffItems/' + planId,
    method: 'get',
  })
}
// å¯¼å‡ºç›ˆäºè®°å½•
export const exportStockProfitLoss = (params) => {
  return request({
    url: '/stockProfitLoss/export',
    method: 'get',
    params,
    responseType: 'blob',
  })
}
src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -397,8 +397,8 @@
  size: 1000,
  total: 0,
  });
  if(res.records){
    customerNameOptions.value = res.records.map(item => ({
  if(res.data.records){
    customerNameOptions.value = res.data.records.map(item => ({
      label: item.customerName,
      value: item.customerName,
      id: item.id
src/views/inventoryManagement/stockCheckPlan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,720 @@
<template>
  <div class="app-container">
    <div class="search_form" style="margin-bottom: 20px;">
      <div>
        <span class="search_title">盘点单号:</span>
        <el-input v-model="searchForm.planNo" placeholder="请输入" clearable style="width: 240px; margin-right: 10px" @change="handleQuery" />
        <span class="search_title">状态:</span>
        <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 240px" @change="handleQuery">
          <el-option label="待执行" value="0" />
          <el-option label="执行中" value="1" />
          <el-option label="已完成" value="2" />
          <el-option label="已取消" value="3" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增盘点方案</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :tableLoading="tableLoading"
        :page="page"
        :isShowPagination="true"
        @pagination="paginationChange"
      />
    </div>
    <!-- æ–°å¢ž/编辑盘点计划 -->
    <FormDialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="950px"
      @close="resetForm"
      @confirm="handleSubmit"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-divider content-position="left">基本信息</el-divider>
        <el-row>
          <el-col :span="8">
            <el-form-item label="盘点单号">
              <el-input v-model="form.planNo" disabled placeholder="系统自动生成" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="制单人" prop="createBy">
              <el-select v-model="form.createBy" placeholder="请选择制单人" style="width: 100%" filterable>
                <el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="制单日期">
              <el-date-picker
                v-model="form.createTime"
                type="date"
                placeholder="请选择"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="8">
            <el-form-item label="盘点人" prop="checkerId">
              <el-select v-model="form.checkerId" placeholder="请选择盘点人" style="width: 100%" filterable>
                <el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="计划日期" prop="planDate">
              <el-date-picker
                v-model="form.planDate"
                type="date"
                placeholder="请选择"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="备注">
              <el-input v-model="form.remark" placeholder="请输入备注" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-divider content-position="left">盘点产品</el-divider>
        <div class="mb10">
          <el-button type="primary" size="small" @click="openProductDialog">选择产品</el-button>
          <el-button size="small" @click="clearProducts">清空</el-button>
        </div>
        <PIMTable
          :column="productColumn"
          :tableData="form.items"
          :isShowPagination="false"
          height="350px"
        />
      </el-form>
    </FormDialog>
    <!-- é€‰æ‹©äº§å“å¼¹æ¡† -->
    <ProductSelectDialog
      v-model="productDialogVisible"
      requestUrl="/stockInventory/pagestockInventoryNoQua"
      @confirm="handleProductSelect"
    />
    <!-- å½•入盘点数据 -->
    <FormDialog
      v-model="checkDialogVisible"
      title="录入盘点数据"
      width="1000px"
      operationType="detail"
      @close="checkDialogVisible = false"
    >
      <el-alert
        title="提示:录入实际盘点数量,系统将自动计算差异"
        type="info"
        :closable="false"
        style="margin-bottom: 15px"
      />
      <el-table
        v-loading="checkLoading"
        :data="checkItemList"
        height="400"
        border
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="productName" label="产品名称" min-width="150" />
        <el-table-column prop="model" label="规格型号" min-width="150" />
        <el-table-column prop="unit" label="单位" width="80" align="center" />
        <el-table-column prop="batchNo" label="批号" width="120" align="center" />
        <el-table-column prop="systemQuantity" label="系统库存" width="100" align="center" />
        <el-table-column label="盘点数量" width="120" align="center">
          <template #default="{ row }">
            <el-input-number
              v-model="row.actualQuantity"
              :min="0"
              :precision="2"
              :controls="false"
              style="width: 100%"
            />
          </template>
        </el-table-column>
        <el-table-column label="差异" width="100" align="center">
          <template #default="{ row }">
            <span :class="{ 'text-profit': (row.actualQuantity - row.systemQuantity) > 0, 'text-loss': (row.actualQuantity - row.systemQuantity) < 0 }">
              {{ row.actualQuantity - row.systemQuantity }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="备注" min-width="150">
          <template #default="{ row }">
            <el-input v-model="row.remark" placeholder="请输入备注" />
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSaveCheckData" :loading="submitLoading">保存</el-button>
          <el-button @click="checkDialogVisible = false">取消</el-button>
        </div>
      </template>
    </FormDialog>
    <!-- æŸ¥çœ‹å·®å¼‚ -->
    <FormDialog
      v-model="diffDialogVisible"
      title="盘点差异汇总"
      width="1000px"
      operationType="detail"
      @close="diffDialogVisible = false"
    >
      <div class="diff-summary">
        <el-row :gutter="20">
          <el-col :span="6">
            <div class="stat-item">
              <div class="stat-label">盘点产品总数</div>
              <div class="stat-value">{{ diffSummary.totalCount }}</div>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="stat-item">
              <div class="stat-label">盘盈产品</div>
              <div class="stat-value text-profit">{{ diffSummary.profitCount }}</div>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="stat-item">
              <div class="stat-label">盘亏产品</div>
              <div class="stat-value text-loss">{{ diffSummary.lossCount }}</div>
            </div>
          </el-col>
          <el-col :span="6">
            <div class="stat-item">
              <div class="stat-label">无差异</div>
              <div class="stat-value">{{ diffSummary.normalCount }}</div>
            </div>
          </el-col>
        </el-row>
      </div>
      <PIMTable
        :column="diffItemColumn"
        :tableData="diffItemList"
        :isShowPagination="false"
        height="400px"
      />
      <template #footer>
        <div class="dialog-footer">
          <!-- <el-button type="success" @click="handleGenerateFromDiff">生成盈亏处理单</el-button> -->
          <el-button @click="diffDialogVisible = false">关闭</el-button>
        </div>
      </template>
    </FormDialog>
    <!-- æŸ¥çœ‹è¯¦æƒ… -->
    <FormDialog
      v-model="viewDialogVisible"
      title="盘点计划详情"
      width="900px"
      operationType="detail"
      @close="viewDialogVisible = false"
    >
      <el-descriptions :column="2" border>
        <el-descriptions-item label="盘点单号">{{ viewData.planNo }}</el-descriptions-item>
        <el-descriptions-item label="盘点人">{{ viewData.checkerName }}</el-descriptions-item>
        <el-descriptions-item label="制单人">{{ viewData.createBy }}</el-descriptions-item>
        <el-descriptions-item label="制单日期">{{ viewData.createTime }}</el-descriptions-item>
        <el-descriptions-item label="计划日期">{{ viewData.planDate }}</el-descriptions-item>
        <el-descriptions-item label="状态">
          <el-tag :type="getStatusType(viewData.status)">{{ getStatusText(viewData.status) }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="备注" :span="2">{{ viewData.remark || '-' }}</el-descriptions-item>
      </el-descriptions>
      <div style="margin-top: 20px;">
        <div class="section-title">盘点产品清单</div>
        <PIMTable
          :column="viewProductColumn"
          :tableData="viewData.items"
          :isShowPagination="false"
          height="300px"
        />
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="viewDialogVisible = false">关闭</el-button>
        </div>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import ProductSelectDialog from '@/views/basicData/product/ProductSelectDialog.vue'
import { userListNoPage } from '@/api/system/user.js'
import {
  getStockCheckPlanPage,
  addStockCheckPlan,
  updateStockCheckPlan,
  deleteStockCheckPlan,
  submitApproval,
  startCheck,
  completeCheck,
  getCheckItems,
  saveCheckData,
  getDiffSummary,
  generateProfitLoss,
} from '@/api/inventoryManagement/stockCheck.js'
const tableData = ref([])
const tableLoading = ref(false)
const page = reactive({ current: 1, size: 20, total: 0 })
const searchForm = reactive({ planNo: '', status: '' })
// ä¸»è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  { label: '盘点单号', prop: 'planNo' },
  { label: '制单人', prop: 'createBy' },
  { label: '盘点人', prop: 'checkerName' },
  { label: '制单日期', prop: 'createTime', align: 'center' },
  { label: '计划日期', prop: 'planDate', align: 'center' },
  {
    label: '状态',
    prop: 'status',
    align: 'center',
    dataType: 'tag',
    formatData: (v) => ({
      '0': '待执行', '1': '执行中', '2': '已完成', '3': '已取消',
    }[v] || v),
    formatType: (v) => ({
      '0': 'info', '1': 'primary', '2': 'success', '3': 'warning',
    }[v] || 'info')
  },
  {
    label: '操作',
    dataType: 'action',
    fixed: 'right',
    align: 'center',
    operation: [
      { name: '编辑', clickFun: (row) => handleEdit(row), showHide: (row) => row.status == '0' },
      { name: '查看', clickFun: (row) => handleView(row) },
      { name: '开始盘点', clickFun: (row) => handleStart(row), showHide: (row) => row.status == '0' },
      { name: '录入盘点数据', clickFun: (row) => openCheckDialog(row), showHide: (row) => row.status == '1' },
      { name: '查看差异', clickFun: (row) => openDiffDialog(row), showHide: (row) => row.status == '2' },
      // { name: '生成盈亏单', clickFun: (row) => handleGenerateProfitLoss(row), showHide: (row) => row.status == '2' },
    ]
  }
])
// äº§å“é€‰æ‹©è¡¨æ ¼åˆ—(编辑用)
const productColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' }
])
// ç›˜ç‚¹æ•°æ®å½•入表格列
const checkItemColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' },
  { label: '系统库存', prop: 'systemQty', align: 'center' },
  {
    label: '盘点数量',
    prop: 'checkQty',
    align: 'center',
    dataType: 'slot',
    slot: 'checkQty'
  },
  {
    label: '差异',
    align: 'center',
    dataType: 'slot',
    slot: 'diff'
  }
])
// å·®å¼‚汇总表格列
const diffItemColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' },
  { label: '批号', prop: 'batchNo', align: 'center' },
  { label: '系统库存', prop: 'systemQuantity', align: 'center' },
  { label: '盘点数量', prop: 'actualQuantity', align: 'center' },
  { label: '差异数量', prop: 'differenceQuantity', align: 'center' },
  { label: '备注', prop: 'remark' }
])
// æŸ¥çœ‹è¯¦æƒ…产品表格列
const viewProductColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' },
  { label: '系统库存', prop: 'systemQuantity', align: 'center' }
])
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogMode = ref('add')
const submitLoading = ref(false)
const formRef = ref(null)
// èŽ·å–å½“å‰æ—¥æœŸ
const getCurrentDate = () => {
  const now = new Date()
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
}
const form = reactive({
  id: null,
  planNo: '',
  checkerId: null,
  checkerName: '',
  createBy: null,
  createByName: '',
  createTime: getCurrentDate(),
  planDate: '',
  remark: '',
  items: [],
})
const userList = ref([])
const productDialogVisible = ref(false)
const checkDialogVisible = ref(false)
const checkLoading = ref(false)
const checkItemList = ref([])
const currentPlanId = ref(null)
const diffDialogVisible = ref(false)
const diffItemList = ref([])
const diffSummary = reactive({ totalCount: 0, profitCount: 0, lossCount: 0, normalCount: 0 })
const viewDialogVisible = ref(false)
const viewData = reactive({
  planNo: '', checkerName: '', createByName: '', createTime: '', planDate: '', status: '', remark: '', items: []
})
const rules = {
  createBy: [{ required: true, message: '请选择制单人', trigger: 'change' }],
  checkerId: [{ required: true, message: '请选择盘点人', trigger: 'change' }],
  planDate: [{ required: true, message: '请选择计划日期', trigger: 'change' }],
}
const getList = () => {
  tableLoading.value = true
  getStockCheckPlanPage({ current: page.current, size: page.size, ...searchForm })
    .then(res => {
      tableData.value = res.data?.records || []
      page.total = res.data?.total || 0
    })
    .finally(() => { tableLoading.value = false })
}
const handleQuery = () => {
  page.current = 1
  getList()
}
const resetQuery = () => {
  Object.assign(searchForm, { planNo: '', status: '' })
  handleQuery()
}
const paginationChange = ({ page: current, limit }) => {
  page.current = current
  page.size = limit
  getList()
}
const openForm = (mode) => {
  dialogMode.value = mode
  dialogTitle.value = mode === 'add' ? '新增盘点方案' : '编辑盘点方案'
  if (mode === 'add') {
    Object.assign(form, {
      id: null,
      planNo: '',
      checkerId: null,
      checkerName: '',
      createBy: null,
      createByName: '',
      createTime: getCurrentDate(),
      planDate: '',
      remark: '',
      items: [],
    })
  }
  dialogVisible.value = true
}
const handleEdit = (row) => {
  Object.assign(form, {
    id: row.id,
    planNo: row.planNo,
    checkerId: row.checkerId,
    checkerName: row.checkerName,
    createBy: row.createBy,
    createByName: row.createByName,
    createTime: row.createTime,
    planDate: row.planDate,
    remark: row.remark,
    items: row.items || [],
  })
  dialogMode.value = 'edit'
  dialogTitle.value = '编辑盘点方案'
  dialogVisible.value = true
}
const handleSubmit = () => {
  formRef.value?.validate(valid => {
    if (!valid) return
    if (form.items.length === 0) {
      ElMessage.warning('请至少选择一个盘点产品')
      return
    }
    submitLoading.value = true
    const data = { ...form }
    const action = dialogMode.value === 'add' ? addStockCheckPlan : updateStockCheckPlan
    action(data).then(() => {
      ElMessage.success('保存成功')
      dialogVisible.value = false
      getList()
    }).finally(() => { submitLoading.value = false })
  })
}
const resetForm = () => {
  formRef.value?.resetFields()
}
const handleView = (row) => {
  Object.assign(viewData, {
    planNo: row.planNo,
    checkerName: row.checkerName,
    createBy: row.createBy,
    createTime: row.createTime,
    planDate: row.planDate,
    status: row.status,
    remark: row.remark,
    items: row.items || [],
  })
  viewDialogVisible.value = true
}
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该盘点计划?', '提示', { type: 'warning' })
    .then(() => deleteStockCheckPlan([row.id]))
    .then(() => {
      ElMessage.success('删除成功')
      getList()
    })
}
const handleSubmitApproval = (row) => {
  ElMessageBox.confirm('确认提交审批?', '提示', { type: 'warning' })
    .then(() => submitApproval(row.id))
    .then(() => {
      ElMessage.success('提交成功')
      getList()
    })
}
const handleStart = (row) => {
  ElMessageBox.confirm('确认开始盘点?开始后将锁定相关库存', '提示', { type: 'warning' })
    .then(() => startCheck(row.id))
    .then(() => {
      ElMessage.success('盘点已开始')
      getList()
    })
}
const openCheckDialog = (row) => {
  currentPlanId.value = row.id
  checkDialogVisible.value = true
  checkLoading.value = true
  getCheckItems(row.id).then(res => {
    const data = res.data || {}
    // ç›´æŽ¥ä½¿ç”¨æŽ¥å£è¿”回的 checkItems
    checkItemList.value = (data.checkItems || []).map(item => ({
      ...item,
      actualQuantity: item.actualQuantity || item.systemQuantity,
    }))
  }).finally(() => { checkLoading.value = false })
}
const handleSaveCheckData = () => {
  submitLoading.value = true
  completeCheck({
    id: currentPlanId.value,
    checkItems: checkItemList.value.map(item => ({
      id: item.id,
      productModelId: item.productModelId,
      productCode: item.productCode,
      productName: item.productName,
      specification: item.model,
      unit: item.unit,
      batchNo: item.batchNo,
      systemQuantity: item.systemQuantity,
      actualQuantity: item.actualQuantity,
      differenceQuantity: item.actualQuantity - item.systemQuantity,
      remark: item.remark,
    })),
  }).then(() => {
    ElMessage.success('盘点数据保存成功')
    checkDialogVisible.value = false
    getList()
  }).finally(() => { submitLoading.value = false })
}
const handleComplete = (row) => {
  ElMessageBox.confirm('确认完成盘点?完成后将生成差异数据', '提示', { type: 'warning' })
    .then(() => completeCheck({ id: row.id }))
    .then(() => {
      ElMessage.success('盘点已完成')
      getList()
    })
}
const openDiffDialog = (row) => {
  currentPlanId.value = row.id
  diffDialogVisible.value = true
  getCheckItems(row.id).then(res => {
    const data = res.data || {}
    // ç›´æŽ¥ä½¿ç”¨æŽ¥å£è¿”回的 checkItems
    diffItemList.value = data.checkItems || []
    // è®¡ç®—汇总数据
    const items = diffItemList.value
    const profitCount = items.filter(i => i.differenceQuantity > 0).length
    const lossCount = items.filter(i => i.differenceQuantity < 0).length
    const normalCount = items.filter(i => i.differenceQuantity === 0).length
    Object.assign(diffSummary, {
      totalCount: items.length,
      profitCount,
      lossCount,
      normalCount,
    })
  })
}
const handleGenerateProfitLoss = (row) => {
  ElMessageBox.confirm('确认根据盘点差异生成盈亏处理单?', '提示', { type: 'warning' })
    .then(() => generateProfitLoss(row.id))
    .then(() => {
      ElMessage.success('盈亏处理单已生成')
      getList()
    })
}
const handleGenerateFromDiff = () => {
  handleGenerateProfitLoss({ id: currentPlanId.value })
  diffDialogVisible.value = false
}
// äº§å“é€‰æ‹©ç›¸å…³
const openProductDialog = () => {
  productDialogVisible.value = true
}
const handleProductSelect = (selectedRows) => {
  // æŽ’除已存在的产品
  const existingIds = form.items.map(item => item.productModelId)
  const newItems = selectedRows
    .filter(p => !existingIds.includes(p.productModelId || p.id))
    .map(p => ({
      productModelId: p.productModelId || p.id,
      productName: p.productName,
      model: p.model,
      unit: p.unit,
      systemQuantity: p.systemQuantity || p.systemQty || 0,
    }))
  if (newItems.length === 0) {
    ElMessage.warning('所选产品已存在')
    return
  }
  form.items.push(...newItems)
  ElMessage.success(`已添加 ${newItems.length} ä¸ªäº§å“`)
}
const clearProducts = () => {
  if (form.items.length === 0) return
  ElMessageBox.confirm('确认清空所有产品?', '提示', { type: 'warning' })
    .then(() => {
      form.items = []
      ElMessage.success('已清空')
    })
}
const removeProduct = (index) => {
  form.items.splice(index, 1)
}
const getStatusText = (status) => ({
  '0': '待执行', '1': '执行中', '2': '已完成', '3': '已取消',
}[status] || status)
const getStatusType = (status) => ({
  '0': 'info', '1': 'primary', '2': 'success', '3': 'warning',
}[status] || 'info')
// åŠ è½½ç”¨æˆ·åˆ—è¡¨
const loadUserList = async () => {
  try {
    const res = await userListNoPage()
    userList.value = res?.data || []
  } catch (err) {
    console.error('获取用户列表失败:', err)
    userList.value = []
  }
}
onMounted(() => {
  getList()
  loadUserList()
})
</script>
<style scoped lang="scss">
.diff-summary {
  margin-bottom: 20px;
  padding: 15px;
  background: #f5f7fa;
  border-radius: 4px;
  .stat-item {
    text-align: center;
    .stat-label {
      color: #606266;
      font-size: 13px;
      margin-bottom: 8px;
    }
    .stat-value {
      font-size: 24px;
      font-weight: bold;
      color: #303133;
    }
  }
}
.text-profit { color: #67c23a; font-weight: bold; }
.text-loss { color: #f56c6c; font-weight: bold; }
.mb10 { margin-bottom: 10px; }
.section-title {
  font-weight: bold;
  margin-bottom: 10px;
  padding-left: 10px;
  border-left: 4px solid #409eff;
}
</style>
src/views/inventoryManagement/stockProfitLoss/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,565 @@
<template>
  <div class="app-container">
    <div class="search_form" style="margin-bottom: 20px;">
      <div>
        <span class="search_title">单号:</span>
        <el-input v-model="searchForm.orderNo" placeholder="请输入" clearable style="width: 240px; margin-right: 10px" @change="handleQuery" />
        <span class="search_title">类型:</span>
        <el-select v-model="searchForm.type" placeholder="请选择" clearable style="width: 240px" @change="handleQuery">
          <el-option label="盘盈" value="profit" />
          <el-option label="盘亏" value="loss" />
        </el-select>
        <span class="search_title" style="margin-left: 10px;">状态:</span>
        <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 240px" @change="handleQuery">
          <el-option label="草稿" value="draft" />
          <el-option label="审批中" value="approving" />
          <el-option label="待执行" value="pending" />
          <el-option label="已执行" value="executed" />
        </el-select>
        <span class="search_title" style="margin-left: 10px;">来源:</span>
        <el-select v-model="searchForm.source" placeholder="请选择" clearable style="width: 240px" @change="handleQuery">
          <el-option label="盘点生成" value="check" />
          <el-option label="手动创建" value="manual" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新建盈亏单</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :tableLoading="tableLoading"
        :page="page"
        :isShowPagination="true"
        @pagination="paginationChange"
      />
    </div>
    <!-- æ–°å¢ž/编辑盈亏单 -->
    <FormDialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="1000px"
      @close="resetForm"
      @confirm="handleSubmit"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="盈亏类型" prop="type">
              <el-radio-group v-model="form.type" :disabled="form.source === 'check'">
                <el-radio-button label="profit">盘盈</el-radio-button>
                <el-radio-button label="loss">盘亏</el-radio-button>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="仓库" prop="warehouseId">
              <el-select v-model="form.warehouseId" placeholder="请选择仓库" style="width: 100%" filterable :disabled="form.source === 'check'">
                <el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.source === 'check'">
          <el-col :span="24">
            <el-form-item label="关联盘点计划">
              <el-input v-model="form.checkPlanNo" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="备注">
              <el-input v-model="form.remark" type="textarea" :rows="2" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-divider content-position="left">明细信息</el-divider>
        <div class="mb10" v-if="form.source === 'manual'">
          <el-button type="primary" size="small" @click="openSelectProduct">添加产品</el-button>
        </div>
        <PIMTable
          :column="itemColumn"
          :tableData="form.items"
          :isShowPagination="false"
          height="300px"
        />
        <div class="total-row" style="margin-top: 10px; text-align: right;">
          <span style="font-weight: bold;">总差异金额:</span>
          <span :class="getDiffClass(totalDiffAmount)" style="font-size: 18px; font-weight: bold;">
            {{ totalDiffAmount > 0 ? '+' : '' }}Â¥{{ totalDiffAmount.toFixed(2) }}
          </span>
        </div>
      </el-form>
    </FormDialog>
    <!-- æŸ¥çœ‹è¯¦æƒ… -->
    <FormDialog
      v-model="viewDialogVisible"
      title="盈亏单详情"
      width="950px"
      operationType="detail"
      @close="viewDialogVisible = false"
    >
      <el-descriptions :column="2" border>
        <el-descriptions-item label="单号">{{ viewData.orderNo }}</el-descriptions-item>
        <el-descriptions-item label="类型">
          <el-tag :type="viewData.type === 'profit' ? 'success' : 'danger'">
            {{ viewData.type === 'profit' ? '盘盈' : '盘亏' }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="仓库">{{ viewData.warehouseName }}</el-descriptions-item>
        <el-descriptions-item label="状态">
          <el-tag :type="getStatusType(viewData.status)">{{ getStatusText(viewData.status) }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="来源">
          <el-tag :type="viewData.source === 'check' ? 'primary' : 'info'" size="small">
            {{ viewData.source === 'check' ? '盘点生成' : '手动创建' }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="关联盘点计划">{{ viewData.checkPlanNo || '-' }}</el-descriptions-item>
        <el-descriptions-item label="创建人">{{ viewData.createByName }}</el-descriptions-item>
        <el-descriptions-item label="创建时间">{{ viewData.createTime }}</el-descriptions-item>
        <el-descriptions-item label="备注" :span="2">{{ viewData.remark || '-' }}</el-descriptions-item>
      </el-descriptions>
      <div style="margin-top: 20px;">
        <div class="section-title">明细信息</div>
        <PIMTable
          :column="viewItemColumn"
          :tableData="viewData.items"
          :isShowPagination="false"
          height="300px"
        />
        <div class="total-row" style="margin-top: 10px; text-align: right;">
          <span style="font-weight: bold;">总差异金额:</span>
          <span :class="getDiffClass(viewData.totalAmount)" style="font-size: 18px; font-weight: bold;">
            {{ viewData.totalAmount > 0 ? '+' : '' }}Â¥{{ viewData.totalAmount?.toFixed(2) }}
          </span>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="viewDialogVisible = false">关闭</el-button>
        </div>
      </template>
    </FormDialog>
    <!-- é€‰æ‹©å•†å“å¼¹æ¡† -->
    <FormDialog
      v-model="productDialogVisible"
      title="选择产品"
      width="850px"
      @close="productDialogVisible = false"
      @confirm="confirmSelectProduct"
    >
      <el-form :model="productSearchForm" inline>
        <el-form-item label="产品名称">
          <el-input v-model="productSearchForm.productName" placeholder="请输入" clearable style="width: 180px" />
        </el-form-item>
        <el-form-item label="规格型号">
          <el-input v-model="productSearchForm.model" placeholder="请输入" clearable style="width: 150px" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleProductSearch">搜索</el-button>
        </el-form-item>
      </el-form>
      <PIMTable
        rowKey="id"
        :column="productColumn"
        :tableData="productList"
        :isSelection="true"
        :isShowPagination="false"
        height="350px"
        @selection-change="handleProductSelectionChange"
      />
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import {
  getStockProfitLossPage,
  getStockProfitLossDetail,
  addStockProfitLoss,
  updateStockProfitLoss,
  deleteStockProfitLoss,
  submitApproval,
  executeProfitLoss,
} from '@/api/inventoryManagement/stockProfitLoss.js'
const tableData = ref([])
const tableLoading = ref(false)
const page = reactive({ current: 1, size: 20, total: 0 })
const searchForm = reactive({ orderNo: '', type: '', status: '', source: '' })
// ä¸»è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  { label: '单号', prop: 'orderNo' },
  {
    label: '类型',
    prop: 'type',
    align: 'center',
    dataType: 'tag',
    formatData: (v) => v === 'profit' ? '盘盈' : '盘亏',
    formatType: (v) => v === 'profit' ? 'success' : 'danger'
  },
  { label: '仓库', prop: 'warehouseName' },
  { label: '商品数量', prop: 'productCount', align: 'center' },
  {
    label: '金额',
    prop: 'totalAmount',
    align: 'right',
    formatData: (v) => `Â¥${v?.toFixed(2) || '0.00'}`
  },
  {
    label: '来源',
    prop: 'source',
    align: 'center',
    dataType: 'tag',
    formatData: (v) => v === 'check' ? '盘点生成' : '手动创建',
    formatType: (v) => v === 'check' ? 'primary' : 'info'
  },
  {
    label: '状态',
    prop: 'status',
    align: 'center',
    dataType: 'tag',
    formatData: (v) => ({
      draft: '草稿', approving: '审批中', pending: '待执行', executed: '已执行'
    }[v] || v),
    formatType: (v) => ({
      draft: 'info', approving: 'warning', pending: 'primary', executed: 'success'
    }[v] || '')
  },
  { label: '创建时间', prop: 'createTime', align: 'center' },
  {
    label: '操作',
    dataType: 'action',
    fixed: 'right',
    align: 'center',
    operation: [
      { name: '查看', clickFun: (row) => handleView(row) },
      { name: '编辑', clickFun: (row) => handleEdit(row), showHide: (row) => row.status === 'draft' && row.source === 'manual' },
      { name: '提交审批', clickFun: (row) => handleSubmitApproval(row), showHide: (row) => row.status === 'draft' && row.source === 'manual' },
      { name: '删除', clickFun: (row) => handleDelete(row), showHide: (row) => row.status === 'draft' && row.source === 'manual' },
      { name: '执行', clickFun: (row) => handleExecute(row), showHide: (row) => row.status === 'pending' },
    ]
  }
])
// æ˜Žç»†è¡¨æ ¼åˆ—配置(编辑用)
const itemColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' },
  { label: '系统库存', prop: 'systemQty', align: 'center' },
  {
    label: '实际数量',
    align: 'center',
    dataType: 'slot',
    slot: 'actualQty'
  },
  {
    label: '差异数量',
    align: 'center',
    dataType: 'slot',
    slot: 'diffQty'
  },
  {
    label: '成本单价',
    align: 'center',
    dataType: 'slot',
    slot: 'costPrice'
  },
  {
    label: '差异金额',
    align: 'center',
    dataType: 'slot',
    slot: 'diffAmount'
  },
  {
    label: '操作',
    align: 'center',
    dataType: 'slot',
    slot: 'action'
  }
])
// æŸ¥çœ‹æ˜Žç»†è¡¨æ ¼åˆ—配置
const viewItemColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' },
  { label: '系统库存', prop: 'systemQty', align: 'center' },
  { label: '实际数量', prop: 'actualQty', align: 'center' },
  {
    label: '差异数量',
    align: 'center',
    dataType: 'slot',
    slot: 'viewDiffQty'
  },
  {
    label: '成本单价',
    align: 'right',
    formatData: (v) => `Â¥${v?.toFixed(2) || '0.00'}`
  },
  {
    label: '差异金额',
    align: 'right',
    dataType: 'slot',
    slot: 'viewDiffAmount'
  }
])
// äº§å“é€‰æ‹©è¡¨æ ¼åˆ—配置
const productColumn = ref([
  { label: '产品名称', prop: 'productName' },
  { label: '规格型号', prop: 'model' },
  { label: '单位', prop: 'unit', align: 'center' },
  { label: '当前库存', prop: 'currentQty', align: 'center' }
])
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogMode = ref('add')
const submitLoading = ref(false)
const formRef = ref(null)
const form = reactive({
  id: null,
  type: 'profit',
  warehouseId: null,
  remark: '',
  source: 'manual',
  checkPlanNo: '',
  items: [],
})
const warehouseList = ref([])
const productList = ref([])
const selectedProducts = ref([])
const productDialogVisible = ref(false)
const productSearchForm = reactive({ productName: '', model: '' })
const viewDialogVisible = ref(false)
const viewData = reactive({
  orderNo: '', type: '', warehouseName: '', status: '', source: '',
  checkPlanNo: '', createByName: '', createTime: '', remark: '',
  items: [], totalAmount: 0,
})
const rules = {
  type: [{ required: true, message: '请选择盈亏类型', trigger: 'change' }],
  warehouseId: [{ required: true, message: '请选择仓库', trigger: 'change' }],
}
const totalDiffAmount = computed(() => {
  return form.items.reduce((sum, item) => sum + (item.actualQty - item.systemQty) * (item.costPrice || 0), 0)
})
const getList = () => {
  tableLoading.value = true
  getStockProfitLossPage({ current: page.current, size: page.size, ...searchForm })
    .then(res => {
      tableData.value = res.data?.records || []
      page.total = res.data?.total || 0
    })
    .finally(() => { tableLoading.value = false })
}
const handleQuery = () => {
  page.current = 1
  getList()
}
const resetQuery = () => {
  Object.assign(searchForm, { orderNo: '', type: '', status: '', source: '' })
  handleQuery()
}
const paginationChange = ({ page: current, limit }) => {
  page.current = current
  page.size = limit
  getList()
}
const openForm = (mode) => {
  dialogMode.value = mode
  dialogTitle.value = mode === 'add' ? '新建盈亏单' : '编辑盈亏单'
  if (mode === 'add') {
    Object.assign(form, {
      id: null, type: 'profit', warehouseId: null, remark: '',
      source: 'manual', checkPlanNo: '', items: [],
    })
  }
  dialogVisible.value = true
}
const handleEdit = (row) => {
  getStockProfitLossDetail(row.id).then(res => {
    const data = res.data
    Object.assign(form, {
      id: data.id,
      type: data.type,
      warehouseId: data.warehouseId,
      remark: data.remark,
      source: data.source,
      checkPlanNo: data.checkPlanNo,
      items: data.items || [],
    })
    dialogMode.value = 'edit'
    dialogTitle.value = '编辑盈亏单'
    dialogVisible.value = true
  })
}
const handleSubmit = () => {
  formRef.value?.validate(valid => {
    if (!valid) return
    if (form.items.length === 0) {
      ElMessage.warning('请添加至少一个产品')
      return
    }
    submitLoading.value = true
    const data = { ...form }
    const action = dialogMode.value === 'add' ? addStockProfitLoss : updateStockProfitLoss
    action(data).then(() => {
      ElMessage.success('保存成功')
      dialogVisible.value = false
      getList()
    }).finally(() => { submitLoading.value = false })
  })
}
const resetForm = () => {
  formRef.value?.resetFields()
}
const handleView = (row) => {
  getStockProfitLossDetail(row.id).then(res => {
    const data = res.data || {}
    Object.assign(viewData, {
      orderNo: data.orderNo,
      type: data.type,
      warehouseName: data.warehouseName,
      status: data.status,
      source: data.source,
      checkPlanNo: data.checkPlanNo,
      createByName: data.createByName,
      createTime: data.createTime,
      remark: data.remark,
      items: data.items || [],
      totalAmount: data.totalAmount || 0,
    })
    viewDialogVisible.value = true
  })
}
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该盈亏单?', '提示', { type: 'warning' })
    .then(() => deleteStockProfitLoss(row.id))
    .then(() => {
      ElMessage.success('删除成功')
      getList()
    })
}
const handleSubmitApproval = (row) => {
  ElMessageBox.confirm('确认提交审批?', '提示', { type: 'warning' })
    .then(() => submitApproval(row.id))
    .then(() => {
      ElMessage.success('提交成功')
      getList()
    })
}
const handleExecute = (row) => {
  const actionText = row.type === 'profit' ? '盘盈入库' : '盘亏出库'
  ElMessageBox.confirm(`确认执行${actionText}?执行后将更新库存数量`, '提示', { type: 'warning' })
    .then(() => executeProfitLoss(row.id))
    .then(() => {
      ElMessage.success('执行成功')
      getList()
    })
}
const openSelectProduct = () => {
  productDialogVisible.value = true
  handleProductSearch()
}
const handleProductSearch = () => {
  // è°ƒç”¨æŽ¥å£æŸ¥è¯¢å•†å“åº“å­˜
}
const handleProductSelectionChange = (selection) => {
  selectedProducts.value = selection
}
const confirmSelectProduct = () => {
  if (selectedProducts.value.length === 0) {
    ElMessage.warning('请选择产品')
    return
  }
  const newItems = selectedProducts.value.map(p => ({
    productId: p.id,
    productName: p.productName,
    model: p.model,
    unit: p.unit,
    batchNo: p.batchNo || '',
    systemQty: p.currentQty || 0,
    actualQty: p.currentQty || 0,
    costPrice: p.costPrice || 0,
  }))
  form.items.push(...newItems)
  productDialogVisible.value = false
}
const removeItem = (index) => {
  form.items.splice(index, 1)
}
const getStatusText = (status) => ({
  draft: '草稿', approving: '审批中', pending: '待执行', executed: '已执行',
}[status] || status)
const getStatusType = (status) => ({
  draft: 'info', approving: 'warning', pending: 'primary', executed: 'success',
}[status] || '')
const getDiffClass = (val) => val > 0 ? 'text-profit' : val < 0 ? 'text-loss' : ''
onMounted(() => {
  getList()
})
</script>
<style scoped lang="scss">
.search_form {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 16px;
}
.dialog-footer {
  display: flex;
  justify-content: center;
  gap: 10px;
}
.section-title {
  font-weight: bold;
  margin-bottom: 10px;
  padding-left: 10px;
  border-left: 4px solid #409eff;
}
.text-profit { color: #67c23a; font-weight: bold; }
.text-loss { color: #f56c6c; font-weight: bold; }
</style>