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

feat: 新增企业客户列表,新增添加编辑弹窗,相关接口对接完成。补充企业客户所有多语言内容。优化获取token方法。

lixuan недель назад: 3
Родитель
Сommit
7ae8cff7e7

+ 3 - 1
apps/web-velofex/src/api/core/menu.ts

@@ -6,5 +6,7 @@ import { requestClient } from '#/api/request';
  * 获取用户所有菜单
  */
 export async function getAllMenusApi() {
-  return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
+  return requestClient.get<RouteRecordStringComponent[]>(
+    '/api/menu/leftMenuList',
+  );
 }

+ 114 - 2
apps/web-velofex/src/api/core/user.ts

@@ -414,7 +414,7 @@ export namespace UserApi {
     code: string;
   }
 
-  export interface GetProjectsResult {
+  export interface GetAvailableProjectsResult {
     isSuccess: boolean;
     code: number;
     result: {
@@ -422,6 +422,72 @@ export namespace UserApi {
     };
     isAuthorized: boolean;
   }
+
+  export interface EnterpriseCustomerFilter {
+    name: string;
+    value?: boolean | number | string;
+  }
+
+  export interface GetEnterpriseCustomersParams {
+    currentPage: number;
+    pageSize: number;
+    orderByProperty?: string;
+    Ascending?: boolean;
+    totalPage?: number;
+    totalCount?: number;
+    filters?: EnterpriseCustomerFilter[];
+  }
+
+  export interface EnterpriseCustomerModel {
+    code: string;
+    id: string;
+    imgPhotoFileId: string;
+    name: string;
+    partnerUserName: string;
+    contact?: string;
+    cellPhone?: string;
+    email?: string;
+    address?: string;
+    website?: string;
+    remark?: string;
+  }
+
+  export interface GetEnterpriseCustomersResult {
+    isSuccess: boolean;
+    code: number;
+    result: {
+      currentPage: number;
+      hasNextPage: boolean;
+      hasPreviousPage: boolean;
+      model: EnterpriseCustomerModel[];
+      pageSize: number;
+      totalCount: number;
+      totalPages: number;
+    };
+    isAuthorized: boolean;
+  }
+
+  export interface EnterpriseCustomerParams {
+    id?: string;
+    fileId?: string;
+    imgPhotoFileId?: string;
+    version?: string;
+    code: string;
+    name: string;
+    contact?: string;
+    cellPhone?: string;
+    email?: string;
+    address?: string;
+    website?: string;
+    remark?: string;
+  }
+
+  export interface EnterpriseCustomerResult {
+    isSuccess: boolean;
+    code: number;
+    result: boolean;
+    isAuthorized: boolean;
+  }
 }
 
 /**
@@ -684,7 +750,7 @@ export async function deleteUserApi(data: any) {
  * 获取用户关联项目
  */
 export async function getAssociatedProjectsApi(userId: string) {
-  return requestClient.post<UserApi.GetProjectsResult>(
+  return requestClient.post<UserApi.GetAvailableProjectsResult>(
     '/api/user/listUserEnterprise',
     {
       filters: [
@@ -730,3 +796,49 @@ export async function getAvailableProjectsApi(
     data,
   );
 }
+
+/**
+ * 获取企业客户列表
+ */
+export async function getEnterpriseCustomersApi(
+  data: UserApi.GetEnterpriseCustomersParams,
+) {
+  return requestClient.post<UserApi.GetEnterpriseCustomersResult>(
+    '/api/enterclient/list',
+    data,
+  );
+}
+
+/**
+ * 添加企业客户
+ */
+export async function addEnterpriseCustomerApi(
+  data: UserApi.EnterpriseCustomerParams,
+) {
+  return requestClient.post<UserApi.EnterpriseCustomerResult>(
+    '/api/enterclient/doCreate',
+    data,
+  );
+}
+
+/**
+ * 更新企业客户
+ */
+export async function updateEnterpriseCustomerApi(
+  data: UserApi.EnterpriseCustomerParams,
+) {
+  return requestClient.post<UserApi.EnterpriseCustomerResult>(
+    '/api/enterclient/doUpdate',
+    data,
+  );
+}
+
+/**
+ * 删除企业客户
+ */
+export async function deleteEnterpriseCustomerApi(data: { code: string }) {
+  return requestClient.post<UserApi.EnterpriseCustomerResult>(
+    '/api/enterclient/doDelete',
+    data,
+  );
+}

BIN
apps/web-velofex/src/assets/image/list1.png


+ 59 - 5
apps/web-velofex/src/layouts/header/header.vue

@@ -1,20 +1,26 @@
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
 
 import { SvgArrowRightIcon } from '@vben/icons';
+import { useAccessStore, useUserStore } from '@vben/stores';
 
 import useAvatar from '@/assets/image/user.png';
 import { $t } from '@/locales';
+import { Dropdown, message, Modal } from 'antdv-next';
 
 import Logo from '#/assets/image/logo.png';
-// import { useLoginModalStore } from '#/store';
+import { router } from '#/router';
 
 import SelectLang from '../select-lang.vue';
 import Avatar from './avatar.vue';
 
-// const loginModalStore = useLoginModalStore();
+const userStore = useUserStore();
 
-const token = localStorage.getItem('token_a');
+const accessStore = useAccessStore();
+
+const token = accessStore.accessToken;
+
+const isLogin = computed(() => !!userStore.userInfo);
 
 const menus = computed(() => [
   { title: $t('homeMenu.toolDownloads'), path: '/Views/Tools/Index.html' },
@@ -36,11 +42,51 @@ const menus = computed(() => [
   },
 ]);
 
+const menuList = ref();
+
 function openLogin() {}
 
+function handleMenuClick({ key }: { key: string }) {
+  if (key === 'Logout') {
+    handleLogout();
+  } else if (key === 'ChangeInformation') {
+    window.open('/Views/Home/userSet.html', '_blank');
+  }
+}
+
+function handleLogout() {
+  Modal.confirm({
+    title: $t('auth.logOut.title'),
+    content: $t('auth.logOut.value'),
+    okText: $t('btn.yes'),
+    okType: 'danger',
+    cancelText: $t('btn.no'),
+    onOk() {
+      userStore.setUserInfo(null);
+      accessStore.setAccessToken(null);
+      router.push('/');
+      message.success($t('auth.logOutOK'));
+    },
+    onCancel() {},
+  });
+}
+
 function handleClick(path: string) {
   window.open(path, '_blank');
 }
+
+watch(
+  () => isLogin.value,
+  (newValue) => {
+    menuList.value = newValue
+      ? [
+          { key: 'Logout', label: $t('auth.logout') },
+          { key: 'ChangeInformation', label: $t('auth.changeInformation') },
+        ]
+      : [];
+  },
+  { immediate: true },
+);
 </script>
 
 <template>
@@ -59,7 +105,15 @@ function handleClick(path: string) {
     </ul>
     <div class="flex gap-2">
       <SelectLang />
-      <Avatar :avatar="useAvatar" class="cursor-pointer" @click="openLogin" />
+      <Dropdown
+        :menu="{
+          items: menuList,
+        }"
+        placement="bottom"
+        @menu-click="(info: any) => handleMenuClick(info)"
+      >
+        <Avatar :avatar="useAvatar" class="cursor-pointer" @click="openLogin" />
+      </Dropdown>
     </div>
   </div>
 </template>

+ 48 - 1
apps/web-velofex/src/locales/langs/en-US/page.json

@@ -48,7 +48,13 @@
       "sendVerificationCodeFailed": "Failed to send verification code",
       "registerSuccess": "Registration successful, please login to use",
       "registerFailed": "Registration failed"
-    }
+    },
+    "logOut":{
+      "title":"Log Out",
+      "value":"Are you sure you want to log out?"
+    },
+    "logOutOK":"Logout successful!",
+    "changeInformation":"Change Information"
   },
   "dashboard": {
     "analytics": "Analytics",
@@ -284,5 +290,46 @@
       "totalProjects": "Total {total} projects",
       "deleteProjectConfirm": "Are you sure to delete this associated project?"
     }
+  },
+  "enterpriseCustomers": {
+    "breadcrumb": "Dashboard / Enterprise Customers",
+    "customerName": "Customer Name",
+    "customerCode": "Customer Code",
+    "status": "Status",
+    "active": "Active",
+    "inactive": "Inactive",
+    "chooseStatus": "Choose Status",
+    "loading": "Loading...",
+    "edit": "Edit",
+    "remove": "Delete",
+    "deleteConfirm": "Are you sure to delete this customer?",
+    "deleteDescription": "Cannot be recovered after deletion",
+    "deleteSuccess": "Delete successfully!",
+    "saveSuccess": "Save successfully!",
+    "searchPlaceholder": "Search customer name or code",
+    "modal": {
+      "addTitle": "Add Enterprise Customer",
+      "editTitle": "Edit Enterprise Customer",
+      "enterpriseLogo": "Enterprise Logo",
+      "uploadLogo": "Upload Logo",
+      "customerCode": "Customer Code",
+      "enterCustomerCode": "Please enter customer code",
+      "customerName": "Customer Name",
+      "enterCustomerName": "Please enter customer name",
+      "contact": "Contact",
+      "enterContact": "Please enter contact",
+      "cellPhone": "Phone",
+      "enterCellPhone": "Please enter phone",
+      "email": "Email",
+      "enterEmail": "Please enter email",
+      "address": "Address",
+      "enterAddress": "Please enter address",
+      "website": "Website",
+      "enterWebsite": "Please enter website",
+      "remark": "Remark",
+      "enterRemark": "Please enter remark",
+      "cancel": "Cancel",
+      "save": "Save"
+    }
   }
 }

+ 48 - 1
apps/web-velofex/src/locales/langs/zh-CN/page.json

@@ -48,7 +48,13 @@
       "sendVerificationCodeFailed": "发送验证码失败",
       "registerSuccess": "注册成功,请登录后使用",
       "registerFailed": "注册失败"
-    }
+    },
+    "logOut":{
+      "title":"退出登录",
+      "value":"确定要退出登录吗?"
+    },
+    "logOutOK":"退出成功!",
+    "changeInformation":"修改个人信息"
   },
   "dashboard": {
     "title": "概览",
@@ -284,5 +290,46 @@
       "totalProjects": "共 {total} 个项目",
       "deleteProjectConfirm": "确定要删除此关联项目吗?"
     }
