Browse Source

2024-12-25

cmy 10 months ago
parent
commit
b4b44749d9

+ 1 - 1
.env.pro

@@ -5,7 +5,7 @@ VITE_NODE_ENV=production
 VITE_API_BASE_PATH=
 
 # 打包路径
-VITE_BASE_PATH=/
+VITE_BASE_PATH=/admin/
 
 # 是否删除debugger
 VITE_DROP_DEBUGGER=true

+ 29 - 2
src/api/goods/index.ts

@@ -1,6 +1,7 @@
 import request from '@/axios'
 import { REQUEST_BASE } from '@/constants'
-import { categoryData } from './types'
+import { categoryData, goodsData } from './types'
+import { productDataParse } from './parseData'
 export const getProductCategory = (params: {
   page?: number
   limit?: number
@@ -12,8 +13,34 @@ export const addProductCategory = (data: categoryData): Promise<IResponse> => {
   return request.post({ url: `${REQUEST_BASE}/productCategory/save`, data })
 }
 export const delProductCategory = (id: number): Promise<IResponse> => {
-  return request.delete({ url: `${REQUEST_BASE}/productCategory/designer/${id}` })
+  return request.post({ url: `${REQUEST_BASE}/productCategory/delete/${id}` })
 }
 export const putProductCategory = (data: categoryData): Promise<IResponse> => {
   return request.post({ url: `${REQUEST_BASE}/productCategory/update/${data.id}`, data })
 }
+export const getProduct = (params: { page?: number; limit?: number }): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/product/index`, params })
+}
+export const addProduct = (data: goodsData): Promise<IResponse> => {
+  const pushData = productDataParse(data)
+  return request.post({ url: `${REQUEST_BASE}/product/save`, data: pushData })
+}
+export const delProduct = (id: number): Promise<IResponse> => {
+  return request.post({ url: `${REQUEST_BASE}/product/delete/${id}` })
+}
+export const putProduct = (data: goodsData): Promise<IResponse> => {
+  const pushData = productDataParse(data)
+  return request.post({ url: `${REQUEST_BASE}/product/update/${data.id}`, data: pushData })
+}
+export const getProductDetail = (
+  id: any
+): Promise<
+  IResponse<{
+    storeInfo: any
+    productValue: any[]
+  }>
+> => {
+  return request.get({
+    url: `${REQUEST_BASE}/product/read/${id}`
+  })
+}

+ 18 - 0
src/api/goods/parseData.ts

@@ -0,0 +1,18 @@
+import { isArray } from 'lodash-es'
+
+export const productDataParse = (data) => {
+  if (data.keyword && isArray(data.keyword)) {
+    data.keyword = data.keyword.join(',')
+  }
+  if (data.image && isArray(data.image)) {
+    data.image = data.image.join(',')
+  }
+  data.attrs_value = data.attrs_value.map((item) => {
+    if (item.image && isArray(item.image)) {
+      item.image = item.image.join(',')
+    }
+    return item
+  })
+  data.is_show = data.is_show ? 1 : 0
+  return data
+}

+ 34 - 0
src/api/goods/types.ts

@@ -6,3 +6,37 @@ export interface categoryData {
   is_show: 1 | 0 //是否显示
   sort: number //排序
 }
+interface attrs {
+  attr_name: string //属性名
+  attr_values: string //属性值
+}
+export interface attrsValue {
+  suk: string //规格参数
+  image: string[] | string //商品图片
+  stock: number //库存
+  price: number //商品价格
+  ot_price: number //商品原价
+  cost: number //商品成本价
+}
+export interface goodsData {
+  id?: number
+  name: string //商品名称
+  video_open: 0 | 1 | true | false //商品视频是否开启:0关闭 1开启
+  video_link?: string //视频链接
+  image: string[] //主图
+  slider_image: string[] //轮播图
+  info?: string //简介
+  keyword?: string[] | string //关键词
+  cate_ids: string[] | string //分类
+  postage: number //上楼费
+  temp_id?: string //上楼费模板ID
+  unit_name: string //单位
+  sales?: number //销量
+  ficti?: number //虚拟销量
+  spec_type: 0 | 1 //规格模式:0=单规格,1=多规格
+  is_show: 0 | 1 //状态:0=未上架,1=上架
+  attrs: attrs[] //规格
+  attrs_value: attrsValue[] //规格参数
+  sort: number //排序
+  description: string //商品内容
+}

+ 16 - 1
src/api/staff/index.ts

@@ -1,6 +1,6 @@
 import request from '@/axios'
 import { REQUEST_BASE } from '@/constants'
-import { designerSearch, designerData, staffCategoryData, jobsData } from './types'
+import { designerSearch, designerData, staffCategoryData, jobsData, SalespersonData } from './types'
 export const getDesigner = (params: designerSearch): Promise<IResponse> => {
   return request.get({ url: `${REQUEST_BASE}/decoration/designer`, params })
 }
@@ -49,3 +49,18 @@ export const delStaffJobs = (id: number): Promise<IResponse> => {
 export const putStaffJobs = (data: jobsData): Promise<IResponse> => {
   return request.put({ url: `${REQUEST_BASE}/decoration/jobs/${data.id}`, data })
 }
+export const getStaffSalesperson = (params: {
+  page?: number
+  limit?: number
+}): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/decoration/salesperson`, params })
+}
+export const addStaffSalesperson = (data: SalespersonData): Promise<IResponse> => {
+  return request.post({ url: `${REQUEST_BASE}/decoration/salesperson`, data })
+}
+export const delStaffSalesperson = (id: number): Promise<IResponse> => {
+  return request.delete({ url: `${REQUEST_BASE}/decoration/salesperson/${id}` })
+}
+export const putStaffSalesperson = (data: SalespersonData): Promise<IResponse> => {
+  return request.put({ url: `${REQUEST_BASE}/decoration/salesperson/${data.id}`, data })
+}

+ 17 - 8
src/api/staff/types.ts

