Browse Source

feat: store

lhl 10 months ago
parent
commit
362bc5f851

+ 14 - 0
src/api/store/index.ts

@@ -0,0 +1,14 @@
+import request from '@/axios'
+import { REQUEST_BASE } from '@/constants'
+
+export const getStoreList = (params: any): Promise<IResponse> => {
+  return request.get({ url: `${REQUEST_BASE}/store`, params })
+}
+
+export const addStore = (data: any): Promise<IResponse> => {
+  return request.post({ url: `${REQUEST_BASE}/store`, data })
+}
+
+export const editStore = (data: any): Promise<IResponse> => {
+  return request.put({ url: `${REQUEST_BASE}/store/${data.id}`, data })
+}

+ 35 - 0
src/api/store/types.ts

@@ -0,0 +1,35 @@
+export type designerSearch = {
+  page?: number
+  limit?: number
+}
+
+export interface designerData {
+  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
+  start_job_year: string
+}
+export type categoryData = {
+  id?: number
+  name: string
+  is_show: 1 | 0
+  type: 1 | 2
+}
+export type jobsData = {
+  id?: number
+  name: string
+  is_show: 1 | 0
+  cate_id: number
+  default_price: number
+}

+ 24 - 0
src/router/model/Store.ts

@@ -0,0 +1,24 @@
+import { Layout } from '@/utils/routerHelper'
+const pre = 'store'
+
+export default {
+  path: `/${pre}`,
+  component: Layout,
+  name: `${pre}`,
+  redirect: '/store/list',
+  meta: {
+    title: '门店管理',
+    icon: 'vi-ep:setting',
+    alwaysShow: true
+  },
+  children: [
+    {
+      path: 'list',
+      component: () => import('@/views/Store/list/index.vue'),
+      name: `${pre}-list`,
+      meta: {
+        title: '门店列表'
+      }
+    }
+  ]
+}

+ 468 - 0
src/views/Store/list/components/Write.vue

