1.新增批量导入 2.新增财务报表

This commit is contained in:
gyq
2026-02-04 14:51:29 +08:00
parent a5b11cf4f4
commit 76af0f5a83
20 changed files with 1517 additions and 268 deletions

View File

@@ -10,6 +10,7 @@ VITE_APP_BASE_API=/dev-api
# VITE_APP_API_URL=https://cashier.sxczgkj.com/ # 正式 # VITE_APP_API_URL=https://cashier.sxczgkj.com/ # 正式
VITE_APP_API_URL=http://192.168.1.42/ # 本地 VITE_APP_API_URL=http://192.168.1.42/ # 本地
VITE_APP_API_PHP_URL=http://192.168.1.42:8000 #php抖音美团测试环境 VITE_APP_API_PHP_URL=http://192.168.1.42:8000 #php抖音美团测试环境
VITE_APP_API_PHP_IMPORT_URL=http://192.168.1.42:8789 #本地php批量导入
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws # WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws

View File

@@ -72,6 +72,23 @@ const OrderApi = {
data data
}); });
}, },
// 查询财务报表
financeSts(params: any) {
return request<any>({
url: `${Order_BaseUrl}/admin/finance/sts`,
method: "get",
params
});
},
// 导出财务报表
financeExport(params: any) {
return request<any>({
url: `${Order_BaseUrl}/admin/finance/export`,
method: "get",
params,
responseType: 'blob'
});
},
}; };
export default OrderApi; export default OrderApi;

View File

