剥离商城相关功能
This commit is contained in:
@@ -30,16 +30,6 @@
|
||||
<template #title>用户审核</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/products">
|
||||
<el-icon><Goods /></el-icon>
|
||||
<template #title>商品管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/orders">
|
||||
<el-icon><List /></el-icon>
|
||||
<template #title>订单管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/points">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<template #title>积分管理</template>
|
||||
|
||||
@@ -50,47 +50,6 @@ const routes = [
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
name: 'Products',
|
||||
component: () => import('@/views/Products.vue'),
|
||||
meta: {
|
||||
title: '商品管理 - 积分商城管理系统',
|
||||
icon: 'Goods',
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'products/create',
|
||||
name: 'CreateProduct',
|
||||
component: () => import('@/views/ProductForm.vue'),
|
||||
meta: {
|
||||
title: '创建商品 - 积分商城管理系统',
|
||||
icon: 'Plus',
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'products/edit/:id',
|
||||
name: 'EditProduct',
|
||||
component: () => import('@/views/ProductForm.vue'),
|
||||
meta: {
|
||||
title: '编辑商品 - 积分商城管理系统',
|
||||
icon: 'EditPen',
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'products/:id/spec-combinations',
|
||||
name: 'ProductSpecCombinations',
|
||||
component: () => import('@/views/ProductSpecCombinations.vue'),
|
||||
meta: {
|
||||
title: '商品规格组合管理 - 积分商城管理系统',
|
||||
icon: 'Grid',
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'Orders',
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
<template>
|
||||
<div class="orders-container">
|
||||
<div class="header">
|
||||
<h2>订单管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="订单号">
|
||||
<el-input
|
||||
v-model="filters.orderNumber"
|
||||
placeholder="请输入订单号"
|
||||
clearable
|
||||
@keyup.enter="loadOrders"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="filters.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keyup.enter="loadOrders"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="display: inline-block; width: 150px"
|
||||
>
|
||||
<el-option label="待发货" value="pending" />
|
||||
<el-option label="已发货" value="shipped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadOrders">搜索</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table :data="orders" v-loading="loading" stripe>
|
||||
<el-table-column prop="order_no" label="订单号" width="200" />
|
||||
<el-table-column prop="username" label="用户" width="120" />
|
||||
<el-table-column prop="total_points" label="总积分" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="points-text">{{ row.total_points }} 积分</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_rongdou" label="总融豆" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="points-text">{{ row.total_rongdou }} 融豆</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品信息" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="order-items">
|
||||
<div v-for="item in row.items" :key="item.id" class="order-item">
|
||||
<el-image
|
||||
:src="getImageUrl(item.image_url)"
|
||||
fit="cover"
|
||||
style="width: 40px; height: 40px; border-radius: 4px"
|
||||
/>
|
||||
<div class="item-info">
|
||||
<div class="item-name">{{ item.product_name }}</div>
|
||||
<div class="item-detail">
|
||||
{{ item.points }}积分 × {{ item.quantity }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="viewOrder(row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
v-if="row.status !== 'cancelled' && row.status !== 'completed'"
|
||||
>
|
||||
<el-button type="warning" size="small">
|
||||
更新状态
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-if="row.status === 'pending'"
|
||||
@click="updateOrderStatus(row, 'shipped')"
|
||||
>
|
||||
标记为已发货
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="row.status === 'shipped'"
|
||||
@click="updateOrderStatus(row, 'completed')"
|
||||
>
|
||||
标记为已完成
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click="updateOrderStatus(row, 'cancelled')"
|
||||
divided
|
||||
>
|
||||
取消订单
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadOrders"
|
||||
@current-change="loadOrders"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="订单详情"
|
||||
width="800px"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<div v-if="selectedOrder" class="order-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="订单号">{{
|
||||
selectedOrder.order_no
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户">{{
|
||||
selectedOrder.username
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="总积分">
|
||||
<span class="points-text"
|
||||
>{{ selectedOrder.total_points }} 积分</span
|
||||
>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">
|
||||
<el-tag :type="getStatusType(selectedOrder.status)">
|
||||
{{ getStatusText(selectedOrder.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{
|
||||
formatDate(selectedOrder.created_at)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{
|
||||
formatDate(selectedOrder.updated_at)
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 style="margin: 20px 0 10px 0">商品清单</h4>
|
||||
<el-table :data="selectedOrder.items" border>
|
||||
<el-table-column label="商品图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="getImageUrl(row.image_url)"
|
||||
fit="cover"
|
||||
style="width: 60px; height: 60px; border-radius: 4px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="product_name" label="商品名称" />
|
||||
<el-table-column prop="points" label="单价">
|
||||
<template #default="{ row }">
|
||||
<span class="points-text">{{ row.points_price }} 积分 | {{ row.rongdou_price }} 融豆</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column label="小计" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="points-text"
|
||||
>{{ row.points_price * row.quantity }} 积分 | {{ row.rongdou_price * row.quantity }} 融豆</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ArrowDown } from '@element-plus/icons-vue';
|
||||
import api from '@/utils/api';
|
||||
import { getImageUrl } from '@/utils/config';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const loading = ref(false);
|
||||
const orders = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const selectedOrder = ref(null);
|
||||
|
||||
const filters = reactive({
|
||||
orderNumber: '',
|
||||
username: '',
|
||||
status: '',
|
||||
dateRange: null,
|
||||
});
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 加载订单列表
|
||||
const loadOrders = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
orderNumber: filters.orderNumber,
|
||||
username: filters.username,
|
||||
status: filters.status,
|
||||
};
|
||||
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
params.startDate = filters.dateRange[0];
|
||||
params.endDate = filters.dateRange[1];
|
||||
}
|
||||
|
||||
const { data } = await api.get('/orders', { params });
|
||||
orders.value = data.data.orders;
|
||||
pagination.total = data.data.total;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载订单列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = () => {
|
||||
Object.assign(filters, {
|
||||
orderNumber: '',
|
||||
username: '',
|
||||
status: '',
|
||||
dateRange: null,
|
||||
});
|
||||
pagination.page = 1;
|
||||
loadOrders();
|
||||
};
|
||||
|
||||
// 查看订单详情
|
||||
const viewOrder = async (order) => {
|
||||
try {
|
||||
const { data } = await api.get(`/orders/${order.id}`);
|
||||
selectedOrder.value = data.data.order;
|
||||
dialogVisible.value = true;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载订单详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 更新订单状态
|
||||
const updateOrderStatus = async (order, newStatus) => {
|
||||
const statusText = getStatusText(newStatus);
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要将订单状态更新为「${statusText}」吗?`,
|
||||
'确认操作',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
await api.put(`/orders/${order.id}/status`, { status: newStatus });
|
||||
ElMessage.success('订单状态更新成功');
|
||||
loadOrders();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('更新订单状态失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
selectedOrder.value = null;
|
||||
};
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const types = {
|
||||
pending: 'warning',
|
||||
shipped: 'primary',
|
||||
completed: 'success',
|
||||
cancelled: 'danger',
|
||||
};
|
||||
return types[status] || 'info';
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: '待发货',
|
||||
shipped: '已发货',
|
||||
pre_order: '待支付',
|
||||
cancelled: '已取消',
|
||||
completed: '已完成',
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadOrders();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.orders-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filters {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.points-text {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.item-detail {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.order-detail {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,438 +0,0 @@
|
||||
<template>
|
||||
<div class="attributes-container">
|
||||
<div class="header">
|
||||
<h2>商品属性管理 - {{ productName }}</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showAddDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加属性
|
||||
</el-button>
|
||||
<el-button @click="$router.back()">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="attributes" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="attribute_key" label="属性名" min-width="150" />
|
||||
<el-table-column prop="attribute_value" label="属性值" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.attribute_value.length <= 50" type="info">
|
||||
{{ row.attribute_value }}
|
||||
</el-tag>
|
||||
<el-tooltip v-else :content="row.attribute_value" placement="top">
|
||||
<el-tag type="info">
|
||||
{{ row.attribute_value.substring(0, 50) }}...
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="editAttribute(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteAttribute(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑属性对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑属性' : '添加属性'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="属性名" prop="attribute_key">
|
||||
<el-input
|
||||
v-model="form.attribute_key"
|
||||
placeholder="请输入属性名,如:品牌、材质、产地"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="属性值" prop="attribute_value">
|
||||
<el-input
|
||||
v-model="form.attribute_value"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入属性值,如:苹果、纯棉、中国制造"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitting">
|
||||
{{ isEdit ? '更新' : '添加' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量添加属性对话框 -->
|
||||
<el-dialog
|
||||
v-model="batchDialogVisible"
|
||||
title="批量添加属性"
|
||||
width="600px"
|
||||
>
|
||||
<div class="batch-form">
|
||||
<el-alert
|
||||
title="批量添加说明"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<template #default>
|
||||
<p>每行一个属性,格式:属性名:属性值</p>
|
||||
<p>示例:</p>
|
||||
<p>品牌:苹果</p>
|
||||
<p>颜色:黑色</p>
|
||||
<p>尺寸:6.1英寸</p>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-input
|
||||
v-model="batchText"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请按格式输入属性,每行一个"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitBatchForm" :loading="submitting">
|
||||
批量添加
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<div class="quick-actions">
|
||||
<el-button type="success" @click="showBatchDialog">
|
||||
<el-icon><DocumentAdd /></el-icon>
|
||||
批量添加
|
||||
</el-button>
|
||||
<el-button type="warning" @click="clearAllAttributes">
|
||||
<el-icon><Delete /></el-icon>
|
||||
清空所有属性
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, ArrowLeft, DocumentAdd, Delete } from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const batchDialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const attributes = ref([])
|
||||
const productName = ref('')
|
||||
const batchText = ref('')
|
||||
|
||||
const form = reactive({
|
||||
attribute_key: '',
|
||||
attribute_value: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
attribute_key: [
|
||||
{ required: true, message: '请输入属性名', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '属性名长度在 1 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
attribute_value: [
|
||||
{ required: true, message: '请输入属性值', trigger: 'blur' },
|
||||
{ min: 1, max: 500, message: '属性值长度在 1 到 500 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载商品属性列表
|
||||
const loadAttributes = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await api.products.getAttributes(route.params.id)
|
||||
attributes.value = data.data.attributes || []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载属性列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载商品信息
|
||||
const loadProduct = async () => {
|
||||
try {
|
||||
const { data } = await api.products.getProductById(route.params.id)
|
||||
productName.value = data.data.product.name
|
||||
} catch (error) {
|
||||
ElMessage.error('加载商品信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加对话框
|
||||
const showAddDialog = () => {
|
||||
isEdit.value = false
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示批量添加对话框
|
||||
const showBatchDialog = () => {
|
||||
batchText.value = ''
|
||||
batchDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑属性
|
||||
const editAttribute = (attr) => {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
id: attr.id,
|
||||
attribute_key: attr.attribute_key,
|
||||
attribute_value: attr.attribute_value
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
|
||||
const submitData = {
|
||||
attribute_key: form.attribute_key,
|
||||
attribute_value: form.attribute_value
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await api.products.updateAttribute(route.params.id, form.id, submitData)
|
||||
ElMessage.success('属性更新成功')
|
||||
} else {
|
||||
await api.products.createAttribute(route.params.id, submitData)
|
||||
ElMessage.success('属性添加成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
loadAttributes()
|
||||
} catch (error) {
|
||||
if (error.response?.data?.message) {
|
||||
ElMessage.error(error.response.data.message)
|
||||
} else {
|
||||
ElMessage.error(isEdit.value ? '更新属性失败' : '添加属性失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量提交表单
|
||||
const submitBatchForm = async () => {
|
||||
if (!batchText.value.trim()) {
|
||||
ElMessage.error('请输入要添加的属性')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
const lines = batchText.value.trim().split('\n')
|
||||
const attributesData = []
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (!trimmedLine) continue
|
||||
|
||||
const colonIndex = trimmedLine.indexOf(':')
|
||||
if (colonIndex === -1) {
|
||||
ElMessage.error(`格式错误:${trimmedLine},请使用"属性名:属性值"格式`)
|
||||
return
|
||||
}
|
||||
|
||||
const key = trimmedLine.substring(0, colonIndex).trim()
|
||||
const value = trimmedLine.substring(colonIndex + 1).trim()
|
||||
|
||||
if (!key || !value) {
|
||||
ElMessage.error(`格式错误:${trimmedLine},属性名和属性值不能为空`)
|
||||
return
|
||||
}
|
||||
|
||||
attributesData.push({
|
||||
attribute_key: key,
|
||||
attribute_value: value
|
||||
})
|
||||
}
|
||||
|
||||
if (attributesData.length === 0) {
|
||||
ElMessage.error('没有有效的属性数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 批量添加属性
|
||||
for (const attrData of attributesData) {
|
||||
await api.products.createAttribute(route.params.id, attrData)
|
||||
}
|
||||
|
||||
ElMessage.success(`成功添加 ${attributesData.length} 个属性`)
|
||||
batchDialogVisible.value = false
|
||||
loadAttributes()
|
||||
} catch (error) {
|
||||
if (error.response?.data?.message) {
|
||||
ElMessage.error(error.response.data.message)
|
||||
} else {
|
||||
ElMessage.error('批量添加属性失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除属性
|
||||
const deleteAttribute = async (attr) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除属性「${attr.attribute_key}: ${attr.attribute_value}」吗?此操作不可恢复!`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.products.deleteAttribute(route.params.id, attr.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadAttributes()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有属性
|
||||
const clearAllAttributes = async () => {
|
||||
if (attributes.value.length === 0) {
|
||||
ElMessage.info('没有属性可以清空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要清空所有 ${attributes.value.length} 个属性吗?此操作不可恢复!`,
|
||||
'确认清空',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 批量删除所有属性
|
||||
for (const attr of attributes.value) {
|
||||
await api.products.deleteAttribute(route.params.id, attr.id)
|
||||
}
|
||||
|
||||
ElMessage.success('清空成功')
|
||||
loadAttributes()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('清空失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
attribute_key: '',
|
||||
attribute_value: ''
|
||||
})
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProduct()
|
||||
loadAttributes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.attributes-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.batch-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,831 +0,0 @@
|
||||
<template>
|
||||
<div class="spec-combinations-container">
|
||||
<div class="header">
|
||||
<h2>商品规格组合管理 - {{ productName }}</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showSpecNamesDialog">
|
||||
<el-icon><Setting /></el-icon>
|
||||
管理规格名称
|
||||
</el-button>
|
||||
<el-button type="success" @click="showGenerateCombinationsDialog">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
生成规格组合
|
||||
</el-button>
|
||||
<el-button @click="$router.back()">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规格名称管理 -->
|
||||
<el-card class="spec-names-card" v-if="specNames.length > 0">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>规格维度</span>
|
||||
<el-button type="primary" size="small" @click="showSpecNamesDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加规格维度
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="spec-names-list">
|
||||
<el-tag
|
||||
v-for="specName in specNames"
|
||||
:key="specName.id"
|
||||
size="large"
|
||||
closable
|
||||
@close="deleteSpecName(specName)"
|
||||
@click="editSpecName(specName)"
|
||||
class="spec-name-tag"
|
||||
>
|
||||
{{ specName.name }}
|
||||
<span class="spec-name-count">({{ getSpecValueCount(specName.id) }}个值)</span>
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 规格组合列表 -->
|
||||
<el-table :data="combinations" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="combination_key" label="组合键" width="200" />
|
||||
<el-table-column prop="display_text" label="规格组合" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-for="(spec, index) in row.spec_details"
|
||||
:key="index"
|
||||
size="small"
|
||||
:type="getTagType(index)"
|
||||
class="spec-tag"
|
||||
>
|
||||
{{ spec.spec_name }}: {{ spec.value }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="price_adjustment" label="价格调整" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="{
|
||||
'price-positive': row.price_adjustment > 0,
|
||||
'price-negative': row.price_adjustment < 0,
|
||||
'price-zero': row.price_adjustment === 0
|
||||
}">
|
||||
{{ row.price_adjustment > 0 ? '+' : '' }}{{ row.price_adjustment }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="points_adjustment" label="积分调整" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="{
|
||||
'price-positive': row.points_adjustment > 0,
|
||||
'price-negative': row.points_adjustment < 0,
|
||||
'price-zero': row.points_adjustment === 0
|
||||
}">
|
||||
{{ row.points_adjustment > 0 ? '+' : '' }}{{ row.points_adjustment }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rongdou_adjustment" label="融豆调整" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="{
|
||||
'price-positive': row.rongdou_adjustment > 0,
|
||||
'price-negative': row.rongdou_adjustment < 0,
|
||||
'price-zero': row.rongdou_adjustment === 0
|
||||
}">
|
||||
{{ row.rongdou_adjustment > 0 ? '+' : '' }}{{ row.rongdou_adjustment }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock" label="库存" width="100" />
|
||||
<el-table-column prop="is_available" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_available ? 'success' : 'danger'">
|
||||
{{ row.is_available ? '可用' : '不可用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="editCombination(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteCombination(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 规格名称管理对话框 -->
|
||||
<el-dialog
|
||||
v-model="specNamesDialogVisible"
|
||||
title="规格名称管理"
|
||||
width="800px"
|
||||
>
|
||||
<div class="spec-names-management">
|
||||
<div class="add-spec-name">
|
||||
<el-input
|
||||
v-model="newSpecName"
|
||||
placeholder="请输入规格名称,如:颜色、尺寸、材质"
|
||||
@keyup.enter="addSpecName"
|
||||
/>
|
||||
<el-button type="primary" @click="addSpecName">添加</el-button>
|
||||
</div>
|
||||
|
||||
<div class="spec-names-with-values">
|
||||
<div
|
||||
v-for="specName in specNames"
|
||||
:key="specName.id"
|
||||
class="spec-name-item"
|
||||
>
|
||||
<div class="spec-name-header">
|
||||
<h4>{{ specName.name }}</h4>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteSpecName(specName)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="spec-values">
|
||||
<div class="add-spec-value">
|
||||
<el-input
|
||||
v-model="newSpecValues[specName.id]"
|
||||
placeholder="请输入规格值"
|
||||
@keyup.enter="addSpecValue(specName.id)"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addSpecValue(specName.id)"
|
||||
>
|
||||
添加值
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="spec-value-list">
|
||||
<el-tag
|
||||
v-for="value in getSpecValues(specName.id)"
|
||||
:key="value.id"
|
||||
closable
|
||||
@close="deleteSpecValue(value)"
|
||||
class="spec-value-tag"
|
||||
>
|
||||
{{ value.value }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑规格组合对话框 -->
|
||||
<el-dialog
|
||||
v-model="combinationDialogVisible"
|
||||
title="编辑规格组合"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="combinationFormRef"
|
||||
:model="combinationForm"
|
||||
:rules="combinationRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="规格组合">
|
||||
<div class="combination-display">
|
||||
<el-tag
|
||||
v-for="(spec, index) in combinationForm.spec_details"
|
||||
:key="index"
|
||||
size="large"
|
||||
:type="getTagType(index)"
|
||||
>
|
||||
{{ spec.spec_name }}: {{ spec.spec_value }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="价格调整" prop="price_adjustment">
|
||||
<el-input-number
|
||||
v-model="combinationForm.price_adjustment"
|
||||
:precision="2"
|
||||
placeholder="价格调整(正数为加价,负数为减价)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="积分调整" prop="points_adjustment">
|
||||
<el-input-number
|
||||
v-model="combinationForm.points_adjustment"
|
||||
:min="0"
|
||||
placeholder="积分调整"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="融豆调整" prop="rongdou_adjustment">
|
||||
<el-input-number
|
||||
v-model="combinationForm.rongdou_adjustment"
|
||||
:min="0"
|
||||
placeholder="融豆调整"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="库存" prop="stock">
|
||||
<el-input-number
|
||||
v-model="combinationForm.stock"
|
||||
:min="0"
|
||||
placeholder="库存数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否可用">
|
||||
<el-switch
|
||||
v-model="combinationForm.is_available"
|
||||
active-text="可用"
|
||||
inactive-text="不可用"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="combinationDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitCombinationForm" :loading="submitting">
|
||||
更新
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 生成规格组合选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="generateDialogVisible"
|
||||
title="选择规格名称生成组合"
|
||||
width="600px"
|
||||
>
|
||||
<div class="generate-combinations-content">
|
||||
<p class="dialog-description">
|
||||
请选择要用于生成规格组合的规格名称。系统将基于选中的规格名称及其规格值生成笛卡尔积组合。
|
||||
</p>
|
||||
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="选择规格名称">
|
||||
<el-checkbox-group v-model="selectedSpecNames">
|
||||
<div class="spec-name-options">
|
||||
<el-checkbox
|
||||
v-for="specName in availableSpecNames"
|
||||
:key="specName.id"
|
||||
:label="specName.id"
|
||||
:disabled="getSpecValues(specName.id).length === 0"
|
||||
class="spec-name-checkbox"
|
||||
>
|
||||
<div class="spec-name-info">
|
||||
<span class="spec-name-text">{{ specName.name }}</span>
|
||||
<span class="spec-value-count">
|
||||
({{ getSpecValues(specName.id).length }}个值)
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="getSpecValues(specName.id).length === 0" class="no-values-tip">
|
||||
该规格名称暂无规格值
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="默认库存">
|
||||
<el-input-number
|
||||
v-model="defaultStock"
|
||||
:min="0"
|
||||
placeholder="生成组合的默认库存数量"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="selectedSpecNames.length > 0" class="preview-info">
|
||||
<el-alert
|
||||
:title="`将生成 ${calculateCombinationCount()} 个规格组合`"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="generateDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirmGenerateCombinations"
|
||||
:disabled="selectedSpecNames.length === 0 || generating"
|
||||
:loading="generating"
|
||||
>
|
||||
生成 {{ calculateCombinationCount() }} 个组合
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, ArrowLeft, Setting, Refresh } from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const combinationFormRef = ref()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const generating = ref(false)
|
||||
const specNamesDialogVisible = ref(false)
|
||||
const combinationDialogVisible = ref(false)
|
||||
const generateDialogVisible = ref(false)
|
||||
|
||||
const productName = ref('')
|
||||
const specNames = ref([])
|
||||
const specValues = ref([])
|
||||
const combinations = ref([])
|
||||
const newSpecName = ref('')
|
||||
const newSpecValues = reactive({})
|
||||
const selectedSpecNames = ref([])
|
||||
const defaultStock = ref(0)
|
||||
|
||||
const combinationForm = reactive({
|
||||
id: null,
|
||||
spec_details: [],
|
||||
price_adjustment: 0,
|
||||
points_adjustment: 0,
|
||||
rongdou_adjustment: 0,
|
||||
stock: 0,
|
||||
is_available: true
|
||||
})
|
||||
|
||||
const combinationRules = {
|
||||
price_adjustment: [
|
||||
{ type: 'number', message: '价格调整必须是数字', trigger: 'blur' }
|
||||
],
|
||||
points_adjustment: [
|
||||
{ type: 'number', min: 0, message: '积分调整不能小于0', trigger: 'blur' }
|
||||
],
|
||||
rongdou_adjustment: [
|
||||
{ type: 'number', min: 0, message: '融豆调整不能小于0', trigger: 'blur' }
|
||||
],
|
||||
stock: [
|
||||
{ required: true, message: '请输入库存数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '库存数量不能小于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性:获取可用的规格名称(有规格值的)
|
||||
const availableSpecNames = computed(() => {
|
||||
return specNames.value.filter(specName => {
|
||||
const values = getSpecValues(specName.id)
|
||||
return values.length > 0
|
||||
})
|
||||
})
|
||||
|
||||
// 获取规格值数量
|
||||
const getSpecValueCount = (specNameId) => {
|
||||
return specValues.value.filter(v => v.spec_name_id === specNameId).length
|
||||
}
|
||||
|
||||
// 获取指定规格名称的规格值
|
||||
const getSpecValues = (specNameId) => {
|
||||
return specValues.value.filter(v => v.spec_name_id === specNameId)
|
||||
}
|
||||
|
||||
// 获取标签类型
|
||||
const getTagType = (index) => {
|
||||
const types = ['primary', 'success', 'warning', 'danger', 'info']
|
||||
return types[index % types.length]
|
||||
}
|
||||
|
||||
// 计算将要生成的组合数量
|
||||
const calculateCombinationCount = () => {
|
||||
if (selectedSpecNames.value.length === 0) return 0
|
||||
|
||||
let count = 1
|
||||
for (const specNameId of selectedSpecNames.value) {
|
||||
const values = getSpecValues(specNameId)
|
||||
count *= values.length
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// 加载商品信息
|
||||
const loadProduct = async () => {
|
||||
try {
|
||||
const { data } = await api.products.getProductById(route.params.id)
|
||||
productName.value = data.data.product.name
|
||||
} catch (error) {
|
||||
ElMessage.error('加载商品信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载规格名称
|
||||
const loadSpecNames = async () => {
|
||||
try {
|
||||
const { data } = await api.specifications.getSpecNames()
|
||||
specNames.value = data.data || []
|
||||
} catch (error) {
|
||||
console.error('加载规格名称失败:', error)
|
||||
specNames.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载规格值
|
||||
const loadSpecValues = async (specNameId = null) => {
|
||||
try {
|
||||
const { data } = await api.specifications.getSpecValues(specNameId)
|
||||
specValues.value = data.data || []
|
||||
} catch (error) {
|
||||
console.error('加载规格值失败:', error)
|
||||
specValues.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载规格组合
|
||||
const loadCombinations = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await api.specifications.getCombinations(route.params.id)
|
||||
combinations.value = data.data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载规格组合失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示规格名称管理对话框
|
||||
const showSpecNamesDialog = () => {
|
||||
specNamesDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示生成规格组合选择对话框
|
||||
const showGenerateCombinationsDialog = () => {
|
||||
// 检查是否有规格名称
|
||||
if (!specNames.value || specNames.value.length === 0) {
|
||||
ElMessage.warning('请先添加规格名称')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有可用的规格名称(有规格值的)
|
||||
if (availableSpecNames.value.length === 0) {
|
||||
ElMessage.warning('没有可用的规格名称,请先为规格名称添加规格值')
|
||||
return
|
||||
}
|
||||
|
||||
// 重置选择状态
|
||||
selectedSpecNames.value = []
|
||||
defaultStock.value = 0
|
||||
generateDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 添加规格名称
|
||||
const addSpecName = async () => {
|
||||
if (!newSpecName.value.trim()) {
|
||||
ElMessage.warning('请输入规格名称')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.specifications.createSpecName({
|
||||
name: newSpecName.value.trim(),
|
||||
display_name: newSpecName.value.trim()
|
||||
})
|
||||
|
||||
ElMessage.success('添加成功')
|
||||
newSpecName.value = ''
|
||||
loadSpecNames()
|
||||
} catch (error) {
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除规格名称
|
||||
const deleteSpecName = async (specName) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除规格名称「${specName.name}」吗?这将同时删除该规格下的所有值和相关组合!`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.specifications.deleteSpecName(specName.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadSpecNames()
|
||||
loadSpecValues()
|
||||
loadCombinations()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加规格值
|
||||
const addSpecValue = async (specNameId) => {
|
||||
const value = newSpecValues[specNameId]
|
||||
if (!value || !value.trim()) {
|
||||
ElMessage.warning('请输入规格值')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.specifications.createSpecValue({
|
||||
spec_name_id: specNameId,
|
||||
value: value.trim(),
|
||||
display_value: value.trim()
|
||||
})
|
||||
|
||||
ElMessage.success('添加成功')
|
||||
newSpecValues[specNameId] = ''
|
||||
loadSpecValues()
|
||||
} catch (error) {
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除规格值
|
||||
const deleteSpecValue = async (specValue) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除规格值「${specValue.value}」吗?这将同时删除相关的规格组合!`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.specifications.deleteSpecValue(specValue.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadSpecValues()
|
||||
loadCombinations()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确认生成规格组合
|
||||
const confirmGenerateCombinations = async () => {
|
||||
try {
|
||||
const combinationCount = calculateCombinationCount()
|
||||
await ElMessageBox.confirm(
|
||||
`确定要生成 ${combinationCount} 个规格组合吗?这将基于选中的规格名称和值生成笛卡尔积组合。`,
|
||||
'确认生成',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
generating.value = true
|
||||
await api.specifications.generateCombinations({
|
||||
product_id: parseInt(route.params.id),
|
||||
spec_name_ids: selectedSpecNames.value,
|
||||
default_stock: defaultStock.value
|
||||
})
|
||||
|
||||
ElMessage.success(`成功生成 ${combinationCount} 个规格组合`)
|
||||
generateDialogVisible.value = false
|
||||
loadCombinations()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('生成失败')
|
||||
}
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑规格组合
|
||||
const editCombination = (combination) => {
|
||||
Object.assign(combinationForm, {
|
||||
id: combination.id,
|
||||
spec_details: combination.spec_details || [],
|
||||
price_adjustment: combination.price_adjustment || 0,
|
||||
points_adjustment: combination.points_adjustment || 0,
|
||||
rongdou_adjustment: combination.rongdou_adjustment || 0,
|
||||
stock: combination.stock || 0,
|
||||
is_available: combination.is_available !== false
|
||||
})
|
||||
combinationDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交规格组合表单
|
||||
const submitCombinationForm = async () => {
|
||||
if (!combinationFormRef.value) return
|
||||
|
||||
try {
|
||||
await combinationFormRef.value.validate()
|
||||
submitting.value = true
|
||||
|
||||
const submitData = {
|
||||
price_adjustment: combinationForm.price_adjustment,
|
||||
points_adjustment: combinationForm.points_adjustment,
|
||||
rongdou_adjustment: combinationForm.rongdou_adjustment,
|
||||
stock: combinationForm.stock,
|
||||
is_available: combinationForm.is_available
|
||||
}
|
||||
|
||||
await api.specifications.updateCombination(combinationForm.id, submitData)
|
||||
ElMessage.success('更新成功')
|
||||
combinationDialogVisible.value = false
|
||||
loadCombinations()
|
||||
} catch (error) {
|
||||
if (error.response?.data?.message) {
|
||||
ElMessage.error(error.response.data.message)
|
||||
} else {
|
||||
ElMessage.error('更新失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除规格组合
|
||||
const deleteCombination = async (combination) => {
|
||||
console.log(combination,'combination');
|
||||
let display_text = combination.spec_details.map(item => `${item.spec_name}: ${item.value}`).join(',')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除规格组合「${display_text}」吗?此操作不可恢复!`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.specifications.deleteCombination(combination.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadCombinations()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProduct()
|
||||
loadSpecNames()
|
||||
loadSpecValues()
|
||||
loadCombinations()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.spec-combinations-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.spec-names-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spec-names-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.spec-name-tag {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.spec-name-count {
|
||||
margin-left: 5px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.spec-tag {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.price-positive {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.price-negative {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.price-zero {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.spec-names-management {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.add-spec-name {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.spec-name-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spec-name-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.spec-name-header h4 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.add-spec-value {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.spec-value-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spec-value-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.combination-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<div class="products-container">
|
||||
<div class="header">
|
||||
<h2>商品管理</h2>
|
||||
<el-button type="primary" @click="$router.push('/products/create')">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="商品名称">
|
||||
<el-input
|
||||
v-model="filters.search"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
@keyup.enter="loadProducts"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="filters.category" placeholder="请选择分类" clearable style="display: inline-block; width: 150px;">
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:label="category"
|
||||
:value="category"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="请选择状态" clearable style="display: inline-block; width: 150px;">
|
||||
<el-option label="上架" value="active" />
|
||||
<el-option label="下架" value="inactive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadProducts">搜索</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table :data="products" v-loading="loading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="商品图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="getImageUrl(row.image)"
|
||||
:preview-src-list="[getImageUrl(row.image)]"
|
||||
fit="cover"
|
||||
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="商品名称" min-width="150" />
|
||||
<el-table-column prop="category" label="分类" width="120" />
|
||||
<el-table-column prop="points" label="积分价格" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="points-text">{{ row.points }} 积分</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rongdou_price" label="融豆价格" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="rongdou-text">{{ row.rongdou_price || 0 }} 融豆</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="shop_name" label="店家" width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="shop-info">
|
||||
<el-avatar v-if="row.shop_avatar" :src="getImageUrl(row.shop_avatar)" :size="24" />
|
||||
<span>{{ row.shop_name || '默认店铺' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock" label="库存" width="80" />
|
||||
<el-table-column prop="sales" label="销量" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ row.status === 'active' ? '上架' : '下架' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="editProduct(row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="info" size="small" @click="viewSpecCombinations(row.id)">
|
||||
规格管理
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="row.status === 'active' ? 'warning' : 'success'"
|
||||
size="small"
|
||||
@click="toggleStatus(row)"
|
||||
>
|
||||
{{ row.status === 'active' ? '下架' : '上架' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteProduct(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadProducts"
|
||||
@current-change="loadProducts"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, ArrowDown, Setting, Grid } from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
import { getImageUrl } from '@/utils/config'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const products = ref([])
|
||||
const categories = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
search: '',
|
||||
category: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 加载商品列表
|
||||
const loadProducts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const {data} = await api.products.getProducts(params)
|
||||
products.value = data.data.products
|
||||
pagination.total = data.data.total
|
||||
} catch (error) {
|
||||
ElMessage.error('加载商品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载商品分类
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const {data} = await api.products.getCategories()
|
||||
categories.value = data.data.categories
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = () => {
|
||||
Object.assign(filters, {
|
||||
search: '',
|
||||
category: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadProducts()
|
||||
}
|
||||
|
||||
// 编辑商品
|
||||
const editProduct = (id) => {
|
||||
router.push(`/products/edit/${id}`)
|
||||
}
|
||||
|
||||
// 切换商品状态
|
||||
const toggleStatus = async (product) => {
|
||||
const action = product.status === 'active' ? '下架' : '上架'
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要${action}商品「${product.name}」吗?`,
|
||||
'确认操作',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const newStatus = product.status === 'active' ? 'inactive' : 'active'
|
||||
await api.products.updateProduct(product.id, { status: newStatus })
|
||||
|
||||
ElMessage.success(`${action}成功`)
|
||||
loadProducts()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(`${action}失败`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除商品
|
||||
const deleteProduct = async (product) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除商品「${product.name}」吗?此操作不可恢复!`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.products.deleteProduct(product.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadProducts()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 查看商品规格组合
|
||||
const viewSpecCombinations = (id) => {
|
||||
router.push(`/products/${id}/spec-combinations`)
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProducts()
|
||||
loadCategories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.products-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filters {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.points-text {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rongdou-text {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shop-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shop-info span {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user