@@ -0,0 +1,468 @@
+<script setup lang="tsx">
+import { Form, FormSchema } from '@/components/Form'
+import { UpImgButtom } from '@/components/UpFile'
+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 { ElAutocomplete, ElAvatar, ElText } from 'element-plus'
+import { getConfigKey, getMapSearch } from '@/api/system/admin'
+import { mapAddressData } from '@/api/system/admin/types'
+import { UserList } from '@/components/UserList'
+import { BaseButton } from '@/components/Button'
+
+const mapKey = ref('')
+const actionAddress = ref<mapAddressData>()
+const { required } = useValidator()
+const center = ref({ lat: 28.655759, lng: 121.420808 })
+const geometries = ref([{ styleId: 'marker', position: { lat: 28.655759, lng: 121.420808 } }])
+const userDetail = ref<any>()
+const showDrawer = ref(false)
+
+const checkedUser = async (res: any) => {
+  userDetail.value = res
+  setValues({
+    uid: res.uid
+    // name: res.real_name,
+    // avatar: [res.avatar],
+    // birth_day_time: res.birthday,
+    // phone: res.phone
+  })
+  showDrawer.value = false
+}
+
+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 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 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 formSchema = ref<FormSchema[]>([
+  {
+    field: 'uid',
+    label: '用户',
+    component: 'CheckboxGroup',
+    formItemProps: {
+      slots: {
+        default: () => {
+          if (userDetail?.value) {
+            return (
+              <>
+                <BaseButton size="large" onClick={() => (showDrawer.value = true)}>
+                  <div class={'flex items-center'}>
+                    <ElAvatar shape="circle" size="small" src={userDetail?.value?.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!'}>{userDetail?.value?.nickname}</ElText>
+                      <ElText size="small" class={'self-start!'}>
+                        UID:{userDetail?.value?.uid}
+                      </ElText>
+                    </div>
+                  </div>
+                </BaseButton>
+              </>
+            )
+          } else {
+            return (
+              <>
+                <BaseButton onClick={() => (showDrawer.value = true)}>选择用户</BaseButton>
+              </>
+            )
+          }
+        }
+      }
+    }
+  },
+  {
+    field: 'type',
+    label: '类型',
+    component: 'Select',
+    colProps: {
+      span: 24
+    },
+    value: 1,
+    componentProps: {
+      options: [
+        {
+          label: '自营',
+          value: 1
+        },
+        {
+          label: '加盟',
+          value: 2
+        }
+      ]
+    }
+  },
+  {
+    field: 'name',
+    label: '门店名称',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: '请输入门店名称'
+    }
+  },
+  {
+    field: 'logo',
+    label: '门店LOGO',
+    component: 'CheckboxGroup',
+    componentProps: {
+      placeholder: '选择LOGO'
+    },
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <UpImgButtom v-model={data.logo}></UpImgButtom>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'introduction',
+    label: '简介',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: '请输入门店简介'
+    }
+  },
+  {
+    field: 'slide_images',
+    label: '推荐图',
+    component: 'CheckboxGroup',
+    componentProps: {
+      placeholder: '选择推荐图'
+    },
+    formItemProps: {
+      slots: {
+        default: (data) => {
+          return (
+            <>
+              <UpImgButtom v-model={data.slide_images}></UpImgButtom>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'manager_name',
+    label: '店长',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: '请输入店长名称'
+    }
+  },
+  {
+    field: 'phone',
+    label: '联系电话',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: '请输入联系电话'
+    }
+  },
+  {
+    field: 'store_account',
+    label: '登录账号',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      strength: true,
+      placeholder: '请输入登录账号'
+    }
+  },
+  {
+    field: 'store_password',
+    label: '登录密码',
+    colProps: {
+      span: 24
+    },
+    component: 'InputPassword',
+    componentProps: {
+      strength: true,
+      placeholder: '请输入登录密码'
+    }
+  },
+  // {
+  //   field: 'true_pwd',
+  //   label: '确认密码',
+  //   component: 'InputPassword',
+  //   componentProps: {
+  //     strength: true,
+  //     placeholder: '请输入重复输入登录密码'
+  //   }
+  // },
+  {
+    field: 'valid_range',
+    label: '有效距离(km)',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: '请输入有效距离'
+    }
+  },
+  {
+    field: 'product_status',
+    colProps: {
+      span: 24
+    },
+    label: '自主添加商品',
+    component: 'Switch'
+  },
+  {
+    field: 'product_verify_status',
+    colProps: {
+      span: 24
+    },
+    label: '商品免审核',
+    component: 'Switch'
+  },
+  {
+    field: 'is_show',
+    colProps: {
+      span: 24
+    },
+    label: '是否显示',
+    component: 'Switch'
+  },
+  {
+    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',
+    label: 'id',
+    hidden: true,
+    component: 'Input'
+  }
+])
+
+const rules = ref({
+  name: [required('请填写门店名称')],
+  introduction: [required('请填写门店简介')],
+  phone: [required('请填写联系号码')],
+  logo: [required('请选择logo')],
+  slide_images: [required('请选择推荐图')],
+  manager_name: [required('请填写店长名称')],
+  valid_range: [required('请填写有效距离')],
+  store_account: [required('请填写登录账号')],
+  store_password: [required('请填写登录密码')],
+  detail_address: [
+    {
+      required: true,
+      validator: (_, val, callback) => {
+        if (!actionAddress.value || !val) {
+          callback(new Error('请选择地址'))
+        } else {
+          callback()
+        }
+      }
+    }
+  ]
+})
+
+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)
+    setValues(currentRow)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+defineExpose({
+  submit
+})
+</script>
+
+<template>
+  <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>
+  <UserList v-model="showDrawer" @confirm="checkedUser" />
+</template>

+ 239 - 0
src/views/Store/list/index.vue

