Procházet zdrojové kódy

feat: 新增销售合作伙伴列表,弹窗新增,补充接口信息,补充翻译

lixuan před 1 měsícem
rodič
revize
f501d7a6a2

+ 181 - 0
apps/web-velofex/src/api/core/user.ts

@@ -222,6 +222,137 @@ export namespace UserApi {
     totalCount: number;
     totalPages: number;
   }
+
+  export interface PartnerFilter {
+    name: string;
+    value?: boolean | number | string;
+  }
+
+  export interface GetPartnersListParams {
+    currentPage: number;
+    pageSize: number;
+    orderByProperty?: string;
+    Ascending?: boolean;
+    totalPage?: number;
+    totalCount?: number;
+    filters?: PartnerFilter[];
+  }
+
+  export interface PartnerModel {
+    account: string;
+    id: string;
+    imgPhotoFileId: string;
+    langName: string;
+    name: string;
+    expiredTime?: string;
+    number0fBusinessScenarios: number;
+    number0fCSiteMaxUser: number;
+    number0fDesigners: number;
+    number0fEnterprise: number;
+    number0fPages: number;
+    number0fTables: number;
+    number0fWorkFlow: number;
+    status: boolean;
+    usedBusinessScenarios: number;
+    usedCSiteMaxUser: number;
+    usedDesigners: number;
+    usedEnterprise: number;
+    usedPages: number;
+    usedTables: number;
+    usedWorkFlow: number;
+  }
+
+  export interface GetPartnersListResult {
+    isSuccess: boolean;
+    code: number;
+    result: {
+      currentPage: number;
+      hasNextPage: boolean;
+      hasPreviousPage: boolean;
+      model: PartnerModel[];
+      pageSize: number;
+      totalCount: number;
+      totalPages: number;
+    };
+    isAuthorized: boolean;
+  }
+
+  export interface DeletePartnerResult {
+    isSuccess: boolean;
+    code: number;
+    result: boolean;
+    isAuthorized: boolean;
+  }
+
+  export interface CreatePartnerParams {
+    langNameList: Array<{
+      name: string;
+      value: string;
+    }>;
+    fileId?: string;
+    imgPhotoFileId?: string;
+    version?: string;
+    account?: string;
+    langName?: string;
+    password?: string;
+    expiredTime?: string;
+    number0fEnterprise?: number;
+    number0fPages?: number;
+    number0fDesigners?: number;
+    number0fCSiteMaxUser?: number;
+    number0fBusinessScenarios?: number;
+    number0fTables?: number;
+    number0fWorkFlow?: number;
+    status?: boolean;
+  }
+
+  export interface CreatePartnerResult {
+    isSuccess: boolean;
+    code: number;
+    result: boolean;
+    isAuthorized: boolean;
+  }
+
+  export interface UpdatePartnerParams {
+    id?: string;
+    langNameList: Array<{
+      name: string;
+      value: string;
+    }>;
+    fileId?: string;
+    imgPhotoFileId?: string;
+    version?: string;
+    account?: string;
+    langName?: string;
+    password?: string;
+    expiredTime?: string;
+    number0fEnterprise?: number;
+    number0fPages?: number;
+    number0fDesigners?: number;
+    number0fCSiteMaxUser?: number;
+    number0fBusinessScenarios?: number;
+    number0fTables?: number;
+    number0fWorkFlow?: number;
+    status?: boolean;
+  }
+
+  export interface UpdatePartnerResult {
+    isSuccess: boolean;
+    code: number;
+    result: boolean;
+    isAuthorized: boolean;
+  }
+
+  export interface GetLangByKeyResult {
+    isSuccess: boolean;
+    code: number;
+    result: {
+      en: string;
+      key: string;
+      'zh-CN': string;
+    };
+    isAuthorized: boolean;
+  }
 }
 
 /**
@@ -262,6 +393,56 @@ export async function getPartnersApi() {
   return requestClient.post('api/partner/AllList');
 }
 
+/**
+ * 获取合作伙伴列表
+ */
+export async function getPartnersListApi(data: UserApi.GetPartnersListParams) {
+  return requestClient.post<UserApi.GetPartnersListResult>(
+    '/api/partner/list',
+    data,
+  );
+}
+
+/**
+ * 创建合作伙伴
+ */
+export async function createPartnerApi(data: UserApi.CreatePartnerParams) {
+  return requestClient.post<UserApi.CreatePartnerResult>(
+    '/api/partner/doCreate',
+    data,
+  );
+}
+
+/**
+ * 更新合作伙伴
+ */
+export async function updatePartnerApi(data: UserApi.UpdatePartnerParams) {
+  return requestClient.post<UserApi.UpdatePartnerResult>(
+    '/api/partner/doUpdate',
+    data,
+  );
+}
+
+/**
+ * 获取语言详情
+ */
+export async function getLangByKeyApi(key: string) {
+  return requestClient.post<UserApi.GetLangByKeyResult>(
+    '/api/system/ListLangByKey',
+    { key },
+  );
+}
+
+/**
+ * 删除合作伙伴
+ */
+export async function deletePartnerApi(data: { id: string }) {
+  return requestClient.post<UserApi.DeletePartnerResult>(
+    '/api/partner/doDelete',
+    data,
+  );
+}
+
 /**
  * 获取可以克隆的项目
  */

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

