@@ -0,0 +1,457 @@ | |||
import request from "@/utils/request"; | |||
export function getProductCodeSuggestions(params) { | |||
return request({ | |||
url: "/reports/product-code-suggestions", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 销售分析模块API接口 | |||
* 包含分析数据、基准数据、分类管理、店铺客户关联、报表分析等功能 | |||
*/ | |||
// ==================== 分析数据管理 ==================== | |||
/** | |||
* 获取分析数据列表 | |||
* @param {Object} params - 查询参数 | |||
* @param {number} params.page - 页码 | |||
* @param {number} params.pageSize - 每页数量 | |||
* @param {string} params.productCode - 商品编号 | |||
* @param {string} params.productName - 商品名称 | |||
* @param {string} params.customerName - 客户名称 | |||
* @param {string} params.shopName - 店铺名称 | |||
* @param {string} params.status - 状态 | |||
* @param {string} params.startDate - 开始日期 | |||
* @param {string} params.endDate - 结束日期 | |||
* @returns {Promise} 返回数据列表和分页信息 | |||
*/ | |||
export function getAnalysisDataList(params) { | |||
return request({ | |||
url: "/analysis-data", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 更新分析数据 | |||
* @param {number} id - 数据ID | |||
* @param {Object} data - 更新数据 | |||
* @returns {Promise} 更新结果 | |||
*/ | |||
export function updateAnalysisData(id, data) { | |||
return request({ | |||
url: `/analysis-data/${id}`, | |||
method: "put", | |||
data, | |||
}); | |||
} | |||
/** | |||
* 删除分析数据 | |||
* @param {number} id - 数据ID | |||
* @returns {Promise} 删除结果 | |||
*/ | |||
export function deleteAnalysisData(id) { | |||
return request({ | |||
url: `/analysis-data/${id}`, | |||
method: "delete", | |||
}); | |||
} | |||
/** | |||
* 批量删除分析数据 | |||
* @param {string} ids - 数据ID列表,逗号分隔 | |||
* @returns {Promise} 批量删除结果 | |||
*/ | |||
export function batchDeleteAnalysisData(ids) { | |||
return request({ | |||
url: `/analysis-data/batch/${ids}`, | |||
method: "delete", | |||
}); | |||
} | |||
/** | |||
* 导出分析数据 | |||
* @param {Object} params - 导出参数 | |||
* @returns {string} 导出文件URL | |||
*/ | |||
export function exportAnalysisData(params) { | |||
const queryString = new URLSearchParams(params).toString(); | |||
return request({ | |||
url: `/analysis-data/export?${queryString}`, | |||
responseType: "blob", | |||
method: "get", | |||
}); | |||
} | |||
// ==================== 数据导入 ==================== | |||
/** | |||
* 批量导入分析数据(物流数据) | |||
* @param {FormData} formData - 包含文件和分类ID的表单数据 | |||
* @returns {Promise} 导入结果 | |||
*/ | |||
export function importAnalysisDataBatch(formData) { | |||
return request({ | |||
url: "/analysis-data/import-batch", | |||
method: "post", | |||
data: formData, | |||
headers: { | |||
"Content-Type": "multipart/form-data", | |||
}, | |||
}); | |||
} | |||
/** | |||
* 批量导入FBA数据 | |||
* @param {FormData} formData - 包含文件和店铺名称的表单数据 | |||
* @returns {Promise} 导入结果 | |||
*/ | |||
export function importFBADataBatch(formData) { | |||
return request({ | |||
url: "/analysis-data/import/fba/batch", | |||
method: "post", | |||
data: formData, | |||
headers: { | |||
"Content-Type": "multipart/form-data", | |||
}, | |||
}); | |||
} | |||
/** | |||
* 保存导入的数据 | |||
* @param {Object} data - 导入的数据 | |||
* @returns {Promise} 保存结果 | |||
*/ | |||
export function saveImportedData(data) { | |||
return request({ | |||
url: "/analysis-data/save-imported", | |||
method: "post", | |||
data, | |||
}); | |||
} | |||
// ==================== 基准数据管理 ==================== | |||
/** | |||
* 获取分类列表(简化版) | |||
* @returns {Promise} 分类列表 | |||
*/ | |||
export function getCategoriesSimple() { | |||
return request({ | |||
url: "/categories/simple", | |||
method: "get", | |||
}); | |||
} | |||
/** | |||
* 获取分类详情 | |||
* @param {number} categoryId - 分类ID | |||
* @returns {Promise} 分类详情 | |||
*/ | |||
export function getCategoryDetail(categoryId) { | |||
return request({ | |||
url: `/categories/${categoryId}`, | |||
method: "get", | |||
}); | |||
} | |||
/** | |||
* 获取基准数据列表 | |||
* @param {Object} params - 查询参数 | |||
* @param {number} params.page - 页码 | |||
* @param {number} params.pageSize - 每页数量 | |||
* @param {number} params.categoryId - 分类ID | |||
* @param {string} params.productCode - 商品编号 | |||
* @param {string} params.productName - 商品名称 | |||
* @returns {Promise} 基准数据列表和分页信息 | |||
*/ | |||
export function getBaseDataList(params) { | |||
return request({ | |||
url: "/basedata", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 导出基准数据 | |||
* @param {number} categoryId - 分类ID | |||
* @returns {Promise} 导出文件URL | |||
*/ | |||
export function exportBaseData(categoryId) { | |||
return request({ | |||
url: `/basedata/export/${categoryId}`, | |||
responseType: "blob", | |||
method: "get", | |||
}); | |||
} | |||
/** | |||
* 删除基准数据 | |||
* @param {number} id - 数据ID | |||
* @returns {Promise} 删除结果 | |||
*/ | |||
export function deleteBaseData(id) { | |||
return request({ | |||
url: `/basedata/${id}`, | |||
method: "delete", | |||
}); | |||
} | |||
/** | |||
* 批量删除基准数据 | |||
* @param {string} ids - 数据ID列表,逗号分隔 | |||
* @returns {Promise} 批量删除结果 | |||
*/ | |||
export function batchDeleteBaseData(ids) { | |||
return request({ | |||
url: `/basedata/batch/${ids}`, | |||
method: "delete", | |||
}); | |||
} | |||
/** | |||
* 新增基准数据 | |||
* @param {Object} data - 新增数据 | |||
* @returns {Promise} 新增结果 | |||
*/ | |||
export function BaseDataAdd(data) { | |||
return request({ | |||
url: "/basedata", | |||
method: "post", | |||
data, | |||
}); | |||
} | |||
/** | |||
* 更新基准数据 | |||
* @param {number} id - 数据ID | |||
* @param {Object} data - 更新数据 | |||
* @returns {Promise} 更新结果 | |||
*/ | |||
export function BaseDataUpdate(id, data) { | |||
return request({ | |||
url: `/basedata/${id}`, | |||
method: "put", | |||
data, | |||
}); | |||
} | |||
// ==================== 分类管理 ==================== | |||
/** | |||
* 获取分类列表 | |||
* @param {Object} params - 查询参数 | |||
* @param {number} params.page - 页码 | |||
* @param {number} params.pageSize - 每页数量 | |||
* @param {string} params.name - 分类名称 | |||
* @returns {Promise} 分类列表和分页信息 | |||
*/ | |||
export function getCategoriesList(params) { | |||
return request({ | |||
url: "/categories", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 创建分类 | |||
* @param {Object} data - 分类数据 | |||
* @returns {Promise} 创建结果 | |||
*/ | |||
export function createCategory(data) { | |||
return request({ | |||
url: "/categories", | |||
method: "post", | |||
data, | |||
}); | |||
} | |||
/** | |||
* 更新分类 | |||
* @param {number} id - 分类ID | |||
* @param {Object} data - 分类数据 | |||
* @returns {Promise} 更新结果 | |||
*/ | |||
export function updateCategory(id, data) { | |||
return request({ | |||
url: `/categories/${id}`, | |||
method: "put", | |||
data, | |||
}); | |||
} | |||
/** | |||
* 删除分类 | |||
* @param {number} id - 分类ID | |||
* @returns {Promise} 删除结果 | |||
*/ | |||
export function deleteCategory(id) { | |||
return request({ | |||
url: `/categories/${id}`, | |||
method: "delete", | |||
}); | |||
} | |||
// ==================== 店铺客户关联管理 ==================== | |||
/** | |||
* 获取店铺客户关联列表 | |||
* @param {Object} params - 查询参数 | |||
* @param {number} params.page - 页码 | |||
* @param {number} params.pageSize - 每页数量 | |||
* @param {string} params.shopName - 店铺名称 | |||
* @param {string} params.customerName - 客户名称 | |||
* @returns {Promise} 关联列表和分页信息 | |||
*/ | |||
export function getShopCustomerList(params) { | |||
return request({ | |||
url: "/shop-customer", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 删除店铺客户关联 | |||
* @param {number} id - 关联ID | |||
* @returns {Promise} 删除结果 | |||
*/ | |||
export function deleteShopCustomer(id) { | |||
return request({ | |||
url: `/shop-customer/${id}`, | |||
method: "delete", | |||
}); | |||
} | |||
/** | |||
* 批量删除店铺客户关联 | |||
* @param {string} ids - 关联ID列表,逗号分隔 | |||
* @returns {Promise} 批量删除结果 | |||
*/ | |||
export function batchDeleteShopCustomer(ids) { | |||
return request({ | |||
url: `/shop-customer/batch/${ids}`, | |||
method: "delete", | |||
}); | |||
} | |||
/** | |||
* 导出店铺客户关联数据 | |||
* @returns {string} 导出文件URL | |||
*/ | |||
export function exportShopCustomer() { | |||
return request({ | |||
url: "/shop-customer/export", | |||
responseType: "blob", | |||
method: "get", | |||
}); | |||
} | |||
/** | |||
* 新增店铺客户关联 | |||
* @param {Object} data - 新增数据 | |||
* @returns {Promise} 新增结果 | |||
*/ | |||
export function ShopCustomerAdd(data) { | |||
return request({ | |||
url: "/shop-customer", | |||
method: "post", | |||
data, | |||
}); | |||
} | |||
/** | |||
* 更新店铺客户关联 | |||
* @param {number} id - 关联ID | |||
* @param {Object} data - 更新数据 | |||
* @returns {Promise} 更新结果 | |||
*/ | |||
export function ShopCustomerUpdate(id, data) { | |||
return request({ | |||
url: `/shop-customer/${id}`, | |||
method: "put", | |||
data, | |||
}); | |||
} | |||
// ==================== 报表分析 ==================== | |||
/** | |||
* 获取报表筛选选项 | |||
* @returns {Promise} 筛选选项数据 | |||
*/ | |||
export function getReportFilterOptions() { | |||
return request({ | |||
url: "/reports/filter-options", | |||
method: "get", | |||
}); | |||
} | |||
/** | |||
* 获取商品分析报表数据 | |||
* @param {Object} params - 查询参数 | |||
* @param {string} params.startDate - 开始日期 | |||
* @param {string} params.endDate - 结束日期 | |||
* @param {string} params.productCode - 商品编号 | |||
* @returns {Promise} 商品分析数据 | |||
*/ | |||
export function getProductAnalysisReport(params) { | |||
return request({ | |||
url: "/reports/product-analysis", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 获取店铺分析报表数据 | |||
* @param {Object} params - 查询参数 | |||
* @param {string} params.startDate - 开始日期 | |||
* @param {string} params.endDate - 结束日期 | |||
* @param {string} params.shop - 店铺名称 | |||
* @returns {Promise} 店铺分析数据 | |||
*/ | |||
export function getShopAnalysisReport(params) { | |||
return request({ | |||
url: "/reports/shop-analysis", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 获取分类分析报表数据 | |||
* @param {Object} params - 查询参数 | |||
* @param {string} params.startDate - 开始日期 | |||
* @param {string} params.endDate - 结束日期 | |||
* @returns {Promise} 分类分析数据 | |||
*/ | |||
export function getCategoryAnalysisReport(params) { | |||
return request({ | |||
url: "/reports/category-analysis", | |||
method: "get", | |||
params, | |||
}); | |||
} | |||
/** | |||
* 获取整体分析报表数据 | |||
* @param {Object} params - 查询参数 | |||
* @param {string} params.startDate - 开始日期 | |||
* @param {string} params.endDate - 结束日期 | |||
* @returns {Promise} 整体分析数据 | |||
*/ | |||
export function getOverallAnalysisReport(params) { | |||
return request({ | |||
url: "/reports/overall-analysis", | |||
method: "get", | |||
params, | |||
}); | |||
} |
@@ -157,10 +157,12 @@ aside { | |||
justify-content: center; | |||
gap: 16px; | |||
padding-left: 16px; | |||
i{ | |||
i { | |||
font-size: 22px; | |||
cursor: pointer; | |||
&:hover{ | |||
&:hover { | |||
color: var(--primary-color); | |||
} | |||
} | |||
@@ -225,6 +227,30 @@ aside { | |||
} | |||
} | |||
.filter-container { | |||
background-color: #FFF; | |||
border-radius: 8px; | |||
padding: 16px; | |||
margin-bottom: 10px; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
.search-filters { | |||
display: flex; | |||
align-items: center; | |||
gap: 10px; | |||
.filter-item { | |||
width: 240px; | |||
} | |||
} | |||
.actions { | |||
display: flex; | |||
align-items: center; | |||
gap: 10px; | |||
} | |||
} | |||
.components-container { | |||
margin: 30px 50px; | |||
position: relative; | |||
@@ -272,15 +298,7 @@ aside { | |||
} | |||
} | |||
.filter-container { | |||
padding-bottom: 10px; | |||
.filter-item { | |||
display: inline-block; | |||
vertical-align: middle; | |||
margin-bottom: 10px; | |||
} | |||
} | |||
//refine vue-multiselect plugin | |||
.multiselect { |
@@ -0,0 +1,170 @@ | |||
<template> | |||
<el-autocomplete | |||
v-model="inputValue" | |||
:fetch-suggestions="fetchSuggestions" | |||
placeholder="请输入商品编码" | |||
:trigger-on-focus="false" | |||
:debounce="300" | |||
:clearable="true" | |||
@select="handleSelect" | |||
@clear="handleClear" | |||
@input="handleInput" | |||
:style="{ width: inputWidth }" | |||
size="small" | |||
class="product-code-autocomplete" | |||
popper-class="product-code-autocomplete-popper" | |||
> | |||
<template #default="{ item }"> | |||
<div class="suggestion-item"> | |||
<div class="product-code">{{ item.value }}</div> | |||
<div class="product-info" :title="item.label">{{ item.label }}</div> | |||
</div> | |||
</template> | |||
</el-autocomplete> | |||
</template> | |||
<script> | |||
import { getProductCodeSuggestions } from "@/api/sales-analysis"; | |||
export default { | |||
name: 'ProductCodeAutocomplete', | |||
props: { | |||
value: { | |||
type: String, | |||
default: '' | |||
}, | |||
width: { | |||
type: String, | |||
default: '200px' | |||
}, | |||
placeholder: { | |||
type: String, | |||
default: '请输入商品编码' | |||
} | |||
}, | |||
data() { | |||
return { | |||
inputValue: this.value, | |||
isLoading: false | |||
} | |||
}, | |||
computed: { | |||
inputWidth() { | |||
return this.width | |||
} | |||
}, | |||
watch: { | |||
value(newVal) { | |||
this.inputValue = newVal | |||
} | |||
}, | |||
methods: { | |||
/** | |||
* 获取联想建议 | |||
*/ | |||
async fetchSuggestions(queryString, callback) { | |||
if (!queryString || queryString.trim().length < 1) { | |||
callback([]) | |||
return | |||
} | |||
try { | |||
this.isLoading = true | |||
const response = await getProductCodeSuggestions({ keyword: queryString.trim() }) | |||
if (response.success) { | |||
callback(response.data || []) | |||
} else { | |||
callback([]) | |||
} | |||
} catch (error) { | |||
console.error('获取商品编码联想数据失败:', error) | |||
callback([]) | |||
} finally { | |||
this.isLoading = false | |||
} | |||
}, | |||
/** | |||
* 选择建议项 | |||
*/ | |||
handleSelect(item) { | |||
this.inputValue = item.value | |||
this.$emit('input', item.value) | |||
this.$emit('select', item) | |||
}, | |||
/** | |||
* 清空输入 | |||
*/ | |||
handleClear() { | |||
this.inputValue = '' | |||
this.$emit('input', '') | |||
this.$emit('clear') | |||
}, | |||
/** | |||
* 输入变化 | |||
*/ | |||
handleInput(value) { | |||
this.inputValue = value | |||
this.$emit('input', value) | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.product-code-autocomplete { | |||
width: 100%; | |||
} | |||
.suggestion-item { | |||
display: flex; | |||
flex-direction: column; | |||
padding: 8px 0; | |||
} | |||
.product-code { | |||
font-weight: 500; | |||
font-size: 14px; | |||
color: #303133; | |||
margin-bottom: 4px; | |||
} | |||
.product-info { | |||
font-size: 12px; | |||
color: #909399; | |||
line-height: 1.4; | |||
width: 100%; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
overflow: hidden; | |||
} | |||
</style> | |||
<style> | |||
.product-code-autocomplete-popper { | |||
max-width: 400px; | |||
} | |||
.product-code-autocomplete-popper .el-autocomplete-suggestion__wrap { | |||
max-height: 280px; | |||
} | |||
.product-code-autocomplete-popper .el-autocomplete-suggestion__list { | |||
padding: 0; | |||
} | |||
.product-code-autocomplete-popper .el-autocomplete-suggestion__item { | |||
padding: 8px 20px; | |||
border-bottom: 1px solid #f0f0f0; | |||
} | |||
.product-code-autocomplete-popper .el-autocomplete-suggestion__item:last-child { | |||
border-bottom: none; | |||
} | |||
.product-code-autocomplete-popper .el-autocomplete-suggestion__item.highlighted { | |||
background-color: #f5f7fa; | |||
} | |||
</style> |
@@ -30,7 +30,7 @@ | |||
> | |||
<div class="avatar-wrapper"> | |||
<img :src="avatar" class="user-avatar"> | |||
<span>{{ name }}</span> | |||
<span>{{ nickname }}</span> | |||
<i class="el-icon-caret-bottom" /> | |||
</div> | |||
<el-dropdown-menu slot="dropdown"> | |||
@@ -68,7 +68,7 @@ export default { | |||
Msg | |||
}, | |||
computed: { | |||
...mapGetters(['sidebar', 'avatar', 'device', 'name']), | |||
...mapGetters(['sidebar', 'avatar', 'device', 'name', 'nickname']), | |||
setting: { | |||
get() { | |||
return this.$store.state.settings.showSettings |
@@ -1,10 +1,10 @@ | |||
import Vue from 'vue' | |||
import Router from 'vue-router' | |||
import Vue from "vue"; | |||
import Router from "vue-router"; | |||
Vue.use(Router) | |||
Vue.use(Router); | |||
/* Layout */ | |||
import Layout from '@/layout' | |||
import Layout from "@/layout"; | |||
/** | |||
* Note: 路由配置项 | |||
@@ -31,167 +31,240 @@ import Layout from '@/layout' | |||
// 公共路由 | |||
export const constantRoutes = [ | |||
{ | |||
path: '/redirect', | |||
path: "/redirect", | |||
component: Layout, | |||
hidden: true, | |||
children: [ | |||
{ | |||
path: '/redirect/:path(.*)', | |||
component: () => import('@/views/redirect') | |||
} | |||
] | |||
path: "/redirect/:path(.*)", | |||
component: () => import("@/views/redirect"), | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/login', | |||
component: () => import('@/views/login'), | |||
hidden: true | |||
path: "/login", | |||
component: () => import("@/views/login"), | |||
hidden: true, | |||
}, | |||
{ | |||
path: '/register', | |||
component: () => import('@/views/register'), | |||
hidden: true | |||
path: "/register", | |||
component: () => import("@/views/register"), | |||
hidden: true, | |||
}, | |||
{ | |||
path: '/404', | |||
component: () => import('@/views/error/404'), | |||
hidden: true | |||
path: "/404", | |||
component: () => import("@/views/error/404"), | |||
hidden: true, | |||
}, | |||
{ | |||
path: '/401', | |||
component: () => import('@/views/error/401'), | |||
hidden: true | |||
path: "/401", | |||
component: () => import("@/views/error/401"), | |||
hidden: true, | |||
}, | |||
{ | |||
path: '', | |||
path: "", | |||
component: Layout, | |||
redirect: 'index', | |||
redirect: "index", | |||
children: [ | |||
{ | |||
path: 'index', | |||
component: () => import('@/views/index'), | |||
name: 'Index', | |||
meta: { title: '首页', icon: 'dashboard', affix: true } | |||
} | |||
] | |||
path: "index", | |||
component: () => import("@/views/index"), | |||
name: "Index", | |||
meta: { title: "首页", icon: "dashboard", affix: true }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/user', | |||
path: "/user", | |||
component: Layout, | |||
hidden: true, | |||
redirect: 'noredirect', | |||
redirect: "noredirect", | |||
children: [ | |||
{ | |||
path: 'profile', | |||
component: () => import('@/views/system/user/profile/index'), | |||
name: 'Profile', | |||
meta: { title: '个人中心', icon: 'user' } | |||
} | |||
] | |||
path: "profile", | |||
component: () => import("@/views/system/user/profile/index"), | |||
name: "Profile", | |||
meta: { title: "个人中心", icon: "user" }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/m/checkin', | |||
component: () => import('@/views/m/checkin'), | |||
meta: { title: '考勤打卡', icon: 'date' } | |||
path: "/m/checkin", | |||
component: () => import("@/views/m/checkin"), | |||
meta: { title: "考勤打卡", icon: "date" }, | |||
}, | |||
] | |||
{ | |||
path: "/sales-analysis", | |||
component: Layout, | |||
meta: { title: "销售分析", icon: "dashboard", noCache: true }, | |||
children: [ | |||
// 基准数据管理 | |||
{ | |||
path: "/sales-analysis/categories", | |||
name: "Categories", | |||
component: () => | |||
import("@/views/sales-analysis/categories/Categories.vue"), | |||
meta: { title: "分类管理", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/base-data", | |||
name: "BaseData", | |||
component: () => | |||
import("@/views/sales-analysis/base-data/BaseData.vue"), | |||
meta: { title: "基准数据", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/shop-customer", | |||
name: "ShopCustomer", | |||
component: () => | |||
import("@/views/sales-analysis/shop-customer/ShopCustomer.vue"), | |||
meta: { title: "店铺客户关联", noCache: true }, | |||
}, | |||
// 分析数据管理 | |||
{ | |||
path: "/sales-analysis/analysis-data/import", | |||
name: "ImportData", | |||
component: () => | |||
import("@/views/sales-analysis/analysis-data/ImportData.vue"), | |||
meta: { title: "导入数据", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/analysis-data/list", | |||
name: "DataList", | |||
component: () => | |||
import("@/views/sales-analysis/analysis-data/DataList.vue"), | |||
meta: { title: "数据展示", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/overall", | |||
name: "OverallAnalysis", | |||
component: () => | |||
import("@/views/sales-analysis/reports/OverallAnalysis.vue"), | |||
meta: { title: "整体销售分析", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/shop", | |||
name: "ShopReports", | |||
component: () => | |||
import("@/views/sales-analysis/reports/ShopAnalysis.vue"), | |||
meta: { title: "店铺销售分析", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/category", | |||
name: "CategoryReports", | |||
component: () => | |||
import("@/views/sales-analysis/reports/CategoryAnalysis.vue"), | |||
meta: { title: "品类销售分析", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/product", | |||
name: "ProductReports", | |||
component: () => | |||
import("@/views/sales-analysis/reports/ProductAnalysis.vue"), | |||
meta: { title: "单品销售分析", noCache: true }, | |||
}, | |||
], | |||
}, | |||
]; | |||
// 动态路由,基于用户权限动态去加载 | |||
export const dynamicRoutes = [ | |||
{ | |||
path: '/system/user-auth', | |||
path: "/system/user-auth", | |||
component: Layout, | |||
hidden: true, | |||
permissions: ['system:user:edit'], | |||
permissions: ["system:user:edit"], | |||
children: [ | |||
{ | |||
path: 'role/:userId(\\d+)', | |||
component: () => import('@/views/system/user/authRole'), | |||
name: 'AuthRole', | |||
meta: { title: '分配角色', activeMenu: '/system/user' } | |||
} | |||
] | |||
path: "role/:userId(\\d+)", | |||
component: () => import("@/views/system/user/authRole"), | |||
name: "AuthRole", | |||
meta: { title: "分配角色", activeMenu: "/system/user" }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/system/role-auth', | |||
path: "/system/role-auth", | |||
component: Layout, | |||
hidden: true, | |||
permissions: ['system:role:edit'], | |||
permissions: ["system:role:edit"], | |||
children: [ | |||
{ | |||
path: 'user/:roleId(\\d+)', | |||
component: () => import('@/views/system/role/authUser'), | |||
name: 'AuthUser', | |||
meta: { title: '分配用户', activeMenu: '/system/role' } | |||
} | |||
] | |||
path: "user/:roleId(\\d+)", | |||
component: () => import("@/views/system/role/authUser"), | |||
name: "AuthUser", | |||
meta: { title: "分配用户", activeMenu: "/system/role" }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/system/dict-data', | |||
path: "/system/dict-data", | |||
component: Layout, | |||
hidden: true, | |||
permissions: ['system:dict:list'], | |||
permissions: ["system:dict:list"], | |||
children: [ | |||
{ | |||
path: 'index/:dictId(\\d+)', | |||
component: () => import('@/views/system/dict/data'), | |||
name: 'Data', | |||
meta: { title: '字典数据', activeMenu: '/system/dict' } | |||
} | |||
] | |||
path: "index/:dictId(\\d+)", | |||
component: () => import("@/views/system/dict/data"), | |||
name: "Data", | |||
meta: { title: "字典数据", activeMenu: "/system/dict" }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/system/oss-config', | |||
path: "/system/oss-config", | |||
component: Layout, | |||
hidden: true, | |||
permissions: ['system:oss:list'], | |||
permissions: ["system:oss:list"], | |||
children: [ | |||
{ | |||
path: 'index', | |||
component: () => import('@/views/system/oss/config'), | |||
name: 'OssConfig', | |||
meta: { title: '配置管理', activeMenu: '/system/oss' } | |||
} | |||
] | |||
path: "index", | |||
component: () => import("@/views/system/oss/config"), | |||
name: "OssConfig", | |||
meta: { title: "配置管理", activeMenu: "/system/oss" }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/tool/gen-edit', | |||
path: "/tool/gen-edit", | |||
component: Layout, | |||
hidden: true, | |||
permissions: ['tool:gen:edit'], | |||
permissions: ["tool:gen:edit"], | |||
children: [ | |||
{ | |||
path: 'index/:tableId(\\d+)', | |||
component: () => import('@/views/tool/gen/editTable'), | |||
name: 'GenEdit', | |||
meta: { title: '修改生成配置', activeMenu: '/tool/gen' } | |||
} | |||
] | |||
path: "index/:tableId(\\d+)", | |||
component: () => import("@/views/tool/gen/editTable"), | |||
name: "GenEdit", | |||
meta: { title: "修改生成配置", activeMenu: "/tool/gen" }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/workflow/dynamicFormDesigner', | |||
path: "/workflow/dynamicFormDesigner", | |||
component: Layout, | |||
hidden: true, | |||
permissions: ['workflow:dynamicForm:edit'], | |||
permissions: ["workflow:dynamicForm:edit"], | |||
children: [ | |||
{ | |||
path: ':id(\\d+)', | |||
component: () => import('@/views/workflow/dynamicForm/dynamicFormDesigner'), | |||
name: 'dynamicFormDesigner', | |||
meta: { title: '设计表单', activeMenu: '/workflow/dynamicForm' } | |||
} | |||
] | |||
} | |||
] | |||
path: ":id(\\d+)", | |||
component: () => | |||
import("@/views/workflow/dynamicForm/dynamicFormDesigner"), | |||
name: "dynamicFormDesigner", | |||
meta: { title: "设计表单", activeMenu: "/workflow/dynamicForm" }, | |||
}, | |||
], | |||
}, | |||
]; | |||
// 防止连续点击多次路由报错 | |||
let routerPush = Router.prototype.push; | |||
Router.prototype.push = function push(location) { | |||
return routerPush.call(this, location).catch(err => err) | |||
} | |||
return routerPush.call(this, location).catch((err) => err); | |||
}; | |||
export default new Router({ | |||
base: process.env.VUE_APP_CONTEXT_PATH, | |||
mode: 'history', // 去掉url中的# | |||
mode: "history", // 去掉url中的# | |||
scrollBehavior: () => ({ y: 0 }), | |||
routes: constantRoutes | |||
}) | |||
routes: constantRoutes, | |||
}); |
@@ -8,6 +8,7 @@ const getters = { | |||
token: (state) => state.user.token, | |||
avatar: (state) => state.user.avatar, | |||
name: (state) => state.user.name, | |||
nickname: (state) => state.user.nickname, | |||
introduction: (state) => state.user.introduction, | |||
roles: (state) => state.user.roles, | |||
userinfo: (state) => state.user.userinfo, |
@@ -21,6 +21,9 @@ const user = { | |||
SET_AVATAR: (state, avatar) => { | |||
state.avatar = avatar | |||
}, | |||
SET_NICKNAME: (state, nickname) => { | |||
state.nickname = nickname | |||
}, | |||
SET_ROLES: (state, roles) => { | |||
state.roles = roles | |||
}, | |||
@@ -70,6 +73,7 @@ const user = { | |||
commit('SET_ROLES', ['ROLE_DEFAULT']) | |||
} | |||
commit('SET_NAME', user.userName) | |||
commit('SET_NICKNAME', user.nickName) | |||
commit('SET_AVATAR', avatar) | |||
commit('SET_USERINFO', user) | |||
resolve(res) |
@@ -0,0 +1,54 @@ | |||
import Vue from 'vue' | |||
import moment from 'moment' | |||
// 日期格式化过滤器 | |||
Vue.filter('dateFormat', (value, format = 'YYYY-MM-DD') => { | |||
if (!value) return '' | |||
return moment(value).format(format) | |||
}) | |||
// 日期时间格式化过滤器 | |||
Vue.filter('dateTimeFormat', (value, format = 'YYYY-MM-DD HH:mm:ss') => { | |||
if (!value) return '' | |||
return moment(value).format(format) | |||
}) | |||
// 金额格式化过滤器 | |||
Vue.filter('currency', (value, currency = '¥') => { | |||
if (!value) return '0.00' | |||
return currency + parseFloat(value).toFixed(2) | |||
}) | |||
// 数字格式化过滤器 | |||
Vue.filter('number', (value) => { | |||
if (!value) return '0' | |||
return parseFloat(value).toLocaleString() | |||
}) | |||
// 状态格式化过滤器 | |||
Vue.filter('statusText', (value) => { | |||
const statusMap = { | |||
1: '正常', | |||
2: '客户名称未匹配', | |||
3: '分类规格未匹配' | |||
} | |||
return statusMap[value] || '未知' | |||
}) | |||
// 字符串截取过滤器 | |||
Vue.filter('truncate', (value, length = 20) => { | |||
if (!value) return '' | |||
if (value.length <= length) return value | |||
return value.substring(0, length) + '...' | |||
}) | |||
// 导出格式化函数供组件使用 | |||
export const formatDateTime = (value, format = 'YYYY-MM-DD HH:mm:ss') => { | |||
if (!value) return '' | |||
return moment(value).format(format) | |||
} | |||
export const formatDate = (value, format = 'YYYY-MM-DD') => { | |||
if (!value) return '' | |||
return moment(value).format(format) | |||
} |
@@ -172,7 +172,7 @@ | |||
<el-table-column | |||
v-if="columns[1].visible" | |||
key="userName" | |||
label="用户名称" | |||
label="用户编号" | |||
align="center" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
@@ -180,7 +180,7 @@ | |||
<el-table-column | |||
v-if="columns[2].visible" | |||
key="nickName" | |||
label="用户昵称" | |||
label="用户名称" | |||
align="center" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
@@ -290,10 +290,10 @@ | |||
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> | |||
<el-row> | |||
<el-col :span="12"> | |||
<el-form-item label="用户昵称" prop="nickName"> | |||
<el-form-item label="用户名称" prop="nickName"> | |||
<el-input | |||
v-model="form.nickName" | |||
placeholder="请输入用户昵称" | |||
placeholder="请输入用户名称" | |||
maxlength="30" | |||
/> | |||
</el-form-item> | |||
@@ -568,8 +568,8 @@ export default { | |||
// 列信息 | |||
columns: [ | |||
{ key: 0, label: `用户编号`, visible: true }, | |||
{ key: 1, label: `用户名称`, visible: true }, | |||
{ key: 2, label: `用户昵称`, visible: true }, | |||
{ key: 1, label: `员工编号`, visible: true }, | |||
{ key: 2, label: `姓名`, visible: true }, | |||
{ key: 3, label: `部门`, visible: true }, | |||
{ key: 4, label: `手机号码`, visible: true }, | |||
{ key: 5, label: `状态`, visible: true }, | |||
@@ -578,7 +578,7 @@ export default { | |||
// 表单校验 | |||
rules: { | |||
userName: [ | |||
{ required: true, message: '用户名称不能为空', trigger: 'blur' }, | |||
{ required: true, message: '员工编号不能为空', trigger: 'blur' }, | |||
{ | |||
min: 2, | |||
max: 20, | |||
@@ -587,7 +587,7 @@ export default { | |||
} | |||
], | |||
nickName: [ | |||
{ required: true, message: '用户昵称不能为空', trigger: 'blur' } | |||
{ required: true, message: '姓名不能为空', trigger: 'blur' } | |||
], | |||
password: [ | |||
{ required: true, message: '用户密码不能为空', trigger: 'blur' }, |
@@ -90,7 +90,7 @@ | |||
<el-table-column | |||
v-if="columns[1].visible" | |||
key="userName" | |||
label="用户名称" | |||
label="员工编号" | |||
align="center" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
@@ -98,7 +98,7 @@ | |||
<el-table-column | |||
v-if="columns[2].visible" | |||
key="nickName" | |||
label="用户昵称" | |||
label="姓名" | |||
align="center" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
@@ -220,8 +220,8 @@ export default { | |||
// 列信息 | |||
columns: [ | |||
{ key: 0, label: `用户编号`, visible: true }, | |||
{ key: 1, label: `用户名称`, visible: true }, | |||
{ key: 2, label: `用户昵称`, visible: true }, | |||
{ key: 1, label: `员工编号`, visible: true }, | |||
{ key: 2, label: `姓名`, visible: true }, | |||
{ key: 3, label: `部门`, visible: true }, | |||
{ key: 4, label: `手机号码`, visible: true }, | |||
{ key: 5, label: `创建时间`, visible: true } |
@@ -109,7 +109,7 @@ | |||
<el-table-column | |||
v-if="columns[1].visible" | |||
key="userName" | |||
label="用户名称" | |||
label="员工编号" | |||
align="center" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
@@ -117,7 +117,7 @@ | |||
<el-table-column | |||
v-if="columns[2].visible" | |||
key="nickName" | |||
label="用户昵称" | |||
label="姓名" | |||
align="center" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
@@ -240,8 +240,8 @@ export default { | |||
// 列信息 | |||
columns: [ | |||
{ key: 0, label: `用户编号`, visible: true }, | |||
{ key: 1, label: `用户名称`, visible: true }, | |||
{ key: 2, label: `用户昵称`, visible: true }, | |||
{ key: 1, label: `员工编号`, visible: true }, | |||
{ key: 2, label: `姓名`, visible: true }, | |||
{ key: 3, label: `部门`, visible: true }, | |||
{ key: 4, label: `手机号码`, visible: true }, | |||
{ key: 5, label: `创建时间`, visible: true } |
@@ -108,7 +108,7 @@ | |||
<el-table-column | |||
v-if="columns[1].visible" | |||
key="userName" | |||
label="用户名称" | |||
label="员工编号" | |||
align="center" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
@@ -116,7 +116,7 @@ | |||
<el-table-column | |||
v-if="columns[2].visible" | |||
key="nickName" | |||
label="用户昵称" | |||
label="姓名" | |||
align="center" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
@@ -235,8 +235,8 @@ export default { | |||
// 列信息 | |||
columns: [ | |||
{ key: 0, label: `用户编号`, visible: true }, | |||
{ key: 1, label: `用户名称`, visible: true }, | |||
{ key: 2, label: `用户昵称`, visible: true }, | |||
{ key: 1, label: `员工编号`, visible: true }, | |||
{ key: 2, label: `姓名`, visible: true }, | |||
{ key: 3, label: `部门`, visible: true }, | |||
{ key: 4, label: `手机号码`, visible: true }, | |||
{ key: 5, label: `创建时间`, visible: true } |
@@ -101,7 +101,7 @@ | |||
<el-table-column | |||
v-if="columns[1].visible" | |||
key="userName" | |||
label="用户名称" | |||
label="员工编号" | |||
align="center" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
@@ -109,7 +109,7 @@ | |||
<el-table-column | |||
v-if="columns[2].visible" | |||
key="nickName" | |||
label="用户昵称" | |||
label="姓名" | |||
align="center" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
@@ -235,8 +235,8 @@ export default { | |||
// 列信息 | |||
columns: [ | |||
{ key: 0, label: `用户编号`, visible: true }, | |||
{ key: 1, label: `用户名称`, visible: true }, | |||
{ key: 2, label: `用户昵称`, visible: true }, | |||
{ key: 1, label: `员工编号`, visible: true }, | |||
{ key: 2, label: `姓名`, visible: true }, | |||
{ key: 3, label: `部门`, visible: true }, | |||
{ key: 4, label: `手机号码`, visible: true }, | |||
{ key: 5, label: `创建时间`, visible: true } |
@@ -180,9 +180,8 @@ | |||
style="width: 100%; margin-top: 20px" | |||
empty-text="暂无关联员工" | |||
> | |||
<el-table-column prop="userName" label="姓名"></el-table-column> | |||
<el-table-column prop="userId" label="工号"></el-table-column> | |||
<el-table-column prop="nickName" label="用户昵称"></el-table-column> | |||
<el-table-column prop="userName" label="员工编号"></el-table-column> | |||
<el-table-column prop="nickName" label="姓名"></el-table-column> | |||
<el-table-column prop="deptName" label="部门"></el-table-column> | |||
<el-table-column label="操作" width="80"> | |||
<template #default="scope"> |
@@ -7,6 +7,8 @@ | |||
<div class="info"> | |||
<h3>{{ warmMessage }}</h3> | |||
<p> | |||
<span>{{ nickname }}</span> | |||
<el-divider direction="vertical" /> | |||
<span>{{ userinfo.userName }}</span> | |||
<el-divider direction="vertical" /> | |||
<span>{{ userinfo.dept.deptName || '未设置部门' }}</span> | |||
@@ -50,7 +52,7 @@ export default { | |||
} | |||
}, | |||
computed: { | |||
...mapGetters(['userinfo', 'avatar']) | |||
...mapGetters(['userinfo', 'avatar', 'nickname']) | |||
}, | |||
created() { | |||
this.warmMessage = messageGenerator( |
@@ -180,9 +180,8 @@ | |||
style="width: 100%; margin-top: 20px" | |||
empty-text="暂无关联员工" | |||
> | |||
<el-table-column prop="userName" label="姓名"></el-table-column> | |||
<el-table-column prop="userId" label="工号"></el-table-column> | |||
<el-table-column prop="nickName" label="用户昵称"></el-table-column> | |||
<el-table-column prop="userName" label="员工编号"></el-table-column> | |||
<el-table-column prop="nickName" label="姓名"></el-table-column> | |||
<el-table-column prop="deptName" label="部门"></el-table-column> | |||
<el-table-column label="操作" width="80"> | |||
<template #default="scope"> |
@@ -0,0 +1,619 @@ | |||
<template> | |||
<div class="data-list-page app-container"> | |||
<!-- 搜索筛选 --> | |||
<div class="filter-container" style="gap: 0"> | |||
<div class="search-filters"> | |||
<el-form :model="searchForm" :inline="true" size="small"> | |||
<el-form-item label="商品编号"> | |||
<el-input | |||
v-model="searchForm.productCode" | |||
placeholder="请输入商品编号" | |||
style="width: 160px" | |||
clearable | |||
/> | |||
</el-form-item> | |||
<el-form-item label="商品名称"> | |||
<el-input | |||
v-model="searchForm.productName" | |||
placeholder="请输入商品名称" | |||
style="width: 160px" | |||
clearable | |||
/> | |||
</el-form-item> | |||
<el-form-item label="客户名称"> | |||
<el-input | |||
v-model="searchForm.customerName" | |||
placeholder="请输入客户名称" | |||
style="width: 160px" | |||
clearable | |||
/> | |||
</el-form-item> | |||
<el-form-item label="店铺名称"> | |||
<el-input | |||
v-model="searchForm.shopName" | |||
placeholder="请输入店铺名称" | |||
style="width: 160px" | |||
clearable | |||
/> | |||
</el-form-item> | |||
<el-form-item label="状态"> | |||
<el-select | |||
v-model="searchForm.status" | |||
placeholder="请选择状态" | |||
style="width: 160px" | |||
clearable | |||
> | |||
<el-option label="全部" value="" /> | |||
<el-option label="正常" value="1" /> | |||
<el-option label="客户未匹配" value="2" /> | |||
<el-option label="规格未匹配" value="3" /> | |||
</el-select> | |||
</el-form-item> | |||
<br /> | |||
<el-form-item label="开始日期"> | |||
<el-date-picker | |||
v-model="searchForm.startDate" | |||
type="date" | |||
placeholder="选择开始日期" | |||
format="yyyy-MM-dd" | |||
value-format="yyyy-MM-dd" | |||
style="width: 160px" | |||
clearable | |||
/> | |||
</el-form-item> | |||
<el-form-item label="结束日期"> | |||
<el-date-picker | |||
v-model="searchForm.endDate" | |||
type="date" | |||
placeholder="选择结束日期" | |||
format="yyyy-MM-dd" | |||
value-format="yyyy-MM-dd" | |||
style="width: 160px" | |||
clearable | |||
/> | |||
</el-form-item> | |||
<el-form-item> | |||
<el-button type="primary" @click="handleSearch" icon="el-icon-search"></el-button> | |||
<el-button @click="handleReset" icon="el-icon-refresh"></el-button> | |||
</el-form-item> | |||
</el-form> | |||
</div> | |||
<div class="actions" style="margin-top: 0; justify-content: flex-end;"> | |||
<el-button | |||
type="success" | |||
plain | |||
@click="goToImport" | |||
icon="el-icon-upload2" | |||
size="small" | |||
>导入Excel</el-button | |||
> | |||
<el-button | |||
type="success" | |||
plain | |||
@click="handleExport" | |||
icon="el-icon-download" | |||
size="small" | |||
>导出Excel</el-button | |||
> | |||
<el-divider direction="vertical"></el-divider> | |||
<el-button | |||
type="danger" | |||
plain | |||
icon="el-icon-delete" | |||
:disabled="selectedRows.length === 0" | |||
@click="handleBatchDelete" | |||
size="small" | |||
>批量删除</el-button | |||
> | |||
</div> | |||
</div> | |||
<!-- 数据表格 --> | |||
<el-table | |||
:data="tableData" | |||
stripe | |||
v-loading="loading" | |||
size="small" | |||
@selection-change="handleSelectionChange" | |||
> | |||
<el-table-column type="selection" width="55" align="center" /> | |||
<el-table-column prop="date" label="日期" width="100" sortable /> | |||
<el-table-column | |||
prop="shopName" | |||
label="店铺" | |||
width="120" | |||
show-overflow-tooltip | |||
/> | |||
<el-table-column | |||
prop="productCode" | |||
label="商品编号" | |||
width="120" | |||
show-overflow-tooltip | |||
/> | |||
<el-table-column | |||
prop="productName" | |||
label="商品名称" | |||
show-overflow-tooltip | |||
/> | |||
<el-table-column | |||
prop="customerName" | |||
label="客户" | |||
width="120" | |||
show-overflow-tooltip | |||
/> | |||
<el-table-column | |||
prop="category" | |||
label="分类" | |||
width="100" | |||
show-overflow-tooltip | |||
/> | |||
<el-table-column prop="status" label="状态" width="120" align="center"> | |||
<template slot-scope="scope"> | |||
<el-tag | |||
:type=" | |||
scope.row.status === 1 | |||
? 'success' | |||
: scope.row.status === 2 | |||
? 'warning' | |||
: 'danger' | |||
" | |||
size="mini" | |||
> | |||
{{ getStatusText(scope.row.status) }} | |||
</el-tag> | |||
</template> | |||
</el-table-column> | |||
<el-table-column label="操作" width="150" align="center" fixed="right"> | |||
<template slot-scope="scope"> | |||
<el-button | |||
type="primary" | |||
plain | |||
size="mini" | |||
@click="handleEdit(scope.row)" | |||
>编辑</el-button | |||
> | |||
<el-button | |||
type="danger" | |||
:disabled="scope.row.status === 1" | |||
size="mini" | |||
plain | |||
@click="handleDelete(scope.row)" | |||
>删除</el-button | |||
> | |||
</template> | |||
</el-table-column> | |||
</el-table> | |||
<pagination | |||
v-show="pagination.total > 0" | |||
:total="pagination.total" | |||
:page.sync="pagination.page" | |||
:limit.sync="pagination.pageSize" | |||
@pagination="fetchData" | |||
/> | |||
<!-- 编辑对话框 --> | |||
<el-dialog | |||
title="编辑分析数据" | |||
:visible.sync="editDialogVisible" | |||
width="800px" | |||
@close="handleEditDialogClose" | |||
> | |||
<el-form | |||
:model="editForm" | |||
:rules="editRules" | |||
ref="editForm" | |||
label-width="100px" | |||
label-position="top" | |||
> | |||
<el-form-item label="日期" prop="date"> | |||
<el-date-picker | |||
v-model="editForm.date" | |||
type="date" | |||
placeholder="选择日期" | |||
format="yyyy-MM-dd" | |||
value-format="yyyy-MM-dd" | |||
style="width: 100%" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="店铺名称" prop="shopName"> | |||
<el-input v-model="editForm.shopName" placeholder="请输入店铺名称" /> | |||
</el-form-item> | |||
<el-form-item label="商品编号" prop="productCode"> | |||
<el-input | |||
v-model="editForm.productCode" | |||
placeholder="请输入商品编号" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="商品名称" prop="productName"> | |||
<el-input | |||
v-model="editForm.productName" | |||
placeholder="请输入商品名称" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="客户名称" prop="customerName"> | |||
<el-input | |||
v-model="editForm.customerName" | |||
placeholder="请输入客户名称" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="分类" prop="category"> | |||
<el-input v-model="editForm.category" placeholder="请输入分类" /> | |||
</el-form-item> | |||
<el-form-item label="分类规格" prop="categorySpecs"> | |||
<el-input | |||
v-model="editForm.categorySpecs" | |||
placeholder="请输入分类规格" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="数量" prop="quantity"> | |||
<el-input-number | |||
v-model="editForm.quantity" | |||
:min="0" | |||
style="width: 100%" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="合计金额" prop="totalAmount"> | |||
<el-input-number | |||
v-model="editForm.totalAmount" | |||
:min="0" | |||
:precision="2" | |||
style="width: 100%" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="出库类型" prop="deliveryType"> | |||
<el-input | |||
v-model="editForm.deliveryType" | |||
placeholder="请输入出库类型" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="目的地" prop="destination"> | |||
<el-input v-model="editForm.destination" placeholder="请输入目的地" /> | |||
</el-form-item> | |||
<el-form-item label="备注" prop="remarks"> | |||
<el-input | |||
v-model="editForm.remarks" | |||
type="textarea" | |||
placeholder="请输入备注" | |||
:rows="3" | |||
/> | |||
</el-form-item> | |||
</el-form> | |||
<div slot="footer" class="dialog-footer"> | |||
<el-button @click="editDialogVisible = false">取消</el-button> | |||
<el-button type="primary" @click="handleEditSubmit">确定</el-button> | |||
</div> | |||
</el-dialog> | |||
</div> | |||
</template> | |||
<script> | |||
import { | |||
getAnalysisDataList, | |||
updateAnalysisData, | |||
deleteAnalysisData, | |||
batchDeleteAnalysisData, | |||
exportAnalysisData, | |||
} from "@/api/sales-analysis"; | |||
export default { | |||
name: "DataList", | |||
data() { | |||
return { | |||
loading: false, | |||
tableData: [], | |||
selectedRows: [], | |||
searchForm: { | |||
productCode: "", | |||
productName: "", | |||
customerName: "", | |||
shopName: "", | |||
status: "", | |||
startDate: "", | |||
endDate: "", | |||
}, | |||
pagination: { | |||
page: 1, | |||
pageSize: 50, | |||
total: 0, | |||
}, | |||
editDialogVisible: false, | |||
editForm: { | |||
id: null, | |||
date: "", | |||
shopName: "", | |||
productCode: "", | |||
productName: "", | |||
customerName: "", | |||
category: "", | |||
categorySpecs: "", | |||
quantity: 0, | |||
totalAmount: 0, | |||
deliveryType: "", | |||
destination: "", | |||
remarks: "", | |||
}, | |||
editRules: { | |||
date: [{ required: true, message: "请选择日期", trigger: "change" }], | |||
shopName: [ | |||
{ required: true, message: "请输入店铺名称", trigger: "blur" }, | |||
], | |||
productCode: [ | |||
{ required: true, message: "请输入商品编号", trigger: "blur" }, | |||
], | |||
productName: [ | |||
{ required: true, message: "请输入商品名称", trigger: "blur" }, | |||
], | |||
}, | |||
}; | |||
}, | |||
mounted() { | |||
this.fetchData(); | |||
}, | |||
methods: { | |||
// 获取数据 | |||
async fetchData() { | |||
try { | |||
this.loading = true; | |||
const params = { | |||
page: this.pagination.page, | |||
pageSize: this.pagination.pageSize, | |||
...this.searchForm, | |||
}; | |||
const response = await getAnalysisDataList(params); | |||
if (response.code === 200) { | |||
this.tableData = response.data.list || []; | |||
this.pagination = response.data.pagination || {}; | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取数据失败:", error); | |||
this.$message.error("获取数据失败"); | |||
} finally { | |||
this.loading = false; | |||
} | |||
}, | |||
// 搜索 | |||
handleSearch() { | |||
this.pagination.page = 1; | |||
this.fetchData(); | |||
}, | |||
// 重置搜索 | |||
handleReset() { | |||
this.searchForm = { | |||
productCode: "", | |||
productName: "", | |||
customerName: "", | |||
shopName: "", | |||
status: "", | |||
startDate: "", | |||
endDate: "", | |||
}; | |||
this.pagination.page = 1; | |||
this.fetchData(); | |||
}, | |||
// 分页大小改变 | |||
handleSizeChange(size) { | |||
this.pagination.pageSize = size; | |||
this.pagination.page = 1; | |||
this.fetchData(); | |||
}, | |||
// 当前页改变 | |||
handleCurrentChange(page) { | |||
this.pagination.page = page; | |||
this.fetchData(); | |||
}, | |||
// 选择行改变 | |||
handleSelectionChange(selection) { | |||
this.selectedRows = selection; | |||
}, | |||
// 编辑 | |||
handleEdit(row) { | |||
this.editForm = { | |||
id: row.id, | |||
date: row.date, | |||
shopName: row.shopName, | |||
productCode: row.productCode, | |||
productName: row.productName, | |||
customerName: row.customerName, | |||
category: row.category, | |||
categorySpecs: row.categorySpecs, | |||
quantity: row.quantity, | |||
totalAmount: row.totalAmount, | |||
deliveryType: row.deliveryType, | |||
destination: row.destination, | |||
remarks: row.remarks, | |||
}; | |||
this.editDialogVisible = true; | |||
}, | |||
// 提交编辑 | |||
handleEditSubmit() { | |||
this.$refs.editForm.validate(async (valid) => { | |||
if (valid) { | |||
try { | |||
const response = await updateAnalysisData( | |||
this.editForm.id, | |||
this.editForm | |||
); | |||
if (response.code === 200) { | |||
this.$message.success("编辑成功"); | |||
this.editDialogVisible = false; | |||
this.fetchData(); | |||
} else { | |||
this.$message.error(response.message || "编辑失败"); | |||
} | |||
} catch (error) { | |||
console.error("编辑失败:", error); | |||
this.$message.error("编辑失败"); | |||
} | |||
} | |||
}); | |||
}, | |||
// 编辑对话框关闭 | |||
handleEditDialogClose() { | |||
this.$refs.editForm.resetFields(); | |||
}, | |||
// 删除 | |||
handleDelete(row) { | |||
this.$confirm("确定要删除这条记录吗?", "提示", { | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
}) | |||
.then(async () => { | |||
try { | |||
const response = await deleteAnalysisData(row.id); | |||
if (response.code === 200) { | |||
this.$message.success("删除成功"); | |||
this.fetchData(); | |||
} else { | |||
this.$message.error(response.message || "删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("删除失败:", error); | |||
this.$message.error("删除失败"); | |||
} | |||
}) | |||
.catch(() => { | |||
this.$message.info("已取消删除"); | |||
}); | |||
}, | |||
// 批量删除 | |||
handleBatchDelete() { | |||
if (this.selectedRows.length === 0) { | |||
this.$message.warning("请选择要删除的数据"); | |||
return; | |||
} | |||
this.$confirm( | |||
`确定要删除选中的 ${this.selectedRows.length} 条记录吗?`, | |||
"提示", | |||
{ | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
} | |||
) | |||
.then(async () => { | |||
try { | |||
const ids = this.selectedRows.map((row) => row.id).join(","); | |||
const response = await batchDeleteAnalysisData(ids); | |||
if (response.code === 200) { | |||
this.$message.success("批量删除成功"); | |||
this.fetchData(); | |||
} else { | |||
this.$message.error(response.message || "批量删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("批量删除失败:", error); | |||
this.$message.error("批量删除失败"); | |||
} | |||
}) | |||
.catch(() => { | |||
this.$message.info("已取消删除"); | |||
}); | |||
}, | |||
// 导出数据 | |||
async handleExport() { | |||
const response = await exportAnalysisData(this.searchForm); | |||
const blob = new Blob([response], { type: "application/vnd.ms-excel" }); | |||
const url = window.URL.createObjectURL(blob); | |||
const a = document.createElement("a"); | |||
a.href = url; | |||
a.download = `analysis-data-${new Date().toLocaleDateString()}.xlsx`; | |||
a.click(); | |||
}, | |||
// 获取状态文本 | |||
getStatusText(status) { | |||
const statusMap = { | |||
1: "正常", | |||
2: "客户未匹配", | |||
3: "规格未匹配", | |||
}; | |||
return statusMap[status] || "未知"; | |||
}, | |||
// 跳转到导入页面 | |||
goToImport() { | |||
this.$router.push("/sales-analysis/analysis-data/import"); | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.page-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 20px; | |||
} | |||
.page-header h2 { | |||
font-size: 24px; | |||
color: #333; | |||
margin: 0; | |||
} | |||
.header-actions { | |||
display: flex; | |||
gap: 10px; | |||
} | |||
.search-card { | |||
margin-bottom: 20px; | |||
} | |||
.table-card { | |||
background: white; | |||
padding: 20px; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
} | |||
.table-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 20px; | |||
} | |||
.table-title { | |||
font-size: 16px; | |||
font-weight: bold; | |||
color: #333; | |||
} | |||
.count { | |||
font-size: 14px; | |||
color: #666; | |||
font-weight: normal; | |||
} | |||
.pagination { | |||
margin-top: 20px; | |||
text-align: center; | |||
} | |||
.dialog-footer { | |||
text-align: center; | |||
} | |||
</style> |
@@ -0,0 +1,850 @@ | |||
<template> | |||
<div class="base-data-layout"> | |||
<!-- 左侧分类导航 --> | |||
<div class="category-sidebar"> | |||
<div class="category-list-wrapper" v-loading="categoriesLoading"> | |||
<div | |||
v-for="category in categoriesList" | |||
:key="category.id" | |||
:class="[ | |||
'category-item', | |||
{ 'is-active': selectedCategoryId === category.id }, | |||
]" | |||
@click="selectCategory(category)" | |||
> | |||
<span class="category-name">{{ category.name }}</span> | |||
<i class="el-icon-arrow-right"></i> | |||
</div> | |||
<el-empty | |||
v-if="!categoriesList.length && !categoriesLoading" | |||
description="暂无分类数据" | |||
:image-size="80" | |||
></el-empty> | |||
</div> | |||
</div> | |||
<!-- 右侧主内容区 --> | |||
<div class="main-content-area"> | |||
<div v-if="!selectedCategory" class="placeholder-view card"> | |||
<el-empty | |||
description="请从左侧选择一个分类进行操作" | |||
:image-size="120" | |||
></el-empty> | |||
</div> | |||
<div v-else class="data-view"> | |||
<div class="filter-container"> | |||
<div class="search-filters"> | |||
<el-input | |||
v-model="searchForm.productCode" | |||
placeholder="按商品编号搜索" | |||
clearable | |||
@clear="handleSearch" | |||
@keyup.enter.native="handleSearch" | |||
style="width: 160px" | |||
size="small" | |||
class="filter-item" | |||
></el-input> | |||
<el-input | |||
v-model="searchForm.productName" | |||
placeholder="按商品名称搜索" | |||
clearable | |||
@clear="handleSearch" | |||
@keyup.enter.native="handleSearch" | |||
style="width: 160px" | |||
size="small" | |||
class="filter-item" | |||
></el-input> | |||
<el-button | |||
type="primary" | |||
icon="el-icon-search" | |||
@click="handleSearch" | |||
size="small" | |||
style="margin-right: 0" | |||
></el-button> | |||
<el-button | |||
icon="el-icon-refresh" | |||
@click="resetSearch" | |||
size="small" | |||
style="margin-left: 0" | |||
></el-button> | |||
<el-button | |||
type="primary" | |||
icon="el-icon-plus" | |||
@click="handleAdd" | |||
size="small" | |||
style="margin-left: auto" | |||
>新增数据</el-button | |||
> | |||
<el-divider direction="vertical"></el-divider> | |||
<el-upload | |||
ref="upload" | |||
:action="uploadUrl" | |||
:data="uploadData" | |||
:before-upload="beforeUpload" | |||
:on-success="onUploadSuccess" | |||
:on-error="onUploadError" | |||
:show-file-list="false" | |||
accept=".xlsx,.xls" | |||
size="small" | |||
> | |||
<el-button | |||
type="success" | |||
plain | |||
icon="el-icon-upload2" | |||
size="small" | |||
>导入Excel</el-button | |||
> | |||
</el-upload> | |||
<el-button | |||
type="warning" | |||
plain | |||
icon="el-icon-download" | |||
@click="handleExport" | |||
:disabled="!baseDataList.length" | |||
size="small" | |||
>导出Excel</el-button | |||
> | |||
<el-divider direction="vertical"></el-divider> | |||
<el-button | |||
type="danger" | |||
icon="el-icon-delete" | |||
@click="handleBatchDelete" | |||
:disabled="!selectedRows.length" | |||
plain | |||
size="small" | |||
>批量删除</el-button | |||
> | |||
</div> | |||
</div> | |||
<!-- 数据表格 --> | |||
<el-table | |||
v-loading="dataLoading" | |||
:data="baseDataList" | |||
style="width: 100%" | |||
@selection-change="handleSelectionChange" | |||
row-key="id" | |||
size="small" | |||
> | |||
<el-table-column | |||
type="selection" | |||
width="55" | |||
align="center" | |||
></el-table-column> | |||
<el-table-column | |||
prop="productCode" | |||
label="商品编号" | |||
min-width="140" | |||
show-overflow-tooltip | |||
sortable | |||
></el-table-column> | |||
<el-table-column | |||
prop="productName" | |||
label="商品名称" | |||
min-width="180" | |||
show-overflow-tooltip | |||
></el-table-column> | |||
<el-table-column | |||
prop="brand" | |||
label="品牌" | |||
min-width="110" | |||
show-overflow-tooltip | |||
></el-table-column> | |||
<el-table-column | |||
v-for="field in categoryFields" | |||
:key="field.fieldName" | |||
:label="field.displayLabel" | |||
:prop="'categorySpecs.' + field.fieldName" | |||
min-width="120" | |||
show-overflow-tooltip | |||
sortable | |||
> | |||
<template slot-scope="scope"> | |||
<span>{{ | |||
scope.row.categorySpecs | |||
? scope.row.categorySpecs[field.fieldName] | |||
: "" | |||
}}</span> | |||
</template> | |||
</el-table-column> | |||
<el-table-column label="操作" width="160" align="center"> | |||
<template slot-scope="scope"> | |||
<el-button | |||
type="primary" | |||
size="mini" | |||
@click="handleEdit(scope.row)" | |||
plain | |||
>编辑</el-button | |||
> | |||
<el-popconfirm | |||
title="确定删除这条数据吗?" | |||
@confirm="handleDelete(scope.row)" | |||
style="margin-left: 10px" | |||
> | |||
<el-button slot="reference" type="danger" size="mini" plain | |||
>删除</el-button | |||
> | |||
</el-popconfirm> | |||
</template> | |||
</el-table-column> | |||
</el-table> | |||
<pagination | |||
v-show="pagination.total > 0" | |||
:total="pagination.total" | |||
:page.sync="pagination.page" | |||
:limit.sync="pagination.pageSize" | |||
@pagination="fetchBaseDataList" | |||
/> | |||
</div> | |||
</div> | |||
<!-- 新增/编辑对话框 --> | |||
<el-dialog | |||
:title="dialogTitle" | |||
:visible.sync="dialogVisible" | |||
width="600px" | |||
:close-on-click-modal="false" | |||
@close="handleDialogClose" | |||
top="8vh" | |||
custom-class="data-dialog" | |||
> | |||
<el-form | |||
ref="dataForm" | |||
:model="dataForm" | |||
:rules="dataRules" | |||
label-width="110px" | |||
label-position="top" | |||
class="data-form" | |||
> | |||
<el-row :gutter="20"> | |||
<el-col :span="12"> | |||
<el-form-item label="商品编号" prop="productCode"> | |||
<el-input | |||
v-model="dataForm.productCode" | |||
placeholder="请输入商品编号" | |||
></el-input> | |||
</el-form-item> | |||
</el-col> | |||
<el-col :span="12"> | |||
<el-form-item label="品牌" prop="brand"> | |||
<el-input | |||
v-model="dataForm.brand" | |||
placeholder="请输入品牌" | |||
></el-input> | |||
</el-form-item> | |||
</el-col> | |||
</el-row> | |||
<el-form-item label="商品名称" prop="productName"> | |||
<el-input | |||
v-model="dataForm.productName" | |||
placeholder="请输入商品名称" | |||
></el-input> | |||
</el-form-item> | |||
<!-- 动态分类规格字段 --> | |||
<div v-if="categoryFields.length" class="spec-section"> | |||
<el-divider>分类规格 ({{ selectedCategory.name }})</el-divider> | |||
<el-row :gutter="20"> | |||
<el-col | |||
:span="12" | |||
v-for="field in categoryFields" | |||
:key="field.fieldName" | |||
> | |||
<el-form-item | |||
:label="field.displayLabel" | |||
:prop="`categorySpecs.${field.fieldName}`" | |||
:rules=" | |||
field.isRequired | |||
? [ | |||
{ | |||
required: true, | |||
message: `${field.displayLabel}不能为空`, | |||
trigger: 'blur', | |||
}, | |||
] | |||
: [] | |||
" | |||
> | |||
<el-input | |||
v-if="field.fieldType === 'text'" | |||
v-model="dataForm.categorySpecs[field.fieldName]" | |||
:placeholder="`请输入${field.displayLabel}`" | |||
></el-input> | |||
<el-input-number | |||
v-else-if="field.fieldType === 'number'" | |||
v-model="dataForm.categorySpecs[field.fieldName]" | |||
:placeholder="`请输入${field.displayLabel}`" | |||
style="width: 100%" | |||
controls-position="right" | |||
></el-input-number> | |||
<el-date-picker | |||
v-else-if="field.fieldType === 'date'" | |||
v-model="dataForm.categorySpecs[field.fieldName]" | |||
type="date" | |||
:placeholder="`请选择${field.displayLabel}`" | |||
style="width: 100%" | |||
value-format="yyyy-MM-dd" | |||
></el-date-picker> | |||
</el-form-item> | |||
</el-col> | |||
</el-row> | |||
</div> | |||
</el-form> | |||
<div slot="footer" class="dialog-footer"> | |||
<el-button @click="dialogVisible = false">取消</el-button> | |||
<el-button type="primary" :loading="submitting" @click="handleSubmit" | |||
>确定</el-button | |||
> | |||
</div> | |||
</el-dialog> | |||
</div> | |||
</template> | |||
<script> | |||
import { formatDateTime } from "@/utils/filters"; | |||
import { | |||
getCategoriesSimple, | |||
getCategoryDetail, | |||
getBaseDataList, | |||
deleteBaseData, | |||
batchDeleteBaseData, | |||
exportBaseData, | |||
BaseDataAdd, | |||
BaseDataUpdate, | |||
} from "@/api/sales-analysis"; | |||
export default { | |||
name: "BaseData", | |||
data() { | |||
return { | |||
categoriesLoading: false, | |||
dataLoading: false, | |||
submitting: false, | |||
categoriesList: [], | |||
baseDataList: [], | |||
selectedCategory: null, | |||
selectedCategoryId: null, | |||
selectedRows: [], | |||
searchForm: { | |||
productCode: "", | |||
productName: "", | |||
}, | |||
pagination: { | |||
page: 1, | |||
pageSize: 50, | |||
total: 0, | |||
}, | |||
dialogVisible: false, | |||
isEdit: false, | |||
dataForm: { | |||
id: null, | |||
productCode: "", | |||
productName: "", | |||
brand: "", | |||
categorySpecs: {}, | |||
}, | |||
dataRules: { | |||
productCode: [ | |||
{ required: true, message: "请输入商品编号", trigger: "blur" }, | |||
], | |||
productName: [ | |||
{ required: true, message: "请输入商品名称", trigger: "blur" }, | |||
], | |||
}, | |||
}; | |||
}, | |||
computed: { | |||
dialogTitle() { | |||
return this.isEdit ? "编辑基准数据" : "新增基准数据"; | |||
}, | |||
categoryFields() { | |||
return this.selectedCategory && this.selectedCategory.fieldConfig | |||
? this.parseFieldConfig(this.selectedCategory.fieldConfig) | |||
: []; | |||
}, | |||
uploadUrl() { | |||
return process.env.VUE_APP_BASE_API + "/basedata/import"; | |||
}, | |||
uploadData() { | |||
return { | |||
categoryId: this.selectedCategoryId, | |||
}; | |||
}, | |||
}, | |||
created() { | |||
this.fetchCategoriesList(); | |||
}, | |||
methods: { | |||
formatDateTime, | |||
// 解析字段配置字符串为 JSON 对象 | |||
parseFieldConfig(fieldConfig) { | |||
try { | |||
if (!fieldConfig || typeof fieldConfig === "object") { | |||
return fieldConfig || []; | |||
} | |||
return JSON.parse(fieldConfig); | |||
} catch (error) { | |||
console.warn("解析字段配置失败:", error); | |||
return []; | |||
} | |||
}, | |||
// 解析分类规格字符串为对象 | |||
parseCategorySpecs(categorySpecs) { | |||
try { | |||
if (!categorySpecs) { | |||
return {}; | |||
} | |||
if (typeof categorySpecs === "object") { | |||
return categorySpecs; | |||
} | |||
return JSON.parse(categorySpecs); | |||
} catch (error) { | |||
console.warn("解析分类规格失败:", error); | |||
return {}; | |||
} | |||
}, | |||
// 将分类规格对象转换为字符串 | |||
stringifyCategorySpecs(categorySpecs) { | |||
try { | |||
if (!categorySpecs || typeof categorySpecs === "string") { | |||
return categorySpecs || ""; | |||
} | |||
return JSON.stringify(categorySpecs); | |||
} catch (error) { | |||
console.warn("转换分类规格为字符串失败:", error); | |||
return ""; | |||
} | |||
}, | |||
// 获取分类列表 | |||
async fetchCategoriesList() { | |||
try { | |||
this.categoriesLoading = true; | |||
const response = await getCategoriesSimple(); | |||
if (response.code === 200) { | |||
this.categoriesList = response.data || []; | |||
} else { | |||
this.$message.error(response.message || "获取分类列表失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取分类列表失败:", error); | |||
this.$message.error("获取分类列表失败"); | |||
} finally { | |||
this.categoriesLoading = false; | |||
} | |||
}, | |||
// 选择分类 | |||
async selectCategory(category) { | |||
if (this.selectedCategoryId === category.id) return; | |||
this.selectedCategoryId = category.id; | |||
this.pagination.page = 1; | |||
this.resetSearch(); | |||
await this.fetchCategoryDetails(category.id); | |||
await this.fetchBaseDataList(); | |||
}, | |||
// 获取分类详情 | |||
async fetchCategoryDetails(categoryId) { | |||
try { | |||
const response = await getCategoryDetail(categoryId); | |||
if (response.code === 200) { | |||
this.selectedCategory = response.data; | |||
} else { | |||
this.$message.error(response.message || "获取分类详情失败"); | |||
this.selectedCategory = null; // 获取失败时重置 | |||
} | |||
} catch (error) { | |||
console.error("获取分类详情失败:", error); | |||
this.$message.error("获取分类详情失败"); | |||
this.selectedCategory = null; | |||
} | |||
}, | |||
// 获取基准数据列表 | |||
async fetchBaseDataList() { | |||
if (!this.selectedCategoryId) return; | |||
try { | |||
this.dataLoading = true; | |||
const params = { | |||
page: this.pagination.page, | |||
pageSize: this.pagination.pageSize, | |||
categoryId: this.selectedCategoryId, | |||
productCode: this.searchForm.productCode, | |||
productName: this.searchForm.productName, | |||
}; | |||
const response = await getBaseDataList(params); | |||
if (response.code === 200) { | |||
// 处理 categorySpecs 字段,将字符串转换为对象 | |||
const list = response.data.list || []; | |||
this.baseDataList = list.map((item) => ({ | |||
...item, | |||
categorySpecs: this.parseCategorySpecs(item.categorySpecs), | |||
})); | |||
this.pagination.total = response.data.pagination.total; | |||
} else { | |||
this.$message.error(response.message || "获取基准数据失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取基准数据失败:", error); | |||
this.$message.error("获取基准数据失败"); | |||
} finally { | |||
this.dataLoading = false; | |||
} | |||
}, | |||
// 搜索 | |||
handleSearch() { | |||
this.pagination.page = 1; | |||
this.fetchBaseDataList(); | |||
}, | |||
// 重置搜索 | |||
resetSearch() { | |||
this.searchForm.productCode = ""; | |||
this.searchForm.productName = ""; | |||
this.handleSearch(); | |||
}, | |||
// 分页大小改变 | |||
handleSizeChange(val) { | |||
this.pagination.pageSize = val; | |||
this.pagination.page = 1; | |||
this.fetchBaseDataList(); | |||
}, | |||
// 当前页改变 | |||
handleCurrentChange(val) { | |||
this.pagination.page = val; | |||
this.fetchBaseDataList(); | |||
}, | |||
// 表格选择改变 | |||
handleSelectionChange(selection) { | |||
this.selectedRows = selection; | |||
}, | |||
// 新增数据 | |||
handleAdd() { | |||
this.isEdit = false; | |||
this.dataForm = { | |||
id: null, | |||
productCode: "", | |||
productName: "", | |||
brand: "", | |||
categorySpecs: {}, | |||
}; | |||
// 初始化分类规格字段 | |||
this.categoryFields.forEach((field) => { | |||
this.$set(this.dataForm.categorySpecs, field.fieldName, ""); | |||
}); | |||
this.dialogVisible = true; | |||
}, | |||
// 编辑数据 | |||
handleEdit(row) { | |||
this.isEdit = true; | |||
this.dataForm = { | |||
id: row.id, | |||
productCode: row.productCode, | |||
productName: row.productName, | |||
brand: row.brand || "", | |||
categorySpecs: { ...row.categorySpecs }, | |||
}; | |||
this.dialogVisible = true; | |||
}, | |||
// 删除数据 | |||
handleDelete(row) { | |||
this.$confirm(`确定要删除商品"${row.productName}"吗?`, "提示", { | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
}) | |||
.then(async () => { | |||
try { | |||
const response = await deleteBaseData(row.id); | |||
if (response.code === 200) { | |||
this.$message.success("删除成功"); | |||
this.fetchBaseDataList(); | |||
} else { | |||
this.$message.error(response.message || "删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("删除失败:", error); | |||
this.$message.error("删除失败"); | |||
} | |||
}) | |||
.catch(() => {}); | |||
}, | |||
// 批量删除 | |||
handleBatchDelete() { | |||
if (!this.selectedRows.length) { | |||
this.$message.warning("请选择要删除的数据"); | |||
return; | |||
} | |||
const ids = this.selectedRows.map((row) => row.id).join(","); | |||
this.$confirm( | |||
`确定要删除选中的${this.selectedRows.length}条数据吗?`, | |||
"提示", | |||
{ | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
} | |||
) | |||
.then(async () => { | |||
try { | |||
const response = await batchDeleteBaseData(ids); | |||
if (response.code === 200) { | |||
this.$message.success("批量删除成功"); | |||
this.fetchBaseDataList(); | |||
} else { | |||
this.$message.error(response.message || "批量删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("批量删除失败:", error); | |||
this.$message.error("批量删除失败"); | |||
} | |||
}) | |||
.catch(() => {}); | |||
}, | |||
// 导出Excel | |||
async handleExport() { | |||
try { | |||
const response = await exportBaseData(this.selectedCategoryId); | |||
// 创建下载链接 | |||
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 = `basedata-${ | |||
this.selectedCategory?.name || "data" | |||
}-${new Date().toLocaleDateString()}.xlsx`; | |||
document.body.appendChild(link); | |||
link.click(); | |||
document.body.removeChild(link); | |||
window.URL.revokeObjectURL(url); | |||
this.$message.success("导出成功"); | |||
} catch (error) { | |||
console.error("导出失败:", error); | |||
this.$message.error("导出失败"); | |||
} | |||
}, | |||
// 上传前验证 | |||
beforeUpload(file) { | |||
const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase()); | |||
if (!isExcel) { | |||
this.$message.error("只能上传Excel文件!"); | |||
return false; | |||
} | |||
const isLt10M = file.size / 1024 / 1024 < 10; | |||
if (!isLt10M) { | |||
this.$message.error("上传文件大小不能超过10MB!"); | |||
return false; | |||
} | |||
return true; | |||
}, | |||
// 上传成功 | |||
onUploadSuccess(response) { | |||
if (response.code === 200) { | |||
const data = response.data || {}; | |||
let message = response.message || "导入成功"; | |||
// 如果有错误详情,显示详细信息 | |||
if (data.errors > 0) { | |||
message += `\n${data.errors}条数据有问题已跳过`; | |||
if (data.errorDetails && data.errorDetails.length > 0) { | |||
this.$alert( | |||
`导入完成!\n\n统计信息:\n- 总共处理:${ | |||
data.total | |||
}条\n- 成功导入:${data.imported}条\n- 跳过错误:${ | |||
data.errors | |||
}条\n\n错误详情(前10条):\n${data.errorDetails | |||
.slice(0, 10) | |||
.join("\n")}`, | |||
"导入结果", | |||
{ | |||
confirmButtonText: "确定", | |||
type: "warning", | |||
} | |||
); | |||
} else { | |||
this.$message.warning(message); | |||
} | |||
} else { | |||
this.$message.success(message); | |||
} | |||
this.fetchBaseDataList(); | |||
} else { | |||
this.$message.error(response.message || "导入失败"); | |||
} | |||
}, | |||
// 上传失败 | |||
onUploadError(error) { | |||
console.error("上传失败:", error); | |||
this.$message.error("文件上传失败"); | |||
}, | |||
// 提交表单 | |||
async handleSubmit() { | |||
try { | |||
await this.$refs.dataForm.validate(); | |||
this.submitting = true; | |||
// 将 categorySpecs 对象转换为字符串 | |||
const submitData = { | |||
...this.dataForm, | |||
categoryId: this.selectedCategoryId, | |||
categorySpecs: this.stringifyCategorySpecs( | |||
this.dataForm.categorySpecs | |||
), | |||
}; | |||
const response = this.isEdit | |||
? await BaseDataUpdate(this.dataForm.id, submitData) | |||
: await BaseDataAdd(submitData); | |||
if (response.code === 200) { | |||
this.$message.success(this.isEdit ? "编辑成功" : "新增成功"); | |||
this.dialogVisible = false; | |||
this.fetchBaseDataList(); | |||
} else { | |||
this.$message.error(response.message || "操作失败"); | |||
} | |||
} catch (error) { | |||
if (error.message !== "validation failed") { | |||
console.error("提交失败:", error); | |||
this.$message.error("操作失败"); | |||
} | |||
} finally { | |||
this.submitting = false; | |||
} | |||
}, | |||
// 对话框关闭 | |||
handleDialogClose() { | |||
this.$refs.dataForm.resetFields(); | |||
this.dataForm.categorySpecs = {}; | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.base-data-layout { | |||
display: flex; | |||
gap: 10px; | |||
padding: 10px; | |||
} | |||
.category-sidebar { | |||
width: 280px; | |||
height: 100%; | |||
flex-shrink: 0; | |||
display: flex; | |||
flex-direction: column; | |||
background-color: #fff; | |||
padding: 10px; | |||
border-radius: 6px; | |||
} | |||
.sidebar-header i { | |||
margin-right: 8px; | |||
} | |||
.category-list-wrapper { | |||
flex-grow: 1; | |||
overflow-y: auto; | |||
padding: 10px 0; | |||
} | |||
.category-item { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
padding: 15px; | |||
border-radius: 6px; | |||
cursor: pointer; | |||
transition: background-color 0.2s, color 0.2s; | |||
margin-bottom: 5px; | |||
} | |||
.category-item:hover { | |||
background-color: #f5f7fa; | |||
} | |||
.category-item.is-active { | |||
background-color: #ecf5ff; | |||
color: #409eff; | |||
font-weight: 500; | |||
} | |||
.category-name { | |||
font-size: 14px; | |||
} | |||
.main-content-area { | |||
width: 0; | |||
flex-grow: 1; | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
.placeholder-view { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
height: 100%; | |||
} | |||
.data-view { | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
.pagination-container { | |||
display: flex; | |||
justify-content: flex-end; | |||
margin-top: 20px; | |||
} | |||
.search-filters { | |||
display: flex; | |||
gap: 10px; | |||
align-items: center; | |||
flex-wrap: wrap; | |||
} | |||
</style> |
@@ -0,0 +1,524 @@ | |||
<template> | |||
<div class="categories-container app-container"> | |||
<div class="filter-container"> | |||
<div class="search-filters"> | |||
<el-input | |||
v-model="searchForm.name" | |||
placeholder="按分类名称搜索" | |||
clearable | |||
@clear="handleSearch" | |||
@keyup.enter.native="handleSearch" | |||
class="filter-item" | |||
style="width: 240px" | |||
size="small" | |||
> | |||
</el-input> | |||
<el-button icon="el-icon-search" type="primary" @click="handleSearch" size="small"></el-button> | |||
<el-button | |||
type="primary" | |||
icon="el-icon-plus" | |||
@click="handleAdd" | |||
size="small" | |||
style="margin-left: auto" | |||
>新增分类</el-button | |||
> | |||
</div> | |||
</div> | |||
<el-table | |||
v-loading="loading" | |||
:data="categoryList" | |||
style="width: 100%" | |||
row-key="id" | |||
size="small" | |||
> | |||
<el-table-column | |||
prop="id" | |||
label="ID" | |||
width="80" | |||
align="center" | |||
></el-table-column> | |||
<el-table-column | |||
prop="name" | |||
label="分类名称" | |||
min-width="180" | |||
show-overflow-tooltip | |||
></el-table-column> | |||
<el-table-column | |||
prop="description" | |||
label="分类描述" | |||
min-width="150" | |||
show-overflow-tooltip | |||
></el-table-column> | |||
<el-table-column label="字段配置" min-width="300"> | |||
<template slot-scope="scope"> | |||
<div | |||
v-if="scope.row.fieldConfig && scope.row.fieldConfig.length" | |||
class="field-tags-container" | |||
> | |||
<el-tag | |||
v-for="field in parseFieldConfig(scope.row.fieldConfig)" | |||
:key="field.fieldName" | |||
:type="getFieldTagType(field.fieldType)" | |||
size="small" | |||
class="field-tag" | |||
style="margin-right: 10px" | |||
> | |||
<i | |||
:class="getFieldIcon(field.fieldType)" | |||
style="margin-right: 4px" | |||
></i> | |||
<strong>{{ field.displayLabel }}</strong> ({{ field.fieldType }}) | |||
<span v-if="field.isRequired" class="required-indicator">*</span> | |||
</el-tag> | |||
</div> | |||
<span v-else class="text-placeholder">暂无配置</span> | |||
</template> | |||
</el-table-column> | |||
<el-table-column | |||
prop="createdAt" | |||
label="创建时间" | |||
width="200" | |||
align="center" | |||
> | |||
<template slot-scope="scope"> | |||
<i class="el-icon-time" style="margin-right: 5px"></i> | |||
<span>{{ formatDateTime(scope.row.createdAt) }}</span> | |||
</template> | |||
</el-table-column> | |||
<el-table-column label="操作" width="180" fixed="right" align="center"> | |||
<template slot-scope="scope"> | |||
<el-button | |||
type="primary" | |||
plain | |||
size="mini" | |||
@click="handleEdit(scope.row)" | |||
>编辑</el-button | |||
> | |||
<el-popconfirm | |||
title="确定删除这个分类吗?" | |||
@confirm="handleDelete(scope.row)" | |||
style="margin-left: 10px" | |||
> | |||
<el-button slot="reference" type="danger" plain size="mini" | |||
>删除</el-button | |||
> | |||
</el-popconfirm> | |||
</template> | |||
</el-table-column> | |||
</el-table> | |||
<pagination | |||
v-show="pagination.total > 0" | |||
:total="pagination.total" | |||
:page.sync="pagination.page" | |||
:limit.sync="pagination.pageSize" | |||
@pagination="fetchCategoryList" | |||
/> | |||
<el-dialog | |||
:title="dialogTitle" | |||
:visible.sync="dialogVisible" | |||
width="850px" | |||
:close-on-click-modal="false" | |||
@close="handleDialogClose" | |||
top="5vh" | |||
custom-class="category-dialog" | |||
> | |||
<el-form | |||
ref="categoryForm" | |||
:model="categoryForm" | |||
:rules="categoryRules" | |||
label-width="100px" | |||
> | |||
<el-form-item label="分类名称" prop="name"> | |||
<el-input | |||
v-model="categoryForm.name" | |||
placeholder="请输入分类名称" | |||
></el-input> | |||
</el-form-item> | |||
<el-form-item label="分类描述" prop="description"> | |||
<el-input | |||
v-model="categoryForm.description" | |||
type="textarea" | |||
:rows="3" | |||
placeholder="请输入分类描述" | |||
></el-input> | |||
</el-form-item> | |||
<el-form-item label="字段配置"> | |||
<div class="field-config-panel"> | |||
<div class="field-config-header"> | |||
<h4 class="field-config-title">自定义字段</h4> | |||
<el-button | |||
type="primary" | |||
size="small" | |||
@click="addField" | |||
icon="el-icon-plus" | |||
plain | |||
>添加字段</el-button | |||
> | |||
</div> | |||
<div | |||
v-if="categoryForm.fieldConfig.length" | |||
class="field-list-container" | |||
> | |||
<transition-group name="fade" tag="div"> | |||
<div | |||
v-for="(field, index) in categoryForm.fieldConfig" | |||
:key="field.id || index" | |||
class="field-item-row" | |||
> | |||
<el-row :gutter="15" align="middle"> | |||
<el-col :span="5"> | |||
<el-form-item | |||
:prop="'fieldConfig.' + index + '.fieldName'" | |||
:rules="{ | |||
required: true, | |||
message: '字段名不能为空', | |||
trigger: 'blur', | |||
}" | |||
> | |||
<el-input | |||
v-model="field.fieldName" | |||
placeholder="字段名称 (英文)" | |||
size="small" | |||
></el-input> | |||
</el-form-item> | |||
</el-col> | |||
<el-col :span="5"> | |||
<el-form-item | |||
:prop="'fieldConfig.' + index + '.displayLabel'" | |||
:rules="{ | |||
required: true, | |||
message: '显示标签不能为空', | |||
trigger: 'blur', | |||
}" | |||
> | |||
<el-input | |||
v-model="field.displayLabel" | |||
placeholder="显示标签 (中文)" | |||
size="small" | |||
></el-input> | |||
</el-form-item> | |||
</el-col> | |||
<el-col :span="5"> | |||
<el-select | |||
v-model="field.fieldType" | |||
placeholder="字段类型" | |||
size="small" | |||
style="width: 100%" | |||
> | |||
<el-option label="文本" value="text"></el-option> | |||
<el-option label="数字" value="number"></el-option> | |||
<el-option label="日期" value="date"></el-option> | |||
</el-select> | |||
</el-col> | |||
<el-col :span="4" class="text-center"> | |||
<el-checkbox v-model="field.isRequired" | |||
>设为必填</el-checkbox | |||
> | |||
</el-col> | |||
<el-col :span="3" class="text-center"> | |||
<el-button | |||
type="danger" | |||
size="mini" | |||
@click="removeField(index)" | |||
icon="el-icon-delete" | |||
circle | |||
plain | |||
></el-button> | |||
</el-col> | |||
</el-row> | |||
</div> | |||
</transition-group> | |||
</div> | |||
<div v-else class="no-fields-placeholder"> | |||
<i class="el-icon-document-add placeholder-icon"></i> | |||
<p>暂无字段配置,点击 "添加字段" 开始创建</p> | |||
</div> | |||
</div> | |||
</el-form-item> | |||
</el-form> | |||
<div slot="footer" class="dialog-footer"> | |||
<el-button @click="dialogVisible = false">取消</el-button> | |||
<el-button type="primary" :loading="submitting" @click="handleSubmit" | |||
>确定</el-button | |||
> | |||
</div> | |||
</el-dialog> | |||
</div> | |||
</template> | |||
<script> | |||
import { formatDateTime } from "@/utils/filters"; | |||
import { | |||
getCategoriesList, | |||
createCategory, | |||
updateCategory, | |||
deleteCategory, | |||
} from "@/api/sales-analysis"; | |||
export default { | |||
name: "Categories", | |||
data() { | |||
return { | |||
loading: false, | |||
submitting: false, | |||
categoryList: [], | |||
searchForm: { | |||
name: "", | |||
}, | |||
pagination: { | |||
page: 1, | |||
pageSize: 10, | |||
total: 0, | |||
}, | |||
dialogVisible: false, | |||
isEdit: false, | |||
categoryForm: { | |||
id: null, | |||
name: "", | |||
description: "", | |||
fieldConfig: [], | |||
}, | |||
categoryRules: { | |||
name: [ | |||
{ required: true, message: "请输入分类名称", trigger: "blur" }, | |||
{ | |||
min: 1, | |||
max: 100, | |||
message: "分类名称长度在 1 到 100 个字符", | |||
trigger: "blur", | |||
}, | |||
], | |||
description: [{ required: false }], | |||
}, | |||
}; | |||
}, | |||
computed: { | |||
dialogTitle() { | |||
return this.isEdit ? "编辑分类" : "新增分类"; | |||
}, | |||
}, | |||
created() { | |||
this.fetchCategoryList(); | |||
}, | |||
methods: { | |||
formatDateTime, | |||
// 解析字段配置字符串为 JSON 对象 | |||
parseFieldConfig(fieldConfig) { | |||
try { | |||
if (!fieldConfig || typeof fieldConfig === 'object') { | |||
return fieldConfig || []; | |||
} | |||
return JSON.parse(fieldConfig); | |||
} catch (error) { | |||
console.warn('解析字段配置失败:', error); | |||
return []; | |||
} | |||
}, | |||
// 获取分类列表 | |||
async fetchCategoryList() { | |||
try { | |||
this.loading = true; | |||
const params = { | |||
page: this.pagination.page, | |||
pageSize: this.pagination.pageSize, | |||
name: this.searchForm.name, | |||
}; | |||
const response = await getCategoriesList(params); | |||
console.log(response); | |||
if (response.code === 200) { | |||
// 处理返回的数据,将 fieldConfig 字符串转换为 JSON 对象 | |||
const categoryList = response.data.list || []; | |||
this.categoryList = categoryList.map(category => ({ | |||
...category, | |||
fieldConfig: this.parseFieldConfig(category.fieldConfig) | |||
})); | |||
this.pagination.total = response.data.pagination.total || 0; | |||
} else { | |||
this.$message.error(response.message || "获取分类列表失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取分类列表失败:", error); | |||
this.$message.error("获取分类列表失败"); | |||
} finally { | |||
this.loading = false; | |||
} | |||
}, | |||
// 搜索 | |||
handleSearch() { | |||
this.pagination.page = 1; | |||
this.fetchCategoryList(); | |||
}, | |||
// 分页大小改变 | |||
handleSizeChange(val) { | |||
this.pagination.pageSize = val; | |||
this.pagination.page = 1; | |||
this.fetchCategoryList(); | |||
}, | |||
// 当前页改变 | |||
handleCurrentChange(val) { | |||
this.pagination.page = val; | |||
this.fetchCategoryList(); | |||
}, | |||
getFieldTagType(type) { | |||
switch (type) { | |||
case "text": | |||
return "primary"; | |||
case "number": | |||
return "warning"; | |||
case "date": | |||
return "success"; | |||
case "select": | |||
return "info"; | |||
default: | |||
return "info"; | |||
} | |||
}, | |||
getFieldIcon(type) { | |||
switch (type) { | |||
case "text": | |||
return "el-icon-tickets"; | |||
case "number": | |||
return "el-icon-sort"; | |||
case "date": | |||
return "el-icon-date"; | |||
case "select": | |||
return "el-icon-arrow-down"; | |||
default: | |||
return "el-icon-document"; | |||
} | |||
}, | |||
// 新增分类 | |||
handleAdd() { | |||
this.isEdit = false; | |||
this.categoryForm = { | |||
id: null, | |||
name: "", | |||
description: "", | |||
fieldConfig: [], | |||
}; | |||
this.dialogVisible = true; | |||
}, | |||
// 编辑分类 | |||
handleEdit(row) { | |||
this.isEdit = true; | |||
this.categoryForm = { | |||
id: row.id, | |||
name: row.name, | |||
description: row.description || "", | |||
fieldConfig: JSON.parse(JSON.stringify(this.parseFieldConfig(row.fieldConfig))), | |||
}; | |||
this.dialogVisible = true; | |||
}, | |||
// 删除分类 | |||
handleDelete(row) { | |||
this.$confirm(`确定要删除分类"${row.name}"吗?`, "提示", { | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
}) | |||
.then(async () => { | |||
try { | |||
const response = await deleteCategory(row.id); | |||
if (response.code === 200) { | |||
this.$message.success("删除成功"); | |||
this.fetchCategoryList(); | |||
} else { | |||
this.$message.error(response.message || "删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("删除分类失败:", error); | |||
this.$message.error("删除失败"); | |||
} | |||
}) | |||
.catch(() => {}); | |||
}, | |||
// 添加字段 | |||
addField() { | |||
this.categoryForm.fieldConfig.push({ | |||
id: Date.now(), // 添加一个唯一ID用于key | |||
fieldName: "", | |||
displayLabel: "", | |||
fieldType: "text", | |||
isRequired: false, | |||
}); | |||
}, | |||
// 移除字段 | |||
removeField(index) { | |||
this.categoryForm.fieldConfig.splice(index, 1); | |||
}, | |||
// 提交表单 | |||
async handleSubmit() { | |||
try { | |||
await this.$refs.categoryForm.validate(); | |||
// 验证字段配置 | |||
if (this.categoryForm.fieldConfig.length > 0) { | |||
const invalidFields = this.categoryForm.fieldConfig.filter( | |||
(field) => | |||
!field.fieldName || !field.displayLabel || !field.fieldType | |||
); | |||
if (invalidFields.length > 0) { | |||
this.$message.error("请完善字段配置信息"); | |||
return; | |||
} | |||
} | |||
this.submitting = true; | |||
// 创建提交数据,将 fieldConfig 转换为字符串 | |||
const submitData = { | |||
...this.categoryForm, | |||
fieldConfig: JSON.stringify(this.categoryForm.fieldConfig) | |||
}; | |||
const response = this.isEdit | |||
? await updateCategory(this.categoryForm.id, submitData) | |||
: await createCategory(submitData); | |||
if (response.code === 200) { | |||
this.$message.success(this.isEdit ? "编辑成功" : "新增成功"); | |||
this.dialogVisible = false; | |||
this.fetchCategoryList(); | |||
} else { | |||
this.$message.error(response.message || "操作失败"); | |||
} | |||
} catch (error) { | |||
if (error.message !== "validation failed") { | |||
console.error("提交失败:", error); | |||
this.$message.error("操作失败"); | |||
} | |||
} finally { | |||
this.submitting = false; | |||
} | |||
}, | |||
// 对话框关闭 | |||
handleDialogClose() { | |||
this.$refs.categoryForm.resetFields(); | |||
this.categoryForm.fieldConfig = []; | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -0,0 +1,939 @@ | |||
<template> | |||
<div class="overall-analysis-page"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
<div class="filter-row"> | |||
<div class="filter-item"> | |||
<label>分类:</label> | |||
<el-select | |||
v-model="filters.category" | |||
placeholder="请选择分类(默认全部)" | |||
clearable | |||
filterable | |||
style="width: 280px" | |||
size="small" | |||
> | |||
<el-option | |||
v-for="category in filterOptions.categories" | |||
:key="category" | |||
:label="category" | |||
:value="category" | |||
/> | |||
</el-select> | |||
</div> | |||
<div class="filter-item"> | |||
<label>日期:</label> | |||
<el-date-picker | |||
v-model="dateRange" | |||
type="daterange" | |||
range-separator="至" | |||
start-placeholder="开始日期" | |||
end-placeholder="结束日期" | |||
format="yyyy-MM-dd" | |||
value-format="yyyy-MM-dd" | |||
@change="handleDateChange" | |||
style="width: 380px" | |||
size="small" | |||
/> | |||
</div> | |||
<div class="filter-item"> | |||
<el-button type="primary" @click="fetchData" :loading="loading" size="small"> | |||
查询分析 | |||
</el-button> | |||
<el-button @click="resetFilter" size="small">重置筛选</el-button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 图表区域 --> | |||
<div class="charts-section" v-if="Object.keys(dimensionCharts).length > 0"> | |||
<div | |||
v-for="(chartData, dimension) in dimensionCharts" | |||
:key="dimension" | |||
class="dimension-section" | |||
> | |||
<div class="dimension-header"> | |||
<div class="dimension-title"> | |||
<i class="el-icon-data-analysis"></i> | |||
<span>{{ dimension }}维度分析</span> | |||
</div> | |||
<div class="dimension-stats"> | |||
<span class="stat-item category-stat"> | |||
<i class="el-icon-folder-opened"></i> | |||
归属分类: <strong>{{ currentCategory }}</strong> | |||
</span> | |||
<span class="stat-item"> | |||
<i class="el-icon-money"></i> | |||
金额项: {{ chartData.amountData.length }} | |||
</span> | |||
<span class="stat-item"> | |||
<i class="el-icon-s-goods"></i> | |||
数量项: {{ chartData.quantityData.length }} | |||
</span> | |||
</div> | |||
</div> | |||
<el-row :gutter="24"> | |||
<el-col :span="12"> | |||
<!-- 销售金额占比 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<div class="header-left"> | |||
<i class="el-icon-money header-icon-amount"></i> | |||
<span>销售金额占比</span> | |||
</div> | |||
<div class="header-right"> | |||
<span class="total-amount" | |||
>总计: ¥{{ | |||
formatNumber(getTotalAmount(chartData.amountData)) | |||
}}</span | |||
> | |||
</div> | |||
</div> | |||
<div class="chart-container"> | |||
<div | |||
:id="`${dimension}AmountChart`" | |||
style="width: 100%; height: 450px" | |||
></div> | |||
</div> | |||
</div> | |||
</el-col> | |||
<el-col :span="12"> | |||
<!-- 销售数量占比 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<div class="header-left"> | |||
<i class="el-icon-s-goods header-icon-quantity"></i> | |||
<span>销售数量占比</span> | |||
</div> | |||
<div class="header-right"> | |||
<span class="total-quantity" | |||
>总计: | |||
{{ | |||
formatNumber(getTotalQuantity(chartData.quantityData)) | |||
}}</span | |||
> | |||
</div> | |||
</div> | |||
<div class="chart-container"> | |||
<div | |||
:id="`${dimension}QuantityChart`" | |||
style="width: 100%; height: 450px" | |||
></div> | |||
</div> | |||
</div> | |||
</el-col> | |||
</el-row> | |||
</div> | |||
</div> | |||
<!-- 无数据提示 --> | |||
<div v-else-if="!loading" class="no-data-section"> | |||
<div class="no-data-content"> | |||
<i class="el-icon-warning-outline"></i> | |||
<p>暂无数据</p> | |||
<small>请尝试调整筛选条件或检查数据</small> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import * as echarts from "echarts"; | |||
import { getReportFilterOptions, getCategoryAnalysisReport } from '@/api/sales-analysis' | |||
export default { | |||
name: "CategoryAnalysis", | |||
components: {}, | |||
data() { | |||
return { | |||
loading: false, | |||
dateRange: null, | |||
filters: { | |||
category: "", | |||
}, | |||
filterOptions: { | |||
categories: [], | |||
}, | |||
dimensionCharts: {}, | |||
chartInstances: new Map(), // 存储图表实例 | |||
currentCategory: "全部分类", | |||
}; | |||
}, | |||
mounted() { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
// this.fetchData(); | |||
}, | |||
beforeDestroy() { | |||
// 销毁所有图表实例 | |||
this.chartInstances.forEach((chart) => { | |||
if (chart) { | |||
chart.dispose(); | |||
} | |||
}); | |||
this.chartInstances.clear(); | |||
}, | |||
methods: { | |||
// 初始化日期范围(默认最近30天) | |||
initDateRange() { | |||
const endDate = new Date(); | |||
const startDate = new Date(); | |||
startDate.setDate(endDate.getDate() - 30); | |||
this.dateRange = [ | |||
startDate.toISOString().split("T")[0], | |||
endDate.toISOString().split("T")[0], | |||
]; | |||
}, | |||
// 日期变化处理 | |||
handleDateChange(dates) { | |||
this.dateRange = dates; | |||
}, | |||
// 重置筛选 | |||
resetFilter() { | |||
this.initDateRange(); | |||
this.fetchData(); | |||
}, | |||
// 获取筛选选项 | |||
async fetchFilterOptions() { | |||
try { | |||
const response = await getReportFilterOptions(); | |||
if (response.code === 200) { | |||
this.filterOptions = { | |||
categories: response.data.categories || [], | |||
}; | |||
} else { | |||
console.error("获取筛选选项失败:", response.message); | |||
} | |||
} catch (error) { | |||
console.error("获取筛选选项失败:", error); | |||
} | |||
}, | |||
// 获取数据 | |||
async fetchData() { | |||
if (!this.dateRange || this.dateRange.length !== 2) { | |||
this.$message.warning("请选择日期范围"); | |||
return; | |||
} | |||
try { | |||
this.loading = true; | |||
// 构建查询参数 | |||
const params = { | |||
startDate: this.dateRange[0], | |||
endDate: this.dateRange[1], | |||
}; | |||
// 添加分类筛选条件(如果选择了特定分类) | |||
if (this.filters.category) { | |||
params.category = this.filters.category; | |||
} | |||
const response = await getCategoryAnalysisReport(params); | |||
if (response.code === 200) { | |||
this.dimensionCharts = response.data.dimensionCharts || {}; | |||
this.currentCategory = response.data.currentCategory || "全部分类"; | |||
this.$nextTick(() => { | |||
this.renderAllCharts(); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取数据失败:", error); | |||
this.$message.error("获取数据失败"); | |||
} finally { | |||
this.loading = false; | |||
} | |||
}, | |||
// 渲染所有图表 | |||
renderAllCharts() { | |||
// 先销毁所有现有图表 | |||
this.chartInstances.forEach((chart) => { | |||
if (chart) { | |||
chart.dispose(); | |||
} | |||
}); | |||
this.chartInstances.clear(); | |||
// 为每个维度渲染图表 | |||
for (const [dimension, chartData] of Object.entries( | |||
this.dimensionCharts | |||
)) { | |||
this.renderDimensionCharts(dimension, chartData); | |||
} | |||
}, | |||
// 渲染单个维度的图表 | |||
renderDimensionCharts(dimension, chartData) { | |||
// 渲染金额占比图表 | |||
this.renderAmountChart(dimension, chartData.amountData); | |||
// 渲染数量占比图表 | |||
this.renderQuantityChart(dimension, chartData.quantityData); | |||
}, | |||
// 渲染金额占比图表 | |||
renderAmountChart(dimension, data) { | |||
const chartId = `${dimension}AmountChart`; | |||
const chartDom = document.getElementById(chartId); | |||
if (!chartDom || !data || data.length === 0) return; | |||
const chart = echarts.init(chartDom); | |||
this.chartInstances.set(chartId, chart); | |||
const totalAmount = this.getTotalAmount(data); | |||
const processedData = this.processChartData(data); | |||
const option = { | |||
tooltip: { | |||
trigger: "item", | |||
backgroundColor: "rgba(255, 255, 255, 0.98)", | |||
borderColor: "#E8E8E8", | |||
borderWidth: 1, | |||
textStyle: { | |||
color: "#333", | |||
fontSize: 13, | |||
}, | |||
padding: 12, | |||
formatter: (params) => { | |||
const percentage = params.data.percentage; | |||
const amount = this.formatNumber(params.value); | |||
return ` | |||
<div style="font-weight: 600; margin-bottom: 8px;">${params.name}</div> | |||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; min-width: 150px; gap: 20px;"> | |||
<span style="color: #666;">销售额</span> | |||
<span style="font-weight: 600; color: #D9534F;">¥${amount}</span> | |||
</div> | |||
<div style="display: flex; justify-content: space-between; align-items: center;"> | |||
<span style="color: #666;">占比</span> | |||
<span style="font-weight: 600; color: #5B9BD5;">${percentage}%</span> | |||
</div> | |||
`; | |||
}, | |||
}, | |||
legend: { | |||
type: "scroll", | |||
orient: "vertical", | |||
right: 20, | |||
top: "center", | |||
data: processedData.map((item) => "" + item.name + ""), | |||
textStyle: { | |||
color: "#666", | |||
fontSize: 14, | |||
}, | |||
itemWidth: 20, | |||
itemHeight: 20, | |||
itemGap: 10, | |||
formatter: (name) => { | |||
const item = processedData.find((p) => p.name === name); | |||
const displayName = | |||
name.length > 15 ? name.slice(0, 15) + "..." : name; | |||
if (item && item.percentage !== undefined) { | |||
return `${displayName} ${item.percentage}%`; | |||
} | |||
return displayName; | |||
}, | |||
}, | |||
series: [ | |||
{ | |||
name: "销售金额", | |||
type: "pie", | |||
radius: ["50%", "75%"], | |||
center: ["38%", "50%"], | |||
avoidLabelOverlap: false, | |||
itemStyle: { | |||
borderColor: "#fff", | |||
borderWidth: 2, | |||
}, | |||
label: { | |||
show: false, | |||
position: "center", | |||
formatter: () => { | |||
return `{total|¥${this.formatNumber( | |||
totalAmount, | |||
true | |||
)}}\n{name|总金额}`; | |||
}, | |||
rich: { | |||
total: { | |||
fontSize: 22, | |||
fontWeight: "bold", | |||
color: "#D9534F", | |||
}, | |||
name: { | |||
fontSize: 14, | |||
color: "#666", | |||
padding: [4, 0, 0, 0], | |||
}, | |||
}, | |||
}, | |||
emphasis: { | |||
scale: true, | |||
scaleSize: 10, | |||
label: { | |||
show: true, | |||
formatter: (params) => { | |||
return `{val|${params.percent}%}\n{name|${params.name}}`; | |||
}, | |||
rich: { | |||
val: { | |||
fontSize: 22, | |||
fontWeight: "bold", | |||
color: "#D9534F", | |||
}, | |||
name: { | |||
fontSize: 14, | |||
color: "#666", | |||
padding: [4, 0, 0, 0], | |||
}, | |||
}, | |||
}, | |||
}, | |||
labelLine: { | |||
show: false, | |||
}, | |||
data: processedData, | |||
color: [ | |||
"#5B9BD5", | |||
"#ED7D31", | |||
"#A5A5A5", | |||
"#FFC000", | |||
"#4472C4", | |||
"#70AD47", | |||
"#255E91", | |||
"#9E480E", | |||
"#636363", | |||
"#997300", | |||
], | |||
}, | |||
], | |||
}; | |||
chart.setOption(option); | |||
}, | |||
// 渲染数量占比图表 | |||
renderQuantityChart(dimension, data) { | |||
const chartId = `${dimension}QuantityChart`; | |||
const chartDom = document.getElementById(chartId); | |||
if (!chartDom || !data || data.length === 0) return; | |||
const chart = echarts.init(chartDom); | |||
this.chartInstances.set(chartId, chart); | |||
const totalQuantity = this.getTotalQuantity(data); | |||
const processedData = this.processChartData(data, false); | |||
const option = { | |||
tooltip: { | |||
trigger: "item", | |||
backgroundColor: "rgba(255, 255, 255, 0.98)", | |||
borderColor: "#E8E8E8", | |||
borderWidth: 1, | |||
textStyle: { | |||
color: "#333", | |||
fontSize: 13, | |||
}, | |||
padding: 12, | |||
formatter: (params) => { | |||
const percentage = params.data.percentage; | |||
const quantity = this.formatNumber(params.value); | |||
return ` | |||
<div style="font-weight: 600; margin-bottom: 8px;">${params.name}</div> | |||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; min-width: 150px; gap: 20px;"> | |||
<span style="color: #666;">销售数量</span> | |||
<span style="font-weight: 600; color: #5CB85C;">${quantity}</span> | |||
</div> | |||
<div style="display: flex; justify-content: space-between; align-items: center;"> | |||
<span style="color: #666;">占比</span> | |||
<span style="font-weight: 600; color: #5B9BD5;">${percentage}%</span> | |||
</div> | |||
`; | |||
}, | |||
}, | |||
legend: { | |||
type: "scroll", | |||
orient: "vertical", | |||
right: 20, | |||
top: "center", | |||
show: true, | |||
data: processedData.map((item) => "" + item.name + ""), | |||
textStyle: { | |||
color: "#666", | |||
fontSize: 14, | |||
}, | |||
itemWidth: 20, | |||
itemHeight: 20, | |||
itemGap: 10, | |||
formatter: (name) => { | |||
const item = processedData.find((p) => p.name === name); | |||
const displayName = | |||
name.length > 30 ? name.slice(0, 30) + "..." : name; | |||
if (item && item.percentage !== undefined) { | |||
return `${displayName} ${item.percentage}%`; | |||
} | |||
return displayName; | |||
}, | |||
}, | |||
series: [ | |||
{ | |||
name: "销售数量", | |||
type: "pie", | |||
radius: ["50%", "75%"], | |||
center: ["38%", "50%"], | |||
avoidLabelOverlap: false, | |||
itemStyle: { | |||
borderRadius: 5, | |||
borderColor: "#fff", | |||
borderWidth: 2, | |||
}, | |||
label: { | |||
show: false, | |||
position: "center", | |||
formatter: () => { | |||
return `{total|${this.formatNumber( | |||
totalQuantity | |||
)}}\n{name|总数量}`; | |||
}, | |||
rich: { | |||
total: { | |||
fontSize: 22, | |||
fontWeight: "bold", | |||
color: "#5CB85C", | |||
}, | |||
name: { | |||
fontSize: 14, | |||
color: "#666", | |||
padding: [4, 0, 0, 0], | |||
}, | |||
}, | |||
}, | |||
emphasis: { | |||
scale: true, | |||
scaleSize: 10, | |||
label: { | |||
show: true, | |||
formatter: (params) => { | |||
return `{val|${params.percent}%}\n{name|${params.name}}`; | |||
}, | |||
rich: { | |||
val: { | |||
fontSize: 22, | |||
fontWeight: "bold", | |||
color: "#5CB85C", | |||
}, | |||
name: { | |||
fontSize: 14, | |||
color: "#666", | |||
padding: [4, 0, 0, 0], | |||
}, | |||
}, | |||
}, | |||
}, | |||
labelLine: { | |||
show: false, | |||
}, | |||
data: processedData, | |||
color: [ | |||
"#70AD47", | |||
"#4472C4", | |||
"#FFC000", | |||
"#ED7D31", | |||
"#5B9BD5", | |||
"#A5A5A5", | |||
"#255E91", | |||
"#9E480E", | |||
"#636363", | |||
"#997300", | |||
], | |||
}, | |||
], | |||
}; | |||
chart.setOption(option); | |||
}, | |||
// 格式化数字 | |||
formatNumber(num, isMoney = false) { | |||
if (!num) return "0"; | |||
if (isMoney && num >= 10000) { | |||
return (num / 10000).toFixed(1) + "万"; | |||
} | |||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |||
}, | |||
// 处理图表数据,聚合TopN和'其它' | |||
processChartData(data, isAmount = true) { | |||
const legendLimit = 9; | |||
if (data.length <= legendLimit + 1) { | |||
return data; | |||
} | |||
const topItems = data.slice(0, legendLimit); | |||
const otherItems = data.slice(legendLimit); | |||
const otherSum = otherItems.reduce((sum, item) => sum + item.value, 0); | |||
const totalSum = data.reduce((sum, item) => sum + item.value, 0); | |||
const otherPercentage = | |||
totalSum > 0 ? ((otherSum / totalSum) * 100).toFixed(2) : 0; | |||
const otherItem = { | |||
name: "其它", | |||
value: otherSum, | |||
percentage: otherPercentage, | |||
}; | |||
if (isAmount) { | |||
otherItem.amount = otherSum; | |||
} else { | |||
otherItem.quantity = otherSum; | |||
} | |||
return [...topItems, otherItem]; | |||
}, | |||
// 获取总图表数量 | |||
getTotalChartsCount() { | |||
return Object.keys(this.dimensionCharts).length * 2; | |||
}, | |||
// 获取总数据项数量 | |||
getTotalDataItems() { | |||
let total = 0; | |||
for (const chartData of Object.values(this.dimensionCharts)) { | |||
total += chartData.amountData.length + chartData.quantityData.length; | |||
} | |||
return total; | |||
}, | |||
// 获取主要维度 | |||
getMainDimension() { | |||
if (Object.keys(this.dimensionCharts).length === 0) return "-"; | |||
let maxItems = 0; | |||
let mainDimension = ""; | |||
for (const [dimension, chartData] of Object.entries( | |||
this.dimensionCharts | |||
)) { | |||
const totalItems = | |||
chartData.amountData.length + chartData.quantityData.length; | |||
if (totalItems > maxItems) { | |||
maxItems = totalItems; | |||
mainDimension = dimension; | |||
} | |||
} | |||
return mainDimension; | |||
}, | |||
// 获取总金额 | |||
getTotalAmount(data) { | |||
return data.reduce((sum, item) => sum + (item.value || 0), 0); | |||
}, | |||
// 获取总数量 | |||
getTotalQuantity(data) { | |||
return data.reduce((sum, item) => sum + (item.value || 0), 0); | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.overall-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 15px; | |||
} | |||
.filter-card { | |||
background-color: #fff; | |||
padding: 20px; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
} | |||
.filter-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
} | |||
.filter-row { | |||
display: flex; | |||
align-items: center; | |||
gap: 15px; | |||
} | |||
.filter-item { | |||
display: flex; | |||
align-items: center; | |||
gap: 6px; | |||
} | |||
.filter-item label { | |||
font-weight: 500; | |||
color: #333; | |||
white-space: nowrap; | |||
} | |||
.filter-item .el-select { | |||
width: 180px; | |||
} | |||
.filter-item .el-input { | |||
width: 180px; | |||
} | |||
.stats-cards { | |||
display: grid; | |||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |||
gap: 15px; | |||
} | |||
.stat-card { | |||
background: #fff; | |||
padding: 20px; | |||
border-radius: 8px; | |||
display: flex; | |||
align-items: center; | |||
gap: 15px; | |||
border: 1px solid #e8e8e8; | |||
transition: background-color 0.2s ease; | |||
} | |||
.stat-card:hover { | |||
background-color: #f8f9fa; | |||
} | |||
.stat-card-1 { | |||
border-left: 4px solid #5b9bd5; | |||
} | |||
.stat-card-2 { | |||
border-left: 4px solid #ed7d31; | |||
} | |||
.stat-card-3 { | |||
border-left: 4px solid #70ad47; | |||
} | |||
.stat-card-4 { | |||
border-left: 4px solid #ffc000; | |||
} | |||
.stat-card-1 .stat-icon { | |||
background-color: #5b9bd5; | |||
} | |||
.stat-card-2 .stat-icon { | |||
background-color: #ed7d31; | |||
} | |||
.stat-card-3 .stat-icon { | |||
background-color: #70ad47; | |||
} | |||
.stat-card-4 .stat-icon { | |||
background-color: #ffc000; | |||
} | |||
.stat-icon { | |||
width: 50px; | |||
height: 50px; | |||
border-radius: 50%; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
font-size: 20px; | |||
color: white; | |||
flex-shrink: 0; | |||
} | |||
.stat-content { | |||
flex: 1; | |||
} | |||
.stat-title { | |||
font-size: 14px; | |||
color: #666; | |||
margin-bottom: 4px; | |||
} | |||
.stat-value { | |||
font-size: 28px; | |||
font-weight: 600; | |||
color: #333; | |||
margin-bottom: 4px; | |||
} | |||
.stat-desc { | |||
font-size: 12px; | |||
color: #999; | |||
} | |||
.charts-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 30px; | |||
} | |||
.dimension-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 20px; | |||
overflow: hidden; | |||
} | |||
.dimension-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
padding: 15px 20px; | |||
background: #f8f9fa; | |||
border-radius: 8px; | |||
border-bottom: 1px solid #e8e8e8; | |||
} | |||
.dimension-title { | |||
display: flex; | |||
align-items: center; | |||
gap: 10px; | |||
font-size: 18px; | |||
font-weight: 600; | |||
color: #333; | |||
} | |||
.dimension-title i { | |||
font-size: 20px; | |||
color: #5b9bd5; | |||
} | |||
.dimension-stats { | |||
display: flex; | |||
gap: 20px; | |||
} | |||
.stat-item { | |||
display: flex; | |||
align-items: center; | |||
gap: 6px; | |||
font-size: 14px; | |||
color: #666; | |||
} | |||
.category-stat { | |||
background-color: #e9ecef; | |||
padding: 4px 8px; | |||
border-radius: 4px; | |||
} | |||
.category-stat strong { | |||
color: #0056b3; | |||
} | |||
.chart-card { | |||
background-color: #fff; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
overflow: hidden; | |||
} | |||
.chart-container { | |||
padding: 15px 0; | |||
width: 100%; | |||
} | |||
.card-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
padding: 15px 20px; | |||
background: #fff; | |||
border-bottom: 1px solid #e8e8e8; | |||
} | |||
.header-left { | |||
display: flex; | |||
align-items: center; | |||
gap: 8px; | |||
font-weight: 600; | |||
font-size: 16px; | |||
color: #333; | |||
} | |||
.header-left i { | |||
font-size: 18px; | |||
} | |||
.header-icon-amount { | |||
color: #d9534f; | |||
} | |||
.header-icon-quantity { | |||
color: #5cb85c; | |||
} | |||
.header-right { | |||
font-size: 14px; | |||
font-weight: 600; | |||
} | |||
.total-amount { | |||
color: #d9534f; | |||
} | |||
.total-quantity { | |||
color: #5cb85c; | |||
} | |||
.no-data-section { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
min-height: 400px; | |||
} | |||
.no-data-content { | |||
text-align: center; | |||
color: #999; | |||
} | |||
.no-data-content i { | |||
font-size: 48px; | |||
margin-bottom: 20px; | |||
display: block; | |||
color: #ccc; | |||
} | |||
.no-data-content p { | |||
font-size: 16px; | |||
margin: 0 0 10px 0; | |||
color: #666; | |||
} | |||
.no-data-content small { | |||
font-size: 14px; | |||
color: #aaa; | |||
} | |||
</style> |
@@ -0,0 +1,507 @@ | |||
<template> | |||
<div class="product-analysis-page"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
<div class="filter-row"> | |||
<div class="filter-item"> | |||
<label>商品编码:</label> | |||
<product-code-autocomplete | |||
v-model="filters.productCode" | |||
placeholder="请选择要分析的商品编码" | |||
width="300px" | |||
@select="handleProductCodeSelect" | |||
size="small" | |||
/> | |||
</div> | |||
<div class="filter-item"> | |||
<label>日期范围:</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: 380px" | |||
size="small" | |||
@change="handleDateChange" | |||
/> | |||
</div> | |||
<div class="filter-item"> | |||
<el-button type="primary" @click="fetchData" :loading="loading" size="small"> | |||
查询分析 | |||
</el-button> | |||
<el-button @click="resetFilter" size="small">重置筛选</el-button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 图表区域 --> | |||
<div class="charts-section" v-if="!isDataEmpty"> | |||
<!-- 商品销售排行 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>单品销售排行</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div id="rankingChart" style="width: 100%; height: 600px"></div> | |||
</div> | |||
</div> | |||
<!-- 商品销售趋势 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>单品销售趋势</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div id="trendChart" style="width: 100%; height: 400px"></div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 无数据提示 --> | |||
<div v-if="!loading && isDataEmpty" class="no-data-section"> | |||
<div class="no-data-content"> | |||
<i class="el-icon-warning-outline"></i> | |||
<p>暂无数据</p> | |||
<small>请尝试调整筛选条件或检查数据</small> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import * as echarts from "echarts"; | |||
import ProductCodeAutocomplete from "@/components/ProductCodeAutocomplete/index.vue"; | |||
import { getProductAnalysisReport } from '@/api/sales-analysis' | |||
export default { | |||
name: "ProductAnalysis", | |||
components: { | |||
ProductCodeAutocomplete, | |||
}, | |||
data() { | |||
return { | |||
loading: false, | |||
dateRange: null, | |||
filters: { | |||
productCode: "", | |||
}, | |||
filterOptions: {}, | |||
statsData: { | |||
rankingData: [], | |||
trendData: [], | |||
}, | |||
trendChart: null, | |||
rankingChart: null, | |||
}; | |||
}, | |||
computed: { | |||
isDataEmpty() { | |||
return ( | |||
(!this.statsData.rankingData || | |||
this.statsData.rankingData.length === 0) && | |||
(!this.statsData.trendData || this.statsData.trendData.length === 0) | |||
); | |||
}, | |||
}, | |||
mounted() { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
// this.fetchData(); | |||
}, | |||
beforeDestroy() { | |||
if (this.trendChart) { | |||
this.trendChart.dispose(); | |||
} | |||
if (this.rankingChart) { | |||
this.rankingChart.dispose(); | |||
} | |||
}, | |||
methods: { | |||
// 初始化日期范围(默认最近30天) | |||
initDateRange() { | |||
const endDate = new Date(); | |||
const startDate = new Date(); | |||
startDate.setDate(endDate.getDate() - 30); | |||
this.dateRange = [ | |||
startDate.toISOString().split("T")[0], | |||
endDate.toISOString().split("T")[0], | |||
]; | |||
}, | |||
// 日期变化处理 | |||
handleDateChange(dates) { | |||
this.dateRange = dates; | |||
}, | |||
// 商品编码选择处理 | |||
handleProductCodeSelect(item) { | |||
this.filters.productCode = item.value; | |||
}, | |||
// 重置筛选 | |||
resetFilter() { | |||
this.initDateRange(); | |||
this.filters = { | |||
productCode: "", | |||
}; | |||
// 重置后不清空数据,等待新的查询 | |||
}, | |||
// 获取筛选选项 - 此处可以留空或用于其他目的 | |||
async fetchFilterOptions() {}, | |||
// 获取数据 | |||
async fetchData() { | |||
if (!this.dateRange || this.dateRange.length !== 2) { | |||
this.$message.warning("请选择日期范围"); | |||
return; | |||
} | |||
if (!this.filters.productCode) { | |||
this.$message.warning("请输入并选择一个商品编码"); | |||
return; | |||
} | |||
this.loading = true; | |||
try { | |||
const params = { | |||
startDate: this.dateRange[0], | |||
endDate: this.dateRange[1], | |||
productCode: this.filters.productCode, | |||
}; | |||
const response = await getProductAnalysisReport(params); | |||
if (response.code === 200) { | |||
this.statsData = response.data || { rankingData: [], trendData: [] }; | |||
this.$nextTick(() => { | |||
this.renderTrendChart(); | |||
this.renderRankingChart(); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
this.statsData = { rankingData: [], trendData: [] }; | |||
this.$nextTick(() => { | |||
this.renderTrendChart(); | |||
this.renderRankingChart(); | |||
}); | |||
} | |||
} catch (error) { | |||
console.error("获取数据失败:", error); | |||
this.$message.error("获取数据失败"); | |||
this.statsData = { rankingData: [], trendData: [] }; | |||
this.$nextTick(() => { | |||
this.renderTrendChart(); | |||
this.renderRankingChart(); | |||
}); | |||
} finally { | |||
this.loading = false; | |||
} | |||
}, | |||
// 渲染趋势图 | |||
renderTrendChart() { | |||
const chartDom = document.getElementById("trendChart"); | |||
if (!chartDom) return; | |||
if (this.trendChart) { | |||
this.trendChart.dispose(); | |||
} | |||
this.trendChart = echarts.init(chartDom); | |||
if (!this.statsData.trendData || this.statsData.trendData.length === 0) { | |||
this.trendChart.clear(); | |||
return; | |||
} | |||
const dates = this.statsData.trendData.map((item) => item.date); | |||
const amounts = this.statsData.trendData.map((item) => item.amount); | |||
const quantities = this.statsData.trendData.map((item) => item.quantity); | |||
const option = { | |||
tooltip: { | |||
trigger: "axis", | |||
axisPointer: { type: "cross" }, | |||
}, | |||
legend: { | |||
data: ["销售额", "销售数量"], | |||
top: "top", | |||
}, | |||
grid: { | |||
left: "3%", | |||
right: "4%", | |||
bottom: "10%", | |||
containLabel: true, | |||
}, | |||
xAxis: [ | |||
{ | |||
type: "category", | |||
data: dates, | |||
axisPointer: { type: "shadow" }, | |||
}, | |||
], | |||
yAxis: [ | |||
{ | |||
type: "value", | |||
name: "销售额", | |||
axisLabel: { | |||
formatter: (value) => `¥${this.formatNumber(value)}`, | |||
}, | |||
}, | |||
{ | |||
type: "value", | |||
name: "销售数量", | |||
axisLabel: { formatter: "{value}" }, | |||
}, | |||
], | |||
series: [ | |||
{ | |||
name: "销售额", | |||
type: "bar", | |||
data: amounts, | |||
itemStyle: { | |||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ | |||
{ offset: 0, color: "#5470C6" }, | |||
{ offset: 1, color: "#80A4F3" }, | |||
]), | |||
}, | |||
}, | |||
{ | |||
name: "销售数量", | |||
type: "line", | |||
yAxisIndex: 1, | |||
smooth: true, | |||
data: quantities, | |||
itemStyle: { color: "#5BCCBD" }, | |||
}, | |||
], | |||
dataZoom: [ | |||
{ type: "inside", start: 0, end: 100 }, | |||
{ start: 0, end: 100 }, | |||
], | |||
}; | |||
this.trendChart.setOption(option); | |||
}, | |||
// 渲染商品排行图 | |||
renderRankingChart() { | |||
const chartDom = document.getElementById("rankingChart"); | |||
if (!chartDom) return; | |||
if (this.rankingChart) { | |||
this.rankingChart.dispose(); | |||
} | |||
this.rankingChart = echarts.init(chartDom); | |||
if ( | |||
!this.statsData.rankingData || | |||
this.statsData.rankingData.length === 0 | |||
) { | |||
this.rankingChart.clear(); | |||
return; | |||
} | |||
const data = [...this.statsData.rankingData].sort( | |||
(a, b) => a.totalAmount - b.totalAmount | |||
); | |||
const dates = data.map((item) => item.date); | |||
const amounts = data.map((item) => item.totalAmount); | |||
const quantities = data.map((item) => item.totalQuantity); | |||
const option = { | |||
tooltip: { | |||
trigger: "axis", | |||
axisPointer: { type: "shadow" }, | |||
formatter: (params) => { | |||
const date = params[0].name; | |||
const amountParam = params.find((p) => p.seriesName === "销售额"); | |||
const quantityParam = params.find( | |||
(p) => p.seriesName === "销售数量" | |||
); | |||
const amount = amountParam ? amountParam.value : 0; | |||
const quantity = quantityParam ? quantityParam.value : 0; | |||
return `日期: ${date}<br/> | |||
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${ | |||
amountParam.color | |||
};"></span>销售额: ¥${this.formatNumber(amount)}<br/> | |||
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${ | |||
quantityParam.color | |||
};"></span>销售数量: ${this.formatNumber(quantity)}`; | |||
}, | |||
}, | |||
legend: { | |||
data: ["销售额", "销售数量"], | |||
top: "top", | |||
}, | |||
grid: { | |||
left: "3%", | |||
right: "10%", | |||
bottom: "3%", | |||
containLabel: true, | |||
}, | |||
xAxis: [ | |||
{ | |||
type: "value", | |||
name: "销售额", | |||
position: "bottom", | |||
splitLine: { show: true }, | |||
axisLabel: { | |||
formatter: (value) => `¥${this.formatNumber(value)}`, | |||
}, | |||
}, | |||
{ | |||
type: "value", | |||
name: "销售数量", | |||
position: "top", | |||
splitLine: { show: false }, | |||
axisLine: { show: true, onZero: false }, | |||
axisTick: { show: true }, | |||
axisLabel: { formatter: "{value}" }, | |||
}, | |||
], | |||
yAxis: { | |||
type: "category", | |||
data: dates, | |||
axisTick: { show: false }, | |||
axisLabel: { | |||
formatter: (value) => | |||
value.length > 30 ? value.substring(0, 30) + "..." : value, | |||
}, | |||
}, | |||
series: [ | |||
{ | |||
name: "销售额", | |||
type: "bar", | |||
xAxisIndex: 0, | |||
data: amounts, | |||
itemStyle: { color: "#5470C6" }, | |||
label: { | |||
show: true, | |||
position: "right", | |||
formatter: (params) => `¥${this.formatNumber(params.value)}`, | |||
}, | |||
}, | |||
{ | |||
name: "销售数量", | |||
type: "bar", | |||
xAxisIndex: 1, | |||
data: quantities, | |||
itemStyle: { color: "#91CC75" }, | |||
label: { | |||
show: true, | |||
position: "right", | |||
formatter: (params) => `${this.formatNumber(params.value)}`, | |||
}, | |||
}, | |||
], | |||
}; | |||
this.rankingChart.setOption(option); | |||
}, | |||
// 格式化数字 | |||
formatNumber(num) { | |||
if (num === null || num === undefined) return "0"; | |||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.product-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 15px; | |||
} | |||
.filter-card { | |||
background-color: #fff; | |||
padding: 20px; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
} | |||
.filter-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
} | |||
.filter-row { | |||
display: flex; | |||
align-items: center; | |||
gap: 20px; | |||
flex-wrap: wrap; | |||
} | |||
.filter-item { | |||
display: flex; | |||
align-items: center; | |||
gap: 8px; | |||
} | |||
.filter-item label { | |||
font-weight: 500; | |||
color: #333; | |||
white-space: nowrap; | |||
} | |||
.charts-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 20px; | |||
} | |||
.chart-card { | |||
background-color: #fff; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
overflow: hidden; | |||
} | |||
.card-header { | |||
display: flex; | |||
align-items: center; | |||
font-weight: 600; | |||
font-size: 18px; | |||
padding: 15px 20px; | |||
border-bottom: 1px solid #e8e8e8; | |||
} | |||
.chart-container { | |||
padding: 20px; | |||
} | |||
.no-data-section { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
min-height: 400px; | |||
flex-direction: column; | |||
} | |||
.no-data-content { | |||
text-align: center; | |||
color: #999; | |||
} | |||
.no-data-content i { | |||
font-size: 48px; | |||
margin-bottom: 20px; | |||
color: #ccc; | |||
display: block; | |||
} | |||
.no-data-content p { | |||
font-size: 16px; | |||
margin: 0 0 10px 0; | |||
color: #666; | |||
} | |||
.no-data-content small { | |||
font-size: 14px; | |||
color: #aaa; | |||
} | |||
</style> |
@@ -0,0 +1,807 @@ | |||
<template> | |||
<div class="overall-analysis-page"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
<div class="filter-row"> | |||
<div class="filter-item" v-if="false"> | |||
<label>店铺:</label> | |||
<el-select | |||
v-model="filters.shop" | |||
placeholder="请选择店铺(默认全部)" | |||
clearable | |||
filterable | |||
style="width: 180px" | |||
size="small" | |||
> | |||
<el-option label="全部店铺" value="" /> | |||
<el-option | |||
v-for="shop in filterOptions.shops" | |||
:key="shop" | |||
:label="shop" | |||
:value="shop" | |||
/> | |||
</el-select> | |||
</div> | |||
<div class="filter-item"> | |||
<label>日期范围:</label> | |||
<el-date-picker | |||
v-model="dateRange" | |||
type="daterange" | |||
range-separator="至" | |||
start-placeholder="开始日期" | |||
end-placeholder="结束日期" | |||
format="yyyy-MM-dd" | |||
value-format="yyyy-MM-dd" | |||
@change="handleDateChange" | |||
style="width: 380px" | |||
size="small" | |||
/> | |||
</div> | |||
<div class="filter-item"> | |||
<el-button type="primary" @click="fetchData" :loading="loading" size="small"> | |||
查询分析 | |||
</el-button> | |||
<el-button @click="resetFilter" size="small">重置筛选</el-button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 图表区域 --> | |||
<div class="charts-section"> | |||
<el-row :gutter="20"> | |||
<el-col :span="12"> | |||
<!-- 各店铺销售金额占比 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>各店铺销售金额占比</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div | |||
id="shopAmountChart" | |||
style="width: 100%; height: 400px" | |||
></div> | |||
</div> | |||
</div> | |||
</el-col> | |||
<el-col :span="12"> | |||
<!-- 各店铺销售数量占比 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>各店铺销售数量占比</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div | |||
id="shopQuantityChart" | |||
style="width: 100%; height: 400px" | |||
></div> | |||
</div> | |||
</div> | |||
</el-col> | |||
</el-row> | |||
<!-- 各品类销售趋势 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>各品类销售数量和销售额趋势</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div id="categoryTrendChart" style="width: 100%; height: 500px"></div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import * as echarts from "echarts"; | |||
import { getReportFilterOptions, getShopAnalysisReport } from '@/api/sales-analysis' | |||
export default { | |||
name: "ShopAnalysis", | |||
components: {}, | |||
data() { | |||
return { | |||
loading: false, | |||
dateRange: null, | |||
filters: { | |||
shop: "", | |||
}, | |||
filterOptions: { | |||
shops: [], | |||
}, | |||
statsData: { | |||
basicStats: null, | |||
shopAmountData: [], | |||
shopQuantityData: [], | |||
categoryTrendData: [], | |||
}, | |||
shopAmountChart: null, | |||
shopQuantityChart: null, | |||
categoryTrendChart: null, | |||
}; | |||
}, | |||
filters: { | |||
jpMoney(value) { | |||
if (value === 0) { | |||
return "0"; | |||
} | |||
if (value < 10000) { | |||
return value.toLocaleString("ja-JP", { | |||
style: "currency", | |||
currency: "JPY", | |||
minimumFractionDigits: 0, | |||
maximumFractionDigits: 0, | |||
}); | |||
} else { | |||
// 将value转换为万日元 | |||
const jpValue = (value / 10000).toFixed(0); | |||
return jpValue; | |||
} | |||
}, | |||
}, | |||
mounted() { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
this.fetchData(); | |||
}, | |||
beforeDestroy() { | |||
if (this.shopAmountChart) { | |||
this.shopAmountChart.dispose(); | |||
} | |||
if (this.shopQuantityChart) { | |||
this.shopQuantityChart.dispose(); | |||
} | |||
if (this.categoryTrendChart) { | |||
this.categoryTrendChart.dispose(); | |||
} | |||
}, | |||
methods: { | |||
// 初始化日期范围(默认最近30天) | |||
initDateRange() { | |||
const endDate = new Date(); | |||
const startDate = new Date(); | |||
startDate.setDate(endDate.getDate() - 30); | |||
this.dateRange = [ | |||
startDate.toISOString().split("T")[0], | |||
endDate.toISOString().split("T")[0], | |||
]; | |||
}, | |||
// 日期变化处理 | |||
handleDateChange(dates) { | |||
this.dateRange = dates; | |||
}, | |||
// 重置筛选 | |||
resetFilter() { | |||
this.initDateRange(); | |||
this.filters = { | |||
shop: "", | |||
}; | |||
this.fetchData(); | |||
}, | |||
// 获取筛选选项 | |||
async fetchFilterOptions() { | |||
try { | |||
const response = await getReportFilterOptions(); | |||
if (response.code === 200) { | |||
this.filterOptions = { | |||
shops: response.data.shops || [], | |||
}; | |||
} else { | |||
console.error("获取筛选选项失败:", response.message); | |||
} | |||
} catch (error) { | |||
console.error("获取筛选选项失败:", error); | |||
} | |||
}, | |||
// 获取数据 | |||
async fetchData() { | |||
if (!this.dateRange || this.dateRange.length !== 2) { | |||
this.$message.warning("请选择日期范围"); | |||
return; | |||
} | |||
try { | |||
this.loading = true; | |||
// 构建查询参数 | |||
const params = { | |||
startDate: this.dateRange[0], | |||
endDate: this.dateRange[1], | |||
}; | |||
// 添加店铺筛选条件(如果选择了特定店铺) | |||
if (this.filters.shop) { | |||
params.shop = this.filters.shop; | |||
} | |||
const response = await getShopAnalysisReport(params); | |||
if (response.code === 200) { | |||
this.statsData = response.data; | |||
this.$nextTick(() => { | |||
this.renderShopAmountChart(); | |||
this.renderShopQuantityChart(); | |||
this.renderCategoryTrendChart(); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取数据失败:", error); | |||
this.$message.error("获取数据失败"); | |||
} finally { | |||
this.loading = false; | |||
} | |||
}, | |||
// 渲染店铺销售金额占比图表 | |||
renderShopAmountChart() { | |||
if ( | |||
!this.statsData.shopAmountData || | |||
this.statsData.shopAmountData.length === 0 | |||
) { | |||
const chartDom = document.getElementById("shopAmountChart"); | |||
if (chartDom && this.shopAmountChart) { | |||
this.shopAmountChart.clear(); | |||
} | |||
return; | |||
} | |||
const chartDom = document.getElementById("shopAmountChart"); | |||
if (!chartDom) return; | |||
if (this.shopAmountChart) { | |||
this.shopAmountChart.dispose(); | |||
} | |||
this.shopAmountChart = echarts.init(chartDom); | |||
const data = this.statsData.shopAmountData.map((item) => ({ | |||
name: item.shopName, | |||
value: item.amount, | |||
percentage: item.percentage, | |||
})); | |||
const option = { | |||
tooltip: { | |||
trigger: "item", | |||
formatter: (params) => { | |||
return `${params.name}<br/>销售额: ¥${this.formatNumber( | |||
params.value | |||
)}<br/>占比: ${params.data.percentage}%`; | |||
}, | |||
}, | |||
legend: { | |||
type: "scroll", | |||
orient: "vertical", | |||
right: 10, | |||
top: 20, | |||
bottom: 20, | |||
data: data.map((item) => item.name), | |||
textStyle: { | |||
color: "#666", | |||
}, | |||
formatter: (name) => { | |||
const item = data.find((p) => p.name === name); | |||
const displayName = | |||
name.length > 15 ? name.slice(0, 15) + "..." : name; | |||
if (item && item.percentage !== undefined) { | |||
return `${displayName} ${item.percentage}%`; | |||
} | |||
return displayName; | |||
}, | |||
}, | |||
series: [ | |||
{ | |||
name: "销售金额", | |||
type: "pie", | |||
radius: ["50%", "70%"], | |||
center: ["40%", "50%"], | |||
avoidLabelOverlap: false, | |||
itemStyle: { | |||
borderRadius: 10, | |||
borderColor: "#fff", | |||
borderWidth: 2, | |||
}, | |||
label: { | |||
show: false, | |||
position: "center", | |||
}, | |||
emphasis: { | |||
label: { | |||
show: true, | |||
fontSize: "20", | |||
fontWeight: "bold", | |||
}, | |||
}, | |||
labelLine: { | |||
show: false, | |||
}, | |||
data: data, | |||
color: [ | |||
"#5470C6", | |||
"#91CC75", | |||
"#FAC858", | |||
"#EE6666", | |||
"#73C0DE", | |||
"#3BA272", | |||
"#FC8452", | |||
"#9A60B4", | |||
"#EA7CCC", | |||
], | |||
}, | |||
], | |||
}; | |||
this.shopAmountChart.setOption(option); | |||
}, | |||
// 渲染店铺销售数量占比图表 | |||
renderShopQuantityChart() { | |||
if ( | |||
!this.statsData.shopQuantityData || | |||
this.statsData.shopQuantityData.length === 0 | |||
) { | |||
const chartDom = document.getElementById("shopQuantityChart"); | |||
if (chartDom && this.shopQuantityChart) { | |||
this.shopQuantityChart.clear(); | |||
} | |||
return; | |||
} | |||
const chartDom = document.getElementById("shopQuantityChart"); | |||
if (!chartDom) return; | |||
if (this.shopQuantityChart) { | |||
this.shopQuantityChart.dispose(); | |||
} | |||
this.shopQuantityChart = echarts.init(chartDom); | |||
const data = this.statsData.shopQuantityData.map((item) => ({ | |||
name: item.shopName, | |||
value: item.quantity, | |||
percentage: item.percentage, | |||
})); | |||
const option = { | |||
tooltip: { | |||
trigger: "item", | |||
formatter: (params) => { | |||
return `${params.name}<br/>销售数量: ${this.formatNumber( | |||
params.value | |||
)}<br/>占比: ${params.data.percentage}%`; | |||
}, | |||
}, | |||
legend: { | |||
type: "scroll", | |||
orient: "vertical", | |||
right: 10, | |||
top: 20, | |||
bottom: 20, | |||
data: data.map((item) => item.name), | |||
textStyle: { | |||
color: "#666", | |||
}, | |||
formatter: (name) => { | |||
const item = data.find((p) => p.name === name); | |||
const displayName = | |||
name.length > 15 ? name.slice(0, 15) + "..." : name; | |||
if (item && item.percentage !== undefined) { | |||
return `${displayName} ${item.percentage}%`; | |||
} | |||
return displayName; | |||
}, | |||
}, | |||
series: [ | |||
{ | |||
name: "销售数量", | |||
type: "pie", | |||
radius: ["50%", "70%"], | |||
center: ["40%", "50%"], | |||
avoidLabelOverlap: false, | |||
itemStyle: { | |||
borderRadius: 10, | |||
borderColor: "#fff", | |||
borderWidth: 2, | |||
}, | |||
label: { | |||
show: false, | |||
position: "center", | |||
}, | |||
emphasis: { | |||
label: { | |||
show: true, | |||
fontSize: "20", | |||
fontWeight: "bold", | |||
}, | |||
}, | |||
labelLine: { | |||
show: false, | |||
}, | |||
data: data, | |||
color: [ | |||
"#91CC75", | |||
"#5470C6", | |||
"#FAC858", | |||
"#EE6666", | |||
"#73C0DE", | |||
"#3BA272", | |||
"#FC8452", | |||
"#9A60B4", | |||
"#EA7CCC", | |||
], | |||
}, | |||
], | |||
}; | |||
this.shopQuantityChart.setOption(option); | |||
}, | |||
// 渲染品类销售趋势图表 | |||
renderCategoryTrendChart() { | |||
if ( | |||
!this.statsData.categoryTrendData || | |||
this.statsData.categoryTrendData.length === 0 | |||
) { | |||
const chartDom = document.getElementById("categoryTrendChart"); | |||
if (chartDom && this.categoryTrendChart) { | |||
this.categoryTrendChart.clear(); | |||
} | |||
return; | |||
} | |||
const chartDom = document.getElementById("categoryTrendChart"); | |||
if (!chartDom) return; | |||
if (this.categoryTrendChart) { | |||
this.categoryTrendChart.dispose(); | |||
} | |||
this.categoryTrendChart = echarts.init(chartDom); | |||
const categories = this.statsData.categoryTrendData.map( | |||
(item) => item.category | |||
); | |||
const amounts = this.statsData.categoryTrendData.map( | |||
(item) => item.amount | |||
); | |||
const quantities = this.statsData.categoryTrendData.map( | |||
(item) => item.quantity | |||
); | |||
const option = { | |||
tooltip: { | |||
trigger: "axis", | |||
axisPointer: { | |||
type: "cross", | |||
crossStyle: { | |||
color: "#999", | |||
}, | |||
}, | |||
formatter: (params) => { | |||
let tooltipText = `${params[0].name}<br/>`; | |||
params.forEach((param) => { | |||
tooltipText += `${param.marker} ${param.seriesName}: `; | |||
const value = param.value || 0; | |||
if (param.seriesName === "销售额") { | |||
tooltipText += `¥${this.formatNumber(value.toFixed(0))}`; | |||
} else { | |||
tooltipText += `${this.formatNumber(value)}`; | |||
} | |||
tooltipText += "<br/>"; | |||
}); | |||
return tooltipText; | |||
}, | |||
}, | |||
legend: { | |||
data: ["销售额", "销售数量"], | |||
top: "top", | |||
itemGap: 20, | |||
textStyle: { | |||
color: "#666", | |||
}, | |||
}, | |||
grid: { | |||
left: "3%", | |||
right: "4%", | |||
bottom: "25%", | |||
containLabel: true, | |||
}, | |||
xAxis: [ | |||
{ | |||
type: "category", | |||
data: categories, | |||
axisPointer: { | |||
type: "shadow", | |||
}, | |||
axisTick: { | |||
show: false, | |||
}, | |||
axisLine: { | |||
show: false, | |||
}, | |||
axisLabel: { | |||
color: "#666", | |||
interval: 0, | |||
rotate: 45, | |||
}, | |||
}, | |||
], | |||
yAxis: [ | |||
{ | |||
type: "value", | |||
name: "销售额", | |||
axisLabel: { | |||
formatter: "¥{value}", | |||
color: "#666", | |||
}, | |||
nameTextStyle: { | |||
color: "#666", | |||
padding: [0, 0, 0, 40], | |||
}, | |||
splitLine: { | |||
lineStyle: { | |||
type: "dashed", | |||
color: "#e0e6f1", | |||
}, | |||
}, | |||
axisLine: { show: false }, | |||
axisTick: { show: false }, | |||
}, | |||
{ | |||
type: "value", | |||
name: "销售数量", | |||
axisLabel: { | |||
formatter: "{value}", | |||
color: "#666", | |||
}, | |||
nameTextStyle: { | |||
color: "#666", | |||
padding: [0, 40, 0, 0], | |||
}, | |||
splitLine: { show: false }, | |||
axisLine: { show: false }, | |||
axisTick: { show: false }, | |||
}, | |||
], | |||
series: [ | |||
{ | |||
name: "销售额", | |||
type: "bar", | |||
barWidth: "40%", | |||
data: amounts, | |||
itemStyle: { | |||
borderRadius: [5, 5, 0, 0], | |||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ | |||
{ offset: 0, color: "#5470C6" }, | |||
{ offset: 1, color: "#80A4F3" }, | |||
]), | |||
}, | |||
}, | |||
{ | |||
name: "销售数量", | |||
type: "line", | |||
yAxisIndex: 1, | |||
smooth: true, | |||
data: quantities, | |||
symbol: "circle", | |||
symbolSize: 8, | |||
areaStyle: { | |||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ | |||
{ | |||
offset: 0, | |||
color: "rgba(91, 204, 189, 0.5)", | |||
}, | |||
{ | |||
offset: 1, | |||
color: "rgba(91, 204, 189, 0)", | |||
}, | |||
]), | |||
}, | |||
itemStyle: { | |||
color: "#5BCCBD", | |||
}, | |||
}, | |||
], | |||
dataZoom: [ | |||
{ | |||
type: "inside", | |||
start: 0, | |||
end: 100, | |||
}, | |||
{ | |||
start: 0, | |||
end: 100, | |||
height: 20, | |||
bottom: 5, | |||
}, | |||
], | |||
}; | |||
this.categoryTrendChart.setOption(option); | |||
}, | |||
// 格式化数字 | |||
formatNumber(num) { | |||
if (!num) return "0"; | |||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.overall-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
} | |||
.filter-card { | |||
background-color: #fff; | |||
padding: 20px; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
} | |||
.filter-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
} | |||
.filter-row { | |||
display: flex; | |||
align-items: center; | |||
gap: 10px; | |||
} | |||
.filter-item { | |||
display: flex; | |||
align-items: center; | |||
gap: 4px; | |||
} | |||
.filter-item label { | |||
font-weight: 500; | |||
color: #333; | |||
white-space: nowrap; | |||
width: 70px; | |||
} | |||
.filter-item .el-select { | |||
width: 180px; | |||
} | |||
.filter-item .el-input { | |||
width: 180px; | |||
} | |||
.filter-actions { | |||
display: flex; | |||
gap: 10px; | |||
} | |||
.stats-cards { | |||
display: grid; | |||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |||
gap: 10px; | |||
} | |||
.stat-card { | |||
background: white; | |||
padding: 20px; | |||
border-radius: 8px; | |||
display: flex; | |||
align-items: center; | |||
gap: 15px; | |||
border: 1px solid #e8e8e8; | |||
} | |||
.stat-icon { | |||
width: 60px; | |||
height: 60px; | |||
border-radius: 50%; | |||
background: #409eff; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
color: white; | |||
font-size: 24px; | |||
flex-shrink: 0; | |||
} | |||
.stat-content { | |||
flex: 1; | |||
} | |||
.stat-title { | |||
font-size: 16px; | |||
color: #505050; | |||
margin-bottom: 5px; | |||
} | |||
.stat-value { | |||
font-size: 32px; | |||
font-weight: 500; | |||
color: #333; | |||
margin-bottom: 5px; | |||
} | |||
.stat-value small { | |||
font-size: 15px; | |||
color: #505050; | |||
} | |||
.stat-desc { | |||
font-size: 12px; | |||
color: #999; | |||
} | |||
.charts-section { | |||
display: grid; | |||
grid-template-columns: 1fr; | |||
gap: 20px; | |||
} | |||
.chart-card { | |||
background: white; | |||
} | |||
.chart-container { | |||
width: 100%; | |||
} | |||
.card-header { | |||
display: flex; | |||
align-items: center; | |||
font-weight: 500; | |||
font-size: 18px; | |||
padding: 20px; | |||
justify-content: center; | |||
} | |||
.chart-card { | |||
background-color: #fff; | |||
border-radius: 8px; | |||
border: 1px solid #e8e8e8; | |||
} | |||
.chart-container { | |||
padding: 10px 0; | |||
} | |||
</style> |
@@ -0,0 +1,656 @@ | |||
<template> | |||
<div class="shop-customer-container app-container"> | |||
<div class="filter-container"> | |||
<div class="search-filters"> | |||
<el-input | |||
v-model="searchForm.shopName" | |||
placeholder="按店铺名称搜索" | |||
clearable | |||
@clear="handleSearch" | |||
@keyup.enter.native="handleSearch" | |||
class="filter-item" | |||
size="small" | |||
></el-input> | |||
<el-input | |||
v-model="searchForm.customerName" | |||
placeholder="按客户名称搜索" | |||
clearable | |||
@clear="handleSearch" | |||
@keyup.enter.native="handleSearch" | |||
class="filter-item" | |||
size="small" | |||
></el-input> | |||
<el-button | |||
type="primary" | |||
icon="el-icon-search" | |||
@click="handleSearch" | |||
size="small" | |||
style="margin-right: 0" | |||
></el-button> | |||
<el-button | |||
icon="el-icon-refresh" | |||
@click="resetSearch" | |||
size="small" | |||
style="margin-left: 0" | |||
></el-button> | |||
<el-button | |||
type="primary" | |||
icon="el-icon-plus" | |||
@click="handleAdd" | |||
size="small" | |||
style="margin-left: auto" | |||
>新增关联</el-button | |||
> | |||
<el-divider direction="vertical"></el-divider> | |||
<el-upload | |||
ref="upload" | |||
:action="uploadUrl" | |||
:before-upload="beforeUpload" | |||
:on-success="onUploadSuccess" | |||
:on-error="onUploadError" | |||
:show-file-list="false" | |||
accept=".xlsx,.xls" | |||
size="small" | |||
> | |||
<el-button type="success" plain icon="el-icon-upload2" size="small" | |||
>导入Excel</el-button | |||
> | |||
</el-upload> | |||
<el-button | |||
type="warning" | |||
plain | |||
icon="el-icon-download" | |||
@click="handleExport" | |||
:disabled="!relationList.length" | |||
size="small" | |||
>导出Excel</el-button | |||
> | |||
<el-divider direction="vertical"></el-divider> | |||
<el-button | |||
type="danger" | |||
icon="el-icon-delete" | |||
@click="handleBatchDelete" | |||
:disabled="!selectedRows.length" | |||
plain | |||
size="small" | |||
>批量删除</el-button | |||
> | |||
</div> | |||
</div> | |||
<el-table | |||
v-loading="loading" | |||
:data="relationList" | |||
style="width: 100%" | |||
@selection-change="handleSelectionChange" | |||
row-key="id" | |||
size="small" | |||
> | |||
<el-table-column | |||
type="selection" | |||
width="55" | |||
align="center" | |||
></el-table-column> | |||
<el-table-column | |||
prop="id" | |||
label="ID" | |||
width="80" | |||
align="center" | |||
></el-table-column> | |||
<el-table-column | |||
prop="shopName" | |||
label="店铺名称" | |||
min-width="220" | |||
show-overflow-tooltip | |||
sortable | |||
> | |||
<template slot-scope="scope"> | |||
<i | |||
class="el-icon-s-shop" | |||
style="margin-right: 5px; color: #409eff" | |||
></i> | |||
<span>{{ scope.row.shopName }}</span> | |||
</template> | |||
</el-table-column> | |||
<el-table-column | |||
prop="customerName" | |||
label="客户名称" | |||
min-width="220" | |||
show-overflow-tooltip | |||
sortable | |||
> | |||
<template slot-scope="scope"> | |||
<i | |||
class="el-icon-user-solid" | |||
style="margin-right: 5px; color: #67c23a" | |||
></i> | |||
<span>{{ scope.row.customerName }}</span> | |||
</template> | |||
</el-table-column> | |||
<el-table-column | |||
prop="createdAt" | |||
label="创建时间" | |||
width="180" | |||
align="center" | |||
sortable | |||
> | |||
<template slot-scope="scope"> | |||
<i class="el-icon-time" style="margin-right: 5px"></i> | |||
<span>{{ formatDateTime(scope.row.createdAt) }}</span> | |||
</template> | |||
</el-table-column> | |||
<el-table-column label="操作" width="160" fixed="right" align="center"> | |||
<template slot-scope="scope"> | |||
<el-button | |||
type="primary" | |||
size="mini" | |||
@click="handleEdit(scope.row)" | |||
plain | |||
>编辑</el-button | |||
> | |||
<el-popconfirm | |||
title="确定删除此关联吗?" | |||
@confirm="handleDelete(scope.row)" | |||
style="margin-left: 10px" | |||
> | |||
<el-button slot="reference" type="danger" size="mini" plain | |||
>删除</el-button | |||
> | |||
</el-popconfirm> | |||
</template> | |||
</el-table-column> | |||
</el-table> | |||
<pagination | |||
v-show="pagination.total > 0" | |||
:total="pagination.total" | |||
:page.sync="pagination.page" | |||
:limit.sync="pagination.pageSize" | |||
@pagination="fetchRelationList" | |||
/> | |||
<!-- 新增/编辑对话框 --> | |||
<el-dialog | |||
:title="dialogTitle" | |||
:visible.sync="dialogVisible" | |||
width="600px" | |||
:close-on-click-modal="false" | |||
@close="handleDialogClose" | |||
custom-class="relation-dialog" | |||
> | |||
<el-form | |||
ref="relationForm" | |||
:model="relationForm" | |||
:rules="relationRules" | |||
label-width="100px" | |||
label-position="top" | |||
> | |||
<el-form-item label="店铺名称" prop="shopName"> | |||
<el-input | |||
v-model="relationForm.shopName" | |||
placeholder="请输入店铺名称" | |||
:disabled="isEdit" | |||
clearable | |||
></el-input> | |||
<div class="form-tips" v-if="isEdit"> | |||
<i class="el-icon-info"></i> | |||
关联关系创建后不可修改,如需调整请删除后重新创建。 | |||
</div> | |||
</el-form-item> | |||
<el-form-item label="客户名称" prop="customerName"> | |||
<el-input | |||
v-model="relationForm.customerName" | |||
placeholder="请输入客户名称" | |||
:disabled="isEdit" | |||
clearable | |||
></el-input> | |||
</el-form-item> | |||
</el-form> | |||
<div | |||
v-if="dialogTitle === '新增店铺客户关联'" | |||
slot="footer" | |||
class="dialog-footer" | |||
> | |||
<el-button @click="dialogVisible = false">取消</el-button> | |||
<el-button type="primary" :loading="submitting" @click="handleSubmit" | |||
>确定</el-button | |||
> | |||
</div> | |||
</el-dialog> | |||
</div> | |||
</template> | |||
<script> | |||
import { formatDateTime } from "@/utils/filters"; | |||
import { | |||
getShopCustomerList, | |||
deleteShopCustomer, | |||
batchDeleteShopCustomer, | |||
exportShopCustomer, | |||
ShopCustomerAdd, | |||
ShopCustomerUpdate, | |||
} from "@/api/sales-analysis"; | |||
export default { | |||
name: "ShopCustomer", | |||
data() { | |||
return { | |||
loading: false, | |||
submitting: false, | |||
relationList: [], | |||
selectedRows: [], | |||
searchForm: { | |||
shopName: "", | |||
customerName: "", | |||
}, | |||
pagination: { | |||
page: 1, | |||
pageSize: 50, | |||
total: 0, | |||
}, | |||
dialogVisible: false, | |||
isEdit: false, | |||
relationForm: { | |||
id: null, | |||
shopName: "", | |||
customerName: "", | |||
}, | |||
relationRules: { | |||
shopName: [ | |||
{ required: true, message: "请输入店铺名称", trigger: "blur" }, | |||
{ | |||
min: 1, | |||
max: 100, | |||
message: "店铺名称长度在 1 到 100 个字符", | |||
trigger: "blur", | |||
}, | |||
], | |||
customerName: [ | |||
{ required: true, message: "请输入客户名称", trigger: "blur" }, | |||
{ | |||
min: 1, | |||
max: 100, | |||
message: "客户名称长度在 1 到 100 个字符", | |||
trigger: "blur", | |||
}, | |||
], | |||
}, | |||
}; | |||
}, | |||
computed: { | |||
dialogTitle() { | |||
return this.isEdit ? "编辑店铺客户关联" : "新增店铺客户关联"; | |||
}, | |||
uploadUrl() { | |||
return process.env.VUE_APP_BASE_API + "/shop-customer/import"; | |||
}, | |||
}, | |||
created() { | |||
this.fetchRelationList(); | |||
}, | |||
methods: { | |||
formatDateTime, | |||
// 获取关联列表 | |||
async fetchRelationList() { | |||
try { | |||
this.loading = true; | |||
const params = { | |||
page: this.pagination.page, | |||
pageSize: this.pagination.pageSize, | |||
shopName: this.searchForm.shopName, | |||
customerName: this.searchForm.customerName, | |||
}; | |||
const response = await getShopCustomerList(params); | |||
if (response.code === 200) { | |||
this.relationList = response.data.list || []; | |||
this.pagination.total = response.data.pagination.total; | |||
} else { | |||
this.$message.error(response.message || "获取关联列表失败"); | |||
} | |||
} catch (error) { | |||
console.error("获取关联列表失败:", error); | |||
this.$message.error("获取关联列表失败"); | |||
} finally { | |||
this.loading = false; | |||
} | |||
}, | |||
// 搜索 | |||
handleSearch() { | |||
this.pagination.page = 1; | |||
this.fetchRelationList(); | |||
}, | |||
// 重置搜索 | |||
resetSearch() { | |||
this.searchForm.shopName = ""; | |||
this.searchForm.customerName = ""; | |||
this.handleSearch(); | |||
}, | |||
// 分页大小改变 | |||
handleSizeChange(val) { | |||
this.pagination.pageSize = val; | |||
this.pagination.page = 1; | |||
this.fetchRelationList(); | |||
}, | |||
// 当前页改变 | |||
handleCurrentChange(val) { | |||
this.pagination.page = val; | |||
this.fetchRelationList(); | |||
}, | |||
// 表格选择改变 | |||
handleSelectionChange(selection) { | |||
this.selectedRows = selection; | |||
}, | |||
// 新增关联 | |||
handleAdd() { | |||
this.isEdit = false; | |||
this.relationForm = { | |||
id: null, | |||
shopName: "", | |||
customerName: "", | |||
}; | |||
this.dialogVisible = true; | |||
}, | |||
// 编辑关联 | |||
handleEdit(row) { | |||
this.isEdit = true; | |||
this.relationForm = { | |||
id: row.id, | |||
shopName: row.shopName, | |||
customerName: row.customerName, | |||
}; | |||
this.dialogVisible = true; | |||
}, | |||
// 删除关联 | |||
handleDelete(row) { | |||
this.$confirm( | |||
`确定要删除店铺"${row.shopName}"与客户"${row.customerName}"的关联吗?`, | |||
"提示", | |||
{ | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
} | |||
) | |||
.then(async () => { | |||
try { | |||
const response = await deleteShopCustomer(row.id); | |||
if (response.code === 200) { | |||
this.$message.success("删除成功"); | |||
this.fetchRelationList(); | |||
} else { | |||
this.$message.error(response.message || "删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("删除失败:", error); | |||
this.$message.error("删除失败"); | |||
} | |||
}) | |||
.catch(() => {}); | |||
}, | |||
// 批量删除 | |||
handleBatchDelete() { | |||
if (!this.selectedRows.length) { | |||
this.$message.warning("请选择要删除的数据"); | |||
return; | |||
} | |||
const ids = this.selectedRows.map((row) => row.id).join(","); | |||
this.$confirm( | |||
`确定要删除选中的${this.selectedRows.length}条关联吗?`, | |||
"提示", | |||
{ | |||
confirmButtonText: "确定", | |||
cancelButtonText: "取消", | |||
type: "warning", | |||
} | |||
) | |||
.then(async () => { | |||
try { | |||
const response = await batchDeleteShopCustomer(ids); | |||
if (response.code === 200) { | |||
this.$message.success("批量删除成功"); | |||
this.fetchRelationList(); | |||
} else { | |||
this.$message.error(response.message || "批量删除失败"); | |||
} | |||
} catch (error) { | |||
console.error("批量删除失败:", error); | |||
this.$message.error("批量删除失败"); | |||
} | |||
}) | |||
.catch(() => {}); | |||
}, | |||
// 导出Excel | |||
async handleExport() { | |||
const response = await exportShopCustomer(); | |||
const blob = new Blob([response], { | |||
type: "application/vnd.ms-excel", | |||
}); | |||
const url = window.URL.createObjectURL(blob); | |||
const a = document.createElement("a"); | |||
a.href = url; | |||
a.download = `shop-customer-${new Date().toLocaleDateString()}.xlsx`; | |||
a.click(); | |||
}, | |||
// 上传前验证 | |||
beforeUpload(file) { | |||
const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase()); | |||
if (!isExcel) { | |||
this.$message.error("只能上传Excel文件!"); | |||
return false; | |||
} | |||
const isLt10M = file.size / 1024 / 1024 < 10; | |||
if (!isLt10M) { | |||
this.$message.error("上传文件大小不能超过10MB!"); | |||
return false; | |||
} | |||
return true; | |||
}, | |||
// 上传成功 | |||
onUploadSuccess(response) { | |||
if (response.code === 200) { | |||
const data = response.data || {}; | |||
let message = response.message || "导入成功"; | |||
// 如果有错误详情,显示详细信息 | |||
if (data.errors > 0) { | |||
message += `\n${data.errors}条数据有问题已跳过`; | |||
if (data.errorDetails && data.errorDetails.length > 0) { | |||
this.$alert( | |||
`导入完成!\n\n统计信息:\n- 总共处理:${ | |||
data.total | |||
}条\n- 成功导入:${data.imported}条\n- 跳过错误:${ | |||
data.errors | |||
}条\n\n错误详情(前10条):\n${data.errorDetails | |||
.slice(0, 10) | |||
.join("\n")}`, | |||
"导入结果", | |||
{ | |||
confirmButtonText: "确定", | |||
type: "warning", | |||
} | |||
); | |||
} else { | |||
this.$message.warning(message); | |||
} | |||
} else { | |||
this.$message.success(message); | |||
} | |||
this.fetchRelationList(); | |||
} else { | |||
this.$message.error(response.message || "导入失败"); | |||
} | |||
}, | |||
// 上传失败 | |||
onUploadError(error) { | |||
console.error("上传失败:", error); | |||
this.$message.error("文件上传失败"); | |||
}, | |||
// 提交表单 | |||
async handleSubmit() { | |||
try { | |||
await this.$refs.relationForm.validate(); | |||
this.submitting = true; | |||
const response = this.isEdit | |||
? await ShopCustomerUpdate(this.relationForm.id, this.relationForm) | |||
: await ShopCustomerAdd(this.relationForm); | |||
if (response.code === 200) { | |||
this.$message.success(this.isEdit ? "编辑成功" : "新增成功"); | |||
this.dialogVisible = false; | |||
this.fetchRelationList(); | |||
} else { | |||
this.$message.error(response.message || "操作失败"); | |||
} | |||
} catch (error) { | |||
if (error.message !== "validation failed") { | |||
console.error("提交失败:", error); | |||
this.$message.error("操作失败"); | |||
} | |||
} finally { | |||
this.submitting = false; | |||
} | |||
}, | |||
// 对话框关闭 | |||
handleDialogClose() { | |||
this.$refs.relationForm.resetFields(); | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.shop-customer-container { | |||
min-height: calc(100vh - 50px); | |||
} | |||
.page-header-container { | |||
display: flex; | |||
align-items: center; | |||
background: #fff; | |||
padding: 15px 20px; | |||
border-radius: 6px; | |||
margin-bottom: 20px; | |||
border: 1px solid #e6ebf5; | |||
} | |||
.header-icon { | |||
font-size: 28px; | |||
margin-right: 15px; | |||
color: #e6a23c; | |||
} | |||
.header-text h1 { | |||
font-size: 20px; | |||
font-weight: 600; | |||
color: #303133; | |||
margin: 0; | |||
} | |||
.header-text p { | |||
font-size: 13px; | |||
color: #909399; | |||
margin-top: 4px; | |||
} | |||
.content-body { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 20px; | |||
} | |||
.action-panel, | |||
.data-table-panel { | |||
border-radius: 6px; | |||
border: 1px solid #e6ebf5; | |||
box-shadow: none; | |||
} | |||
.action-panel .el-card__body { | |||
padding: 15px 20px; | |||
} | |||
.data-table-panel .el-card__body { | |||
padding: 20px; | |||
} | |||
.toolbar { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
flex-wrap: wrap; | |||
gap: 15px; | |||
} | |||
.search-filters { | |||
display: flex; | |||
gap: 10px; | |||
align-items: center; | |||
flex-wrap: wrap; | |||
} | |||
.search-filters .el-input { | |||
width: 200px; | |||
} | |||
.action-buttons { | |||
display: flex; | |||
gap: 10px; | |||
align-items: center; | |||
flex-wrap: wrap; | |||
} | |||
.el-table { | |||
border-radius: 4px; | |||
} | |||
.pagination-container { | |||
padding-top: 20px; | |||
text-align: right; | |||
} | |||
.el-dialog__wrapper >>> .relation-dialog { | |||
border-radius: 8px; | |||
} | |||
.form-tips { | |||
font-size: 12px; | |||
color: #909399; | |||
line-height: 1.5; | |||
margin-top: 5px; | |||
} | |||
.form-tips i { | |||
margin-right: 4px; | |||
} | |||
.dialog-footer { | |||
text-align: right; | |||
} | |||
</style> |
@@ -86,12 +86,12 @@ | |||
> | |||
<el-table-column type="selection" width="55" align="center" /> | |||
<el-table-column | |||
label="用户名称" | |||
label="员工编号" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
/> | |||
<el-table-column | |||
label="用户昵称" | |||
label="姓名" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
/> |
@@ -26,8 +26,8 @@ | |||
<el-row> | |||
<el-table @row-click="clickRow" ref="table" :data="userList" @selection-change="handleSelectionChange" height="260px"> | |||
<el-table-column type="selection" width="55"></el-table-column> | |||
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" /> | |||
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" /> | |||
<el-table-column label="员工编号" prop="userName" :show-overflow-tooltip="true" /> | |||
<el-table-column label="姓名" prop="nickName" :show-overflow-tooltip="true" /> | |||
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" /> | |||
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" /> | |||
<el-table-column label="状态" align="center" prop="status"> |
@@ -4,12 +4,12 @@ | |||
<el-form ref="form" :model="form" label-width="80px"> | |||
<el-row> | |||
<el-col :span="8" :offset="2"> | |||
<el-form-item label="用户昵称" prop="nickName"> | |||
<el-form-item label="姓名" prop="nickName"> | |||
<el-input v-model="form.nickName" disabled /> | |||
</el-form-item> | |||
</el-col> | |||
<el-col :span="8" :offset="2"> | |||
<el-form-item label="登录账号" prop="userName"> | |||
<el-form-item label="员工编号" prop="userName"> | |||
<el-input v-model="form.userName" disabled /> | |||
</el-form-item> | |||
</el-col> |
@@ -171,7 +171,7 @@ | |||
<el-table-column | |||
v-if="columns[1].visible" | |||
key="userName" | |||
label="用户名称" | |||
label="员工编号" | |||
align="center" | |||
prop="userName" | |||
:show-overflow-tooltip="true" | |||
@@ -179,7 +179,7 @@ | |||
<el-table-column | |||
v-if="columns[2].visible" | |||
key="nickName" | |||
label="用户昵称" | |||
label="姓名" | |||
align="center" | |||
prop="nickName" | |||
:show-overflow-tooltip="true" | |||
@@ -289,10 +289,10 @@ | |||
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> | |||
<el-row> | |||
<el-col :span="12"> | |||
<el-form-item label="用户昵称" prop="nickName"> | |||
<el-form-item label="姓名" prop="nickName"> | |||
<el-input | |||
v-model="form.nickName" | |||
placeholder="请输入用户昵称" | |||
placeholder="请输入姓名" | |||
maxlength="30" | |||
/> | |||
</el-form-item> | |||
@@ -588,8 +588,8 @@ export default { | |||
// 列信息 | |||
columns: [ | |||
{ key: 0, label: `用户编号`, visible: true }, | |||
{ key: 1, label: `用户名称`, visible: true }, | |||
{ key: 2, label: `用户昵称`, visible: true }, | |||
{ key: 1, label: `员工编号`, visible: true }, | |||
{ key: 2, label: `姓名`, visible: true }, | |||
{ key: 3, label: `部门`, visible: true }, | |||
{ key: 4, label: `手机号码`, visible: true }, | |||
{ key: 5, label: `状态`, visible: true }, | |||
@@ -607,7 +607,7 @@ export default { | |||
} | |||
], | |||
nickName: [ | |||
{ required: true, message: '用户昵称不能为空', trigger: 'blur' } | |||
{ required: true, message: '姓名不能为空', trigger: 'blur' } | |||
], | |||
password: [ | |||
{ required: true, message: '用户密码不能为空', trigger: 'blur' }, |
@@ -1,6 +1,6 @@ | |||
<template> | |||
<el-form ref="form" :model="user" :rules="rules" label-width="80px"> | |||
<el-form-item label="用户昵称" prop="nickName"> | |||
<el-form-item label="姓名" prop="nickName"> | |||
<el-input v-model="user.nickName" maxlength="30" /> | |||
</el-form-item> | |||
<el-form-item label="手机号码" prop="phonenumber"> | |||
@@ -36,7 +36,7 @@ export default { | |||
// 表单校验 | |||
rules: { | |||
nickName: [ | |||
{ required: true, message: "用户昵称不能为空", trigger: "blur" } | |||
{ required: true, message: "姓名不能为空", trigger: "blur" } | |||
], | |||
email: [ | |||
{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, |