This commit is contained in:
dzc committed 2025-10-20 17:33:13 +08:00
commit ef79fbba05
23 files changed
+2269 -96

No files matched your search

+7 -22
View File
@@ -1,30 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# 表示是最顶层的配置文件
root = true
# 对所有文件生效
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# 对 JS、TS、Vue 文件的配置
[*.{js,jsx,ts,tsx,vue}]
indent_style = tab
indent_size = 2
# 对 JSON、YAML 文件的配置
[*.{json,yml,yaml}]
indent_style = tab
indent_size = 2
# 对 Markdown 文件的配置
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
[*.{js,ts,vue,json}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+8 -33
View File
@@ -1,37 +1,12 @@
# 设置默认行为,以防人们没有设置 core.autocrlf
* text=auto
# 明确声明你想要始终规范化并在检出时转换为原生行结束符的文本文件
*.vue text eol=lf
# 禁用行尾转换
* text=auto eol=lf
*.js text eol=lf
*.ts text eol=lf
*.jsx text eol=lf
*.tsx text eol=lf
*.vue text eol=lf
*.json text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.sass text eol=lf
*.html text eol=lf
*.htm text eol=lf
*.xml text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# 明确声明你不想规范化并希望原样保留的文件
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.eot binary
*.ttf binary
*.woff binary
*.woff2 binary
*.otf binary
# 禁用空白行处理
*.js -whitespace
*.ts -whitespace
*.vue -whitespace
*.json -whitespace
+39 -25
View File
@@ -1,34 +1,48 @@
.DS_Store
node_modules
/dist
docker/dist
# Cursor
.cursor/
.cursorrules
.cursorignore
# IDE
.vscode/
.idea/
# local env files
.env
.env.development
.env.prod
.env.dev-local
.env.*.local
# 临时文件
*.tmp
*.temp
# Log files
# 日志文件
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 依赖目录
node_modules/
.pnpm-store/
# lock
package-lock.json
yarn.lock
pnpm-lock.yaml
# 构建输出
dist/
build/
.env.development
.env.local
.env.development.local
.env.test.local
# 操作系统文件
.DS_Store
Thumbs.db
# 编辑器文件
*.swp
*.swo
*~
# 测试覆盖率
coverage/
.nyc_output/
# 缓存文件
.cache/
.parcel-cache/
@@ -0,0 +1,44 @@
import request from "/@/utils/request"
import type {
InventoryInOutQueryParams,
InventoryInOutPageApiResponse,
InventoryInOutListApiResponse,
InventoryInOutSummaryApiResponse,
} from "./types/inventoryInOutReport"
// 分页查询进销存报表
export function getInventoryInOutPage(params?: InventoryInOutQueryParams): Promise<InventoryInOutPageApiResponse> {
return request({
url: "/supermarket/report/inventory-inout/page",
method: "get",
params,
})
}
// 列表查询进销存报表(用于导出)
export function getInventoryInOutList(params?: InventoryInOutQueryParams): Promise<InventoryInOutListApiResponse> {
return request({
url: "/supermarket/report/inventory-inout/list",
method: "get",
params,
})
}
// 获取进销存汇总统计
export function getInventoryInOutSummary(params?: InventoryInOutQueryParams): Promise<InventoryInOutSummaryApiResponse> {
return request({
url: "/supermarket/report/inventory-inout/summary",
method: "get",
params,
})
}
// 导出Excel(后端直接导出)
export function exportInventoryInOutExcel(params?: InventoryInOutQueryParams): Promise<Blob> {
return request({
url: "/supermarket/report/inventory-inout/export",
method: "get",
params,
responseType: 'blob'
})
}
+97
View File
@@ -17,3 +17,100 @@ export function getInventoryPageSum(params?: object) {
params,
})
}
// 导出出入库明细 orderType: 1入库 2出库
export function exportInventory(params?: object) {
return request({
url: "/supermarket/smErpInventoryRecords/inventoryType/export",
method: "get",
params,
})
}
// ==================== 新增报表接口 ====================
// 1. 进货单明细
export function getPurchaseDetailPage(params?: object) {
return request({
url: "/supermarket/superMarketReport/purchaseDetail/page",
method: "get",
params,
})
}
// 2. 退货单明细
export function getReturnDetailPage(params?: object) {
return request({
url: "/supermarket/superMarketReport/returnDetail/page",
method: "get",
params,
})
}
// 3. 进销存报表
export function getInventoryReportPage(params?: object) {
return request({
url: "/supermarket/superMarketReport/inventory/page",
method: "get",
params,
})
}
export function getInventoryReportList(params?: object) {
return request({
url: "/supermarket/superMarketReport/inventory/list",
method: "get",
params,
})
}
// 4. 毛利报表
export function getProfitReportPage(params?: object) {
return request({
url: "/supermarket/superMarketReport/profit/page",
method: "get",
params,
})
}
export function getProfitReportList(params?: object) {
return request({
url: "/supermarket/superMarketReport/profit/list",
method: "get",
params,
})
}
// 5. 销售报表
export function getSalesReportPage(params?: object) {
return request({
url: "/supermarket/superMarketReport/sales/page",
method: "get",
params,
})
}
export function getSalesReportList(params?: object) {
return request({
url: "/supermarket/superMarketReport/sales/list",
method: "get",
params,
})
}
// 6. 盘存报表
export function getStocktakeReportPage(params?: object) {
return request({
url: "/supermarket/superMarketReport/stocktake/page",
method: "get",
params,
})
}
export function getStocktakeReportList(params?: object) {
return request({
url: "/supermarket/superMarketReport/stocktake/list",
method: "get",
params,
})
}
@@ -39,3 +39,13 @@ export function putObj(obj?: Object) {
})
}
/**
* 下拉
*/
export function getSimpleCategoryAll() {
return request({
url: '/supermarket/supermarketCategory/simpleAll',
method: 'get',
})
}
+78 -5
View File
@@ -40,6 +40,7 @@
lazy
:load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@expand-change="handleExpandChange"
>
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" align="left" show-overflow-tooltip min-width="200" />
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" align="center" show-overflow-tooltip />
@@ -107,6 +108,8 @@ const tableRef = ref()
const menuDialogRef = ref()
const showSearch = ref(true)
const queryRef = ref()
// 保存已展开的节点ID
const expandedRowIds = ref<Set<string>>(new Set())
const state: BasicTableProps = reactive<BasicTableProps>({
createdIsNeed: false,
pageList: pageList,
@@ -136,6 +139,10 @@ const expandRowKeys = computed(() => {
// 打开新增菜单弹窗
const onOpenAddMenu = (type?: string, row?: any) => {
// 如果是添加子菜单,记录父节点ID,以便刷新后自动展开
if (row && row.id) {
expandedRowIds.value.add(row.id)
}
menuDialogRef.value.openDialog(type, row)
}
// 打开编辑菜单弹窗
@@ -149,11 +156,24 @@ const deleteMenuDisabled = (row: any) => {
}
// 清空搜索条件
const resetQuery = () => {
const resetQuery = async () => {
queryRef.value.resetFields()
state.queryForm.parentId = "-1"
state.dataList = []
getDataList()
await getDataList()
// 恢复展开状态
await restoreExpandedRows()
}
// 监听展开/折叠事件
const handleExpandChange = (row: any, expanded: boolean) => {
if (expanded) {
// 展开时记录
expandedRowIds.value.add(row.id)
} else {
// 折叠时移除
expandedRowIds.value.delete(row.id)
}
}
const load = (row: any, treeNode: unknown, resolve: (date: any[]) => void) => {
@@ -186,15 +206,64 @@ const tableList = computed(() => {
return list
})
const query = () => {
// 恢复展开状态(支持多级展开)
const restoreExpandedRows = async () => {
await nextTick()
if (!tableRef.value || expandedRowIds.value.size === 0) return
// 递归加载并展开节点
const loadAndExpandRows = async (list: any[]) => {
for (const row of list || []) {
if (expandedRowIds.value.has(row.id)) {
// 如果节点有子节点且未加载,先加载子节点
if (row.hasChildren && (!row.children || row.children.length === 0)) {
try {
const res = await pageList({ parentId: row.id })
const childrenList = res.data
if (Array.isArray(childrenList) && childrenList.length > 0) {
setHasChildren(childrenList)
row.children = childrenList
}
} catch (error) {
console.error('加载子节点失败:', error)
}
}
// 展开该节点
try {
tableRef.value.toggleRowExpansion(row, true)
// 等待 DOM 更新,确保子节点渲染完成
await nextTick()
} catch (error) {
console.error('展开节点失败:', error)
}
// 如果有子节点,递归处理
if (row.children && row.children.length > 0) {
await loadAndExpandRows(row.children)
}
}
}
}
await loadAndExpandRows(state.dataList)
}
const query = async () => {
state.dataList = []
if (state.queryForm.menuName) {
state.queryForm.parentId = ""
// 搜索时清空展开记录
expandedRowIds.value.clear()
} else {
state.queryForm.parentId = "-1"
}
getDataList()
await getDataList()
// 刷新后恢复展开状态
if (!state.queryForm.menuName) {
await restoreExpandedRows()
}
}
// 删除操作
const handleDelete = async (row: any) => {
@@ -206,7 +275,11 @@ const handleDelete = async (row: any) => {
try {
await delObj(row.id)
getDataList()
// 删除后刷新数据
state.dataList = []
await getDataList()
// 恢复展开状态
await restoreExpandedRows()
useMessage().success(t("common.delSuccessText"))
} catch (err: any) {
useMessage().error(err.msg)
@@ -0,0 +1,190 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" :inline="true" ref="queryRef" @keyup.enter="getDataList">
<el-form-item label="商品名称" prop="productName">
<el-input placeholder="请输入商品名称" v-model.trim="state.queryForm.productName" style="width: 180px" />
</el-form-item>
<el-form-item label="商品编码" prop="productCode">
<el-input placeholder="请输入商品编码" v-model.trim="state.queryForm.productCode" style="width: 180px" />
</el-form-item>
<el-form-item label="商品分类" prop="categoryId">
<el-select v-model="state.queryForm.categoryId" placeholder="请选择商品分类" clearable style="width: 200px">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="state.queryForm.supplierId" placeholder="请选择供应商" clearable style="width: 200px">
<el-option v-for="item in supplierList" :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" />
</el-select>
</el-form-item>
<el-form-item label="统计日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="只显示有变动" prop="onlyChanged">
<el-switch v-model="state.queryForm.onlyChanged" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
:max-height="600"
>
<el-table-column type="index" label="#" width="50" fixed="left" />
<el-table-column prop="productCode" label="商品编码" show-overflow-tooltip min-width="120" fixed="left" />
<el-table-column prop="productName" label="商品名称" show-overflow-tooltip min-width="150" fixed="left" />
<el-table-column prop="categoryName" label="分类" show-overflow-tooltip min-width="100" />
<el-table-column prop="spec" label="规格" show-overflow-tooltip min-width="100" />
<el-table-column prop="unit" label="单位" show-overflow-tooltip min-width="80" />
<el-table-column prop="packageNum" label="装数" show-overflow-tooltip min-width="80" align="right" />
<el-table-column prop="purchasePrice" label="参考进价(元)" show-overflow-tooltip min-width="110" align="right" />
<el-table-column prop="retailPrice" label="零售价(元)" show-overflow-tooltip min-width="110" align="right" />
<el-table-column label="期初" align="center">
<el-table-column prop="openingQty" label="期初库存" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="openingAmount" label="期初金额(元)" show-overflow-tooltip min-width="110" align="right" />
</el-table-column>
<el-table-column label="采购" align="center">
<el-table-column prop="purchaseInQty" label="采购入库数量" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="purchaseInAmount" label="采购入库金额(元)" show-overflow-tooltip min-width="130" align="right" />
<el-table-column prop="purchaseReturnQty" label="采购退货数量" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="purchaseReturnAmount" label="采购退货金额(元)" show-overflow-tooltip min-width="130" align="right" />
</el-table-column>
<el-table-column label="销售" align="center">
<el-table-column prop="salesQty" label="销售数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="salesAmount" label="销售金额(元)" show-overflow-tooltip min-width="110" align="right" />
<el-table-column prop="salesReturnQty" label="销售退货数量" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="salesReturnAmount" label="销售退货金额(元)" show-overflow-tooltip min-width="130" align="right" />
<el-table-column prop="actualSalesQty" label="实际销售数量" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="actualSalesAmount" label="实际销售金额(元)" show-overflow-tooltip min-width="130" align="right" />
</el-table-column>
<el-table-column label="成本与毛利" align="center">
<el-table-column prop="salesCost" label="销售成本(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="grossProfit" label="销售毛利(元)" show-overflow-tooltip min-width="120" align="right" />
</el-table-column>
<el-table-column label="期末" align="center">
<el-table-column prop="closingQty" label="期末数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="closingAmount" label="期末库存金额(元)" show-overflow-tooltip min-width="130" align="right" />
<el-table-column prop="closingRetailAmount" label="期末库存售额(元)" show-overflow-tooltip min-width="130" align="right" />
</el-table-column>
<el-table-column prop="firstPurchaseDate" label="首次进货日" show-overflow-tooltip min-width="120" />
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { getInventoryReportPage } from "/@/api/supermarketerp/report"
import { getSupplierSimpleAll } from "/@/api/supermarketerp/smErpSupplier"
import { getSimpleCategoryAll } from "/@/api/supermarketerp/supermarketCategory"
const queryRef = ref()
const showSearch = ref(true)
const supplierList = ref<any[]>([])
const categoryList = ref<any[]>([])
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
onlyChanged: false,
},
pageList: getInventoryReportPage,
isPage: true,
})
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields()
state.queryForm.onlyChanged = false
getDataList()
}
// 导出excel
const exportExcel = () => {
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
delete params.dateRange
downBlobFile("/supermarket/superMarketReport/inventory/export", params, "进销存报表.xlsx")
}
// 获取供应商列表
const getSupplierList = async () => {
try {
const res = await getSupplierSimpleAll()
supplierList.value = res.data || []
} catch (error) {
console.error("获取供应商列表失败:", error)
}
}
// 获取商品分类列表
const getCategoryList = async () => {
try {
const res = await getSimpleCategoryAll()
categoryList.value = res.data || []
} catch (error) {
console.error("获取商品分类列表失败:", error)
}
}
// 页面加载时获取下拉数据
onMounted(() => {
getSupplierList()
getCategoryList()
})
</script>
@@ -345,3 +345,12 @@ const handleCurrentChange = (page: number) => {
@@ -195,3 +195,12 @@ defineExpose({
@@ -211,3 +211,12 @@ const trendClass = computed(() => {
@@ -16,3 +16,12 @@ export { default as ProductRankingTable } from './ProductRankingTable.vue';
@@ -216,3 +216,12 @@ export const SUCCESS_MESSAGES = {
@@ -205,3 +205,12 @@ export const TABLE_CONFIG = {
+143
View File
@@ -0,0 +1,143 @@
# 进销存报表功能流程
## 页面布局流程
```
┌─────────────────────────────────────────────────────────────┐
│ 页面头部 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 进销存报表 │ │ 刷新数据按钮 │ │
│ │ 统计商品库存变化 │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 查询条件区域 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │时间范围 │ │商品名称 │ │商品编码 │ │分类选择 │ │仓库选择 ││
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘│
│ ┌─────────┐ ┌─────────────────────────────────────────────┐│
│ │仅显示有 │ │ 查询/重置/导出按钮 ││
│ │发生额 │ │ ││
│ └─────────┘ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 汇总统计卡片 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │商品总数 │ │总入库 │ │总出库 │ │库存周转 │ │
│ │150 │ │3000.0 │ │2500.0 │ │率48% │ │
│ │有发生额85│ │500000元 │ │200000元 │ │均价80元 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据表格 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │商品 │ │商品 │ │分类 │ │单位 │ │期初 │ │入库 │ │出库 │ │
│ │编码 │ │名称 │ │ │ │ │ │数量 │ │数量 │ │数量 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │期末 │ │入库 │ │出库 │ │操作 │ │
│ │数量 │ │金额 │ │金额 │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 分页组件 │
│ 共100条记录 每页10条 ← 1 2 3 4 5 → 跳转到第__页 │
└─────────────────────────────────────────────────────────────┘
```
## 数据流程
```
用户输入查询条件
│
▼
验证查询参数
│
▼
┌─────────────────┐
│ 调用分页查询API │
│ /page接口 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 调用汇总统计API │
│ /summary接口 │
└─────────────────┘
│
▼
更新页面数据
│
▼
渲染表格和卡片
```
## 导出流程
```
用户点击导出按钮
│
▼
显示确认对话框
│
▼
用户确认导出
│
▼
┌─────────────────┐
│ 调用导出API │
│ /export接口 │
└─────────────────┘
│
▼
接收Excel文件流
│
▼
创建下载链接
│
▼
自动下载文件
```
## 错误处理流程
```
API调用失败
│
▼
捕获错误信息
│
▼
显示错误提示
│
▼
加载模拟数据
│
▼
继续正常显示
```
## 响应式布局
```
桌面端 (>= 768px) 移动端 (< 768px)
┌─────────────────┐ ┌─────────────────┐
│ 水平排列查询条件 │ │ 垂直堆叠查询条件 │
│ 4列汇总卡片 │ │ 1列汇总卡片 │
│ 完整表格显示 │ │ 简化表格显示 │
└─────────────────┘ └─────────────────┘
```
@@ -0,0 +1,135 @@
# 进销存报表功能
## 功能概述
进销存报表模块用于统计商品在特定时间段内的库存变化情况,包括:
- **期初库存**:统计期初始时的库存数量
- **入库数量及金额**:统计期内所有入库操作的数量和金额
- **出库数量及金额**:统计期内所有出库操作的数量和金额
- **期末库存**:统计期结束时的库存数量
支持按商品、分类、时间范围等多维度查询,并提供分页和导出功能。
## 文件结构
```
src/views/supermarketerp/report/jxc/
├── index.vue # 主页面组件
└── README.md # 说明文档
src/api/supermarketerp/
└── inventoryInOutReport.ts # API接口文件
```
## 主要功能
### 1. 查询条件
- **时间范围**:支持日期范围选择器
- **商品名称**:支持模糊查询
- **商品编码**:支持模糊查询
- **分类**:下拉选择器
- **仓库**:下拉选择器
- **仅显示有发生额**:复选框,过滤无变动商品
### 2. 汇总统计卡片
- **商品总数**:显示总商品数和有发生额商品数
- **总入库**:显示总入库数量和金额
- **总出库**:显示总出库数量和金额
- **库存周转率**:显示周转率和平均单价
### 3. 数据表格
- **商品编码**:商品唯一标识
- **商品名称**:商品名称
- **分类**:商品分类
- **单位**:基础单位
- **期初数量**:统计期初始库存
- **入库数量**:统计期内入库总量
- **出库数量**:统计期内出库总量
- **期末数量**:统计期结束库存
- **入库金额**:统计期内入库总金额
- **出库金额**:统计期内出库总金额
### 4. 分页功能
- 支持页码切换
- 支持每页显示条数切换(10、20、50、100)
- 显示总记录数
### 5. 导出功能
- 支持导出Excel文件
- 导出文件名包含查询时间范围
- 后端直接生成Excel文件
## API接口
### 1. 分页查询
```typescript
getInventoryInOutPage(params)
// URL: /admin/report/inventory-inout/page
// Method: GET
```
### 2. 汇总统计
```typescript
getInventoryInOutSummary(params)
// URL: /admin/report/inventory-inout/summary
// Method: GET
```
### 3. 导出Excel
```typescript
exportInventoryInOutExcel(params)
// URL: /admin/report/inventory-inout/export
// Method: GET
```
## 权限要求
- **查看权限**:`supermarket_inventoryInOutReport_view`
- **导出权限**:`supermarket_inventoryInOutReport_export`
## 使用说明
1. **进入页面**:导航到超市ERP > 报表 > 进销存报表
2. **设置查询条件**:选择时间范围和其他过滤条件
3. **点击查询**:获取符合条件的数据
4. **查看汇总**:在卡片区域查看关键指标
5. **浏览详情**:在表格中查看详细数据
6. **导出数据**:点击导出按钮下载Excel文件
## 技术特性
- **响应式设计**:支持移动端和桌面端
- **实时查询**:查询条件变化时自动重新加载数据
- **错误处理**:网络异常时显示模拟数据
- **加载状态**:查询和导出时显示加载状态
- **数据格式化**:货币、数字、百分比格式化显示
## 样式特点
- **现代化UI**:使用Element Plus组件库
- **渐变卡片**:汇总统计卡片使用渐变背景
- **悬停效果**:卡片和按钮支持悬停动画
- **响应式布局**:移动端自动调整布局
## 注意事项
1. **时间范围**:建议查询时间范围不超过1年
2. **数据量**:导出数据量建议控制在10000条以内
3. **权限**:确保用户具有相应的查看和导出权限
4. **网络**:网络异常时会使用模拟数据展示
## 更新日志
- **v1.0.0** (2024-12-19)
- 初始版本发布
- 实现基础查询功能
- 实现汇总统计功能
- 实现导出Excel功能
- 完善响应式设计
@@ -0,0 +1,630 @@
<template>
<div class="inventory-inout-report">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<h2 class="page-title">进销存报表</h2>
<p class="page-description">统计商品在特定时间段内的库存变化情况</p>
</div>
<div class="header-right">
<el-button type="primary" @click="handleQuery" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新数据
</el-button>
</div>
</div>
<!-- 查询条件 -->
<el-card class="query-card" shadow="never">
<el-form :model="queryForm" :inline="true" class="query-form">
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
@change="handleDateChange"
/>
</el-form-item>
<el-form-item label="商品名称">
<el-input
v-model="queryForm.productName"
placeholder="请输入商品名称"
clearable
style="width: 160px"
/>
</el-form-item>
<el-form-item label="商品编码">
<el-input
v-model="queryForm.productCode"
placeholder="请输入商品编码"
clearable
style="width: 160px"
/>
</el-form-item>
<el-form-item label="分类">
<el-select
v-model="queryForm.categoryId"
placeholder="请选择分类"
clearable
style="width: 160px"
>
<el-option label="水果" value="101" />
<el-option label="蔬菜" value="102" />
<el-option label="肉类" value="103" />
<el-option label="乳制品" value="104" />
<el-option label="饮料" value="105" />
</el-select>
</el-form-item>
<el-form-item>
<el-checkbox v-model="queryForm.onlyChanged">仅显示有发生额</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>
查询
</el-button>
<el-button @click="handleReset">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
<el-button type="success" @click="handleExport" :loading="exporting">
<el-icon><Download /></el-icon>
导出Excel
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 汇总统计卡片 -->
<div class="summary-cards" v-loading="loading">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="summary-card total-products">
<div class="card-content">
<div class="card-icon">
<el-icon><Box /></el-icon>
</div>
<div class="card-info">
<div class="card-title">商品总数</div>
<div class="card-value">{{ formatNumber(summary.totalProducts || 0) }}</div>
<div class="card-subtitle">
有发生额: {{ formatNumber(summary.changedProducts || 0) }}
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="summary-card total-in">
<div class="card-content">
<div class="card-icon">
<el-icon><Upload /></el-icon>
</div>
<div class="card-info">
<div class="card-title">总入库</div>
<div class="card-value">{{ formatNumber(summary.totalInQty || 0) }}</div>
<div class="card-subtitle">
金额: {{ formatCurrency(summary.totalInAmount || 0) }}
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="summary-card total-out">
<div class="card-content">
<div class="card-icon">
<el-icon><Download /></el-icon>
</div>
<div class="card-info">
<div class="card-title">总出库</div>
<div class="card-value">{{ formatNumber(summary.totalOutQty || 0) }}</div>
<div class="card-subtitle">
金额: {{ formatCurrency(summary.totalOutAmount || 0) }}
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="summary-card turnover-rate">
<div class="card-content">
<div class="card-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="card-info">
<div class="card-title">库存周转率</div>
<div class="card-value">{{ formatPercent(summary.turnoverRate || 0) }}</div>
<div class="card-subtitle">
平均单价: {{ formatCurrency(summary.avgPrice || 0) }}
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 数据表格 -->
<el-card class="table-card" shadow="never">
<el-table
:data="tableData"
:loading="loading"
stripe
border
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<el-table-column prop="productCode" label="商品编码" width="150" fixed="left" />
<el-table-column prop="productName" label="商品名称" width="200" show-overflow-tooltip />
<el-table-column prop="categoryName" label="分类" width="120" />
<el-table-column prop="unit" label="单位" width="80" align="center" />
<el-table-column prop="openingQty" label="期初数量" width="120" align="right">
<template #default="{ row }">
{{ formatNumber(row.openingQty) }}
</template>
</el-table-column>
<el-table-column prop="inQty" label="入库数量" width="120" align="right">
<template #default="{ row }">
{{ formatNumber(row.inQty) }}
</template>
</el-table-column>
<el-table-column prop="outQty" label="出库数量" width="120" align="right">
<template #default="{ row }">
{{ formatNumber(row.outQty) }}
</template>
</el-table-column>
<el-table-column prop="closingQty" label="期末数量" width="120" align="right">
<template #default="{ row }">
{{ formatNumber(row.closingQty) }}
</template>
</el-table-column>
<el-table-column prop="inAmount" label="入库金额(元)" width="140" align="right">
<template #default="{ row }">
{{ formatCurrency(row.inAmount) }}
</template>
</el-table-column>
<el-table-column prop="outAmount" label="出库金额(元)" width="140" align="right">
<template #default="{ row }">
{{ formatCurrency(row.outAmount) }}
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" />
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="InventoryInOutReport">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
Refresh,
Download,
Search,
RefreshLeft,
Box,
Upload,
TrendCharts,
} from '@element-plus/icons-vue';
import {
getInventoryInOutPage,
getInventoryInOutSummary,
exportInventoryInOutExcel,
} from '/@/api/supermarketerp/inventoryInOutReport';
// 响应式数据
const loading = ref(false);
const exporting = ref(false);
const tableData = ref([]);
const summary = ref({});
const dateRange = ref([]);
// 查询表单
const queryForm = reactive({
startDate: '',
endDate: '',
productName: '',
productCode: '',
categoryId: null,
onlyChanged: false,
});
// 分页数据
const pagination = reactive({
current: 1,
size: 10,
total: 0,
});
// 初始化查询条件
const initQueryForm = () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, today.getDate());
const startDateStr = lastMonth.toISOString().slice(0, 10);
const endDateStr = today.toISOString().slice(0, 10);
dateRange.value = [startDateStr, endDateStr];
queryForm.startDate = startDateStr;
queryForm.endDate = endDateStr;
};
// 日期范围变化处理
const handleDateChange = () => {
if (dateRange.value && dateRange.value.length === 2) {
queryForm.startDate = dateRange.value[0];
queryForm.endDate = dateRange.value[1];
} else {
queryForm.startDate = '';
queryForm.endDate = '';
}
};
// 查询数据
const handleQuery = async () => {
loading.value = true;
try {
// 更新日期参数
handleDateChange();
const params = {
current: pagination.current,
size: pagination.size,
startDate: queryForm.startDate,
endDate: queryForm.endDate,
productName: queryForm.productName,
productCode: queryForm.productCode,
categoryId: queryForm.categoryId,
onlyChanged: queryForm.onlyChanged,
};
const response = await getInventoryInOutPage(params);
if (response.code === 0 && response.data) {
tableData.value = response.data.records || [];
pagination.total = response.data.total || 0;
pagination.current = response.data.current || 1;
pagination.size = response.data.size || 10;
// 同时获取汇总数据
await loadSummary();
} else {
throw new Error(response.msg || '查询失败');
}
} catch (error) {
console.error('查询失败:', error);
ElMessage.error('查询失败,请稍后重试');
// 清空数据
tableData.value = [];
summary.value = {};
} finally {
loading.value = false;
}
};
// 获取汇总统计
const loadSummary = async () => {
try {
const params = {
startDate: queryForm.startDate,
endDate: queryForm.endDate,
productName: queryForm.productName,
productCode: queryForm.productCode,
categoryId: queryForm.categoryId,
onlyChanged: queryForm.onlyChanged,
};
const response = await getInventoryInOutSummary(params);
if (response.code === 0 && response.data) {
summary.value = response.data;
}
} catch (error) {
console.error('获取汇总数据失败:', error);
summary.value = {};
}
};
// 导出Excel
const handleExport = async () => {
try {
await ElMessageBox.confirm(
'确定要导出进销存报表吗?',
'导出确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
);
exporting.value = true;
const params = {
startDate: queryForm.startDate,
endDate: queryForm.endDate,
productName: queryForm.productName,
productCode: queryForm.productCode,
categoryId: queryForm.categoryId,
onlyChanged: queryForm.onlyChanged,
};
const response = await exportInventoryInOutExcel(params);
// 创建下载链接
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `进销存报表_${queryForm.startDate}_${queryForm.endDate}.xlsx`;
link.click();
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
} catch (error) {
if (error !== 'cancel') {
console.error('导出失败:', error);
ElMessage.error('导出失败,请稍后重试');
}
} finally {
exporting.value = false;
}
};
// 重置查询
const handleReset = () => {
dateRange.value = [];
Object.assign(queryForm, {
startDate: '',
endDate: '',
productName: '',
productCode: '',
categoryId: null,
onlyChanged: false,
});
pagination.current = 1;
initQueryForm();
handleQuery();
};
// 格式化货币
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(value || 0);
};
// 格式化数字
const formatNumber = (value: number) => {
return new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(value || 0);
};
// 格式化百分比
const formatPercent = (value: number) => {
return ((value || 0) * 100).toFixed(2) + '%';
};
// 组件挂载
onMounted(() => {
initQueryForm();
handleQuery();
});
</script>
<style lang="scss" scoped>
.inventory-inout-report {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.header-left {
.page-title {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.page-description {
margin: 0;
color: #909399;
font-size: 14px;
}
}
.header-right {
display: flex;
gap: 12px;
}
}
.query-card {
margin-bottom: 20px;
.query-form {
.el-form-item {
margin-bottom: 16px;
}
}
}
.summary-cards {
margin-bottom: 20px;
.summary-card {
height: 120px;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
}
&.total-products {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
.card-icon {
background: rgba(255, 255, 255, 0.2);
}
}
&.total-in {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
.card-icon {
background: rgba(255, 255, 255, 0.2);
}
}
&.total-out {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
.card-icon {
background: rgba(255, 255, 255, 0.2);
}
}
&.turnover-rate {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
.card-icon {
background: rgba(255, 255, 255, 0.2);
}
}
.card-content {
display: flex;
align-items: center;
height: 100%;
.card-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
.el-icon {
font-size: 24px;
}
}
.card-info {
flex: 1;
.card-title {
font-size: 14px;
margin-bottom: 8px;
opacity: 0.9;
}
.card-value {
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
}
.card-subtitle {
font-size: 12px;
opacity: 0.8;
}
}
}
}
}
.table-card {
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.inventory-inout-report {
padding: 10px;
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.header-right {
width: 100%;
justify-content: flex-end;
}
}
.summary-cards {
.el-col {
margin-bottom: 16px;
}
}
.query-form {
.el-form-item {
width: 100%;
margin-bottom: 12px;
.el-form-item__content {
width: 100%;
}
}
}
}
}
</style>
@@ -0,0 +1,155 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" :inline="true" ref="queryRef" @keyup.enter="getDataList">
<el-form-item label="商品名称" prop="productName">
<el-input placeholder="请输入商品名称" v-model.trim="state.queryForm.productName" style="width: 180px" />
</el-form-item>
<el-form-item label="商品编码" prop="productCode">
<el-input placeholder="请输入商品编码" v-model.trim="state.queryForm.productCode" style="width: 180px" />
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-input placeholder="分类ID" v-model.number="state.queryForm.categoryId" style="width: 150px" />
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-input placeholder="供应商ID" v-model.number="state.queryForm.supplierId" style="width: 150px" />
</el-form-item>
<el-form-item label="统计日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="排序方式" prop="sortBy">
<el-select v-model="state.queryForm.sortBy" placeholder="请选择" style="width: 150px">
<el-option label="按销售额降序" value="" />
<el-option label="按毛利降序" value="profit" />
<el-option label="按毛利率降序" value="margin" />
</el-select>
</el-form-item>
<el-form-item label="只显示有销售" prop="onlyWithSales">
<el-switch v-model="state.queryForm.onlyWithSales" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="#" width="50" fixed="left" />
<el-table-column prop="productCode" label="商品编码" show-overflow-tooltip min-width="120" fixed="left" />
<el-table-column prop="productName" label="商品名称" show-overflow-tooltip min-width="150" fixed="left" />
<el-table-column prop="categoryName" label="分类" show-overflow-tooltip min-width="100" />
<el-table-column prop="spec" label="规格" show-overflow-tooltip min-width="100" />
<el-table-column prop="unit" label="单位" show-overflow-tooltip min-width="80" />
<el-table-column prop="salesQty" label="销售数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="salesAmount" label="销售金额(元)" show-overflow-tooltip min-width="120" align="right">
<template #default="{ row }">
<span :style="{ color: row.salesAmount > 0 ? '#67c23a' : '' }">
{{ row.salesAmount?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="salesPrice" label="销售单价(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="costPrice" label="成本单价(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="salesCost" label="销售成本(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="grossProfit" label="销售毛利(元)" show-overflow-tooltip min-width="120" align="right">
<template #default="{ row }">
<span :style="{ color: row.grossProfit > 0 ? '#67c23a' : row.grossProfit < 0 ? '#f56c6c' : '' }">
{{ row.grossProfit?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="profitMargin" label="毛利率(%)" show-overflow-tooltip min-width="100" align="right">
<template #default="{ row }">
<el-tag v-if="row.profitMargin >= 30" type="success">{{ row.profitMargin?.toFixed(2) }}%</el-tag>
<el-tag v-else-if="row.profitMargin >= 20" type="primary">{{ row.profitMargin?.toFixed(2) }}%</el-tag>
<el-tag v-else-if="row.profitMargin >= 10" type="warning">{{ row.profitMargin?.toFixed(2) }}%</el-tag>
<el-tag v-else-if="row.profitMargin > 0" type="info">{{ row.profitMargin?.toFixed(2) }}%</el-tag>
<el-tag v-else type="danger">{{ row.profitMargin?.toFixed(2) }}%</el-tag>
</template>
</el-table-column>
<el-table-column prop="salesCount" label="销售笔数" show-overflow-tooltip min-width="100" align="right" />
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { getProfitReportPage } from "/@/api/supermarketerp/report"
const queryRef = ref()
const showSearch = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
sortBy: "",
onlyWithSales: false,
},
pageList: getProfitReportPage,
isPage: true,
})
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields()
state.queryForm.sortBy = ""
state.queryForm.onlyWithSales = false
getDataList()
}
// 导出excel
const exportExcel = () => {
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
delete params.dateRange
downBlobFile("/supermarket/superMarketReport/profit/export", params, "毛利报表.xlsx")
}
</script>
@@ -0,0 +1,163 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" :inline="true" ref="queryRef" @keyup.enter="getDataList">
<el-form-item label="关键字" prop="key">
<el-input placeholder="单据编号/商品名称" v-model.trim="state.queryForm.key" style="width: 200px" />
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="state.queryForm.supplierId" placeholder="请选择供应商" clearable style="width: 200px">
<el-option v-for="item in supplierList" :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" />
</el-select>
</el-form-item>
<el-form-item label="商品分类" prop="categoryId">
<el-select v-model="state.queryForm.categoryId" placeholder="请选择商品分类" clearable style="width: 200px">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="单据日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="#" width="50" fixed="left" />
<el-table-column prop="inventoryCode" label="单据编号" show-overflow-tooltip min-width="180" />
<el-table-column prop="inventoryDate" label="单据日期" show-overflow-tooltip min-width="120" />
<el-table-column prop="productName" label="商品名称" show-overflow-tooltip min-width="150" />
<el-table-column prop="spec" label="规格" show-overflow-tooltip min-width="120" />
<el-table-column prop="unit" label="单位" show-overflow-tooltip min-width="80" />
<el-table-column prop="packageNum" label="装数" show-overflow-tooltip min-width="80" align="right" />
<el-table-column prop="quantity" label="数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="unitPrice" label="单价(元)" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="totalAmount" label="总金额(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="supplierName" label="供应商" show-overflow-tooltip min-width="150" />
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="150" />
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { getPurchaseDetailPage } from "/@/api/supermarketerp/report"
import { ElMessage } from "element-plus"
import { getSupplierSimpleAll } from "/@/api/supermarketerp/smErpSupplier"
import { getSimpleCategoryAll } from "/@/api/supermarketerp/supermarketCategory"
const queryRef = ref()
const showSearch = ref(true)
const supplierList = ref<any[]>([])
const categoryList = ref<any[]>([])
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: getPurchaseDetailPage,
isPage: true,
})
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields()
getDataList()
}
// 导出excel
const exportExcel = () => {
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
delete params.dateRange
downBlobFile("/supermarket/superMarketReport/purchaseDetail/export", params, "进货单明细.xlsx").catch(async (error: any) => {
// 当 responseType 为 blob 时,错误信息也是 blob 格式,需要转换
if (error instanceof Blob) {
try {
const text = await error.text()
const errorData = JSON.parse(text)
ElMessage.error(errorData?.msg || errorData?.message || "导出失败")
} catch {
ElMessage.error("导出失败")
}
} else {
ElMessage.error(error?.msg || error?.message || "导出失败")
}
})
}
// 获取供应商列表
const getSupplierList = async () => {
try {
const res = await getSupplierSimpleAll()
supplierList.value = res.data || []
} catch (error) {
console.error("获取供应商列表失败:", error)
}
}
// 获取商品分类列表
const getCategoryList = async () => {
try {
const res = await getSimpleCategoryAll()
categoryList.value = res.data || []
} catch (error) {
console.error("获取商品分类列表失败:", error)
}
}
// 页面加载时获取下拉数据
onMounted(() => {
getSupplierList()
getCategoryList()
})
</script>
@@ -0,0 +1,109 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" :inline="true" ref="queryRef" @keyup.enter="getDataList">
<el-form-item label="关键字" prop="key">
<el-input placeholder="单据编号/商品名称/供应商" v-model.trim="state.queryForm.key" style="width: 200px" />
</el-form-item>
<el-form-item label="单据日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="#" width="50" fixed="left" />
<el-table-column prop="inventoryCode" label="单据编号" show-overflow-tooltip min-width="180" />
<el-table-column prop="inventoryDate" label="单据日期" show-overflow-tooltip min-width="120" />
<el-table-column prop="productName" label="商品名称" show-overflow-tooltip min-width="150" />
<el-table-column prop="spec" label="规格" show-overflow-tooltip min-width="120" />
<el-table-column prop="unit" label="单位" show-overflow-tooltip min-width="80" />
<el-table-column prop="packageNum" label="装数" show-overflow-tooltip min-width="80" align="right" />
<el-table-column prop="quantity" label="数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="unitPrice" label="单价(元)" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="totalAmount" label="总金额(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="supplierName" label="供应商" show-overflow-tooltip min-width="150" />
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="150" />
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { getReturnDetailPage } from "/@/api/supermarketerp/report"
const queryRef = ref()
const showSearch = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: getReturnDetailPage,
isPage: true,
})
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields()
getDataList()
}
// 导出excel
const exportExcel = () => {
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
delete params.dateRange
downBlobFile("/supermarket/superMarketReport/returnDetail/export", params, "退货单明细.xlsx")
}
</script>
@@ -0,0 +1,178 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" :inline="true" ref="queryRef" @keyup.enter="getDataList">
<el-form-item label="商品名称" prop="productName">
<el-input placeholder="请输入商品名称" v-model.trim="state.queryForm.productName" style="width: 180px" />
</el-form-item>
<el-form-item label="商品编码" prop="productCode">
<el-input placeholder="请输入商品编码" v-model.trim="state.queryForm.productCode" style="width: 180px" />
</el-form-item>
<el-form-item label="商品分类" prop="categoryId">
<el-select v-model="state.queryForm.categoryId" placeholder="请选择商品分类" clearable style="width: 200px">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="统计日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="排序方式" prop="sortBy">
<el-select v-model="state.queryForm.sortBy" placeholder="请选择" style="width: 150px">
<el-option label="按销售额降序" value="" />
<el-option label="按销售量降序" value="qty" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="#" width="50" fixed="left" />
<el-table-column prop="productCode" label="商品编码" show-overflow-tooltip min-width="120" fixed="left" />
<el-table-column prop="productName" label="商品名称" show-overflow-tooltip min-width="150" fixed="left" />
<el-table-column prop="categoryName" label="分类" show-overflow-tooltip min-width="100" />
<el-table-column prop="spec" label="规格" show-overflow-tooltip min-width="100" />
<el-table-column prop="unit" label="单位" show-overflow-tooltip min-width="80" />
<el-table-column label="销售" align="center">
<el-table-column prop="salesQty" label="销售数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="salesAmount" label="销售金额(元)" show-overflow-tooltip min-width="120" align="right">
<template #default="{ row }">
<span style="color: #67c23a; font-weight: bold;">
{{ row.salesAmount?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="salesPrice" label="销售单价(元)" show-overflow-tooltip min-width="120" align="right" />
<el-table-column prop="salesCount" label="销售笔数" show-overflow-tooltip min-width="100" align="right" />
</el-table-column>
<el-table-column label="退货" align="center">
<el-table-column prop="returnQty" label="退货数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="returnAmount" label="退货金额(元)" show-overflow-tooltip min-width="120" align="right">
<template #default="{ row }">
<span style="color: #f56c6c;">
{{ row.returnAmount?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="实际销售" align="center">
<el-table-column prop="actualSalesQty" label="实际销售数量" show-overflow-tooltip min-width="120" align="right">
<template #default="{ row }">
<span style="font-weight: bold;">
{{ row.actualSalesQty?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="actualSalesAmount" label="实际销售金额(元)" show-overflow-tooltip min-width="130" align="right">
<template #default="{ row }">
<span style="color: #409eff; font-weight: bold;">
{{ row.actualSalesAmount?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { getSalesReportPage } from "/@/api/supermarketerp/report"
import { getSimpleCategoryAll } from "/@/api/supermarketerp/supermarketCategory"
const queryRef = ref()
const showSearch = ref(true)
const categoryList = ref<any[]>([])
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
sortBy: "",
},
pageList: getSalesReportPage,
isPage: true,
})
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields()
state.queryForm.sortBy = ""
getDataList()
}
// 导出excel
const exportExcel = () => {
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
delete params.dateRange
downBlobFile("/supermarket/superMarketReport/sales/export", params, "销售报表.xlsx")
}
// 获取商品分类列表
const getCategoryList = async () => {
try {
const res = await getSimpleCategoryAll()
categoryList.value = res.data || []
} catch (error) {
console.error("获取商品分类列表失败:", error)
}
}
// 页面加载时获取下拉数据
onMounted(() => {
getCategoryList()
})
</script>
@@ -0,0 +1,192 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" :inline="true" ref="queryRef" @keyup.enter="getDataList">
<el-form-item label="盘点单号" prop="stocktakeCode">
<el-input placeholder="请输入盘点单号" v-model.trim="state.queryForm.stocktakeCode" style="width: 180px" />
</el-form-item>
<el-form-item label="商品名称" prop="productName">
<el-input placeholder="请输入商品名称" v-model.trim="state.queryForm.productName" style="width: 180px" />
</el-form-item>
<el-form-item label="商品编码" prop="productCode">
<el-input placeholder="请输入商品编码" v-model.trim="state.queryForm.productCode" style="width: 180px" />
</el-form-item>
<el-form-item label="商品分类" prop="categoryId">
<el-select v-model="state.queryForm.categoryId" placeholder="请选择商品分类" clearable style="width: 200px">
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="state.queryForm.supplierId" placeholder="请选择供应商" clearable style="width: 200px">
<el-option v-for="item in supplierList" :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" />
</el-select>
</el-form-item>
<el-form-item label="盘点日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="差异类型" prop="diffType">
<el-select v-model="state.queryForm.diffType" placeholder="请选择" style="width: 150px">
<el-option label="全部" value="all" />
<el-option label="盘盈" value="surplus" />
<el-option label="盘亏" value="shortage" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="index" label="#" width="50" fixed="left" />
<el-table-column prop="stocktakeCode" label="盘点单号" show-overflow-tooltip min-width="180" fixed="left" />
<el-table-column prop="stocktakeDate" label="盘点日期" show-overflow-tooltip min-width="120" />
<el-table-column prop="productCode" label="商品编码" show-overflow-tooltip min-width="120" />
<el-table-column prop="productName" label="商品名称" show-overflow-tooltip min-width="150" />
<el-table-column prop="categoryName" label="分类" show-overflow-tooltip min-width="100" />
<el-table-column prop="spec" label="规格" show-overflow-tooltip min-width="100" />
<el-table-column prop="unit" label="单位" show-overflow-tooltip min-width="80" />
<el-table-column label="盘点数据" align="center">
<el-table-column prop="bookQty" label="账面数量" show-overflow-tooltip min-width="100" align="right" />
<el-table-column prop="physicalQty" label="实盘数量" show-overflow-tooltip min-width="100" align="right" />
</el-table-column>
<el-table-column label="差异" align="center">
<el-table-column prop="diffQty" label="差异数量" show-overflow-tooltip min-width="100" align="right">
<template #default="{ row }">
<span :style="{ color: row.diffQty > 0 ? '#67c23a' : row.diffQty < 0 ? '#f56c6c' : '' }">
{{ row.diffQty?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
<el-table-column prop="diffAmount" label="差异金额(元)" show-overflow-tooltip min-width="120" align="right">
<template #default="{ row }">
<span :style="{ color: row.diffAmount > 0 ? '#67c23a' : row.diffAmount < 0 ? '#f56c6c' : '' }">
{{ row.diffAmount?.toFixed(2) || '0.00' }}
</span>
</template>
</el-table-column>
<el-table-column label="状态" show-overflow-tooltip min-width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.diffQty > 0" type="success">盘盈</el-tag>
<el-tag v-else-if="row.diffQty < 0" type="danger">盘亏</el-tag>
<el-tag v-else type="info">盘平</el-tag>
</template>
</el-table-column>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="150" />
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { getStocktakeReportPage } from "/@/api/supermarketerp/report"
import { getSupplierSimpleAll } from "/@/api/supermarketerp/smErpSupplier"
import { getSimpleCategoryAll } from "/@/api/supermarketerp/supermarketCategory"
const queryRef = ref()
const showSearch = ref(true)
const supplierList = ref<any[]>([])
const categoryList = ref<any[]>([])
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
diffType: "all",
},
pageList: getStocktakeReportPage,
isPage: true,
})
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields()
state.queryForm.diffType = "all"
getDataList()
}
// 导出excel
const exportExcel = () => {
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
delete params.dateRange
downBlobFile("/supermarket/superMarketReport/stocktake/export", params, "盘存报表.xlsx")
}
// 获取供应商列表
const getSupplierList = async () => {
try {
const res = await getSupplierSimpleAll()
supplierList.value = res.data || []
} catch (error) {
console.error("获取供应商列表失败:", error)
}
}
// 获取商品分类列表
const getCategoryList = async () => {
try {
const res = await getSimpleCategoryAll()
categoryList.value = res.data || []
} catch (error) {
console.error("获取商品分类列表失败:", error)
}
}
// 页面加载时获取下拉数据
onMounted(() => {
getSupplierList()
getCategoryList()
})
</script>
@@ -6,15 +6,17 @@
<el-form-item label="单据编号" prop="key">
<el-input placeholder="单据编号" v-model.trim="state.queryForm.key" />
</el-form-item>
<el-form-item label="单据日期" prop="inventoryDate">
<el-date-picker
v-model="state.queryForm.inventoryDate"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="date"
placeholder="请输入单据日期"
/>
</el-form-item>
<el-form-item label="单据区间日期" prop="dateRange">
<el-date-picker
v-model="state.queryForm.dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -25,6 +27,7 @@
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="true"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@@ -71,9 +74,23 @@ const state: BasicTableProps = reactive<BasicTableProps>({
orderType: 1,
},
pageList: getInventoryPage,
isPage: true,
})
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
// 包装 getDataList 方法,处理日期范围
const { getDataList: originalGetDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state)
const getDataList = () => {
// 处理日期范围
if (state.queryForm.dateRange && state.queryForm.dateRange.length === 2) {
state.queryForm.startDate = state.queryForm.dateRange[0]
state.queryForm.endDate = state.queryForm.dateRange[1]
} else {
state.queryForm.startDate = undefined
state.queryForm.endDate = undefined
}
originalGetDataList()
}
// 清空搜索条件
const resetQuery = () => {
@@ -89,6 +106,15 @@ const selectionChangHandle = (objs: { id: string }[]) => {
// 导出excel
const exportExcel = () => {
downBlobFile("/supermarketerp/smErpInventoryWarning/export", Object.assign(state.queryForm, { ids: selectObjs }), "入库记录.xlsx")
// 处理日期范围
const params: any = { ...state.queryForm }
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0]
params.endDate = params.dateRange[1]
}
// 删除前端使用的 dateRange 字段
delete params.dateRange
downBlobFile("/supermarket/smErpInventoryRecords/inventoryType/export", params, "入库记录.xlsx")
}
</script>