@@ -14,15 +14,33 @@ import Avatar from './avatar.vue';
 
 // const loginModalStore = useLoginModalStore();
 
+const token = localStorage.getItem('token_a');
+
 const menus = computed(() => [
-  { title: $t('homeMenu.toolDownloads'), path: '/' },
-  { title: $t('homeMenu.aIGeneratedApps'), path: '/' },
-  { title: $t('homeMenu.aIAssistants'), path: '/' },
-  { title: $t('homeMenu.shaluAcademy'), path: '/' },
-  { title: $t('homeMenu.applicationMarket'), path: '/' },
+  { title: $t('homeMenu.toolDownloads'), path: '/Views/Tools/Index.html' },
+  {
+    title: $t('homeMenu.aIGeneratedApps'),
+    path: '/Views/Designer/AIGenerateApp.html',
+  },
+  {
+    title: $t('homeMenu.aIAssistants'),
+    path: '/Views/Designer/AIAssistant.html',
+  },
+  {
+    title: $t('homeMenu.shaluAcademy'),
+    path: `/Views/Account/SSOIndex.html?system=design&token=${token}`,
+  },
+  {
+    title: $t('homeMenu.applicationMarket'),
+    path: '/Views/Designer/systemAppStore.html?tab=application',
+  },
 ]);
 
 function openLogin() {}
+
+function handleClick(path: string) {
+  window.open(path, '_blank');
+}
 </script>
 
 <template>
@@ -33,6 +51,7 @@ function openLogin() {}
         v-for="menuItem in menus"
         :key="menuItem.title"
         class="flex items-center gap-2"
+        @click="handleClick(menuItem.path)"
       >
         <span>{{ menuItem.title }}</span>
         <SvgArrowRightIcon class="arrow size-2" />

+ 37 - 0
apps/web-velofex/src/locales/langs/en-US/page.json

@@ -186,5 +186,42 @@
       "cancel": "Cancel",
       "save": "Save"
     }
+  },
+  "salesPartners": {
+    "breadcrumb": "Dashboard / Sales Partners",
+    "name": "Partners Name:",
+    "account": "Partners Account:",
+    "loading": "Loading...",
+    "edit": "Edit",
+    "remove": "Remove",
+    "deleteConfirm": "Are you sure delete this partner?",
+    "deleteDescription": "Cannot be recovered after deletion",
+    "deleteSuccess": "Delete successfully!",
+    "saveSuccess": "Save successfully!",
+    "modal": {
+      "addTitle": "Add Partner",
+      "editTitle": "Edit Partner",
+      "enterpriseLogo": "Enterprise Logo",
+      "uploadLogo": "Upload Logo",
+      "account": "Account",
+      "enterAccount": "Please enter account",
+      "password": "Password",
+      "enterPassword": "Please enter password",
+      "nameCn": "Chinese Name",
+      "nameEn": "English Name",
+      "enterName": "Please enter name",
+      "expiredTime": "Valid Until",
+      "selectExpiredTime": "Please select valid date",
+      "isEnabled": "Is Enabled",
+      "number0fEnterprise": "Enterprise Count",
+      "number0fWorkFlow": "Process Count",
+      "number0fPages": "Page Count",
+      "number0fTables": "Table Count",
+      "number0fDesigners": "Designer Count",
+      "number0fBusinessScenarios": "Scenario Count",
+      "number0fCSiteMaxUser": "User Count",
+      "save": "Save",
+      "cancel": "Cancel"
+    }
   }
 }

