剥离商城相关功能

This commit is contained in:
dzl
2025-09-23 09:00:22 +08:00
parent e68b805004
commit 08695ffddb
7 changed files with 0 additions and 3171 deletions

View File

@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>