diff --git a/src/api/crm/product/index.ts b/src/api/crm/product/index.ts index c6d5dfdb..2d88cb09 100644 --- a/src/api/crm/product/index.ts +++ b/src/api/crm/product/index.ts @@ -41,3 +41,8 @@ export const deleteProduct = async (id: number) => { export const exportProduct = async (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..da0fd34c 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -504,7 +504,8 @@ const remainingRouter: AppRouteRecordRaw[] = [ meta: { title: '客户详情', noCache: true, - hidden: true + hidden: true, + activeMenu: '/crm/customer' }, component: () => import('@/views/crm/customer/detail/index.vue') }, @@ -514,9 +515,21 @@ const remainingRouter: AppRouteRecordRaw[] = [ meta: { title: '联系人详情', noCache: true, - hidden: true + hidden: true, + activeMenu: '/crm/contact' }, component: () => import('@/views/crm/contact/detail/index.vue') + }, + { + path: 'product/detail/:id', + name: 'CrmProductDetail', + meta: { + title: '产品详情', + noCache: true, + hidden: true, + activeMenu: '/crm/product' + }, + 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..e12af4c8 100644 --- a/src/views/crm/product/ProductForm.vue +++ b/src/views/crm/product/ProductForm.vue @@ -62,13 +62,13 @@ </el-col> <el-col :span="12"> <el-form-item label="价格" prop="price"> - <el-input - type="number" + <el-input-number v-model="formData.price" placeholder="请输入价格" :min="0" :precision="2" :step="0.1" + class="w-full!" /> </el-form-item> </el-col> @@ -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 } diff --git a/src/views/crm/product/detail/ProductDetailsHeader.vue b/src/views/crm/product/detail/ProductDetailsHeader.vue new file mode 100644 index 00000000..356f1dbc --- /dev/null +++ b/src/views/crm/product/detail/ProductDetailsHeader.vue @@ -0,0 +1,55 @@ +<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..847e9445 --- /dev/null +++ b/src/views/crm/product/detail/index.vue @@ -0,0 +1,62 @@ +<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..5100d7f4 100644 --- a/src/views/crm/product/index.vue +++ b/src/views/crm/product/index.vue @@ -28,8 +28,8 @@ </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" /> 新增 </el-button> @@ -40,7 +40,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,8 +50,14 @@ <!-- 列表 --> <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="name" width="160"> + <template #default="scope"> + <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="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" /> @@ -62,14 +69,15 @@ align="center" prop="price" :formatter="fenToYuanFormat" + width="100" /> - <el-table-column label="产品描述" align="center" prop="description" /> - <el-table-column label="是否上下架" align="center" prop="status"> + <el-table-column label="产品描述" align="center" prop="description" width="150" /> + <el-table-column label="上架状态" align="center" prop="status" width="120"> <template #default="scope"> <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" width="120" /> <el-table-column label="更新时间" align="center" @@ -77,7 +85,7 @@ :formatter="dateFormatter" width="180px" /> - <el-table-column label="创建" align="center" prop="creatorName" /> + <el-table-column label="创建人" align="center" prop="creatorName" width="120" /> <el-table-column label="创建时间" align="center" @@ -85,16 +93,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" @@ -125,8 +125,6 @@ <!-- 表单弹窗:添加/修改 --> <ProductForm ref="formRef" @success="getList" /> - <!-- 表单弹窗:详情 --> - <ProductDetail ref="detailRef" @success="getList" /> </template> <script setup lang="ts"> @@ -135,7 +133,6 @@ 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' defineOptions({ name: 'CrmProduct' }) @@ -184,10 +181,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 } }) } /** 删除按钮操作 */ @@ -218,8 +216,13 @@ const handleExport = async () => { } } +/** 激活时 */ +onActivated(() => { + getList() +}) + /** 初始化 **/ -onMounted(async () => { - await getList() +onMounted(() => { + getList() }) </script>