+ 37 - 0
apps/web-velofex/src/locales/langs/zh-CN/page.json

@@ -186,5 +186,42 @@
       "cancel": "取消",
       "save": "保存"
     }
+  },
+  "salesPartners": {
+    "breadcrumb": "首页 / 销售合作伙伴",
+    "name": "合作伙伴名称:",
+    "account": "合作伙伴账号:",
+    "loading": "加载中...",
+    "edit": "编辑",
+    "remove": "删除",
+    "deleteConfirm": "确定要删除此合作伙伴吗?",
+    "deleteDescription": "删除后无法恢复",
+    "deleteSuccess": "删除成功!",
+    "saveSuccess": "保存成功!",
+    "modal": {
+      "addTitle": "添加合作伙伴",
+      "editTitle": "编辑合作伙伴",
+      "enterpriseLogo": "企业Logo",
+      "uploadLogo": "上传Logo",
+      "account": "账号",
+      "enterAccount": "请输入账号",
+      "password": "密码",
+      "enterPassword": "请输入密码",
+      "nameCn": "中文名称",
+      "nameEn": "英文名称",
+      "enterName": "请输入名称",
+      "expiredTime": "有效期",
+      "selectExpiredTime": "请选择有效期",
+      "isEnabled": "是否启用",
+      "number0fEnterprise": "企业数",
+      "number0fWorkFlow": "流程数",
+      "number0fPages": "页面数",
+      "number0fTables": "数据表",
+      "number0fDesigners": "设计人员数",
+      "number0fBusinessScenarios": "业务场景数",
+      "number0fCSiteMaxUser": "C端用户数",
+      "save": "保存",
+      "cancel": "取消"
+    }
   }
 }

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

@@ -43,6 +43,15 @@ const routes: RouteRecordRaw[] = [
           title: $t('homeModule.applicationManagement'),
         },
       },
+      {
+        name: 'SalesPartners',
+        path: '/sales-partners',
+        component: () => import('#/views/dashboard/sales-partners/index.vue'),
+        meta: {
+          icon: 'carbon:user-multiple',
+          title: $t('homeModule.salesPartners'),
+        },
+      },
     ],
   },
 ];

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

