浏览代码

律师模块完善,对账总览菜单完善

sgc 3 周之前
父节点
当前提交
b9560fd4d4

+ 36 - 19
src/api/modules/lawyer.ts

@@ -1,4 +1,5 @@
 import http from "@/api";
+const url = "/alienLawyer";
 
 /**
  * @name 律师管理模块
@@ -6,73 +7,89 @@ import http from "@/api";
 
 // 律所列表
 export const getLawFirmPage = (params: any) => {
-  return http.get(`/lawyer/firm/getPage`, params);
+  return http.get(url + `/lawyer/firm/getPage`, params);
 };
 // 新增律所
 export const addLawFirm = (params: any) => {
-  return http.post(`/lawyer/firm/addLawFirm`, params);
+  return http.post(url + `/lawyer/firm/addLawFirm`, params);
 };
 // 删除律所
 export const deleteLawFirm = (params: any) => {
-  return http.delete(`/lawyer/firm/deleteLawFirm`, params);
+  return http.delete(url + `/lawyer/firm/deleteLawFirm`, params);
 };
 // 编辑律所
 export const editLawFirm = (params: any) => {
-  return http.post(`/lawyer/firm/editLawFirm`, params);
+  return http.post(url + `/lawyer/firm/editLawFirm`, params);
 };
 
 // 律师列表
 export const getLawyerPage = (params: any) => {
-  return http.get(`/lawyer/user/getLawyerList`, params);
+  return http.get(url + `/lawyer/user/getLawyerList`, params);
 };
 // 新增律师
 export const addLawyerUser = (params: any) => {
-  return http.post(`/lawyer/user/addLawyerUser`, params);
+  return http.post(url + `/lawyer/user/addLawyerUser`, params);
 };
 // 删除律师
 export const deleteLawyerUser = (params: any) => {
-  return http.delete(`/lawyer/user/deleteLawyerUser`, params);
+  return http.delete(url + `/lawyer/user/deleteLawyerUser`, params);
 };
 // 编辑律师
 export const editLawyerUser = (params: any) => {
-  return http.post(`/lawyer/user/editLawyerUser`, params);
+  return http.post(url + `/lawyer/user/editLawyerUser`, params);
 };
 
 // 法律问题场景 列表查询
 export const getScenePage = (params: any) => {
-  return http.get(`/lawyer/legalProblemScenar/getPage`, params);
+  return http.get(url + `/lawyer/legalProblemScenar/getPage`, params);
 };
 // 新增法律问题场景
 export const addScene = (params: any) => {
-  return http.post(`/lawyer/legalProblemScenar/addLawyerLegalProblemScenar`, params);
+  return http.post(url + `/lawyer/legalProblemScenar/addLawyerLegalProblemScenar`, params);
 };
 //  删除法律问题场景
 export const deleteScene = (params: any) => {
-  return http.delete(`/lawyer/legalProblemScenar/deleteLawyerLegalProblemScenar`, params);
+  return http.delete(url + `/lawyer/legalProblemScenar/deleteLawyerLegalProblemScenar`, params);
 };
 // 编辑法律问题场景
 export const editScene = (params: any) => {
-  return http.post(`/lawyer/legalProblemScenar/editLawyerLegalProblemScenar`, params);
+  return http.post(url + `/lawyer/legalProblemScenar/editLawyerLegalProblemScenar`, params);
 };
 
 // 擅长领域 列表查询
 export const getExpertiseAreaPage = (params: any) => {
-  return http.get(`/lawyer/expertiseArea/getPage`, params);
+  return http.get(url + `/lawyer/expertiseArea/getPage`, params);
 };
 // 新增擅长领域
 export const addExpertiseArea = (params: any) => {
-  return http.post(`/lawyer/expertiseArea/addExpertiseArea`, params);
+  return http.post(url + `/lawyer/expertiseArea/addExpertiseArea`, params);
 };
 //  删除擅长领域
 export const deleteExpertiseArea = (params: any) => {
-  return http.delete(`/lawyer/expertiseArea/deleteExpertiseArea`, params);
+  return http.delete(url + `/lawyer/expertiseArea/deleteExpertiseArea`, params);
 };
 //  编辑擅长领域
 export const editExpertiseArea = (params: any) => {
-  return http.post(`/lawyer/expertiseArea/editExpertiseArea`, params);
+  return http.post(url + `/lawyer/expertiseArea/editExpertiseArea`, params);
 };
 
-//  律师账单/申请列表
-export const getApplicationExpertList = (params: any) => {
-  return http.get(`/lifeUserExpert/getApplicationExpertList`, params);
+//  律师账单列表
+export const getOrderList = (params: any) => {
+  return http.get(url + `/lawyer/firm/reconciliation/getOrderList`, params);
+};
+//  数据总览
+export const getOverview = (params: any) => {
+  return http.get(url + `/lawyer/firm/reconciliation/getOverview`, params);
+};
+//  导入律所数据
+export const importData = (params: any) => {
+  return http.post(url + `/lawyer/firm/import`, params);
+};
+// 导入模板
+export const downloadLawFirmTemplate = (params?: any) => {
+  return http.get(url + `/lawyer/firm/downloadTemplate`, params, { responseType: "blob" });
+};
+// 导出律所数据
+export const exportLawFirm = (params?: any) => {
+  return http.get(url + `/lawyer/firm/export`, params, { responseType: "blob" });
 };

+ 11 - 2
src/api/modules/login.ts

@@ -4,6 +4,7 @@ import authMenuList from "@/assets/json/authMenuList.json";
 import authButtonList from "@/assets/json/authButtonList.json";
 import http from "@/api";
 import httpLogin from "@/api/indexLogin";
+import { useUserStore } from "@/stores/modules/user";
 /**
  * @name 登录模块
  */
