ShopTRAINING/UI/src/views/StoreManagementView.vue
xz2000 9bd824c389 ---
**日期**: 2025-07-15 14:05
**主题**: 仪表盘UI调整

### 描述
根据用户请求,将仪表盘上的“数据管理”卡片替换为“店铺管理”。

### 主要改动
*   **文件**: `UI/src/views/DashboardView.vue`
*   **修改**:
    1.  在 `featureCards` 数组中,将原“数据管理”的对象修改为“店铺管理”。
    2.  更新了卡片的 `title`, `description`, `icon` 和 `path`,使其指向店铺管理页面 (`/store-management`)。
    3.  在脚本中导入了新的 `Shop` 图标。

### 结果
仪表盘现在直接提供到“店铺管理”页面的快捷入口,提高了操作效率,调整店铺管理的样式。
2025-07-15 19:18:25 +08:00

650 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="store-management-container">
<el-card class="full-height-card">
<template #header>
<div class="card-header">
<span>店铺管理</span>
<div class="header-actions">
<el-button type="primary" @click="showCreateDialog">
<el-icon><Plus /></el-icon>
新增店铺
</el-button>
<el-button @click="refreshStores">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</template>
<!-- 搜索和过滤 -->
<div class="table-container" ref="tableContainerRef">
<div class="filter-section" ref="filterSectionRef">
<el-row :gutter="20" align="middle">
<el-col :span="6">
<el-input
v-model="searchQuery"
placeholder="搜索店铺名称或ID"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="handleFilter" style="width: 100%;">
<el-option label="全部状态" value="" />
<el-option label="营业中" value="active" />
<el-option label="暂停营业" value="inactive" />
</el-select>
</el-col>
<el-col :span="4">
<el-select v-model="typeFilter" placeholder="类型筛选" clearable @change="handleFilter" style="width: 100%;">
<el-option label="全部类型" value="" />
<el-option label="旗舰店" value="旗舰店" />
<el-option label="标准店" value="标准店" />
<el-option label="便民店" value="便民店" />
<el-option label="社区店" value="社区店" />
</el-select>
</el-col>
</el-row>
</div>
<!-- 店铺列表 -->
<el-table
:data="pagedStores"
v-loading="loading"
stripe
@selection-change="handleSelectionChange"
class="store-table"
:height="tableHeight"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="store_id" label="店铺ID" width="100" align="center" />
<el-table-column prop="store_name" label="店铺名称" width="250" align="center" show-overflow-tooltip />
<el-table-column prop="location" label="位置" width="250" align="center" show-overflow-tooltip/>
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="{ row }">
<el-tag :type="getStoreTypeTag(row.type)">
{{ row.type }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="size" label="面积(㎡)" width="150" align="center"/>
<el-table-column prop="opening_date" label="开业日期" width="150" align="center"/>
<el-table-column prop="status" label="状态" width="150" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status === 'active' ? '营业中' : '暂停营业' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="viewStoreDetails(row)">
详情
</el-button>
<el-button link type="primary" @click="editStore(row)">
编辑
</el-button>
<el-button link type="primary" @click="viewStoreProducts(row)">
产品
</el-button>
<el-button link type="danger" @click="deleteStore(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="total > pageSize"
layout="total, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
@size-change="handleSizeChange"
class="pagination"
ref="paginationRef"
/>
</div>
</el-card>
<!-- 新增/编辑店铺对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEditing ? '编辑店铺' : '新增店铺'"
width="600px"
@close="resetForm"
class="form-dialog"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="店铺ID" prop="store_id">
<el-input
v-model="form.store_id"
:disabled="isEditing"
placeholder="请输入店铺ID如S001"
/>
</el-form-item>
<el-form-item label="店铺名称" prop="store_name">
<el-input
v-model="form.store_name"
placeholder="请输入店铺名称"
/>
</el-form-item>
<el-form-item label="位置" prop="location">
<el-input
v-model="form.location"
placeholder="请输入店铺地址"
/>
</el-form-item>
<el-form-item label="店铺类型" prop="type">
<el-select v-model="form.type" placeholder="请选择店铺类型" style="width: 100%">
<el-option label="旗舰店" value="旗舰店" />
<el-option label="标准店" value="标准店" />
<el-option label="便民店" value="便民店" />
<el-option label="社区店" value="社区店" />
</el-select>
</el-form-item>
<el-form-item label="面积(㎡)" prop="size">
<el-input-number
v-model="form.size"
:min="1"
:max="10000"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="开业日期" prop="opening_date">
<el-date-picker
v-model="form.opening_date"
type="date"
placeholder="选择开业日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="active">营业中</el-radio>
<el-radio label="inactive">暂停营业</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting">
{{ isEditing ? '更新' : '创建' }}
</el-button>
</template>
</el-dialog>
<!-- 店铺详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="店铺详情"
width="800px"
>
<div v-if="selectedStore" class="store-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="店铺ID">{{ selectedStore.store_id }}</el-descriptions-item>
<el-descriptions-item label="店铺名称">{{ selectedStore.store_name }}</el-descriptions-item>
<el-descriptions-item label="位置">{{ selectedStore.location }}</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag :type="getStoreTypeTag(selectedStore.type)">
{{ selectedStore.type }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="面积">{{ selectedStore.size }} ㎡</el-descriptions-item>
<el-descriptions-item label="开业日期">{{ selectedStore.opening_date }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="selectedStore.status === 'active' ? 'success' : 'danger'">
{{ selectedStore.status === 'active' ? '营业中' : '暂停营业' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 店铺统计信息 -->
<div class="store-stats" v-if="storeStats">
<h4>店铺统计</h4>
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="销售产品种类" :value="storeStats.product_count || 0" />
</el-col>
<el-col :span="6">
<el-statistic title="总销售额" :value="storeStats.total_sales || 0" :precision="2" prefix="¥" />
</el-col>
<el-col :span="6">
<el-statistic title="总销量" :value="storeStats.total_quantity || 0" />
</el-col>
<el-col :span="6">
<el-statistic title="销售记录数" :value="storeStats.record_count || 0" />
</el-col>
</el-row>
</div>
</div>
</el-dialog>
<!-- 店铺产品对话框 -->
<el-dialog
v-model="productsDialogVisible"
title="店铺产品列表"
width="1000px"
>
<div v-if="storeProducts.length > 0">
<el-table :data="storeProducts" stripe>
<el-table-column prop="product_id" label="产品ID" width="100" />
<el-table-column prop="product_name" label="产品名称" width="200" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="total_sales" label="总销量" width="100" align="right" />
<el-table-column prop="avg_price" label="平均价格" width="100" align="right">
<template #default="{ row }">
¥{{ row.avg_price?.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="last_sale_date" label="最后销售日期" width="120" />
</el-table>
</div>
<el-empty v-else description="该店铺暂无产品销售记录" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
// 响应式数据
const stores = ref([])
const loading = ref(false)
const selectedStores = ref([])
// 搜索和过滤
const searchQuery = ref('')
const statusFilter = ref('')
const typeFilter = ref('')
// 分页
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
// 布局和高度
const tableContainerRef = ref(null);
const filterSectionRef = ref(null);
const paginationRef = ref(null);
const tableHeight = ref(400); // 默认高度
// 对话框
const dialogVisible = ref(false)
const detailDialogVisible = ref(false)
const productsDialogVisible = ref(false)
const isEditing = ref(false)
const submitting = ref(false)
// 表单
const formRef = ref()
const form = ref({
store_id: '',
store_name: '',
location: '',
type: '',
size: null,
opening_date: '',
status: 'active'
})
// 详情数据
const selectedStore = ref(null)
const storeStats = ref(null)
const storeProducts = ref([])
// 表单验证规则
const rules = {
store_id: [
{ required: true, message: '请输入店铺ID', trigger: 'blur' },
{ pattern: /^[A-Z]\d{3}$/, message: '店铺ID格式应为S001', trigger: 'blur' }
],
store_name: [
{ required: true, message: '请输入店铺名称', trigger: 'blur' },
{ min: 2, max: 50, message: '店铺名称长度在2到50个字符', trigger: 'blur' }
],
location: [
{ required: true, message: '请输入店铺位置', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择店铺类型', trigger: 'change' }
]
}
// 计算属性
const filteredStores = computed(() => {
let result = stores.value;
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter(
(store) =>
store.store_name.toLowerCase().includes(query) ||
store.store_id.toLowerCase().includes(query)
);
}
if (statusFilter.value) {
result = result.filter((store) => store.status === statusFilter.value);
}
if (typeFilter.value) {
result = result.filter((store) => store.type === typeFilter.value);
}
return result;
});
const pagedStores = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
total.value = filteredStores.value.length;
return filteredStores.value.slice(start, end);
});
// 方法
const fetchStores = async () => {
try {
loading.value = true
const response = await axios.get('/api/stores')
if (response.data.status === 'success') {
stores.value = response.data.data
} else {
ElMessage.error('获取店铺列表失败')
}
} catch (error) {
ElMessage.error('请求失败')
console.error(error)
} finally {
loading.value = false
}
}
const refreshStores = () => {
fetchStores()
}
const handleSearch = () => {
currentPage.value = 1
}
const handleFilter = () => {
currentPage.value = 1
}
const handlePageChange = (page) => {
currentPage.value = page
}
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
}
const handleSelectionChange = (selection) => {
selectedStores.value = selection
}
const getStoreTypeTag = (type) => {
const typeMap = {
'旗舰店': 'primary',
'标准店': 'success',
'便民店': 'info',
'社区店': 'warning'
}
return typeMap[type] || 'info'
}
const showCreateDialog = () => {
isEditing.value = false
dialogVisible.value = true
resetForm()
}
const editStore = (store) => {
isEditing.value = true
form.value = { ...store }
dialogVisible.value = true
}
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields()
}
form.value = {
store_id: '',
store_name: '',
location: '',
type: '',
size: null,
opening_date: '',
status: 'active'
}
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
submitting.value = true
const url = isEditing.value ? `/api/stores/${form.value.store_id}` : '/api/stores'
const method = isEditing.value ? 'put' : 'post'
const response = await axios[method](url, form.value)
if (response.data.status === 'success') {
ElMessage.success(isEditing.value ? '店铺更新成功' : '店铺创建成功')
dialogVisible.value = false
await fetchStores()
} else {
ElMessage.error(response.data.message || '操作失败')
}
} catch (error) {
ElMessage.error('请求失败')
console.error(error)
} finally {
submitting.value = false
}
}
})
}
const deleteStore = async (store) => {
try {
await ElMessageBox.confirm(
`确定要删除店铺 "${store.store_name}" 吗?此操作不可恢复。`,
'确认删除',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await axios.delete(`/api/stores/${store.store_id}`)
if (response.data.status === 'success') {
ElMessage.success('店铺删除成功')
await fetchStores()
} else {
ElMessage.error(response.data.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除请求失败')
console.error(error)
}
}
}
const viewStoreDetails = async (store) => {
selectedStore.value = store
detailDialogVisible.value = true
// 获取店铺统计信息
try {
const response = await axios.get(`/api/stores/${store.store_id}/statistics`)
if (response.data.status === 'success') {
storeStats.value = response.data.data
}
} catch (error) {
console.error('获取店铺统计失败:', error)
}
}
const viewStoreProducts = async (store) => {
selectedStore.value = store
productsDialogVisible.value = true
// 获取店铺产品列表
try {
const response = await axios.get(`/api/stores/${store.store_id}/products`)
if (response.data.status === 'success') {
storeProducts.value = response.data.data
}
} catch (error) {
console.error('获取店铺产品失败:', error)
storeProducts.value = []
}
}
// 生命周期
const updateTableHeight = () => {
nextTick(() => {
if (tableContainerRef.value) {
const containerHeight = tableContainerRef.value.clientHeight;
const filterHeight = filterSectionRef.value?.offsetHeight || 0;
const paginationHeight = paginationRef.value?.$el.offsetHeight || 0;
// 减去筛选区、分页区以及一些间距
const calculatedHeight = containerHeight - filterHeight - paginationHeight - 20;
tableHeight.value = calculatedHeight > 200 ? calculatedHeight : 200; // 最小高度
}
});
};
onMounted(() => {
fetchStores();
updateTableHeight();
window.addEventListener('resize', updateTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight);
});
</script>
<style scoped>
.store-management-container {
height: 97%;
padding: 6px 10px 15px 15px;
}
.full-height-card {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.el-card__body) {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
gap: 10px;
}
.table-container {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden; /* 确保容器本身不滚动 */
}
.filter-section {
padding-bottom: 20px;
}
.store-table {
width: 100%;
}
:deep(.store-table .el-table__cell) {
padding: 12px 2px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 14px 0;
}
.store-detail {
padding: 5px 0;
}
.store-stats {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
.store-stats h4 {
margin-bottom: 20px;
color: #303133;
}
@media (max-width: 768px) {
.store-management-container {
padding: 10px;
}
.filter-section .el-col {
margin-bottom: 10px;
}
.header-actions {
flex-direction: column;
gap: 5px;
}
}
.form-dialog :deep(.el-dialog) {
background: transparent;
box-shadow: none;
}
</style>