This commit is contained in:
weiwei committed 2025-10-21 10:18:13 +08:00
commit ef9cce63fe
44 files changed
+4657 -313

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/
+1
View File
@@ -18,6 +18,7 @@
"@axolo/json-editor-vue": "^0.3.2",
"@chenfengyuan/vue-qrcode": "^2.0.0",
"@element-plus/icons-vue": "^2.0.10",
"@map-component/vue-tmap": "^1.0.1",
"@popperjs/core": "2.11.8",
"@vueuse/core": "^10.4.1",
"@vxe-ui/plugin-render-element": "^4.0.10",
@@ -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',
})
}
+40
View File
@@ -0,0 +1,40 @@
import request from "/@/utils/request"
export function fetchList(query?: Object) {
return request({
url: "/wallet/mch-pick-up/page",
method: "get",
params: query,
})
}
export function addObj(obj?: Object) {
return request({
url: "/wallet/mch-pick-up",
method: "post",
data: obj,
})
}
export function getObj(id?: string) {
return request({
url: "/wallet/mch-pick-up/" + id,
method: "get",
})
}
export function delObjs(ids?: Object) {
return request({
url: "/wallet/mch-pick-up",
method: "delete",
data: ids,
})
}
export function putObj(obj?: Object) {
return request({
url: "/wallet/mch-pick-up",
method: "put",
data: obj,
})
}
+40
View File
@@ -15,6 +15,14 @@ export function pageTh(query?: any) {
})
}
export function thTotal(data?: any) {
return request({
url: "/wallet/wallet-transaction/th-total",
method: "post",
data: data,
})
}
export function pageCz(query?: any) {
return request({
url: "/wallet/wallet-transaction/page-cz",
@@ -27,6 +35,14 @@ export function pageCz(query?: any) {
})
}
export function czTotal(data?: any) {
return request({
url: "/wallet/wallet-transaction/cz-total",
method: "post",
data: data,
})
}
export function pageJz(query?: any) {
return request({
url: "/wallet/wallet-transaction/page-jz",
@@ -39,6 +55,14 @@ export function pageJz(query?: any) {
})
}
export function jzTotal(data?: any) {
return request({
url: "/wallet/wallet-transaction/jz-total",
method: "post",
data: data,
})
}
export function pageXf(query?: any) {
return request({
url: "/wallet/wallet-transaction/page-xf",
@@ -51,6 +75,14 @@ export function pageXf(query?: any) {
})
}
export function xfTotal(data: any) {
return request({
url: "/wallet/wallet-transaction/xf-total",
method: "post",
data: data,
})
}
export function pageTk(query?: any) {
return request({
url: "/wallet/wallet-transaction/page-tk",
@@ -63,6 +95,14 @@ export function pageTk(query?: any) {
})
}
export function tkTotal(data: any) {
return request({
url: "/wallet/wallet-transaction/tk-total",
method: "post",
data: data,
})
}
export function updateBalance(obj?: Object) {
return request({
url: "/wallet/wallet-transaction/update-balance",
+251
View File
@@ -0,0 +1,251 @@
<template>
<div class="business-hours-container">
<div class="business-hours-header">
<span class="title">营业时间设置</span>
<el-button type="primary" size="small" @click="addBusinessHour" :icon="Plus">
添加时间段
</el-button>
</div>
<div class="business-hours-list">
<div v-for="(item, index) in businessHours" :key="item.id" class="business-hour-item">
<div class="item-header">
<span class="item-index">{{ index + 1 }}</span>
<el-button type="danger" size="small" :icon="Delete" circle @click="removeBusinessHour(index)"
:disabled="businessHours.length <= 1" />
</div>
<div class="item-content">
<el-row :gutter="16">
<el-col :span="24" class="mb16">
<el-form-item :label="`时间段${index + 1}标题`" :prop="`businessHours.${index}.title`">
<el-input v-model="item.title" placeholder="请输入时间段标题(如:工作日、周末、上午、下午等)" @input="handleChange" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb16">
<el-form-item :label="`开始时间`" :prop="`businessHours.${index}.startTime`">
<el-time-picker v-model="item.startTime" placeholder="选择开始时间" format="HH:mm" value-format="HH:mm"
@change="handleChange" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb16">
<el-form-item :label="`结束时间`" :prop="`businessHours.${index}.endTime`">
<el-time-picker v-model="item.endTime" placeholder="选择结束时间" format="HH:mm" value-format="HH:mm"
@change="handleChange" />
</el-form-item>
</el-col>
</el-row>
</div>
</div>
</div>
<div v-if="businessHours.length === 0" class="empty-state">
<el-empty description="暂无营业时间,请点击上方按钮添加" />
</div>
</div>
</template>
<script setup lang="ts" name="BusinessHours">
import { Plus, Delete } from '@element-plus/icons-vue'
// 定义接口
interface BusinessHour {
id: string
title: string
startTime: string
endTime: string
}
// 定义props
interface Props {
modelValue?: BusinessHour[]
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => []
})
// 定义emits
const emit = defineEmits<{
'update:modelValue': [value: BusinessHour[]]
'change': [value: BusinessHour[]]
}>()
// 响应式数据
const businessHours = ref<BusinessHour[]>([])
// 生成唯一ID
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
// 创建默认营业时间项
const createDefaultBusinessHour = (): BusinessHour => ({
id: generateId(),
title: '',
startTime: '09:00',
endTime: '18:00'
})
// 初始化数据
const initData = () => {
if (props.modelValue && props.modelValue.length > 0) {
businessHours.value = props.modelValue.map(item => ({
...item,
id: item.id || generateId()
}))
} else {
businessHours.value = [createDefaultBusinessHour()]
}
}
// 添加营业时间
const addBusinessHour = () => {
businessHours.value.push(createDefaultBusinessHour())
handleChange()
}
// 删除营业时间
const removeBusinessHour = (index: number) => {
if (businessHours.value.length > 1) {
businessHours.value.splice(index, 1)
handleChange()
}
}
// 时间转换为分钟
const timeToMinutes = (time: string): number => {
const [hours, minutes] = time.split(':').map(Number)
return hours * 60 + minutes
}
// 处理数据变化
const handleChange = () => {
const validBusinessHours = businessHours.value.filter(item =>
item.startTime && item.endTime
)
emit('update:modelValue', [...businessHours.value])
emit('change', validBusinessHours)
}
// 监听props变化
watch(() => props.modelValue, (newValue) => {
if (JSON.stringify(newValue) !== JSON.stringify(businessHours.value)) {
initData()
handleChange()
}
}, { deep: true })
// 组件挂载时初始化
onMounted(() => {
initData()
handleChange()
})
// 暴露验证方法
const validate = (): Promise<boolean> => {
return new Promise((resolve) => {
let isValid = true
for (let i = 0; i < businessHours.value.length; i++) {
const item = businessHours.value[i]
if (!item.title || !item.startTime || !item.endTime) {
isValid = false
break
}
const startMinutes = timeToMinutes(item.startTime)
const endMinutes = timeToMinutes(item.endTime)
if (endMinutes <= startMinutes) {
isValid = false
break
}
}
resolve(isValid)
})
}
defineExpose({
validate
})
</script>
<style scoped lang="scss">
.business-hours-container {
.business-hours-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
}
.business-hours-list {
.business-hour-item {
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
background-color: #fafafa;
&:last-child {
margin-bottom: 0;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.item-index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 500;
}
}
.item-content {
.mb16 {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
.empty-state {
text-align: center;
padding: 40px 0;
}
}
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__label) {
font-size: 13px;
color: #606266;
}
</style>
+5 -13
View File
@@ -1,17 +1,7 @@
<template>
<el-select
style="width: 100%"
v-model="selectId"
:placeholder="props.placeholder"
clearable
remote-show-suffix
filterable
remote
:disabled="props.disabled"
:remote-method="remoteSearch"
:loading="state.loading"
@visible-change="(e: boolean) => e && getOptions()"
>
<el-select style="width: 100%" v-model="selectId" :placeholder="props.placeholder" clearable remote-show-suffix
filterable remote :disabled="props.disabled" :remote-method="remoteSearch" :loading="state.loading"
@visible-change="(e: boolean) => e && getOptions()" :multiple="props.multiple">
<el-option v-for="v in state.options" :key="v.id" :label="`${v.name}`" :value="v.id">
<div class="flex items-center">
<div class="color-primary">{{ v.name }}</div>
@@ -37,12 +27,14 @@ const props = withDefaults(
apiParams?: any
autoFirst?: boolean
disabled?: boolean
multiple?: boolean
}>(),
{
placeholder: "请选择",
apiParams: {},
autoFirst: false,
disabled: false,
multiple: false
},
)
+4
View File
@@ -36,6 +36,8 @@ import SelectCabinet from "/@/components/Select/SelectCabinet.vue"
import SelectMchCatory from "/@/components/Select/SelectMchCatory.vue"
import SelectCanteenSupplier from "/@/components/Select/SelectCanteenSupplier.vue"
import SelectWalletInvoice from "/@/components/Select/SelectWalletInvoice.vue"
// 地图组件
import MapSelect from "/@/components/map/map-select.vue"
// 第三方组件
import ElementPlus from "element-plus"
@@ -94,6 +96,8 @@ export default {
app.component("SelectCanteenSupplier", SelectCanteenSupplier)
app.component("SelectWalletInvoice", SelectWalletInvoice)
app.component("MapSelect", MapSelect)
// 导入全部的elmenet-plus的图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
+414
View File
@@ -0,0 +1,414 @@
<template>
<el-dialog v-model="state.visibleMapDialog" title="地图位置选择器" width="80%" :before-close="handleClose"
class="map-picker-dialog">
<div class="map-picker-container">
<!-- 地图容器 -->
<div class="map-container">
<tmap-map ref="mapRef" style="width: 100%; height: 500px; cursor: pointer;"
mapKey="CGABZ-3MH66-6VGST-MEMS3-K6U3V-DGBKA" :center="map.center" :zoom="map.zoom"
:doubleClickZoom="map.doubleClickZoom" :events="{
click: handleMapClick,
mousemove: handleMapMouseMove,
mouseleave: handleMapMouseLeave,
}">
<!-- 标记点 -->
<tmap-marker v-if="state.selectedLocation" :position="state.selectedLocation" :draggable="true"
@dragend="handleMarkerDragEnd">
<template #default>
<div class="custom-marker">
<div class="marker-icon">
<i class="el-icon-location-outline"></i>
手动发送发送到发送方
</div>
<div class="marker-shadow"></div>
</div>
</template>
</tmap-marker>
</tmap-map>
<!-- 鼠标悬停坐标显示 -->
<div v-if="state.showHoverCoords && state.hoverLocation" class="hover-coords">
<div class="coords-tooltip">
<span>经度: {{ state.hoverLocation.lng.toFixed(6) }}</span>
<span>纬度: {{ state.hoverLocation.lat.toFixed(6) }}</span>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm" :disabled="!state.selectedLocation">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="map-picker-dialog">
import { reactive, ref, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
// 定义事件
const emit = defineEmits(['confirm', 'close'])
// 响应式状态
const state = reactive({
visibleMapDialog: false,
selectedLocation: null as any,
hoverLocation: null as any, // 鼠标悬停位置
showHoverCoords: false // 是否显示悬停坐标
})
// 地图配置
const map = reactive<any>({
// 默认中心点(可以根据需要调整)
center: { lat: 39.908823, lng: 116.397470 }, // 北京天安门
zoom: 15,
doubleClickZoom: true,
})
// 地图引用
const mapRef = ref()
// 打开对话框
const open = (initialLocation?: any) => {
state.visibleMapDialog = true
resetState()
// 如果有初始位置,设置为当前选中位置
if (initialLocation) {
state.selectedLocation = { ...initialLocation }
map.center = initialLocation
}
// 等待DOM更新后初始化地图
nextTick(() => {
console.log('地图对话框已打开')
})
}
// 关闭对话框
const handleClose = () => {
state.visibleMapDialog = false
resetState()
emit('close')
}
// 重置状态
const resetState = () => {
state.selectedLocation = null
state.hoverLocation = null
state.showHoverCoords = false
}
// 地图鼠标移动事件
const handleMapMouseMove = (event: any) => {
const { lat, lng } = event.latLng
state.hoverLocation = { lat, lng }
state.showHoverCoords = true
}
// 地图鼠标离开事件
const handleMapMouseLeave = () => {
state.showHoverCoords = false
state.hoverLocation = null
}
// 地图点击事件
const handleMapClick = (event: any) => {
const { lat, lng } = event.latLng
state.selectedLocation = { lat, lng }
console.log('选中位置:', { lat, lng })
}
// 标记拖拽结束事件
const handleMarkerDragEnd = (event: any) => {
const { lat, lng } = event.latLng
state.selectedLocation = { lat, lng }
console.log('拖拽到位置:', { lat, lng })
}
// 确认选择
const handleConfirm = () => {
if (!state.selectedLocation) {
ElMessage.warning('请先选择一个位置')
return
}
const result = {
location: state.selectedLocation,
coordinates: `${state.selectedLocation.lng},${state.selectedLocation.lat}`,
// 提供多种坐标格式
formats: {
lngLat: `${state.selectedLocation.lng},${state.selectedLocation.lat}`,
latLng: `${state.selectedLocation.lat},${state.selectedLocation.lng}`,
lng: state.selectedLocation.lng,
lat: state.selectedLocation.lat
}
}
emit('confirm', result)
handleClose()
}
// 暴露方法
defineExpose({
open
})
</script>
<style scoped>
.map-picker-dialog {
.el-dialog__body {
padding: 0;
}
}
.map-picker-container {
display: flex;
flex-direction: column;
height: 600px;
}
.search-bar {
padding: 16px;
border-bottom: 1px solid #e4e7ed;
background: #fff;
}
.search-input {
width: 100%;
}
.map-container {
position: relative;
flex: 1;
overflow: hidden;
}
/* 鼠标悬停坐标显示 */
.hover-coords {
position: absolute;
top: 10px;
left: 10px;
pointer-events: none;
z-index: 1000;
}
.coords-tooltip {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0px 6px;
border-radius: 6px;
font-size: 12px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(4px);
}
.coords-tooltip i {
font-size: 14px;
color: #67c23a;
}
/* 自定义标记点样式 */
.custom-marker {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
}
.marker-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
border: 3px solid white;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
transition: all 0.3s ease;
}
.marker-icon:hover {
transform: rotate(-45deg) scale(1.1);
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.6);
}
.marker-icon i {
color: white;
font-size: 16px;
transform: rotate(45deg);
font-weight: bold;
}
.marker-shadow {
width: 20px;
height: 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 50%;
margin-top: 2px;
filter: blur(2px);
}
.info-panel {
padding: 16px;
border-top: 1px solid #e4e7ed;
background: #f8f9fa;
max-height: 200px;
overflow-y: auto;
}
.location-info {
margin-bottom: 16px;
}
.info-row {
display: flex;
margin-bottom: 8px;
font-size: 14px;
}
.label {
color: #606266;
width: 80px;
flex-shrink: 0;
}
.value {
color: #303133;
flex: 1;
word-break: break-all;
}
.search-results {
border-top: 1px solid #e4e7ed;
padding-top: 16px;
}
.results-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 12px;
}
.results-list {
max-height: 150px;
overflow-y: auto;
}
.result-item {
padding: 8px 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
background: #fff;
}
.result-item:hover {
border-color: #409eff;
background: #f0f9ff;
}
.result-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.result-address {
font-size: 12px;
color: #909399;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.map-picker-container {
height: 500px;
}
.info-panel {
max-height: 150px;
}
.info-row {
flex-direction: column;
}
.label {
width: auto;
margin-bottom: 4px;
}
}
/* 地图标记样式优化 */
:deep(.tmap-marker) {
animation: bounce 0.6s ease-in-out;
}
@keyframes bounce {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -8px, 0);
}
70% {
transform: translate3d(0, -4px, 0);
}
90% {
transform: translate3d(0, -2px, 0);
}
}
/* 搜索结果滚动条样式 */
.results-list::-webkit-scrollbar,
.info-panel::-webkit-scrollbar {
width: 6px;
}
.results-list::-webkit-scrollbar-track,
.info-panel::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.results-list::-webkit-scrollbar-thumb,
.info-panel::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.results-list::-webkit-scrollbar-thumb:hover,
.info-panel::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
+230
View File
@@ -0,0 +1,230 @@
<template>
<div class="map-example-container">
<el-card header="地图位置选择器示例">
<div class="example-section">
<h3>基础用法</h3>
<map-select v-model="basicLocation" placeholder="请选择位置" @change="handleBasicChange" />
<div v-if="basicLocation" class="result-display">
<p><strong>选中地址:</strong>{{ basicLocation.address }}</p>
<p><strong>经纬度:</strong>{{ basicLocation.coordinates }}</p>
</div>
</div>
<el-divider />
<div class="example-section">
<h3>只返回坐标</h3>
<map-select v-model="coordinatesOnly" placeholder="选择位置(只返回坐标)" return-format="coordinates"
@change="handleCoordinatesChange" />
<div v-if="coordinatesOnly" class="result-display">
<p><strong>坐标:</strong>{{ coordinatesOnly }}</p>
</div>
</div>
<el-divider />
<div class="example-section">
<h3>只返回地址</h3>
<map-select v-model="addressOnly" placeholder="选择位置(只返回地址)" return-format="address"
@change="handleAddressChange" />
<div v-if="addressOnly" class="result-display">
<p><strong>地址:</strong>{{ addressOnly }}</p>
</div>
</div>
<el-divider />
<div class="example-section">
<h3>隐藏坐标信息</h3>
<map-select v-model="noCoordinates" placeholder="选择位置(隐藏坐标)" :show-coordinates="false"
@change="handleNoCoordinatesChange" />
<div v-if="noCoordinates" class="result-display">
<p><strong>选中地址:</strong>{{ noCoordinates.address }}</p>
</div>
</div>
<el-divider />
<div class="example-section">
<h3>禁用状态</h3>
<map-select v-model="disabledLocation" placeholder="禁用状态" disabled />
</div>
<el-divider />
<div class="example-section">
<h3>预设位置</h3>
<el-button @click="setPresetLocation">设置预设位置(天安门)</el-button>
<map-select v-model="presetLocation" placeholder="预设位置示例" @change="handlePresetChange" />
<div v-if="presetLocation" class="result-display">
<p><strong>选中地址:</strong>{{ presetLocation.address }}</p>
<p><strong>经纬度:</strong>{{ presetLocation.coordinates }}</p>
</div>
</div>
<el-divider />
<div class="example-section">
<h3>表单集成示例</h3>
<el-form :model="formData" label-width="120px">
<el-form-item label="商店名称">
<el-input v-model="formData.name" placeholder="请输入商店名称" />
</el-form-item>
<el-form-item label="商店位置">
<map-select v-model="formData.location" placeholder="请选择商店位置"
@change="handleFormLocationChange" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="formData.phone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import MapSelect from './map-select.vue'
// 定义位置数据类型
interface LocationData {
address: string
location: { lat: number, lng: number }
coordinates: string
}
// 基础用法
const basicLocation = ref<LocationData | null>(null)
const handleBasicChange = (value: any) => {
console.log('基础用法选择结果:', value)
}
// 只返回坐标
const coordinatesOnly = ref<string | null>(null)
const handleCoordinatesChange = (value: any) => {
console.log('坐标结果:', value)
}
// 只返回地址
const addressOnly = ref<string | null>(null)
const handleAddressChange = (value: any) => {
console.log('地址结果:', value)
}
// 隐藏坐标信息
const noCoordinates = ref<LocationData | null>(null)
const handleNoCoordinatesChange = (value: any) => {
console.log('隐藏坐标结果:', value)
}
// 禁用状态
const disabledLocation = ref<LocationData>({
address: '北京市朝阳区某某街道',
location: { lat: 39.9042, lng: 116.4074 },
coordinates: '116.4074,39.9042'
})
// 预设位置
const presetLocation = ref<LocationData | null>(null)
const setPresetLocation = () => {
presetLocation.value = {
address: '北京市东城区天安门广场',
location: { lat: 39.9042, lng: 116.4074 },
coordinates: '116.4074,39.9042'
}
}
const handlePresetChange = (value: any) => {
console.log('预设位置结果:', value)
}
// 表单集成
const formData = reactive<{
name: string
location: LocationData | null
phone: string
}>({
name: '',
location: null,
phone: ''
})
const handleFormLocationChange = (value: any) => {
console.log('表单位置选择:', value)
}
const submitForm = () => {
if (!formData.name) {
ElMessage.warning('请输入商店名称')
return
}
if (!formData.location) {
ElMessage.warning('请选择商店位置')
return
}
if (!formData.phone) {
ElMessage.warning('请输入联系电话')
return
}
console.log('提交表单数据:', formData)
ElMessage.success('表单提交成功')
}
const resetForm = () => {
formData.name = ''
formData.location = null
formData.phone = ''
ElMessage.info('表单已重置')
}
</script>
<style scoped>
.map-example-container {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.example-section {
margin-bottom: 24px;
}
.example-section h3 {
margin-bottom: 16px;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.result-display {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
font-size: 14px;
}
.result-display p {
margin: 4px 0;
color: #606266;
}
.result-display strong {
color: #303133;
}
.el-divider {
margin: 32px 0;
}
.el-form {
max-width: 500px;
}
</style>
+257
View File
@@ -0,0 +1,257 @@
<template>
<div class="map-select-container">
<el-input v-model="state.selectAddress" :placeholder="props.placeholder" :disabled="props.disabled"
:clearable="props.clearable" class="map-select-input" readonly @clear="handleClear">
<template #prepend v-if="state.selectedLocation">
<el-icon class="location-icon">
<Location />
</el-icon>
</template>
<template #append>
<el-button @click="handleMapClick" :disabled="props.disabled" class="map-button">
<el-icon>
<MapLocation />
</el-icon>
选择位置
</el-button>
</template>
</el-input>
<!-- 坐标信息显示 -->
<div v-if="state.selectedLocation && state.showCoordinates" class="coordinates-info">
<span class="coordinates-text">
经纬度: {{ state.selectedLocation.lng.toFixed(6) }}, {{ state.selectedLocation.lat.toFixed(6) }}
</span>
<el-button text size="small" @click="copyCoordinates" class="copy-btn">
<el-icon>
<CopyDocument />
</el-icon>
复制
</el-button>
</div>
<map-dialog ref="mapDialogRef" @confirm="handleLocationConfirm" @close="handleDialogClose" />
</div>
</template>
<script setup lang="ts" name="map-location-select">
import { reactive, ref, onMounted, watch, defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import { MapLocation, Location, CopyDocument } from '@element-plus/icons-vue'
const MapDialog = defineAsyncComponent(() => import('./map-dialog.vue'))
// 定义事件
const emit = defineEmits(['change', 'update:modelValue'])
// 定义属性
const props = withDefaults(
defineProps<{
modelValue?: any
placeholder?: string
disabled?: boolean
clearable?: boolean
showCoordinates?: boolean
returnFormat?: 'object' | 'coordinates' | 'address'
}>(),
{
placeholder: "请点击选择地图位置",
disabled: false,
clearable: true,
showCoordinates: true,
returnFormat: 'object'
}
)
// 响应式状态
const state = reactive<any>({
selectAddress: '',
selectedLocation: null,
selectedData: null
})
// 引用
const mapDialogRef = ref()
// 清空选择
const handleClear = () => {
state.selectAddress = ''
state.selectedLocation = null
state.selectedData = null
emit('update:modelValue', null)
emit('change', null)
}
// 打开地图选择器
const handleMapClick = () => {
if (props.disabled) return
// 如果已有位置,传递给对话框作为初始位置
mapDialogRef.value?.open(state.selectedLocation)
}
// 处理位置确认
const handleLocationConfirm = (result: any) => {
state.selectAddress = result.address
state.selectedLocation = result.location
state.selectedData = result
// 根据返回格式发送不同的值
let emitValue
switch (props.returnFormat) {
case 'coordinates':
emitValue = result.coordinates
break
case 'address':
emitValue = result.address
break
case 'object':
default:
emitValue = result
break
}
emit('update:modelValue', emitValue)
emit('change', emitValue)
ElMessage.success('位置选择成功')
}
// 处理对话框关闭
const handleDialogClose = () => {
// 对话框关闭时的处理
}
// 监听外部值变化
watch(() => props.modelValue, (newValue) => {
if (newValue) {
if (typeof newValue === 'object' && newValue.address) {
state.selectAddress = newValue.address
state.selectedLocation = newValue.location
state.selectedData = newValue
} else if (typeof newValue === 'string') {
state.selectAddress = newValue
}
} else {
handleClear()
}
}, { immediate: true })
onMounted(() => {
// 初始化
})
// 复制坐标
const copyCoordinates = async () => {
if (!state.selectedLocation) return
const coordinates = `${state.selectedLocation.lng},${state.selectedLocation.lat}`
try {
await navigator.clipboard.writeText(coordinates)
ElMessage.success('坐标已复制到剪贴板')
} catch (error) {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = coordinates
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
ElMessage.success('坐标已复制到剪贴板')
}
}
// 暴露方法
defineExpose({
open: handleMapClick,
clear: handleClear,
getLocation: () => state.selectedLocation,
getAddress: () => state.selectAddress,
getData: () => state.selectedData
})
</script>
<style scoped>
.map-select-container {
width: 100%;
}
.map-select-input {
width: 100%;
}
.map-select-input :deep(.el-input__inner) {
cursor: pointer;
}
.location-icon {
color: #409eff;
}
.map-button {
border: none;
background: #409eff;
color: white;
padding: 0 16px;
display: flex;
align-items: center;
gap: 4px;
transition: background-color 0.2s;
}
.map-button:hover {
background: #337ecc;
}
.map-button:disabled {
background: #c0c4cc;
cursor: not-allowed;
}
.coordinates-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 8px 12px;
background: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
font-size: 12px;
}
.coordinates-text {
color: #606266;
flex: 1;
}
.copy-btn {
padding: 4px 8px;
height: auto;
font-size: 12px;
}
.copy-btn:hover {
color: #409eff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.map-button {
padding: 0 12px;
font-size: 12px;
}
.coordinates-info {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.copy-btn {
align-self: flex-end;
}
}
</style>
+2
View File
@@ -13,6 +13,7 @@ import VxeUITable from "vxe-table"
import "vxe-table/lib/style.css"
import VxeUIPluginRenderElement from "@vxe-ui/plugin-render-element"
import "@vxe-ui/plugin-render-element/dist/style.css"
import Tmap from "@map-component/vue-tmap"
// Initialize anti-debug protection
initAntiDebug()
@@ -37,4 +38,5 @@ app
.use(i18n) // 国际化
.use(VxeUI)
.use(VxeUITable)
.use(Tmap) // 腾讯地图
.mount("#app")
+187
View File
@@ -0,0 +1,187 @@
/**
* JSONP 工具函数
* 用于跨域请求
*/
interface JsonpOptions {
timeout?: number
callbackName?: string
callbackParam?: string
debug?: boolean
}
/**
* JSONP 请求
* @param url 请求地址
* @param options 配置选项
* @returns Promise
*/
export function jsonp(url: string, options: JsonpOptions = {}): Promise<any> {
const {
timeout = 15000,
callbackName = `jsonp_callback_${Date.now()}_${Math.random().toString(36).substr(2)}`,
callbackParam = "callback",
debug = false
} = options
if (debug) {
console.log('[JSONP] 发起请求:', url)
console.log('[JSONP] 回调函数名:', callbackName)
}
return new Promise((resolve, reject) => {
// 创建 script 标签
const script = document.createElement("script")
// 设置超时
const timer = setTimeout(() => {
cleanup()
const error = new Error(`JSONP request timeout after ${timeout}ms`)
if (debug) {
console.error('[JSONP] 请求超时:', error)
}
reject(error)
}, timeout)
// 清理函数
const cleanup = () => {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
if ((window as any)[callbackName]) {
delete (window as any)[callbackName]
}
clearTimeout(timer)
}
// 设置全局回调函数
;(window as any)[callbackName] = (data: any) => {
if (debug) {
console.log('[JSONP] 收到响应:', data)
}
cleanup()
resolve(data)
}
// 处理错误
script.onerror = (event) => {
cleanup()
const error = new Error("JSONP request failed to load script")
if (debug) {
console.error('[JSONP] 脚本加载失败:', event)
}
reject(error)
}
// 构建请求 URL
const separator = url.includes("?") ? "&" : "?"
const requestUrl = `${url}${separator}${callbackParam}=${callbackName}`
if (debug) {
console.log('[JSONP] 完整请求URL:', requestUrl)
}
// 发起请求
script.src = requestUrl
script.async = true
document.head.appendChild(script)
})
}
/**
* 腾讯地图 API JSONP 请求封装
*/
export class TencentMapAPI {
private apiKey: string
private debug: boolean
constructor(apiKey: string, debug: boolean = false) {
this.apiKey = apiKey
this.debug = debug
}
/**
* 逆地址解析
* @param lat 纬度
* @param lng 经度
* @returns Promise
*/
async reverseGeocode(lat: number, lng: number): Promise<any> {
const url = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${this.apiKey}&get_poi=1&output=jsonp`
if (this.debug) {
console.log('[TencentMapAPI] 逆地址解析请求:', { lat, lng })
}
try {
const data = await jsonp(url, { debug: this.debug })
if (this.debug) {
console.log('[TencentMapAPI] 逆地址解析响应:', data)
}
return data
} catch (error) {
console.error("逆地址解析失败:", error)
throw error
}
}
/**
* 地址搜索
* @param keyword 搜索关键词
* @param boundary 搜索范围
* @returns Promise
*/
async searchLocation(keyword: string, boundary: string = "region(全国,0)"): Promise<any> {
const url = `https://apis.map.qq.com/ws/place/v1/search?boundary=${encodeURIComponent(boundary)}&keyword=${encodeURIComponent(
keyword,
)}&page_size=10&page_index=1&key=${this.apiKey}&output=jsonp`
if (this.debug) {
console.log('[TencentMapAPI] 地址搜索请求:', { keyword, boundary })
}
try {
const data = await jsonp(url, { debug: this.debug })
if (this.debug) {
console.log('[TencentMapAPI] 地址搜索响应:', data)
}
return data
} catch (error) {
console.error("地址搜索失败:", error)
throw error
}
}
/**
* 地址解析(正向地理编码)
* @param address 地址
* @returns Promise
*/
async geocode(address: string): Promise<any> {
const url = `https://apis.map.qq.com/ws/geocoder/v1/?address=${encodeURIComponent(address)}&key=${this.apiKey}&output=jsonp`
try {
const data = await jsonp(url)
return data
} catch (error) {
console.error("地址解析失败:", error)
throw error
}
}
/**
* POI 搜索
* @param keyword 搜索关键词
* @param boundary 搜索范围
* @returns Promise
*/
async searchPOI(keyword: string, boundary: string = "region(全国,0)"): Promise<any> {
return this.searchLocation(keyword, boundary)
}
}
export default jsonp
+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)
+179
View File
@@ -0,0 +1,179 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="120px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="门店名称" prop="name">
<el-input v-model="form.name" placeholder="请输入门店名称" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门店logo" prop="logo">
<upload-img v-model:imageUrl="form.logo" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门店联系手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入门店联系手机号" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="营业时间" prop="businessHoursJson">
<BusinessHours v-model="form.businessHoursJson" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="合作商户" prop="mchIds">
<select-mch multiple v-model="form.mchIds" placeholder="请选择合作商户" />
</el-form-item>
</el-col>
<el-col :span="24" v-if="false" class="mb20">
<el-form-item label="门店位置" prop="location">
<MapSelect />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item prop="sort">
<template #label>
<span>排序:</span>
<tip content="越大越在前" />
</template>
<el-input-number v-model="form.sort" placeholder="" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门店简介" prop="introduction">
<el-input v-model="form.introduction" placeholder="请输入门店简介" type="textarea" :autosize="{ minRows: 2 }" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门店详细地址" prop="detailAddress">
<el-input v-model="form.detailAddress" placeholder="请输入门店详细地址" type="textarea" :autosize="{ minRows: 2 }" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="MchPickUpDialog">
import { useMessage } from "/@/hooks/message";
import * as mchPickUpApi from '/@/api/wallet/mch-pick-up'
import BusinessHours from '/@/components/BusinessHours/index.vue'
// 定义营业时间接口
interface BusinessHour {
id: string
title: string
startTime: string
endTime: string
}
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
// 定义字典
// 提交表单数据
const form = reactive({
id: '',
name: '',
logo: '',
introduction: '',
phone: '',
status: '',
latitude: '',
longitude: '',
detailAddress: '',
sort: 0,
businessHoursJson: [] as BusinessHour[],
mchIds: [],
});
// 定义校验规则
const dataRules = ref({
name: [{ required: true, message: "门店名称不能为空", trigger: "blur" }],
})
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true
form.id = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 重置营业时间数据
form.businessHoursJson = []
// 获取mchPickUp信息
if (id) {
form.id = id
getmchPickUpData(id)
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => { });
if (!valid) return false;
try {
loading.value = true;
const _data: any = { ...form }
if (_data.businessHoursJson && _data.businessHoursJson.length) {
_data.businessHoursJson = JSON.stringify(_data.businessHoursJson)
}
form.id ? await mchPickUpApi.putObj(_data) : await mchPickUpApi.addObj(_data);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getmchPickUpData = (id: string) => {
// 获取数据
loading.value = true
mchPickUpApi.getObj(id).then((res: any) => {
Object.assign(form, res.data)
if (res.data.businessHoursJson) {
try {
form.businessHoursJson = JSON.parse(res.data.businessHoursJson)
} catch (error) {
console.warn('解析营业时间JSON失败:', error)
form.businessHoursJson = []
}
}
}).finally(() => {
loading.value = false
})
};
// 暴露变量
defineExpose({
openDialog
});
</script>
+167
View File
@@ -0,0 +1,167 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-form v-if="showSearch" ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-row>
<el-form-item style="width: 250px" label="门店名称" prop="name">
<el-input v-model="state.queryForm.name" placeholder="请输入" />
</el-form-item>
<el-form-item style="width: 250px" label="门店联系手机号" prop="phone">
<el-input v-model="state.queryForm.phone" placeholder="请输入" />
</el-form-item>
<el-form-item style="width: 250px" label="合作状态" prop="status">
<dict-select v-model="state.queryForm.status" dict-code="mch_pick_up_status_enum" />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">
{{ $t("common.queryBtn") }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
</el-form-item>
</el-row>
</el-form>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
v-auth="'mch_mchPickUp_add'">
新 增
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'mch_mchPickUp_del'"
@click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'mch_mchPickUp_export'" @exportExcel="exportExcel"
class="ml10 mr20" style="float: right;" @queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
@sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="name" label="门店名称" show-overflow-tooltip />
<el-table-column prop="logo" label="门店logo">
<template #default="{ row }">
<preview-image v-if="row.logo" :src="row.logo" />
<div style="width: 50px;height: 50px;" v-else />
</template>
</el-table-column>
<el-table-column prop="phone" label="门店手机" show-overflow-tooltip />
<el-table-column prop="status" label="门店状态" width="200">
<template #default="{ row }">
<el-switch active-value="20" inactive-value="10" active-text="正在合作" inactive-text="未合作" v-model="row.status"
@change="statusChange(row)" />
</template>
</el-table-column>
<el-table-column prop="detailAddress" label="门店详细地址" show-overflow-tooltip />
<el-table-column prop="businessHoursJson" label="营业时间">
<template #default="{ row }">
<div v-for="v in JSON.parse(row.businessHoursJson)" :key="v">
<span>{{ v.title }}</span>
<span>:</span>
<span>{{ v.startTime }}</span>
<span>-</span>
<span>{{ v.endTime }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'mch_mchPickUp_edit'"
@click="formDialogRef.openDialog(scope.row.id)">编辑</el-button>
<el-button icon="delete" text type="primary" v-auth="'mch_mchPickUp_del'"
@click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="systemMchPickUp">
import { BasicTableProps, useTable } from "/@/hooks/table";
import * as mchPickUpApi from '/@/api/wallet/mch-pick-up'
import { useMessage, useMessageBox } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
// 定义查询字典
const { mch_pick_up_status_enum } = useDict("mch_pick_up_status_enum")
// 定义变量内容
const formDialogRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: mchPickUpApi.fetchList
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile,
tableStyle
} = useTable(state)
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
// 清空多选
selectObjs.value = []
getDataList()
}
// 导出excel
const exportExcel = () => {
downBlobFile('/mch/mch-pick-up/export', Object.assign(state.queryForm, { ids: selectObjs }), 'mchPickUp.xlsx')
}
// 多选事件
const selectionChangHandle = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
const statusChange = async (row: any) => {
try {
await mchPickUpApi.putObj(row)
getDataList()
useMessage().success("操作成功")
} catch (e) {
useMessage().error("操作错误")
}
}
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await mchPickUpApi.delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>
@@ -3,7 +3,7 @@
v-loading="state.loading">
<!-- **************************** start 喜爱菜品排行 start **************************** -->
<el-divider class="my-12">喜爱菜品排行配置</el-divider>
<el-form-item prop="cleaningData.carGap">
<el-form-item prop="nutritionSetting.carGap">
<template #label>
<div class="flex items-center gap-1">
<span>需要排除的菜品分类</span>
@@ -11,7 +11,36 @@
</template>
<select-ctcg v-model="form.nutritionSetting.topExcludeCategoryIds" multiple placeholder="请选择需要排除的菜品分类" />
</el-form-item>
<!-- **************************** end 同步间隔配置 end **************************** -->
<!-- **************************** end 喜爱菜品排行 end **************************** -->
<!-- **************************** start 营养分析 start **************************** -->
<el-divider class="my-12">营养分析配置</el-divider>
<el-form-item prop="nutritionSetting.hcCategoryIds">
<template #label>
<div class="flex items-center gap-1">
<span>荤菜菜品分类</span>
</div>
</template>
<select-ctcg v-model="form.nutritionSetting.hcCategoryIds" multiple placeholder="请选择荤菜菜品分类" />
</el-form-item>
<el-form-item prop="nutritionSetting.scCategoryIds">
<template #label>
<div class="flex items-center gap-1">
<span>素菜菜品分类</span>
</div>
</template>
<select-ctcg v-model="form.nutritionSetting.scCategoryIds" multiple placeholder="请选择素菜菜品分类" />
</el-form-item>
<el-form-item prop="nutritionSetting.zsCategoryIds">
<template #label>
<div class="flex items-center gap-1">
<span>主食菜品分类</span>
</div>
</template>
<select-ctcg v-model="form.nutritionSetting.zsCategoryIds" multiple placeholder="请选择主食菜品分类" />
</el-form-item>
<!-- **************************** end 营养分析 end **************************** -->
</el-form>
</template>
@@ -31,12 +60,21 @@ const form = reactive<any>({
nutritionSetting: {
// 喜爱菜品排行需要排除的菜品分类
topExcludeCategoryIds: [],
// 荤菜菜品分类
hcCategoryIds: [],
// 素菜菜品分类
scCategoryIds: [],
// 主食菜品分类
zsCategoryIds: [],
},
})
const dataRules = ref({
// 车辆考勤配置
"nutritionSetting.topExcludeCategoryIds": [{ required: true, message: '喜爱菜品排行需要排除的菜品分类不能为空', trigger: 'blur' }],
"nutritionSetting.hcCategoryIds": [{ required: true, message: '荤菜菜品分类不能为空', trigger: 'blur' }],
"nutritionSetting.scCategoryIds": [{ required: true, message: '素菜菜品分类不能为空', trigger: 'blur' }],
"nutritionSetting.zsCategoryIds": [{ required: true, message: '主食菜品分类不能为空', trigger: 'blur' }],
})
onMounted(() => {
@@ -50,6 +88,9 @@ async function getSetting() {
if (!data.nutritionSetting) {
data.nutritionSetting = {
topExcludeCategoryIds: [],
hcCategoryIds: [],
scCategoryIds: [],
zsCategoryIds: [],
}
}
if (data) {
@@ -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>
+208
View File
@@ -0,0 +1,208 @@
<template>
<div class="test-jsonp-page">
<h2>JSONP 跨域测试页面</h2>
<div class="test-section">
<h3>1. 逆地址解析测试</h3>
<el-button @click="testReverseGeocode" type="primary">
测试逆地址解析 (北京天安门)
</el-button>
<div v-if="reverseResult" class="result">
<p><strong>结果:</strong> {{ reverseResult }}</p>
</div>
</div>
<div class="test-section">
<h3>2. 地址搜索测试</h3>
<el-input
v-model="searchKeyword"
placeholder="输入搜索关键词"
style="width: 300px; margin-right: 10px;"
/>
<el-button @click="testSearch" type="primary">搜索</el-button>
<div v-if="searchResults.length > 0" class="result">
<p><strong>搜索结果:</strong></p>
<ul>
<li v-for="(item, index) in searchResults" :key="index">
{{ item.title }} - {{ item.address }}
</li>
</ul>
</div>
</div>
<div class="test-section">
<h3>3. 地图选择器组件测试</h3>
<map-select
ref="mapSelectRef"
v-model="selectedLocation"
placeholder="点击选择位置"
style="width: 400px;"
/>
<div v-if="selectedLocation" class="result">
<p><strong>选中位置:</strong></p>
<p>地址: {{ selectedLocation.address }}</p>
<p>坐标: {{ selectedLocation.coordinates }}</p>
</div>
</div>
<div class="logs">
<h3>日志信息</h3>
<div class="log-content">
<div v-for="(log, index) in logs" :key="index" class="log-item">
{{ log }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElButton, ElInput, ElMessage } from 'element-plus'
import { TencentMapAPI } from '../utils/jsonp'
import MapSelect from '../components/map/map-select.vue'
// 创建腾讯地图API实例 (启用调试模式)
const mapAPI = new TencentMapAPI('CGABZ-3MH66-6VGST-MEMS3-K6U3V-DGBKA', true)
// 定义类型
interface LocationData {
address: string
coordinates: string
location: {
lat: number
lng: number
}
}
// 响应式数据
const reverseResult = ref('')
const searchKeyword = ref('北京大学')
const searchResults = ref<any[]>([])
const selectedLocation = ref<LocationData | null>(null)
const logs = ref<string[]>([])
// 添加日志
const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString()
logs.value.unshift(`[${timestamp}] ${message}`)
if (logs.value.length > 10) {
logs.value.pop()
}
}
// 测试逆地址解析
const testReverseGeocode = async () => {
addLog('开始测试逆地址解析...')
try {
// 北京天安门坐标
const data = await mapAPI.reverseGeocode(39.908823, 116.397470)
if (data.status === 0) {
reverseResult.value = data.result.address
addLog(`逆地址解析成功: ${data.result.address}`)
ElMessage.success('逆地址解析成功')
} else {
addLog(`逆地址解析失败: ${data.message}`)
ElMessage.error('逆地址解析失败')
}
} catch (error) {
addLog(`逆地址解析错误: ${error}`)
ElMessage.error('逆地址解析错误')
console.error('逆地址解析错误:', error)
}
}
// 测试地址搜索
const testSearch = async () => {
if (!searchKeyword.value.trim()) {
ElMessage.warning('请输入搜索关键词')
return
}
addLog(`开始搜索: ${searchKeyword.value}`)
try {
const data = await mapAPI.searchLocation(searchKeyword.value)
if (data.status === 0) {
searchResults.value = data.data.map((item: any) => ({
title: item.title,
address: item.address,
location: item.location
}))
addLog(`搜索成功,找到 ${searchResults.value.length} 个结果`)
ElMessage.success(`搜索成功,找到 ${searchResults.value.length} 个结果`)
} else {
addLog(`搜索失败: ${data.message}`)
ElMessage.error('搜索失败')
}
} catch (error) {
addLog(`搜索错误: ${error}`)
ElMessage.error('搜索错误')
console.error('搜索错误:', error)
}
}
</script>
<style scoped>
.test-jsonp-page {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
}
.test-section h3 {
margin-top: 0;
color: #409eff;
}
.result {
margin-top: 15px;
padding: 10px;
background-color: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
}
.result ul {
margin: 5px 0;
padding-left: 20px;
}
.logs {
margin-top: 30px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #f8f9fa;
}
.logs h3 {
margin-top: 0;
color: #67c23a;
}
.log-content {
max-height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.log-item {
padding: 2px 0;
border-bottom: 1px solid #eee;
}
.log-item:last-child {
border-bottom: none;
}
</style>
+72 -36
View File
@@ -1,16 +1,9 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<template #default>
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-row>
@@ -24,7 +17,8 @@
<dict-select v-model="state.queryForm.type" dict-code="wallet_biz_recharge" />
</el-form-item>
<el-form-item style="width: 300px" label="充值时间" prop="createTimeList">
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
type="daterange" />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">
@@ -38,28 +32,18 @@
</el-form>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
:search="false"
v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
<right-toolbar :search="false" v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
style="float: right" @queryTable="getDataList">
<el-tooltip class="item" effect="dark" content="统计" placement="top">
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
</el-tooltip>
</right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:summary-method="getSummaries"
show-summary
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="username" label="用户" />
@@ -97,29 +81,56 @@
<el-table-column prop="createTime" width="200" label="充值时间" />
<el-table-column v-if="false" label="操作" width="150">
<template #default="scope">
<el-button icon="delete" text type="primary" v-auth="'wallet_walletRecharge_del'" @click="handleDelete([scope.row.id])"
>删除</el-button
>
<el-button icon="delete" text type="primary" v-auth="'wallet_walletRecharge_del'"
@click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</template>
</DraggableDepTree>
</div>
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<el-drawer v-model="showDrawer" @closed="drawerClose">
<el-table :data="[balanceStatistics]" style="width: 100%">
<el-table-column align="center" label="充值统计">
<el-table-column align="center" prop="total" label="充值金额">
<template #default="{ row }">
{{ displayPrice(row.totalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="eatTotalPrice" label="饭卡钱包充值金额">
<template #default="{ row }">
{{ displayPrice(row.eatTotalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="xfTotalPrice" label="消费钱包充值金额">
<template #default="{ row }">
{{ displayPrice(row.xfTotalPrice) }}
</template>
</el-table-column>
</el-table-column>
</el-table>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageCz, delObjs } from "/@/api/wallet/wallet-transaction"
import { pageCz, czTotal, delObjs } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import { deptTree } from "/@/api/admin/dept"
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
const balanceStatistics = reactive<any>({
})
// 引入组件
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
// 定义查询字典
@@ -154,6 +165,31 @@ const resetQuery = () => {
getDataList()
}
// 关闭抽屉
const drawerClose = () => {
for (const key in balanceStatistics) {
balanceStatistics[key] = 0
}
}
// 余额统计
const viewBalanceStatistics = async () => {
showDrawer.value = true
balanceStatisticsLoading.value = true
try {
const { code, data } = await czTotal({ ...state.queryForm })
if (code == 0) {
Object.assign(balanceStatistics, data)
return
}
return useMessage().error(data.msg || "获取数据失败")
} catch (error: any) {
useMessage().error(error.msg)
} finally {
balanceStatisticsLoading.value = false
}
}
const getSummaries = (param: any) => {
const { columns, data } = param
const totalColumns = ["price", "balance"]
+71 -33
View File
@@ -1,16 +1,9 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<template #default>
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-row>
@@ -21,7 +14,8 @@
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
</el-form-item>
<el-form-item style="width: 300px" label="消费时间" prop="createTimeList">
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
type="daterange" />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">
@@ -35,28 +29,16 @@
</el-form>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
:search="false"
v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
<right-toolbar :search="false" v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
style="float: right" @queryTable="getDataList">
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
</right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:summary-method="getSummaries"
show-summary
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="username" label="用户" show-overflow-tooltip />
@@ -88,21 +70,51 @@
</el-table-column>
<el-table-column prop="createTime" width="200" label="记账扣款时间" />
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</template>
</DraggableDepTree>
</div>
<el-drawer v-model="showDrawer" @closed="drawerClose">
<el-table :data="[balanceStatistics]" style="width: 100%">
<el-table-column align="center" label="记账扣款统计">
<el-table-column align="center" prop="total" label="记账扣款金额">
<template #default="{ row }">
{{ displayPrice(row.totalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="eatTotalPrice" label="饭卡钱包记账扣款金额">
<template #default="{ row }">
{{ displayPrice(row.eatTotalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="xfTotalPrice" label="消费钱包记账扣款金额">
<template #default="{ row }">
{{ displayPrice(row.xfTotalPrice) }}
</template>
</el-table-column>
</el-table-column>
</el-table>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageJz } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { pageJz, jzTotal } from "/@/api/wallet/wallet-transaction"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
const { push } = useRouter()
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
const balanceStatistics = reactive<any>({
})
// 定义查询字典
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
// 搜索变量
@@ -133,6 +145,32 @@ const resetQuery = () => {
getDataList()
}
// 关闭抽屉
const drawerClose = () => {
for (const key in balanceStatistics) {
balanceStatistics[key] = 0
}
}
// 余额统计
const viewBalanceStatistics = async () => {
showDrawer.value = true
balanceStatisticsLoading.value = true
try {
const { code, data } = await jzTotal({ ...state.queryForm })
if (code == 0) {
Object.assign(balanceStatistics, data)
return
}
return useMessage().error(data.msg || "获取数据失败")
} catch (error: any) {
useMessage().error(error.msg)
} finally {
balanceStatisticsLoading.value = false
}
}
// 导出excel
const exportExcel = () => {
postDownBlobFile(
@@ -1,16 +1,9 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item style="width: 250px" label="用户" prop="userId">
<select-user v-model="state.queryForm.userId" />
@@ -22,10 +15,12 @@
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_rule_transation_type" />
</el-form-item>
<el-form-item style="width: 350px" label="转账时间" prop="createTimeList">
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
type="daterange" />
</el-form-item>
<el-form-item style="width: 350px" label="退回时间" prop="refundTimeList">
<el-date-picker v-model="state.queryForm.refundTimeList" value-format="YYYY-MM-DD" type="daterange" />
<el-date-picker v-model="state.queryForm.refundTimeList" value-format="YYYY-MM-DD"
type="daterange" />
</el-form-item>
<el-row>
<el-form-item>
@@ -40,25 +35,14 @@
</el-form>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
v-model:showSearch="showSearch"
:export="'wallet_walletRuleTransaction_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
<!-- :export="'wallet_walletRuleTransaction_export'" -->
<right-toolbar v-model:showSearch="showSearch" @exportExcel="exportExcel" class="ml10 mr20"
style="float: right" @queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
@sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="80" />
<el-table-column label="用户">
@@ -126,22 +110,18 @@
<el-table-column prop="refundTime" width="170" label="退回时间" />
</el-table-column>
<el-table-column prop="createTime" width="170" label="创建时间" />
<el-table-column label="操作" fixed="right" width="80" v-auths="['wallet_walletRuleTransaction_refund']">
<el-table-column label="操作" fixed="right" width="80"
v-auths="['wallet_walletRuleTransaction_refund']">
<template #default="{ row }">
<el-button
v-if="row.isRefund == '0'"
icon="RefreshRight"
text
type="primary"
v-auth="'wallet_walletRuleTransaction_refund'"
@click="doRefund(row)"
>
<el-button v-if="row.isRefund == '0'" icon="RefreshRight" text type="primary"
v-auth="'wallet_walletRuleTransaction_refund'" @click="doRefund(row)">
退回
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</DraggableDepTree>
</div>
+56 -2
View File
@@ -28,7 +28,9 @@
<div class="mb8" style="width: 100%">
<right-toolbar :search="false" v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
style="float: right" @queryTable="getDataList"></right-toolbar>
style="float: right" @queryTable="getDataList">
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
</right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
@@ -78,17 +80,44 @@
</template>
</DraggableDepTree>
</div>
<el-drawer v-model="showDrawer" @closed="drawerClose">
<el-table :data="[balanceStatistics]" style="width: 100%">
<el-table-column align="center" label="退还各自食堂统计">
<el-table-column align="center" prop="total" label="退还各自食堂金额">
<template #default="{ row }">
{{ displayPrice(row.totalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="eatTotalPrice" label="饭卡钱包记账扣款金额">
<template #default="{ row }">
{{ displayPrice(row.eatTotalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="xfTotalPrice" label="消费钱包记账扣款金额">
<template #default="{ row }">
{{ displayPrice(row.xfTotalPrice) }}
</template>
</el-table-column>
</el-table-column>
</el-table>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageTh } from "/@/api/wallet/wallet-transaction"
import { pageTh, thTotal } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import * as hardAfterSaleApi from "/@/api/hard/hard-after-sale"
import { displayPrice } from "/@/utils/formatMoney"
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
const balanceStatistics = reactive<any>({
})
// 定义查询字典
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
// 定义变量内容
@@ -123,6 +152,31 @@ const resetQuery = () => {
getDataList()
}
// 关闭抽屉
const drawerClose = () => {
for (const key in balanceStatistics) {
balanceStatistics[key] = 0
}
}
// 余额统计
const viewBalanceStatistics = async () => {
showDrawer.value = true
balanceStatisticsLoading.value = true
try {
const { code, data } = await thTotal({ ...state.queryForm })
if (code == 0) {
Object.assign(balanceStatistics, data)
return
}
return useMessage().error(data.msg || "获取数据失败")
} catch (error: any) {
useMessage().error(error.msg)
} finally {
balanceStatisticsLoading.value = false
}
}
const doRefund = async (row: any) => {
const { data } = await hardAfterSaleApi.getObj(row.bizId)
if (!data) {
+70 -42
View File
@@ -1,16 +1,9 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<template #default>
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-row>
@@ -18,7 +11,8 @@
<select-user v-model="state.queryForm.userId" />
</el-form-item>
<el-form-item style="width: 300px" label="退款时间" prop="createTimeList">
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
type="daterange" />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">
@@ -32,28 +26,16 @@
</el-form>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
:search="false"
v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
<right-toolbar :search="false" v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
style="float: right" @queryTable="getDataList">
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
</right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:summary-method="getSummaries"
show-summary
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="username" label="用户" show-overflow-tooltip />
@@ -87,36 +69,57 @@
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
<el-button
v-if="false"
icon="edit-pen"
text
type="primary"
<el-button v-if="false" icon="edit-pen" text type="primary"
v-auth="'wallet_walletRecharge_edit'"
@click="formDialogRef.openDialog(row.id)"
>编辑</el-button
>
@click="formDialogRef.openDialog(row.id)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</template>
</DraggableDepTree>
</div>
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<el-drawer v-model="showDrawer" @closed="drawerClose">
<el-table :data="[balanceStatistics]" style="width: 100%">
<el-table-column align="center" label="充值统计">
<el-table-column align="center" prop="total" label="充值金额">
<template #default="{ row }">
{{ displayPrice(row.totalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="eatTotalPrice" label="饭卡钱包充值金额">
<template #default="{ row }">
{{ displayPrice(row.eatTotalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="xfTotalPrice" label="消费钱包充值金额">
<template #default="{ row }">
{{ displayPrice(row.xfTotalPrice) }}
</template>
</el-table-column>
</el-table-column>
</el-table>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageTk } from "/@/api/wallet/wallet-transaction"
import { pageTk, tkTotal } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import * as hardAfterSaleApi from "/@/api/hard/hard-after-sale"
import { displayPrice } from "/@/utils/formatMoney"
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
const balanceStatistics = reactive<any>({
})
// 引入组件
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
// 定义查询字典
@@ -153,6 +156,31 @@ const resetQuery = () => {
getDataList()
}
// 关闭抽屉
const drawerClose = () => {
for (const key in balanceStatistics) {
balanceStatistics[key] = 0
}
}
// 余额统计
const viewBalanceStatistics = async () => {
showDrawer.value = true
balanceStatisticsLoading.value = true
try {
const { code, data } = await tkTotal({ ...state.queryForm })
if (code == 0) {
Object.assign(balanceStatistics, data)
return
}
return useMessage().error(data.msg || "获取数据失败")
} catch (error: any) {
useMessage().error(error.msg)
} finally {
balanceStatisticsLoading.value = false
}
}
const doRefund = async (row: any) => {
const { data } = await hardAfterSaleApi.getObj(row.bizId)
if (!data) {
+72 -50
View File
@@ -1,16 +1,9 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<template #default>
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-row>
@@ -21,7 +14,8 @@
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
</el-form-item>
<el-form-item style="width: 300px" label="消费时间" prop="createTimeList">
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
type="daterange" />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">
@@ -35,28 +29,16 @@
</el-form>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar
:search="false"
v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
<right-toolbar :search="false" v-model:showSearch="showSearch"
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
style="float: right" @queryTable="getDataList">
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
</right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:summary-method="getSummaries"
show-summary
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="username" label="用户" show-overflow-tooltip />
@@ -90,40 +72,49 @@
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
<el-button
v-if="false"
icon="edit-pen"
text
type="primary"
<el-button v-if="false" icon="edit-pen" text type="primary"
v-auth="'wallet_walletRecharge_edit'"
@click="formDialogRef.openDialog(row.id)"
>编辑</el-button
>
<el-button
v-if="false"
icon="delete"
text
type="primary"
v-auth="'wallet_walletRecharge_del'"
@click="handleDelete([row.id])"
>删除</el-button
>
@click="formDialogRef.openDialog(row.id)">编辑</el-button>
<el-button v-if="false" icon="delete" text type="primary"
v-auth="'wallet_walletRecharge_del'" @click="handleDelete([row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</template>
</DraggableDepTree>
</div>
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<el-drawer v-model="showDrawer" @closed="drawerClose">
<el-table :data="[balanceStatistics]" style="width: 100%">
<el-table-column align="center" label="充值统计">
<el-table-column align="center" prop="total" label="充值金额">
<template #default="{ row }">
{{ displayPrice(row.totalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="eatTotalPrice" label="饭卡钱包充值金额">
<template #default="{ row }">
{{ displayPrice(row.eatTotalPrice) }}
</template>
</el-table-column>
<el-table-column align="center" prop="xfTotalPrice" label="消费钱包充值金额">
<template #default="{ row }">
{{ displayPrice(row.xfTotalPrice) }}
</template>
</el-table-column>
</el-table-column>
</el-table>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageXf } from "/@/api/wallet/wallet-transaction"
import { pageXf, xfTotal } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
@@ -131,6 +122,13 @@ import { deptTree } from "/@/api/admin/dept"
const { push } = useRouter()
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
const balanceStatistics = reactive<any>({
})
// 引入组件
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
// 定义查询字典
@@ -165,6 +163,30 @@ const resetQuery = () => {
getDataList()
}
// 关闭抽屉
const drawerClose = () => {
for (const key in balanceStatistics) {
balanceStatistics[key] = 0
}
}
const viewBalanceStatistics = async () => {
showDrawer.value = true
balanceStatisticsLoading.value = true
try {
const { code, data } = await xfTotal({ ...state.queryForm })
if (code == 0) {
Object.assign(balanceStatistics, data)
return
}
return useMessage().error(data.msg || "获取数据失败")
} catch (error: any) {
useMessage().error(error.msg)
} finally {
balanceStatisticsLoading.value = false
}
}
// 导出excel
const exportExcel = () => {
postDownBlobFile(