@@ -17,8 +18,16 @@ export const loginApi = (params: Login.ReqLoginForm): Promise<{ data: Login.ResL
 };
 
 // 获取菜单列表
-export const getAuthMenuListApi = () => {
-  // return http.get<Menu.MenuOptions[]>(PORT1 + `/menu/list`, {}, { loading: false });
+export const getAuthMenuListApi = (params: any = {}) => {
+  // const userStore = useUserStore();
+  // const requestParams = { ...params };
+  // const loginAccount = userStore.userInfo?.name?.trim();
+  // if (loginAccount && loginAccount.toLowerCase() === "admin") {
+  //   requestParams.type = 1;
+  // } else {
+  //   requestParams.type = 0;
+  // }
+  // return http.get<Menu.MenuOptions[]>(PORT1+ `/routingInfo`, requestParams, { loading: false });
   // 如果想让菜单变为本地数据,注释上一行代码,并引入本地 authMenuList.json 数据
   return authMenuList;
 };

+ 8 - 4
src/api/modules/user.ts

@@ -5,13 +5,17 @@ import http from "@/api";
  */
 // 获取用户列表
 export const getUserList = (params: any) => {
-  return http.get(`/platformLifeUser/getUserList`, params);
+  return http.get(`/sys/list`, params);
 };
 // 新增用户
 export const addUser = (params: any) => {
-  return http.get(`/platformLifeUser/getUserList`, params);
+  return http.post(`/sys/register`, params);
 };
-// 编辑用户列表
+// 编辑用户
 export const editUser = (params: any) => {
-  return http.get(`/platformLifeUser/getUserList`, params);
+  return http.post(`/sys/updateAccounInfo`, params);
+};
+// 删除用户
+export const deleteUser = (params: any) => {
+  return http.delete(`/sys/delete`, params);
 };

+ 4 - 1
src/stores/interface/index.ts

@@ -29,7 +29,10 @@ export interface GlobalState {
 /* UserState */
 export interface UserState {
   token: string;
-  userInfo: { name: string };
+  userInfo: {
+    name: string;
+    firmId?: string | number;
+  };
 }
 
 /* tabsMenuProps */

+ 1 - 1
src/stores/modules/user.ts

@@ -6,7 +6,7 @@ export const useUserStore = defineStore({
   id: "geeker-user",
   state: (): UserState => ({
     token: "",
-    userInfo: { name: "Geeker" }
+    userInfo: { name: "Geeker", firmId: "" }
   }),
   getters: {},
   actions: {

+ 180 - 0
src/views/lawyerManagement/lawFirm/components/ImportDialog.vue

@@ -0,0 +1,180 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="520px" destroy-on-close @closed="handleClosed">
+    <div class="import-dialog">
+      <section class="import-step">
+        <div class="import-step__title">1. 下载模板</div>
+        <el-button type="primary" :loading="downloadLoading" :icon="Download" @click="handleDownload"> 下载模板 </el-button>
+      </section>
+      <section class="import-step import-step--upload">
+        <div class="import-step__title">2. 上传文件</div>
+        <div class="import-upload-wrapper">
+          <el-upload
+            ref="uploadRef"
+            drag
+            action="#"
+            :auto-upload="false"
+            :limit="1"
+            :show-file-list="true"
+            :before-upload="beforeUpload"
+            :on-exceed="handleFileExceed"
+            :on-remove="handleFileRemove"
+            accept=".xls,.xlsx"
+          >
+            <el-icon class="el-icon--upload">
+              <upload-filled />
+            </el-icon>
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip">仅支持 .xls / .xlsx 文件,大小不超过 {{ fileLimitMB }}M</div>
+            </template>
+          </el-upload>
+        </div>
+      </section>
+    </div>
+    <template #footer>
+      <el-button @click="visible = false"> 取 消 </el-button>
+      <el-button type="primary" :loading="submitLoading" @click="handleSubmit"> 保存 </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import { ElMessage, type UploadInstance, type UploadProps, type UploadRawFile } from "element-plus";
+import { Download, UploadFilled } from "@element-plus/icons-vue";
+
+const props = withDefaults(
+  defineProps<{
+    modelValue: boolean;
+    title?: string;
+    downloadHandler: () => Promise<any>;
+    importHandler: (formData: FormData) => Promise<any>;
+    fileLimitMB?: number;
+  }>(),
+  {
+    title: "导入数据",
+    fileLimitMB: 10
+  }
+);
+
+const emits = defineEmits<{
+  "update:modelValue": [boolean];
+  success: [];
+}>();
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: val => emits("update:modelValue", val)
+});
+
+const uploadRef = ref<UploadInstance>();
+const selectedFile = ref<UploadRawFile>();
+const submitLoading = ref(false);
+const downloadLoading = ref(false);
+const fileLimitMB = computed(() => props.fileLimitMB);
+
+const handleDownload = async () => {
+  if (!props.downloadHandler) return;
+  downloadLoading.value = true;
+  try {
+    await props.downloadHandler();
+  } finally {
+    downloadLoading.value = false;
+  }
+};
+
+const beforeUpload: UploadProps["beforeUpload"] = file => {
+  const isExcel =
+    file.type === "application/vnd.ms-excel" || file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+  const isLimit = file.size / 1024 / 1024 <= fileLimitMB.value;
+  if (!isExcel) {
+    ElMessage.warning("请上传 xls 或 xlsx 格式文件");
+    return false;
+  }
+  if (!isLimit) {
+    ElMessage.warning(`文件大小不能超过 ${fileLimitMB.value}M`);
+    return false;
+  }
+  selectedFile.value = file;
+  return false;
+};
+
+const handleFileExceed: UploadProps["onExceed"] = files => {
+  uploadRef.value?.clearFiles();
+  const file = files[0] as UploadRawFile;
+  selectedFile.value = file;
+  uploadRef.value?.handleStart(file);
+};
+
+const handleFileRemove: UploadProps["onRemove"] = () => {
+  selectedFile.value = undefined;
+};
+
+const handleSubmit = async () => {
+  if (!selectedFile.value) {
+    ElMessage.warning("请先上传文件");
+    return;
+  }
+  const formData = new FormData();
+  formData.append("file", selectedFile.value);
+  submitLoading.value = true;
+  try {
+    await props.importHandler(formData);
+    ElMessage.success("导入成功");
+    emits("success");
+    visible.value = false;
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+const handleClosed = () => {
+  uploadRef.value?.clearFiles();
+  selectedFile.value = undefined;
+};
+</script>
+
+<style scoped lang="scss">
+.import-dialog {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+.import-step {
+  display: flex;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color);
+  border-radius: 10px;
+  box-shadow: 0 4px 12px rgb(0 0 0 / 6%);
+  &__title {
+    font-size: 15px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    text-align: left;
+  }
+}
+.import-step--upload {
+  flex-direction: column;
+  gap: 16px;
+}
+.import-upload-wrapper {
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  :deep(.el-upload-dragger) {
+    padding: 24px 16px;
+    text-align: center;
+    background-color: #f8fafc;
+    border-style: dashed;
+    border-radius: 12px;
+  }
+  :deep(.el-upload__text em) {
+    color: var(--el-color-primary);
+  }
+  :deep(.el-upload__tip) {
+    text-align: center;
+  }
+}
+</style>

+ 45 - 2
src/views/lawyerManagement/lawFirm/index.vue

@@ -3,6 +3,8 @@
     <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :data-callback="dataCallback">
       <template #tableHeader>
         <el-button type="primary" :icon="CirclePlus" @click="handleCreate"> 新增律所 </el-button>
+        <el-button type="primary" :icon="Upload" @click="handleImport"> 导入 </el-button>
+        <el-button type="primary" :icon="Download" :loading="exportLoading" @click="handleExport"> 导出 </el-button>
       </template>
       <template #paymentAccount="scope">
         <div class="payment-tags">
@@ -17,6 +19,13 @@
       </template>
     </ProTable>
     <LawFirmDialog ref="lawFirmDialogRef" @success="refreshTable" />
+    <ImportDialog
+      v-model="importVisible"
+      title="导入律所"
+      :download-handler="handleDownloadTemplate"
+      :import-handler="handleImportSubmit"
+      @success="handleImportSuccess"
+    />
   </div>
 </template>
 
@@ -25,12 +34,24 @@ import { ref, reactive, onActivated } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import { getLawFirmPage, addLawFirm, deleteLawFirm, editLawFirm } from "@/api/modules/lawyer";
-import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
+import {
+  getLawFirmPage,
+  addLawFirm,
+  deleteLawFirm,
+  editLawFirm,
+  importData,
+  downloadLawFirmTemplate,
+  exportLawFirm
+} from "@/api/modules/lawyer";
+import { useDownload } from "@/hooks/useDownload";
+import { CirclePlus, Delete, EditPen, Upload, Download } from "@element-plus/icons-vue";
 import LawFirmDialog from "./components/LawFirmDialog.vue";
+import ImportDialog from "./components/ImportDialog.vue";
 
 const proTable = ref<ProTableInstance>();
 const lawFirmDialogRef = ref<InstanceType<typeof LawFirmDialog>>();
+const importVisible = ref(false);
+const exportLoading = ref(false);
 
 const columns = reactive<ColumnProps<any>[]>([
   { label: "序号", type: "index", width: 60, align: "center" },
@@ -87,6 +108,10 @@ const handleCreate = () => {
   });
 };
 
+const handleImport = () => {
+  importVisible.value = true;
+};
+
 const handleEdit = (row: any) => {
   lawFirmDialogRef.value?.open({
     title: "编辑律所",
@@ -110,6 +135,24 @@ const handleDelete = (row: any) => {
     .catch(() => {});
 };
 
+const handleDownloadTemplate = () => useDownload(downloadLawFirmTemplate, "律所导入模板");
+
+const handleImportSubmit = (formData: FormData) => importData(formData);
+
+const handleImportSuccess = () => {
+  refreshTable();
+};
+
+const handleExport = async () => {
+  exportLoading.value = true;
+  try {
+    const params = { ...(proTable.value?.searchParam || {}) };
+    await useDownload(exportLawFirm, "律所列表", params);
+  } finally {
+    exportLoading.value = false;
+  }
+};
+
 onActivated(() => {
   refreshTable();
 });

+ 15 - 15
src/views/lawyerManagement/lawyer/components/LawyerDialog.vue

@@ -16,14 +16,14 @@
           style="width: 100%"
         />
       </el-form-item>
-      <el-form-item label="专业领域" prop="specialtyFields">
-        <el-select v-model="form.specialtyFields" placeholder="请选择专业领域" filterable style="width: 100%">
+      <el-form-item label="专业领域" prop="expertiseAreaInfo">
+        <el-select v-model="form.expertiseAreaInfo" placeholder="请选择专业领域" filterable style="width: 100%">
           <el-option v-for="item in props.professionalOptions" :key="item.value" :label="item.label" :value="item.value" />
         </el-select>
       </el-form-item>
-      <el-form-item label="法律场景" prop="expertiseCases">
+      <el-form-item label="法律场景" prop="firstLevelScenario">
         <el-tree-select
-          v-model="form.expertiseCases"
+          v-model="form.firstLevelScenario"
           :data="props.sceneOptions"
           :props="sceneTreeProps"
           check-strictly
@@ -35,8 +35,8 @@
       <el-form-item label="接单状态" prop="status">
         <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
       </el-form-item>
-      <el-form-item label="收款账号" prop="paymentAccount">
-        <el-input v-model="form.paymentAccount" placeholder="请输入收款账号" clearable />
+      <el-form-item label="收款账号" prop="paymentNum">
+        <el-input v-model="form.paymentNum" placeholder="请输入收款账号" clearable />
       </el-form-item>
       <el-form-item label="所属律所" prop="lawFirmId">
         <el-select v-model="form.lawFirmId" placeholder="请选择所属律所" filterable>
@@ -81,10 +81,10 @@ const form = reactive({
   name: "",
   phone: "",
   practiceStartDate: "",
-  specialtyFields: "",
-  expertiseCases: "",
+  expertiseAreaInfo: "",
+  firstLevelScenario: "",
   status: 1,
-  paymentAccount: "",
+  paymentNum: "",
   lawFirmId: ""
 });
 
@@ -95,9 +95,9 @@ const rules = reactive({
     { pattern: /^1\d{10}$/, message: "请输入正确手机号", trigger: "blur" }
   ],
   practiceStartDate: [{ required: true, message: "请输入从业时间", trigger: "blur" }],
-  specialtyFields: [{ required: true, message: "请输入专业领域", trigger: "blur" }],
-  expertiseCases: [{ required: true, message: "请选择法律场景", trigger: "change" }],
-  paymentAccount: [{ required: true, message: "请输入收款账号", trigger: "blur" }],
+  expertiseAreaInfo: [{ required: true, message: "请输入专业领域", trigger: "blur" }],
+  firstLevelScenario: [{ required: true, message: "请选择法律场景", trigger: "change" }],
+  paymentNum: [{ required: true, message: "请输入收款账号", trigger: "blur" }],
   lawFirmId: [{ required: true, message: "请选择所属律所", trigger: "change" }]
 });
 
@@ -108,10 +108,10 @@ const resetForm = () => {
   form.name = "";
   form.phone = "";
   form.practiceStartDate = "";
-  form.specialtyFields = "";
-  form.expertiseCases = "";
+  form.expertiseAreaInfo = "";
+  form.firstLevelScenario = "";
   form.status = 1;
-  form.paymentAccount = "";
+  form.paymentNum = "";
   form.lawFirmId = "";
 };
 

+ 4 - 4
src/views/lawyerManagement/lawyer/index.vue

@@ -59,8 +59,8 @@ const columns = reactive<ColumnProps<any>[]>([
       }
     }
   },
-  { label: "专业领域", prop: "areaInfo", width: 140 },
-  { label: "法律场景", prop: "scenarioNames" },
+  { label: "专业领域", prop: "expertiseAreaInfo" },
+  { label: "法律场景", prop: "firstLevelScenario" },
   {
     label: "接单状态",
     prop: "status",
@@ -72,8 +72,8 @@ const columns = reactive<ColumnProps<any>[]>([
     ],
     fieldNames: { label: "label", value: "value" }
   },
-  { label: "收款账号", prop: "paymentAccount" },
-  { label: "所属律所", prop: "lawFirm" },
+  { label: "收款账号", prop: "paymentNum" },
+  { label: "所属律所", prop: "firmName" },
   { label: "操作", prop: "operation", width: 200, fixed: "right" }
 ]);
 

+ 249 - 114
src/views/lawyerManagement/reconciliation/index.vue

@@ -1,117 +1,147 @@
 <template>
   <div class="reconciliation-page">
-    <div class="summary-wrapper">
-      <div class="summary-card" v-for="item in summaryCards" :key="item.label">
-        <div class="summary-label">
-          {{ item.label }}
+    <div class="layout-wrapper">
+      <div v-if="isAdmin" class="tree-panel">
+        <div class="tree-panel__header">
+          <div class="tree-panel__title">律所列表</div>
+          <el-input v-model="listFilterText" placeholder="搜索律所" size="small" clearable :prefix-icon="Search" />
         </div>
-        <div class="summary-value">
-          {{ item.value }}
+        <div class="tree-panel__body">
+          <el-scrollbar>
+            <div
+              v-for="item in filteredLawFirmList"
+              :key="String(item.id)"
+              class="firm-item"
+              :class="{ active: selectedFirmId === item.id }"
+              @click="handleFirmSelect(item.id)"
+            >
+              {{ item.label }}
+            </div>
+          </el-scrollbar>
         </div>
       </div>
-    </div>
-    <div class="filter-bar">
-      <div class="filter-left">
-        <div class="filter-title">律师账单数据</div>
-        <div class="filter-desc">查看该律所下所有律师的订单明细数据</div>
-      </div>
-      <div class="filter-right">
-        <el-button type="primary" plain> 导出数据 </el-button>
-        <el-button> 刷新 </el-button>
-      </div>
-    </div>
-    <div class="search-panel">
-      <el-input v-model="filters.name" placeholder="输入律师姓名搜索" clearable />
-      <el-date-picker
-        v-model="filters.dateRange"
-        type="daterange"
-        range-separator="至"
-        start-placeholder="开始日期"
-        end-placeholder="结束日期"
-        value-format="YYYY-MM-DD"
-      />
-      <el-button type="primary" :icon="Search" @click="handleSearch"> 搜索 </el-button>
-      <el-button @click="handleReset"> 重置 </el-button>
-    </div>
-    <div class="custom-table-head">
-      <span> 律师信息 </span>
-      <span> 订单数量 </span>
-      <span> 订单金额 </span>
-      <span> 平台信息服务费 </span>
-      <span> 操作 </span>
-    </div>
-    <ProTable
-      ref="proTable"
-      :columns="columns"
-      :request-api="getTableList"
-      :data-callback="dataCallback"
-      :tool-button="false"
-      :show-table-setting="false"
-    >
-      <template #userName="scope">
-        <div class="lawyer-info">
-          <el-avatar :size="48" :src="scope.row.avatar || defaultAvatar" />
-          <div class="lawyer-meta">
-            <div class="lawyer-name">
-              {{ scope.row.userName || "--" }}
+
+      <div class="content-panel">
+        <div class="summary-wrapper">
+          <div class="summary-card" v-for="item in summaryCards" :key="item.label">
+            <div class="summary-label">
+              {{ item.label }}
+            </div>
+            <div class="summary-value">
+              {{ item.value }}
             </div>
-            <div class="lawyer-extra">执业证号:{{ scope.row.licenseNo || "——" }}</div>
-            <div class="lawyer-extra">手机号:{{ scope.row.userPhone || "——" }}</div>
           </div>
         </div>
-      </template>
-      <template #orderCount="scope">
-        {{ scope.row.orderCount ?? "--" }}
-      </template>
-      <template #orderAmount="scope">
-        {{ formatCurrency(scope.row.orderAmount) }}
-      </template>
-      <template #platformFee="scope">
-        {{ formatCurrency(scope.row.platformFee) }}
-      </template>
-      <template #operation="scope">
-        <el-button type="primary" link @click="handleDetail(scope.row)"> 查看详情 </el-button>
-      </template>
-    </ProTable>
+
+        <ProTable
+          ref="proTable"
+          :columns="columns"
+          :request-api="getTableList"
+          :data-callback="dataCallback"
+          :request-auto="false"
+        >
+          <template #lawyerName="scope">
+            <div class="lawyer-info">
+              <el-avatar :size="48" :src="scope.row.headImg" />
+              <div class="lawyer-meta">
+                <div class="lawyer-name">
+                  {{ scope.row.lawyerName }}
+                </div>
+                <div class="lawyer-extra">执业证号:{{ scope.row.lawyerCertificateNo }}</div>
+              </div>
+            </div>
+          </template>
+          <template #operation="scope">
+            <el-button type="primary" link :icon="EditPen" @click="handleDetail(scope.row)"> 查看详情 </el-button>
+          </template>
+        </ProTable>
+      </div>
+    </div>
     <DetailDialog ref="detailDialog" />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onActivated, computed } from "vue";
+import { ref, reactive, onActivated, onMounted, computed, nextTick } from "vue";
 import type { Course } from "@/api/interface";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import { Search } from "@element-plus/icons-vue";
 import DetailDialog from "./detailDialog.vue";
-import { getApplicationExpertList } from "@/api/modules/lawyer";
+import { getOrderList, getOverview, getLawFirmPage } from "@/api/modules/lawyer";
+import { useUserStore } from "@/stores/modules/user";
+import { EditPen, Search } from "@element-plus/icons-vue";
 
 const proTable = ref<ProTableInstance>();
 const detailDialog = ref<any>(null);
+const userStore = useUserStore();
+
+const summaryData = ref({
+  totalOrderCount: 0,
+  totalOrderAmountYuan: 0,
+  platformServiceFeeYuan: 0
+});
+const selectedFirmId = ref<string | number>("");
+const lawFirmList = ref<{ id: string | number; label: string }[]>([]);
+const listFilterText = ref("");
+const isAdmin = computed(() => (userStore.userInfo?.name || "").toLowerCase() === "admin");
+const currentFirmId = computed(() => {
+  return isAdmin.value ? selectedFirmId.value : userStore.userInfo?.firmId || "";
+});
 
 const columns = reactive<ColumnProps<Course.ReqCourseParams>[]>([
-  { label: "律师信息", prop: "userName", minWidth: 280 },
-  { label: "订单数量", prop: "orderCount", width: 160, align: "center" },
+  { label: "律师信息", prop: "lawyerName", minWidth: 280, search: { el: "input", props: { placeholder: "请输入律师姓名" } } },
+  { label: "订单数量", prop: "orderAmount", width: 160, align: "center" },
   { label: "订单金额", prop: "orderAmount", width: 200, align: "center" },
   { label: "平台信息服务费", prop: "platformFee", width: 220, align: "center" },
+  {
+    label: "订单日期",
+    prop: "orderTime",
+    width: 180,
+    search: { el: "date-picker", props: { type: "date", valueFormat: "YYYY-MM-DD", placeholder: "请选择从业时间" } }
+  },
   { label: "操作", prop: "operation", width: 160, align: "center", fixed: "right" }
 ]);
-const getTableList = async (params: any) => {
-  let tempParams = JSON.parse(JSON.stringify(params));
+const buildQueryParams = (params: any) => {
+  const tempParams = { ...params };
   delete tempParams.time;
-  // 深拷贝原始参数
-  let newParams = JSON.parse(JSON.stringify(tempParams));
-  newParams.page = newParams.pageNum;
-  newParams.size = newParams.pageSize;
-  delete newParams.pageNum;
-  delete newParams.pageSize;
+  const newParams = {
+    ...tempParams,
+    pageNum: tempParams.pageNum,
+    pageSize: tempParams.pageSize
+  };
   if (params.time) {
     newParams.createdTime = params.time[0];
     newParams.endTime = params.time[1];
   }
+  if (currentFirmId.value) {
+    newParams.firmId = currentFirmId.value;
+  }
+  return newParams;
+};
 
-  const res = await getApplicationExpertList(newParams);
-  return res;
+const fetchOverview = async (params: any) => {
+  try {
+    const res: any = await getOverview(params);
+    const overview = res?.data || res || {};
+    summaryData.value = {
+      totalOrderCount: Number(overview.totalOrderCount) || 0,
+      totalOrderAmountYuan: Number(overview.totalOrderAmountYuan) || 0,
+      platformServiceFeeYuan: Number(overview.platformServiceFeeYuan) || 0
+    };
+  } catch (error) {
+    summaryData.value = {
+      totalOrderCount: 0,
+      totalOrderAmountYuan: 0,
+      platformServiceFeeYuan: 0
+    };
+    console.error("获取对账概览失败", error);
+  }
+};
+
+const getTableList = async (params: any) => {
+  const queryParams = buildQueryParams(params);
+  const [listRes] = await Promise.all([getOrderList(queryParams), fetchOverview(queryParams)]);
+  return listRes;
 };
 const dataCallback = (data: any) => {
   return {
@@ -126,20 +156,56 @@ const filters = reactive({
   dateRange: [] as string[]
 });
 
-const defaultAvatar = "https://img.alicdn.com/tfs/TB1jFtVwYj1gK0jSZFuXXcrHpXa-200-200.png";
+const summaryCards = computed(() => [
+  { label: "总订单数量", value: formatNumber(summaryData.value.totalOrderCount) },
+  { label: "总订单金额", value: formatCurrency(summaryData.value.totalOrderAmountYuan) },
+  { label: "平台信息服务费", value: formatCurrency(summaryData.value.platformServiceFeeYuan) }
+]);
 
-const summaryCards = computed(() => {
-  const list = proTable.value?.tableData || [];
-  const totalOrders = list.reduce((sum: number, item: any) => sum + (Number(item.orderCount) || 0), 0);
-  const totalAmount = list.reduce((sum: number, item: any) => sum + (Number(item.orderAmount) || 0), 0);
-  const totalFee = list.reduce((sum: number, item: any) => sum + (Number(item.platformFee) || 0), 0);
-  return [
-    { label: "总订单数量", value: formatNumber(totalOrders) },
-    { label: "总订单金额", value: formatCurrency(totalAmount) },
-    { label: "平台信息服务费", value: formatCurrency(totalFee) }
-  ];
+const filteredLawFirmList = computed(() => {
+  if (!listFilterText.value) return lawFirmList.value;
+  const keyword = listFilterText.value.toLowerCase();
+  return lawFirmList.value.filter(item => String(item.label).toLowerCase().includes(keyword));
 });
 
+const fetchLawFirmList = async () => {
+  try {
+    const res: any = await getLawFirmPage({ page: 1, size: 999 });
+    const list = res?.records || res?.data?.records || res?.data?.list || [];
+    const nodes = list.map((item: any) => ({
+      id: item.id,
+      label: item.firmName
+    }));
+    lawFirmList.value = nodes;
+    if (!selectedFirmId.value && nodes.length) {
+      selectedFirmId.value = nodes[0].id;
+    }
+  } catch (error) {
+    console.error("获取律所列表失败", error);
+    lawFirmList.value = [];
+  }
+};
+
+const handleFirmSelect = (id: string | number) => {
+  if (selectedFirmId.value === id) return;
+  selectedFirmId.value = id;
+  refreshTable();
+};
+
+const refreshTable = () => {
+  proTable.value?.getTableList();
+};
+
+const initializeContext = async () => {
+  if (isAdmin.value) {
+    await fetchLawFirmList();
+  } else {
+    selectedFirmId.value = userStore.userInfo?.firmId || "";
+  }
+  await nextTick();
+  refreshTable();
+};
+
 const handleSearch = () => {
   if (!proTable.value) return;
   if (filters.name) proTable.value.searchParam.userName = filters.name;
@@ -174,18 +240,106 @@ const handleDetail = (row: any) => {
   detailDialog.value?.open(row.userId);
 };
 
+onMounted(() => {
+  initializeContext();
+});
+
 onActivated(() => {
-  proTable.value?.getTableList();
+  refreshTable();
 });
 </script>
 
 <style scoped lang="scss">
 .reconciliation-page {
+  box-sizing: border-box;
   display: flex;
   flex-direction: column;
   gap: 16px;
+  min-height: calc(100vh - 150px);
   padding: 16px;
 }
+.layout-wrapper {
+  display: flex;
+  flex: 1;
+  gap: 16px;
+  align-items: stretch;
+  height: 100%;
+  min-height: 0;
+}
+.tree-panel {
+  display: flex;
+  flex-direction: column;
+  width: 280px;
+  min-width: 240px;
+  height: calc(100vh - 180px);
+  padding: 18px;
+  background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
+  border: 1px solid #e3e8ef;
+  border-radius: 14px;
+  box-shadow: 0 12px 24px rgb(31 37 50 / 6%);
+}
+.tree-panel__header {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.tree-panel__title {
+  font-size: 17px;
+  font-weight: 600;
+  color: #1f2532;
+  letter-spacing: 0.5px;
+}
+.tree-panel__body {
+  flex: 1;
+  min-height: 0;
+  margin-top: 14px;
+  overflow: hidden;
+  background: #ffffff;
+  border: 1px solid #edf0f5;
+  border-radius: 10px;
+}
+.tree-panel__body :deep(.el-scrollbar__wrap) {
+  padding: 8px 0;
+}
+.firm-item {
+  position: relative;
+  padding: 12px 16px 12px 20px;
+  font-size: 14px;
+  color: #565d6d;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+.firm-item::before {
+  position: absolute;
+  top: 50%;
+  left: 8px;
+  width: 4px;
+  height: 18px;
+  content: "";
+  background: transparent;
+  border-radius: 2px;
+  transition: background 0.2s;
+  transform: translateY(-50%);
+}
+.firm-item:hover {
+  color: #1f2532;
+  background: #f3f6fb;
+}
+.firm-item.active {
+  font-weight: 600;
+  color: var(--el-color-primary);
+  background: #e9f3ff;
+}
+.firm-item.active::before {
+  background: var(--el-color-primary);
+}
+.content-panel {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 0;
+}
 .summary-wrapper {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@@ -193,7 +347,7 @@ onActivated(() => {
 }
 .summary-card {
   padding: 16px;
-  background: #f8f9fb;
+  background: #ffffff;
   border: 1px solid #e2e6ef;
   border-radius: 12px;
 }
@@ -228,30 +382,11 @@ onActivated(() => {
   display: flex;
   gap: 12px;
 }
-.search-panel {
-  display: flex;
-  gap: 12px;
-  align-items: center;
-}
-.custom-table-head {
-  display: grid;
-  grid-template-columns: 3fr 1fr 1fr 1fr 1fr;
-  padding: 12px 16px;
-  font-weight: 600;
-  color: #7a7f87;
-  background: #ffffff;
-  border: 1px solid #e6e9ef;
-  border-bottom: none;
-  border-radius: 12px 12px 0 0;
-}
 :deep(.el-table) {
   border: 1px solid #e6e9ef;
   border-top: none;
   border-radius: 0 0 12px 12px;
 }
-:deep(.el-table__header) {
-  display: none;
-}
 .lawyer-info {
   display: flex;
   gap: 12px;

+ 5 - 2
src/views/login/components/LoginForm.vue

@@ -107,13 +107,16 @@ const login = (formEl: FormInstance | undefined) => {
       // 1.执行登录接口
       const loginPayload = {
         username: loginForm.username,
-        password: md5(loginForm.password)
-        // password: loginForm.password
+        // password: md5(loginForm.password)
+        password: loginForm.password
       };
       const { data } = (await loginApi(loginPayload)) as { data: Login.ResLogin };
       console.log(data);
       if (data.result) {
         userStore.setToken(data.token);
+        const extraInfo: any = data;
+        const firmId = extraInfo?.firmId ?? extraInfo?.lawFirmId ?? extraInfo?.userInfo?.firmId ?? "";
+        userStore.setUserInfo({ name: loginForm.username, firmId });
 
         // 2.添加动态路由
         await initDynamicRouter();

+ 14 - 15
src/views/userManagement/components/UserDialog.vue

@@ -1,20 +1,20 @@
 <template>
   <el-dialog v-model="visible" :title="dialogTitle" width="520px" destroy-on-close>
     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-suffix=":">
-      <el-form-item label="登录账号" prop="loginAccount">
-        <el-input v-model="form.loginAccount" placeholder="请输入登录账号" clearable />
+      <el-form-item label="登录账号" prop="userName">
+        <el-input v-model="form.userName" placeholder="请输入登录账号" clearable />
       </el-form-item>
       <el-form-item label="关联律所" prop="roleId">
         <el-select v-model="form.roleId" placeholder="请选择关联律所" filterable>
           <el-option v-for="item in lawFirmOptions" :key="item.value" :label="item.label" :value="item.value" />
         </el-select>
       </el-form-item>
-      <el-form-item label="登录密码" prop="loginPassword">
-        <el-input v-model="form.loginPassword" placeholder="请输入登录密码" type="password" show-password />
+      <el-form-item label="登录密码" prop="userPassword">
+        <el-input v-model="form.userPassword" placeholder="请输入登录密码" type="userPassword" show-user-password />
         <span v-if="isEdit" class="form-tip">若无需修改密码,可留空</span>
       </el-form-item>
       <el-form-item label="账号状态" prop="status">
-        <el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
+        <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
       </el-form-item>
       <el-form-item label="备注" prop="remark">
         <el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入备注信息" />
@@ -30,7 +30,6 @@
 <script setup lang="ts">
 import { reactive, ref, computed, nextTick, watch } from "vue";
 import type { FormInstance } from "element-plus";
-import md5 from "md5";
 import type { User } from "@/api/interface";
 
 interface DialogOptions {
@@ -55,9 +54,9 @@ const options = ref<DialogOptions>({
 
 const form = reactive({
   id: "",
-  loginAccount: "",
+  userName: "",
   roleId: "",
-  loginPassword: "",
+  userPassword: "",
   status: 1,
   remark: "",
   roleName: ""
@@ -67,9 +66,9 @@ const dialogTitle = computed(() => options.value.title);
 const isEdit = computed(() => options.value.mode === "edit");
 
 const rules = reactive({
-  loginAccount: [{ required: true, message: "请输入登录账号", trigger: "blur" }],
+  userName: [{ required: true, message: "请输入登录账号", trigger: "blur" }],
   roleId: [{ required: true, message: "请选择关联律所", trigger: "change" }],
-  loginPassword: [
+  userPassword: [
     {
       validator: (_: any, value: string, callback: (error?: Error) => void) => {
         if (options.value.mode === "add" && !value) return callback(new Error("请输入登录密码"));
@@ -82,9 +81,9 @@ const rules = reactive({
 
 const resetForm = () => {
   form.id = "";
-  form.loginAccount = "";
+  form.userName = "";
   form.roleId = "";
-  form.loginPassword = "";
+  form.userPassword = "";
   form.status = 1;
   form.remark = "";
   form.roleName = "";
@@ -118,14 +117,14 @@ const emits = defineEmits<{
 const buildPayload = () => {
   const payload: Record<string, any> = {
     id: form.id,
-    loginAccount: form.loginAccount,
+    userName: form.userName,
     roleId: form.roleId,
     status: form.status,
     remark: form.remark
   };
   if (!payload.id) delete payload.id;
-  if (form.loginPassword) {
-    payload.loginPassword = md5(form.loginPassword);
+  if (form.userPassword) {
+    payload.userPassword = form.userPassword;
   }
   return payload;
 };

+ 62 - 19
src/views/userManagement/index.vue

@@ -1,11 +1,12 @@
 <template>
   <div class="table-box">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :data-callback="dataCallback" row-key="id">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :data-callback="dataCallback">
       <template #tableHeader>
         <el-button type="primary" :icon="CirclePlus" @click="handleCreate"> 新增账号 </el-button>
       </template>
       <template #operation="scope">
         <el-button type="primary" link :icon="EditPen" @click="handleEdit(scope.row)"> 编辑 </el-button>
+        <el-button type="danger" link :icon="Delete" @click="handleDelete(scope.row)"> 删除 </el-button>
       </template>
     </ProTable>
 
@@ -14,29 +15,37 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from "vue";
-import { ElMessage } from "element-plus";
+import { ref, reactive, onMounted, computed } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
 import type { User } from "@/api/interface";
-import { getUserList } from "@/api/modules/user";
+import { getUserList, addUser, editUser, deleteUser } from "@/api/modules/user";
 import { getLawFirmPage } from "@/api/modules/lawyer";
-import { CirclePlus, EditPen } from "@element-plus/icons-vue";
+import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
 import UserDialog from "./components/UserDialog.vue";
 
 const proTable = ref<ProTableInstance>();
 const userDialogRef = ref<InstanceType<typeof UserDialog>>();
 const lawFirmOptions = ref<{ label: string; value: string | number }[]>([]);
+const lawFirmMap = computed<Record<string, string>>(() => {
+  const map: Record<string, string> = {};
+  lawFirmOptions.value.forEach(item => {
+    map[String(item.value)] = item.label;
+  });
+  return map;
+});
+type UserRow = Partial<User.ResUserList> & { userName?: string };
 
 const statusOptions = [
   { label: "启用", value: 1 },
   { label: "禁用", value: 0 }
 ];
 
-const columns = reactive<ColumnProps<User.ResUserList>[]>([
+const columns = reactive<ColumnProps<User.ResUserList & { roleName?: string }>[]>([
   { type: "index", label: "序号", width: 60 },
-  { prop: "loginAccount", label: "登录账号", search: { el: "input", props: { placeholder: "请输入登录账号" } } },
-  { prop: "loginPassword", label: "登录密码" },
+  { prop: "userName", label: "登录账号", search: { el: "input", props: { placeholder: "请输入登录账号" } } },
+  { prop: "userPassword", label: "登录密码" },
   { prop: "roleName", label: "关联律所" },
   {
     prop: "status",
@@ -48,7 +57,7 @@ const columns = reactive<ColumnProps<User.ResUserList>[]>([
     tag: true
   },
   { prop: "remark", label: "备注", width: 220 },
-  { prop: "createTime", label: "创建时间", width: 200 },
+  { prop: "createdTime", label: "创建时间", width: 200 },
   { prop: "operation", label: "操作", width: 150, fixed: "right" }
 ]);
 
@@ -61,10 +70,18 @@ const getTableList = (params: any) => {
   return getUserList(tempParams);
 };
 
-const dataCallback = (data: any) => ({
-  list: data.records,
-  total: data.total
-});
+const dataCallback = (data: any) => {
+  const list = data;
+  const total = data?.total ?? data?.records?.length ?? 0;
+  const mappedList = list.map((item: any) => ({
+    ...item,
+    roleName: item.roleName || lawFirmMap.value[String(item.roleId)] || "-"
+  }));
+  return {
+    list: mappedList,
+    total
+  };
+};
 
 const refreshTable = () => {
   proTable.value?.getTableList();
@@ -75,24 +92,50 @@ const handleCreate = () => {
     title: "新增账号",
     mode: "add",
     onSubmit: async payload => {
-      // await addUser(payload);
+      await addUser(payload);
       ElMessage.success("新增账号成功");
     }
   });
 };
 
-const handleEdit = (row: Partial<User.ResUserList>) => {
+const handleEdit = (row: UserRow) => {
+  const editRow = { ...row };
+  if (!editRow.roleId && editRow.roleName) {
+    const target = lawFirmOptions.value.find(item => item.label === editRow.roleName);
+    if (target) editRow.roleId = String(target.value);
+  } else if (editRow.roleId && !editRow.roleName) {
+    editRow.roleName = lawFirmMap.value[String(editRow.roleId)];
+  }
   userDialogRef.value?.open({
     title: "编辑账号",
     mode: "edit",
-    row,
+    row: editRow,
     onSubmit: async payload => {
-      // await editUser(payload);
+      await editUser(payload);
       ElMessage.success("编辑账号成功");
     }
   });
 };
 
+const handleDelete = (row: UserRow) => {
+  if (!row?.id) {
+    ElMessage.warning("未找到该账号的唯一标识,无法删除");
+    return;
+  }
+  const displayName = row.userName ?? row.username ?? "";
+  ElMessageBox.confirm(`确定删除账号「${displayName}」吗?`, "提示", {
+    type: "warning",
+    confirmButtonText: "确定",
+    cancelButtonText: "取消"
+  })
+    .then(async () => {
+      await deleteUser({ id: row.id });
+      ElMessage.success("删除账号成功");
+      refreshTable();
+    })
+    .catch(() => {});
+};
+
 const fetchLawFirmOptions = async () => {
   try {
     const res: any = await getLawFirmPage({ page: 1, size: 999 });
@@ -107,8 +150,8 @@ const fetchLawFirmOptions = async () => {
   }
 };
 
-onMounted(() => {
-  fetchLawFirmOptions();
+onMounted(async () => {
+  await fetchLawFirmOptions();
   refreshTable();
 });
 </script>