+  },
+  "enterpriseCustomers": {
+    "breadcrumb": "首页 / 企业客户",
+    "customerName": "客户名称",
+    "customerCode": "客户代码",
+    "status": "状态",
+    "active": "启用",
+    "inactive": "禁用",
+    "chooseStatus": "选择状态",
+    "loading": "加载中...",
+    "edit": "编辑",
+    "remove": "删除",
+    "deleteConfirm": "确定要删除此客户吗?",
+    "deleteDescription": "删除后无法恢复",
+    "deleteSuccess": "删除成功!",
+    "saveSuccess": "保存成功!",
+    "searchPlaceholder": "搜索客户名称或代码",
+    "modal": {
+      "addTitle": "添加企业客户",
+      "editTitle": "编辑企业客户",
+      "enterpriseLogo": "企业Logo",
+      "uploadLogo": "上传Logo",
+      "customerCode": "客户代码",
+      "enterCustomerCode": "请输入客户代码",
+      "customerName": "客户名称",
+      "enterCustomerName": "请输入客户名称",
+      "contact": "联系人",
+      "enterContact": "请输入联系人",
+      "cellPhone": "手机号",
+      "enterCellPhone": "请输入手机号",
+      "email": "邮箱",
+      "enterEmail": "请输入邮箱",
+      "address": "地址",
+      "enterAddress": "请输入地址",
+      "website": "网址",
+      "enterWebsite": "请输入网址",
+      "remark": "备注",
+      "enterRemark": "请输入备注",
+      "cancel": "取消",
+      "save": "保存"
+    }
   }
 }

