This commit is contained in:
2026-01-09 15:58:17 +08:00
parent 54acbd0f31
commit f131a23a52
5 changed files with 423 additions and 20 deletions

1
components.d.ts vendored
View File

@@ -25,6 +25,7 @@ declare module 'vue' {
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']

View File

@@ -289,6 +289,14 @@ export const productAdminApi = {
updateCategory: (id, data) => request.put(`/admin/product-categories/${id}`, data), updateCategory: (id, data) => request.put(`/admin/product-categories/${id}`, data),
deleteCategory: (id) => request.delete(`/admin/product-categories/${id}`), deleteCategory: (id) => request.delete(`/admin/product-categories/${id}`),
// 小类管理
getSubCategories: (params) => request.get('/admin/sub-categories', { params }),
getSubCategoryDetail: (id) => request.get(`/admin/sub-categories/${id}`),
createSubCategory: (data) => request.post('/admin/sub-categories', data),
updateSubCategory: (id, data) => request.put(`/admin/sub-categories/${id}`, data),
deleteSubCategory: (id) => request.delete(`/admin/sub-categories/${id}`),
getSubCategoriesByCategory: (categoryId) => request.get(`/admin/product-categories/${categoryId}/sub-categories`),
// 订阅管理 // 订阅管理
getSubscriptions: (params) => request.get('/admin/subscriptions', { params }), getSubscriptions: (params) => request.get('/admin/subscriptions', { params }),
getSubscriptionStats: () => request.get('/admin/subscriptions/stats'), getSubscriptionStats: () => request.get('/admin/subscriptions/stats'),

View File

@@ -31,19 +31,16 @@
placeholder="请输入产品名称" placeholder="请输入产品名称"
/> />
</el-form-item> </el-form-item>
<el-form-item label="产品分类" prop="category_id"> <el-form-item label="产品分类" prop="category_cascader">
<el-select <el-cascader
v-model="form.category_id" v-model="form.category_cascader"
:options="categoryOptions"
:props="cascaderProps"
placeholder="选择产品分类" placeholder="选择产品分类"
class="w-full" class="w-full"
> clearable
<el-option @change="handleCategoryChange"
v-for="category in categories" />
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="产品类型" prop="is_package"> <el-form-item label="产品类型" prop="is_package">
@@ -380,6 +377,39 @@ const props = defineProps({
} }
}) })
// 子分类数据
const subCategories = ref([])
// 级联选择器配置
const cascaderProps = {
value: 'id',
label: 'name',
children: 'children',
emitPath: false,
checkStrictly: false
}
// 分类选项(包含子分类)
const categoryOptions = computed(() => {
return props.categories.map(category => {
// 查找该分类下的子分类
const children = subCategories.value
.filter(sub => sub.category_id === category.id)
.map(sub => ({
id: sub.id,
name: sub.name,
code: sub.code
}))
return {
id: category.id,
name: category.name,
code: category.code,
children: children.length > 0 ? children : undefined
}
})
})
const emit = defineEmits(['update:modelValue', 'success']) const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false) const visible = ref(false)
@@ -408,6 +438,8 @@ const form = reactive({
description: '', description: '',
content: '', content: '',
category_id: '', category_id: '',
sub_category_id: '',
category_cascader: [], // 用于级联选择器
price: 0, price: 0,
cost_price: 0, cost_price: 0,
remark: '', remark: '',
@@ -431,7 +463,7 @@ const rules = {
{ required: true, message: '请输入产品名称', trigger: 'blur' }, { required: true, message: '请输入产品名称', trigger: 'blur' },
{ min: 2, max: 50, message: '产品名称长度在 2 到 50 个字符', trigger: 'blur' } { min: 2, max: 50, message: '产品名称长度在 2 到 50 个字符', trigger: 'blur' }
], ],
category_id: [ category_cascader: [
{ required: true, message: '请选择产品分类', trigger: 'change' } { required: true, message: '请选择产品分类', trigger: 'change' }
], ],
price: [ price: [
@@ -492,6 +524,9 @@ const initForm = async () => {
// 重置所有状态 // 重置所有状态
resetFormState() resetFormState()
// 加载子分类
await loadSubCategories()
if (props.product) { if (props.product) {
// 编辑模式 // 编辑模式
await handleEditMode() await handleEditMode()
@@ -535,6 +570,15 @@ const handleEditMode = async () => {
form.ui_component_price = props.product.ui_component_price form.ui_component_price = props.product.ui_component_price
} }
// 设置级联选择器的值
if (props.product.sub_category_id) {
// 有二级分类级联选择器设置为二级分类的ID
form.category_cascader = props.product.sub_category_id
} else if (props.product.category_id) {
// 只有一级分类级联选择器设置为一级分类的ID
form.category_cascader = props.product.category_id
}
// 如果是组合包,处理子产品数据 // 如果是组合包,处理子产品数据
if (props.product.is_package) { if (props.product.is_package) {
await handlePackageData() await handlePackageData()
@@ -676,6 +720,38 @@ const loadAvailableProducts = async () => {
} }
} }
// 加载子分类
const loadSubCategories = async () => {
try {
const response = await productAdminApi.getSubCategories({ page: 1, page_size: 100 })
subCategories.value = response.data?.items || []
} catch (error) {
console.error('加载子分类失败:', error)
}
}
// 处理分类选择变化
const handleCategoryChange = (value) => {
// value可能是单个值选择叶子节点或数组选择非叶子节点
// 查找选择的分类是否是二级分类
const isSubCategory = subCategories.value.some(sub => sub.id === value)
if (isSubCategory) {
// 选择的是二级分类需要同时设置category_id和sub_category_id
const subCategory = subCategories.value.find(sub => sub.id === value)
form.category_id = subCategory.category_id
form.sub_category_id = value
} else if (value) {
// 选择的是一级分类只设置category_id
form.category_id = value
form.sub_category_id = undefined
} else {
// 清空选择
form.category_id = ''
form.sub_category_id = undefined
}
}
// 处理组合包搜索 // 处理组合包搜索
const handlePackageSearch = () => { const handlePackageSearch = () => {
if (packageSearchTimer) { if (packageSearchTimer) {
@@ -826,10 +902,18 @@ const handleSubmit = async () => {
const submitData = { ...form } const submitData = { ...form }
// 移除级联选择器字段
delete submitData.category_cascader
// 确保布尔值正确传递 // 确保布尔值正确传递
submitData.is_enabled = Boolean(form.is_enabled) submitData.is_enabled = Boolean(form.is_enabled)
submitData.is_visible = Boolean(form.is_visible) submitData.is_visible = Boolean(form.is_visible)
// 如果sub_category_id为undefined移除该字段不传递
if (submitData.sub_category_id === undefined) {
delete submitData.sub_category_id
}
if (isEdit.value) { if (isEdit.value) {
// 编辑模式 // 编辑模式
await productAdminApi.updateProduct(props.product.id, submitData) await productAdminApi.updateProduct(props.product.id, submitData)

View File

@@ -40,9 +40,16 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="180" fixed="right"> <el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex gap-2"> <div class="flex gap-2">
<el-button
type="info"
size="small"
@click="handleManageSubCategories(row)"
>
小类管理
</el-button>
<el-button <el-button
type="primary" type="primary"
size="small" size="small"
@@ -128,6 +135,137 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 小类管理弹窗 -->
<el-dialog
v-model="subCategoryDialogVisible"
:title="`${selectedCategory?.name || '分类'} - 小类管理`"
width="900px"
@close="handleCloseSubCategoryDialog"
>
<div class="mb-4">
<el-button type="primary" @click="handleCreateSubCategory">
新增小类
</el-button>
</div>
<el-table
v-loading="subCategoryLoading"
:data="subCategories"
stripe
class="w-full"
>
<el-table-column prop="code" label="小类编号" width="120" />
<el-table-column prop="name" label="小类名称" min-width="150" />
<el-table-column prop="description" label="小类描述" min-width="200" />
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="is_enabled" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.is_enabled ? 'success' : 'danger'" size="small">
{{ row.is_enabled ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_visible" label="展示" width="80">
<template #default="{ row }">
<el-tag :type="row.is_visible ? 'success' : 'warning'" size="small">
{{ row.is_visible ? '显示' : '隐藏' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<div class="flex gap-2">
<el-button
type="primary"
size="small"
@click="handleEditSubCategory(row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDeleteSubCategory(row)"
>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="flex justify-end">
<el-button @click="subCategoryDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 小类表单弹窗 -->
<el-dialog
v-model="subCategoryFormDialogVisible"
:title="isSubCategoryEdit ? '编辑小类' : '新增小类'"
width="600px"
>
<el-form
ref="subCategoryFormRef"
:model="subCategoryForm"
:rules="subCategoryRules"
label-width="100px"
>
<el-form-item label="小类编号" prop="code">
<el-input
v-model="subCategoryForm.code"
placeholder="请输入小类编号"
:disabled="isSubCategoryEdit"
/>
</el-form-item>
<el-form-item label="小类名称" prop="name">
<el-input
v-model="subCategoryForm.name"
placeholder="请输入小类名称"
/>
</el-form-item>
<el-form-item label="小类描述" prop="description">
<el-input
v-model="subCategoryForm.description"
type="textarea"
:rows="3"
placeholder="请输入小类描述"
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="subCategoryForm.sort"
:min="0"
:max="999"
placeholder="排序值"
class="w-full"
/>
</el-form-item>
<el-form-item label="是否启用" prop="is_enabled">
<el-switch v-model="subCategoryForm.is_enabled" />
</el-form-item>
<el-form-item label="是否展示" prop="is_visible">
<el-switch v-model="subCategoryForm.is_visible" />
</el-form-item>
</el-form>
<template #footer>
<div class="flex justify-end gap-3">
<el-button @click="handleSubCategoryCancel">取消</el-button>
<el-button type="primary" @click="handleSubCategorySubmit" :loading="subCategorySubmitting">
{{ isSubCategoryEdit ? '保存修改' : '创建小类' }}
</el-button>
</div>
</template>
</el-dialog>
</template> </template>
</ListPageLayout> </ListPageLayout>
</template> </template>
@@ -145,6 +283,16 @@ const submitting = ref(false)
const editingCategory = ref(null) const editingCategory = ref(null)
const formRef = ref(null) const formRef = ref(null)
// 小类管理相关数据
const subCategoryDialogVisible = ref(false)
const subCategoryFormDialogVisible = ref(false)
const subCategoryLoading = ref(false)
const subCategories = ref([])
const selectedCategory = ref(null)
const editingSubCategory = ref(null)
const subCategoryFormRef = ref(null)
const subCategorySubmitting = ref(false)
// 表单初始值 // 表单初始值
const initialFormData = { const initialFormData = {
code: '', code: '',
@@ -173,8 +321,38 @@ const rules = {
] ]
} }
// 小类表单初始值
const initialSubCategoryFormData = {
category_id: '',
code: '',
name: '',
description: '',
sort: 0,
is_enabled: true,
is_visible: true
}
// 小类表单数据
const subCategoryForm = reactive({ ...initialSubCategoryFormData })
// 小类表单验证规则
const subCategoryRules = {
code: [
{ required: true, message: '请输入小类编号', trigger: 'blur' },
{ min: 2, max: 20, message: '小类编号长度在 2 到 20 个字符', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入小类名称', trigger: 'blur' },
{ min: 2, max: 50, message: '小类名称长度在 2 到 50 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入小类描述', trigger: 'blur' }
]
}
// 计算属性 // 计算属性
const isEdit = computed(() => !!editingCategory.value) const isEdit = computed(() => !!editingCategory.value)
const isSubCategoryEdit = computed(() => !!editingSubCategory.value)
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
@@ -299,6 +477,138 @@ const handleCancel = () => {
editingCategory.value = null editingCategory.value = null
}, 300) }, 300)
} }
// 小类管理相关方法
// 管理小类
const handleManageSubCategories = async (category) => {
selectedCategory.value = category
subCategoryDialogVisible.value = true
await loadSubCategories(category.id)
}
// 加载小类列表
const loadSubCategories = async (categoryId) => {
subCategoryLoading.value = true
try {
const response = await productAdminApi.getSubCategories({ category_id: categoryId, page: 1, page_size: 100 })
subCategories.value = response.data?.items || []
} catch (error) {
console.error('加载小类失败:', error)
ElMessage.error('加载小类失败')
} finally {
subCategoryLoading.value = false
}
}
// 新增小类
const handleCreateSubCategory = () => {
if (!selectedCategory.value) {
ElMessage.warning('请先选择分类')
return
}
editingSubCategory.value = null
resetSubCategoryForm()
subCategoryForm.category_id = selectedCategory.value.id
subCategoryFormDialogVisible.value = true
}
// 编辑小类
const handleEditSubCategory = (subCategory) => {
editingSubCategory.value = { ...subCategory }
Object.keys(subCategoryForm).forEach(key => {
if (subCategory[key] !== undefined) {
subCategoryForm[key] = subCategory[key]
}
})
subCategoryFormDialogVisible.value = true
}
// 删除小类
const handleDeleteSubCategory = async (subCategory) => {
try {
await ElMessageBox.confirm(
`确定要删除小类"${subCategory.name}"吗?此操作不可撤销。`,
'确认删除',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
)
await productAdminApi.deleteSubCategory(subCategory.id)
ElMessage.success('小类删除成功')
await loadSubCategories(selectedCategory.value.id)
} catch (error) {
if (error !== 'cancel') {
console.error('删除小类失败:', error)
ElMessage.error('删除小类失败')
}
}
}
// 提交小类表单
const handleSubCategorySubmit = async () => {
if (!subCategoryFormRef.value) return
try {
await subCategoryFormRef.value.validate()
subCategorySubmitting.value = true
const submitData = { ...subCategoryForm }
if (isSubCategoryEdit.value) {
await productAdminApi.updateSubCategory(editingSubCategory.value.id, submitData)
ElMessage.success('小类更新成功')
} else {
await productAdminApi.createSubCategory(submitData)
ElMessage.success('小类创建成功')
}
subCategoryFormDialogVisible.value = false
await loadSubCategories(selectedCategory.value.id)
} catch (error) {
if (error !== false) { // 不是表单验证错误
console.error('提交小类失败:', error)
ElMessage.error(isSubCategoryEdit.value ? '更新小类失败' : '创建小类失败')
}
} finally {
subCategorySubmitting.value = false
}
}
// 重置小类表单
const resetSubCategoryForm = () => {
subCategoryForm.category_id = ''
subCategoryForm.code = ''
subCategoryForm.name = ''
subCategoryForm.description = ''
subCategoryForm.sort = 0
subCategoryForm.is_enabled = true
subCategoryForm.is_visible = true
// 清除表单验证状态
nextTick(() => {
if (subCategoryFormRef.value) {
subCategoryFormRef.value.clearValidate()
}
})
}
// 取消小类操作
const handleSubCategoryCancel = () => {
subCategoryFormDialogVisible.value = false
setTimeout(() => {
resetSubCategoryForm()
editingSubCategory.value = null
}, 300)
}
// 关闭小类管理弹窗
const handleCloseSubCategoryDialog = () => {
selectedCategory.value = null
subCategories.value = []
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -182,7 +182,7 @@
</label> </label>
<!-- 图片上传字段photo_data --> <!-- 图片上传字段photo_data -->
<div v-if="field.name === 'photo_data' && field.type === 'textarea'" class="space-y-2"> <div v-if="field.name === 'photo_data' || field.name === 'vlphoto_data' && field.type === 'textarea'" class="space-y-2">
<div class="flex gap-2 mb-2"> <div class="flex gap-2 mb-2">
<el-upload <el-upload
:auto-upload="false" :auto-upload="false"