Explorar el Código

feat: 产品管理

IlhamTahir hace 11 meses
padre
commit
57dcbe2d37

+ 1 - 0
components.d.ts

@@ -39,6 +39,7 @@ declare module 'vue' {
     TSpace: typeof import('tdesign-vue-next')['Space']
     TTable: typeof import('tdesign-vue-next')['Table']
     TTag: typeof import('tdesign-vue-next')['Tag']
+    TTagInput: typeof import('tdesign-vue-next')['TagInput']
     TTextarea: typeof import('tdesign-vue-next')['Textarea']
     TUpload: typeof import('tdesign-vue-next')['Upload']
   }

+ 22 - 0
src/api/product.ts

@@ -0,0 +1,22 @@
+import type { CreateProductRequest, Product, SearchProductFilter, UpdateProductRequest } from '@/model/product'
+import httpClient from '@/api/httpClient'
+import type { PageResult } from '@/model/base'
+
+export const searchProducts = async (searchProductFilter: SearchProductFilter) => {
+  return httpClient.get<PageResult<Product>>('/products', searchProductFilter)
+}
+
+
+export const createProduct = async (createProductRequest: CreateProductRequest) => {
+  return httpClient.post<Product>('/products', createProductRequest)
+}
+
+
+export const editProduct = async (id: string, updateProductRequest: UpdateProductRequest) => {
+  return httpClient.put<Product>(`/products/${id}`, updateProductRequest)
+}
+
+
+export const deleteProduct = async (id: string) => {
+  return httpClient.delete(`/products/${id}`)
+}

+ 5 - 2
src/components/ImageUpload.vue