@@ -3,68 +3,49 @@
<!-- 表格工具栏 --> <!-- 表格工具栏 -->
<div class="flex-x-between mb-[10px]"> <div class="flex-x-between mb-[10px]">
<!-- 左侧工具栏 --> <!-- 左侧工具栏 -->
<div> <div style="display: flex;">
<template v-for="item in toolbar" :key="item"> <template v-for="item in toolbar" :key="item">
<template v-if="typeof item === 'string'"> <template v-if="typeof item === 'string'">
<!-- 新增 --> <!-- 新增 -->
<template v-if="item === 'add'"> <template v-if="item === 'add'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="primary" icon="plus"
v-hasPerm="[`${contentConfig.pageName}:${item}`]" @click="handleToolbar(item)">
type="primary"
icon="plus"
@click="handleToolbar(item)"
>
新增 新增
</el-button> </el-button>
</template> </template>
<!-- 删除 --> <!-- 删除 -->
<template v-else-if="item === 'delete'"> <template v-else-if="item === 'delete'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="danger" icon="delete"
v-hasPerm="[`${contentConfig.pageName}:${item}`]" :disabled="removeIds.length === 0" @click="handleToolbar(item)">
type="danger"
icon="delete"
:disabled="removeIds.length === 0"
@click="handleToolbar(item)"
>
删除 删除
</el-button> </el-button>
</template> </template>
<!-- 导入 --> <!-- 导入 -->
<template v-else-if="item === 'import'"> <template v-else-if="item === 'import'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="default" icon="upload"
v-hasPerm="[`${contentConfig.pageName}:${item}`]" @click="handleToolbar(item)">
type="default"
icon="upload"
@click="handleToolbar(item)"
>
导入 导入
</el-button> </el-button>
</template> </template>
<!-- 导出 --> <!-- 导出 -->
<template v-else-if="item === 'export'"> <template v-else-if="item === 'export'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="default" icon="download"
v-hasPerm="[`${contentConfig.pageName}:${item}`]" @click="handleToolbar(item)">
type="default"
icon="download"
@click="handleToolbar(item)"
>
导出 导出
</el-button> </el-button>
</template> </template>
</template> </template>
<!-- 其他 --> <!-- 其他 -->
<template v-else-if="typeof item === 'object'"> <template v-else-if="typeof item === 'object'">
<el-button <el-button v-if="item.hidden === undefined || item.hidden === false"
v-if="item.hidden === undefined || item.hidden === false" v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" :icon="item.icon" :type="item.type ?? 'default'"
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" @click="handleToolbar(item.name)">
:icon="item.icon"
:type="item.type ?? 'default'"
@click="handleToolbar(item.name)"
>
{{ item.text }} {{ item.text }}
</el-button> </el-button>
</template> </template>
</template> </template>
<!-- 插槽 -->
<slot name="custom" />
</div> </div>
<!-- 右侧工具栏 --> <!-- 右侧工具栏 -->
<div> <div>
@@ -89,68 +70,36 @@
</template> </template>
<!-- 导出 --> <!-- 导出 -->
<template v-else-if="item === 'exports'"> <template v-else-if="item === 'exports'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:export`]" icon="download" circle title="导出"
v-hasPerm="[`${contentConfig.pageName}:export`]" @click="handleToolbar(item)" />
icon="download"
circle
title="导出"
@click="handleToolbar(item)"
/>
</template> </template>
<!-- 导入 --> <!-- 导入 -->
<template v-else-if="item === 'imports'"> <template v-else-if="item === 'imports'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:import`]" icon="upload" circle title="导入"
v-hasPerm="[`${contentConfig.pageName}:import`]" @click="handleToolbar(item)" />
icon="upload"
circle
title="导入"
@click="handleToolbar(item)"
/>
</template> </template>
<!-- 搜索 --> <!-- 搜索 -->
<template v-else-if="item === 'search'"> <template v-else-if="item === 'search'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:query`]" icon="search" circle title="搜索"
v-hasPerm="[`${contentConfig.pageName}:query`]" @click="handleToolbar(item)" />
icon="search"
circle
title="搜索"
@click="handleToolbar(item)"
/>
</template> </template>
</template> </template>
<!-- 其他 --> <!-- 其他 -->
<template v-else-if="typeof item === 'object'"> <template v-else-if="typeof item === 'object'">
<template v-if="item.auth"> <template v-if="item.auth">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" :icon="item.icon" circle
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" :title="item.title" @click="handleToolbar(item.name)" />
:icon="item.icon"
circle
:title="item.title"
@click="handleToolbar(item.name)"
/>
</template> </template>
<template v-else> <template v-else>
<el-button <el-button :icon="item.icon" circle :title="item.title" @click="handleToolbar(item.name)" />
:icon="item.icon"
circle
:title="item.title"
@click="handleToolbar(item.name)"
/>
</template> </template>
</template> </template>
</template> </template>
</div> </div>
</div> </div>
<!-- 列表 --> <!-- 列表 -->
<el-table <el-table ref="tableRef" v-loading="loading" v-bind="contentConfig.table" :data="pageData" :row-key="pk"
ref="tableRef" @selection-change="handleSelectionChange" @filter-change="handleFilterChange">
v-loading="loading"
v-bind="contentConfig.table"
:data="pageData"
:row-key="pk"
@selection-change="handleSelectionChange"
@filter-change="handleFilterChange"
>
<template v-for="col in cols" :key="col"> <template v-for="col in cols" :key="col">
<el-table-column v-if="col.show" v-bind="col"> <el-table-column v-if="col.show" v-bind="col">
<template #default="scope"> <template #default="scope">
@@ -159,24 +108,15 @@
<template v-if="col.prop"> <template v-if="col.prop">
<template v-if="Array.isArray(scope.row[col.prop])"> <template v-if="Array.isArray(scope.row[col.prop])">
<template v-for="(item, index) in scope.row[col.prop]" :key="item"> <template v-for="(item, index) in scope.row[col.prop]" :key="item">
<el-image <el-image :src="item" :preview-src-list="scope.row[col.prop]" :initial-index="index"
:src="item" :preview-teleported="true" :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40
:preview-src-list="scope.row[col.prop]" }px`" />
:initial-index="index"
:preview-teleported="true"
:style="`width: ${col.imageWidth ?? 40}px; height: ${
col.imageHeight ?? 40
}px`"
/>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<el-image <el-image :src="scope.row[col.prop]" :preview-src-list="[scope.row[col.prop]]"
:src="scope.row[col.prop]"
:preview-src-list="[scope.row[col.prop]]"
:preview-teleported="true" :preview-teleported="true"
:style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`" :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`" />
/>
</template> </template>
</template> </template>
</template> </template>
@@ -198,30 +138,20 @@
<template v-else-if="col.templet === 'switch'"> <template v-else-if="col.templet === 'switch'">
<template v-if="col.prop"> <template v-if="col.prop">
<!-- pageData.length>0: 解决el-switch组件会在表格初始化的时候触发一次change事件 --> <!-- pageData.length>0: 解决el-switch组件会在表格初始化的时候触发一次change事件 -->
<el-switch <el-switch v-model="scope.row[col.prop]" :active-value="col.activeValue ?? 1"
v-model="scope.row[col.prop]" :inactive-value="col.inactiveValue ?? 0" :inline-prompt="true" :active-text="col.activeText ?? ''"
:active-value="col.activeValue ?? 1" :inactive-text="col.inactiveText ?? ''" :validate-event="false"
:inactive-value="col.inactiveValue ?? 0" :disabled="!hasAuth(`${contentConfig.pageName}:modify`)" @change="
:inline-prompt="true"
:active-text="col.activeText ?? ''"
:inactive-text="col.inactiveText ?? ''"
:validate-event="false"
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)"
@change="
pageData.length > 0 && handleModify(col.prop, scope.row[col.prop], scope.row) pageData.length > 0 && handleModify(col.prop, scope.row[col.prop], scope.row)
" " />
/>
</template> </template>
</template> </template>
<!-- 生成输入框组件 --> <!-- 生成输入框组件 -->
<template v-else-if="col.templet === 'input'"> <template v-else-if="col.templet === 'input'">
<template v-if="col.prop"> <template v-if="col.prop">
<el-input <el-input v-model="scope.row[col.prop]" :type="col.inputType ?? 'text'"
v-model="scope.row[col.prop]"
:type="col.inputType ?? 'text'"
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)" :disabled="!hasAuth(`${contentConfig.pageName}:modify`)"
@blur="handleModify(col.prop, scope.row[col.prop], scope.row)" @blur="handleModify(col.prop, scope.row[col.prop], scope.row)" />
/>
</template> </template>
</template> </template>
<!-- 格式化为价格 --> <!-- 格式化为价格 -->
@@ -253,7 +183,7 @@
{{ {{
scope.row[col.prop] scope.row[col.prop]
? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss") ? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss")
.value .value
: "" : ""
}} }}
</template> </template>
@@ -264,21 +194,15 @@
<template v-if="typeof item === 'string'"> <template v-if="typeof item === 'string'">
<!-- 编辑/删除 --> <!-- 编辑/删除 -->
<template v-if="item === 'edit' || item === 'delete'"> <template v-if="item === 'edit' || item === 'delete'">
<el-button <el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]"
v-hasPerm="[`${contentConfig.pageName}:${item}`]" :type="item === 'edit' ? 'primary' : 'danger'" :icon="item" size="small" link @click="
:type="item === 'edit' ? 'primary' : 'danger'"
:icon="item"
size="small"
link
@click="
handleOperat({ handleOperat({
name: item, name: item,
row: scope.row, row: scope.row,
column: scope.column, column: scope.column,
$index: scope.$index, $index: scope.$index,
}) })
" ">
>
{{ item === "edit" ? "编辑" : "删除" }} {{ item === "edit" ? "编辑" : "删除" }}
</el-button> </el-button>
</template> </template>
@@ -286,63 +210,43 @@
<!-- 其他 --> <!-- 其他 -->
<template v-else-if="typeof item === 'object'"> <template v-else-if="typeof item === 'object'">
<template v-if="item.hidden === undefined || item.hidden === false"> <template v-if="item.hidden === undefined || item.hidden === false">
<el-button <el-button v-if="item.isBtn" v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
v-if="item.isBtn" :icon="item.icon" :type="item.type ?? 'primary'" size="small" link @click="
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
:icon="item.icon"
:type="item.type ?? 'primary'"
size="small"
link
@click="
handleOperat({ handleOperat({
name: item.name, name: item.name,
row: scope.row, row: scope.row,
column: scope.column, column: scope.column,
$index: scope.$index, $index: scope.$index,
}) })
" ">
>
{{ item.text }} {{ item.text }}
</el-button> </el-button>
<el-dropdown style="margin-top: 4px" v-else> <el-dropdown style="margin-top: 4px" v-else>
<el-button <el-button v-if="item.render === undefined || item.render(scope.row)" v-bind="item.auth
v-if="item.render === undefined || item.render(scope.row)" ? { 'v-hasPerm': [`${contentConfig.pageName}:${item.auth}`] }
v-bind=" : {}
item.auth " :icon="item.icon" :type="item.type ?? 'primary'" size="small" link @click="
? { 'v-hasPerm': [`${contentConfig.pageName}:${item.auth}`] }
: {}
"
:icon="item.icon"
:type="item.type ?? 'primary'"
size="small"
link
@click="
handleOperat({ handleOperat({
name: item.name, name: item.name,
row: scope.row, row: scope.row,
column: scope.column, column: scope.column,
$index: scope.$index, $index: scope.$index,
}) })
" ">
>
{{ item.text }} {{ item.text }}
</el-button> </el-button>
<template #dropdown v-if="item.options && item.options.length > 0"> <template #dropdown v-if="item.options && item.options.length > 0">
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item @click="
@click=" handleOperat({
handleOperat({ name: item.name,
name: item.name, row: scope.row,
row: scope.row, column: scope.column,
column: scope.column, $index: scope.$index,
$index: scope.$index, command: opt.command ? opt.command : '',
command: opt.command ? opt.command : '', })
}) " v-for="opt in item.options" :key="opt.value">
"
v-for="opt in item.options"
:key="opt.value"
>
{{ opt.label }} {{ opt.label }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
@@ -364,33 +268,18 @@
<template v-if="showPagination"> <template v-if="showPagination">
<el-scrollbar> <el-scrollbar>
<div class="mt-[12px]"> <div class="mt-[12px]">
<el-pagination <el-pagination v-bind="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
v-bind="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div> </div>
</el-scrollbar> </el-scrollbar>
</template> </template>
<!-- 导出弹窗 --> <!-- 导出弹窗 -->
<el-dialog <el-dialog v-model="exportsModalVisible" :align-center="true" title="导出数据" width="600px" style="padding-right: 0"
v-model="exportsModalVisible" @close="handleCloseExportsModal">
:align-center="true"
title="导出数据"
width="600px"
style="padding-right: 0"
@close="handleCloseExportsModal"
>
<!-- 滚动 --> <!-- 滚动 -->
<el-scrollbar max-height="60vh"> <el-scrollbar max-height="60vh">
<!-- 表单 --> <!-- 表单 -->
<el-form <el-form ref="exportsFormRef" label-width="auto" style="padding-right: var(--el-dialog-padding-primary)"
ref="exportsFormRef" :model="exportsFormData" :rules="exportsFormRules">
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="exportsFormData"
:rules="exportsFormRules"
>
<el-form-item label="文件名" prop="filename"> <el-form-item label="文件名" prop="filename">
<el-input v-model="exportsFormData.filename" clearable /> <el-input v-model="exportsFormData.filename" clearable />
</el-form-item> </el-form-item>
@@ -400,16 +289,10 @@
<el-form-item label="数据源" prop="origin"> <el-form-item label="数据源" prop="origin">
<el-select v-model="exportsFormData.origin"> <el-select v-model="exportsFormData.origin">
<el-option label="当前数据 (当前页的数据)" :value="ExportsOriginEnum.CURRENT" /> <el-option label="当前数据 (当前页的数据)" :value="ExportsOriginEnum.CURRENT" />
<el-option <el-option label="选中数据 (所有选中的数据)" :value="ExportsOriginEnum.SELECTED"
label="选中数据 (所有选中的数据)" :disabled="selectionData.length <= 0" />
:value="ExportsOriginEnum.SELECTED" <el-option label="全量数据 (所有分页的数据)" :value="ExportsOriginEnum.REMOTE"
:disabled="selectionData.length <= 0" :disabled="contentConfig.exportsAction === undefined" />
/>
<el-option
label="全量数据 (所有分页的数据)"
:value="ExportsOriginEnum.REMOTE"
:disabled="contentConfig.exportsAction === undefined"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="字段" prop="fields"> <el-form-item label="字段" prop="fields">
@@ -430,35 +313,17 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 导入弹窗 --> <!-- 导入弹窗 -->
<el-dialog <el-dialog v-model="importModalVisible" :align-center="true" title="导入数据" width="600px" style="padding-right: 0"
v-model="importModalVisible" @close="handleCloseImportModal">
:align-center="true"
title="导入数据"
width="600px"
style="padding-right: 0"
@close="handleCloseImportModal"
>
<!-- 滚动 --> <!-- 滚动 -->
<el-scrollbar max-height="60vh"> <el-scrollbar max-height="60vh">
<!-- 表单 --> <!-- 表单 -->
<el-form <el-form ref="importFormRef" label-width="auto" style="padding-right: var(--el-dialog-padding-primary)"
ref="importFormRef" :model="importFormData" :rules="importFormRules">
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files"> <el-form-item label="文件名" prop="files">
<el-upload <el-upload ref="uploadRef" v-model:file-list="importFormData.files" class="w-full"
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true" :drag="true" :limit="1" :auto-upload="false" :on-exceed="handleFileExceed">
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon> <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text"> <div class="el-upload__text">
<span>将文件拖到此处,或</span> <span>将文件拖到此处,或</span>
@@ -467,13 +332,8 @@
<template #tip> <template #tip>
<div class="el-upload__tip"> <div class="el-upload__tip">
*.xlsx / *.xls *.xlsx / *.xls
<el-link <el-link v-if="contentConfig.importTemplate" type="primary" icon="download" :underline="false"
v-if="contentConfig.importTemplate" @click="handleDownloadTemplate">
type="primary"
icon="download"
:underline="false"
@click="handleDownloadTemplate"
>
下载模板 下载模板
</el-link> </el-link>
</div> </div>
@@ -485,11 +345,7 @@
<!-- 弹窗底部操作按钮 --> <!-- 弹窗底部操作按钮 -->
<template #footer> <template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)"> <div style="padding-right: var(--el-dialog-padding-primary)">
<el-button <el-button type="primary" :disabled="importFormData.files.length === 0" @click="handleImportSubmit">
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleImportSubmit"
>
确 定 确 定
</el-button> </el-button>
<el-button @click="handleCloseImportModal">取 消</el-button> <el-button @click="handleCloseImportModal">取 消</el-button>
@@ -1005,10 +861,10 @@ function fetchPageData(formData: IObject = {}, isRestart = false) {
.indexAction( .indexAction(
showPagination showPagination
? { ? {
[request.pageName]: pagination.currentPage, [request.pageName]: pagination.currentPage,
[request.limitName]: pagination.pageSize, [request.limitName]: pagination.pageSize,
...formData, ...formData,
} }
: formData : formData
) )
.then((data) => { .then((data) => {
@@ -1073,7 +929,7 @@ function saveXlsx(fileData: BlobPart, fileName: string) {
document.body.removeChild(downloadLink); document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl); window.URL.revokeObjectURL(downloadUrl);
} }
function test(rows: any[]) {} function test(rows: any[]) { }
const defaultSelData = ref<IObject[]>([]); const defaultSelData = ref<IObject[]>([]);
// 设置默认选择 // 设置默认选择
@@ -1115,17 +971,21 @@ defineExpose({
:deep(.el-table .el-table__cell) { :deep(.el-table .el-table__cell) {
z-index: inherit; z-index: inherit;
} }
.el-card { .el-card {
overflow: visible; overflow: visible;
} }
:deep(.el-table) { :deep(.el-table) {
overflow: visible; overflow: visible;
.el-table__header-wrapper { .el-table__header-wrapper {
position: sticky; position: sticky;
z-index: calc(var(--el-table-index) + 2); z-index: calc(var(--el-table-index) + 2);
top: 0; top: 0;
} }
} }
:deep(.el-table td.el-table__cell div) { :deep(.el-table td.el-table__cell div) {
vertical-align: middle; vertical-align: middle;
} }

View File

@@ -1,44 +1,42 @@
<!-- 文件上传组件 --> <!-- 文件上传组件 -->
<template> <template>
<div> <div>
<el-upload <el-upload v-model:file-list="fileList" :style="props.style" :before-upload="handleBeforeUpload"
v-model:file-list="fileList" :http-request="handleUpload" :on-progress="handleProgress" :on-success="handleSuccess" :on-error="handleError"
:style="props.style" :accept="props.accept" :limit="props.limit" multiple drag :tip="'支持多个文件上传,单个文件不超过 ' + props.maxFileSize + 'MB'">
:before-upload="handleBeforeUpload" <!-- 拖拽上传区域 -->
:http-request="handleUpload" <div>
:on-progress="handleProgress" <el-icon class="el-icon--upload">
:on-success="handleSuccess" <UploadFilled />
:on-error="handleError" </el-icon>
:accept="props.accept" <div class="el-upload__text">
:limit="props.limit" 将文件拖到此处 <em>点击上传</em>
multiple <br />
> <small>支持格式{{ props.accept }}</small>
<!-- 上传文件按钮 --> </div>
<el-button type="primary" :disabled="fileList.length >= props.limit"> </div>
{{ props.uploadBtnText }}
</el-button>
<!-- 文件列表 --> <!-- 文件列表 -->
<template #file="{ file }"> <template #file="{ file }">
<div class="el-upload-list__item-info"> <div class="el-upload-list__item-info">
<a class="el-upload-list__item-name" @click="handleDownload(file)"> <a class="el-upload-list__item-name" @click="handleDownload(file)">
<el-icon><Document /></el-icon> <el-icon>
<Document />
</el-icon>
<span class="el-upload-list__item-file-name">{{ file.name }}</span> <span class="el-upload-list__item-file-name">{{ file.name }}</span>
<span class="el-icon--close" @click="handleRemove(file.url!)"> <span class="el-icon--close" @click="handleRemove(file.url)">
<el-icon><Close /></el-icon> <el-icon>
<Close />
</el-icon>
</span> </span>
</a> </a>
</div> </div>
</template> </template>
</el-upload> </el-upload>
<el-progress :style="{
<el-progress display: showProgress ? 'inline-flex' : 'none',
:style="{ width: '100%',
display: showProgress ? 'inline-flex' : 'none', marginTop: '10px',
width: '100%', }" :percentage="progressPercent" />
}"
:percentage="progressPercent"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -48,9 +46,14 @@ import {
UploadProgressEvent, UploadProgressEvent,
UploadRequestOptions, UploadRequestOptions,
} from "element-plus"; } from "element-plus";
import { Upload, Document, Close } from "@element-plus/icons-vue";
import CommonApi, { FileInfo, uploadResponse } from "@/api/account/common"; import CommonApi, { FileInfo, uploadResponse } from "@/api/account/common";
const emit = defineEmits<{
"upload-success": [fileInfo: string];
}>();
const props = defineProps({ const props = defineProps({
/** /**
* 请求携带的额外参数 * 请求携带的额外参数
@@ -96,7 +99,6 @@ const props = defineProps({
type: String, type: String,
default: "上传文件", default: "上传文件",
}, },
/** /**
* 样式 * 样式
*/ */
@@ -104,7 +106,7 @@ const props = defineProps({
type: Object, type: Object,
default: () => { default: () => {
return { return {
width: "300px", width: "100%",
}; };
}, },
}, },
@@ -147,6 +149,7 @@ function handleBeforeUpload(file: UploadRawFile) {
ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M"); ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M");
return false; return false;
} }
showProgress.value = true;
return true; return true;
} }
@@ -190,10 +193,13 @@ const handleProgress = (event: UploadProgressEvent) => {
const handleSuccess = (fileInfo: string) => { const handleSuccess = (fileInfo: string) => {
ElMessage.success("上传成功"); ElMessage.success("上传成功");
modelValue.value = [...modelValue.value, fileInfo]; modelValue.value = [...modelValue.value, fileInfo];
emit("upload-success", fileInfo);
showProgress.value = false;
}; };
const handleError = (error: any) => { const handleError = (error: any) => {
ElMessage.error("上传失败"); ElMessage.error("上传失败");
showProgress.value = false;
}; };
/** /**

View File

@@ -0,0 +1,242 @@
<!-- 文件上传组件 -->
<template>
<div>
<el-upload v-model:file-list="fileList" :style="props.style" :before-upload="handleBeforeUpload"
:on-change="handleChange" :accept="accept" multiple drag :tip="'支持多个文件上传,单个文件不超过 ' + props.maxFileSize + 'MB'"
:auto-upload="false">
<!-- 拖拽上传区域 -->
<div>
<el-icon class="el-icon--upload">
<UploadFilled />
</el-icon>
<div class="el-upload__text">
将文件拖到此处 <em>点击上传</em>
<br />
<small>支持格式{{ accept }}</small>
</div>
</div>
<!-- 文件列表 -->
<template #file="{ file }">
<div class="el-upload-list__item-info">
<a class="el-upload-list__item-name" @click="handleDownload(file)">
<el-icon>
<Document />
</el-icon>
<span class="el-upload-list__item-file-name">{{ file.name }}</span>
<span class="el-icon--close" @click="handleRemove(file.url)">
<el-icon>
<Close />
</el-icon>
</span>
</a>
</div>
</template>
</el-upload>
</div>
</template>
<script setup>
import { UploadFilled, Document, Close } from "@element-plus/icons-vue";
const emit = defineEmits({
"file-selected": null,
});
const props = defineProps({
/**
* 文件上传数量限制
*/
limit: {
type: Number,
default: 10,
},
/**
* 单个文件上传大小限制(单位MB)
*/
maxFileSize: {
type: Number,
default: 10,
},
/**
* 上传文件类型
*/
accept: {
type: String,
default: "*",
},
/**
* 选择文件成功的回调函数
*/
successCallback: {
type: Function,
default: null,
},
/**
* 样式
*/
style: {
type: Object,
default: () => {
return {
width: "100%",
};
},
},
});
const fileList = defineModel({
type: Array,
default: () => [],
});
/**
* 文件选择变化
*/
function handleChange(file, fileList) {
if (file.status === 'ready') {
// 如果限制为1个文件移除之前的文件只保留当前新文件
if (props.limit === 1 && fileList.length > 1) {
fileList.splice(0, fileList.length - 1);
}
const rawFile = file.raw;
// 限制文件类型
if (props.accept !== "*") {
const acceptedTypes = props.accept.split(',').map(type => type.trim());
const isAccepted = acceptedTypes.some(type => {
let checkType = type;
if (!type.startsWith('.') && !type.includes('/')) {
checkType = '.' + type;
}
if (checkType.startsWith('.')) {
return rawFile.name.toLowerCase().endsWith(checkType.toLowerCase());
} else if (checkType.includes('*')) {
const [main] = checkType.split('/');
return rawFile.type.startsWith(main + '/');
} else {
return rawFile.type === checkType;
}
});
if (!isAccepted) {
ElMessage.warning("文件类型不符合要求");
// 从 fileList 中移除不符合的文件
fileList.splice(fileList.indexOf(file), 1);
return;
}
}
if (props.successCallback) {
props.successCallback(rawFile);
} else {
emit("file-selected", rawFile);
}
}
}
/**
* 上传前校验
*/
function handleBeforeUpload(file) {
// 限制文件大小
if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传文件不能大于" + props.maxFileSize + "M");
return false;
}
// 限制文件类型
if (props.accept !== "*") {
const acceptedTypes = props.accept.split(',').map(type => type.trim());
const isAccepted = acceptedTypes.some(type => {
if (type.startsWith('.')) {
return file.name.toLowerCase().endsWith(type.toLowerCase());
} else {
return file.type === type;
}
});
if (!isAccepted) {
ElMessage.warning("文件类型不符合要求");
return false;
}
}
return true;
}
/**
* 删除文件
*/
function handleRemove(fileUrl) {
// 从 fileList 中移除
fileList.value = fileList.value.filter(f => f.url !== fileUrl);
}
/**
* 下载文件
*/
function handleDownload(file) {
// 本地文件,不支持下载
}
</script>
<style lang="scss" scoped>
.el-upload-list__item .el-icon--close {
position: absolute;
top: 50%;
right: 5px;
color: var(--el-text-color-regular);
cursor: pointer;
opacity: 0.75;
transition: opacity var(--el-transition-duration);
transform: translateY(-50%);
&:hover {
opacity: 1;
color: var(--el-color-danger);
}
}
:deep(.el-upload-list) {
margin: 10px 0 0 0;
}
:deep(.el-upload-list__item) {
margin: 5px 0;
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 8px 12px;
display: flex;
align-items: center;
transition: all 0.3s ease;
&:hover {
background: #f0f0f0;
border-color: var(--el-color-primary-light-3);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.el-upload-list__item-info {
flex: 1;
a {
display: flex;
align-items: center;
text-decoration: none;
color: var(--el-text-color-primary);
&:hover {
color: var(--el-color-primary);
}
.el-icon {
margin-right: 8px;
color: var(--el-color-primary);
}
.el-upload-list__item-file-name {
flex: 1;
font-size: 14px;
font-weight: 500;
}
.el-icon--close {
margin-left: 8px;
}
}
}
}
</style>

View File

@@ -0,0 +1,252 @@
<!-- 批量导入数据dialog -->
<template>
<div>
<div class="btn_row">
<el-button type="primary" icon="Upload" @click="show">批量导入</el-button>
<el-button icon="Download" @click="downloadTemplateAjax">下载银收客模板</el-button>
</div>
<el-dialog title="批量导入" width="800px" v-model="visible" :close-on-click-modal="false"
:close-on-press-escape="false">
<div class="row">
<tabHeader v-model="tabActive" :list="tabs" />
</div>
<div class="row mt14">
<div class="import_container" v-if="tabActive == 0">
<div class="header_title">第一步选择模板</div>
<div class="row mt14 pb50">
<div class="list">
<div class="item" :class="{ active: platformActive == index }" v-for="(item, index) in platformList"
:key="item.id" @click="selectPlatform(item)">
<img class="img" :src="item.img" alt="">
</div>
</div>
</div>
<div class="header_title">第二步上传文件
<span>单次仅可上传一个文件</span>
</div>
<div class="row mt14">
<GfileUpload v-model="form.files" :accept="platformList[platformActive]?.file_type || ''" :limit="1"
@file-selected="fileSelected" />
</div>
<div class="row mt14">
<div class="footer_wrap">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" :disabled="!form.files.length" :loading="loading" @click="startImportHandle">
<template v-if="!form.files.length">请选择文件</template>
<template v-else>开始导入</template>
</el-button>
</div>
</div>
</div>
<el-table :data="tableData" border stripe v-if="tabActive == 1" height="419px">
<el-table-column prop="file_name" label="文件名称" width="300" />
<el-table-column prop="created_time" label="导入时间" width="200" />
<el-table-column prop="status_text" label="导入状态" width="150">
<template #default="scope">
<el-tag v-if="scope.row.status == 0" type="info" disable-transitions>待处理...</el-tag>
<el-tag v-else-if="scope.row.status == 1" type="warning" disable-transitions>处理中...</el-tag>
<el-tag v-else-if="scope.row.status == 2" type="success" disable-transitions>处理完成</el-tag>
<el-tag v-else-if="scope.row.status == -1" type="danger" disable-transitions>导入失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="importResult" label="导入结果">
<template #default="scope">
<template v-if="scope.row.status == 2">
<div class="column">
<div>
<el-text type="success">成功{{ scope.row.success_num }} </el-text>
</div>
<div>
<el-text type="danger">失败{{ scope.row.fail_num }} </el-text>
</div>
</div>
</template>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { getplatlist, uploadFile, importlist, downloadTemp } from '@/importDataApi/index.js';
import GfileUpload from '../Upload/GfileUpload.vue';
import tabHeader from '@/views/marketing_center/components/tabHeader.vue';
const props = defineProps({
type: {
type: [String, Number],
default: 3, // 3商品 4台桌区域 5台桌 6会员 7菜品销售统计 8台桌销售统计 9订单销售统计
},
});
const platformList = ref([]);
const platformActive = ref(0);
function selectPlatform(item) {
form.value.files = [];
platformActive.value = platformList.value.findIndex(i => i.id === item.id);
}
const tabs = ref([
{ label: '导入数据', name: 'importData' },
{ label: '导入记录', name: 'importRecord' },
]);
const tabActive = ref(0)
watch(tabActive, (newVal) => {
if (newVal === 1) {
getImportRecord();
}
});
const visible = ref(false);
const loading = ref(false);
const form = ref({
files: [],
platform: ''
})
function fileSelected(file) {
console.log('fileSelected', file);
}
// 开始导入
async function startImportHandle() {
try {
form.value.platform = platformList.value[platformActive.value]?.id;
const formData = new FormData();
formData.append('file', form.value.files[0].raw);
formData.append('shop_id', localStorage.getItem('shopId'));
formData.append('type', props.type);
formData.append('platform', form.value.platform);
loading.value = true;
await uploadFile(formData);
ElMessage.success('文件上传成功,正在导入数据,请在导入记录中查看导入结果');
form.value.files = []
tabActive.value = 1
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false;
}, 500);
}
// 获取平台列表
async function getPlatformList() {
try {
const res = await getplatlist({ plat_type: props.type });
platformList.value = res;
} catch (error) {
console.log(error);
}
}
// 导入记录
const tableData = ref([]);
async function getImportRecord() {
try {
const res = await importlist({ shop_id: localStorage.getItem('shopId') });
tableData.value = res;
} catch (error) {
console.log(error);
}
}
// 下载模板
async function downloadTemplateAjax() {
try {
const res = await downloadTemp({ plat_type: props.type });
window.open(res, '_blank');
} catch (error) {
console.log(error);
}
}
onMounted(() => {
getPlatformList()
});
function show() {
visible.value = true;
}
defineExpose({
show,
});
</script>
<style scoped lang="scss">
.btn_row {
display: flex;
gap: 4px;
margin-left: 14px;
}
.header_title {
font-size: 16px;
font-weight: bold;
color: #333333;
display: flex;
align-items: center;
span {
font-size: 12px;
color: #999999;
margin-left: 10px;
}
}
.row {
&.mt14 {
margin-top: 14px;
}
&.pb50 {
padding-bottom: 50px;
}
}
.import_container {
.list {
display: flex;
gap: 24px;
.item {
width: 122px;
height: 42px;
border: 1px solid #fff;
border-radius: 4px;
overflow: hidden;
&:hover {
cursor: pointer;
border-color: var(--el-color-primary);
}
&.active {
border-color: var(--el-color-primary);
}
.img {
width: 100%;
height: 100%;
}
}
}
.footer_wrap {
display: flex;
gap: 14px;
justify-content: flex-end;
}
}
.column {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,57 @@
import request from "@/utils/request-import-php";
/**
* 平台列表
* @param {*} data
* @returns
*/
export function getplatlist(data) {
return request({
url: "plat/getplatlist",
method: "post",
data
});
}
/**
* 导入
* @param {*} data
* @returns
*/
export function uploadFile(data) {
return request({
url: "upload",
method: "post",
headers: {
'Content-Type': 'multipart/form-data',
},
data
});
}
/**
* 导入记录
* @param {*} data
* @returns
*/
export function importlist(data) {
return request({
url: "plat/importlist",
method: "post",
data
});
}
/**
* 下载模板
* @param {*} data
* @returns
*/
export function downloadTemp(data) {
return request({
url: "plat/downloadTemp",
method: "post",
data
});
}

View File

@@ -63,6 +63,14 @@ export const constantRoutes: RouteRecordRaw[] = [
keepAlive: true, keepAlive: true,
}, },
}, },
{
path: "finance",
name: "",
component: () => import("@/views/data/finance.vue"),
meta: {
title: "财务报表"
},
},
{ {
path: "work", path: "work",
name: "workStatistics", name: "workStatistics",

View File

@@ -0,0 +1,131 @@
import axios from "axios";
import router from "@/router";
import { getDouyinToken, getToken } from "@/utils/auth";
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_API_PHP_IMPORT_URL, // api 的 base_url
// baseURL: "/import_api", // api 的 base_url
timeout: 1000 * 20, // 请求超时时间
});
// request拦截器
service.interceptors.request.use(
(config) => {
if (getToken()) {
config.headers["token"] = getToken();
}
if (!config.headers["Content-Type"]) {
config.headers["Content-Type"] = "application/json";
}
return config;
},
(error) => {
// 确保错误回调返回Promise避免状态异常
return Promise.reject(error);
}
);
// response 拦截器
service.interceptors.response.use(
(response) => {
const data = response.data;
console.log(data);
if (data.code == 0) {
ElNotification.error({
title: data.msg,
duration: 5000,
});
return;
}
if (data.code == 439 || data.code == 303) {
ElNotification.error({
title: "请登录",
duration: 5000,
});
return;
}
if (data.code == 4399) {
ElNotification.error({
title: data.msg,
duration: 5000,
});
return data;
}
// if (data.code == 1 && !data.data) {
// ElNotification.success({
// title: data.msg,
// duration: 5000
// })
// return true;
// }
return data.data;
},
(error) => {
console.log(error);
if (axios.isCancel(error)) {
console.log("请求已取消");
ElNotification.error({
title: "请求已取消",
duration: 5000,
});
return Promise.reject("请求已取消");
}
// 兼容blob下载出错json提示
if (
error.response.data instanceof Blob &&
error.response.data.type.toLowerCase().indexOf("json") !== -1
) {
const reader = new FileReader();
reader.readAsText(error.response.data, "utf-8");
reader.onload = function (e) {
const errorMsg = JSON.parse(reader.result).message;
ElNotification.error({
title: errorMsg,
duration: 5000,
});
};
} else {
let code = 0;
try {
code = error.response.data.status;
} catch (e) {
if (error.toString().indexOf("Error: timeout") !== -1) {
ElNotification.error({
title: "网络请求超时",
duration: 5000,
});
return Promise.reject(error);
}
}
console.log(code);
if (code) {
if (code === 401) {
// store.dispatch("LogOut").then(() => {
// // 用户登录界面提示
// Cookies.set("point", 401);
// location.reload();
// });
} else if (code === 403) {
router.push({ path: "/401" });
} else {
const errorMsg = error.response.data.message;
if (errorMsg !== undefined) {
ElNotification.error({
title: errorMsg,
duration: 5000,
});
}
}
} else {
ElNotification.error({
title: "接口请求失败",
duration: 5000,
});
}
}
return Promise.reject(error);
}
);
export default service;

