Files
backmanagerweb/src/components/products.vue
ChoChoX 8360cbea77 修复
2026-06-29 14:49:46 +08:00

517 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { Search } from '@element-plus/icons-vue'
import { ref, onMounted, reactive, watch, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getProductsList,
createProducts,
updateProducts,
deleteProductsAPI
} from '../api/products'
import { getSupplierList } from '../api/suppliers'
import { hasPermission } from '../store/user'
const form = reactive({
name: '',
type: '',
quantity: 0,
price: 0,
unit: '件',
specification: '',
supplier: '',
remark: ''
})
const search = ref('')
const select = ref('2')
const filterType = ref('')
const searchResults = ref([])
const showSearchResults = ref(false)
const showAddForm = ref(false)
const isEditMode = ref(false)
const currentProductId = ref(null)
const loading = ref(false)
const showDetailDialog = ref(false)
const currentDetail = ref(null)
const supplierList = ref([])
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 产品类型映射
const productTypeMap = {
'茶饮原料': '茶饮原料',
'包装物料': '包装物料',
'设备器具': '设备器具'
}
onMounted(() => {
fetchData()
fetchSuppliers()
})
const fetchSuppliers = async () => {
try {
const res = await getSupplierList(null, { quiet: true })
supplierList.value = res.data?.list || res.data || []
} catch {}
}
// 监听筛选变化
watch(filterType, () => {
currentPage.value = 1
handleSearch()
})
// 分页后的数据服务端已分页searchResults 即为当前页数据)
const paginatedData = computed(() => searchResults.value)
// 获取产品列表(服务端分页)
const fetchData = async (page) => {
loading.value = true
const params = {
page: page || currentPage.value,
pageSize: pageSize.value
}
const res = await getProductsList(params)
if (res.code === 0 || res.code === 200) {
if (res.data && Array.isArray(res.data.list)) {
searchResults.value = res.data.list
total.value = res.data.total
} else if (Array.isArray(res.data)) {
searchResults.value = res.data
total.value = res.data.length
}
}
showSearchResults.value = true
loading.value = false
}
// 搜索
const handleSearch = async () => {
// 如果在表单页面,先关闭表单
if (showAddForm.value) {
cancelAdd()
}
loading.value = true
const params = {
page: currentPage.value,
pageSize: pageSize.value,
type: filterType.value || undefined
}
if (select.value === '1') {
params.id = Number(search.value)
} else if (select.value === '2') {
params.name = search.value
} else if (select.value === '3') {
params.supplier = search.value
}
const res = await getProductsList(params)
if (res.code === 0 || res.code === 200) {
if (res.data && Array.isArray(res.data.list)) {
searchResults.value = res.data.list
total.value = res.data.total
} else if (Array.isArray(res.data)) {
searchResults.value = res.data
total.value = res.data.length
}
}
showSearchResults.value = true
loading.value = false
}
// 重置搜索
const resetSearch = () => {
search.value = ''
filterType.value = ''
showSearchResults.value = false
showAddForm.value = false
currentPage.value = 1
fetchData()
}
// 清空搜索
const clearSearch = () => {
resetSearch()
}
// 清空表单
const resetForm = () => {
form.name = ''
form.type = ''
form.quantity = 0
form.price = 0
form.unit = '件'
form.specification = ''
form.supplier = ''
form.remark = ''
isEditMode.value = false
currentProductId.value = null
}
// 显示新增表单
const addProduct = () => {
resetForm()
showAddForm.value = true
showSearchResults.value = false
}
// 编辑
const editProduct = (row) => {
isEditMode.value = true
currentProductId.value = row.id
form.name = row.name
form.type = row.type
form.quantity = row.quantity
form.price = row.price
form.unit = row.unit
form.specification = row.specification
form.supplier = row.supplier
form.remark = row.remark
showAddForm.value = true
showSearchResults.value = false
}
// 保存产品
const saveProduct = async () => {
const formData = {
name: form.name,
type: form.type,
quantity: form.quantity,
price: form.price,
unit: form.unit,
specification: form.specification,
supplier: form.supplier,
remark: form.remark
}
try {
if (isEditMode.value) {
await updateProducts(currentProductId.value, formData)
ElMessage.success('更新成功')
} else {
await createProducts(formData)
ElMessage.success('新增成功')
}
cancelAdd()
fetchData()
} catch (error) {
ElMessage.error(error.response?.data?.message || error.message || '操作失败')
}
}
// 删除产品
const deleteProduct = async (id) => {
ElMessageBox.confirm('确定删除该产品吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteProductsAPI(id)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error(error.response?.data?.message || error.message || '删除失败')
}
})
}
// 取消
const cancelAdd = () => {
resetForm()
showAddForm.value = false
showSearchResults.value = true
}
// 双击行查看详情
const handleRowDblClick = (row) => {
currentDetail.value = { ...row }
showDetailDialog.value = true
}
// 关闭详情弹窗
const closeDetail = () => {
showDetailDialog.value = false
currentDetail.value = null
}
// 产品类型标签样式
const getTypeTag = (type) => {
const map = {
'茶饮原料': '',
'包装物料': 'success',
'设备器具': 'warning'
}
return map[type] || 'info'
}
// 页码变化(服务端分页:重新请求后端)
const handleCurrentChange = (val) => {
currentPage.value = val
fetchData(val)
}
// 检查权限
const canCreate = computed(() => hasPermission('product', 'create'))
const canUpdate = computed(() => hasPermission('product', 'update'))
const canDelete = computed(() => hasPermission('product', 'delete'))
const canOperate = computed(() => canUpdate.value || canDelete.value)
</script>
<template>
<div class="products-container">
<!-- 搜索栏区域 -->
<div class="products-search">
<div class="search-row">
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
class="products-search-with-select" @keyup.enter="handleSearch">
<template #prepend>
<el-select v-model="select" placeholder="请选择" style="width: 115px">
<el-option label="产品编号" value="1" />
<el-option label="产品名称" value="2" />
<el-option label="供应商" value="3" />
</el-select>
</template>
<template #append>
<el-button :icon="Search" @click="handleSearch" />
</template>
</el-input>
<div class="search-buttons">
<el-button type="success" @click="addProduct" v-if="canCreate">新增产品</el-button>
<el-button type="default" @click="resetSearch">重置</el-button>
</div>
</div>
<div class="filter-row">
<span class="filter-label">筛选条件</span>
<el-select v-model="filterType" placeholder="产品类型" clearable style="width: 140px; margin-right: 12px;">
<el-option label="茶饮原料" value="茶饮原料" />
<el-option label="包装物料" value="包装物料" />
<el-option label="设备器具" value="设备器具" />
</el-select>
</div>
</div>
<div v-if="showSearchResults" class="products-list">
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="searchResults.length === 0" class="empty-container">
<el-empty description="暂无数据" />
</div>
<div v-else>
<div class="list-header">
<span class="total-count">共找到 {{ total }} 条记录</span>
</div>
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%"
@row-dblclick="handleRowDblClick" row-class-name="clickable-row">
<el-table-column prop="id" label="产品编号" width="90" />
<el-table-column prop="name" label="产品名称" min-width="120" />
<el-table-column prop="type" label="产品类型" width="110">
<template #default="{ row }">
<el-tag :type="getTypeTag(row.type)">
{{ productTypeMap[row.type] || row.type }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="quantity" label="库存" width="80" />
<el-table-column prop="price" label="单价" width="100">
<template #default="{ row }">
¥{{ row.price?.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="70" />
<el-table-column prop="specification" label="规格" min-width="140" show-overflow-tooltip />
<el-table-column prop="supplier" label="供应商" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="150" fixed="right" v-if="canOperate">
<template #default="{ row }">
<el-button link type="primary" @click="editProduct(row)" v-if="canUpdate">编辑</el-button>
<el-button link type="danger" @click="deleteProduct(row.id)" v-if="canDelete">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
prev-text="上一页"
next-text="下一页"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
<!-- 新增/编辑产品表单 -->
<div v-if="showAddForm" class="products-info-label">
<el-divider content-position="left">{{ isEditMode ? '编辑产品信息' : '新增产品' }}</el-divider>
<el-form :model="form" label-width="auto" style="max-width: 600px" label-position="top">
<el-form-item label="产品名称" required>
<el-input v-model="form.name" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="产品类型" required>
<el-select v-model="form.type" placeholder="请选择产品类型" style="width: 100%;">
<el-option label="茶饮原料" value="茶饮原料" />
<el-option label="包装物料" value="包装物料" />
<el-option label="设备器具" value="设备器具" />
</el-select>
</el-form-item>
<el-form-item label="产品库存" required>
<el-input-number v-model="form.quantity" :min="0" :step="10" style="width: 100%;" />
</el-form-item>
<el-form-item label="产品单价" required>
<el-input-number v-model="form.price" :min="0" :step="0.01" :precision="2" style="width: 100%;" />
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="form.unit" placeholder="请输入计量单位" />
</el-form-item>
<el-form-item label="产品规格">
<el-input v-model="form.specification" placeholder="请输入产品规格/型号" />
</el-form-item>
<el-form-item label="供应商">
<el-select v-model="form.supplier" placeholder="请选择供应商" style="width: 100%;" filterable clearable>
<el-option v-for="s in supplierList" :key="s.id" :label="s.name" :value="s.name" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveProduct">保存</el-button>
<el-button type="danger" @click="cancelAdd">取消</el-button>
</el-form-item>
</el-form>
</div>
<!-- 产品详情弹窗 -->
<el-dialog v-model="showDetailDialog" title="产品详情" width="650px" @close="closeDetail">
<div v-if="currentDetail" class="detail-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="产品编号">{{ currentDetail.id }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ currentDetail.name }}</el-descriptions-item>
<el-descriptions-item label="产品类型">
<el-tag :type="getTypeTag(currentDetail.type)">
{{ productTypeMap[currentDetail.type] || currentDetail.type }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="库存数量">{{ currentDetail.quantity }}</el-descriptions-item>
<el-descriptions-item label="产品单价">
<span class="amount-text">¥{{ currentDetail.price?.toFixed(2) }}</span>
</el-descriptions-item>
<el-descriptions-item label="计量单位">{{ currentDetail.unit }}</el-descriptions-item>
<el-descriptions-item label="产品规格" :span="2">{{ currentDetail.specification || '无' }}</el-descriptions-item>
<el-descriptions-item label="供应商" :span="2">{{ currentDetail.supplier || '无' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ currentDetail.remark || '无' }}
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button @click="closeDetail">关闭</el-button>
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editProduct(d) })()" v-if="canUpdate">编辑</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.products-container {
padding: 20px;
}
.products-search {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.search-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.search-buttons {
display: flex;
gap: 10px;
}
.filter-row {
display: flex;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.filter-label {
color: #606266;
font-size: 14px;
margin-right: 10px;
}
.list-header {
margin-bottom: 15px;
}
.total-count {
color: #909399;
font-size: 14px;
}
.loading-container,
.empty-container {
padding: 40px 0;
}
.clickable-row {
cursor: pointer;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.detail-content {
padding: 10px 0;
}
.amount-text {
font-weight: 600;
color: #e6a23c;
font-size: 16px;
}
</style>