+ 10 - 0
apps/web-velofex/src/router/routes/external/router-a.ts

@@ -62,6 +62,16 @@ const routes: RouteRecordRaw[] = [
           title: $t('homeModule.deliveryPartners'),
         },
       },
+      {
+        name: 'EnterpriseCustomers',
+        path: '/enterprise-customers',
+        component: () =>
+          import('#/views/dashboard/enterprise-customers/index.vue'),
+        meta: {
+          icon: 'carbon:building',
+          title: $t('homeModule.enterpriseCustomers'),
+        },
+      },
     ],
   },
 ];

+ 5 - 1
apps/web-velofex/src/views/dashboard/application-management/application-modal.vue

@@ -4,6 +4,8 @@ import type { Dayjs } from 'dayjs';
 
 import { computed, ref, watch } from 'vue';
 
+import { useAccessStore } from '@vben/stores';
+
 import { $t } from '@/locales';
 import {
   Button,
@@ -50,7 +52,9 @@ const emit = defineEmits<{
 
 const activeMenu = ref('basic');
 
-const token = localStorage.getItem('token_a');
+const accessStore = useAccessStore();
+
+const token = accessStore.accessToken;
 
 const formData = ref({
   code: '',

+ 282 - 0
apps/web-velofex/src/views/dashboard/enterprise-customers/enterprise-customers-modal.vue

@@ -0,0 +1,282 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+
+import { useAccessStore } from '@vben/stores';
+
+import { $t } from '@/locales';
+import { Button, Input, message, Modal, Upload } from 'antdv-next';
+
+import { addEnterpriseCustomerApi, updateEnterpriseCustomerApi } from '#/api';
+
+interface Props {
+  open: boolean;
+  mode: 'add' | 'edit';
+  customerData?: any;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  (e: 'save', data: any): void;
+  (e: 'update:open', value: boolean): void;
+}>();
+
+const accessStore = useAccessStore();
+
+const token = accessStore.accessToken;
+
+const formData = ref({
+  code: '',
+  id: '',
+  fileId: '',
+  imgPhotoFileId: '',
+  fileList: [] as any[],
+  name: '',
+  contact: '',
+  cellPhone: '',
+  email: '',
+  address: '',
+  website: '',
+  remark: '',
+});
+
+watch(
+  () => props.open,
+  (val) => {
+    if (val && props.mode === 'edit' && props.customerData) {
+      Object.assign(formData.value, props.customerData);
+      if (props.customerData.imgPhotoFileId) {
+        formData.value.fileList = [
+          {
+            uid: '-1',
+            name: 'logo.png',
+            status: 'done',
+            url: `/File/Download?fileId=${props.customerData.imgPhotoFileId}`,
+            response: {
+              result: [
+                {
+                  id: props.customerData.imgPhotoFileId,
+                },
+              ],
+            },
+          },
+        ];
+      }
+    }
+    if (val && props.mode === 'add') {
+      resetFormData();
+    }
+  },
+);
+
+const isOpen = computed({
+  get: () => props.open,
+  set: (val) => emit('update:open', val),
+});
+
+function handleLogoUpload(info: any) {
+  if (info.file.status === 'done' && info.file.response?.result?.[0]?.id) {
+    formData.value.fileId = info.file.response.result[0].id;
+    formData.value.imgPhotoFileId = info.file.response.result[0].id;
+  }
+}
+
+async function handleSave() {
+  const data = {
+    id: formData.value.id,
+    fileId: formData.value.fileId,
+    imgPhotoFileId: formData.value.imgPhotoFileId,
+    version: `v${formData.value.imgPhotoFileId || '0'}`,
+    code: formData.value.code,
+    name: formData.value.name,
+    contact: formData.value.contact,
+    cellPhone: formData.value.cellPhone,
+    email: formData.value.email,
+    address: formData.value.address,
+    website: formData.value.website,
+    remark: formData.value.remark,
+  };
+  try {
+    const result = formData.value.id
+      ? await updateEnterpriseCustomerApi(data)
+      : await addEnterpriseCustomerApi(data);
+    if (result?.isSuccess) {
+      emit('save', data);
+      isOpen.value = false;
+      message.success($t('enterpriseCustomers.saveSuccess'));
+    }
+  } catch {}
+}
+
+function handleCancel() {
+  isOpen.value = false;
+}
+
+function resetFormData() {
+  formData.value = {
+    code: '',
+    id: '',
+    fileId: '',
+    imgPhotoFileId: '',
+    fileList: [] as any[],
+    name: '',
+    contact: '',
+    cellPhone: '',
+    email: '',
+    address: '',
+    website: '',
+    remark: '',
+  };
+}
+</script>
+
+<template>
+  <Modal
+    v-model:open="isOpen"
+    :footer="null"
+    :title="
+      mode === 'add'
+        ? $t('enterpriseCustomers.modal.addTitle')
+        : $t('enterpriseCustomers.modal.editTitle')
+    "
+    width="800px"
+  >
+    <div class="overflow-y-auto p-6">
+      <div class="space-y-6">
+        <div class="flex items-center gap-4">
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.enterpriseLogo')
+            }}</label>
+            <Upload
+              v-model:file-list="formData.fileList"
+              :action="`/fileApi/File/UploadFiles?Authorization=${token}`"
+              :headers="{ Authorization: String(token) }"
+              :max-count="1"
+              list-type="picture-card"
+              @change="handleLogoUpload"
+            >
+              <div
+                class="flex h-[100px] w-[200px] items-center justify-center border-2 border-dashed"
+              >
+                <div class="text-center">
+                  <div class="text-4xl">+</div>
+                  <div class="text-sm text-gray-500">
+                    {{ $t('enterpriseCustomers.modal.uploadLogo') }}
+                  </div>
+                </div>
+              </div>
+            </Upload>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-6">
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.customerCode')
+            }}</label>
+            <Input
+              v-model:value="formData.code"
+              :disabled="props.mode === 'edit'"
+              :placeholder="$t('enterpriseCustomers.modal.enterCustomerCode')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.customerName')
+            }}</label>
+            <Input
+              v-model:value="formData.name"
+              :placeholder="$t('enterpriseCustomers.modal.enterCustomerName')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.contact')
+            }}</label>
+            <Input
+              v-model:value="formData.contact"
+              :placeholder="$t('enterpriseCustomers.modal.enterContact')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.cellPhone')
+            }}</label>
+            <Input
+              v-model:value="formData.cellPhone"
+              :placeholder="$t('enterpriseCustomers.modal.enterCellPhone')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.email')
+            }}</label>
+            <Input
+              v-model:value="formData.email"
+              :placeholder="$t('enterpriseCustomers.modal.enterEmail')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.website')
+            }}</label>
+            <Input
+              v-model:value="formData.website"
+              :placeholder="$t('enterpriseCustomers.modal.enterWebsite')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.address')
+            }}</label>
+            <Input
+              v-model:value="formData.address"
+              :placeholder="$t('enterpriseCustomers.modal.enterAddress')"
+            />
+          </div>
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium">{{
+              $t('enterpriseCustomers.modal.remark')
+            }}</label>
+            <Input
+              v-model:value="formData.remark"
+              :placeholder="$t('enterpriseCustomers.modal.enterRemark')"
+              :rows="3"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="flex justify-end gap-2 border-t pt-4">
+      <Button @click="handleCancel">
+        {{ $t('enterpriseCustomers.modal.cancel') }}
+      </Button>
+      <Button type="primary" @click="handleSave">
+        {{ $t('enterpriseCustomers.modal.save') }}
+      </Button>
+    </div>
+  </Modal>
+</template>
+
+<style lang="scss">
+.ant-menu-light .ant-menu-item-selected,
+.ant-menu-light > .ant-menu .ant-menu-item-selected {
+  background-color: #f4f2f2;
+
+  .ant-menu-title-content {
+    color: #462424;
+  }
+}
+
+.ant-table-wrapper .ant-table-tbody > tr > th,
+.ant-table-wrapper .ant-table-tbody > tr > td {
+  padding: 8px 16px;
+}
+
+.ant-table-wrapper .ant-table-thead > tr > th,
+.ant-table-wrapper .ant-table-thead > tr > td {
+  padding: 8px 16px;
+}
+</style>