@@ -0,0 +1,239 @@
+<script setup lang="tsx">
+import { reactive, ref, unref, useTemplateRef } from 'vue'
+import { delStaffCategory } from '@/api/staff'
+import { getStoreList, addStore, editStore } from '@/api/store'
+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 { Dialog } from '@/components/Dialog'
+import { BaseButton } from '@/components/Button'
+import { ElDivider, ElMessage, ElMessageBox } from 'element-plus'
+import Write from './components/Write.vue'
+
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const res = await getStoreList({
+      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: 'type_chs',
+    label: '类型',
+    minWidth: 100
+  },
+  {
+    field: 'name',
+    label: '分类',
+    minWidth: 140
+  },
+  {
+    field: 'is_show_chs',
+    label: '是否显示',
+    width: 120
+  },
+  {
+    field: 'action',
+    label: t('userDemo.action'),
+    width: 200,
+    fixed: 'right',
+    align: 'center',
+    headerAlign: 'center',
+    slots: {
+      default: (data: any) => {
+        const row = data.row
+        return (
+          <>
+            <BaseButton link size="small" type="primary" onClick={() => action('edit', row)}>
+              编辑
+            </BaseButton>
+            <ElDivider direction="vertical" />
+            <BaseButton link size="small" type="danger" onClick={() => delAction(row)}>
+              删除
+            </BaseButton>
+          </>
+        )
+      }
+    }
+  }
+])
+
+const searchSchema = reactive<FormSchema[]>([
+  {
+    field: 'type',
+    label: '类型',
+    component: 'Select',
+    value: '',
+    componentProps: {
+      placeholder: '全部',
+      options: [
+        {
+          label: '全部',
+          value: ''
+        },
+        {
+          label: '自营',
+          value: '1'
+        },
+        {
+          label: '加盟',
+          value: '2'
+        }
+      ]
+    }
+  }
+])
+
+const searchParams = ref<{
+  type?: string
+}>({
+  type: ''
+})
+const setSearchParams = (data: any) => {
+  searchParams.value = data
+  getList()
+}
+
+const dialogVisible = ref(false)
+const currentRow = ref()
+const dialogTitle = ref('')
+const actionType = ref('')
+const saveLoading = ref(false)
+const writeRef = useTemplateRef('writeRef')
+
+const action = async (type: string, row?: any) => {
+  actionType.value = type
+  if (type == 'add') {
+    dialogTitle.value = t('exampleDemo.add')
+    currentRow.value = undefined
+  }
+  if (type == 'edit') {
+    dialogTitle.value = t('exampleDemo.edit')
+    currentRow.value = {
+      id: row.id,
+      name: row.name,
+      is_show: row.is_show == 1 ? true : false
+    }
+  }
+  dialogVisible.value = true
+}
+const delAction = (row: any) => {
+  ElMessageBox.confirm('删除后无法恢复,是否删除?', {
+    confirmButtonText: '删除',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      const re = await delStaffCategory(row.id)
+      if (re) {
+        ElMessage({
+          showClose: true,
+          message: '删除成功',
+          type: 'success'
+        })
+      }
+      await getList()
+    })
+    .catch(() => {})
+}
+const save = async () => {
+  const write = unref(writeRef)
+  const formData = await write?.submit()
+  if (formData) {
+    saveLoading.value = true
+    const data: any = {
+      uid: formData.uid,
+      id: formData.id || '',
+      name: formData.name,
+      type: formData.type,
+      logo: formData.logo[0],
+      introduction: formData.introduction,
+      slide_images: formData.slide_images[0],
+      manager_name: formData.manager_name,
+      phone: formData.phone,
+      valid_range: formData.valid_range,
+      product_status: formData.product_status ? 1 : 0,
+      product_verify_status: formData.product_verify_status ? 1 : 0,
+      detailed_address: formData.detail_address,
+      address: formData.detail_address,
+      area: formData.area,
+      city: formData.city,
+      province: formData.province,
+      latitude: formData.latitude,
+      longitude: formData.longitude,
+      is_show: formData.is_show ? 1 : 0,
+      store_account: formData.store_account,
+      store_password: formData.store_password
+    }
+    console.log(data, 'data')
+    if (actionType.value === 'edit') {
+      await editStore(data)
+    } else if (actionType.value === 'add') {
+      await addStore(data)
+    }
+    ElMessage({
+      showClose: true,
+      message: '保存成功',
+      type: 'success'
+    })
+    getList()
+    saveLoading.value = false
+    dialogVisible.value = false
+  }
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
+    <div class="mb-10px">
+      <BaseButton type="primary" @click="action('add')">{{ 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" maxHeight="700px">
+    <Write ref="writeRef" :current-row="currentRow" />
+    <template #footer>
+      <BaseButton type="primary" :loading="saveLoading" @click="save">
+        {{ t('exampleDemo.save') }}
+      </BaseButton>
+      <BaseButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</BaseButton>
+    </template>
+  </Dialog>
+</template>