From d4bc592b5d0c42bc69abd5bd6334c191205dc244 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期六, 09 五月 2026 17:42:14 +0800
Subject: [PATCH] 浪潮 1.日志前端页面开发与联调 2.仓库盘点页面开发与联调
---
src/api/inventoryManagement/stockProfitLoss.js | 95 +++
src/views/customerService/feedbackRegistration/components/formDia.vue | 4
src/views/inventoryManagement/stockCheckPlan/index.vue | 720 ++++++++++++++++++++++++++++
src/api/inventoryManagement/stockCheck.js | 112 ++++
src/views/inventoryManagement/stockProfitLoss/index.vue | 565 ++++++++++++++++++++++
5 files changed, 1,494 insertions(+), 2 deletions(-)
diff --git a/src/api/inventoryManagement/stockCheck.js b/src/api/inventoryManagement/stockCheck.js
new file mode 100644
index 0000000..3b52a2e
--- /dev/null
+++ b/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',
+ })
+}
\ No newline at end of file
diff --git a/src/api/inventoryManagement/stockProfitLoss.js b/src/api/inventoryManagement/stockProfitLoss.js
new file mode 100644
index 0000000..d818d7b
--- /dev/null
+++ b/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',
+ })
+}
\ No newline at end of file
diff --git a/src/views/customerService/feedbackRegistration/components/formDia.vue b/src/views/customerService/feedbackRegistration/components/formDia.vue
index 5c9e565..e5905ee 100644
--- a/src/views/customerService/feedbackRegistration/components/formDia.vue
+++ b/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
diff --git a/src/views/inventoryManagement/stockCheckPlan/index.vue b/src/views/inventoryManagement/stockCheckPlan/index.vue
new file mode 100644
index 0000000..a934bb3
--- /dev/null
+++ b/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>
diff --git a/src/views/inventoryManagement/stockProfitLoss/index.vue b/src/views/inventoryManagement/stockProfitLoss/index.vue
new file mode 100644
index 0000000..fdd21fb
--- /dev/null
+++ b/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>
\ No newline at end of file
--
Gitblit v1.9.3