@@ -550,7 +550,7 @@ function resetFormData() {
                 v-model:file-list="formData.fileList"
                 :headers="{ Authorization: String(token) }"
                 :max-count="1"
-                action="http://a.dev.jbpm.shalu.com/fileApi/File/UploadFiles"
+                action="/fileApi/File/UploadFiles"
                 list-type="picture-card"
                 @change="handleLogoUpload"
               >

+ 49 - 29
apps/web-velofex/src/views/dashboard/home/sales-partners.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 { getPartnersListApi, type UserApi } from '#/api';
+
 const userStore = useUserStore();
 const isLogin = computed(() => !!userStore.userInfo);
+const router = useRouter();
+
+const partnerList = ref<UserApi.PartnerModel[]>([]);
+
+function handleMoreClick() {
+  router.push('/sales-partners');
+}
+
+async function fetchPartnerList() {
+  if (!isLogin.value) {
+    return;
+  }
+
+  try {
+    const result = await getPartnersListApi({
+      currentPage: 1,
+      pageSize: 6,
+      orderByProperty: 'name',
+      Ascending: true,
+      filters: [
+        {
+          name: 'status',
+          value: 1,
+        },
+      ],
+    });
+    if (result?.result?.model) {
+      partnerList.value = result.result.model;
+    }
+  } catch {}
+}
+
+watch(
+  () => isLogin.value,
+  (newValue) => {
+    if (newValue) {
+      fetchPartnerList();
+    }
+  },
+  { immediate: true },
+);
 </script>
 
 <template>
@@ -20,6 +64,7 @@ const isLogin = computed(() => !!userStore.userInfo);
         alt="more"
         class="h-[29px] w-[29px] cursor-pointer"
         src="@/assets/image/home-more.png"
+        @click="handleMoreClick"
       />
     </div>
     <p
@@ -30,39 +75,14 @@ const isLogin = computed(() => !!userStore.userInfo);
     </p>
     <div v-else class="mt-[38px] flex flex-wrap gap-[18px]">
       <div
+        v-for="partner in partnerList"
+        :key="partner.id"
         class="flex h-[37px] cursor-pointer items-center rounded-[11px] bg-[#fff] p-[6px_10px] shadow-md"
       >
         <img
+          :src="`/File/Download?fileId=${partner.imgPhotoFileId}`"
           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/partners2.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/partners4.png"
         />
       </div>
     </div>

+ 379 - 0
apps/web-velofex/src/views/dashboard/sales-partners/index.vue

@@ -0,0 +1,379 @@
+<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 { deletePartnerApi, getPartnersListApi, type UserApi } from '#/api';
+
+import SalesPartnersModal from './sales-partners-modal.vue';
+
+const modalOpen = ref(false);
+const modalMode = ref<'add' | 'edit'>('add');
+const currentPartner = ref<any>(null);
+
+const userStore = useUserStore();
+const isLogin = computed(() => !!userStore.userInfo);
+
+const partnerList = ref<UserApi.PartnerModel[]>([]);
+const loading = ref(false);
+
+const searchParams = ref({
+  name: '',
+  account: '',
+  status: 1,
+  currentPage: 1,
+  pageSize: 12,
+});
+
+const totalPage = ref(0);
+const totalCount = ref(0);
+
+function handleSearch() {
+  searchParams.value.currentPage = 1;
+  fetchPartnerList();
+}
+
+function handleAddNew() {
+  modalMode.value = 'add';
+  currentPartner.value = null;
+  modalOpen.value = true;
+}
+
+function handleEdit(item: any) {
+  modalMode.value = 'edit';
+  currentPartner.value = item;
+  modalOpen.value = true;
+}
+
+function handleModalSave() {
+  modalOpen.value = false;
+  fetchPartnerList();
+}
+
+function handleMenuClick({ key }: { key: string }, item: any) {
+  if (key === 'Edit') {
+    handleEdit(item);
+  } else if (key === 'Remove') {
+    Modal.confirm({
+      title: $t('salesPartners.deleteConfirm'),
+      content: $t('salesPartners.deleteDescription'),
+      okText: $t('btn.yes'),
+      okType: 'danger',
+      cancelText: $t('btn.no'),
+      onOk() {
+        deletePartner(item);
+      },
+      onCancel() {},
+    });
+  }
+}
+
+function handleBack() {
+  window.history.back();
+}
+
+function handleClear() {
+  searchParams.value.name = '';
+  searchParams.value.account = '';
+  searchParams.value.status = 1;
+  handleSearch();
+}
+
+function handlePageChange(page: number) {
+  searchParams.value.currentPage = page;
+  fetchPartnerList();
+}
+
+async function fetchPartnerList() {
+  if (!isLogin.value) {
+    return;
+  }
+
+  try {
+    loading.value = true;
+    const result = await getPartnersListApi({
+      currentPage: searchParams.value.currentPage,
+      pageSize: searchParams.value.pageSize,
+      orderByProperty: 'name',
+      Ascending: true,
+      filters: [
+        {
+          name: 'name',
+          value: searchParams.value.name,
+        },
+        {
+          name: 'account',
+          value: searchParams.value.account,
+        },
+        {
+          name: 'status',
+          value: searchParams.value.status,
+        },
+      ],
+    });
+    if (result?.result?.model) {
+      partnerList.value = result.result.model;
+      totalPage.value = result.result.totalPages;
+      totalCount.value = result.result.totalCount;
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function deletePartner(item: any) {
+  if (!isLogin.value) {
+    return;
+  }
+
+  try {
+    loading.value = true;
+    const result = await deletePartnerApi({
+      id: item.id,
+    });
+    if (result?.result) {
+      fetchPartnerList();
+      message.success($t('salesPartners.deleteSuccess'));
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+watch(
+  () => isLogin.value,
+  (newValue) => {
+    if (newValue) {
+      fetchPartnerList();
+    }
+  },
+  { 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('salesPartners.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.salesPartners') }}
+    </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('salesPartners.name')
+        }}</label>
+        <Input
+          v-model:value="searchParams.name"
+          class="h-[42px] w-[147px] rounded-[11px] border-[#707070]"
+          placeholder=""
+        />
+      </div>
+      <div class="flex flex-col gap-1">
+        <label class="text-[11px] text-[#000]">{{
+          $t('salesPartners.account')
+        }}</label>
+        <Input
+          v-model:value="searchParams.account"
+          class="h-[42px] w-[147px] rounded-[11px] border-[#707070]"
+          placeholder=""
+        />
+      </div>
+      <div class="ml-[86px] flex flex-col gap-1">
+        <label class="text-[11px] text-[#000]">{{
+          $t('applicationManagement.activateNow')
+        }}</label>
+        <div class="flex h-[42px] items-center gap-2">
+          <label
+            class="flex cursor-pointer items-center gap-2 text-sm"
+            @click="searchParams.status = 1"
+          >
+            <div
+              class="flex h-[20px] w-[20px] items-center justify-center rounded-[5px] border-[1px] border-[#707070]"
+            >
+              <div
+                v-if="searchParams.status === 1"
+                class="h-[14px] w-[14px] flex-shrink-0 rounded-[3px] bg-[#462424]"
+              ></div>
+            </div>
+            {{ $t('btn.yes') }}
+          </label>
+          <label
+            class="flex cursor-pointer items-center gap-2 text-sm"
+            @click="searchParams.status = 0"
+          >
+            <div
+              class="flex h-[20px] w-[20px] items-center justify-center rounded-[5px] border-[1px] border-[#707070]"
+            >
+              <div
+                v-if="searchParams.status === 0"
+                class="h-[14px] w-[14px] flex-shrink-0 rounded-[3px] bg-[#462424]"
+              ></div>
+            </div>
+            {{ $t('btn.no') }}
+          </label>
+        </div>
+      </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 v-if="loading" class="py-8 text-center">
+      {{ $t('salesPartners.loading') }}
+    </div>
+    <div
+      v-else-if="partnerList.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 partnerList"
+        :key="item.id"
+        class="flex h-[78px] cursor-pointer items-center gap-[25px] rounded-[11px] bg-[#fff] px-[20px] shadow-md"
+      >
+        <img
+          :src="`/File/Download?fileId=${item.imgPhotoFileId}`"
+          alt=""
+          class="h-[48px] w-auto object-contain"
+        />
+        <Dropdown
+          :menu="{
+            items: [
+              { key: 'Edit', label: $t('salesPartners.edit') },
+              { key: 'Remove', label: $t('salesPartners.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>
+
+    <SalesPartnersModal
+      v-model:open="modalOpen"
+      :mode="modalMode"
+      :partner-data="currentPartner"
+      @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>

+ 382 - 0
apps/web-velofex/src/views/dashboard/sales-partners/sales-partners-modal.vue

@@ -0,0 +1,382 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+
+import { $t } from '@/locales';
+import {
+  Button,
+  DatePicker,
+  Input,
+  message,
+  Modal,
+  Switch,
+  Upload,
+} from 'antdv-next';
+import MD5 from 'crypto-js/md5';
+import dayjs from 'dayjs';
+
+import { createPartnerApi, getLangByKeyApi, updatePartnerApi } from '#/api';
+
+interface Props {
+  open: boolean;
+  mode: 'add' | 'edit';
+  partnerData?: any;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  (e: 'save', data: any): void;
+  (e: 'update:open', value: boolean): void;
+}>();
+
+const token = localStorage.getItem('token_a');
+
+const formData = ref({
+  id: '',
+  logo: null,
+  fileId: '',
+  imgPhotoFileId: '',
+  fileList: [] as any[],
+  account: '',
+  password: '',
+  nameCn: '',
+  nameEn: '',
+  langName: '',
+  isEnabled: true,
+  expiredTime: null as any,
+  number0fEnterprise: 0,
+  number0fWorkFlow: 0,
+  number0fPages: 0,
+  number0fTables: 0,
+  number0fDesigners: 0,
+  number0fBusinessScenarios: 0,
+  number0fCSiteMaxUser: 0,
+});
+
+const isOpen = computed({
+  get: () => props.open,
+  set: (value) => emit('update:open', value),
+});
+
+watch(
+  () => props.open,
+  (newValue) => {
+    if (newValue && props.partnerData) {
+      fetchPartnerDetail();
+    } else if (newValue) {
+      resetForm();
+    }
+  },
+);
+
+async function fetchPartnerDetail() {
+  if (!props.partnerData) {
+    return;
+  }
+
+  formData.value.id = props.partnerData.id || '';
+  formData.value.account = props.partnerData.account || '';
+  formData.value.langName = props.partnerData.langName || '';
+  formData.value.isEnabled = props.partnerData.status ?? true;
+  formData.value.expiredTime = props.partnerData.expiredTime
+    ? dayjs(props.partnerData.expiredTime)
+    : null;
+  formData.value.number0fEnterprise = props.partnerData.number0fEnterprise || 0;
+  formData.value.number0fWorkFlow = props.partnerData.number0fWorkFlow || 0;
+  formData.value.number0fPages = props.partnerData.number0fPages || 0;
+  formData.value.number0fTables = props.partnerData.number0fTables || 0;
+  formData.value.number0fDesigners = props.partnerData.number0fDesigners || 0;
+  formData.value.number0fBusinessScenarios =
+    props.partnerData.number0fBusinessScenarios || 0;
+  formData.value.number0fCSiteMaxUser =
+    props.partnerData.number0fCSiteMaxUser || 0;
+
+  if (props.partnerData.imgPhotoFileId) {
+    formData.value.imgPhotoFileId = props.partnerData.imgPhotoFileId;
+    formData.value.fileList = [
+      {
+        uid: '-1',
+        name: 'logo.png',
+        status: 'done',
+        url: `/File/Download?fileId=${props.partnerData.imgPhotoFileId}`,
+      },
+    ];
+  }
+
+  if (props.mode === 'edit' && props.partnerData.langName) {
+    try {
+      const result = await getLangByKeyApi(props.partnerData.langName);
+      if (result?.result) {
+        formData.value.nameCn = result.result['zh-CN'] || '';
+        formData.value.nameEn = result.result.en || '';
+      }
+    } catch {}
+  } else {
+    formData.value.nameCn =
+      props.partnerData.langNameList?.find((item: any) => item.name === 'zh-CN')
+        ?.value || '';
+    formData.value.nameEn =
+      props.partnerData.langNameList?.find((item: any) => item.name === 'en')
+        ?.value || '';
+  }
+}
+
+function resetForm() {
+  formData.value = {
+    id: '',
+    logo: null,
+    fileId: '',
+    imgPhotoFileId: '',
+    fileList: [] as any[],
+    account: '',
+    password: '',
+    nameCn: '',
+    nameEn: '',
+    langName: '',
+    isEnabled: true,
+    expiredTime: null,
+    number0fEnterprise: 0,
+    number0fWorkFlow: 0,
+    number0fPages: 0,
+    number0fTables: 0,
+    number0fDesigners: 0,
+    number0fBusinessScenarios: 0,
+    number0fCSiteMaxUser: 0,
+  };
+}
+
+function handleLogoUpload(info: any) {
+  if (info.file.status === 'done') {
+    formData.value.logo = info.file;
+    if (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,
+    langNameList: [
+      {
+        name: 'zh-CN',
+        value: formData.value.nameCn,
+      },
+      {
+        name: 'en',
+        value: formData.value.nameEn,
+      },
+    ],
+    fileId: formData.value.fileId,
+    imgPhotoFileId: formData.value.imgPhotoFileId,
+    version: 'v1',
+    account: formData.value.account,
+    langName:
+      props.mode === 'edit' ? formData.value.langName : formData.value.nameCn,
+    password:
+      props.mode === 'add' ? MD5(formData.value.password).toString() : '',
+    expiredTime: formData.value.expiredTime
+      ? formData.value.expiredTime.format('YYYY-MM-DD')
+      : '',
+    number0fEnterprise: formData.value.number0fEnterprise,
+    number0fPages: formData.value.number0fPages,
+    number0fDesigners: formData.value.number0fDesigners,
+    number0fCSiteMaxUser: formData.value.number0fCSiteMaxUser,
+    number0fBusinessScenarios: formData.value.number0fBusinessScenarios,
+    number0fTables: formData.value.number0fTables,
+    number0fWorkFlow: formData.value.number0fWorkFlow,
+    status: formData.value.isEnabled,
+  };
+
+  try {
+    const result =
+      props.mode === 'add'
+        ? await createPartnerApi(data)
+        : await updatePartnerApi(data);
+
+    if (result?.isSuccess) {
+      message.success($t('salesPartners.saveSuccess'));
+      emit('save', data);
+    }
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+}
+
+function handleCancel() {
+  isOpen.value = false;
+}
+</script>
+
+<template>
+  <Modal
+    v-model:open="isOpen"
+    :footer="null"
+    :title="
+      mode === 'add'
+        ? $t('salesPartners.modal.addTitle')
+        : $t('salesPartners.modal.editTitle')
+    "
+    class="mt-[-50px]"
+    width="1200px"
+  >
+    <div class="">
+      <div class="flex-1 overflow-y-auto p-6">
+        <div class="space-y-4">
+          <div class="flex items-center gap-4">
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.enterpriseLogo')
+              }}</label>
+              <Upload
+                v-model:file-list="formData.fileList"
+                :headers="{ Authorization: String(token) }"
+                :max-count="1"
+                action="/fileApi/File/UploadFiles"
+                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('salesPartners.modal.uploadLogo') }}
+                    </div>
+                  </div>
+                </div>
+              </Upload>
+            </div>
+          </div>
+
+          <div class="grid grid-cols-2 gap-4">
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.account')
+              }}</label>
+              <Input
+                v-model:value="formData.account"
+                :placeholder="$t('salesPartners.modal.enterAccount')"
+              />
+            </div>
+            <div v-if="mode === 'add'" class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.password')
+              }}</label>
+              <Input
+                v-model:value="formData.password"
+                :placeholder="$t('salesPartners.modal.enterPassword')"
+              />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.nameCn')
+              }}</label>
+              <Input
+                v-model:value="formData.nameCn"
+                :placeholder="$t('salesPartners.modal.enterName')"
+              />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.nameEn')
+              }}</label>
+              <Input
+                v-model:value="formData.nameEn"
+                :placeholder="$t('salesPartners.modal.enterName')"
+              />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.expiredTime')
+              }}</label>
+              <DatePicker
+                v-model:value="formData.expiredTime"
+                :placeholder="$t('salesPartners.modal.selectExpiredTime')"
+                class="h-[32px] w-full"
+                format="YYYY-MM-DD"
+              />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.isEnabled')
+              }}</label>
+              <Switch v-model:checked="formData.isEnabled" class="w-[40px]" />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fEnterprise')
+              }}</label>
+              <Input
+                v-model:value="formData.number0fEnterprise"
+                type="number"
+              />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fWorkFlow')
+              }}</label>
+              <Input v-model:value="formData.number0fWorkFlow" type="number" />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fPages')
+              }}</label>
+              <Input v-model:value="formData.number0fPages" type="number" />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fTables')
+              }}</label>
+              <Input v-model:value="formData.number0fTables" type="number" />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fDesigners')
+              }}</label>
+              <Input v-model:value="formData.number0fDesigners" type="number" />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fBusinessScenarios')
+              }}</label>
+              <Input
+                v-model:value="formData.number0fBusinessScenarios"
+                type="number"
+              />
+            </div>
+            <div class="flex flex-col gap-2">
+              <label class="text-sm font-medium">{{
+                $t('salesPartners.modal.number0fCSiteMaxUser')
+              }}</label>
+              <Input
+                v-model:value="formData.number0fCSiteMaxUser"
+                type="number"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex justify-end gap-4">
+        <Button @click="handleCancel">
+          {{ $t('salesPartners.modal.cancel') }}
+        </Button>
+        <Button type="primary" @click="handleSave">
+          {{ $t('salesPartners.modal.save') }}
+        </Button>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<style lang="scss" scoped>
+:deep(.ant-upload-list-picture-card-container) {
+  .ant-upload-list-item {
+    width: 80px;
+    height: 80px;
+  }
+}
+</style>