Explorar el Código

增加身份绑定页面与身份等级管理页面

liuyuhang hace 5 meses
padre
commit
7b4be95222

+ 12 - 0
src/master/components/nav-bar/nav-bar-menu.ts

@@ -32,6 +32,18 @@ export default {
       IsShow: "1",
       Path: "/layout/diy-page",
     },
+    {
+      MenuName: "身份等级管理",
+      IsUse: "1",
+      IsShow: "1",
+      Path: "/layout/vip-table",
+    },
+    {
+      MenuName: "CRM身份绑定",
+      IsUse: "1",
+      IsShow: "1",
+      Path: "/layout/vip-crm",
+    },
     // 内嵌iframe
     // {
     //   MenuName: "分组中心",

+ 4 - 1
src/master/router/index.ts

@@ -2,7 +2,7 @@ import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
 import layout from "@/views/layout/router-view-comp.vue";
 import store from "@/store/index";
 import { loginRouter, errorRouter, iframeRouter } from "@kasite/intelmt-admin/router";
-import { Specification, Table } from "./routes";
+import { Specification, Table, VipTable, VipTableEdit, VipCrm } from "./routes";
 import { DiyPage } from "../../branch/hospital2/router/routes"
 
 const cache: Map<string, any> = new Map();
@@ -23,6 +23,9 @@ const router = createRouter({
         // 表格类型页面
         Table,
         DiyPage,
+        VipTableEdit,
+        VipTable,
+        VipCrm,
       ],
     },
   ] as Array<RouteRecordRaw>,

+ 29 - 0
src/master/router/routes.ts

@@ -18,3 +18,32 @@ export const Table: RouteRecordRaw = {
   },
   component: () => import("@/views/demo/table.vue"),
 };
+/** VIP处理页面 */
+export const VipTable: RouteRecordRaw = {
+  path: "vip-table",
+  name: "vipTable",
+  meta: {
+    title: "身份等级管理",
+  },
+  component: () => import("@/views/vip/vip-table.vue"),
+};
+
+/** VIP编辑页面 */
+export const VipTableEdit: RouteRecordRaw = {
+  path: "vip-edit",
+  name: "vipEdit",
+  meta: {
+    title: "身份等级编辑",
+  },
+  component: () => import("@/views/vip/vip-edit.vue"),
+};
+
+/** CRM身份级别绑定页面 */
+export const VipCrm: RouteRecordRaw = {
+  path: "vip-crm",
+  name: "vipCrm",
+  meta: {
+    title: "绑定CRM身份级别",
+  },
+  component: () => import("@/views/vip/vip_crm.vue"),
+};

+ 0 - 14
src/master/service/api/demo/index.ts

@@ -1,14 +0,0 @@
-import request from "@kasite/intelmt-admin/service/api/request";
-
-const apiUrl = {
-  QueryDictList: "wsgw/basic/dictApi/QueryDictList/callApiJSON.do",
-};
-
-/**
- * 查询字典配置
- * @param {*} reqData
- */
-export const QueryDictList = async (data: Object = {}) => {
-  const res = await request.doPost(apiUrl.QueryDictList, data);
-  return res;
-};

+ 74 - 0
src/master/service/api/vip/index.ts

@@ -0,0 +1,74 @@
+import request from "@kasite/intelmt-admin/service/api/request";
+
+const apiUrl = {
+  // 字典查询(示例接口)
+  QueryDictList: "wsgw/sys/dict/DataList/callApiJSON.do",
+  // VIP 身份等级相关接口(对应后端 IDiyVipService 上的 4 个方法)
+  InsertPatientLevel: "wsgw/diy/vip/InsertPatientLevel/callApiJSON.do",
+  QueryPatientLevel: "wsgw/diy/vip/QueryPatientLevel/callApiJSON.do",
+  UpdatePatientLevel: "wsgw/diy/vip/UpdatePatientLevel/callApiJSON.do",
+  DelPatientLevel: "wsgw/diy/vip/DelPatientLevel/callApiJSON.do",
+  // vip crm 绑定相关接口
+  SelectByCrmCode: "wsgw/diy/vip/SelectByCrmCode/callApiJSON.do",
+  UpdateByCrmCode: "wsgw/diy/vip/UpdateByCrmCode/callApiJSON.do",
+};
+
+/**
+ * 查询字典配置
+ */
+export const QueryDictList = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.QueryDictList, data);
+  return res;
+};
+
+/**
+ * 新增 VIP 身份等级
+ */
+export const InsertPatientLevel = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.InsertPatientLevel, data);
+  return res;
+};
+
+/**
+ * 查询 VIP 身份等级列表
+ */
+export const QueryPatientLevel = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.QueryPatientLevel, data);
+  return res;
+};
+
+/**
+ * 修改 VIP 身份等级
+ */
+export const UpdatePatientLevel = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.UpdatePatientLevel, data);
+  return res;
+};
+
+/**
+ * 删除 VIP 身份等级
+ */
+export const DelPatientLevel = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.DelPatientLevel, data);
+  return res;
+};
+
+/**
+ * 根据crm code查询vip身份等级
+ * @param data 
+ * @returns 
+ */
+export const SelectByCrmCode = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.SelectByCrmCode, data);
+  return res;
+};
+
+/**
+ * 更新crm code对应的vip身份等级
+ * @param data 
+ * @returns 
+ */
+export const UpdateByCrmCode = async (data: object = {}) => {
+  const res = await request.doPost(apiUrl.UpdateByCrmCode, data);
+  return res;
+};