+ 383 - 0
apps/web-velofex/src/views/dashboard/enterprise-customers/index.vue

@@ -0,0 +1,383 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+
+import { useUserStore } from '@vben/stores';
+
+import { $t } from '@/locales';
+import {
+  Button,
+  Dropdown,
+  Empty,
+  Input,
+  message,
+  Modal,
+  Pagination,
+} from 'antdv-next';
+
+import { deleteEnterpriseCustomerApi, getEnterpriseCustomersApi } from '#/api';
+
+import EnterpriseCustomersModal from './enterprise-customers-modal.vue';
+
+const modalOpen = ref(false);
+const modalMode = ref<'add' | 'edit'>('add');
+const currentCustomer = ref<any>(null);
+
+const userStore = useUserStore();
+const isLogin = computed(() => !!userStore.userInfo);
+
+const customerList = ref<any[]>([]);
+const loading = ref(false);
+
+const searchParams = ref({
+  name: '',
+  code: '',
+  isEnable: '',
+  currentPage: 1,
+  pageSize: 12,
+});
+
+const totalPage = ref(0);
+const totalCount = ref(0);
+
+function handleSearch() {
+  searchParams.value.currentPage = 1;
+  fetchEnterpriseCustomers();
+}
+
+function handleAddNew() {
+  modalMode.value = 'add';
+  currentCustomer.value = null;
+  modalOpen.value = true;
+}
+
+function handleEdit(item: any) {
+  modalMode.value = 'edit';
+  currentCustomer.value = item;
+  modalOpen.value = true;
+}
+
+function handleModalSave() {
+  modalOpen.value = false;
+  fetchEnterpriseCustomers();
+}
+
+function handleMenuClick({ key }: { key: string }, item: any) {
+  if (key === 'Edit') {
+    handleEdit(item);
+  } else if (key === 'Remove') {
+    Modal.confirm({
+      title: $t('enterpriseCustomers.deleteConfirm'),
+      content: $t('enterpriseCustomers.deleteDescription'),
+      okText: $t('btn.yes'),
+      okType: 'danger',
+      cancelText: $t('btn.no'),
+      onOk() {
+        deleteCustomer(item);
+      },
+      onCancel() {},
+    });
+  }
+}
+
+function handleBack() {
+  window.history.back();
+}
+
+function handleClear() {
+  searchParams.value.name = '';
+  searchParams.value.code = '';
+  searchParams.value.isEnable = '';
+  handleSearch();
+}
+
+function handlePageChange(page: number) {
+  searchParams.value.currentPage = page;
+  fetchEnterpriseCustomers();
+}
+
+async function fetchEnterpriseCustomers() {
+  if (!isLogin.value) {
+    return;
+  }
+
+  try {
+    loading.value = true;
+    const result = await getEnterpriseCustomersApi({
+      currentPage: searchParams.value.currentPage,
+      pageSize: searchParams.value.pageSize,
+      orderByProperty: 'Id',
+      Ascending: false,
+      filters: [
+        {
+          name: 'name',
+          value: searchParams.value.name,
+        },
+        {
+          name: 'code',
+          value: searchParams.value.code,
+        },
+        {
+          name: 'isEnable',
+          value: searchParams.value.isEnable,
+        },
+      ],
+    });
+    if (result?.result?.model) {
+      customerList.value = result.result.model;
+      totalPage.value = result.result.totalPages;
+      totalCount.value = result.result.totalCount;
+    }
+  } catch {}
+}
+
+async function deleteCustomer(item: any) {
+  if (!isLogin.value) {
+    return;
+  }
+
+  try {
+    loading.value = true;
+    const result = await deleteEnterpriseCustomerApi({
+      code: item.code,
+    });
+    if (result?.result) {
+      fetchEnterpriseCustomers();
+      message.success($t('enterpriseCustomers.deleteSuccess'));
+    }
+  } catch {}
+}
+
+watch(
+  () => isLogin.value,
+  (newValue) => {
+    if (newValue) {
+      fetchEnterpriseCustomers();
+    }
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <div class="p-5">
+    <div class="mb-4 flex items-center justify-between">
+      <div class="text-sm text-[#462424] text-gray-500">
+        {{ $t('enterpriseCustomers.breadcrumb') }}
+      </div>
+      <div
+        class="flex cursor-pointer items-center gap-[9px] text-[16px] font-bold text-[#462424]"
+        @click="handleBack"
+      >
+        <div
+          class="global-color flex h-[22px] w-[22px] items-center justify-center rounded-full"
+        >
+          <img
+            alt=""
+            class="h-[11px] w-[10px]"
+            src="@/assets/image/the-left.png"
+          />
+        </div>
+        {{ $t('btn.back') }}
+      </div>
+    </div>
+
+    <div class="mb-[21px] mt-[30px] text-[26px] font-bold text-[#462424]">
+      {{ $t('homeModule.enterpriseCustomers') }}
+    </div>
+
+    <div class="mb-4 flex flex-wrap items-center gap-4">
+      <div class="flex flex-col gap-1">
+        <label class="text-[11px] text-[#000]">{{
+          $t('enterpriseCustomers.customerName')
+        }}</label>
+        <Input
+          v-model:value="searchParams.name"
+          class="h-[42px] w-[220px] rounded-[11px] border-[#707070]"
+          placeholder=""
+        />
+      </div>
+      <div class="flex flex-col gap-1">
+        <label class="text-[11px] text-[#000]">{{
+          $t('enterpriseCustomers.customerCode')
+        }}</label>
+        <Input
+          v-model:value="searchParams.code"
+          class="h-[42px] w-[220px] rounded-[11px] border-[#707070]"
+          placeholder=""
+        />
+      </div>
+      <!-- <div class="flex flex-col gap-1">
+        <label class="text-[11px] text-[#000]">{{
+          $t('enterpriseCustomers.status')
+        }}</label>
+        <Select
+          v-model:value="searchParams.isEnable"
+          :options="[
+            { value: '1', label: $t('enterpriseCustomers.active') },
+            { value: '0', label: $t('enterpriseCustomers.inactive') }
+          ]"
+          placeholder="{{ $t('enterpriseCustomers.chooseStatus') }}"
+          class="h-[42px] w-[168px] rounded-[11px] border-[#707070] text-[12px]"
+        />
+      </div> -->
+      <div class="ml-auto flex items-center gap-2">
+        <Button class="h-[42px]" @click="handleSearch">
+          <svg
+            class="h-[21.5px] w-[21.5px] cursor-pointer"
+            fill="none"
+            viewBox="0 0 24 24"
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <circle
+              cx="11"
+              cy="11"
+              r="8"
+              stroke="currentColor"
+              stroke-width="2"
+            />
+            <path
+              d="M21 21L16.65 16.65"
+              stroke="currentColor"
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              stroke-width="2"
+            />
+          </svg>
+          {{ $t('btn.search') }}
+        </Button>
+        <Button class="h-[42px]" @click="handleClear">
+          <svg
+            class="h-[21.5px] w-[21.5px] cursor-pointer"
+            fill="none"
+            viewBox="0 0 24 24"
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <path
+              d="M3 6H5"
+              stroke="currentColor"
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              stroke-width="2"
+            />
+            <path
+              d="M19 6V20C19 21.1046 18.1046 22 17 22H7C5.89543 22 5 21.1046 5 20V6M8 6V4C8 3.79086 8.79086 3 10 3H14C15.2091 3 16 3.79086 16 6V8M10 11V17M14 11V17"
+              stroke="currentColor"
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              stroke-width="2"
+            />
+          </svg>
+          {{ $t('btn.reset') }}
+        </Button>
+        <Button class="h-[42px]" type="primary" @click="handleAddNew">
+          <img
+            alt=""
+            class="h-[21.5px] w-[21.5px] cursor-pointer"
+            src="@/assets/image/new.png"
+          />
+          {{ $t('btn.addNew') }}
+        </Button>
+      </div>
+    </div>
+
+    <!-- <div class="w-[722px] h-[auto] z-[-1] absolute top-[0] right-[0]">
+      <img src="@/assets/image/list1.png" class="w-full h-full object-cover" alt="">
+    </div> -->
+
+    <div v-if="loading" class="py-8 text-center">
+      {{ $t('enterpriseCustomers.loading') }}
+    </div>
+    <div
+      v-else-if="customerList.length === 0"
+      class="mt-[100px] py-8 text-center text-gray-500"
+    >
+      <Empty />
+    </div>
+    <div v-else class="mb-[76px] mt-[38px] flex flex-wrap gap-[18px]">
+      <div
+        v-for="item in customerList"
+        :key="item.id"
+        class="flex h-[78px] cursor-pointer items-center gap-[25px] rounded-[11px] bg-[#fff] px-[20px] shadow-md"
+      >
+        <img
+          v-if="item.imgPhotoFileId"
+          :src="`/File/Download?fileId=${item.imgPhotoFileId}`"
+          alt=""
+          class="h-[48px] w-auto object-contain"
+        />
+        <div
+          v-else
+          class="flex h-[48px] w-[48px] items-center justify-center rounded bg-gray-200"
+        >
+          <svg
+            class="h-6 w-6 text-gray-400"
+            fill="none"
+            viewBox="0 0 24 24"
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <path
+              d="M12 16V12M12 8H12.01M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
+              stroke="currentColor"
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              stroke-width="2"
+            />
+          </svg>
+        </div>
+        <!-- <div class="flex-1">
+          <div class="text-sm font-bold">{{ item.name }}</div>
+          <div class="text-xs text-gray-500">{{ item.code }}</div>
+          <div v-if="item.partnerUserName" class="text-xs text-gray-500">合作伙伴: {{ item.partnerUserName }}</div>
+        </div> -->
+        <Dropdown
+          :menu="{
+            items: [
+              { key: 'Edit', label: $t('enterpriseCustomers.edit') },
+              { key: 'Remove', label: $t('enterpriseCustomers.remove') },
+            ],
+          }"
+          placement="bottom"
+          @menu-click="(info: any) => handleMenuClick(info, item)"
+        >
+          <div class="flex cursor-pointer items-center gap-2">
+            <img
+              alt=""
+              class="h-[19px] w-[19px] cursor-pointer"
+              src="@/assets/image/more-tow.png"
+            />
+          </div>
+        </Dropdown>
+      </div>
+    </div>
+
+    <div class="list-pagination">
+      <Pagination
+        v-model:current="searchParams.currentPage"
+        :hide-on-single-page="true"
+        :page-size="12"
+        :show-size-changer="false"
+        :total="totalCount"
+        @change="handlePageChange"
+      />
+    </div>
+
+    <EnterpriseCustomersModal
+      v-model:open="modalOpen"
+      :customer-data="currentCustomer"
+      :mode="modalMode"
+      @save="handleModalSave"
+    />
+  </div>
+</template>
+
+<style lang="scss">
+.ant-pagination-item-active {
+  color: #7a003d !important;
+  background-color: #f8f2f5 !important;
+  border-color: transparent !important;
+
+  a {
+    color: #c48da8 !important;
+  }
+}
+</style>

