Browse Source

feat: 员工分组

cmy 9 months ago
parent
commit
57f4ca26e6

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

@@ -74,6 +74,9 @@ export interface productRule {
   rule_name: string
   rule_value: productRuleValue[]
 }
+/**
+ * 添加商品规格项目
+ */
 export interface AttrBaseItem {
   bar_code: string
   cost: number

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

@@ -1,9 +1,20 @@
 import request from '@/axios'
 import { REQUEST_BASE } from '@/constants'
-import { designerSearch, designerData, staffCategoryData, jobsData, SalespersonData } from './types'
+import {
+  designerSearch,
+  designerData,
+  staffCategoryData,
+  jobsData,
+  SalespersonData,
+  staffGroupSearch,
+  staffGroupData
+} from './types'
 export const getDesigner = (params: designerSearch): Promise<IResponse> => {
   return request.get({ url: `${REQUEST_BASE}/decoration/designer`, params })
 }
+export const getDesignerDetail = (id: number): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/decoration/designer/${id}` })
+}
 export const addDesigner = (data: designerData): Promise<IResponse> => {
   return request.post({ url: `${REQUEST_BASE}/decoration/designer`, data })
 }
@@ -16,6 +27,9 @@ export const putDesigner = (data: designerData): Promise<IResponse> => {
 export const getWorker = (params: designerSearch): Promise<IResponse> => {
   return request.get({ url: `${REQUEST_BASE}/decoration/worker`, params })
 }
+export const getWorkerDetail = (id: number): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/decoration/worker/${id}` })
+}
 export const addWorker = (data: designerData): Promise<IResponse> => {
   return request.post({ url: `${REQUEST_BASE}/decoration/worker`, data })
 }
@@ -64,3 +78,29 @@ export const delStaffSalesperson = (id: number): Promise<IResponse> => {
 export const putStaffSalesperson = (data: SalespersonData): Promise<IResponse> => {
   return request.put({ url: `${REQUEST_BASE}/decoration/salesperson/${data.id}`, data })
 }
+
+export const getDesignerGroup = (params: staffGroupSearch): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/decoration/designer_group`, params })
+}
+export const addDesignerGroup = (data: staffGroupData): Promise<IResponse> => {
+  return request.post({ url: `${REQUEST_BASE}/decoration/designer_group`, data })
+}
+export const delDesignerGroup = (id: number): Promise<IResponse> => {
+  return request.delete({ url: `${REQUEST_BASE}/decoration/designer_group/${id}` })
+}
+export const putDesignerGroup = (data: staffGroupData): Promise<IResponse> => {
+  return request.put({ url: `${REQUEST_BASE}/decoration/designer_group/${data.id}`, data })
+}
+
+export const getWorkerGroup = (params: staffGroupSearch): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/decoration/worker_group`, params })
+}
+export const addWorkerGroup = (data: staffGroupData): Promise<IResponse> => {
+  return request.post({ url: `${REQUEST_BASE}/decoration/worker_group`, data })
+}
+export const delWorkerGroup = (id: number): Promise<IResponse> => {
+  return request.delete({ url: `${REQUEST_BASE}/decoration/worker_group/${id}` })
+}
+export const putWorkerGroup = (data: staffGroupData): Promise<IResponse> => {
+  return request.put({ url: `${REQUEST_BASE}/decoration/worker_group/${data.id}`, data })
+}

+ 15 - 0
src/api/staff/types.ts

@@ -42,3 +42,18 @@ export interface SalespersonData extends staff {
   level?: number
   parent_id?: number
 }
+
+export interface staffGroupSearch {
+  page?: number
+  limit?: number
+  name?: string // 组名称
+  group_leader_id?: string // 组长id
+}
+export interface staffGroupData {
+  id?: number
+  name?: string // 组名称
+  province?: string
+  city?: string
+  area?: string
+  group_leader_id?: number
+}

+ 7 - 3
src/api/system/admin/index.ts

@@ -1,5 +1,5 @@
 import request from '@/axios'
