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, | |||||
}); | |||||
} |
justify-content: center; | justify-content: center; | ||||
gap: 16px; | gap: 16px; | ||||
padding-left: 16px; | padding-left: 16px; | ||||
i{ | |||||
i { | |||||
font-size: 22px; | font-size: 22px; | ||||
cursor: pointer; | cursor: pointer; | ||||
&:hover{ | |||||
&:hover { | |||||
color: var(--primary-color); | color: var(--primary-color); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
.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 { | .components-container { | ||||
margin: 30px 50px; | margin: 30px 50px; | ||||
position: relative; | position: relative; | ||||
} | } | ||||
} | } | ||||
.filter-container { | |||||
padding-bottom: 10px; | |||||
.filter-item { | |||||
display: inline-block; | |||||
vertical-align: middle; | |||||
margin-bottom: 10px; | |||||
} | |||||
} | |||||
//refine vue-multiselect plugin | //refine vue-multiselect plugin | ||||
.multiselect { | .multiselect { |
<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> |
> | > | ||||
<div class="avatar-wrapper"> | <div class="avatar-wrapper"> | ||||
<img :src="avatar" class="user-avatar"> | <img :src="avatar" class="user-avatar"> | ||||
<span>{{ name }}</span> | |||||
<span>{{ nickname }}</span> | |||||
<i class="el-icon-caret-bottom" /> | <i class="el-icon-caret-bottom" /> | ||||
</div> | </div> | ||||
<el-dropdown-menu slot="dropdown"> | <el-dropdown-menu slot="dropdown"> | ||||
Msg | Msg | ||||
}, | }, | ||||
computed: { | computed: { | ||||
...mapGetters(['sidebar', 'avatar', 'device', 'name']), | |||||
...mapGetters(['sidebar', 'avatar', 'device', 'name', 'nickname']), | |||||
setting: { | setting: { | ||||
get() { | get() { | ||||
return this.$store.state.settings.showSettings | return this.$store.state.settings.showSettings |
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 */ | /* Layout */ | ||||
import Layout from '@/layout' | |||||
import Layout from "@/layout"; | |||||
/** | /** | ||||
* Note: 路由配置项 | * Note: 路由配置项 | ||||
// 公共路由 | // 公共路由 | ||||
export const constantRoutes = [ | export const constantRoutes = [ | ||||
{ | { | ||||
path: '/redirect', | |||||
path: "/redirect", | |||||
component: Layout, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
children: [ | 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, | component: Layout, | ||||
redirect: 'index', | |||||
redirect: "index", | |||||
children: [ | 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, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
redirect: 'noredirect', | |||||
redirect: "noredirect", | |||||
children: [ | 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 = [ | export const dynamicRoutes = [ | ||||
{ | { | ||||
path: '/system/user-auth', | |||||
path: "/system/user-auth", | |||||
component: Layout, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
permissions: ['system:user:edit'], | |||||
permissions: ["system:user:edit"], | |||||
children: [ | 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, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
permissions: ['system:role:edit'], | |||||
permissions: ["system:role:edit"], | |||||
children: [ | 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, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
permissions: ['system:dict:list'], | |||||
permissions: ["system:dict:list"], | |||||
children: [ | 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, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
permissions: ['system:oss:list'], | |||||
permissions: ["system:oss:list"], | |||||
children: [ | 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, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
permissions: ['tool:gen:edit'], | |||||
permissions: ["tool:gen:edit"], | |||||
children: [ | 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, | component: Layout, | ||||
hidden: true, | hidden: true, | ||||
permissions: ['workflow:dynamicForm:edit'], | |||||
permissions: ["workflow:dynamicForm:edit"], | |||||
children: [ | 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; | let routerPush = Router.prototype.push; | ||||
Router.prototype.push = function push(location) { | 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({ | export default new Router({ | ||||
base: process.env.VUE_APP_CONTEXT_PATH, | base: process.env.VUE_APP_CONTEXT_PATH, | ||||
mode: 'history', // 去掉url中的# | |||||
mode: "history", // 去掉url中的# | |||||
scrollBehavior: () => ({ y: 0 }), | scrollBehavior: () => ({ y: 0 }), | ||||
routes: constantRoutes | |||||
}) | |||||
routes: constantRoutes, | |||||
}); |
token: (state) => state.user.token, | token: (state) => state.user.token, | ||||
avatar: (state) => state.user.avatar, | avatar: (state) => state.user.avatar, | ||||
name: (state) => state.user.name, | name: (state) => state.user.name, | ||||
nickname: (state) => state.user.nickname, | |||||
introduction: (state) => state.user.introduction, | introduction: (state) => state.user.introduction, | ||||
roles: (state) => state.user.roles, | roles: (state) => state.user.roles, | ||||
userinfo: (state) => state.user.userinfo, | userinfo: (state) => state.user.userinfo, |
SET_AVATAR: (state, avatar) => { | SET_AVATAR: (state, avatar) => { | ||||
state.avatar = avatar | state.avatar = avatar | ||||
}, | }, | ||||
SET_NICKNAME: (state, nickname) => { | |||||
state.nickname = nickname | |||||
}, | |||||
SET_ROLES: (state, roles) => { | SET_ROLES: (state, roles) => { | ||||
state.roles = roles | state.roles = roles | ||||
}, | }, | ||||
commit('SET_ROLES', ['ROLE_DEFAULT']) | commit('SET_ROLES', ['ROLE_DEFAULT']) | ||||
} | } | ||||
commit('SET_NAME', user.userName) | commit('SET_NAME', user.userName) | ||||
commit('SET_NICKNAME', user.nickName) | |||||
commit('SET_AVATAR', avatar) | commit('SET_AVATAR', avatar) | ||||
commit('SET_USERINFO', user) | commit('SET_USERINFO', user) | ||||
resolve(res) | resolve(res) |
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) | |||||
} |
<el-table-column | <el-table-column | ||||
v-if="columns[1].visible" | v-if="columns[1].visible" | ||||
key="userName" | key="userName" | ||||
label="用户名称" | |||||
label="用户编号" | |||||
align="center" | align="center" | ||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-table-column | <el-table-column | ||||
v-if="columns[2].visible" | v-if="columns[2].visible" | ||||
key="nickName" | key="nickName" | ||||
label="用户昵称" | |||||
label="用户名称" | |||||
align="center" | align="center" | ||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> | <el-form ref="form" :model="form" :rules="rules" label-width="80px"> | ||||
<el-row> | <el-row> | ||||
<el-col :span="12"> | <el-col :span="12"> | ||||
<el-form-item label="用户昵称" prop="nickName"> | |||||
<el-form-item label="用户名称" prop="nickName"> | |||||
<el-input | <el-input | ||||
v-model="form.nickName" | v-model="form.nickName" | ||||
placeholder="请输入用户昵称" | |||||
placeholder="请输入用户名称" | |||||
maxlength="30" | maxlength="30" | ||||
/> | /> | ||||
</el-form-item> | </el-form-item> | ||||
// 列信息 | // 列信息 | ||||
columns: [ | columns: [ | ||||
{ key: 0, label: `用户编号`, visible: true }, | { 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: 3, label: `部门`, visible: true }, | ||||
{ key: 4, label: `手机号码`, visible: true }, | { key: 4, label: `手机号码`, visible: true }, | ||||
{ key: 5, label: `状态`, visible: true }, | { key: 5, label: `状态`, visible: true }, | ||||
// 表单校验 | // 表单校验 | ||||
rules: { | rules: { | ||||
userName: [ | userName: [ | ||||
{ required: true, message: '用户名称不能为空', trigger: 'blur' }, | |||||
{ required: true, message: '员工编号不能为空', trigger: 'blur' }, | |||||
{ | { | ||||
min: 2, | min: 2, | ||||
max: 20, | max: 20, | ||||
} | } | ||||
], | ], | ||||
nickName: [ | nickName: [ | ||||
{ required: true, message: '用户昵称不能为空', trigger: 'blur' } | |||||
{ required: true, message: '姓名不能为空', trigger: 'blur' } | |||||
], | ], | ||||
password: [ | password: [ | ||||
{ required: true, message: '用户密码不能为空', trigger: 'blur' }, | { required: true, message: '用户密码不能为空', trigger: 'blur' }, |
<el-table-column | <el-table-column | ||||
v-if="columns[1].visible" | v-if="columns[1].visible" | ||||
key="userName" | key="userName" | ||||
label="用户名称" | |||||
label="员工编号" | |||||
align="center" | align="center" | ||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-table-column | <el-table-column | ||||
v-if="columns[2].visible" | v-if="columns[2].visible" | ||||
key="nickName" | key="nickName" | ||||
label="用户昵称" | |||||
label="姓名" | |||||
align="center" | align="center" | ||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
// 列信息 | // 列信息 | ||||
columns: [ | columns: [ | ||||
{ key: 0, label: `用户编号`, visible: true }, | { 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: 3, label: `部门`, visible: true }, | ||||
{ key: 4, label: `手机号码`, visible: true }, | { key: 4, label: `手机号码`, visible: true }, | ||||
{ key: 5, label: `创建时间`, visible: true } | { key: 5, label: `创建时间`, visible: true } |
<el-table-column | <el-table-column | ||||
v-if="columns[1].visible" | v-if="columns[1].visible" | ||||
key="userName" | key="userName" | ||||
label="用户名称" | |||||
label="员工编号" | |||||
align="center" | align="center" | ||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-table-column | <el-table-column | ||||
v-if="columns[2].visible" | v-if="columns[2].visible" | ||||
key="nickName" | key="nickName" | ||||
label="用户昵称" | |||||
label="姓名" | |||||
align="center" | align="center" | ||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
// 列信息 | // 列信息 | ||||
columns: [ | columns: [ | ||||
{ key: 0, label: `用户编号`, visible: true }, | { 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: 3, label: `部门`, visible: true }, | ||||
{ key: 4, label: `手机号码`, visible: true }, | { key: 4, label: `手机号码`, visible: true }, | ||||
{ key: 5, label: `创建时间`, visible: true } | { key: 5, label: `创建时间`, visible: true } |
<el-table-column | <el-table-column | ||||
v-if="columns[1].visible" | v-if="columns[1].visible" | ||||
key="userName" | key="userName" | ||||
label="用户名称" | |||||
label="员工编号" | |||||
align="center" | align="center" | ||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-table-column | <el-table-column | ||||
v-if="columns[2].visible" | v-if="columns[2].visible" | ||||
key="nickName" | key="nickName" | ||||
label="用户昵称" | |||||
label="姓名" | |||||
align="center" | align="center" | ||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
// 列信息 | // 列信息 | ||||
columns: [ | columns: [ | ||||
{ key: 0, label: `用户编号`, visible: true }, | { 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: 3, label: `部门`, visible: true }, | ||||
{ key: 4, label: `手机号码`, visible: true }, | { key: 4, label: `手机号码`, visible: true }, | ||||
{ key: 5, label: `创建时间`, visible: true } | { key: 5, label: `创建时间`, visible: true } |
<el-table-column | <el-table-column | ||||
v-if="columns[1].visible" | v-if="columns[1].visible" | ||||
key="userName" | key="userName" | ||||
label="用户名称" | |||||
label="员工编号" | |||||
align="center" | align="center" | ||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-table-column | <el-table-column | ||||
v-if="columns[2].visible" | v-if="columns[2].visible" | ||||
key="nickName" | key="nickName" | ||||
label="用户昵称" | |||||
label="姓名" | |||||
align="center" | align="center" | ||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
// 列信息 | // 列信息 | ||||
columns: [ | columns: [ | ||||
{ key: 0, label: `用户编号`, visible: true }, | { 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: 3, label: `部门`, visible: true }, | ||||
{ key: 4, label: `手机号码`, visible: true }, | { key: 4, label: `手机号码`, visible: true }, | ||||
{ key: 5, label: `创建时间`, visible: true } | { key: 5, label: `创建时间`, visible: true } |
style="width: 100%; margin-top: 20px" | style="width: 100%; margin-top: 20px" | ||||
empty-text="暂无关联员工" | 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 prop="deptName" label="部门"></el-table-column> | ||||
<el-table-column label="操作" width="80"> | <el-table-column label="操作" width="80"> | ||||
<template #default="scope"> | <template #default="scope"> |
<div class="info"> | <div class="info"> | ||||
<h3>{{ warmMessage }}</h3> | <h3>{{ warmMessage }}</h3> | ||||
<p> | <p> | ||||
<span>{{ nickname }}</span> | |||||
<el-divider direction="vertical" /> | |||||
<span>{{ userinfo.userName }}</span> | <span>{{ userinfo.userName }}</span> | ||||
<el-divider direction="vertical" /> | <el-divider direction="vertical" /> | ||||
<span>{{ userinfo.dept.deptName || '未设置部门' }}</span> | <span>{{ userinfo.dept.deptName || '未设置部门' }}</span> | ||||
} | } | ||||
}, | }, | ||||
computed: { | computed: { | ||||
...mapGetters(['userinfo', 'avatar']) | |||||
...mapGetters(['userinfo', 'avatar', 'nickname']) | |||||
}, | }, | ||||
created() { | created() { | ||||
this.warmMessage = messageGenerator( | this.warmMessage = messageGenerator( |
style="width: 100%; margin-top: 20px" | style="width: 100%; margin-top: 20px" | ||||
empty-text="暂无关联员工" | 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 prop="deptName" label="部门"></el-table-column> | ||||
<el-table-column label="操作" width="80"> | <el-table-column label="操作" width="80"> | ||||
<template #default="scope"> | <template #default="scope"> |
<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> |
<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> |
<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> |
<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> |
<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> |
<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> |
<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> |
> | > | ||||
<el-table-column type="selection" width="55" align="center" /> | <el-table-column type="selection" width="55" align="center" /> | ||||
<el-table-column | <el-table-column | ||||
label="用户名称" | |||||
label="员工编号" | |||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
/> | /> | ||||
<el-table-column | <el-table-column | ||||
label="用户昵称" | |||||
label="姓名" | |||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
/> | /> |
<el-row> | <el-row> | ||||
<el-table @row-click="clickRow" ref="table" :data="userList" @selection-change="handleSelectionChange" height="260px"> | <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 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="email" :show-overflow-tooltip="true" /> | ||||
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" /> | <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" /> | ||||
<el-table-column label="状态" align="center" prop="status"> | <el-table-column label="状态" align="center" prop="status"> |
<el-form ref="form" :model="form" label-width="80px"> | <el-form ref="form" :model="form" label-width="80px"> | ||||
<el-row> | <el-row> | ||||
<el-col :span="8" :offset="2"> | <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-input v-model="form.nickName" disabled /> | ||||
</el-form-item> | </el-form-item> | ||||
</el-col> | </el-col> | ||||
<el-col :span="8" :offset="2"> | <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-input v-model="form.userName" disabled /> | ||||
</el-form-item> | </el-form-item> | ||||
</el-col> | </el-col> |
<el-table-column | <el-table-column | ||||
v-if="columns[1].visible" | v-if="columns[1].visible" | ||||
key="userName" | key="userName" | ||||
label="用户名称" | |||||
label="员工编号" | |||||
align="center" | align="center" | ||||
prop="userName" | prop="userName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-table-column | <el-table-column | ||||
v-if="columns[2].visible" | v-if="columns[2].visible" | ||||
key="nickName" | key="nickName" | ||||
label="用户昵称" | |||||
label="姓名" | |||||
align="center" | align="center" | ||||
prop="nickName" | prop="nickName" | ||||
:show-overflow-tooltip="true" | :show-overflow-tooltip="true" | ||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> | <el-form ref="form" :model="form" :rules="rules" label-width="80px"> | ||||
<el-row> | <el-row> | ||||
<el-col :span="12"> | <el-col :span="12"> | ||||
<el-form-item label="用户昵称" prop="nickName"> | |||||
<el-form-item label="姓名" prop="nickName"> | |||||
<el-input | <el-input | ||||
v-model="form.nickName" | v-model="form.nickName" | ||||
placeholder="请输入用户昵称" | |||||
placeholder="请输入姓名" | |||||
maxlength="30" | maxlength="30" | ||||
/> | /> | ||||
</el-form-item> | </el-form-item> | ||||
// 列信息 | // 列信息 | ||||
columns: [ | columns: [ | ||||
{ key: 0, label: `用户编号`, visible: true }, | { 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: 3, label: `部门`, visible: true }, | ||||
{ key: 4, label: `手机号码`, visible: true }, | { key: 4, label: `手机号码`, visible: true }, | ||||
{ key: 5, label: `状态`, visible: true }, | { key: 5, label: `状态`, visible: true }, | ||||
} | } | ||||
], | ], | ||||
nickName: [ | nickName: [ | ||||
{ required: true, message: '用户昵称不能为空', trigger: 'blur' } | |||||
{ required: true, message: '姓名不能为空', trigger: 'blur' } | |||||
], | ], | ||||
password: [ | password: [ | ||||
{ required: true, message: '用户密码不能为空', trigger: 'blur' }, | { required: true, message: '用户密码不能为空', trigger: 'blur' }, |
<template> | <template> | ||||
<el-form ref="form" :model="user" :rules="rules" label-width="80px"> | <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-input v-model="user.nickName" maxlength="30" /> | ||||
</el-form-item> | </el-form-item> | ||||
<el-form-item label="手机号码" prop="phonenumber"> | <el-form-item label="手机号码" prop="phonenumber"> | ||||
// 表单校验 | // 表单校验 | ||||
rules: { | rules: { | ||||
nickName: [ | nickName: [ | ||||
{ required: true, message: "用户昵称不能为空", trigger: "blur" } | |||||
{ required: true, message: "姓名不能为空", trigger: "blur" } | |||||
], | ], | ||||
email: [ | email: [ | ||||
{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, | { required: true, message: "邮箱地址不能为空", trigger: "blur" }, |