625
src/views/data/finance.vue Normal file
View File

@@ -0,0 +1,625 @@
<template>
<div class="gyq_container">
<div class="row">
<div class="between">
<el-form :model="queryForm" inline>
<el-form-item>
<el-date-picker v-model="queryForm.queryDate" type="date" placeholder="选择日期" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" :disabled-date="disabledFutureDate" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="loading" @click="handleQuery">查询</el-button>
<el-button @click="handleReset" icon="Refresh" :loading="loading">重置</el-button>
</el-form-item>
</el-form>
<el-form inline>
<el-form-item>
<el-select v-model="queryForm.platform" placeholder="选择平台" clearable style="width: 150px;">
<el-option v-for="item in platformList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleExport" icon="Download">导出</el-button>
</el-form-item>
</el-form>
</div>
</div>
<div class="row" v-loading="loading">
<div class="warp" style="width: 400px;">
<div class="card">
<div class="header_title">营业额</div>
<div class="num" style="color: var(--el-color-primary);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.turnover || 0) }}
</div>
<div class="intro">营业额</div>
<div class="pay_wrap">
<div class="item">
<span class="n">{{ tableData.turnover.wechat || 0 }}</span>
<span class="t">微信支付金额</span>
</div>
<div class="item">
<span class="n">{{ tableData.turnover.alipay || 0 }}</span>
<span class="t">支付宝支付金额</span>
</div>
<div class="item">
<span class="n">{{ tableData.turnover.selfScan || 0 }}</span>
<span class="t">主扫收款</span>
</div>
<div class="item">
<span class="n">{{ tableData.turnover.cash || 0 }}</span>
<span class="t">现金收款</span>
</div>
<div class="item">
<span class="n">{{ tableData.turnover.recharge || 0 }}</span>
<span class="t">充值</span>
</div>
<div class="item">
<span class="n">{{ tableData.turnover.owed || 0 }}</span>
<span class="t">挂账</span>
</div>
<div class="item">
<span class="n">{{ tableData.turnover.balance || 0 }}</span>
<span class="t">余额支付</span>
</div>
</div>
<!-- <div class="order_info">
<div class="order_title">
<span class="dot"></span>
<span class="t">订单</span>
</div>
<div class="order_lits">
<div class="item">
<span class="t">订单金额</span>
<span class="n">{{ tableData.order.orderAmount || 0 }}</span>
</div>
<span class="line">|</span>
<div class="item">
<span class="t">订单总数</span>
<span class="n">{{ tableData.order.orderCount || 0 }}</span>
</div>
</div>
</div> -->
</div>
<div class="card">
<div class="header_title">数据统计</div>
<div class="pay_wrap">
<div class="item">
<span class="n">{{ tableData.sts.customerCount || 0 }}</span>
<span class="t">就餐人数</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.orderCount || 0 }}</span>
<span class="t">订单数</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.tableCount || 0 }}</span>
<span class="t">桌台数</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.avgPayAmount || 0 }}</span>
<div class="t">客单价
<el-tooltip class="box-item" effect="dark" content="实付金额(包含现金支付 包含会员支付 包含挂账)/就餐人数没有具体人数时默认一桌按照1人计算"
placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<span class="n">{{ tableData.sts.turnoverRate || 0 }}%</span>
<div class="t">翻台率
<el-tooltip class="box-item" effect="dark" content="(订单数-桌台数)/桌台数*100%" placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<span class="n">{{ tableData.sts.profitAmount || 0 }}</span>
<div class="t">毛利润
<el-tooltip class="box-item" effect="dark" content="(订单实付金额-商品成本)" placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<span class="n">{{ tableData.sts.productCostAmount || 0 }}</span>
<span class="t">商品成本</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.profitRate || 0 }}%</span>
<div class="t">毛利率
<el-tooltip class="box-item" effect="dark" content="(订单实付金额-商品成本)/订单实付金额*100%" placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<span class="n">{{ tableData.sts.netProfitAmount || 0 }}%</span>
<span class="t">净利率</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.netProfitRate || 0 }}</span>
<span class="t">净利润</span>
</div>
</div>
</div>
</div>
<div class="card" style="flex: 1;">
<div class="header_title">商家经营数据</div>
<div class="content">
<div class="card" style="flex: 1;">
<div class="num" style="color: var(--el-color-success);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.discountAmount || 0) }}
</div>
<div class="intro">优惠金额</div>
<div class="item_list">
<div class="item">
<div class="n">
{{ tableData.discount.discountCount || 0 }}
</div>
<div class="label">
优惠笔数
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.newConsumerDiscount || 0 }}
</div>
<div class="label">
新客立减
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.freeCashAmount || 0 }}
</div>
<div class="label">
霸王餐
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.fullMinusAmount || 0 }}
</div>
<div class="label">
满减活动
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.couponAmount || 0 }}
</div>
<div class="label">
优惠券
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.memberDiscount || 0 }}
</div>
<div class="label">
会员折扣
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.pointsDiscountAmount || 0 }}
</div>
<div class="label">
积分抵扣金额
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.discount.orderDiscount || 0 }}
</div>
<div class="label">
订单改价
</div>
</div>
</div>
</div>
<div class="card" style="flex: 1;">
<div class="num" style="color: var(--el-color-danger);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.refundAmount || 0) }}
</div>
<div class="intro">退款金额</div>
<div class="item_list">
<div class="item">
<div class="n">
{{ tableData.refund.onlineRefundAmount || 0 }}
</div>
<div class="label">
线上退款金额
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.refund.cashRefundAmount || 0 }}
</div>
<div class="label">
现金退款金额
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.refund.rechargeRefundAmount || 0 }}
</div>
<div class="label">
充值退款金额
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.refund.onlineRechargeRefundAmount || 0 }}
</div>
<div class="label">
线上充值退款金额
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.refund.cashRechargeRefundAmount || 0 }}
</div>
<div class="label">
现金充值退款金额
</div>
</div>
<div class="item">
<div class="n">
{{ tableData.refund.memberRefundAmount || 0 }}
</div>
<div class="label">
会员退款金额
</div>
</div>
</div>
</div>
<div class="card" style="flex: 1;">
<div class="num" style="color: var(--el-color-warning);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.order.orderAmount || 0) }}
</div>
<div class="intro">订单金额</div>
<div class="item_list">
<div class="item">
<div class="n">
{{ tableData.order.orderCount || 0 }}
</div>
<div class="label">
订单总数
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import dayjs from "dayjs";
import { ref, onMounted } from "vue";
import OrderApi from "@/api/order/order";
import { downloadFile, multiplyAndFormat } from '@/utils'
const queryForm = ref({
queryDate: dayjs().format('YYYY-MM-DD'), // 查询日期 yyyy-MM-dd
platform: 'czg',
shopId: localStorage.getItem('shopId') || '',
mainShopId: ''
});
const platformList = ref([
{ value: 'czg', label: '银收客' },
{ value: 'MTuan', label: '美团' },
{ value: 'CMeMe', label: '菜么么' },
{ value: 'KRuYun', label: '客如云' },
])
// 禁用今天以后的日期
const disabledFutureDate = (time) => {
return dayjs(time).isAfter(dayjs().startOf('day'))
}
// 查询
function handleQuery() {
getData()
}
// 重置
function handleReset() {
queryForm.value.queryDate = dayjs().format('YYYY-MM-DD')
queryForm.value.platform = 'czg'
getData()
}
// 导出
async function handleExport() {
try {
if (!queryForm.value.platform) {
ElMessage.error('请选择导出平台')
return
}
const res = await OrderApi.financeExport(queryForm.value)
downloadFile(res, '财务报表', 'xlsx')
} catch (error) {
console.log(error);
}
}
const loading = ref(false);
const tableData = ref({
turnover: {},
order: {},
discount: {},
refund: {},
sts: {}
});
// 查询财务报表
async function getData() {
try {
loading.value = true
const res = await OrderApi.financeSts(queryForm.value)
tableData.value = res
console.log('tableData.value', tableData.value);
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false
}, 500);
}
onMounted(() => {
getData()
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.between {
flex: 1;
display: flex;
justify-content: space-between;
}
.row {
display: flex;
gap: 14px;
&.mt14 {
margin-top: 14px;
}
}
.warp {
display: flex;
flex-direction: column;
gap: 14px;
}
.card {
border-radius: 12px;
background-color: #fff;
padding: 20px;
.header_title {
font-size: 16px;
color: 333;
font-weight: bold;
}
.content {
background-color: #F8F8F8;
border-radius: 8px;
margin-top: 20px;
padding: 14px;
display: flex;
gap: 14px;
.item_list {
margin-top: 14px;
.item {
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
&:nth-child(odd) {
background-color: #F3F7FA;
}
.n {
font-size: 16px;
color: #333;
font-weight: bold;
}
.label {
font-size: 14px;
color: #666;
}
}
}
}
.num {
font-size: 24px;
font-weight: bold;
display: flex;
align-items: center;
.i {
font-size: 12px;
position: relative;
top: 3px;
}
}
.intro {
font-size: 14px;
color: #999;
}
.pay_wrap {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 0px;
grid-row-gap: 14px;
background-color: #F8F8F8;
padding: 14px;
margin-top: 14px;
border-radius: 8px;
.item {
display: flex;
flex-direction: column;
.n {
font-size: 20px;
font-weight: bold;
color: #333;
}
.t {
font-size: 14px;
color: #666;
display: flex;
align-items: center;
gap: 4px;
}
}
}
.order_info {
border-top: 1px solid #ececec;
padding-top: 14px;
.order_title {
display: flex;
gap: 10px;
align-items: center;
.dot {
--size: 12px;
width: var(--size);
height: var(--size);
border-radius: 50%;
background-color: var(--el-color-danger);
}
.t {
font-size: 16px;
color: #333;
font-weight: bold;
}
}
.order_lits {
display: flex;
gap: 14px;
margin-top: 4px;
.line {
color: #ececec;
}
.item {
display: flex;
align-items: center;
gap: 14px;
.t {
font-size: 14px;
color: #666;
}
.n {
font-size: 16px;
color: #333;
font-weight: bold;
}
}
}
}
}
.waterfall {
/* 核心属性1定义瀑布流列数关键 */
column-count: 3;
/* 核心属性2列之间的间距替代margin避免卡片间距错乱 */
column-gap: 14px;
/* 可选:防止卡片内容被列分割(关键!避免卡片跨列断裂) */
break-inside: avoid;
.waterfall-card {
/* 必须:适配多列布局,避免卡片宽度溢出 */
width: 100%;
/* 卡片间距仅需设置底部外边距上下间距左右间距由column-gap控制 */
margin-bottom: 14px;
padding: 24px;
background-color: #fff;
border-radius: 12px;
/* 配合break-inside: avoid强化卡片不可分割 */
page-break-inside: avoid;
.header-title {
display: flex;
gap: 14px;
align-items: center;
padding-bottom: 10px;
.dot {
--size: 10px;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.t {
font-size: 24px;
color: #000;
font-weight: bold;
}
}
p {
margin-top: 10px;
font-size: 14px;
color: #666;
padding-left: 24px;
}
}
}
.tips {
margin-top: 6px;
font-size: 14px;
color: #999;
padding-left: 24px;
}
</style>

View File

@@ -52,6 +52,7 @@
<span>营业</span> <span>营业</span>
</div> </div>
<div class="u-flex" style="flex-wrap: wrap"> <div class="u-flex" style="flex-wrap: wrap">
<importData :type="9" style="margin-right: 14px;" />
<el-select v-if="isHeadShop == 1 && loginType == 0" v-model="shopId" placeholder="选择分店" <el-select v-if="isHeadShop == 1 && loginType == 0" v-model="shopId" placeholder="选择分店"
style="width: 200px; margin-right: 10px;" @change="shopChange"> style="width: 200px; margin-right: 10px;" @change="shopChange">
<el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" /> <el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" />
@@ -495,6 +496,7 @@
</template> </template>
<script> <script>
import importData from "@/components/importData/index.vue";
import dataSummaryApi from "@/api/order/data-summary"; import dataSummaryApi from "@/api/order/data-summary";
import ShopApi from "@/api/account/shop"; import ShopApi from "@/api/account/shop";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -504,6 +506,7 @@ import { formatDateRange } from './utils/index.js'
import { multiplyAndFormat } from '@/utils/index.js' import { multiplyAndFormat } from '@/utils/index.js'
export default { export default {
name: "home", name: "home",
components: { importData },
data() { data() {
return { return {
multiplyAndFormat, multiplyAndFormat,

View File

@@ -44,6 +44,7 @@
<span v-if="!downloadLoading">导出Excel</span> <span v-if="!downloadLoading">导出Excel</span>
<span v-else>下载中...</span> <span v-else>下载中...</span>
</el-button> </el-button>
<importData :type="7" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@@ -189,6 +190,7 @@
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import importData from "@/components/importData/index.vue";
import saleSummaryApi from "@/api/order/sale-summary"; import saleSummaryApi from "@/api/order/sale-summary";
import categoryApi from "@/api/product/productclassification"; import categoryApi from "@/api/product/productclassification";
import ShopApi from "@/api/account/shop"; import ShopApi from "@/api/account/shop";
@@ -197,6 +199,7 @@ import { downloadFile, multiplyAndFormat } from "@/utils/index";
import { formatDateRange } from './utils/index.js' import { formatDateRange } from './utils/index.js'
export default { export default {
components: { importData },
data() { data() {
return { return {
multiplyAndFormat, multiplyAndFormat,

View File

@@ -27,13 +27,14 @@
<el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" /> <el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" />
</el-select> </el-select>
</div> </div>
<div> <div style="display: flex;">
<el-button type="primary" @click="getTableData">查询</el-button> <el-button type="primary" @click="getTableData">查询</el-button>
<el-button @click="resetHandle">重置</el-button> <el-button @click="resetHandle">重置</el-button>
<el-button icon="download" v-loading="downloadLoading" @click="downloadHandle"> <el-button icon="download" v-loading="downloadLoading" @click="downloadHandle">
<span v-if="!downloadLoading">导出Excel</span> <span v-if="!downloadLoading">导出Excel</span>
<span v-else>下载中...</span> <span v-else>下载中...</span>
</el-button> </el-button>
<importData :type="8" />
</div> </div>
</div> </div>
</el-form> </el-form>
@@ -100,6 +101,7 @@
<script> <script>
import _ from "lodash"; import _ from "lodash";
import importData from "@/components/importData/index.vue";
import tableSummaryApi from "@/api/order/table-summary"; import tableSummaryApi from "@/api/order/table-summary";
import ShopApi from "@/api/account/shop"; import ShopApi from "@/api/account/shop";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -107,6 +109,7 @@ import { downloadFile } from "@/utils/index";
import { formatDateRange } from './utils/index.js' import { formatDateRange } from './utils/index.js'
export default { export default {
components: { importData },
data() { data() {
return { return {
timeValue: "today", timeValue: "today",

View File

@@ -12,7 +12,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick, watch } from 'vue'
const props = defineProps({ const props = defineProps({
// {label: '设置',value: 1} // {label: '设置',value: 1}
@@ -29,10 +29,8 @@ const modelValue = defineModel('modelValue', {
required: true required: true
}) })
// 改变索引 // 更新active_wrap位置
function changeHandle(index) { function updateActivePosition(index) {
modelValue.value = index
let left = 0 let left = 0
itemsWidth.value.forEach((val, i) => { itemsWidth.value.forEach((val, i) => {
if (i < index) { if (i < index) {
@@ -40,7 +38,12 @@ function changeHandle(index) {
} }
}) })
leftValue.value = left + gap.value * index leftValue.value = left + gap.value * index
}
// 改变索引
function changeHandle(index) {
modelValue.value = index
updateActivePosition(index)
emits('change', index) emits('change', index)
} }
@@ -57,10 +60,14 @@ onMounted(() => {
console.log('itemRefs===', itemRefs.value); console.log('itemRefs===', itemRefs.value);
console.log('itemsWidth===', itemsWidth.value); console.log('itemsWidth===', itemsWidth.value);
updateActivePosition(modelValue.value)
changeHandle(modelValue.value)
}) })
}) })
// 监听modelValue变化更新位置
watch(modelValue, (newVal) => {
updateActivePosition(newVal)
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -15,6 +15,9 @@
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag> </el-tag>
</template> --> </template> -->
<template #custom>
<importData :type="3" />
</template>
<template #type="scope"> <template #type="scope">
{{ typeFilter(scope.row[scope.prop]) }} {{ typeFilter(scope.row[scope.prop]) }}
</template> </template>
@@ -170,6 +173,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import importData from "@/components/importData/index.vue";
import UserAPI from "@/api/product/index"; import UserAPI from "@/api/product/index";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import type { IObject, IOperatData } from "@/components/CURD/types"; import type { IObject, IOperatData } from "@/components/CURD/types";
@@ -184,6 +188,8 @@ import searchConfig from "./indexconfig/search";
import MyDialog from "@/components/mycomponents/myDialog.vue"; import MyDialog from "@/components/mycomponents/myDialog.vue";
import Statistics from "./indexconfig/statistics.vue"; import Statistics from "./indexconfig/statistics.vue";
const importDataRef = ref(null);
const { const {
searchRef, searchRef,
contentRef, contentRef,
@@ -302,6 +308,11 @@ function getTongji(params: IObject | undefined) {
}); });
} }
// 显示批量导入
function handleUploadClick() {
importDataRef.value.show()
}
// 导出商品 // 导出商品
async function handleExportClick() { async function handleExportClick() {
try { try {
@@ -386,6 +397,9 @@ async function handleToolbarClick(name: string) {
let res = await UserAPI.sync(); let res = await UserAPI.sync();
ElMessage.success("操作成功,数据正在后台同步中..."); ElMessage.success("操作成功,数据正在后台同步中...");
} }
if (name === "upload") {
importDataRef.value.show()
}
} }
async function confirm() { async function confirm() {
let res = await UserAPI.stockWarning(form.warnLine); let res = await UserAPI.stockWarning(form.warnLine);

View File

@@ -85,7 +85,7 @@ const contentConfig: IContentConfig<UserPageQuery> = {
type: "", type: "",
name: "export", name: "export",
auth: "import", auth: "import",
}, }
], ],
cols: [ cols: [
// { type: "selection", width: 50, align: "center" }, // { type: "selection", width: 50, align: "center" },

View File

@@ -84,10 +84,10 @@ export default {
"https://zhyx.eingdong.com/qrcode/api.php?url=https%3A%2F%2Fzhyx.eingdong.com%2Fcopyright%2F%23%2Fpay%3Fid%3D139451580", "https://zhyx.eingdong.com/qrcode/api.php?url=https%3A%2F%2Fzhyx.eingdong.com%2Fcopyright%2F%23%2Fpay%3Fid%3D139451580",
pays: [ pays: [
{ {
text: "扫", text: "扫码收款",
}, },
{ {
text: "被扫", text: "二维码收款",
}, },
], ],
number: "0", number: "0",

View File

@@ -23,13 +23,18 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class=""> <div class="" style="display: flex;">
<el-button icon="plus" @click="addEaraShow()">添加区域</el-button> <el-button icon="plus" @click="addEaraShow()">添加区域</el-button>
<el-button type="primary" icon="plus" @click="addTableShow()">添加台桌</el-button> <el-button type="primary" icon="plus" @click="addTableShow()">添加台桌</el-button>
<el-button type="danger" icon="Setting" @click="clearTabDialogRef.show()">清台设置</el-button> <el-button type="danger" icon="Setting" @click="clearTabDialogRef.show()">清台设置</el-button>
<el-button type="primary" icon="download" @click="showDownloadTableCode"> <el-button type="primary" icon="download" @click="showDownloadTableCode">
下载桌台码 下载桌台码
</el-button> </el-button>
<!-- 批量导入 -->
<importData :type="5" />
<!-- <el-button icon="Upload" @click="importDataRef.show()">
批量导入
</el-button> -->
<!-- <el-button type="primary" icon="download" @click="downloadShopCpde"> <!-- <el-button type="primary" icon="download" @click="downloadShopCpde">
下载店铺码 下载店铺码
</el-button> --> </el-button> -->
@@ -205,6 +210,7 @@
</template> </template>
<script setup> <script setup>
import importData from "@/components/importData/index.vue";
import status from "./status.js"; import status from "./status.js";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
const shopUser = useUserStore(); const shopUser = useUserStore();

View File

@@ -29,8 +29,11 @@
</div> </div>
<!-- 列表 --> <!-- 列表 -->
<page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick" <page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick"
@edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick" @edit-click="handleEditClick" @export-click="handleExportClick" @upload-click="" @search-click="handleSearchClick"
@toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange"> @toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
<template #custom>
<importData ref="importDataRef" :type="6" />
</template>
<template #status="scope"> <template #status="scope">
<el-link :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-link :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -146,6 +149,7 @@
</template> </template>
<script setup> <script setup>
import importData from "@/components/importData/index.vue";
import UserCouponDialog from "./components/user-coupon-dialog.vue"; import UserCouponDialog from "./components/user-coupon-dialog.vue";
import GiveCoupon from "./components/give-coupon.vue"; import GiveCoupon from "./components/give-coupon.vue";
import usePage from "@/components/CURD/usePage"; import usePage from "@/components/CURD/usePage";
@@ -159,6 +163,7 @@ import shopUserApi from "@/api/account/shopUser";
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
import { downloadFile } from "@/utils/index"; import { downloadFile } from "@/utils/index";
const editMoneyModalRef = ref(null); const editMoneyModalRef = ref(null);
const userCouponDialogRef = ref(null); const userCouponDialogRef = ref(null);
const GiveCouponRef = ref(null); const GiveCouponRef = ref(null);
@@ -250,6 +255,8 @@ function handleToolbarClick(name) {
console.log(name); console.log(name);
if (name === "custom1") { if (name === "custom1") {
ElMessage.success("点击了自定义1按钮"); ElMessage.success("点击了自定义1按钮");
} else if (name === "upload") {
importDataRef.value.show()
} }
} }

View File

@@ -54,6 +54,13 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
target: env.VITE_APP_API_URL, target: env.VITE_APP_API_URL,
rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""),
}, },
// 代理 /dev-api 的请求
'/import_api': {
changeOrigin: true,
// 代理目标地址https://api.youlai.tech
target: env.VITE_APP_API_PHP_IMPORT_URL,
rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_API_PHP_IMPORT_URL), ""),
},
}, },
}, },
plugins: [ plugins: [