-import type { upAdmin, verify, mapSearch } from './types'
+import type { upAdmin, verify, mapSearch, searchCity } from './types'
 import { REQUEST_BASE, REQUEST_GENERAL } from '@/constants'
 
 /**
@@ -62,8 +62,12 @@ export const getCodeKey = (): Promise<IResponse> => {
 export const postVerify = (data: verify): Promise<IResponse> => {
   return request.post({ url: `${REQUEST_GENERAL}/verify`, data })
 }
-
-export const getCity = (params: { pid: number; type: string }): Promise<IResponse> => {
+/**
+ *
+ * @param params pid: number, type: 1 | 2 | 3 //1:省市 2:省市区 0、3:省市区街道
+ * @returns
+ */
+export const getCity = (params: searchCity): Promise<IResponse> => {
   return request.get({ url: `${REQUEST_BASE}/city`, params })
 }
 export const getMapSearch = (params: mapSearch): Promise<IResponse> => {

+ 4 - 0
src/api/system/admin/types.ts

@@ -30,3 +30,7 @@ export interface mapAddressData {
   city?: string
   district?: string
 }
+export interface searchCity {
+  pid: number //省市区id
+  type: 1 | 2 | 3 //1:省市 2:省市区 0、3:省市区街道
+}

+ 4 - 0
src/components/designerList/index.ts

@@ -0,0 +1,4 @@
+import DesignerList from './src/list.vue'
+import DesignerButtom from './src/buttom.vue'
+
+export { DesignerList, DesignerButtom }

+ 46 - 0
src/components/designerList/src/buttom.vue

@@ -0,0 +1,46 @@
+<script setup lang="tsx">
+import { ref, watch } from 'vue'
+import { getDesignerDetail } from '@/api/staff'
+import { ElAvatar, ElText } from 'element-plus'
+import list from './list.vue'
+const modelValue = defineModel<number>({
+  default: 0
+})
+const detail = ref<any>()
+const showDrawer = ref(false)
+const emit = defineEmits(['change'])
+const checkedUser = async (res: any) => {
+  detail.value = res
+  emit('change', res)
+  showDrawer.value = false
+}
+watch(
+  () => modelValue.value,
+  async (value) => {
+    if (!value) return
+    const res = await getDesignerDetail(value)
+    if (res) {
+      detail.value = res.data
+    }
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+</script>
+<template>
+  <BaseButton v-if="detail" size="large" @click="showDrawer = true">
+    <div class="flex items-center">
+      <ElAvatar shape="circle" size="small" :src="detail?.avatar">
+        <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" />
+      </ElAvatar>
+      <div class="flex flex-col ml-5px items-start justify-start">
+        <ElText class="self-start!">{{ detail?.name }}</ElText>
+        <ElText size="small" class="self-start!"> ID:{{ detail?.id }} </ElText>
+      </div>
+    </div>
+  </BaseButton>
+  <BaseButton v-else @click="showDrawer = true">选择设计师</BaseButton>
+  <list v-model="showDrawer" @confirm="checkedUser" />
+</template>

+ 136 - 0
src/components/designerList/src/list.vue

@@ -0,0 +1,136 @@
+<script setup lang="tsx">
+import { ElDrawer, ElAvatar } from 'element-plus'
+import { reactive, ref, unref } from 'vue'
+import { useTable } from '@/hooks/web/useTable'
+import { Table, TableColumn } from '@/components/Table'
+import { BaseButton } from '@/components/Button'
+import { Search } from '@/components/Search'
+import { designerSearch } from '@/api/staff/types'
+import { FormSchema } from '@/components/Form'
+import { getDesigner } from '@/api/staff'
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { data } = await getDesigner({
+      page: unref(currentPage) || 1,
+      limit: unref(pageSize) || 10,
+      ...unref(searchParams)
+    })
+    return {
+      list: data.list || [],
+      total: data.count || 0
+    }
+  }
+})
+const { dataList, loading, currentPage, pageSize, total } = tableState
+const { getList } = tableMethods
+const modelValue = defineModel<boolean>()
+
+const tableColumns = reactive<TableColumn[]>([
+  {
+    field: 'id',
+    label: 'ID',
+    align: 'center',
+    headerAlign: 'center',
+    minWidth: '60px'
+  },
+  {
+    field: 'info',
+    label: '头像',
+    align: 'center',
+    headerAlign: 'center',
+    minWidth: '60px',
+    slots: {
+      default: (data: any) => {
+        const row = data.row
+        return (
+          <>
+            <ElAvatar shape="circle" size="small" src={row.avatar}>
+              <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" />
+            </ElAvatar>
+          </>
+        )
+      }
+    }
+  },
+  {
+    field: 'name',
+    label: '姓名',
+    minWidth: '90px'
+  },
+  {
+    field: 'phone',
+    label: '手机号',
+    minWidth: '130px'
+  },
+  {
+    field: 'detail_address',
+    label: '地址',
+    minWidth: '130px'
+  },
+  {
+    field: 'action',
+    label: '操作',
+    width: '90px',
+    slots: {
+      default: (data: any) => {
+        const row = data.row
+        return (
+          <>
+            <BaseButton type="primary" onClick={() => confirm(row)}>
+              选择
+            </BaseButton>
+          </>
+        )
+      }
+    }
+  }
+])
+
+const emit = defineEmits(['confirm'])
+const confirm = (row: any) => {
+  const data = Object.assign({}, row)
+  emit('confirm', data)
+}
+const searchSchema = reactive<FormSchema[]>([
+  {
+    field: 'name',
+    label: '搜索',
+    component: 'Input',
+    componentProps: {
+      placeholder: '姓名查询'
+    }
+  }
+])
+const searchParams = ref<designerSearch>({
+  name: ''
+})
+const setSearchParams = (data: any) => {
+  searchParams.value = data
+  getList()
+}
+</script>
+
+<template>
+  <ElDrawer v-model="modelValue" title="选择用户" size="700px">
+    <Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
+    <div class="mt-10px"></div>
+    <Table
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :pagination="{
+        total
+      }"
+      :columns="tableColumns"
+      stripe
+      :data="dataList"
+      :loading="loading"
+      @register="tableRegister"
+    />
+    <template #footer>
+      <div>
+        <BaseButton @click="() => (modelValue = false)">关闭</BaseButton>
+      </div>
+    </template>
+  </ElDrawer>
+</template>

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