@@ -1,35 +1,44 @@
-export type designerSearch = {
+export interface designerSearch {
   page?: number
   limit?: number
+  gender?: string
+  tag_list?: string
+  name?: string
 }
-
-export interface designerData {
+interface staff {
   id?: number
   uid: number | null
   name: string //真实姓名
   phone: number | null //手机号
-  gender: 0 | 1 | 2 //性别 0保密 1男 2女
   avatar: string //头像
-  birth_day_time?: string | number //生日
   province: string //省
   city: string //市
   area: string //区
-  tag_list: Array<string> //标签
   longitude: string | number //经度
   latitude: string | number //纬度
   detail_address: string
+}
+
+export interface designerData extends staff {
+  gender: 0 | 1 | 2 //性别 0保密 1男 2女
+  birth_day_time?: string | number //生日
+  tag_list: Array<string> //标签
   start_job_year: string
 }
-export type staffCategoryData = {
+export interface staffCategoryData {
   id?: number
   name: string
   is_show: 1 | 0
   type: 1 | 2
 }
-export type jobsData = {
+export interface jobsData {
   id?: number
   name: string
   is_show: 1 | 0
   cate_id: number
   default_price: number
 }
+export interface SalespersonData extends staff {
+  level?: number
+  parent_id?: number
+}

+ 1 - 1
src/api/system/admin/index.ts

@@ -26,7 +26,7 @@ export const updateAdmin = (data: upAdmin): Promise<IResponse> => {
   return request.put({ url: `${REQUEST_BASE}/update_admin`, data })
 }
 
-export const upImage = (data: any): Promise<IResponse> => {
+export const upImage = (data: { pid: number; type: number; file: File }): Promise<IResponse> => {
   return request.post({
     url: `${REQUEST_BASE}/file/upload/${data.type}`,
     data,

+ 31 - 1
src/components/Editor/src/Editor.vue

@@ -6,7 +6,10 @@ import { propTypes } from '@/utils/propTypes'
 import { isNumber } from '@/utils/is'
 import { ElMessage } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { REQUEST_BASE } from '@/constants'
 
+const userStore = useUserStoreWithOut()
 const localeStore = useLocaleStore()
 
 const currentLocale = computed(() => localeStore.getCurrentLocale)
@@ -50,11 +53,30 @@ const handleCreated = (editor: IDomEditor) => {
   editorRef.value = editor
   valueHtml.value = props.modelValue
 }
-
+const toolbarConfig = {
+  excludeKeys: ['group-video']
+}
 // 编辑器配置
 const editorConfig = computed((): IEditorConfig => {
   return Object.assign(
     {
+      MENU_CONF: {
+        uploadImage: {
+          fieldName: 'file',
+          meta: {
+            pid: 0
+          },
+          headers: {
+            'Authori-Zation': userStore.getTokenKey + userStore.getToken
+          },
+          allowedFileTypes: ['image/*'],
+          server: `${REQUEST_BASE}/file/upload/0`,
+          customInsert(res: any, insertFn) {
+            // console.log(res, 'res')
+            insertFn(res.data.src, res.data.src, res.data.src)
+          }
+        }
+      },
       readOnly: false,
       customAlert: (s: string, t: string) => {
         switch (t) {
@@ -104,6 +126,13 @@ onBeforeUnmount(() => {
 
 const getEditorRef = async (): Promise<IDomEditor> => {
   await nextTick()
+  // const editor = unref(editorRef.value)
+  // if (!editor) {
+  //   console.error('editorRef is null')
+  //   throw new Error('editorRef is null')
+  // }
+  // const toolbar = DomEditor.getToolbar(editor)
+  // console.log(toolbar)
   return unref(editorRef.value) as IDomEditor
 }
 
@@ -117,6 +146,7 @@ defineExpose({
     <!-- 工具栏 -->
     <Toolbar
       :editor="editorRef"
+      :defaultConfig="toolbarConfig"
       :editorId="editorId"
       class="border-0 b-b-1 border-solid border-[var(--el-border-color)]"
     />

+ 8 - 4
src/components/UpFile/src/upImageButtom.vue

@@ -1,6 +1,6 @@
 <script setup lang="tsx">
 import upFile from './index.vue'
-import { PropType, ref, defineModel } from 'vue'
+import { PropType, ref, defineModel, watch } from 'vue'
 import { Dialog } from '@/components/Dialog'
 import { ElImage, ElMessage } from 'element-plus'
 const modelValue = defineModel<string[]>({
@@ -43,8 +43,7 @@ const save = async () => {
       saveLoading.value = false
       return ElMessage.error('最多选择再' + props.num + '张图片')
     }
-    modelValue.value.push(...actionData.value.map((item: any) => item.att_dir))
-    modelValue.value = [...modelValue.value]
+    modelValue.value = [...modelValue.value, ...actionData.value.map((item: any) => item.att_dir)]
   } else if (saveType.value === 'edit') {
     if (actionData.value.length > 1) {
       saveLoading.value = false
@@ -58,6 +57,11 @@ const save = async () => {
   saveLoading.value = false
   showImage.value = false
 }
+watch(showImage, (val) => {
+  if (!val) {
+    actionData.value = []
+  }
+})
 </script>
 
 <template>
@@ -65,7 +69,7 @@ const save = async () => {
     <div class="flex">
       <template v-if="modelValue.length > 0">
         <div class="imageItem" v-for="(item, index) in modelValue" :key="index">
-          <ElImage :src="item" class="image">
+          <ElImage :src="item" class="image" fit="cover">
             <template #error>
               <div class="flex items-center justify-center bg-gray-200 w-full h-full">
                 <Icon icon="vi-ep:picture" />

+ 7 - 5
src/components/tableImage/tableImage.vue

@@ -27,11 +27,13 @@ defineProps({
   <div
     class="lh-none"
     @click="
-      createImageViewer({
-        urlList: list.length > 0 ? list : [src],
-        initialIndex: ind || 0,
-        hideOnClickModal: true
-      })
+      src
+        ? createImageViewer({
+            urlList: list.length > 0 ? list : [src],
+            initialIndex: ind || 0,
+            hideOnClickModal: true
+          })
+        : null
     "
   >
     <ElImage :alt="alt" fit="contain" class="w-[50px] h-[50px]" :src="src">

+ 8 - 0
src/router/model/Goods.ts

@@ -26,6 +26,14 @@ export default {
       meta: {
         title: '商品分类'
       }
+    },
+    {
+      path: 'edit/:type/:id',
+      component: () => import('@/views/Goods/edit/add.vue'),
+      name: `${pre}-edit`,
+      meta: {
+        title: '添加修改商品'
+      }
     }
   ]
 }

+ 8 - 0
src/router/model/Staff.ts

@@ -27,6 +27,14 @@ export default {
         title: '装修工'
       }
     },
+    {
+      path: 'salesman',
+      component: () => import('@/views/Staff/salesman/index.vue'),
+      name: `${pre}-salesman`,
+      meta: {
+        title: '业务员'
+      }
+    },
     {
       path: 'category',
       component: () => import('@/views/Staff/category/index.vue'),

+ 10 - 9
src/views/Authorization/Menu/components/Detail.vue

@@ -125,6 +125,15 @@ const api = [
       }
     }
   },
+  {
+    field: 'meta.noTagsView',
+    label: '是否隐藏标签页',
+    slots: {
+      default: (data) => {
+        return renderTag(data.meta.noTagsView)
+      }
+    }
+  },
   {
     field: 'meta.alwaysShow',
     label: '是否一直显示',
@@ -161,15 +170,7 @@ const api = [
       }
     }
   },
-  {
-    field: 'meta.noTagsView',
-    label: '是否隐藏标签页',
-    slots: {
-      default: (data) => {
-        return renderTag(data.meta.noTagsView)
-      }
-    }
-  },
+
   {
     field: 'meta.canTo',
     label: '是否可跳转',

+ 11 - 6
src/views/Goods/classify/index.vue

@@ -21,7 +21,7 @@ const { t } = useI18n()
 const { tableRegister, tableState, tableMethods } = useTable({
   fetchDataApi: async () => {
     const res = await getProductCategory({
-      pid: 0,
+      pid: -1,
       page: unref(currentPage) || 1,
       limit: unref(pageSize) || 10
     })
@@ -39,8 +39,7 @@ const tableColumns = reactive<TableColumn[]>([
   {
     field: 'id',
     label: 'ID',
-    align: 'center',
-    width: 70
+    width: 100
   },
   {
     field: 'name',
@@ -183,8 +182,14 @@ const save = async () => {
     }
   }
 }
-const load = async (row: any, treeNode: unknown, resolve: any) => {
-  console.log(row, resolve)
+const load = async (row: any, _: unknown, resolve: any) => {
+  const res = await getProductCategory({
+    pid: row.id,
+    page: 1,
+    limit: 100
+  })
+  resolve(res.data)
+  console.log(res)
 }
 </script>
 
@@ -198,7 +203,7 @@ const load = async (row: any, treeNode: unknown, resolve: any) => {
       v-model:page-size="pageSize"
       :columns="tableColumns"
       default-expand-all
-      node-key="cate_name"
+      row-key="id"
       lazy
       :load="load"
       :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"

+ 415 - 0
src/views/Goods/edit/add.vue

@@ -0,0 +1,415 @@
+<script setup lang="tsx">
+import { onMounted, reactive, ref } from 'vue'
+import { ContentWrap } from '@/components/ContentWrap'
+import {
+  ElInput,
+  FormInstance,
+  ElSwitch,
+  ElTabPane,
+  ElCascader,
+  ElTabs,
+  ElForm,
+  ElFormItem,
+  ElInputTag,
+  ElMessage,
+  ElMessageBox
+} from 'element-plus'
+import { Editor, EditorExpose } from '@/components/Editor'
+// import { IDomEditor } from '@wangeditor/editor'
+import { useRoute, useRouter } from 'vue-router'
+import { goodsData, attrsValue } from '@/api/goods/types'
+import { getProductCategory, addProduct, getProductDetail, putProduct } from '@/api/goods'
+import { UpImgButtom } from '@/components/UpFile'
+const pageTitle = ref('添加')
+const { params } = useRoute()
+const { push } = useRouter()
+const actinoTabIndex = ref(1)
+const formData = reactive<goodsData & { attrs_value: attrsValue[]; keyword: string[] }>({
+  id: 0,
+  name: '',
+  video_open: 0,
+  video_link: '',
+  slider_image: [],
+  image: [],
+  info: '',
+  keyword: [],
+  cate_ids: [],
+  postage: 0,
+  temp_id: '',
+  unit_name: '',
+  sales: 0,
+  ficti: 0,
+  spec_type: 0,
+  is_show: 1,
+  attrs: [],
+  attrs_value: [], // 初始化为空数组
+  sort: 0,
+  description: ''
+})
+const attrs_value = reactive<
+  attrsValue & {
+    image: string[]
+  }
+>({
+  suk: '默认',
+  image: [],
+  stock: 0,
+  price: 0,
+  ot_price: 0,
+  cost: 0
+})
+const validateField = (field, errorMessage) => {
+  return {
+    message: errorMessage,
+    required: true,
+    validator: (_, __, callback) => {
+      if (!attrs_value[field]) {
+        callback(new Error(errorMessage))
+      } else {
+        callback()
+      }
+    }
+  }
+}
+
+const getRulesForSpecType = (specType) => {
+  if (specType === 0) {
+    return {
+      sutimage: [validateField('image', '请选择商品图片')],
+      stock: [validateField('stock', '请填写库存')],
+      price: [validateField('price', '请填写售价')],
+      cost: [validateField('cost', '请填写成本价')]
+    }
+  } else if (specType === 1) {
+    return {}
+  } else {
+    console.warn('Unknown spec_type:', specType)
+    return {}
+  }
+}
+
+const rules = reactive({
+  cate_ids: [{ required: true, message: '请选择商品分类', trigger: 'change' }],
+  unit_name: [{ required: true, message: '请填写商品单位', trigger: 'change' }],
+  name: [{ required: true, message: '请输入活动名称', trigger: 'blur' }],
+  image: [{ required: true, message: '请选择商品主图', trigger: 'change' }],
+  slider_image: [
+    {
+      message: '请选择轮播图片',
+      trigger: 'change',
+      required: true,
+      validator: (_, val, callback) => {
+        // console.log(val, formData.slider_image, 'val')
+        if (!val || val.length === 0) {
+          callback(new Error('请选择轮播图片'))
+        } else {
+          callback()
+        }
+      }
+    }
+  ],
+  ...getRulesForSpecType(formData.spec_type)
+})
+const formRef = ref<FormInstance>()
+
+const cateIdsProps = {
+  // checkStrictly: true,
+  emitPath: false,
+  multiple: true,
+  lazy: true,
+  lazyLoad: (node, resolve) => {
+    getProductCategory({ page: 1, limit: 100, pid: node.value || -1 })
+      .then((res) => {
+        if (res) {
+          const nodes = res.data.map((item) => {
+            return {
+              value: item.id,
+              label: item.name,
+              leaf: !item.hasChildren
+            }
+          })
+          resolve(nodes)
+        }
+      })
+      .catch((e) => {
+        console.log(e)
+      })
+  }
+}
+
+// const changeHtml = (editor: IDomEditor) => {
+//   console.log(formData.description)
+// }
+
+const editorRef = ref<typeof Editor & EditorExpose>()
+
+const save = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  const valid = await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+    } else {
+      for (const key in fields) {
+        // console.log(fields)
+        ElMessage.error(fields[key][0].message)
+        return
+      }
+      // console.log('error submit!', fields)
+    }
+  })
+  // console.log(valid, 'params')
+  if (valid) {
+    const data: goodsData = {
+      name: formData.name,
+      video_open: formData.video_open,
+      video_link: formData.video_link,
+      slider_image: formData.slider_image,
+      info: formData.info,
+      image: formData.image,
+      keyword: formData.keyword,
+      cate_ids: formData.cate_ids,
+      postage: formData.postage,
+      temp_id: formData.temp_id,
+      unit_name: formData.unit_name,
+      sales: formData.sales,
+      ficti: formData.ficti,
+      spec_type: formData.spec_type,
+      attrs: formData.attrs,
+      attrs_value: formData.attrs_value,
+      description: formData.name,
+      sort: formData.sort,
+      is_show: formData.is_show
+    }
+    if (formData.spec_type == 0) {
+      data.attrs_value = [{ ...attrs_value }]
+    }
+    try {
+      let res = {}
+      // console.log(params.type == 'add', '11111')
+      if (params.type == 'add') {
+        res = await addProduct(data)
+      } else if (params.type == 'edit') {
+        data.id = formData.id
+        res = await putProduct(data)
+      }
+      if (res) {
+        let title = '添加'
+        if (params.type == 'edit') {
+          title = '编辑'
+        }
+        ElMessage.success('保存成功')
+        ElMessageBox.confirm(`${title}成功是否返回商铺列表`, '提示', {
+          confirmButtonText: '返回列表',
+          cancelButtonText: `继续${title}`,
+          type: 'warning'
+        }).then(() => {
+          backList()
+        })
+      }
+    } catch (error) {
+      console.log(error)
+    } finally {
+      console.log(data)
+    }
+  }
+}
+
+const loadingData = ref(false)
+onMounted(async () => {
+  if (params.type == 'add') {
+    pageTitle.value = '添加商品'
+  } else if (params.type == 'edit') {
+    pageTitle.value = '编辑商品'
+    try {
+      const res = await getProductDetail(params.id)
+      const data = res.data.storeInfo
+      // console.log(data.cate_ids, 'res')
+      formData.id = data.id
+      formData.name = data.name
+      formData.video_open = data.video_open
+      formData.video_link = data.video_link
+      formData.slider_image = data.slider_image
+      formData.image = data.image
+      formData.keyword = data.keyword
+      formData.cate_ids = data.cate_ids.map((re) => parseInt(re))
+      formData.postage = data.postage
+      formData.temp_id = data.temp_id
+      formData.unit_name = data.unit_name
+      formData.sales = data.sales
+      formData.ficti = data.ficti
+      formData.spec_type = data.spec_type
+      formData.is_show = data.is_show
+      formData.attrs = data.attrs
+      formData.attrs_value = res.data.productValue.map((e) => e)
+      formData.sort = data.sort
+      formData.description = data.description
+      // 判断是否单规格
+      if (formData.spec_type == 0) {
+        const values = res.data.productValue[0]
+        // console.log(values, 'values')
+        attrs_value.cost = values.cost
+        attrs_value.price = values.price
+        attrs_value.ot_price = values.ot_price
+        attrs_value.stock = values.stock
+        attrs_value.suk = values.suk
+        attrs_value.image = [values.image]
+      }
+    } catch (error) {
+      console.log(error, 'error')
+    }
+  }
+})
+const backList = () => {
+  push('/goods/list')
+}
+// const initData = () => {
+//   formData.name = ''
+//   formData.video_open = 0
+//   formData.video_link = ''
+//   formData.slider_image = []
+//   formData.image = []
+//   formData.keyword = []
+//   formData.cate_ids = []
+//   formData.postage = 0
+//   formData.temp_id = ''
+//   formData.unit_name = ''
+//   formData.sales = 0
+//   formData.ficti = 0
+//   formData.spec_type = 0
+//   formData.is_show = 1
+//   formData.attrs = []
+//   formData.attrs_value = []
+//   formData.sort = 0
+//   formData.description = ''
+//   attrs_value.cost = 0
+//   attrs_value.price = 0
+//   attrs_value.ot_price = 0
+//   attrs_value.stock = 0
+//   attrs_value.suk = '默认'
+//   attrs_value.image = []
+// }
+</script>
+<template>
+  <ContentWrap
+    :title="pageTitle"
+    class="!min-h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height)-var(--app-content-padding))]"
+  >
+    <template #header>
+      <BaseButton size="small" @click="backList"> 返回列表 </BaseButton>
+    </template>
+    <ElForm
+      v-loading="loadingData"
+      ref="formRef"
+      :model="formData"
+      :rules="rules"
+      label-width="100px"
+    >
+      <ElTabs v-model="actinoTabIndex" type="border-card">
+        <ElTabPane label="商品信息" :name="1">
+          <ElFormItem label="商品分类" prop="cate_ids">
+            <ElCascader
+              v-model="formData.cate_ids"
+              :show-all-levels="false"
+              clearable
+              class="w-full"
+              :props="cateIdsProps"
+              placeholder="请选择商品分类"
+            />
+          </ElFormItem>
+          <ElFormItem label="商品名称" prop="name">
+            <ElInput v-model="formData.name" placeholder="请输入商品名称" />
+          </ElFormItem>
+          <ElFormItem label="单位" prop="unit_name">
+            <ElInput v-model="formData.unit_name" placeholder="请输入商品单位" />
+          </ElFormItem>
+          <ElFormItem label="关键词" prop="keyword">
+            <ElInputTag v-model="formData.keyword" tagEffect="light" tagType="primary" clearable />
+          </ElFormItem>
+          <ElFormItem label="商品简介" prop="info">
+            <ElInput
+              type="textarea"
+              :rows="4"
+              v-model="formData.info"
+              placeholder="请输入商品简介"
+            />
+          </ElFormItem>
+          <ElFormItem label="商品封面图" prop="image">
+            <UpImgButtom v-model="formData.image" />
+          </ElFormItem>
+          <ElFormItem label="商品轮播图" prop="slider_image">
+            <UpImgButtom
+              tip="建议尺寸:800 * 800px,可拖拽改变图片顺序,默认首张图为主图,最多上传10张"
+              v-model="formData.slider_image"
+              :num="10"
+            />
+          </ElFormItem>
+          <ElFormItem label="上楼费" prop="postage">
+            <ElInput type="number" v-model="formData.postage" placeholder="请输入上楼费" />
+          </ElFormItem>
+          <ElFormItem label="上下架" prop="is_show">
+            <ElSwitch
+              active-text="上架"
+              inactive-text="下架"
+              inline-prompt
+              :active-value="1"
+              :inactive-value="0"
+              v-model="formData.is_show"
+            />
+          </ElFormItem>
+          <ElFormItem label="排序" prop="sort">
+            <ElInput type="number" v-model="formData.sort" placeholder="请输入上楼费" />
+          </ElFormItem>
+        </ElTabPane>
+        <ElTabPane label="商品规格" :name="2">
+          <template v-if="formData.spec_type == 0">
+            <ElFormItem label="商品规格图" prop="sutimage">
+              <UpImgButtom v-model="attrs_value.image" />
+            </ElFormItem>
+            <ElFormItem label="售价" prop="price">
+              <ElInput type="number" v-model="attrs_value.price" placeholder="请输入商品单位" />
+            </ElFormItem>
+            <ElFormItem label="商品原价" prop="ot_price">
+              <ElInput type="number" v-model="attrs_value.ot_price" placeholder="请输入商品单位" />
+            </ElFormItem>
+            <ElFormItem label="商品成本价" prop="cost">
+              <ElInput type="number" v-model="attrs_value.cost" placeholder="请输入商品单位" />
+            </ElFormItem>
+            <ElFormItem label="商品库存" prop="stock">
+              <ElInput type="number" v-model="attrs_value.stock" placeholder="请输入商品单位" />
+            </ElFormItem>
+          </template>
+
+          <!-- <ElFormItem label="规格类型" prop="spec_type">
+            <el-radio-group v-model="formData.spec_type">
+              <el-radio-button label="单规格" :value="0" />
+              <el-radio-button label="多规格" :value="1" />
+            </el-radio-group>
+          </ElFormItem> -->
+
+          <!-- <ElFormItem v-if="formData.spec_type == 1" label="" prop="spec_type">
+            <BaseButton>添加新规格</BaseButton>
+            <BaseButton>立即生成</BaseButton>
+          </ElFormItem> -->
+        </ElTabPane>
+        <ElTabPane label="商品详情" :name="3">
+          <Editor v-model="formData.description" ref="editorRef" />
+        </ElTabPane>
+        <ElTabPane label="其他设置" :name="4">
+          <ElFormItem label="虚拟销量" prop="ficti">
+            <ElInput type="number" v-model="formData.ficti" placeholder="请输入虚拟销量" />
+          </ElFormItem>
+        </ElTabPane>
+      </ElTabs>
+    </ElForm>
+  </ContentWrap>
+  <div
+    class="text-center border-t-1px border-solid border-gray-200 py-10px mt-10px position-sticky bottom-0 left-0 right-0 bg-white"
+  >
+    <BaseButton v-if="actinoTabIndex > 1" @click="actinoTabIndex--"> 上一步 </BaseButton>
+    <BaseButton v-if="actinoTabIndex < 4" @click="actinoTabIndex++"> 下一步 </BaseButton>
+    <BaseButton v-if="params.type == 'edit'" @click="save(formRef)"> 保存 </BaseButton>
+    <BaseButton v-if="params.type == 'add' && actinoTabIndex == 4" @click="save(formRef)">
+      保存
+    </BaseButton>
+  </div>
+</template>

+ 0 - 237
src/views/Goods/list/components/Detail.vue

@@ -1,237 +0,0 @@
-<script setup lang="tsx">
-import { ref, watch, PropType, useTemplateRef } from 'vue'
-import {
-  ElDrawer,
-  ElCol,
-  ElRow,
-  ElAvatar,
-  ElMessage,
-  ElButton,
-  ElOption,
-  ElSelect,
-  ElDialog,
-  ElRadioGroup,
-  ElRadio,
-  ElInput,
-  ElFormItem,
-  ElForm
-} from 'element-plus'
-import {
-  getUserDetail,
-  putUserData,
-  // getBalanceLog,
-  // getSpreadList,
-  // getBrokerageLog,
-  // getBillLog,
-  putGiveLevel,
-  updateAccount
-} from '@/api/user'
-import { userData } from '@/api/user/types'
-import DetailFrom from './DetailFrom.vue'
-
-const modelValue = defineModel({ type: Boolean, default: false })
-const props = defineProps({
-  uid: {
-    type: Number,
-    default: () => 0
-  },
-  levelList: {
-    type: Array as PropType<any[]>,
-    default: () => []
-  },
-  groupList: {
-    type: Array as PropType<any[]>,
-    default: () => []
-  }
-})
-const loading = ref(true)
-const userDetail = ref<userData>()
-watch(
-  () => modelValue,
-  async ({ value }) => {
-    if (props.uid == 0 || !value) return
-    const { data } = await getUserDetail(props.uid)
-    userDetail.value = data
-    loading.value = false
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
-const detailFromRef = useTemplateRef('detailFromRef')
-const saveLoading = ref(false)
-const showDetail = ref(false)
-const saveDetail = async () => {
-  const formData = detailFromRef?.value?.getFormData()
-  if (formData) {
-    saveLoading.value = true
-    const res = await putUserData(formData)
-    if (res) {
-      ElMessage({
-        showClose: true,
-        message: '保存成功',
-        type: 'success'
-      })
-      emit('confirm')
-      const { data } = await getUserDetail(props.uid)
-      userDetail.value = data
-      showDetail.value = false
-    }
-    saveLoading.value = false
-  }
-}
-const actionLevel = ref()
-const dialogVisible = ref(false)
-const putUserLevel = async () => {
-  const res = await putGiveLevel({ id: props.uid, level_id: actionLevel.value })
-  if (res) {
-    ElMessage({
-      showClose: true,
-      message: '赠送成功',
-      type: 'success'
-    })
-    emit('confirm')
-    modelValue.value = false
-    dialogVisible.value = false
-  }
-}
-const emit = defineEmits(['confirm'])
-const upTypeTitle = ref('')
-const upType = ref('')
-const showDialogType = ref(false)
-const something = ref(1)
-const upTypeValue = ref<number>()
-const typeMark = ref<string>()
-const openTypeDialog = (type, title) => {
-  upType.value = type
-  showDialogType.value = true
-  upTypeTitle.value = title
-}
-const typeSaveLoding = ref(false)
-const updateAccountData = async () => {
-  typeSaveLoding.value = true
-  const res = await updateAccount(
-    {
-      money_type: upType.value,
-      pm: something.value,
-      num: upTypeValue.value || 0,
-      mark: typeMark.value || ''
-    },
-    props.uid
-  )
-  if (res) {
-    ElMessage({
-      showClose: true,
-      message: something.value == 1 ? '增加成功' : '减少成功',
-      type: 'success'
-    })
-    emit('confirm')
-    showDialogType.value = false
-    modelValue.value = false
-  }
-  typeSaveLoding.value = false
-}
-</script>
-
-<template>
-  <div>
-    <ElDrawer :show-close="false" v-model="modelValue" size="1000px">
-      <template #header>
-        <ElRow :gutter="20" v-loading="loading">
-          <ElCol :span="12">
-            <div class="flex juistify-start items-center">
-              <el-avatar :size="50" :src="userDetail?.avatar">
-                <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
-              /></el-avatar>
-              <div class="ml-[10px]">{{ userDetail?.nickname }}</div>
-            </div>
-          </ElCol>
-          <ElCol :span="12">
-            <div class="flex justify-end items-center h-full">
-              <BaseButton v-if="!showDetail" type="primary" @click="showDetail = true">
-                编辑
-              </BaseButton>
-              <BaseButton v-else @click="showDetail = false">取消</BaseButton>
-              <BaseButton
-                v-if="showDetail"
-                type="primary"
-                @click="saveDetail"
-                :loading="saveLoading"
-              >
-                完成
-              </BaseButton>
-              <BaseButton type="success" @click="openTypeDialog('now_money', '余额修改')"
-                >余额</BaseButton
-              >
-              <BaseButton @click="dialogVisible = true">赠送会员</BaseButton>
-            </div>
-          </ElCol>
-        </ElRow>
-      </template>
-      <DetailFrom
-        ref="detailFromRef"
-        v-loading="loading"
-        v-model="showDetail"
-        :userDetail="userDetail"
-        :levelList="levelList"
-        :groupList="groupList"
-      />
-    </ElDrawer>
-    <ElDialog title="赠送等级" v-model="dialogVisible" width="500">
-      <ElSelect v-model="actionLevel">
-        <ElOption
-          v-for="item in levelList"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
-        />
-      </ElSelect>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="putUserLevel">确定</el-button>
-        </div>
-      </template>
-    </ElDialog>
-    <ElDialog :title="upTypeTitle" v-model="showDialogType" width="500">
-      <ElForm>
-        <ElFormItem label="类型">
-          <ElRadioGroup v-model="something">
-            <ElRadio :label="0">减少</ElRadio>
-            <ElRadio :label="1">增加</ElRadio>
-          </ElRadioGroup>
-        </ElFormItem>
-        <ElFormItem label="金额"
-          ><ElInput
-            v-model="upTypeValue"
-            type="number"
-            :placeholder="something == 0 ? '请输入要减少的余额' : '请输入要增加的余额'"
-        /></ElFormItem>
-        <ElFormItem label="备注">
-          <ElInput
-            class="mt-10px"
-            :rows="3"
-            v-model="typeMark"
-            type="textarea"
-            placeholder="备注信息"
-          />
-        </ElFormItem>
-      </ElForm>
-
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="showDialogType = false">取消</el-button>
-          <el-button type="primary" :loading="typeSaveLoding" @click="updateAccountData"
-            >确定</el-button
-          >
-        </div>
-      </template>
-    </ElDialog>
-  </div>
-</template>
-<style lang="less" scoped>
-:deep(.el-tabs__content) {
-  padding: 0px;
-}
-</style>

+ 0 - 420
src/views/Goods/list/components/DetailFrom.vue

@@ -1,420 +0,0 @@
-<script setup lang="tsx">
-import { ref, watch, PropType, defineExpose } from 'vue'
-import {
-  FormRules,
-  ElTabs,
-  ElTabPane,
-  ElTable,
-  ElTableColumn,
-  ElPagination,
-  ElFormItem,
-  ElForm,
-  ElInput,
-  ElSelect,
-  ElOption,
-  ElDatePicker,
-  ElDivider,
-  ElSwitch,
-  ElTag,
-  ElDescriptions,
-  ElDescriptionsItem
-} from 'element-plus'
-import {
-  getBalanceLog,
-  getSpreadList,
-  getBrokerageLog
-  // getBillLog
-} from '@/api/user'
-import { userData } from '@/api/user/types'
-import { useValidator } from '@/hooks/web/useValidator'
-import InputPassword from '@/components/InputPassword/src/InputPassword.vue'
-import { formatToDate } from '@/utils/dateUtil'
-const { required, phone } = useValidator()
-const modelValue = defineModel<boolean>()
-const props = defineProps({
-  userDetail: {
-    type: Object as PropType<userData>,
-    default: () => ({})
-  },
-  levelList: {
-    type: Array as PropType<any[]>,
-    default: () => []
-  },
-  groupList: {
-    type: Array as PropType<any[]>,
-    default: () => []
-  }
-})
-const localUserDetail = ref<userData>({ ...props.userDetail })
-
-watch(
-  () => props.userDetail,
-  (newVal) => {
-    localUserDetail.value = { ...newVal }
-    if (typeof newVal.birthday == 'number') {
-      localUserDetail.value.birthday = newVal.birthday ? formatToDate(newVal.birthday * 1000) : ''
-      localUserDetail.value.is_promoter = newVal.is_promoter == 1 ? true : false
-      localUserDetail.value.status = newVal.status == 1 ? true : false
-    }
-  },
-  { deep: true, immediate: true }
-)
-
-const rules = ref<FormRules>({
-  phone: [required('请填写手机号'), phone()],
-  pwd: [{}],
-  true_pwd: [
-    {
-      validator: (_, val, callback) => {
-        if (!val) return callback()
-        if (localUserDetail.value.pwd !== val) {
-          callback(new Error('两次密码不一致'))
-        } else {
-          callback()
-        }
-      }
-    }
-  ],
-  true_trade_pwd: [
-    {
-      validator: (_, val, callback) => {
-        if (!val) return callback()
-        if (localUserDetail.value.trade_pwd && localUserDetail.value.trade_pwd !== val) {
-          callback(new Error('两次密码不一致'))
-        } else {
-          callback()
-        }
-      }
-    }
-  ]
-})
-const userRecordList = ref([
-  {
-    name: '佣金记录',
-    type: '3',
-    page: 1,
-    limit: 10,
-    total: 0,
-    request: getBrokerageLog,
-    data: [],
-    loading: true,
-    columns: [
-      {
-        label: '时间',
-        field: 'add_time'
-      },
-      {
-        label: '变动金额',
-        field: 'number'
-      },
-      {
-        label: '变动后余额',
-        field: 'balance'
-      },
-      {
-        label: '变动类型',
-        field: 'type'
-      },
-      {
-        label: '备注'
-      }
-    ]
-  },
-  {
-    name: '余额记录',
-    type: '2',
-    page: 1,
-    limit: 10,
-    total: 0,
-    loading: true,
-    request: getBalanceLog,
-    data: [],
-    columns: [
-      {
-        label: '时间',
-        field: 'add_time'
-      },
-      {
-        label: '变动金额',
-        field: 'number'
-      },
-      {
-        label: '变动后余额',
-        field: 'balance'
-      },
-      {
-        label: '变动类型',
-        field: 'type'
-      },
-      {
-        label: '备注'
-      }
-    ]
-  },
-  {
-    name: '推荐人列表',
-    type: '1',
-    page: 1,
-    limit: 10,
-    total: 100,
-    loading: true,
-    request: getSpreadList,
-    data: [],
-    columns: [
-      {
-        label: '用户ID',
-        field: 'uid'
-      },
-      {
-        label: '用户昵称',
-        field: 'nickname'
-      },
-      {
-        label: '用户手机号',
-        field: 'phone'
-      },
-      {
-        label: '用户余额',
-        field: 'now_money'
-      }
-    ]
-  }
-])
-const changeTable = (row: any) => {
-  for (let index = 0; index < userRecordList.value.length; index++) {
-    const element = userRecordList.value[index]
-    if (row == element.type) {
-      if (element.loading) {
-        element
-          .request(
-            {
-              page: element.page,
-              limit: element.limit
-            },
-            props.userDetail.uid
-          )
-          .then((res) => {
-            element.data = res.data.list
-            element.total = res.data.total
-            element.loading = false
-          })
-      }
-      break
-    }
-  }
-}
-const changePage = (row: any, item) => {
-  console.log(row, item, 'changePage')
-}
-const getFormData = () => {
-  const formData: userData = {
-    uid: localUserDetail.value.uid,
-    real_name: localUserDetail.value.real_name,
-    phone: localUserDetail.value.phone,
-    pwd: localUserDetail.value.pwd,
-    true_pwd: localUserDetail.value.true_pwd,
-    trade_pwd: localUserDetail.value.trade_pwd,
-    true_trade_pwd: localUserDetail.value.true_trade_pwd,
-    sex: localUserDetail.value.sex,
-    spread_uid: localUserDetail.value.spread_uid,
-    is_promoter: localUserDetail.value.is_promoter ? 1 : 0,
-    card_id: localUserDetail.value.card_id,
-    birthday: formatToDate(localUserDetail.value.birthday),
-    mark: localUserDetail.value.mark,
-    status: localUserDetail.value.status ? 1 : 0,
-    level: localUserDetail.value.level,
-    group_id: localUserDetail.value.group_id
-  }
-  return formData
-}
-defineExpose({
-  getFormData
-})
-
-const getLabel = (list, id) => {
-  // 检查列表是否存在且为数组类型,若不满足条件则返回null
-  if (!list || !Array.isArray(list)) return null
-
-  // 在列表中查找匹配给定ID的元素
-  const item = list.find((e) => e.value === id)
-
-  // 如果找到匹配的元素,则返回该元素的标签;否则,返回null
-  return item ? item.label : null
-}
-</script>
-
-<template>
-  <ElTabs type="border-card" @tab-change="changeTable">
-    <ElTabPane label="用户信息">
-      <div v-show="modelValue" class="p-20px">
-        <el-form label-width="110px" inline :rules="rules" :model="localUserDetail">
-          <ElDivider content-position="left"
-            ><span class="font-size-16px">基本信息</span></ElDivider
-          >
-          <el-form-item class="w-50%" label="用户编号" prop="uid">
-            <el-input disabled v-model="localUserDetail.uid" />
-          </el-form-item>
-
-          <el-form-item class="w-50%" label="真实姓名" prop="real_name">
-            <el-input v-model="localUserDetail.real_name" />
-          </el-form-item>
-
-          <el-form-item class="w-50%" label="手机号" prop="phone">
-            <el-input type="number" v-model="localUserDetail.phone" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="生日" prop="birthday">
-            <el-date-picker type="date" v-model="localUserDetail.birthday" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="身份证" prop="card_id">
-            <el-input v-model="localUserDetail.card_id" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="性别" prop="sex">
-            <el-select v-model="localUserDetail.sex">
-              <el-option label="保密" :value="0" />
-              <el-option label="男" :value="1" />
-              <el-option label="女" :value="2" />
-            </el-select>
-          </el-form-item>
-          <ElDivider content-position="left"><span class="font-size-16px">密码</span></ElDivider>
-          <el-form-item class="w-50%" label="登录密码" prop="pwd">
-            <InputPassword class="w-100%" strength v-model="localUserDetail.pwd" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="确认密码" prop="true_pwd">
-            <InputPassword class="w-100%" strength v-model="localUserDetail.true_pwd" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="交易密码" prop="trade_pwd">
-            <el-input
-              maxlength="6"
-              minlength="6"
-              type="password"
-              v-model="localUserDetail.trade_pwd"
-            />
-          </el-form-item>
-          <el-form-item class="w-50%" label="确认交易密码" prop="true_trade_pwd">
-            <el-input
-              maxlength="6"
-              minlength="6"
-              type="password"
-              v-model="localUserDetail.true_trade_pwd"
-            />
-          </el-form-item>
-          <ElDivider class="w-50%" content-position="left"
-            ><span class="font-size-16px">用户概况</span></ElDivider
-          >
-          <el-form-item class="w-50%" label="用户分组" prop="group_id">
-            <el-select v-model="localUserDetail.group_id">
-              <el-option
-                v-for="(item, index) in groupList"
-                :key="index"
-                :label="item.label"
-                :value="item.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item class="w-50%" label="会员等级" prop="level">
-            <el-select v-model="localUserDetail.level">
-              <el-option
-                v-for="(item, index) in levelList"
-                :key="index"
-                :label="item.label"
-                :value="item.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item class="w-50%" label="推广员" prop="is_promoter">
-            <el-switch v-model="localUserDetail.is_promoter" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="状态" prop="status">
-            <el-switch v-model="localUserDetail.status" />
-          </el-form-item>
-          <el-form-item class="w-50%" label="推荐人ID" prop="spread_uid">
-            <el-input v-model="localUserDetail.spread_uid" />
-          </el-form-item>
-          <el-form-item class="w-100%" label="备注" prop="mark">
-            <el-input type="textarea" :rows="2" v-model="localUserDetail.mark" />
-          </el-form-item>
-        </el-form>
-      </div>
-      <div v-show="!modelValue" class="p-20px">
-        <ElDivider content-position="left"><span class="font-size-16px">基本信息</span></ElDivider>
-        <el-descriptions>
-          <el-descriptions-item label="用户编号">{{ localUserDetail.uid }}</el-descriptions-item>
-          <el-descriptions-item label="真实姓名">{{
-            localUserDetail.real_name
-          }}</el-descriptions-item>
-          <el-descriptions-item label="手机号">{{ localUserDetail.phone }}</el-descriptions-item>
-          <el-descriptions-item label="生日">
-            {{ localUserDetail.birthday ? formatToDate(localUserDetail.birthday) : '' }}
-          </el-descriptions-item>
-          <el-descriptions-item label="身份证">
-            {{ localUserDetail.card_id }}
-          </el-descriptions-item>
-          <el-descriptions-item label="性别">
-            <span v-if="localUserDetail.sex === 0">保密</span>
-            <span v-else-if="localUserDetail.sex === 1">男</span>
-            <span v-else-if="localUserDetail.sex === 2">女</span>
-          </el-descriptions-item>
-        </el-descriptions>
-        <ElDivider content-position="left"><span class="font-size-16px">密码</span></ElDivider>
-        <el-descriptions>
-          <el-descriptions-item label="登录密码"> ****** </el-descriptions-item>
-          <el-descriptions-item label="交易密码"> ****** </el-descriptions-item>
-        </el-descriptions>
-        <ElDivider content-position="left"><span class="font-size-16px">用户概况</span></ElDivider>
-        <el-descriptions>
-          <el-descriptions-item label="用户分组">
-            {{ getLabel(groupList, localUserDetail.group_id) || '' }}
-          </el-descriptions-item>
-          <el-descriptions-item label="会员等级">
-            {{ getLabel(levelList, localUserDetail.level) || '' }}
-          </el-descriptions-item>
-          <el-descriptions-item label="推广员">
-            <ElTag v-if="localUserDetail.is_promoter" size="small" type="success">开启</ElTag>
-            <ElTag v-else size="small" type="danger">关闭</ElTag>
-          </el-descriptions-item>
-          <el-descriptions-item label="状态">
-            <ElTag v-if="localUserDetail.status" size="small" type="success">启用</ElTag>
-            <ElTag v-else size="small" type="danger">禁用</ElTag>
-          </el-descriptions-item>
-          <el-descriptions-item label="推荐人ID">
-            {{ localUserDetail.spread_uid }}
-          </el-descriptions-item>
-          <el-descriptions-item label="备注">
-            {{ localUserDetail.mark }}
-          </el-descriptions-item>
-        </el-descriptions>
-      </div>
-    </ElTabPane>
-    <ElTabPane
-      :label="item.name"
-      :name="item.type"
-      v-for="(item, index) in userRecordList"
-      :key="index"
-    >
-      <ElTable v-loading="item.loading" :data="item.data" stripe>
-        <ElTableColumn
-          v-for="(ls, ind) in item.columns"
-          :key="ind"
-          :prop="ls.field"
-          :label="ls.label"
-        />
-      </ElTable>
-      <ElPagination
-        @change="changePage($event, item)"
-        v-model:current-page="item.page"
-        v-model:page-size="item.limit"
-        layout="sizes, prev, pager, next, jumper, ->, total"
-        :page-sizes="[10, 20, 30, 40, 50, 100]"
-        class="mt-10px"
-        :total="item.total"
-      />
-    </ElTabPane>
-  </ElTabs>
-</template>
-<style lang="less" scoped>
-:deep(.el-form-item) {
-  &.el-form-item {
-    margin-right: 0 !important;
-  }
-}
-</style>

+ 65 - 391
src/views/Goods/list/index.vue

@@ -1,31 +1,15 @@
 <script setup lang="tsx">
-import { onMounted, reactive, ref, unref, nextTick, watch } from 'vue'
-import {
-  getUserList,
-  getUserGroup,
-  getUserLevel,
-  addUserData,
-  // delUserData,
-  putUserData,
-  clearUserLevel
-} from '@/api/user'
-import type { userData } from '@/api/user/types'
+import { onMounted, reactive, ref, unref, watch } from 'vue'
+import { getProduct, getProductCategory } from '@/api/goods'
 import { useI18n } from '@/hooks/web/useI18n'
 import { Search } from '@/components/Search'
 import { FormSchema } from '@/components/Form'
 import { ContentWrap } from '@/components/ContentWrap'
-import Detail from './components/Detail.vue'
-import Write from './components/Write.vue'
-import { Dialog } from '@/components/Dialog'
 import { BaseButton } from '@/components/Button'
 import { userSearch } from '@/api/user/types'
 import {
-  ElSelect,
-  ElOption,
-  ElMessage,
+  ElSwitch,
   ElDivider,
-  ElText,
-  ElMessageBox,
   ElDropdown,
   ElDropdownMenu,
   ElDropdownItem,
@@ -34,17 +18,14 @@ import {
   ElPagination
 } from 'element-plus'
 import { searchTime } from '@/utils/searchTime'
-import { formatToDate } from '@/utils/dateUtil'
 import { TableImage } from '@/components/tableImage'
 import BatchSet from './components/BatchSet.vue'
 import { useSearch } from '@/hooks/web/useSearch'
 import Icon from '@/components/Icon/src/Icon.vue'
-const { searchRegister, searchMethods } = useSearch()
-const { setSchema } = searchMethods
-searchTime.field = 'user_time'
+import { useRouter } from 'vue-router'
+const { push } = useRouter()
+const { searchRegister } = useSearch()
 const { t } = useI18n()
-const groupList = ref<any[]>([])
-const levelList = ref<any[]>([])
 const currentPage = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
@@ -52,54 +33,21 @@ const dataList = ref<any[]>([])
 const loading = ref(false)
 const getList = async () => {
   loading.value = true
-  const { data } = await getUserList({
+  const { data } = await getProduct({
     page: unref(currentPage) || 1,
     limit: unref(pageSize) || 10,
     ...unref(searchParams)
   })
   loading.value = false
-  dataList.value = data.list
+  dataList.value = data.list.map((item) => {
+    item.is_show_bol = item.is_show == 1 ? true : false
+    return item
+  })
   total.value = data.count
 }
 onMounted(async () => {
   try {
     getList()
-    const [levelResponse, groupResponse] = await Promise.all([
-      getUserLevel({ page: 1, limit: 999 }),
-      getUserGroup({ page: 1, limit: 999 })
-    ])
-
-    // 处理 userLevel 数据
-    const processLevelData = (data) =>
-      data.list.map((res) => ({
-        label: res.name,
-        value: res.id
-      }))
-
-    // 处理 userGroup 数据
-    const processGroupData = (data) =>
-      data.list.map((res) => ({
-        label: res.group_name,
-        value: res.id
-      }))
-
-    levelList.value = processLevelData(levelResponse.data)
-    groupList.value = processGroupData(groupResponse.data)
-
-    await nextTick()
-
-    setSchema([
-      {
-        field: 'level',
-        path: 'componentProps.options',
-        value: levelList.value
-      },
-      {
-        field: 'group_id',
-        path: 'componentProps.options',
-        value: groupList.value
-      }
-    ])
   } catch (error) {
     console.error('Failed to fetch data:', error)
     // 可以在这里添加更详细的错误处理逻辑,例如显示错误信息给用户
@@ -109,188 +57,28 @@ onMounted(async () => {
 const searchSchema = reactive<FormSchema[]>([
   {
     field: 'nickname',
-    label: '搜索',
-    component: 'Input',
+    label: '商品分类',
+    component: 'Cascader',
     componentProps: {
-      placeholder: '姓名、手机、UID查询',
-      slots: {
-        prepend: () => {
-          return (
-            <>
-              <ElSelect
-                v-model={searchParams.value.field_key}
-                placeholder="全部"
-                style={'width: 80px'}
-              >
-                <ElOption label="全部" value="all" />
-                <ElOption label="昵称" value="nickname" />
-                <ElOption label="手机" value="phone" />
-                <ElOption label="UID" value="uid" />
-              </ElSelect>
-            </>
-          )
+      placeholder: '请选择商品分类',
+      props: {
+        lazy: true,
+        lazyLoad(node, resolve) {
+          getProductCategory({ page: 1, limit: 100, pid: node.value || -1 }).then((res) => {
+            console.log(res)
+            const nodes = res.data.map((item) => {
+              return {
+                value: item.id,
+                label: item.name,
+                leaf: !item.hasChildren
+              }
+            })
+            resolve(nodes)
+          })
         }
       }
     }
   },
-  {
-    field: 'status',
-    label: '用户状态',
-    component: 'Select',
-    componentProps: {
-      placeholder: '全部',
-      options: [
-        {
-          label: '全部',
-          value: ''
-        },
-        {
-          label: '封禁',
-          value: 0
-        },
-        {
-          label: '正常',
-          value: 1
-        }
-      ]
-    }
-  },
-  {
-    field: 'level',
-    label: '用户等级',
-    component: 'Select',
-    componentProps: {
-      placeholder: '全部'
-    }
-  },
-  {
-    field: 'group_id',
-    label: '用户分组',
-    component: 'Select',
-    componentProps: {
-      placeholder: '全部'
-    }
-  },
-  {
-    field: 'sex',
-    label: '性别',
-    value: '',
-    component: 'Select',
-    componentProps: {
-      placeholder: '全部',
-      options: [
-        {
-          label: '全部',
-          value: ''
-        },
-        {
-          label: '保密',
-          value: 0
-        },
-        {
-          label: '男',
-          value: 1
-        },
-        {
-          label: '女',
-          value: 2
-        }
-      ]
-    }
-  },
-  {
-    field: 'is_promoter',
-    label: '推广权限',
-    component: 'Select',
-    componentProps: {
-      placeholder: '全部',
-
-      options: [
-        {
-          label: '全部',
-          value: ''
-        },
-        {
-          label: '关闭',
-          value: 0
-        },
-        {
-          label: '开启',
-          value: 1
-        }
-      ]
-    }
-  },
-  {
-    field: 'now_money',
-    label: '余额排序',
-    component: 'Select',
-    componentProps: {
-      placeholder: '默认',
-      options: [
-        {
-          label: '默认',
-          value: ''
-        },
-        {
-          label: '降序',
-          value: 'desc'
-        },
-        {
-          label: '升序',
-          value: 'asc'
-        }
-      ]
-    }
-  },
-  // {
-  //   field: 'country',
-  //   label: '国家',
-  //   component: 'Select',
-  //   value: '',
-  //   componentProps: {
-  //     options: [
-  //       {
-  //         label: '全部',
-  //         value: ''
-  //       },
-  //       {
-  //         label: '国内',
-  //         value: 'domestic'
-  //       },
-  //       {
-  //         label: '国外',
-  //         value: 'abroad'
-  //       }
-  //     ]
-  //   }
-  // },
-  {
-    field: 'user_time_type',
-    label: '时间筛选类型',
-    value: 'all',
-    component: 'Select',
-    componentProps: {
-      options: [
-        {
-          label: '全部',
-          value: 'all'
-        },
-        {
-          label: '访问时间不在',
-          value: 'visitno'
-        },
-        {
-          label: '访问时间',
-          value: 'visit'
-        },
-        {
-          label: '添加时间',
-          value: 'add_time'
-        }
-      ]
-    }
-  },
   searchTime
 ])
 
@@ -315,114 +103,16 @@ const setSearchParams = (data: any) => {
   getList()
 }
 
-const dialogVisible = ref(false)
-const currentRow = ref()
-const dialogTitle = ref('')
-const actionType = ref('')
-const writeRef = ref<ComponentRef<typeof Write>>()
-const saveLoading = ref(false)
-const action = async (row: any, type: string) => {
-  dialogTitle.value = t('exampleDemo.edit')
-  actionType.value = type
-  currentRow.value = row.uid
-  dialogVisible.value = true
-}
-
-const AddAction = () => {
-  dialogTitle.value = t('exampleDemo.add')
-  currentRow.value = undefined
-  dialogVisible.value = true
-  actionType.value = ''
-}
-
-const delLevel = async (uid: any) => {
-  ElMessageBox.confirm('清除后无法恢复,是否清除用户等级?', {
-    confirmButtonText: '清除',
-    cancelButtonText: '取消',
-    type: 'warning'
-  })
-    .then(async () => {
-      const res = await clearUserLevel(uid)
-      if (res.status === 200) {
-        ElMessage.success(res.msg)
-        getList()
-      }
-    })
-    .catch(() => {})
+const action = async (type: string, id?: number) => {
+  push(`/goods/edit/${type}/${id || 0}`)
 }
 
-// const delAction = (row: any) => {
-//   ElMessageBox.confirm('删除后无法恢复,是否删除用户?', {
-//     confirmButtonText: '删除',
-//     cancelButtonText: '取消',
-//     type: 'warning'
-//   })
-//     .then(async () => {
-//       const re = await delUserData(row.uid)
-//       if (re) {
-//         ElMessage({
-//           showClose: true,
-//           message: '删除成功',
-//           type: 'success'
-//         })
-//       }
-//       await getList()
-//     })
-//     .catch(() => {})
+// const drawerUser = ref(false)
+// const drawerUserId = ref()
+// const detail = (item) => {
+//   drawerUser.value = true
+//   drawerUserId.value = item.uid
 // }
-const save = async () => {
-  const write = unref(writeRef)
-  const formData = await write?.submit()
-  if (formData) {
-    saveLoading.value = true
-    const data: userData = {
-      uid: formData.uid,
-      real_name: formData.real_name,
-      phone: formData.phone,
-      pwd: formData.pwd,
-      true_pwd: formData.true_pwd,
-      trade_pwd: formData.trade_pwd,
-      true_trade_pwd: formData.true_trade_pwd,
-      sex: formData.sex,
-      spread_uid: formData.spread_uid,
-      is_promoter: formData.is_promoter ? 1 : 0,
-      card_id: formData.card_id,
-      birthday: formatToDate(formData.birthday),
-      mark: formData.mark,
-      status: formData.status ? 1 : 0,
-      level: formData.level,
-      group_id: formData.group_id
-    }
-    try {
-      let res: any = {}
-      // console.log(formData, 'formData')
-      if (actionType.value === 'edit') {
-        res = await putUserData(data)
-      } else if (actionType.value === '') {
-        res = await addUserData(data)
-      }
-      if (res?.status == 200) {
-        ElMessage({
-          showClose: true,
-          message: '保存成功',
-          type: 'success'
-        })
-        getList()
-        dialogVisible.value = false
-      }
-    } catch (error) {
-      console.log(error)
-    } finally {
-      saveLoading.value = false
-    }
-  }
-}
-const drawerUser = ref(false)
-const drawerUserId = ref()
-const detail = (item) => {
-  drawerUser.value = true
-  drawerUserId.value = item.uid
-}
 const selectChange = (res) => {
   selectList.value = res
 }
@@ -438,6 +128,12 @@ watch(currentPage, (val) => {
     getList()
   }
 })
+const changeStatus = async (val: any, id: number) => {
+  console.log(val, id, 'val')
+}
+const delData = (res) => {
+  console.log(res)
+}
 </script>
 
 <template>
@@ -448,34 +144,38 @@ watch(currentPage, (val) => {
         @reset="setSearchParams"
         @search="setSearchParams"
         @register="searchRegister"
-        show-expand
         buttonPosition="right"
-        expand-field="status"
       />
     </div>
     <div class="mb-10px">
-      <BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
+      <BaseButton type="primary" @click="action('add')">{{ t('exampleDemo.add') }}</BaseButton>
       <BaseButton @click="allSet" :disabled="selectList.length == 0">批量设置</BaseButton>
     </div>
-    <ElTable @selection-change="selectChange" :loading="loading" node-key="uid" :data="dataList">
+    <ElTable @selection-change="selectChange" :loading="loading" node-key="id" :data="dataList">
       <ElTableColumn type="selection" prop="selection" />
-      <ElTableColumn prop="uid" headerAlign="center" align="center" label="ID" width="70" />
-      <ElTableColumn prop="avatar" headerAlign="center" align="center" label="头像" width="80">
+      <ElTableColumn prop="id" headerAlign="center" align="center" label="ID" width="70" />
+      <ElTableColumn prop="avatar" headerAlign="center" align="center" label="商品图片" width="80">
         <template #default="{ row }">
-          <TableImage :src="row.avatar" alt="头像" />
+          <TableImage :src="row.image" alt="商品图片" />
         </template>
       </ElTableColumn>
-      <ElTableColumn prop="nickname" label="昵称" min-width="120">
+      <ElTableColumn prop="name" label="商品名称" minWidth="180" />
+      <ElTableColumn prop="price" label="商品售价" minWidth="80" />
+      <ElTableColumn prop="stock" label="库存" minWidth="80" />
+      <ElTableColumn prop="sales" label="销量" minWidth="80" />
+      <ElTableColumn prop="is_show" label="上下架" minWidth="80">
         <template #default="{ row }">
-          {{ row.nickname }}<br />
-          <ElText v-if="row.delete_time" type="danger">{{ row.delete }}</ElText>
+          <ElSwitch
+            active-text="上架"
+            inactive-text="下架"
+            inline-prompt
+            :active-value="1"
+            :inactive-value="0"
+            v-model="row.is_show"
+            @change="(val) => changeStatus(val, row.id)"
+          />
         </template>
       </ElTableColumn>
-      <ElTableColumn prop="vip_name" label="等级" min-width="80" />
-      <ElTableColumn prop="group_id" label="分组" min-width="80" />
-      <ElTableColumn prop="phone" label="手机号" minWidth="120" />
-      <ElTableColumn prop="spread_uid_nickname" label="推荐人" min-width="80" />
-      <ElTableColumn prop="now_money" label="余额" min-width="80" />
       <ElTableColumn
         prop="action"
         label="操作"
@@ -486,13 +186,10 @@ watch(currentPage, (val) => {
         fixed="right"
       >
         <template #default="{ row }">
-          <BaseButton link size="small" type="primary" @click="action(row, 'edit')">
+          <BaseButton link size="small" type="primary" @click="action('edit', row.id)">
             编辑
           </BaseButton>
-          <template v-if="!row.delete_time">
-            <ElDivider direction="vertical" />
-            <BaseButton link size="small" type="success" @click="detail(row)"> 详情 </BaseButton>
-          </template>
+          <BaseButton link size="small" type="primary" @click="delData(row)" />
           <ElDivider direction="vertical" />
           <ElDropdown>
             <BaseButton link size="small" type="info">
@@ -500,9 +197,9 @@ watch(currentPage, (val) => {
             </BaseButton>
             <template #dropdown
               ><ElDropdownMenu>
-                <ElDropdownItem @click="delLevel(row.uid)">清除等级</ElDropdownItem>
-              </ElDropdownMenu></template
-            >
+                <ElDropdownItem>移到回收站 </ElDropdownItem>
+              </ElDropdownMenu>
+            </template>
           </ElDropdown>
         </template>
       </ElTableColumn>
@@ -518,34 +215,11 @@ watch(currentPage, (val) => {
   </ContentWrap>
 
   <BatchSet
-    :group-list="groupList"
     :where="searchParams"
     v-model="dialogSet"
     :select-list="selectList"
     @confirm="getList()"
   />
-  <Detail
-    :level-list="levelList"
-    :group-list="groupList"
-    v-model="drawerUser"
-    :uid="drawerUserId"
-    @confirm="getList()"
-  />
-  <Dialog v-model="dialogVisible" max-height="500px" :title="dialogTitle" width="700px">
-    <Write
-      ref="writeRef"
-      :current-row="currentRow"
-      :level-list="levelList"
-      :group-list="groupList"
-      :type="actionType"
-    />
-    <template #footer>
-      <BaseButton type="primary" :loading="saveLoading" @click="save">
-        {{ t('exampleDemo.save') }}
-      </BaseButton>
-      <BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
-    </template>
-  </Dialog>
 </template>
 <style lang="less">
 .searchBox {

+ 5 - 5
src/views/Staff/designer/index.vue

@@ -9,11 +9,10 @@ import { FormSchema } from '@/components/Form'
 import { ContentWrap } from '@/components/ContentWrap'
 import Write from './components/Write.vue'
 import { BaseButton } from '@/components/Button'
-import { userLevelSearch } from '@/api/user/types'
 import { ElMessage, ElDivider, ElMessageBox, ElTag } from 'element-plus'
 import { Dialog } from '@/components/Dialog'
 import { TableImage } from '@/components/tableImage'
-import { designerData } from '@/api/staff/types'
+import { designerData, designerSearch } from '@/api/staff/types'
 import { formatToDate } from '@/utils/dateUtil'
 const { t } = useI18n()
 
@@ -194,9 +193,10 @@ const searchSchema = reactive<FormSchema[]>([
   }
 ])
 
-const searchParams = ref<userLevelSearch>({
-  title: '',
-  is_show: ''
+const searchParams = ref<designerSearch>({
+  gender: '',
+  tag_list: '',
+  name: ''
 })
 const setSearchParams = (data: any) => {
   searchParams.value = data

+ 318 - 0
src/views/Staff/salesman/components/Write.vue

@@ -0,0 +1,318 @@
+<script setup lang="tsx">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, watch, ref, onMounted, unref } from 'vue'
+import { useValidator } from '@/hooks/web/useValidator'
+import { cloneDeep } from 'lodash-es'
+import { ElInputTag, ElAutocomplete } from 'element-plus'
+import { getMapSearch } from '@/api/system/admin'
+import { UpImgButtom } from '@/components/UpFile'
+import { mapAddressData } from '@/api/system/admin/types'
+import { getConfigKey } from '@/api/system/admin'
+import { UserButtom } from '@/components/UserList'
+const { required, phone } = useValidator()
+const mapKey = ref('')
+onMounted(async () => {
+  const res = await getConfigKey('tengxun_map_key')
+  if (res) {
+    mapKey.value = res.data.tengxun_map_key
+  }
+})
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<any>,
+    default: () => null
+  }
+})
+const formSchema = ref<FormSchema[]>([
+  {
+    field: 'uid',
+    label: '用户',
+    component: 'CheckboxGroup',
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <UserButtom
+                v-model={data.uid}
+                onChangeUser={(res) => {
+                  checkedUser(res)
+                }}
+              ></UserButtom>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'name',
+    label: '姓名',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入姓名'
+    }
+  },
+  {
+    field: 'avatar',
+    label: '头像',
+    component: 'CheckboxGroup',
+    componentProps: {
+      placeholder: '选择头像'
+    },
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <UpImgButtom v-model={data.avatar}></UpImgButtom>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'phone',
+    label: '手机号',
+    component: 'Input',
+    componentProps: {
+      type: 'number',
+      placeholder: '请输入手机号'
+    }
+  },
+
+  {
+    field: 'detail_address',
+    label: '省市区地址',
+    colProps: {
+      span: 24
+    },
+    component: 'Input',
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <ElAutocomplete
+                v-model={data.detail_address}
+                value-key="label"
+                onSelect={(val: any) => {
+                  selectAddress(val)
+                }}
+                fetch-suggestions={(val, cb) => {
+                  searchAddress(val, cb)
+                }}
+              >
+                {{
+                  default: ({ item }) => {
+                    return (
+                      <>
+                        <div class="lh-none mt-10px">{item.label}</div>
+                        <div class="font-size-12px lh-none  mt-5px pb-10px b-b-solid b-b-light b-b-1px">
+                          {item.address}
+                        </div>
+                      </>
+                    )
+                  }
+                }}
+              </ElAutocomplete>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'province',
+    label: '省份',
+    component: 'Input',
+    hidden: true
+  },
+  {
+    field: 'city',
+    label: '市',
+    hidden: true,
+    component: 'Input'
+  },
+  {
+    field: 'area',
+    label: '区',
+    hidden: true,
+    component: 'Input'
+  },
+  {
+    field: 'longitude',
+    label: '经度',
+    hidden: true,
+    component: 'Input'
+  },
+  {
+    field: 'latitude',
+    label: '纬度',
+    hidden: true,
+    component: 'Input'
+  },
+  {
+    field: 'id',
+    hidden: true,
+    component: 'Input'
+  }
+])
+
+const rules = ref({
+  uid: [required('请选择用户')],
+  avatar: [required('请上传头像')],
+  phone: [required('请填写手机号'), phone()],
+  detail_address: [
+    {
+      required: true,
+      validator: (_, val, callback) => {
+        if (!actionAddress.value || !val) {
+          callback(new Error('请选择地址'))
+        } else {
+          callback()
+        }
+      }
+    }
+  ],
+  name: [required('请填写等级名称')]
+})
+
+const { formRegister, formMethods } = useForm()
+const { setValues, getFormData, getElFormExpose } = formMethods
+
+const submit = async () => {
+  const elForm = await getElFormExpose()
+  const valid = await elForm?.validate().catch((err) => {
+    console.log(err)
+  })
+  if (valid) {
+    const formData = await getFormData()
+    return formData
+  }
+}
+
+defineExpose({
+  submit
+})
+const checkedUser = async (res: any) => {
+  setValues({
+    uid: res.uid,
+    name: res.real_name,
+    avatar: [res.avatar],
+    birth_day_time: res.birthday,
+    phone: res.phone,
+    gender: res.sex || 0
+  })
+}
+const actionAddress = ref<mapAddressData>()
+const searchAddress = async (queryString: string, cb: (arg: any) => void) => {
+  if (queryString) {
+    const { data } = await getMapSearch({
+      key: unref(mapKey),
+      keyword: queryString
+    })
+    cb(
+      data.map((item): mapAddressData => {
+        return {
+          adcode: item.adcode,
+          label: item.title,
+          address: item.address,
+          lng: item.location.lng,
+          lat: item.location.lat,
+          province: item.province,
+          city: item.city,
+          district: item.district
+        }
+      })
+    )
+  }
+}
+const selectAddress = (item: mapAddressData) => {
+  if (!item || !item.adcode || !item.lng || !item.lat) {
+    console.error('Invalid input item:', item)
+    return
+  }
+
+  const adcodeStr = String(item.adcode).padStart(6, '0') // 确保 adcode 至少有6位
+  const province = adcodeStr.slice(0, 2)
+  const city = adcodeStr.slice(2, 4)
+  const area = adcodeStr
+
+  setValues({
+    province: `${province}`,
+    city: `${province}${city}00000000`,
+    area: `${area}000000`,
+    longitude: item.lng,
+    latitude: item.lat
+  })
+
+  actionAddress.value = item
+  center.value = {
+    lng: item.lng,
+    lat: item.lat
+  }
+  geometries.value = [{ styleId: 'marker', position: { lat: item.lat, lng: item.lng } }]
+}
+const center = ref({ lat: 28.655759, lng: 121.420808 })
+const geometries = ref([{ styleId: 'marker', position: { lat: 28.655759, lng: 121.420808 } }])
+
+watch(
+  () => props.currentRow,
+  async (value) => {
+    if (!value) return
+    const currentRow = cloneDeep(value)
+    actionAddress.value = {
+      adcode: currentRow.area,
+      label: currentRow.detail_address,
+      lng: currentRow.longitude,
+      lat: currentRow.latitude
+    }
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+</script>
+
+<template>
+  <div class="flex flex-col">
+    <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+    <TlbsMap
+      v-if="mapKey"
+      ref="map"
+      :api-key="mapKey"
+      :zoom="15"
+      :center="center"
+      :control="{
+        scale: {},
+        zoom: {
+          position: 'topRight'
+        }
+      }"
+    >
+      <TlbsMultiMarker
+        :geometries="geometries"
+        :styles="{
+          marker: {
+            width: 20,
+            height: 30,
+            anchor: { x: 10, y: 30 }
+          }
+        }"
+      />
+    </TlbsMap>
+  </div>
+</template>
+<style lang="less" scoped>
+:deep(.el-table__row) {
+  .list {
+    width: 50px;
+    text-align: center;
+  }
+}
+</style>

+ 275 - 0
src/views/Staff/salesman/index.vue

@@ -0,0 +1,275 @@
+<script setup lang="tsx">
+import { reactive, ref, unref } from 'vue'
+import {
+  getStaffSalesperson,
+  addStaffSalesperson,
+  putStaffSalesperson,
+  delStaffSalesperson
+} from '@/api/staff'
+import { useTable } from '@/hooks/web/useTable'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table, TableColumn } from '@/components/Table'
+import { Search } from '@/components/Search'
+import { FormSchema } from '@/components/Form'
+import { ContentWrap } from '@/components/ContentWrap'
+import Write from './components/Write.vue'
+import { BaseButton } from '@/components/Button'
+import { userLevelSearch } from '@/api/user/types'
+import { ElMessage, ElDivider, ElMessageBox, ElTag } from 'element-plus'
+import { Dialog } from '@/components/Dialog'
+import { TableImage } from '@/components/tableImage'
+import { SalespersonData } from '@/api/staff/types'
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { pageSize, currentPage } = tableState
+    const res = await getStaffSalesperson({
+      page: unref(currentPage) || 1,
+      limit: unref(pageSize) || 10,
+      ...unref(searchParams)
+    })
+    return {
+      list: res.data.list,
+      total: res.data.count || 0
+    }
+  }
+})
+const { dataList, loading, total, currentPage, pageSize } = tableState
+const { getList } = tableMethods
+
+const tableColumns = reactive<TableColumn[]>([
+  {
+    field: 'id',
+    label: 'ID',
+    align: 'center',
+    width: 70
+  },
+  {
+    field: 'avatar',
+    label: '头像',
+    width: 80,
+    slots: {
+      default: ({ row }: any) => {
+        return (
+          <>
+            <TableImage src={row.avatar} />
+          </>
+        )
+      }
+    }
+  },
+  {
+    field: 'name',
+    label: '姓名',
+    minWidth: 100
+  },
+  {
+    field: 'phone',
+    label: '手机号',
+    minWidth: 120
+  },
+  {
+    field: 'area',
+    label: '区域',
+    minWidth: 200,
+    slots: {
+      default: ({ row }: any) => {
+        return (
+          <>
+            {row.province_text} {row.city_text} {row.area_text}
+          </>
+        )
+      }
+    }
+  },
+  {
+    field: 'detail_address',
+    label: '定位',
+    minWidth: 150
+  },
+  {
+    field: 'action',
+    label: t('userDemo.action'),
+    width: 110,
+    align: 'center',
+    headerAlign: 'center',
+    fixed: 'right',
+    slots: {
+      default: (data: any) => {
+        const row = data.row
+        return (
+          <>
+            <BaseButton link size="small" type="primary" onClick={() => action(row, 'edit')}>
+              {t('exampleDemo.edit')}
+            </BaseButton>
+            <ElDivider direction="vertical" />
+            <BaseButton link size="small" type="danger" onClick={() => delAction(row)}>
+              {t('exampleDemo.del')}
+            </BaseButton>
+          </>
+        )
+      }
+    }
+  }
+])
+
+const searchSchema = reactive<FormSchema[]>([
+  {
+    field: 'name',
+    label: '姓名',
+    component: 'Input',
+    value: '',
+    componentProps: {
+      placeholder: '请输入姓名'
+    }
+  }
+])
+
+const searchParams = ref({
+  name: ''
+})
+const setSearchParams = (data: any) => {
+  searchParams.value = data
+  getList()
+}
+
+const dialogVisible = ref(false)
+const currentRow = ref()
+const dialogTitle = ref('')
+const actionType = ref('')
+const writeRef = ref<ComponentRef<typeof Write>>()
+const saveLoading = ref(false)
+const action = async (row: any, type: string) => {
+  dialogTitle.value = t(type === 'edit' ? 'exampleDemo.edit' : 'exampleDemo.detail')
+  actionType.value = type
+  currentRow.value = {
+    name: row.name,
+    uid: row.uid,
+    id: row.id,
+    phone: row.phone,
+    avatar: [row.avatar],
+    province: row.province,
+    city: row.city,
+    area: row.area,
+    longitude: row.longitude,
+    latitude: row.latitude,
+    detail_address: row.detail_address
+  }
+  dialogVisible.value = true
+}
+
+const AddAction = () => {
+  dialogTitle.value = t('exampleDemo.add')
+  currentRow.value = undefined
+  dialogVisible.value = true
+  actionType.value = ''
+}
+
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    saveLoading.value = true
+    const data: SalespersonData = {
+      name: formData.name,
+      uid: formData.uid,
+      phone: formData.phone,
+      avatar: formData.avatar[0],
+      province: formData.province,
+      city: formData.city,
+      area: formData.area,
+      longitude: formData.longitude,
+      latitude: formData.latitude,
+      detail_address: formData.detail_address
+    }
+    try {
+      let res: any = {}
+      if (actionType.value === 'edit') {
+        data.id = formData.id
+        res = await putStaffSalesperson(data)
+      } else if (actionType.value === '') {
+        res = await addStaffSalesperson(data)
+      }
+      if (res?.status === 200) {
+        ElMessage({
+          showClose: true,
+          message: '保存成功',
+          type: 'success'
+        })
+        getList()
+        dialogVisible.value = false
+      }
+    } catch (error) {
+      console.log(error)
+    } finally {
+      saveLoading.value = false
+    }
+  }
+}
+
+const delAction = (row: any) => {
+  ElMessageBox.confirm('删除后无法恢复,是否删除?', {
+    confirmButtonText: '删除',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      const re = await delStaffSalesperson(row.id)
+      if (re) {
+        ElMessage({
+          showClose: true,
+          message: '删除成功',
+          type: 'success'
+        })
+      }
+      await getList()
+    })
+    .catch(() => {})
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <div class="searchBox">
+      <Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
+    </div>
+    <div class="mb-10px">
+      <BaseButton type="primary" @click="AddAction">{{ t('exampleDemo.add') }}</BaseButton>
+    </div>
+    <Table
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :columns="tableColumns"
+      default-expand-all
+      node-key="id"
+      stripe
+      :data="dataList"
+      :loading="loading"
+      @register="tableRegister"
+      :pagination="{
+        total
+      }"
+    />
+  </ContentWrap>
+
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="900px" max-height="700px">
+    <Write v-if="actionType !== 'detail'" ref="writeRef" :current-row="currentRow" />
+    <template #footer>
+      <BaseButton
+        v-if="actionType !== 'detail'"
+        type="primary"
+        :loading="saveLoading"
+        @click="save"
+      >
+        {{ t('exampleDemo.save') }}
+      </BaseButton>
+      <BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
+    </template>
+  </Dialog>
+</template>
+<style lang="less">
+.searchBox {
+  max-width: 1000px;
+}
+</style>

+ 5 - 6
src/views/Staff/worker/index.vue

@@ -9,11 +9,10 @@ import { FormSchema } from '@/components/Form'
 import { ContentWrap } from '@/components/ContentWrap'
 import Write from './components/Write.vue'
 import { BaseButton } from '@/components/Button'
-import { userLevelSearch } from '@/api/user/types'
 import { ElMessage, ElDivider, ElMessageBox, ElTag } from 'element-plus'
 import { Dialog } from '@/components/Dialog'
 import { TableImage } from '@/components/tableImage'
-import { designerData } from '@/api/staff/types'
+import { designerData, designerSearch } from '@/api/staff/types'
 import { formatToDate } from '@/utils/dateUtil'
 const { t } = useI18n()
 
@@ -193,9 +192,10 @@ const searchSchema = reactive<FormSchema[]>([
   }
 ])
 
-const searchParams = ref<userLevelSearch>({
-  title: '',
-  is_show: ''
+const searchParams = ref<designerSearch>({
+  gender: '',
+  tag_list: '',
+  name: ''
 })
 const setSearchParams = (data: any) => {
   searchParams.value = data
@@ -276,7 +276,6 @@ const save = async () => {
         getList()
         dialogVisible.value = false
       }
-      saveLoading.value = false
     } catch (error) {
       console.log(error)
     } finally {

+ 1 - 1
src/views/User/group/index.vue

@@ -94,7 +94,7 @@ const delAction = async (row: any) => {
     .catch(() => {})
 }
 
-const save = async (item) => {
+const save = (item) => {
   ElMessageBox.prompt('请输入修改用户组名称', '修改用户组', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',