+ 49 - 29
apps/web-velofex/src/views/dashboard/home/enterprise-customers.vue

@@ -1,12 +1,56 @@
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { useUserStore } from '@vben/stores';
 
 import { $t } from '@/locales';
 
+import { getEnterpriseCustomersApi } from '#/api';
+import { useLoginModalStore } from '#/store';
+
 const userStore = useUserStore();
 const isLogin = computed(() => !!userStore.userInfo);
+const router = useRouter();
+const loginModalStore = useLoginModalStore();
+const customerList = ref<any[]>([]);
+
+function navigateToEnterpriseCustomers() {
+  if (!isLogin.value) {
+    loginModalStore.open();
+    return;
+  }
+  router.push('/enterprise-customers');
+}
+
+async function fetchEnterpriseCustomers() {
+  if (!isLogin.value) {
+    return;
+  }
+
+  try {
+    const result = await getEnterpriseCustomersApi({
+      currentPage: 1,
+      pageSize: 6,
+      orderByProperty: 'Id',
+      Ascending: false,
+      filters: [],
+    });
+    if (result?.result?.model) {
+      customerList.value = result.result.model;
+    }
+  } catch {}
+}
+
+watch(
+  () => isLogin.value,
+  (newValue) => {
+    if (newValue) {
+      fetchEnterpriseCustomers();
+    }
+  },
+  { immediate: true },
+);
 </script>
 
 <template>