@@ -50,6 +50,22 @@ export default {
       meta: {
         title: '一口价分类'
       }
+    },
+    {
+      path: 'group/designer',
+      component: () => import('@/views/Staff/group/designer.vue'),
+      name: `${pre}-group`,
+      meta: {
+        title: '设计师分组'
+      }
+    },
+    {
+      path: 'group/worker',
+      component: () => import('@/views/Staff/group/worker.vue'),
+      name: `${pre}-worker`,
+      meta: {
+        title: '装修工分组'
+      }
     }
   ]
 }

+ 11 - 8
src/views/Goods/list/index.vue

@@ -354,10 +354,11 @@ const tabsClick = async (res) => {
     console.error('Invalid res or paneName')
     return
   }
-  console.log(res.paneName, activeName.value)
-  if (res.paneName === activeName.value) {
-    return
-  }
+  // if (res.paneName === activeName.value) {
+  //   return
+  // }
+  //保存当前分类
+  // activeName.value = res.paneName
   try {
     // 设置 schema 配置
     const schemaConfig = res.paneName === 'all' ? false : true
@@ -426,9 +427,10 @@ const audit = async (id, is_verify, refusal) => {
       <ElTabPane
         v-for="(item, index) in tabsConfig"
         :key="index"
-        :label="`${item.title}${item.total > 0 ? '(' + item.total + ')' : ''}`"
+        :label="`${item.title}${item.total > 0 ? `(${item.total})` : ''}`"
         :name="index"
-      />
+        >{{ index }}
+      </ElTabPane>
     </ElTabs>
     <div class="searchBox">
       <Search
@@ -545,10 +547,11 @@ const audit = async (id, is_verify, refusal) => {
 .searchBox {
   max-width: 1000px;
 }
+
 .example-showcase .el-dropdown-link {
-  cursor: pointer;
-  color: var(--el-color-primary);
   display: flex;
+  color: var(--el-color-primary);
+  cursor: pointer;
   align-items: center;
 }
 </style>

+ 160 - 0
src/views/Staff/group/components/designerWrite.vue

@@ -0,0 +1,160 @@
+<script setup lang="tsx">
+import { Form, FormSchema } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { PropType, watch, ref } from 'vue'
+import { useValidator } from '@/hooks/web/useValidator'
+import { cloneDeep } from 'lodash-es'
+import { getCity } from '@/api/system/admin'
+import { DesignerButtom } from '@/components/designerList'
+const { required } = useValidator()
+
+const props = defineProps({
+  currentRow: {
+    type: Object as PropType<any>,
+    default: () => null
+  }
+})
+const getCityList = async (node, resolve) => {
+  const { value } = node
+  const res = await getCity({ pid: value || 0, type: 2 })
+  const data = res.data.map((ree) => {
+    return {
+      label: ree.label,
+      value: ree.value,
+      leaf: !ree.children
+    }
+  })
+  resolve(data)
+}
+const cityChange = (value) => {
+  console.log(value, 'value')
+  setValues({
+    province: value[0],
+    city: value[1],
+    area: value[2]
+  })
+}
+const checked = (res) => {
+  // console.log(res, 'res')
+  setValues({
+    group_leader_id: res.id
+  })
+}
+const formSchema = ref<FormSchema[]>([
+  {
+    field: 'name',
+    label: '组名',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: '请输入组名称'
+    }
+  },
+  {
+    field: 'group_leader_id',
+    label: '组长',
+    component: 'CheckboxGroup',
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <DesignerButtom
+                v-model={data.id}
+                onChange={(res) => {
+                  checked(res)
+                }}
+              ></DesignerButtom>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'address',
+    label: '地址选择',
+    component: 'Cascader',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      on: {
+        change: cityChange
+      },
+      props: {
+        lazy: true,
+        lazyLoad: getCityList
+      },
+      placeholder: '请输入组名称'
+    }
+  },
+  {
+    field: 'province',
+    label: '省份',
+    component: 'Input',
+    hidden: true
+  },
+  {
+    field: 'city',
+    label: '市',
+    hidden: true,
+    component: 'Input'
+  },
+  {
+    field: 'area',
+    label: '区',
+    hidden: true,
+    component: 'Input'
+  },
+  {
+    field: 'id',
+    label: 'id',
+    hidden: true,
+    component: 'Input'
+  }
+])
+
+const rules = ref({
+  name: [required('请填写组名称')],
+  group_leader_id: [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
+  }
+}
+
+watch(
+  () => props.currentRow,
+  async (value) => {
+    if (!value) return
+    const currentRow = cloneDeep(value)
+    console.log(currentRow, 'currentRow')
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit
+})
+</script>
+
+<template>
+  <Form :rules="rules" @register="formRegister" :schema="formSchema" />
+</template>

+ 382 - 0
src/views/Staff/group/components/workerWrite.vue

@@ -0,0 +1,382 @@
+<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: 'gender',
+    label: '性别',
+    component: 'Select',
+    value: 0,
+    componentProps: {
+      placeholder: '请选择性别',
+      options: [
+        {
+          label: '保密',
+          value: 0
+        },
+        {
+          label: '男',
+          value: 1
+        },
+        {
+          label: '女',
+          value: 2
+        }
+      ]
+    }
+  },
+
+  {
+    field: 'birth_day_time',
+    label: '出生日期',
+    component: 'DatePicker'
+  },
+
+  {
+    field: 'start_job_year',
+    label: '就业年份',
+    component: 'Input',
+    componentProps: {
+      placeholder: '例子:2014'
+    }
+  },
+
+  {
+    field: 'tag_list',
+    label: '标签',
+    colProps: {
+      span: 24
+    },
+    component: 'CheckboxGroup',
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <ElInputTag
+                v-model={data.tag_list}
+                tagEffect="light"
+                tagType="primary"
+                clearable
+              ></ElInputTag>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    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()],
+  birth_day_time: [required('请选择出生日期')],
+  start_job_year: [required('请填写就业年份')],
+  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>

+ 227 - 0
src/views/Staff/group/designer.vue

@@ -0,0 +1,227 @@
+<script setup lang="tsx">
+import { reactive, ref, unref } from 'vue'
+import { getDesignerGroup, addDesignerGroup, putDesignerGroup, delDesignerGroup } 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/designerWrite.vue'
+import { BaseButton } from '@/components/Button'
+import { ElMessage, ElDivider, ElMessageBox } from 'element-plus'
+import { Dialog } from '@/components/Dialog'
+import { staffGroupSearch, staffGroupData } from '@/api/staff/types'
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { pageSize, currentPage } = tableState
+    const res = await getDesignerGroup({
+      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: 'name',
+    label: '组名',
+    minWidth: 100
+  },
+  {
+    field: 'group_leader_id',
+    label: '组长',
+    minWidth: 120
+  },
+  {
+    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<staffGroupSearch>({
+  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,
+    id: row.id,
+    province: row.province,
+    city: row.city,
+    area: row.area,
+    group_leader_id: row.group_leader_id,
+    address: [parseInt(row.province), parseInt(row.city), parseInt(row.area)]
+  }
+  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: staffGroupData = {
+      name: formData.name,
+      id: formData.id,
+      province: formData.province,
+      city: formData.city,
+      area: formData.area,
+      group_leader_id: formData.group_leader_id
+    }
+    try {
+      let res: any = {}
+      if (actionType.value === 'edit') {
+        data.id = formData.id
+        res = await putDesignerGroup(data)
+      } else if (actionType.value === '') {
+        res = await addDesignerGroup(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 delDesignerGroup(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="500px">
+    <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>

+ 317 - 0
src/views/Staff/group/worker.vue

@@ -0,0 +1,317 @@
+<script setup lang="tsx">
+import { reactive, ref, unref } from 'vue'
+import { getWorkerGroup, addWorkerGroup, putWorkerGroup, delWorkerGroup } 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/workerWrite.vue'
+import { BaseButton } from '@/components/Button'
+import { ElMessage, ElDivider, ElMessageBox } from 'element-plus'
+import { Dialog } from '@/components/Dialog'
+import { designerData, designerSearch } from '@/api/staff/types'
+import { formatToDate } from '@/utils/dateUtil'
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const { pageSize, currentPage } = tableState
+    const res = await getWorkerGroup({
+      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: 'name',
+    label: '组名称',
+    minWidth: 100
+  },
+  {
+    field: 'phone',
+    label: '手机号',
+    minWidth: 120
+  },
+  {
+    field: 'province',
+    label: '省',
+    align: 'center',
+    headerAlign: 'center',
+    minWidth: 100
+  },
+  {
+    field: 'city',
+    align: 'center',
+    headerAlign: 'center',
+    label: '市',
+    minWidth: 60
+  },
+  {
+    field: 'area',
+    align: 'center',
+    headerAlign: 'center',
+    label: '区',
+    minWidth: 60
+  },
+  // {
+  //   field: 'area',
+  //   label: '区域',
+  //   minWidth: 200,
+  //   slots: {·
+  //     default: ({ row }: any) => {
+  //       return (
+  //         <>
+  //           {row.province_text} {row.city_text} {row.area_text}
+  //         </>
+  //       )
+  //     }
+  //   }
+  // },
+  {
+    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: 'gender',
+    label: '性别',
+    component: 'Select',
+    value: '',
+    componentProps: {
+      placeholder: '性别',
+      options: [
+        {
+          label: '全部',
+          value: ''
+        },
+        {
+          label: '保密',
+          value: 0
+        },
+        {
+          label: '男',
+          value: 1
+        },
+        {
+          label: '女',
+          value: 2
+        }
+      ]
+    }
+  },
+  {
+    field: 'tag_list',
+    label: '标签',
+    component: 'Input',
+    value: '',
+    componentProps: {
+      placeholder: '请输入搜索标签'
+    }
+  },
+  {
+    field: 'name',
+    label: '姓名',
+    component: 'Input',
+    value: '',
+    componentProps: {
+      placeholder: '请输入姓名'
+    }
+  }
+])
+
+const searchParams = ref<designerSearch>({
+  gender: '',
+  tag_list: '',
+  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],
+    gender: row.gender,
+    birth_day_time: formatToDate(row.birth_day_time * 1000),
+    province: row.province,
+    city: row.city,
+    area: row.area,
+    tag_list: row.tag_list.split(','),
+    longitude: row.longitude,
+    latitude: row.latitude,
+    detail_address: row.detail_address,
+    start_job_year: row.start_job_year
+  }
+  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: designerData = {
+      name: formData.name,
+      uid: formData.uid,
+      phone: formData.phone,
+      avatar: formData.avatar[0],
+      gender: formData.gender,
+      birth_day_time: formatToDate(formData.birth_day_time),
+      province: formData.province,
+      city: formData.city,
+      area: formData.area,
+      tag_list: formData.tag_list,
+      longitude: formData.longitude,
+      latitude: formData.latitude,
+      detail_address: formData.detail_address,
+      start_job_year: formData.start_job_year
+    }
+    try {
+      let res: any = {}
+      if (actionType.value === 'edit') {
+        data.id = formData.id
+        res = await putWorkerGroup(data)
+      } else if (actionType.value === '') {
+        res = await addWorkerGroup(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 delWorkerGroup(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>

+ 1 - 2
src/views/User/list/components/DetailFrom.vue

@@ -17,8 +17,7 @@ import {
   ElSwitch,
   ElTag,
   ElDescriptions,
-  ElDescriptionsItem,
-  ElMention
+  ElDescriptionsItem
 } from 'element-plus'
 import {
   getBalanceLog,