+ 6 - 2
src/master/service/interface-entry.ts

@@ -1,5 +1,9 @@
 import base from "@kasite/intelmt-admin/service/api/base/base";
+import * as manage from "./api/demo/manage";
+import * as vip from "./api/vip";
 
 export default {
-	base
-}
+  base,
+  manage,
+  vip,
+};

+ 0 - 1
src/master/views/demo/table.vue

@@ -37,7 +37,6 @@
 
     <div class="fw mt-12 p_flexEnd">
       <zy-pagination
-        :page-size="table.page.PSize"
         :page-sizes="[10, 20, 30, 50, 100]"
         :pager-count="5"
         :total="table.page.PCount"

+ 456 - 0
src/master/views/vip/vip-edit.vue

@@ -0,0 +1,456 @@
+<template>
+  <div class="vip-edit-page">
+    <div class="page-inner">
+      <div class="card header-card">
+        <div class="edit-header">
+          <zy-button class="back-btn" type="primary" link @click="goBack">返回</zy-button>
+          <span class="edit-title">身份等级编辑</span>
+        </div>
+      </div>
+
+      <div class="edit-content">
+        <div class="card basic-card">
+          <div class="card-title">基本信息</div>
+          <zy-form label-position="top" class="basic-form">
+            <zy-form-item label="身份等级名称:">
+              <zy-input v-model="form.levelName" placeholder="请输入等级名称" />
+            </zy-form-item>
+            <zy-form-item v-if="isCreate" label="身份等级编号:">
+              <zy-input v-model="form.levelCode" placeholder="请输入等级编号" />
+            </zy-form-item>
+            <zy-form-item label="身份等级图标:">
+              <zy-upload
+                :action="fileUpdateUrl"
+                :before-upload="beforeAvatarUpload"
+                :file-list="iconFileList"
+                :headers="fileUpdateHeaders"
+                :limit="5"
+                :on-exceed="handlePictureExceed"
+                :on-preview="handlePictureCardPreview"
+                :on-success="handleUploadSuccess"
+                class="avatar-uploader"
+                list-type="picture-card"
+                name="newsFile"
+              >
+                <zy-icon class="avatar-uploader-icon">
+                  <el-icon-plus />
+                </zy-icon>
+              </zy-upload>
+            </zy-form-item>
+            <zy-form-item label="全流程主题:">
+              <zy-select v-model="form.theme">
+                <zy-option
+                  v-for="item in themeOptions"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </zy-select>
+            </zy-form-item>
+            <zy-form-item label="当前状态:">
+              <zy-select v-model="form.status">
+                <zy-option label="上架" value="1" />
+                <zy-option label="下架" value="0" />
+              </zy-select>
+            </zy-form-item>
+          </zy-form>
+          <zy-button type="primary" class="save-btn" @click="handleSave">保存信息</zy-button>
+          <zy-button 
+            v-if="!isCreate" 
+            class="delete-btn" 
+            link 
+            type="primary" 
+            @click="handleDelete"
+          >
+            删除身份等级
+          </zy-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+
+const route = useRoute();
+const router = useRouter();
+
+const isCreate = computed(() => route.query.mode !== "edit");
+
+const form = reactive({
+  levelName: "", // 新建时为空,编辑时在 onMounted 里填充
+  levelCode: "",
+  iconTheme: "tag-vip",
+  levelIconLabel: "",
+  shelfText: "上架",
+  theme: "", // 真实值由主题字典返回
+  status: "1", // 1=上架, 0=下架
+});
+
+const goBack = () => {
+  router.back();
+};
+
+const toggleShelf = () => {
+  if (form.shelfText === "上架") {
+    form.shelfText = "下架";
+    form.status = "0";
+  } else {
+    form.shelfText = "上架";
+    form.status = "1";
+  }
+};
+
+const { proxy } = getCurrentInstance() as any;
+
+// 上传相关状态
+const iconFileList = ref<any[]>([]);
+const viewPictureUrl = ref("");
+const viewPictureModal = ref(false);
+
+// 主题下拉数据
+const themeOptions = ref<{ label: string; value: string }[]>([]);
+
+// 从全局配置里取后端 baseURL
+const baseURL = (window as any).config?.baseURL || "";
+
+// 上传地址和头信息
+const fileUpdateUrl = computed(
+  () => `${baseURL}/upload/uploadFile.do?fileDir=identityLevel`
+);
+const fileUpdateHeaders = computed(() => ({
+  token: proxy.$toolEntry.cookie.get("token"),
+}));
+
+// 图片超出数量
+const handlePictureExceed = () => {
+  proxy.$message.error("图片超出上传个数限制");
+};
+
+// 预览
+const handlePictureCardPreview = (file: any) => {
+  viewPictureUrl.value = file.url;
+  viewPictureModal.value = true;
+};
+
+// 上传前校验
+const beforeAvatarUpload = (file: any) => {
+  const isImage =
+    file.type === "image/jpeg" || file.type === "image/png";
+  const isLt2M = file.size / 1024 / 1024 < 2;
+
+  if (!isImage) {
+    proxy.$message.error("图片格式不正确!");
+  }
+  if (!isLt2M) {
+    proxy.$message.error("图片大小不能超过 2MB!");
+  }
+  return isImage && isLt2M;
+};
+
+// 上传成功
+const handleUploadSuccess = (response: any, file: any) => {
+  
+  if (response.RespCode !== 10000) {
+    proxy.$message.error(response.RespMessage || "上传失败");
+  } else {
+    // 把所有的反斜杠统一替换成正斜杠
+    const url = (response.url || "").replace(/\\+/g, "/");
+    file.url = url;
+
+    // 把图标地址存到表单里,保存时一并提交
+    form.levelIconLabel = url;
+  }
+};
+
+// 保存(新建 / 修改)
+const handleSave = async () => {
+  if (!form.levelName) {
+    proxy.$message.warning("请输入身份等级名称");
+    return;
+  }
+  if (isCreate.value && !form.levelCode) {
+    proxy.$message.warning("请输入身份等级编号");
+    return;
+  }
+
+  const payload: any = {
+    IdLevelName: form.levelName,
+    IdLevelCode: form.levelCode,
+    IdLevelIcon: form.levelIconLabel,
+    Theme: form.theme,
+    Status: Number(form.status), // 直接转换为数字:1=上架, 0=下架
+  };
+
+  try {
+    let resWrap;
+    console.log(payload);
+    
+    if (isCreate.value) {
+      // 新建
+      ({ res: resWrap } = await proxy.$interfaceEntry.vip.InsertPatientLevel(payload));
+    } else {
+      // 修改
+      ({ res: resWrap } = await proxy.$interfaceEntry.vip.UpdatePatientLevel(payload));
+    }
+    const data = resWrap?.data || {};
+    if (data.RespCode == 10000) {
+      proxy.$message.success(isCreate.value ? "新增成功" : "修改成功");
+      router.back();
+    } else {
+      proxy.$message.error(data.errorMsg || data.msg || "保存失败");
+    }
+  } catch (e) {
+    proxy.$message.error("保存失败");
+  }
+};
+
+// 删除身份等级
+const handleDelete = async () => {
+  if (!form.levelCode) {
+    proxy.$message.warning("无法获取身份等级编号");
+    return;
+  }
+
+  try {
+    // 确认删除
+    await proxy.$messageBox.confirm("确定要删除该身份等级吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning",
+    });
+
+    // 用户确认后执行删除
+    const { res } = await proxy.$interfaceEntry.vip.DelPatientLevel({
+      IdLevelCode: form.levelCode,
+    });
+
+    const data = res?.data || {};
+    if (data.RespCode == 10000) {
+      proxy.$message.success("删除成功");
+      router.back();
+    } else {
+      proxy.$message.error(data.errorMsg || data.msg || "删除失败");
+    }
+  } catch (e: any) {
+    // 用户取消删除时,confirm 会 reject,这里不处理
+    // 如果是其他错误,提示删除失败
+    if (e && e !== "cancel" && typeof e !== "string") {
+      proxy.$message.error("删除失败");
+    }
+  }
+};
+
+// 查询主题字典
+const queryTheme = async () => {
+  try {
+    const { res } = await proxy.$interfaceEntry.vip.QueryDictList({
+      dictType: "VipColor",
+    });
+    const data = res.data || {};
+    const list =
+      data.Data ||
+      data.data ||
+      data.result ||
+      data.list ||
+      [];
+
+    if (Array.isArray(list) && list.length) {
+      themeOptions.value = list.map((item: any) => ({
+        label:
+          item.dictLabel,
+        value:
+          item.dictValue 
+      }));
+      if (!form.theme) {
+        form.theme = themeOptions.value[0].value;
+      }
+    } else {
+      themeOptions.value = [
+        { label: "金色", value: "gold" },
+        { label: "银色", value: "silver" },
+        { label: "蓝色", value: "blue" },
+      ];
+      if (!form.theme) {
+        form.theme = themeOptions.value[0].value;
+      }
+    }
+  } catch (e) {
+    themeOptions.value = [
+      { label: "金色", value: "gold" },
+      { label: "银色", value: "silver" },
+      { label: "蓝色", value: "blue" },
+    ];
+    if (!form.theme) {
+      form.theme = themeOptions.value[0].value;
+    }
+  }
+};
+
+onMounted(() => {
+  // 如果是编辑,从路由参数里回填表单
+  if (!isCreate.value) {
+    form.levelName = (route.query.name as string) || "";
+    form.levelCode = (route.query.code as string) || "";
+    form.theme = (route.query.theme as string) || "";
+    form.status = (route.query.status as string) || "1"; // 1=上架, 0=下架
+    form.levelIconLabel = (route.query.icon as string) || "";
+    if (form.levelIconLabel) {
+      iconFileList.value = [
+        {
+          name: "icon",
+          url: form.levelIconLabel,
+        },
+      ];
+    }
+  }
+
+  queryTheme();
+});
+</script>
+
+<style scoped lang="scss">
+.vip-edit-page {
+  min-height: 100%;
+  padding: 24px 0 32px;
+  background: #f5f7fb;
+  box-sizing: border-box;
+}
+
+.page-inner {
+  width: 1600px;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.card {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 6px 24px rgba(44, 69, 105, 0.08);
+  padding: 20px 24px;
+  box-sizing: border-box;
+}
+
+.header-card {
+  padding: 12px 20px;
+}
+
+.edit-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.edit-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.edit-content {
+  display: flex;
+  gap: 16px;
+  flex: 1;
+  align-items: flex-start;
+}
+
+.basic-card {
+  width: 300px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.basic-form :deep(.el-form-item) {
+  margin-bottom: 20px;
+}
+
+.basic-form :deep(.el-form-item__label) {
+  line-height: 20px;
+  padding-bottom: 6px;
+  color: #606266;
+  font-weight: 500;
+}
+
+.basic-form :deep(.el-form-item__content) {
+  flex: none;
+}
+
+.basic-form :deep(.el-input__wrapper),
+.basic-form :deep(.el-select) {
+  width: 260px;
+}
+
+.icon-field {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.status-link {
+  color: #409eff;
+  cursor: pointer;
+}
+
+.save-btn {
+  width: 100%;
+  background: var(--el-color-primary);
+  border-color: var(--el-color-primary);
+}
+
+.delete-btn {
+  align-self: center;
+  color: var(--el-color-primary);
+}
+
+.level-tag {
+  display: inline-block;
+  min-width: 48px;
+  text-align: center;
+  padding: 4px 12px;
+  border-radius: 6px;
+  color: #fff;
+  font-weight: 600;
+}
+
+.tag-vip {
+  background: #f69c45;
+}
+
+/* 图片上传样式:图片卡片+加号按钮 */
+.avatar-uploader :deep(.el-upload) {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  width: 86px;
+  height: 86px;
+  background-color: #fafafa;
+}
+
+.avatar-uploader :deep(.el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 86px;
+  height: 86px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-uploader :deep(.el-upload-list__item) {
+  width: 86px;
+  height: 86px;
+}
+</style>
+

+ 429 - 0
src/master/views/vip/vip-table.vue

@@ -0,0 +1,429 @@
+<template>
+  <div class="vip-page">
+    <div class="page-inner">
+      <div class="page-title">患者身份等级管理</div>
+
+      <!-- 搜索模块 -->
+      <div class="card search-card">
+        <div class="search-fields">
+          <zy-form inline class="fw search-form">
+            <zy-form-item label="权益名称:">
+              <zy-input
+                v-model="queryData.benefitName"
+                placeholder="请输入权益名称"
+                clearable
+              />
+      </zy-form-item>
+
+            <zy-form-item label="当前状态:">
+              <zy-select
+                v-model="queryData.status"
+                placeholder="全部"
+                clearable
+              >
+                <zy-option
+                  v-for="item in statusOptions"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+        </zy-select>
+      </zy-form-item>
+    </zy-form>
+
+          <div class="search-actions">
+            <zy-button class="btn-search" type="primary" @click="getTableData()">
+              查询
+            </zy-button>
+            <zy-button class="btn-create" @click="handleCreate">
+              + 新建服务权益包
+            </zy-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 表格模块 -->
+      <div class="card table-card" v-loading="table.loading">
+        <zy-table
+          height="100%"
+          :data="table.data"
+          header-align="center"
+          align="center"
+          :border="true"
+          :fit="true"
+        >
+          <zy-table-column
+            prop="levelCode"
+            label="身份等级编号"
+          />
+          <zy-table-column
+            prop="levelIconLabel"
+            label="身份等级图标"
+          >
+            <template #default="{ row }">
+              <img
+                v-if="row.levelIconLabel"
+                :src="getImageUrl(row.levelIconLabel)"
+                class="level-icon-img"
+                alt="身份等级图标"
+              />
+              <span v-else class="level-tag" :class="row.iconTheme">{{ row.levelIconLabel || "VIP" }}</span>
+            </template>
+          </zy-table-column>
+          <zy-table-column
+            prop="levelName"
+            label="身份等级名称"
+          />
+          <zy-table-column
+            prop="theme"
+            label="主题"
+          />
+          <zy-table-column
+            prop="statusLabel"
+            label="当前状态"
+          />
+          <zy-table-column
+            label="操作"
+          >
+            <template #default="{ row }">
+              <zy-button type="text" class="link-btn" @click="handleEdit(row)">编辑</zy-button>
+            </template>
+          </zy-table-column>
+      </zy-table>
+    </div>
+
+      <!-- 分页 -->
+      <div class="card pagination-card">
+      <zy-pagination
+        :page-size="table.page.PSize"
+        :total="table.page.PCount"
+          :pager-count="7"
+          layout="pager"
+          v-model:currentPage="table.page.PIndex"
+        @current-change="getTableData"
+      ></zy-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getCurrentInstance, reactive, onMounted } from "vue";
+import { useStore } from "vuex";
+import { useRoute, useRouter } from "vue-router";
+
+const { proxy } = getCurrentInstance() as any;
+const store = useStore();
+const route = useRoute();
+const router = useRouter();
+
+// 搜索条件
+let queryData = reactive({
+  benefitName: "",
+  status: "",
+});
+
+const statusOptions = [
+  { label: "全部", value: "" },
+  { label: "上架", value: "1" },
+  { label: "下架", value: "0" },
+];
+
+const table = reactive({
+  loading: false,
+  data: [] as any[],
+  page: {
+    PIndex: 1,
+    PSize: 10,
+    PCount: 0,
+  },
+});
+
+/* 获取表格数据 */
+const getTableData = async (e: any = undefined) => {
+  !e && (table.page.PIndex = 1);
+  table.loading = true;
+  try {
+    // 直接使用 status 值,空字符串转为 undefined,其他转为数字
+    const status = queryData.status === "" ? undefined : Number(queryData.status);
+
+    const { res } = await proxy.$interfaceEntry.vip.QueryPatientLevel({
+      IdLevelName: queryData.benefitName || undefined,
+      Status: status,
+      PageIndex: table.page.PIndex,
+      PageSize: table.page.PSize,
+    });
+    
+    if (res.data?.RespCode != 10000) {
+      proxy.$message.error(res.data?.errorMsg || res.data?.msg || "查询失败");
+      table.data = [];
+      table.page.PCount = 0;
+      return;
+    }
+
+    // 数据结构:res.data.Data[0].DataList 是实际的数据列表
+    const dataWrapper = res.data.Data?.[0] || {};
+    const list = dataWrapper.DataList || res.data.Data || res.data.list || res.data.List || [];
+    
+    // 分页信息:从 Page 对象获取
+    const pageInfo = res.data.Page || {};
+    const totalCount = pageInfo.PCount || dataWrapper.Total || res.data.PCount || res.data.total || list.length;
+
+    table.page.PCount = totalCount;
+    table.data = list.map((item: any) => {
+      // 后端返回的字段可能是 IdLevelIcon(大写)或 idLevelIcon(小写)
+      const iconPath = item.IdLevelIcon || item.idLevelIcon || "";
+
+      return {
+        levelCode: item.IdLevelCode || item.idLevelCode,
+        levelIconLabel: iconPath,
+        levelName: item.IdLevelName || item.idLevelName,
+        theme: item.Theme || item.theme,
+        statusLabel: (item.Status || item.status) === 1 ? "上架" : "下架",
+        // 保存原始数据,编辑时可能需要
+        rawData: item,
+      };
+    });
+  } catch (err) {
+    table.data = [];
+    table.page.PCount = 0;
+    proxy.$message.error("查询失败");
+  } finally {
+    table.loading = false;
+  }
+};
+
+/** 重置搜索条件 */
+const resetData = () => {
+  Object.keys(queryData).forEach((key: any) => {
+    (queryData as any)[key] = "";
+  });
+  getTableData();
+};
+
+const handleCreate = () => {
+  router.push({
+    name: "vipEdit",
+    query: {
+      mode: "create",
+    },
+  });
+};
+
+// 获取图片完整 URL
+const getImageUrl = (iconPath: string) => {
+  if (!iconPath) return "";
+  // 如果路径不是以 http:// 或 https:// 或 / 开头,加上 / 前缀
+  if (!iconPath.startsWith("http://") && !iconPath.startsWith("https://") && !iconPath.startsWith("/")) {
+    return "/" + iconPath;
+  }
+  return iconPath;
+};
+
+const handleEdit = (row: any) => {
+  router.push({
+    name: "vipEdit",
+    query: {
+      mode: "edit",
+      code: row.levelCode,
+      name: row.levelName,
+      theme: row.theme,
+      status: row.statusLabel === "上架" ? "1" : "0",
+      icon: row.levelIconLabel,
+    },
+  });
+};
+
+onMounted(() => {
+  getTableData();
+});
+</script>
+
+<style scoped>
+.vip-page {
+  min-height: 100%;
+  background: #f5f7fb;
+  padding: 24px 0 32px;
+  box-sizing: border-box;
+}
+
+.page-inner {
+  width: 1560px;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.page-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.card {
+  background: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 6px 24px rgba(44, 69, 105, 0.08);
+  padding: 18px 24px;
+  box-sizing: border-box;
+}
+
+.search-card {
+  display: flex;
+  flex-direction: column;
+}
+
+.search-fields {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  flex-wrap: nowrap;
+  gap: 16px;
+}
+
+.search-form {
+  display: flex;
+  align-items: center;
+}
+
+.search-form :deep(.el-form-item) {
+  margin: 0 16px 0 0;
+}
+
+.search-form :deep(.el-form-item__label) {
+  color: #555;
+}
+
+.search-form :deep(.el-input__inner) {
+  width: 220px;
+}
+
+.search-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-left: auto;
+  white-space: nowrap;
+}
+
+.btn-search {
+  background: var(--el-color-primary);
+  border-color: var(--el-color-primary);
+  color: #fff;
+  padding: 10px 32px;
+}
+
+.btn-create {
+  background: var(--el-color-primary-light-9);
+  border-color: var(--el-color-primary-light-7);
+  color: var(--el-color-primary);
+  padding: 10px 24px;
+}
+
+.table-card {
+  padding: 0;
+  overflow: hidden;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.table-card :deep(.el-table) {
+  width: 100%;
+  border: none;
+  flex: 1;
+}
+
+.table-card :deep(.el-table__header th),
+.table-card :deep(.el-table__body td) {
+  text-align: center;
+  font-size: 14px;
+}
+
+.table-card :deep(.el-table__body-wrapper),
+.table-card :deep(.el-table__header-wrapper) {
+  padding: 0;
+}
+
+/* 按 3:3:8:2:2:2 比例填满整行(共 20 份) */
+.table-card :deep(.el-table__header colgroup col:nth-child(1)),
+.table-card :deep(.el-table__body colgroup col:nth-child(1)) {
+  width: 15% !important; /* 3/20 */
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(2)),
+.table-card :deep(.el-table__body colgroup col:nth-child(2)) {
+  width: 15% !important; /* 3/20 */
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(3)),
+.table-card :deep(.el-table__body colgroup col:nth-child(3)) {
+  width: 40% !important; /* 8/20 */
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(4)),
+.table-card :deep(.el-table__body colgroup col:nth-child(4)) {
+  width: 10% !important; /* 2/20 */
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(5)),
+.table-card :deep(.el-table__body colgroup col:nth-child(5)) {
+  width: 10% !important; /* 2/20 */
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(6)),
+.table-card :deep(.el-table__body colgroup col:nth-child(6)) {
+  width: 10% !important; /* 2/20 */
+}
+
+.level-icon-img {
+  width: 60px;
+  height: 60px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
+.level-tag {
+  display: inline-block;
+  padding: 6px 18px;
+  border-radius: 6px;
+  font-size: 14px;
+  color: #fff;
+  min-width: 60px;
+}
+
+.tag-vip {
+  background: #f69c45;
+}
+.tag-svip {
+  background: #f67f45;
+}
+.tag-wip {
+  background: #f45d45;
+}
+
+.link-btn {
+  color: #2e74c6;
+}
+
+.pagination-card {
+  display: flex;
+  justify-content: center;
+  padding: 16px;
+  margin-top: auto;
+  margin-bottom: 0;
+}
+
+.pagination-card :deep(.el-pagination.is-background .el-pager li) {
+  background-color: #fff;
+  border: 1px solid #dcdfe6;
+  color: var(--el-text-color-regular);
+  margin: 0 6px;
+  border-radius: 6px;
+  min-width: 42px;
+  height: 34px;
+  line-height: 34px;
+}
+
+.pagination-card :deep(.el-pagination.is-background .el-pager li.is-active) {
+  background-color: var(--el-color-primary);
+  border-color: var(--el-color-primary);
+  color: #fff;
+}
+</style>

+ 422 - 0
src/master/views/vip/vip_crm.vue

@@ -0,0 +1,422 @@
+<template>
+  <div class="vip-page">
+    <div class="page-inner">
+      <div class="page-title">CRM身份绑定</div>
+
+      <!-- 表格模块 -->
+      <div class="card table-card" v-loading="loading">
+        <zy-table
+          height="100%"
+          :data="tableData"
+          header-align="center"
+          align="center"
+          :border="true"
+          :fit="true"
+        >
+          <zy-table-column prop="levelCode" label="身份等级编号" />
+          <zy-table-column prop="levelName" label="身份等级名称" />
+          <zy-table-column prop="crmLevel" label="CRM身份级别" />
+          <zy-table-column label="操作">
+            <template #default="{ row }">
+              <zy-button type="text" class="link-btn" @click="openCrmDialog(row)">设置</zy-button>
+            </template>
+          </zy-table-column>
+        </zy-table>
+      </div>
+
+      <!-- 分页 -->
+      <div class="card pagination-card">
+        <zy-pagination
+          :page-size="page.PSize"
+          :total="page.PCount"
+          :pager-count="7"
+          layout="pager"
+          v-model:currentPage="page.PIndex"
+          @current-change="handlePageChange"
+        ></zy-pagination>
+      </div>
+    </div>
+
+    <zy-dialog
+      v-model="crmDialog.visible"
+      title="绑定CRM身份级别"
+      width="520px"
+      class="crm-dialog"
+    >
+      <div class="dialog-content">
+        <zy-form label-width="120px" class="dialog-form">
+          <zy-form-item label="身份等级名称">
+            <span class="form-value">{{ crmDialog.form.levelName }}</span>
+          </zy-form-item>
+          <zy-form-item label="CRM身份级别">
+            <zy-select
+              v-model="crmDialog.form.crmLevel"
+              placeholder="请选择CRM身份级别"
+              style="width: 100%"
+            >
+              <zy-option
+                v-for="item in crmLevelOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </zy-select>
+          </zy-form-item>
+        </zy-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <zy-button @click="handleCrmDialogClose">取消</zy-button>
+          <zy-button type="primary" @click="handleCrmConfirm">确定</zy-button>
+        </div>
+      </template>
+    </zy-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getCurrentInstance, onMounted, reactive, ref } from "vue";
+
+const { proxy } = getCurrentInstance() as any;
+
+// 表格数据
+const tableData = ref<any[]>([]);
+
+// 加载状态
+const loading = ref(false);
+
+// 分页信息
+const page = reactive({
+  PIndex: 1,
+  PSize: 10,
+  PCount: 0,
+});
+
+const defaultCrmOptions = [
+  { label: "VIP", value: "VIP" },
+  { label: "SVIP", value: "SVIP" },
+];
+const crmLevelOptions = ref<Array<{ label: string; value: string }>>([...defaultCrmOptions]);
+
+// 查询CRM级别字典
+const queryCrmLevel = async () => {
+  try {
+    const { res } = await proxy.$interfaceEntry.vip.QueryDictList({
+      dictType: "CrmLevel",
+    });
+    const data = res.data || {};
+    
+    // 支持多种可能的数据结构
+    const list =
+      data.Data ||
+      data.data ||
+      data.result ||
+      data.list ||
+      [];
+
+    if (Array.isArray(list) && list.length) {
+      crmLevelOptions.value = list.map((item: any) => ({
+        label: item.dictLabel || item.label,
+        value: item.dictValue || item.value,
+      }));
+    } else {
+      crmLevelOptions.value = [...defaultCrmOptions];
+    }
+  } catch (e) {
+    console.error("查询CRM级别字典失败:", e);
+    // 如果查询失败,使用默认值
+    crmLevelOptions.value = [...defaultCrmOptions];
+  }
+};
+
+
+const crmDialog = reactive({
+  visible: false,
+  form: {
+    levelCode: "",
+    levelName: "",
+    crmLevel: "",
+  },
+});
+
+/* 获取表格数据(调用 CRM 查询方法) */
+const getTableData = async () => {
+  loading.value = true;
+  try {
+    const { res } = await proxy.$interfaceEntry.vip.SelectByCrmCode({
+      pageIndex: page.PIndex,
+      pageSize: page.PSize,
+    });
+
+    const data = res.data || {};
+    if (data.RespCode != 10000) {
+      proxy.$message.error(data.errorMsg || data.msg || "查询失败");
+      tableData.value = [];
+      page.PCount = 0;
+      return;
+    }
+
+    // 数据结构:res.data.Data[0].DataList 是实际的数据列表
+    const dataWrapper = data.Data?.[0] || {};
+    const list = dataWrapper.DataList || data.Data || [];
+
+    // 分页信息:从 Page 对象获取
+    const pageInfo = data.Page || {};
+    const totalCount = pageInfo.PCount || dataWrapper.Total || data.PCount || list.length;
+
+    page.PIndex = pageInfo.PIndex || page.PIndex;
+    page.PSize = pageInfo.PSize || page.PSize;
+    page.PCount = totalCount;
+
+    tableData.value = list.map((item: any) => ({
+      levelCode: item.IdLevelCode || item.idLevelCode,
+      levelName: item.IdLevelName || item.idLevelName,
+      crmLevel: item.CrmName || item.crmName || "未绑定",
+    }));
+  } catch (err) {
+    proxy.$message.error("查询失败");
+    tableData.value = [];
+    page.PCount = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+
+const openCrmDialog = (row: any) => {
+  crmDialog.form = {
+    levelCode: row.levelCode,
+    levelName: row.levelName,
+    crmLevel: row.crmLevel,
+  };
+  crmDialog.visible = true;
+};
+
+const handleCrmDialogClose = () => {
+  crmDialog.visible = false;
+  crmDialog.form = {
+    levelCode: "",
+    levelName: "",
+    crmLevel: "",
+  };
+};
+
+const handleCrmConfirm = async () => {
+  if (!crmDialog.form.crmLevel) {
+    proxy.$message.warning("请选择CRM身份级别");
+    return;
+  }
+
+  if (!crmDialog.form.levelCode) {
+    proxy.$message.warning("身份等级编号不能为空");
+    return;
+  }
+
+  try {
+    const payload = {
+      IdLevelCode: crmDialog.form.levelCode.trim(),
+      CrmName: crmDialog.form.crmLevel.trim(),
+    };
+
+    const { res } = await proxy.$interfaceEntry.vip.UpdateByCrmCode(payload);
+    const data = res?.data || {};
+
+    if (data.RespCode == 10000) {
+      // 后端会根据是否存在记录返回"更新成功"或"插入成功"
+      const successMsg = data.message || data.RespMessage || "操作成功";
+      proxy.$message.success(successMsg);
+      
+      handleCrmDialogClose();
+      // 重新查询,确保与后端数据一致
+      await getTableData();
+    } else {
+      proxy.$message.error(data.errorMsg || data.msg || data.RespMessage || "操作失败");
+    }
+  } catch (err) {
+    console.error("绑定CRM失败:", err);
+    proxy.$message.error("操作失败,请稍后重试");
+  }
+};
+
+// 分页切换
+const handlePageChange = (pageIndex: number) => {
+  page.PIndex = pageIndex;
+  getTableData();
+};
+
+onMounted(() => {
+  queryCrmLevel();
+  getTableData();
+});
+</script>
+
+<style scoped>
+.vip-page {
+  min-height: 100%;
+  background: #f5f7fb;
+  padding: 24px 0 32px;
+  box-sizing: border-box;
+}
+
+.page-inner {
+  width: 1560px;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.page-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.card {
+  background: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 6px 24px rgba(44, 69, 105, 0.08);
+  padding: 18px 24px;
+  box-sizing: border-box;
+}
+
+.table-card {
+  padding: 0;
+  overflow: hidden;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.table-card :deep(.el-table) {
+  width: 100%;
+  border: none;
+  flex: 1;
+}
+
+.table-card :deep(.el-table__header th),
+.table-card :deep(.el-table__body td) {
+  text-align: center;
+  font-size: 14px;
+}
+
+.table-card :deep(.el-table__body-wrapper),
+.table-card :deep(.el-table__header-wrapper) {
+  padding: 0;
+}
+
+/* 按比例填满整行(4列:编号、名称、CRM级别、操作) */
+.table-card :deep(.el-table__header colgroup col:nth-child(1)),
+.table-card :deep(.el-table__body colgroup col:nth-child(1)) {
+  width: 25% !important;
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(2)),
+.table-card :deep(.el-table__body colgroup col:nth-child(2)) {
+  width: 25% !important;
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(3)),
+.table-card :deep(.el-table__body colgroup col:nth-child(3)) {
+  width: 25% !important;
+}
+.table-card :deep(.el-table__header colgroup col:nth-child(4)),
+.table-card :deep(.el-table__body colgroup col:nth-child(4)) {
+  width: 25% !important;
+}
+
+.link-btn {
+  color: #2e74c6;
+}
+
+.pagination-card {
+  display: flex;
+  justify-content: center;
+  padding: 16px;
+  margin-top: auto;
+  margin-bottom: 0;
+}
+
+.pagination-card :deep(.el-pagination.is-background .el-pager li) {
+  background-color: #fff;
+  border: 1px solid #dcdfe6;
+  color: var(--el-text-color-regular);
+  margin: 0 6px;
+  border-radius: 6px;
+  min-width: 42px;
+  height: 34px;
+  line-height: 34px;
+}
+
+.pagination-card :deep(.el-pagination.is-background .el-pager li.is-active) {
+  background-color: var(--el-color-primary);
+  border-color: var(--el-color-primary);
+  color: #fff;
+}
+
+/* 弹框样式 */
+.crm-dialog :deep(.el-dialog) {
+  border-radius: 8px;
+  box-shadow: 0 6px 24px rgba(44, 69, 105, 0.08);
+}
+
+.crm-dialog :deep(.el-dialog__header) {
+  padding: 20px 24px 16px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.crm-dialog :deep(.el-dialog__title) {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.crm-dialog :deep(.el-dialog__body) {
+  padding: 0;
+}
+
+.dialog-content {
+  padding: 24px;
+  background: #ffffff;
+}
+
+.dialog-form :deep(.el-form-item) {
+  margin-bottom: 20px;
+}
+
+.dialog-form :deep(.el-form-item__label) {
+  color: #606266;
+  font-weight: 500;
+  line-height: 32px;
+}
+
+.dialog-form :deep(.el-form-item__content) {
+  line-height: 32px;
+}
+
+.form-value {
+  color: var(--el-text-color-primary);
+  font-size: 14px;
+}
+
+.crm-dialog :deep(.el-dialog__footer) {
+  padding: 16px 24px 20px;
+  border-top: 1px solid #f0f0f0;
+  background: #fafafa;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+.dialog-footer .el-button {
+  padding: 10px 24px;
+  border-radius: 4px;
+}
+
+.dialog-footer .el-button--primary {
+  background: var(--el-color-primary);
+  border-color: var(--el-color-primary);
+}
+</style>
+