From 4f89eb6ba9d832ad616c98c36b74c982f1a31787 Mon Sep 17 00:00:00 2001 From: anhaohao <1036606149@qq.com> Date: Thu, 11 Jan 2024 17:51:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/crm/product/index.ts | 17 ++-- src/router/modules/remaining.ts | 10 +++ src/views/crm/product/ProductDetail.vue | 71 ---------------- src/views/crm/product/ProductForm.vue | 36 ++++---- .../product/detail/ProductDetailsHeader.vue | 56 ++++++++++++ .../crm/product/detail/ProductDetailsInfo.vue | 45 ++++++++++ src/views/crm/product/detail/index.vue | 64 ++++++++++++++ src/views/crm/product/index.vue | 85 +++++++++++-------- 8 files changed, 253 insertions(+), 131 deletions(-) delete mode 100644 src/views/crm/product/ProductDetail.vue create mode 100644 src/views/crm/product/detail/ProductDetailsHeader.vue create mode 100644 src/views/crm/product/detail/ProductDetailsInfo.vue create mode 100644 src/views/crm/product/detail/index.vue diff --git a/src/api/crm/product/index.ts b/src/api/crm/product/index.ts index c6d5dfdb..a89d6e81 100644 --- a/src/api/crm/product/index.ts +++ b/src/api/crm/product/index.ts @@ -14,30 +14,35 @@ export interface ProductVO { // 查询产品列表 export const getProductPage = async (params) => { - return await request.get({ url: `/crm/product/page`, params }) + return await request.get({url: `/crm/product/page`, params}) } // 查询产品详情 export const getProduct = async (id: number) => { - return await request.get({ url: `/crm/product/get?id=` + id }) + return await request.get({url: `/crm/product/get?id=` + id}) } // 新增产品 export const createProduct = async (data: ProductVO) => { - return await request.post({ url: `/crm/product/create`, data }) + return await request.post({url: `/crm/product/create`, data}) } // 修改产品 export const updateProduct = async (data: ProductVO) => { - return await request.put({ url: `/crm/product/update`, data }) + return await request.put({url: `/crm/product/update`, data}) } // 删除产品 export const deleteProduct = async (id: number) => { - return await request.delete({ url: `/crm/product/delete?id=` + id }) + return await request.delete({url: `/crm/product/delete?id=` + id}) } // 导出产品 Excel export const exportProduct = async (params) => { - return await request.download({ url: `/crm/product/export-excel`, params }) + return await request.download({url: `/crm/product/export-excel`, params}) +} + +// 查询产品操作日志 +export const getOperateLogPage = async (params: any) => { + return await request.get({url: '/crm/product/operate-log-page', params}) } diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index a562ea85..639fae36 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -517,6 +517,16 @@ const remainingRouter: AppRouteRecordRaw[] = [ hidden: true }, component: () => import('@/views/crm/contact/detail/index.vue') + }, + { + path: 'product/detail/:id', + name: 'CrmProductDetail', + meta: { + title: '产品详情', + noCache: true, + hidden: true + }, + component: () => import('@/views/crm/product/detail/index.vue') } ] } diff --git a/src/views/crm/product/ProductDetail.vue b/src/views/crm/product/ProductDetail.vue deleted file mode 100644 index f2638df0..00000000 --- a/src/views/crm/product/ProductDetail.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="产品详情"> - <el-descriptions :column="1" border> - <el-descriptions-item label="产品名称"> - {{ detailData.name }} - </el-descriptions-item> - <el-descriptions-item label="创建时间"> - {{ formatDate(detailData.createTime) }} - </el-descriptions-item> - <el-descriptions-item label="状态"> - <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="detailData.status" /> - </el-descriptions-item> - <el-descriptions-item label="产品分类"> - {{ productCategoryList?.find((c) => c.id === detailData.categoryId)?.name }} - </el-descriptions-item> - <el-descriptions-item label="产品编码"> - {{ detailData.no }} - </el-descriptions-item> - <el-descriptions-item label="产品描述"> - {{ detailData.description }} - </el-descriptions-item> - <el-descriptions-item label="负责人"> - {{ detailData.ownerUserId }} - </el-descriptions-item> - <el-descriptions-item label="单位"> - <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="detailData.unit" /> - </el-descriptions-item> - <el-descriptions-item label="价格"> - {{ fenToYuan(detailData.price) }}元 - </el-descriptions-item> - </el-descriptions> - </Dialog> -</template> -<script setup lang="ts"> -// TODO 芋艿:统一改成,独立 tab -import { DICT_TYPE } from '@/utils/dict' -import * as ProductCategoryApi from '@/api/crm/product/productCategory' -import * as ProductApi from '@/api/crm/product' -import { formatDate } from '@/utils/formatTime' -import { fenToYuan } from '@/utils' -import { getSimpleUserList, UserVO } from '@/api/system/user' - -defineOptions({ name: 'CrmProductDetail' }) - -const { t } = useI18n() // 国际化 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const detailLoading = ref(false) // 表单的加载中 -const detailData = ref() // 详情数据 - -/** 打开弹窗 */ -const open = async (data: ProductApi.ProductVO) => { - dialogVisible.value = true - // 设置数据 - detailLoading.value = true - try { - detailData.value = data - } finally { - detailLoading.value = false - } -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -const productCategoryList = ref([]) // 产品分类树 -const userList = ref<UserVO[]>([]) // 系统用户 - -onMounted(async () => { - productCategoryList.value = await ProductCategoryApi.getProductCategoryList({}) - userList.value = await getSimpleUserList() -}) -</script> diff --git a/src/views/crm/product/ProductForm.vue b/src/views/crm/product/ProductForm.vue index 0864c8d8..74dc7662 100644 --- a/src/views/crm/product/ProductForm.vue +++ b/src/views/crm/product/ProductForm.vue @@ -10,7 +10,7 @@ <el-row> <el-col :span="12"> <el-form-item label="产品名称" prop="name"> - <el-input v-model="formData.name" placeholder="请输入产品名称" /> + <el-input v-model="formData.name" placeholder="请输入产品名称"/> </el-form-item> </el-col> <el-col :span="12"> @@ -57,7 +57,7 @@ </el-col> <el-col :span="12"> <el-form-item label="产品编码" prop="no"> - <el-input v-model="formData.no" placeholder="请输入产品编码" /> + <el-input v-model="formData.no" placeholder="请输入产品编码"/> </el-form-item> </el-col> <el-col :span="12"> @@ -74,7 +74,7 @@ </el-col> <el-col :span="12"> <el-form-item label="产品描述" prop="description"> - <el-input v-model="formData.description" placeholder="请输入产品描述" /> + <el-input v-model="formData.description" placeholder="请输入产品描述"/> </el-form-item> </el-col> <el-col :span="12"> @@ -98,17 +98,17 @@ </Dialog> </template> <script setup lang="ts"> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import {DICT_TYPE, getIntDictOptions} from '@/utils/dict' import * as ProductApi from '@/api/crm/product' import * as ProductCategoryApi from '@/api/crm/product/productCategory' -import { defaultProps, handleTree } from '@/utils/tree' -import { getSimpleUserList, UserVO } from '@/api/system/user' -import { useUserStore } from '@/store/modules/user' -import { fenToYuan, yuanToFen } from '@/utils' +import {defaultProps, handleTree} from '@/utils/tree' +import {getSimpleUserList, UserVO} from '@/api/system/user' +import {useUserStore} from '@/store/modules/user' +import {fenToYuan, yuanToFen} from '@/utils' -defineOptions({ name: 'CrmProductForm' }) +defineOptions({name: 'CrmProductForm'}) -const { t } = useI18n() // 国际化 +const {t} = useI18n() // 国际化 const message = useMessage() // 消息弹窗 const dialogVisible = ref(false) // 弹窗的是否展示 @@ -128,12 +128,12 @@ const formData = ref({ ownerUserId: -1 }) const formRules = reactive({ - name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }], - no: [{ required: true, message: '产品编码不能为空', trigger: 'blur' }], - status: [{ required: true, message: '状态不能为空', trigger: 'change' }], - categoryId: [{ required: true, message: '产品分类ID不能为空', trigger: 'blur' }], - ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }], - price: [{ required: true, message: '价格不能为空', trigger: 'blur' }] + name: [{required: true, message: '产品名称不能为空', trigger: 'blur'}], + no: [{required: true, message: '产品编码不能为空', trigger: 'blur'}], + status: [{required: true, message: '状态不能为空', trigger: 'change'}], + categoryId: [{required: true, message: '产品分类ID不能为空', trigger: 'blur'}], + ownerUserId: [{required: true, message: '负责人不能为空', trigger: 'blur'}], + price: [{required: true, message: '价格不能为空', trigger: 'blur'}] }) const formRef = ref() // 表单 Ref @@ -149,7 +149,7 @@ const open = async (type: string, id?: number) => { formLoading.value = true try { formData.value = await ProductApi.getProduct(id) - formData.value.price = fenToYuan(formData.value.price) + formData.value.price = Number(fenToYuan(formData.value.price)) } finally { formLoading.value = false } @@ -157,7 +157,7 @@ const open = async (type: string, id?: number) => { formData.value.ownerUserId = userId } } -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +defineExpose({open}) // 提供 open 方法,用于打开弹窗 /** 提交表单 */ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 diff --git a/src/views/crm/product/detail/ProductDetailsHeader.vue b/src/views/crm/product/detail/ProductDetailsHeader.vue new file mode 100644 index 00000000..c54060be --- /dev/null +++ b/src/views/crm/product/detail/ProductDetailsHeader.vue @@ -0,0 +1,56 @@ +<template> + <div> + <div class="flex items-start justify-between"> + <div> + <el-col> + <el-row> + <span class="text-xl font-bold">{{ product.name }}</span> + </el-row> + </el-col> + </div> + <div> + <!-- 右上:按钮 --> + <el-button @click="openForm('update', product.id)" v-hasPermi="['crm:product:update']"> + 编辑 + </el-button> + </div> + </div> + </div> + <ContentWrap class="mt-10px"> + <el-descriptions :column="5" direction="vertical"> + <el-descriptions-item label="产品类别"> + {{ productCategoryList?.find((c) => c.id === product.categoryId)?.name }} + </el-descriptions-item> + <el-descriptions-item label="产品单位"> + <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="product.unit"/> + </el-descriptions-item> + <el-descriptions-item label="产品价格">{{ fenToYuan(product.price) }}元</el-descriptions-item> + <el-descriptions-item label="产品编码">{{ product.no }}</el-descriptions-item> + </el-descriptions> + </ContentWrap> + <!-- 表单弹窗:添加/修改 --> + <ProductForm ref="formRef" @success="emit('refresh')"/> +</template> +<script setup lang="ts"> +import ProductForm from '@/views/crm/product/ProductForm.vue' +import {DICT_TYPE} from "@/utils/dict"; +import {fenToYuan} from "@/utils"; +import * as ProductApi from "@/api/crm/product"; +import * as ProductCategoryApi from '@/api/crm/product/productCategory' + + +//操作修改 +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} +const {product} = defineProps<{ product: ProductApi.ProductVO }>() +const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调 + +/** 初始化 */ +const productCategoryList = ref([]) // 产品分类树 + +onMounted(async () => { + productCategoryList.value = await ProductCategoryApi.getProductCategoryList({}) +}) +</script> diff --git a/src/views/crm/product/detail/ProductDetailsInfo.vue b/src/views/crm/product/detail/ProductDetailsInfo.vue new file mode 100644 index 00000000..85046b85 --- /dev/null +++ b/src/views/crm/product/detail/ProductDetailsInfo.vue @@ -0,0 +1,45 @@ +<template> + <ContentWrap> + <el-collapse v-model="activeNames"> + <el-collapse-item name="basicInfo"> + <template #title> + <span class="text-base font-bold">基本信息</span> + </template> + <el-descriptions :column="4"> + <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item> + <el-descriptions-item label="产品编码">{{ product.no }}</el-descriptions-item> + <el-descriptions-item label="价格">{{ fenToYuan(product.price) }}元</el-descriptions-item> + <el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item> + <el-descriptions-item label="产品类型"> + {{ productCategoryList?.find((c) => c.id === product.categoryId)?.name }} + </el-descriptions-item> + <el-descriptions-item label="是否上下架"> + <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="product.status"/> + </el-descriptions-item> + <el-descriptions-item label="单位"> + <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="product.unit"/> + </el-descriptions-item> + </el-descriptions> + </el-collapse-item> + </el-collapse> + </ContentWrap> +</template> +<script setup lang="ts"> +import {DICT_TYPE} from '@/utils/dict' +import * as ProductApi from '@/api/crm/product' +import {fenToYuan} from '@/utils' +import * as ProductCategoryApi from '@/api/crm/product/productCategory' + +const {product} = defineProps<{ + product: ProductApi.ProductVO +}>() + +// 展示的折叠面板 +const activeNames = ref(['basicInfo']) + +/** 初始化 */ +const productCategoryList = ref([]) // 产品分类树 +onMounted(async () => { + productCategoryList.value = await ProductCategoryApi.getProductCategoryList({}) +}) +</script> diff --git a/src/views/crm/product/detail/index.vue b/src/views/crm/product/detail/index.vue new file mode 100644 index 00000000..9e84d2e5 --- /dev/null +++ b/src/views/crm/product/detail/index.vue @@ -0,0 +1,64 @@ +<template> + <ProductDetailsHeader :product="product" :loading="loading" @refresh="getProductData(id)"/> + <el-col> + <el-tabs> + <el-tab-pane label="详细资料"> + <ProductDetailsInfo :product="product"/> + </el-tab-pane> + <el-tab-pane label="操作日志"> + <OperateLogV2 :log-list="logList"/> + </el-tab-pane> + </el-tabs> + </el-col> +</template> +<script setup lang="ts"> +import {useTagsViewStore} from '@/store/modules/tagsView' +import {OperateLogV2VO} from '@/api/system/operatelog' +import * as ProductApi from '@/api/crm/product' +import ProductDetailsHeader from '@/views/crm/product/detail/ProductDetailsHeader.vue' +import ProductDetailsInfo from '@/views/crm/product/detail/ProductDetailsInfo.vue' + +defineOptions({name: 'CrmProductDetail'}) + +const route = useRoute() +const id = Number(route.params.id) // 编号 +const loading = ref(true) // 加载中 +const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) // 详情 + +/** 获取详情 */ +const getProductData = async (id: number) => { + loading.value = true + try { + product.value = await ProductApi.getProduct(id) + await getOperateLog(id) + } finally { + loading.value = false + } +} + +/** + * 获取操作日志 + */ +const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表 +const getOperateLog = async (productId: number) => { + if (!productId) { + return + } + const data = await ProductApi.getOperateLogPage({ + bizId: productId + }) + logList.value = data.list +} + +/** 初始化 */ +const {delView} = useTagsViewStore() // 视图操作 +const {currentRoute} = useRouter() // 路由 +onMounted(async () => { + if (!id) { + ElMessage.warning('参数错误,产品不能为空!') + delView(unref(currentRoute)) + return + } + await getProductData(id) +}) +</script> diff --git a/src/views/crm/product/index.vue b/src/views/crm/product/index.vue index 3f01455c..aa517c17 100644 --- a/src/views/crm/product/index.vue +++ b/src/views/crm/product/index.vue @@ -28,10 +28,17 @@ </el-select> </el-form-item> <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button @click="handleQuery"> + <Icon icon="ep:search" class="mr-5px"/> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon icon="ep:refresh" class="mr-5px"/> + 重置 + </el-button> <el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:product:create']"> - <Icon icon="ep:plus" class="mr-5px" /> 新增 + <Icon icon="ep:plus" class="mr-5px"/> + 新增 </el-button> <el-button type="success" @@ -40,7 +47,8 @@ :loading="exportLoading" v-hasPermi="['crm:product:export']" > - <Icon icon="ep:download" class="mr-5px" /> 导出 + <Icon icon="ep:download" class="mr-5px"/> + 导出 </el-button> </el-form-item> </el-form> @@ -49,27 +57,33 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="产品名称" align="center" prop="name" /> - <el-table-column label="产品类型" align="center" prop="categoryName" /> - <el-table-column label="产品单位" align="center" prop="unit"> + <el-table-column label="产品名称" align="center" prop="name" width="160"> <template #default="scope"> - <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="scope.row.unit" /> + <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)"> + {{ scope.row.name }} + </el-link> </template> </el-table-column> - <el-table-column label="产品编码" align="center" prop="no" /> + <el-table-column label="产品类型" align="center" prop="categoryName" width="160"/> + <el-table-column label="产品单位" align="center" prop="unit"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="scope.row.unit"/> + </template> + </el-table-column> + <el-table-column label="产品编码" align="center" prop="no"/> <el-table-column label="价格(元)" align="center" prop="price" :formatter="fenToYuanFormat" /> - <el-table-column label="产品描述" align="center" prop="description" /> + <el-table-column label="产品描述" align="center" prop="description"/> <el-table-column label="是否上下架" align="center" prop="status"> <template #default="scope"> - <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="scope.row.status" /> + <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="scope.row.status"/> </template> </el-table-column> - <el-table-column label="负责人" align="center" prop="ownerUserName" /> + <el-table-column label="负责人" align="center" prop="ownerUserName"/> <el-table-column label="更新时间" align="center" @@ -77,7 +91,7 @@ :formatter="dateFormatter" width="180px" /> - <el-table-column label="创建" align="center" prop="creatorName" /> + <el-table-column label="创建" align="center" prop="creatorName"/> <el-table-column label="创建时间" align="center" @@ -85,16 +99,8 @@ :formatter="dateFormatter" width="180px" /> - <el-table-column label="操作" align="center" width="160"> + <el-table-column label="操作" align="center" fixed="right" width="160"> <template #default="scope"> - <el-button - v-hasPermi="['crm:product:query']" - link - type="primary" - @click="openDetail(scope.row)" - > - 详情 - </el-button> <el-button link type="primary" @@ -124,24 +130,21 @@ </ContentWrap> <!-- 表单弹窗:添加/修改 --> - <ProductForm ref="formRef" @success="getList" /> - <!-- 表单弹窗:详情 --> - <ProductDetail ref="detailRef" @success="getList" /> + <ProductForm ref="formRef" @success="getList"/> </template> <script setup lang="ts"> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' +import {DICT_TYPE, getIntDictOptions} from '@/utils/dict' +import {dateFormatter} from '@/utils/formatTime' import download from '@/utils/download' import * as ProductApi from '@/api/crm/product' import ProductForm from './ProductForm.vue' -import ProductDetail from './ProductDetail.vue' -import { fenToYuanFormat } from '@/utils/formatter' +import {fenToYuanFormat} from '@/utils/formatter' -defineOptions({ name: 'CrmProduct' }) +defineOptions({name: 'CrmProduct'}) const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 +const {t} = useI18n() // 国际化 const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 @@ -184,10 +187,11 @@ const formRef = ref() const openForm = (type: string, id?: number) => { formRef.value.open(type, id) } -/** 详情操作 */ -const detailRef = ref() -const openDetail = (data: ProductApi.ProductVO) => { - detailRef.value.open(data) + +/** 打开详情 */ +const {currentRoute, push} = useRouter() +const openDetail = (id: number) => { + push({name: 'CrmProductDetail', params: {id}}) } /** 删除按钮操作 */ @@ -200,7 +204,8 @@ const handleDelete = async (id: number) => { message.success(t('common.delSuccess')) // 刷新列表 await getList() - } catch {} + } catch { + } } /** 导出按钮操作 */ @@ -218,6 +223,14 @@ const handleExport = async () => { } } +/** 监听路由变化更新列表 */ +watch( + () => currentRoute.value, + () => { + getList() + } +) + /** 初始化 **/ onMounted(async () => { await getList()