Просмотр исходного кода

!361 新增:产品详情页面
Merge pull request !361 from 安浩浩/dev

芋道源码 1 год назад
Родитель
Сommit
2be14817a0

+ 11 - 6
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})
 }

+ 10 - 0
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')
       }
     ]
   }

+ 0 - 71
src/views/crm/product/ProductDetail.vue

@@ -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>

+ 18 - 18
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 事件,用于操作成功后的回调

+ 56 - 0
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>

+ 45 - 0
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>

+ 64 - 0
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>

+ 48 - 35
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="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" />
+          <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="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()