@@ -40,8 +40,11 @@ const emits = defineEmits<{
   'update:modelValue': [string]
 }>()
 watch(() => props.modelValue,(newVal) => {
-  if (!newVal) return
-  fileList.value = [{url: newVal}]
+  if (newVal) {
+    fileList.value = [{url: newVal}]
+  } else {
+    fileList.value = []
+  }
 
 },{
   deep:true,

+ 2 - 2
src/model/base.ts

@@ -7,7 +7,7 @@ export interface ErrorResponse {
 export interface BaseModel {
   id: string
   createdTime: string
-  updateTime: string
+  updatedTime: string
 }
 
 export interface AuditBaseModel extends BaseModel {
@@ -29,4 +29,4 @@ export interface PageResult<T> {
   data: T[]
 }
 
-export interface ErrorResponseList extends Array<ErrorResponse> {}
+export interface ErrorResponseList extends Array<ErrorResponse> {}

+ 33 - 0
src/model/product.ts

@@ -0,0 +1,33 @@
+import type { BaseFilterRequest, BaseModel } from '@/model/base'
+
+
+
+export interface Product extends BaseModel{
+  name: string;
+  category: ProductCategory
+  photo: string;
+  tags: string[];
+  totalCalories: number;
+  totalWeight: number;
+}
+
+export interface CreateProductRequest extends Pick<Product, "name" | 'photo' | 'tags' | 'totalCalories' | 'totalWeight' | 'category'> {}
+
+export interface SearchProductFilter extends BaseFilterRequest{
+}
+
+
+export interface UpdateProductRequest extends CreateProductRequest {}
+
+export enum ProductCategory {
+  DRY_FOOD,
+  WET_FOOD,
+}
+
+export const productCategoryOptions: {
+  label: string
+  value: ProductCategory
+}[] = [
+  { label: '干粮', value: ProductCategory.DRY_FOOD },
+  { label: '湿粮', value: ProductCategory.WET_FOOD },
+]

+ 129 - 0
src/pages/product/components/ProductDialog.vue

@@ -0,0 +1,129 @@
+<script setup lang="ts">
+import { type CreateProductRequest, type Product, ProductCategory, productCategoryOptions } from '@/model/product'
+import { type FormInstanceFunctions, type FormRules, MessagePlugin } from 'tdesign-vue-next'
+import ImageUpload from '@/components/ImageUpload.vue'
+import { createProduct, editProduct } from '@/api/product'
+
+const props = defineProps<{
+  data: Product | null
+}>()
+
+const defaultData: CreateProductRequest = {
+  name: '',
+  photo: '',
+  category: ProductCategory.DRY_FOOD,
+  tags: [],
+  totalWeight: 0,
+  totalCalories: 0
+}
+
+const formData = ref<CreateProductRequest>(defaultData)
+
+watch(
+  () => props.data,
+  (newData) => {
+    if (newData) {
+      formData.value = {
+        name: newData.name,
+        category: newData.category,
+        photo: newData.photo,
+        tags: newData.tags,
+        totalWeight: newData.totalWeight,
+        totalCalories: newData.totalCalories
+      }
+    } else {
+      formData.value = defaultData
+    }
+  }
+)
+
+const rules: FormRules<CreateProductRequest> = {
+  name: [
+    {
+      required: true,
+      message: '产品名称不能为空'
+    }
+  ],
+  photo: [
+    {
+      required: true,
+      message: '产品图片不能为空'
+    }
+  ],
+  totalWeight: [
+    {
+      required: true,
+      message: '净重量不能为空'
+    }
+  ],
+  totalCalories: [
+    {
+      required: true,
+      message: '总热量不能为空'
+    }
+  ]
+}
+
+const formRef = ref<FormInstanceFunctions | null>(null)
+const emits = defineEmits<{
+  success: [void]
+}>()
+
+const editId = computed(() => props.data?.id)
+const handleSave = async () => {
+  const validate = await formRef.value?.validate()
+
+  if (validate !== true) return
+
+  try {
+    editId.value
+      ? await editProduct(editId.value, formData.value)
+      : await createProduct(formData.value)
+    await MessagePlugin.success(`${editId.value ? '编辑' : '创建'}成功`)
+    emits('success')
+  } finally {
+    formRef.value?.reset()
+  }
+}
+</script>
+
+<template>
+  <TDialog @confirm="handleSave">
+    <TForm ref="formRef" :data="formData" :rules="rules" resetType="initial">
+      <TFormItem label="产品名称" name="name">
+        <TInput v-model.trim="formData.name" clearable placeholder="请输入产品名称" />
+      </TFormItem>
+      <TFormItem label="产品分类" name="category">
+        <TSelect v-model="formData.category" :options="productCategoryOptions" />
+      </TFormItem>
+      <TFormItem label="产品名称" name="photo">
+        <ImageUpload v-model="formData.photo" />
+      </TFormItem>
+      <TFormItem label="产品标签" name="tags">
+        <TTagInput v-model="formData.tags" clearable placeholder="请输入产品标签" />
+      </TFormItem>
+      <TFormItem label="净重量" name="totalWeight">
+        <TInputNumber
+          v-model="formData.totalWeight"
+          clearable
+          placeholder="请输入净重量"
+          theme="normal"
+          align="right"
+          suffix="g"
+        ></TInputNumber>
+      </TFormItem>
+      <TFormItem label="总热量" name="totalCalories">
+        <TInputNumber
+          v-model="formData.totalCalories"
+          clearable
+          placeholder="请输入总热量"
+          theme="normal"
+          align="right"
+          suffix="kcal"
+        ></TInputNumber>
+      </TFormItem>
+    </TForm>
+  </TDialog>
+</template>
+
+<style scoped></style>

+ 151 - 0
src/pages/product/index.vue

@@ -0,0 +1,151 @@
+<script setup lang="ts">
+import { onMounted } from 'vue'
+import RegularPage from '@/components/RegularPage.vue'
+
+import { type BaseTableColumns, MessagePlugin } from 'tdesign-vue-next'
+import { useSearchable } from '@/composables/useSearchable'
+import { deleteProduct, searchProducts } from '@/api/product'
+import type { Product, SearchProductFilter } from '@/model/product'
+import ProductDialog from '@/pages/product/components/ProductDialog.vue'
+import ImagePreviewer from '@/components/ImagePreviewer.vue'
+const { data, loading, pagination, onPageChange, fetchData } = useSearchable<
+  SearchProductFilter,
+  Product
+>(searchProducts)
+
+const columns: BaseTableColumns = [
+  {
+    title: '产品名称',
+    colKey: 'name',
+  },
+  {
+    title: '产品分类',
+    colKey: 'category',
+  },
+  {
+    title: '产品图片',
+    colKey: 'photo',
+  },
+  {
+    title: '产品标签',
+    colKey: 'tags',
+  },
+  {
+    title: '净重',
+    colKey: 'totalWeight',
+  },
+  {
+    title: '总热量',
+    colKey: 'totalCalories'
+  },
+  {
+    title: '创建时间',
+    colKey: 'createdTime',
+  },
+  {
+    title: '操作',
+    colKey: 'operation',
+  }
+]
+
+
+onMounted(fetchData)
+
+
+const productDialogVisible = ref(false)
+
+const editData = ref<Product | null>(null)
+
+const handleEdit = (data: Product) => {
+  editData.value = data
+  productDialogVisible.value = true
+}
+
+
+const handleDelete = async (id: string) => {
+  loading.value = true
+  try {
+    await deleteProduct(id)
+    await fetchData()
+    await MessagePlugin.success('删除成功')
+  } catch (e) {
+    await MessagePlugin.error('删除失败')
+  }finally {
+    loading.value = false
+  }
+}
+
+
+const handleSuccess = async () => {
+  await fetchData()
+  productDialogVisible.value = false
+  editData.value = null
+}
+
+const handleClose = () => {
+  editData.value = null
+}
+
+</script>
+
+<template>
+  <RegularPage title="产品管理">
+    <template #right-area>
+      <TButton @click="productDialogVisible = true">创建产品</TButton>
+    </template>
+    <TTable
+      class="w-full h-full"
+      row-key="id"
+      height="92%"
+      table-layout="auto"
+      :data="data"
+      :loading="loading"
+      :columns="columns"
+      cell-empty-content="-"
+      :paginationAffixedBottom="true"
+      :pagination="{
+        total: pagination.total,
+        current: pagination.page,
+        pageSize: pagination.size,
+        onChange: onPageChange
+      }"
+    >
+
+      <template #photo="{ row }">
+        <ImagePreviewer :url="row.photo"/>
+      </template>
+
+      <template #tags="{ row }">
+        <TSpace>
+          <TTag v-for="tag in row.tags" :key="tag">{{ tag }}</TTag>
+        </TSpace>
+      </template>
+
+      <template #totalWeight="{ row }">
+        {{ row.totalWeight }}g
+      </template>
+      <template #totalCalories="{ row }">
+        {{ row.totalCalories }}kcal
+      </template>
+      <template #operation="{ row }">
+        <TSpace :size="1">
+          <TButton variant="text" size="small" theme="primary" @click="() => handleEdit(row)"
+          >编辑</TButton
+          >
+          <t-popconfirm
+            theme="default"
+            content="确定删除此产品吗?"
+            @confirm="() => handleDelete(row.id)"
+          >
+            <TButton variant="text" size="small" theme="danger">删除</TButton>
+          </t-popconfirm>
+        </TSpace>
+      </template>
+    </TTable>
+    <ProductDialog v-model:visible="productDialogVisible" :data="editData" @success="handleSuccess" @close="handleClose"></ProductDialog>
+  </RegularPage>
+</template>
+
+<style scoped>
+
+</style>

+ 9 - 0
src/router/index.ts

@@ -32,6 +32,15 @@ export const asyncRoutes: RouteRecordRaw[] = [
       icon: 'image'
     }
   },
+  {
+    name: 'product',
+    path: 'product',
+    meta: {
+      title: '产品管理',
+      icon: 'shop'
+    },
+    component: () => import('@/pages/product/index.vue')
+  }
 ]
 
 const router = createRouter({