Files
todo-frontend/src/pages/category/index.vue
Agent 2a36fab5bb
All checks were successful
continuous-integration/drone/push Build is passing
feat: 种类属性配置和商品属性录入
2026-03-29 14:48:34 +00:00

468 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- 顶部标题 -->
<view class="header">
<text class="title">种类管理</text>
</view>
<!-- 分类列表 -->
<scroll-view scroll-y class="category-list">
<view v-for="cat in categories" :key="cat.categoryId" class="category-item">
<view class="category-info">
<text class="category-name">{{ cat.name }}</text>
<text class="category-desc">{{ cat.description || '暂无描述' }}</text>
</view>
<view class="category-actions">
<text class="action-btn attr" @click="editAttributes(cat)">属性配置</text>
<text class="action-btn edit" @click="editCategory(cat)">编辑</text>
<text class="action-btn delete" @click="deleteCategory(cat)">删除</text>
</view>
</view>
<view v-if="categories.length === 0" class="empty">
<Icon name="product" :size="80" color="#ccc" />
<text class="empty-text">暂无分类</text>
</view>
</scroll-view>
<!-- 添加按钮 -->
<view class="add-btn" @click="showAddDialog">
<Icon name="add" :size="40" color="#fff" />
</view>
<!-- 添加/编辑弹窗 -->
<view class="dialog-mask" v-if="showDialog" @click="closeDialog">
<view class="dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">{{ isEdit ? '编辑分类' : '新增分类' }}</text>
</view>
<view class="dialog-body">
<view class="form-item">
<text class="label">名称</text>
<input class="input" v-model="form.name" placeholder="请输入分类名称" />
</view>
<view class="form-item">
<text class="label">描述</text>
<input class="input" v-model="form.description" placeholder="请输入分类描述" />
</view>
</view>
<view class="dialog-footer">
<text class="btn-cancel" @click="closeDialog">取消</text>
<text class="btn-confirm" @click="saveCategory">确定</text>
</view>
</view>
</view>
<!-- 属性配置弹窗 -->
<view class="dialog-mask" v-if="showAttrDialog" @click="closeAttrDialog">
<view class="dialog dialog-wide" @click.stop>
<view class="dialog-header">
<text class="dialog-title">{{ currentCategory.name }} - 属性配置</text>
</view>
<view class="dialog-body">
<view v-for="(attr, index) in attributes" :key="index" class="attr-item">
<input class="input attr-name" v-model="attr.name" placeholder="属性名称" />
<select class="input attr-type" v-model="attr.attrType">
<option value="number">数字</option>
<option value="text">文本</option>
</select>
<input class="input attr-unit" v-model="attr.unit" placeholder="单位" />
<text class="attr-delete" @click="removeAttr(index)">×</text>
</view>
<view class="add-attr-btn" @click="addAttr">
<text>+ 添加属性</text>
</view>
</view>
<view class="dialog-footer">
<text class="btn-cancel" @click="closeAttrDialog">取消</text>
<text class="btn-confirm" @click="saveAttributes">保存</text>
</view>
</view>
</view>
</view>
</template>
<script>
import productApi from '@/api/product'
export default {
data() {
return {
categories: [],
showDialog: false,
isEdit: false,
form: {
categoryId: '',
name: '',
description: ''
},
// 属性配置相关
showAttrDialog: false,
currentCategory: {},
attributes: []
}
},
onLoad() {
this.loadCategories()
},
methods: {
async loadCategories() {
try {
const categories = await productApi.getCategories()
this.categories = categories || []
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
showAddDialog() {
this.isEdit = false
this.form = { categoryId: '', name: '', description: '' }
this.showDialog = true
},
editCategory(cat) {
this.isEdit = true
this.form = { categoryId: cat.categoryId, name: cat.name, description: cat.description || '' }
this.showDialog = true
},
closeDialog() {
this.showDialog = false
},
async saveCategory() {
if (!this.form.name) {
uni.showToast({ title: '请输入名称', icon: 'none' })
return
}
try {
if (this.isEdit) {
await productApi.updateCategory(this.form.categoryId, {
name: this.form.name,
description: this.form.description
})
uni.showToast({ title: '修改成功', icon: 'success' })
} else {
await productApi.createCategory({
name: this.form.name,
description: this.form.description
})
uni.showToast({ title: '添加成功', icon: 'success' })
}
this.closeDialog()
this.loadCategories()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
},
deleteCategory(cat) {
// 先检查该分类下是否有商品
uni.showLoading({ title: '检查中...' })
productApi.getProducts({ categoryId: cat.categoryId, page: 1, pageSize: 1 }).then(res => {
uni.hideLoading()
if (res.records && res.records.length > 0) {
uni.showToast({ title: '该分类下有商品,无法删除', icon: 'none' })
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除 "${cat.name}" 吗?`,
success: async (res) => {
if (res.confirm) {
try {
await productApi.deleteCategory(cat.categoryId)
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadCategories()
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '检查失败', icon: 'none' })
})
},
// ============ 属性配置 ============
async editAttributes(cat) {
this.currentCategory = cat
this.showAttrDialog = true
try {
const attrs = await productApi.getCategoryAttributes(cat.categoryId)
this.attributes = (attrs || []).map(a => ({
name: a.name,
attrType: a.attrType || 'number',
unit: a.unit || '',
attrId: a.attrId
}))
if (this.attributes.length === 0) {
this.attributes.push({ name: '', attrType: 'number', unit: '', attrId: '' })
}
} catch (e) {
this.attributes = [{ name: '', attrType: 'number', unit: '', attrId: '' }]
}
},
addAttr() {
this.attributes.push({ name: '', attrType: 'number', unit: '', attrId: '' })
},
removeAttr(index) {
this.attributes.splice(index, 1)
},
closeAttrDialog() {
this.showAttrDialog = false
},
async saveAttributes() {
const validAttrs = this.attributes.filter(a => a.name.trim())
if (validAttrs.length === 0) {
uni.showToast({ title: '请至少添加一个属性', icon: 'none' })
return
}
try {
await productApi.saveCategoryAttributes(this.currentCategory.categoryId, validAttrs)
uni.showToast({ title: '保存成功', icon: 'success' })
this.closeAttrDialog()
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
}
}
</script>
<style>
.page {
min-height: 100vh;
background: #f8f9fa;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.category-list {
padding: 20rpx;
height: calc(100vh - 140rpx);
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.category-info {
flex: 1;
}
.category-name {
display: block;
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.category-desc {
font-size: 24rpx;
color: #999;
}
.category-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
font-size: 26rpx;
padding: 10rpx 20rpx;
border-radius: 8rpx;
}
.action-btn.edit {
background: #e6f7ff;
color: #1890ff;
}
.action-btn.attr {
background: #f6ffed;
color: #52c41a;
}
.action-btn.delete {
background: #fff1f0;
color: #ff4d4f;
}
.empty {
padding: 100rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
.add-btn {
position: fixed;
right: 40rpx;
bottom: 40rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 30rpx rgba(102, 126, 234, 0.4);
}
/* 弹窗 */
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.dialog {
width: 80%;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.dialog-header {
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.dialog-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.dialog-body {
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-item .label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
}
.form-item .input {
width: 100%;
height: 80rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.dialog-footer {
display: flex;
border-top: 1rpx solid #f5f5f5;
}
.btn-cancel, .btn-confirm {
flex: 1;
text-align: center;
padding: 30rpx;
font-size: 30rpx;
}
.btn-cancel {
color: #666;
border-right: 1rpx solid #f5f5f5;
}
.btn-confirm {
color: #667eea;
font-weight: bold;
}
/* 属性配置弹窗 */
.dialog-wide {
width: 90%;
max-height: 80vh;
}
.dialog-body {
max-height: 60vh;
overflow-y: auto;
}
.attr-item {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
}
.attr-name {
flex: 2;
}
.attr-type {
flex: 1;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 10rpx;
height: 70rpx;
font-size: 24rpx;
}
.attr-unit {
flex: 1;
}
.attr-delete {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 36rpx;
color: #ff4d4f;
background: #fff1f0;
border-radius: 8rpx;
}
.add-attr-btn {
padding: 20rpx;
text-align: center;
color: #667eea;
font-size: 28rpx;
border: 2rpx dashed #667eea;
border-radius: 12rpx;
margin-top: 20rpx;
}
</style>