@@ -22,6 +66,7 @@ const isLogin = computed(() => !!userStore.userInfo);
         alt="more"
         class="h-[29px] w-[29px] cursor-pointer"
         src="@/assets/image/home-more.png"
+        @click="navigateToEnterpriseCustomers"
       />
     </div>
     <p
@@ -32,39 +77,14 @@ const isLogin = computed(() => !!userStore.userInfo);
     </p>
     <div v-else class="mt-[38px] flex flex-wrap gap-[18px]">
       <div
+        v-for="(item, index) in customerList"
+        :key="index"
         class="flex h-[37px] cursor-pointer items-center rounded-[11px] bg-[#fff] p-[6px_10px] shadow-md"
       >
         <img
+          :src="`/File/Download?fileId=${item.imgPhotoFileId}`"
           alt=""
           class="h-[24px] w-auto object-contain"
-          src="@/assets/image/partners5.png"
-        />
-      </div>
-      <div
-        class="flex h-[37px] cursor-pointer items-center rounded-[11px] bg-[#fff] p-[6px_10px] shadow-md"
-      >
-        <img
-          alt=""
-          class="h-[24px] w-auto object-contain"
-          src="@/assets/image/partners3.png"
-        />
-      </div>
-      <div
-        class="flex h-[37px] cursor-pointer items-center rounded-[11px] bg-[#fff] p-[6px_10px] shadow-md"
-      >
-        <img
-          alt=""
-          class="h-[24px] w-auto object-contain"
-          src="@/assets/image/partners1.png"
-        />
-      </div>
-      <div
-        class="flex h-[37px] cursor-pointer items-center rounded-[11px] bg-[#fff] p-[6px_10px] shadow-md"
-      >
-        <img
-          alt=""
-          class="h-[24px] w-auto object-contain"
-          src="@/assets/image/partners4.png"
         />
       </div>
     </div>

+ 5 - 1
apps/web-velofex/src/views/dashboard/sales-partners/sales-partners-modal.vue

@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import { computed, ref, watch } from 'vue';
 
+import { useAccessStore } from '@vben/stores';
+
 import { $t } from '@/locales';
 import {
   Button,
@@ -29,7 +31,9 @@ const emit = defineEmits<{
   (e: 'update:open', value: boolean): void;
 }>();
 
-const token = localStorage.getItem('token_a');
+const accessStore = useAccessStore();
+
+const token = accessStore.accessToken;
 
 const formData = ref({
   id: '',