Sfoglia il codice sorgente

律所信息变更,对账总览重构完善

sgc 3 settimane fa
parent
commit
f78aeb4076

+ 26 - 12
src/api/modules/lawyer.ts

@@ -4,10 +4,10 @@ const url = "/alienLawyer";
 /**
  * @name 律师管理模块
  */
-
+//////////////////////////// 律所信息管理 ////////////////////////////
 // 律所列表
 export const getLawFirmPage = (params: any) => {
-  return http.get(url + `/lawyer/firm/getPage`, params);
+  return http.get(url + `/lawyer/firm/getPaymentPageWithFirm`, params);
 };
 // 新增律所
 export const addLawFirm = (params: any) => {
@@ -21,7 +21,7 @@ export const deleteLawFirm = (params: any) => {
 export const editLawFirm = (params: any) => {
   return http.post(url + `/lawyer/firm/editLawFirm`, params);
 };
-
+//////////////////////////// 律师信息管理 ////////////////////////////
 // 律师列表
 export const getLawyerPage = (params: any) => {
   return http.get(url + `/lawyer/user/getLawyerList`, params);
@@ -38,7 +38,7 @@ export const deleteLawyerUser = (params: any) => {
 export const editLawyerUser = (params: any) => {
   return http.post(url + `/lawyer/user/editLawyerUser`, params);
 };
-
+//////////////////////////// 法律场景管理 ////////////////////////////
 // 法律问题场景 列表查询
 export const getScenePage = (params: any) => {
   return http.get(url + `/lawyer/legalProblemScenar/getPage`, params);
@@ -55,32 +55,42 @@ export const deleteScene = (params: any) => {
 export const editScene = (params: any) => {
   return http.post(url + `/lawyer/legalProblemScenar/editLawyerLegalProblemScenar`, params);
 };
-
-// 擅长领域 列表查询
+//////////////////////////// 专业领域管理 ////////////////////////////
+// 专业领域 列表查询
 export const getExpertiseAreaPage = (params: any) => {
   return http.get(url + `/lawyer/expertiseArea/getPage`, params);
 };
-// 新增擅长领域
+// 新增专业领域
 export const addExpertiseArea = (params: any) => {
   return http.post(url + `/lawyer/expertiseArea/addExpertiseArea`, params);
 };
-//  删除擅长领域
+// 删除专业领域
 export const deleteExpertiseArea = (params: any) => {
   return http.delete(url + `/lawyer/expertiseArea/deleteExpertiseArea`, params);
 };
-//  编辑擅长领域
+// 编辑专业领域
 export const editExpertiseArea = (params: any) => {
   return http.post(url + `/lawyer/expertiseArea/editExpertiseArea`, params);
 };
+//////////////////////////// 对账总览 ////////////////////////////
 
-//  律师账单列表
-export const getOrderList = (params: any) => {
-  return http.get(url + `/lawyer/firm/reconciliation/getOrderList`, params);
+//  全部律所账单列表
+export const getAllLawFirmList = (params: any) => {
+  return http.get(url + `/lawyer/firm/reconciliation/getAllLawFirmList`, params);
+};
+//  单个律师账单列表
+export const getLawyerList = (params: any) => {
+  return http.get(url + `/lawyer/firm/reconciliation/getLawyerListWithName`, params);
+};
+//  账单详情
+export const getLawyerDetails = (params: any) => {
+  return http.get(url + `/lawyer/firm/reconciliation/getLawyerOrderList`, 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);
@@ -93,3 +103,7 @@ export const downloadLawFirmTemplate = (params?: any) => {
 export const exportLawFirm = (params?: any) => {
   return http.get(url + `/lawyer/firm/export`, params, { responseType: "blob" });
 };
+// 导出律所对账数据
+export const exportLawFirmReconciliation = (params?: any) => {
+  return http.get(url + `/lawyer/firm/reconciliation/export`, params, { responseType: "blob" });
+};

+ 10 - 10
src/api/modules/login.ts

@@ -19,17 +19,17 @@ export const loginApi = (params: Login.ReqLoginForm): Promise<{ data: Login.ResL
 
 // 获取菜单列表
 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 });
+  // 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;
+  return authMenuList;
 };
 
 // 获取按钮权限

+ 28 - 0
src/assets/json/authMenuList.json

@@ -98,6 +98,20 @@
             "isAffix": false,
             "isKeepAlive": false
           }
+        },
+        {
+          "path": "/lawyerManagement/reconciliation/lawyerDetail",
+          "name": "lawyerDetail",
+          "component": "/lawyerManagement/reconciliation/lawyerDetail",
+          "meta": {
+            "icon": "Briefcase",
+            "title": "对账总览",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
         }
       ]
     },
@@ -114,6 +128,20 @@
         "isAffix": false,
         "isKeepAlive": false
       }
+    },
+    {
+      "path": "/refund",
+      "name": "refund",
+      "component": "/refund/index",
+      "meta": {
+        "icon": "UserFilled",
+        "title": "退款审核",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      }
     }
   ],
   "msg": "成功"

+ 3 - 1
src/views/lawyerManagement/lawFirm/components/ImportDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-dialog v-model="visible" :title="title" width="520px" destroy-on-close @closed="handleClosed">
+  <el-dialog v-model="visible" :title="title" width="640px" destroy-on-close @closed="handleClosed">
     <div class="import-dialog">
       <section class="import-step">
         <div class="import-step__title">1. 下载模板</div>
@@ -139,6 +139,8 @@ const handleClosed = () => {
   display: flex;
   flex-direction: column;
   gap: 20px;
+  max-width: 560px;
+  margin: 0 auto;
 }
 .import-step {
   display: flex;

+ 76 - 25
src/views/lawyerManagement/lawFirm/components/LawFirmDialog.vue

@@ -1,28 +1,38 @@
 <template>
-  <el-dialog v-model="visible" :title="dialogTitle" width="520px" destroy-on-close>
-    <el-form ref="formRef" :model="form" :rules="rules" label-width="140px" label-suffix=":">
-      <el-form-item label="律所名称" prop="firmName">
-        <el-input v-model="form.firmName" placeholder="请输入律所名称" clearable />
-      </el-form-item>
-      <el-form-item label="负责人姓名" prop="directorName">
-        <el-input v-model="form.directorName" placeholder="请输入负责人姓名" clearable />
-      </el-form-item>
-      <el-form-item label="律所位置" prop="address">
-        <el-input v-model="form.address" placeholder="请输入律所位置" clearable />
-      </el-form-item>
-      <el-form-item label="收款账号" prop="paymentAccount">
-        <div class="bank-card-input">
-          <el-input v-model="bankCardDisplay" placeholder="请输入银行卡号" maxlength="23" clearable />
-          <el-button type="primary" link @click="handleAddCard"> 添加 </el-button>
-        </div>
-        <div class="bank-card-tags">
-          <el-tag v-for="(card, index) in bankCardList" :key="card" closable @close="removeCard(index)">
-            {{ formatCard(card) }}
-          </el-tag>
-        </div>
-        <div class="bank-card-tip">最多可添加 3 张银行卡</div>
-      </el-form-item>
-    </el-form>
+  <el-dialog v-model="visible" :title="dialogTitle" width="640px" destroy-on-close>
+    <div class="dialog-form">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="150px" label-suffix=":">
+        <el-form-item label="律所名称" prop="firmName">
+          <el-input v-model="form.firmName" placeholder="请输入律所名称" clearable />
+        </el-form-item>
+        <el-form-item label="负责人姓名" prop="directorName">
+          <el-input v-model="form.directorName" placeholder="请输入负责人姓名" clearable />
+        </el-form-item>
+        <el-form-item label="律所位置" prop="address">
+          <el-input v-model="form.address" placeholder="请输入律所位置" clearable />
+        </el-form-item>
+        <el-form-item label="社会统一信用代码" prop="creditCode">
+          <el-input v-model="form.creditCode" placeholder="请输入社会统一信用代码" clearable />
+        </el-form-item>
+        <el-form-item label="信息服务费" prop="platformCommissionRatio">
+          <el-input-number v-model="form.platformCommissionRatio" :min="1" :max="99" :step="1" controls-position="right" />
+          <span class="form-tip">仅支持 1 - 99 的整数</span>
+        </el-form-item>
+        <el-form-item label="收款账号" prop="paymentAccount">
+          <div class="bank-card-input">
+            <el-input v-model="bankCardDisplay" placeholder="请输入银行卡号" maxlength="23" clearable />
+            <el-button type="primary" link @click="handleAddCard"> 添加 </el-button>
+          </div>
+        </el-form-item>
+        <el-form-item>
+          <div class="bank-card-tags">
+            <el-tag v-for="(card, index) in bankCardList" :key="card" closable @close="removeCard(index)">
+              {{ formatCard(card) }}
+            </el-tag>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
     <template #footer>
       <el-button @click="visible = false"> 取消 </el-button>
       <el-button type="primary" :loading="loading" @click="handleSubmit"> 确定 </el-button>
@@ -53,17 +63,38 @@ const form = reactive({
   firmName: "",
   directorName: "",
   address: "",
-  paymentAccount: ""
+  creditCode: "",
+  paymentAccount: "",
+  platformCommissionRatio: 3
 });
 const bankCardInput = ref("");
 const bankCardList = ref<string[]>([]);
 const bankCardReg = /^([1-9]\d{15}|\d{17}|\d{18}|\d{19})$/;
 
+const validateServiceFee = (_rule: any, value: number | string, callback: (err?: Error) => void) => {
+  if (value === undefined || value === null || value === "") {
+    callback(new Error("请输入信息服务费"));
+    return;
+  }
+  const numericValue = Number(value);
+  if (!Number.isFinite(numericValue) || !Number.isInteger(numericValue)) {
+    callback(new Error("信息服务费需为整数"));
+    return;
+  }
+  if (numericValue < 1 || numericValue > 99) {
+    callback(new Error("信息服务费范围为 1-99"));
+    return;
+  }
+  callback();
+};
+
 const rules = reactive({
   firmName: [{ required: true, message: "请输入律所名称", trigger: "blur" }],
   directorName: [{ required: true, message: "请输入负责人姓名", trigger: "blur" }],
   address: [{ required: true, message: "请输入律所位置", trigger: "blur" }],
+  creditCode: [{ required: true, message: "请输入社会统一信用代码", trigger: "blur" }],
   paymentAccount: [
+    { required: true, message: "请至少添加一张银行卡", trigger: "change" },
     {
       validator: (_rule, _value, callback) => {
         if (!bankCardList.value.length) {
@@ -74,6 +105,10 @@ const rules = reactive({
       },
       trigger: "change"
     }
+  ],
+  platformCommissionRatio: [
+    { required: true, message: "请输入信息服务费", trigger: "change" },
+    { validator: validateServiceFee, trigger: "change" }
   ]
 });
 
@@ -84,7 +119,9 @@ const resetForm = () => {
   form.firmName = "";
   form.directorName = "";
   form.address = "";
+  form.creditCode = "";
   form.paymentAccount = "";
+  form.platformCommissionRatio = 3;
   bankCardInput.value = "";
   bankCardList.value = [];
 };
@@ -100,6 +137,11 @@ const open = (payload: DialogOptions) => {
       .filter(card => !!card);
     bankCardList.value = cards.slice(0, 3);
     form.paymentAccount = bankCardList.value.join(",");
+    form.platformCommissionRatio = Number(payload.row.platformCommissionRatio ?? 3);
+    form.creditCode = payload.row.creditCode || "";
+  } else {
+    form.platformCommissionRatio = 3;
+    form.creditCode = "";
   }
   visible.value = true;
   nextTick(() => formRef.value?.clearValidate());
@@ -165,6 +207,10 @@ defineExpose({
 </script>
 
 <style scoped lang="scss">
+.dialog-form {
+  max-width: 560px;
+  margin: 0 auto;
+}
 .bank-card-input {
   display: flex;
   gap: 12px;
@@ -185,4 +231,9 @@ defineExpose({
   font-size: 12px;
   color: var(--el-text-color-secondary);
 }
+.form-tip {
+  margin-left: 12px;
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
 </style>

+ 83 - 12
src/views/lawyerManagement/lawFirm/index.vue

@@ -2,9 +2,17 @@
   <div class="table-box">
     <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>
+        <el-button type="primary" :icon="CirclePlus" :disabled="!isAdmin" @click="handleCreate"> 新增律所 </el-button>
+        <el-button type="primary" :icon="Upload" :disabled="!isAdmin" @click="handleImport"> 导入 </el-button>
+        <el-button
+          type="primary"
+          :icon="Download"
+          :loading="exportLoading"
+          :disabled="!hasExportableData || !isAdmin"
+          @click="handleExport"
+        >
+          导出
+        </el-button>
       </template>
       <template #paymentAccount="scope">
         <div class="payment-tags">
@@ -14,8 +22,8 @@
         </div>
       </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>
+        <el-button type="primary" link :icon="EditPen" :disabled="!isAdmin" @click="handleEdit(scope.row)"> 编辑 </el-button>
+        <el-button type="danger" link :icon="Delete" :disabled="!isAdmin" @click="handleDelete(scope.row)"> 删除 </el-button>
       </template>
     </ProTable>
     <LawFirmDialog ref="lawFirmDialogRef" @success="refreshTable" />
@@ -30,7 +38,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onActivated } from "vue";
+import { ref, reactive, onActivated, computed } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
@@ -47,11 +55,22 @@ 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";
+import { useUserStore } from "@/stores/modules/user";
+
+const userStore = useUserStore();
+const isAdmin = computed(() => (userStore.userInfo?.name || "").toLowerCase() === "admin");
+const ensureAdmin = () => {
+  if (isAdmin.value) return true;
+  ElMessage.warning("仅管理员可使用该功能");
+  return false;
+};
 
 const proTable = ref<ProTableInstance>();
 const lawFirmDialogRef = ref<InstanceType<typeof LawFirmDialog>>();
 const importVisible = ref(false);
 const exportLoading = ref(false);
+const tableTotal = ref(0);
+const hasExportableData = computed(() => tableTotal.value > 0);
 
 const columns = reactive<ColumnProps<any>[]>([
   { label: "序号", type: "index", width: 60, align: "center" },
@@ -63,6 +82,16 @@ const columns = reactive<ColumnProps<any>[]>([
     search: { el: "input", props: { placeholder: "请输入负责人姓名" } }
   },
   { label: "位置", prop: "address" },
+  { label: "统一社会信用代码", prop: "creditCode", width: 220 },
+  {
+    label: "信息服务费",
+    prop: "platformCommissionRatio",
+    width: 160,
+    align: "center",
+    render: scope => {
+      return `${scope.row.platformCommissionRatio ?? "0"}%`;
+    }
+  },
   { label: "收款账号", prop: "paymentAccount", align: "center" },
   { label: "操作", prop: "operation", width: 200, fixed: "right" }
 ]);
@@ -74,10 +103,14 @@ const getTableList = async (params: any) => {
   return getLawFirmPage(newParams);
 };
 
-const dataCallback = (data: any) => ({
-  list: data.records,
-  total: data.total
-});
+const dataCallback = (data: any) => {
+  const total = Number(data.total) || 0;
+  tableTotal.value = total;
+  return {
+    list: data.records,
+    total
+  };
+};
 
 const refreshTable = () => {
   proTable.value?.getTableList();
@@ -99,31 +132,62 @@ const formatCard = (card: string) => {
 };
 
 const handleCreate = () => {
+  if (!ensureAdmin()) return;
   lawFirmDialogRef.value?.open({
     title: "新增律所",
     onSubmit: async payload => {
-      await addLawFirm(payload);
+      // 防止的修改的参数混入
+      let params = {
+        creditCode: payload.creditCode,
+        directorName: payload.directorName,
+        firmName: payload.firmName,
+        platformCommissionRatio: payload.platformCommissionRatio,
+        paymentList: [
+          {
+            address: payload.address,
+            paymentAccount: payload.paymentAccount
+          }
+        ]
+      };
+      await addLawFirm(params);
       ElMessage.success("新增成功");
     }
   });
 };
 
 const handleImport = () => {
+  if (!ensureAdmin()) return;
   importVisible.value = true;
 };
 
 const handleEdit = (row: any) => {
+  if (!ensureAdmin()) return;
   lawFirmDialogRef.value?.open({
     title: "编辑律所",
     row,
     onSubmit: async payload => {
-      await editLawFirm(payload);
+      let params = {
+        id: row.firmId,
+        creditCode: payload.creditCode,
+        directorName: payload.directorName,
+        firmName: payload.firmName,
+        platformCommissionRatio: payload.platformCommissionRatio,
+        paymentList: [
+          {
+            id: row.id,
+            address: payload.address,
+            paymentAccount: payload.paymentAccount
+          }
+        ]
+      };
+      await editLawFirm(params);
       ElMessage.success("编辑成功");
     }
   });
 };
 
 const handleDelete = (row: any) => {
+  if (!ensureAdmin()) return;
   ElMessageBox.confirm(`确定删除律所【${row.firmName}】吗?`, "提示", {
     type: "warning"
   })
@@ -144,6 +208,13 @@ const handleImportSuccess = () => {
 };
 
 const handleExport = async () => {
+  if (!ensureAdmin()) {
+    return;
+  }
+  if (!hasExportableData.value) {
+    ElMessage.warning("暂无可导出的数据");
+    return;
+  }
   exportLoading.value = true;
   try {
     const params = { ...(proTable.value?.searchParam || {}) };

+ 54 - 45
src/views/lawyerManagement/lawyer/components/LawyerDialog.vue

@@ -1,49 +1,51 @@
 <template>
-  <el-dialog v-model="visible" :title="dialogTitle" width="600px" destroy-on-close>
-    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-suffix=":">
-      <el-form-item label="律师姓名" prop="name">
-        <el-input v-model="form.name" placeholder="请输入律师姓名" clearable />
-      </el-form-item>
-      <el-form-item label="联系电话" prop="phone">
-        <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" clearable />
-      </el-form-item>
-      <el-form-item label="从业时间" prop="practiceStartDate">
-        <el-date-picker
-          v-model="form.practiceStartDate"
-          type="date"
-          placeholder="请选择从业时间"
-          value-format="YYYY-MM-DD"
-          style="width: 100%"
-        />
-      </el-form-item>
-      <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="firstLevelScenario">
-        <el-tree-select
-          v-model="form.firstLevelScenario"
-          :data="props.sceneOptions"
-          :props="sceneTreeProps"
-          check-strictly
-          placeholder="请选择法律场景"
-          style="width: 100%"
-          clearable
-        />
-      </el-form-item>
-      <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="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>
-          <el-option v-for="item in lawFirmOptions" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </el-form-item>
-    </el-form>
+  <el-dialog v-model="visible" :title="dialogTitle" width="760px" destroy-on-close>
+    <div class="dialog-form">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-suffix=":">
+        <el-form-item label="律师姓名" prop="name">
+          <el-input v-model="form.name" placeholder="请输入律师姓名" clearable />
+        </el-form-item>
+        <el-form-item label="联系电话" prop="phone">
+          <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" clearable />
+        </el-form-item>
+        <el-form-item label="从业时间" prop="practiceStartDate">
+          <el-date-picker
+            v-model="form.practiceStartDate"
+            type="date"
+            placeholder="请选择从业时间"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <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="firstLevelScenario">
+          <el-tree-select
+            v-model="form.firstLevelScenario"
+            :data="props.sceneOptions"
+            :props="sceneTreeProps"
+            check-strictly
+            placeholder="请选择法律场景"
+            style="width: 100%"
+            clearable
+          />
+        </el-form-item>
+        <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="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>
+            <el-option v-for="item in lawFirmOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </div>
     <template #footer>
       <el-button @click="visible = false"> 取消 </el-button>
       <el-button type="primary" :loading="loading" @click="handleSubmit"> 确定 </el-button>
@@ -152,3 +154,10 @@ defineExpose({
   open
 });
 </script>
+
+<style scoped lang="scss">
+.dialog-form {
+  max-width: 640px;
+  margin: 0 auto;
+}
+</style>

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

@@ -2,11 +2,11 @@
   <div class="table-box">
     <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="CirclePlus" :disabled="!isAdmin" @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>
+        <el-button type="primary" link :icon="EditPen" :disabled="!isAdmin" @click="handleEdit(scope.row)"> 编辑 </el-button>
+        <el-button type="danger" link :icon="Delete" :disabled="!isAdmin" @click="handleDelete(scope.row)"> 删除 </el-button>
       </template>
     </ProTable>
     <LawyerDialog
@@ -20,7 +20,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from "vue";
+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";
@@ -35,6 +35,7 @@ import {
 } from "@/api/modules/lawyer";
 import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
 import LawyerDialog from "./components/LawyerDialog.vue";
+import { useUserStore } from "@/stores/modules/user";
 
 const proTable = ref<ProTableInstance>();
 const lawyerDialogRef = ref<InstanceType<typeof LawyerDialog>>();
@@ -42,6 +43,14 @@ const lawFirmOptions = ref<{ label: string; value: string | number }[]>([]);
 const sceneOptions = ref<any[]>([]);
 const professionalOptions = ref<{ label: string; value: string | number }[]>([]);
 
+const userStore = useUserStore();
+const isAdmin = computed(() => (userStore.userInfo?.name || "").toLowerCase() === "admin");
+const ensureAdmin = () => {
+  if (isAdmin.value) return true;
+  ElMessage.warning("仅管理员可使用该功能");
+  return false;
+};
+
 const columns = reactive<ColumnProps<any>[]>([
   { label: "序号", type: "index", width: 60, align: "center" },
   { label: "律师姓名", prop: "name", width: 110, search: { el: "input", props: { placeholder: "请输入律师姓名" } } },
@@ -94,6 +103,7 @@ const refreshTable = () => {
 };
 
 const handleCreate = () => {
+  if (!ensureAdmin()) return;
   lawyerDialogRef.value?.open({
     title: "新增律师",
     onSubmit: async payload => {
@@ -105,6 +115,7 @@ const handleCreate = () => {
 };
 
 const handleEdit = (row: any) => {
+  if (!ensureAdmin()) return;
   lawyerDialogRef.value?.open({
     title: "编辑律师",
     row,
@@ -117,6 +128,7 @@ const handleEdit = (row: any) => {
 };
 
 const handleDelete = (row: any) => {
+  if (!ensureAdmin()) return;
   ElMessageBox.confirm(`确定删除律师【${row.userName}】吗?`, "提示", {
     type: "warning"
   })

+ 34 - 15
src/views/lawyerManagement/legalScene/components/SceneDialog.vue

@@ -1,19 +1,26 @@
 <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="name">
-        <el-input v-model="form.name" placeholder="请输入场景分类" clearable />
-      </el-form-item>
-      <el-form-item v-if="parentSceneName" label="父级场景">
-        <el-input v-model="parentSceneName" disabled />
-      </el-form-item>
-      <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="remark">
-        <el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入描述" />
-      </el-form-item>
-    </el-form>
+  <el-dialog v-model="visible" :title="dialogTitle" width="680px" destroy-on-close>
+    <div class="dialog-form">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-suffix=":">
+        <el-form-item label="场景分类" prop="name">
+          <el-input v-model="form.name" placeholder="请输入场景分类" clearable />
+        </el-form-item>
+        <el-form-item v-if="parentSceneName" label="父级场景">
+          <el-input v-model="parentSceneName" disabled />
+        </el-form-item>
+        <el-form-item label="展示图片">
+          <UploadImg v-model:image-url="form.imgUrl" :drag="false" width="220px" height="140px" :file-size="3">
+            <template #tip> 支持 jpg/png,大小不超过 3MB </template>
+          </UploadImg>
+        </el-form-item>
+        <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="remark">
+          <el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入描述" />
+        </el-form-item>
+      </el-form>
+    </div>
     <template #footer>
       <el-button @click="visible = false"> 取 消 </el-button>
       <el-button type="primary" :loading="loading" @click="handleSubmit"> 保 存 </el-button>
@@ -23,6 +30,7 @@
 
 <script setup lang="ts">
 import { ref, reactive, computed, nextTick } from "vue";
+import UploadImg from "@/components/Upload/Img.vue";
 import type { FormInstance } from "element-plus";
 
 interface DialogOptions {
@@ -48,6 +56,7 @@ const form = reactive({
   name: "",
   status: 1,
   remark: "",
+  imgUrl: "",
   parentId: "",
   parentName: ""
 });
@@ -64,6 +73,7 @@ const resetForm = () => {
   form.name = "";
   form.status = 1;
   form.remark = "";
+  form.imgUrl = "";
   form.parentId = "";
   parentSceneName.value = "";
 };
@@ -73,11 +83,13 @@ const open = (payload: DialogOptions) => {
   options.value = payload;
   if (payload.row) {
     Object.assign(form, payload.row);
+    form.imgUrl = payload.row.imgUrl || "";
   }
   // 新增不需要回显名字
   if (payload.type === "add") {
     form.parentName = form.name;
     form.name = "";
+    form.imgUrl = "";
   }
   visible.value = true;
   nextTick(() => formRef.value?.clearValidate());
@@ -105,3 +117,10 @@ defineExpose({
   open
 });
 </script>
+
+<style scoped lang="scss">
+.dialog-form {
+  max-width: 520px;
+  margin: 0 auto;
+}
+</style>

+ 19 - 13
src/views/lawyerManagement/legalScene/index.vue

@@ -9,9 +9,10 @@
       </div>
     </div>
     <div class="scene-header">
-      <span>场景分类</span>
-      <span>状态</span>
-      <span>操作</span>
+      <span style="flex: 1">场景分类</span>
+      <span style="width: 200px; text-align: center">展示图片</span>
+      <span style="width: 200px; text-align: center">状态</span>
+      <span style="width: 300px; text-align: center">操作</span>
     </div>
     <div class="scene-tree">
       <el-tree
@@ -25,6 +26,9 @@
         <template #default="{ data }">
           <span class="scene-node">
             <span class="scene-name">{{ data.name }}</span>
+            <span class="scene-img">
+              <el-image v-if="data.imgUrl" :src="data.imgUrl" fit="cover" class="scene-thumb" :preview-src-list="[data.imgUrl]" />
+            </span>
             <span class="scene-status">
               <el-tag size="small" :type="data.status === 1 ? 'success' : 'info'">
                 {{ data.status === 1 ? "启用" : "停用" }}
@@ -269,8 +273,7 @@ onMounted(() => {
   width: 360px;
 }
 .scene-header {
-  display: grid;
-  grid-template-columns: 3fr 1fr 1fr;
+  display: flex;
   padding: 12px 16px;
   font-weight: 600;
   background-color: var(--el-fill-color-light);
@@ -286,23 +289,26 @@ onMounted(() => {
   border-radius: 0 0 4px 4px;
 }
 .scene-node {
-  display: grid;
-  grid-template-columns: 3fr 1fr 1fr;
-  align-items: center;
+  display: flex;
   width: 100%;
   padding: 12px 16px;
 }
 .scene-name {
   display: flex;
-  align-items: center;
+  flex: 1;
+  text-align: center;
+}
+.scene-img {
+  width: 200px;
+  text-align: center;
 }
 .scene-status {
-  display: flex;
-  justify-content: flex-start;
+  width: 200px;
+  text-align: center;
 }
 .scene-actions {
-  display: flex;
-  gap: 8px;
+  width: 300px;
+  text-align: center;
 }
 :deep(.el-tree-node__content) {
   height: auto;

+ 15 - 6
src/views/lawyerManagement/professionalField/components/ProfessionalDialog.vue

@@ -1,10 +1,12 @@
 <template>
-  <el-dialog v-model="visible" :title="dialogTitle" width="420px" destroy-on-close>
-    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-suffix=":">
-      <el-form-item label="专业领域" prop="expertiseAreaInfo">
-        <el-input v-model="form.expertiseAreaInfo" placeholder="请输入专业领域" clearable />
-      </el-form-item>
-    </el-form>
+  <el-dialog v-model="visible" :title="dialogTitle" width="560px" destroy-on-close>
+    <div class="dialog-form">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-suffix=":">
+        <el-form-item label="专业领域" prop="expertiseAreaInfo">
+          <el-input v-model="form.expertiseAreaInfo" placeholder="请输入专业领域" clearable />
+        </el-form-item>
+      </el-form>
+    </div>
     <template #footer>
       <el-button @click="visible = false"> 取 消 </el-button>
       <el-button type="primary" :loading="loading" @click="handleSubmit"> 保 存 </el-button>
@@ -78,3 +80,10 @@ defineExpose({
   open
 });
 </script>
+
+<style scoped lang="scss">
+.dialog-form {
+  max-width: 420px;
+  margin: 0 auto;
+}
+</style>

+ 50 - 131
src/views/lawyerManagement/reconciliation/detailDialog.vue

@@ -1,160 +1,79 @@
 <template>
-  <el-dialog v-model="dialogShow" title="达人详情" draggable width="700px" @close="handleClose">
-    <div v-if="detailData.imgUrl.length > 0" class="image-gallery">
-      <div class="image-grid">
-        <div v-for="(url, index) in detailData.imgUrl" :key="index" class="image-item">
-          <el-image
-            :src="url"
-            :preview-src-list="detailData.imgUrl"
-            fit="cover"
-            class="gallery-image"
-            :hide-on-click-modal="false"
-            :preview-teleported="true"
-          />
-        </div>
-      </div>
-    </div>
-
-    <!-- 详情 -->
-    <div class="review-detail">
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="用户ID">
-          {{ detailData.id }}
-        </el-descriptions-item>
-        <el-descriptions-item label="用户昵称">
-          {{ detailData.userName }}
-        </el-descriptions-item>
-        <el-descriptions-item label="姓名">
-          {{ detailData.realName }}
-        </el-descriptions-item>
-        <el-descriptions-item label="身份证号码">
-          {{ detailData.idCard }}
-        </el-descriptions-item>
-        <el-descriptions-item label="联系电话">
-          {{ detailData.userPhone }}
-        </el-descriptions-item>
-
-        <el-descriptions-item label="推广模块">
-          <el-tag v-for="(item, index) in detailData.promoteType" :key="index" style="margin-right: 5px; margin-bottom: 5px">
-            {{ item }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="状态">
-          <el-tag v-if="detailData.expertStatus == 0" type="success"> 已通过 </el-tag>
-          <el-tag v-if="detailData.expertStatus == 1" type="primary"> 待审核 </el-tag>
-          <el-tag v-if="detailData.expertStatus == 2" type="danger"> 已驳回 </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="审核时间" v-if="detailData.reviewTime">
-          {{ detailData.reviewTime || "未审核" }}
-        </el-descriptions-item>
-        <el-descriptions-item label="驳回原因" v-if="detailData.refuseBecause">
-          {{ detailData.refuseBecause }}
-        </el-descriptions-item>
-        <el-descriptions-item label="简介">
-          <div class="introduction">
-            {{ detailData.addExplanation || "--" }}
-          </div>
-        </el-descriptions-item>
-      </el-descriptions>
+  <el-dialog
+    v-model="dialogShow"
+    title="订单详情"
+    width="920px"
+    destroy-on-close
+    :close-on-click-modal="false"
+    @close="handleClose"
+  >
+    <div class="detail-table">
+      <ProTable ref="proTable" :data="detailList" :columns="columns" :tool-button="false" :border="true" height="360" />
     </div>
   </el-dialog>
 </template>
 
 <script setup lang="ts">
-import { ref, defineExpose, defineEmits } from "vue";
+import { ref, defineExpose } from "vue";
 import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import type { ColumnProps } from "@/components/ProTable/interface";
+import { getLawyerDetails } from "@/api/modules/lawyer";
 
-// 定义弹窗状态
 const dialogShow = ref(false);
+const detailList = ref<any[]>([]);
+const currentQuery = ref<String>("");
 
-// 详情数据
-const detailData: any = ref({});
+const columns: ColumnProps[] = [
+  { label: "用户ID", prop: "clientUserId", minWidth: 120 },
+  { label: "用户名", prop: "clientUserName", minWidth: 160, showOverflowTooltip: true },
+  { label: "订单编号", prop: "orderNumber", minWidth: 160, showOverflowTooltip: true },
+  { label: "订单金额", prop: "orderAmount", minWidth: 140, align: "center" },
+  { label: "购买时间", prop: "orderTime", minWidth: 180 }
+];
 
-const emit = defineEmits(["close"]);
-// 显示弹窗方法
-const open = async (userId: string | number) => {
+const fetchDetails = async () => {
   try {
-    const res: any = await getViewDetails(userId);
-    detailData.value = {
-      ...res.data
-    };
-    if (res.data.imgUrl) {
-      detailData.value.imgUrl = res.data.imgUrl.split(",");
-    }
-    if (res.data.promoteType) {
-      detailData.value.promoteType = res.data.promoteType.split(",");
-    }
-    dialogShow.value = true;
+    const res: any = await getLawyerDetails({
+      lawyerId: currentQuery.value
+    });
+    detailList.value = res.data?.records;
   } catch (error) {
-    ElMessage.error("加载详情失败,请重试");
-    handleClose();
+    detailList.value = [];
+    ElMessage.error("加载详情失败,请稍后重试");
   }
 };
 
-// 关闭弹窗
+const open = async (lawyerId: String) => {
+  currentQuery.value = lawyerId;
+  dialogShow.value = true;
+  await fetchDetails();
+};
+
 const handleClose = () => {
   dialogShow.value = false;
+  detailList.value = [];
+  currentQuery.value = "";
 };
 
-// 暴露给父组件的方法
 defineExpose({
   open
 });
 </script>
 
-<style lang="scss" scoped>
-/* 样式保持不变 */
-.review-detail {
-  margin-bottom: 20px;
-  .introduction {
-    line-height: 1.5;
-    word-break: break-word;
-    white-space: pre-wrap;
-  }
+<style scoped lang="scss">
+.detail-table {
+  min-height: 360px;
+  max-height: 460px;
+  overflow: hidden;
 }
-.form-tip {
-  margin-top: 5px;
-  font-size: 12px;
-  color: #909399;
+.detail-table :deep(.el-table__body-wrapper) {
+  max-height: 380px;
+  overflow: auto;
 }
-.image-gallery {
-  padding: 10px;
-  margin-bottom: 20px;
-  background-color: #f5f7fa;
-  border-radius: 4px;
-  .image-grid {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 10px;
-    .image-item {
-      width: 100px;
-      height: 100px;
-      cursor: pointer;
-      transition: all 0.3s;
-      &:hover {
-        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
-        transform: scale(1.05);
-      }
-      .gallery-image {
-        width: 100%;
-        height: 100%;
-        overflow: hidden;
-        border-radius: 4px;
-        :deep(.el-image__inner) {
-          width: 100%;
-          height: 100%;
-          object-fit: cover;
-        }
-        :deep(.el-image__error) {
-          display: flex;
-          align-items: center;
-          justify-content: center;
-          font-size: 12px;
-          color: #909399;
-          background-color: #f0f2f5;
-        }
-      }
-    }
-  }
+.detail-pagination {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 12px;
 }
 </style>

+ 58 - 179
src/views/lawyerManagement/reconciliation/index.vue

@@ -1,29 +1,9 @@
 <template>
   <div class="reconciliation-page">
     <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="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 class="content-panel">
         <div class="summary-wrapper">
-          <div class="summary-card" v-for="item in summaryCards" :key="item.label">
+          <div v-for="item in summaryCards" :key="item.label" class="summary-card">
             <div class="summary-label">
               {{ item.label }}
             </div>
@@ -32,7 +12,6 @@
             </div>
           </div>
         </div>
-
         <ProTable
           ref="proTable"
           :columns="columns"
@@ -40,16 +19,8 @@
           :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 #tableHeader>
+            <el-button type="primary" :icon="Download" :disabled="!canExport" @click="handleExport"> 导出 </el-button>
           </template>
           <template #operation="scope">
             <el-button type="primary" link :icon="EditPen" @click="handleDetail(scope.row)"> 查看详情 </el-button>
@@ -57,47 +28,58 @@
         </ProTable>
       </div>
     </div>
-    <DetailDialog ref="detailDialog" />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onActivated, onMounted, computed, nextTick } from "vue";
+import { ref, reactive, onActivated, onMounted, computed } from "vue";
+import { useRouter } from "vue-router";
 import type { Course } from "@/api/interface";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import DetailDialog from "./detailDialog.vue";
-import { getOrderList, getOverview, getLawFirmPage } from "@/api/modules/lawyer";
+import { getAllLawFirmList, getOverview, exportLawFirmReconciliation } from "@/api/modules/lawyer";
 import { useUserStore } from "@/stores/modules/user";
-import { EditPen, Search } from "@element-plus/icons-vue";
+import { Download, EditPen } from "@element-plus/icons-vue";
 
 const proTable = ref<ProTableInstance>();
-const detailDialog = ref<any>(null);
-const userStore = useUserStore();
+const router = useRouter();
 
 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 canExport = computed(() => {
+  const table = proTable.value;
+  if (!table) return false;
+  const data = table.tableData as any;
+  if (Array.isArray(data)) return data.length > 0;
+  if (data && typeof data === "object" && Array.isArray((data as any).list)) return (data as any).list.length > 0;
+  return false;
 });
 
 const columns = reactive<ColumnProps<Course.ReqCourseParams>[]>([
-  { 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: "firmName", minWidth: 280, search: { el: "input", props: { placeholder: "请输入律所名称" } } },
+  { label: "订单数量", prop: "totalOrderCount", width: 160, align: "center" },
+  { label: "订单金额", prop: "totalOrderAmountYuan", width: 200, align: "center" },
+  { label: "平台信息服务费", prop: "platformServiceFeeYuan", width: 220, align: "center" },
   {
     label: "订单日期",
     prop: "orderTime",
+    isShow: false,
     width: 180,
-    search: { el: "date-picker", props: { type: "date", valueFormat: "YYYY-MM-DD", placeholder: "请选择从业时间" } }
+    search: {
+      el: "date-picker",
+      props: {
+        type: "monthrange",
+        valueFormat: "YYYY-MM",
+        format: "YYYY-MM",
+        startPlaceholder: "开始月份",
+        endPlaceholder: "结束月份",
+        rangeSeparator: "至",
+        unlinkPanels: true
+      }
+    }
   },
   { label: "操作", prop: "operation", width: 160, align: "center", fixed: "right" }
 ]);
@@ -109,20 +91,18 @@ const buildQueryParams = (params: any) => {
     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;
+  if (params.orderTime) {
+    newParams.startTime = params.orderTime[0] + "-00";
+    newParams.endTime = params.orderTime[1] + "-31";
   }
+  delete newParams.orderTime;
   return newParams;
 };
 
 const fetchOverview = async (params: any) => {
   try {
     const res: any = await getOverview(params);
-    const overview = res?.data || res || {};
+    const overview = res?.data;
     summaryData.value = {
       totalOrderCount: Number(overview.totalOrderCount) || 0,
       totalOrderAmountYuan: Number(overview.totalOrderAmountYuan) || 0,
@@ -140,7 +120,7 @@ const fetchOverview = async (params: any) => {
 
 const getTableList = async (params: any) => {
   const queryParams = buildQueryParams(params);
-  const [listRes] = await Promise.all([getOrderList(queryParams), fetchOverview(queryParams)]);
+  const [listRes] = await Promise.all([getAllLawFirmList(queryParams), fetchOverview(queryParams)]);
   return listRes;
 };
 const dataCallback = (data: any) => {
@@ -152,64 +132,24 @@ const dataCallback = (data: any) => {
 
 // 处理推广板块字符串
 const filters = reactive({
-  name: "",
+  firmName: "",
   dateRange: [] as string[]
 });
 
 const summaryCards = computed(() => [
-  { label: "总订单数量", value: formatNumber(summaryData.value.totalOrderCount) },
+  { label: "总订单数量", value: summaryData.value.totalOrderCount },
   { label: "总订单金额", value: formatCurrency(summaryData.value.totalOrderAmountYuan) },
   { label: "平台信息服务费", value: formatCurrency(summaryData.value.platformServiceFeeYuan) }
 ]);
 
-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;
-  else delete proTable.value.searchParam.userName;
+  if (filters.firmName) proTable.value.searchParam.firmName = filters.firmName;
+  else delete proTable.value.searchParam.firmName;
 
   if (filters.dateRange?.length === 2) {
     proTable.value.searchParam.time = filters.dateRange;
@@ -220,28 +160,34 @@ const handleSearch = () => {
 };
 
 const handleReset = () => {
-  filters.name = "";
+  filters.firmName = "";
   filters.dateRange = [];
   handleSearch();
 };
 
 const formatCurrency = (val?: number | string) => {
-  const num = Number(val) || 0;
-  return `¥${num.toLocaleString("zh-CN", { minimumFractionDigits: 2 })}`;
+  return `¥${val || 0}`;
 };
 
-const formatNumber = (val?: number | string) => {
-  const num = Number(val) || 0;
-  return num.toLocaleString("zh-CN");
+const handleDetail = (row: any) => {
+  if (!row?.firmId) return;
+  router.push({
+    path: "/lawyerManagement/reconciliation/lawyerDetail",
+    query: {
+      firmId: row.firmId,
+      firmName: row.firmName
+    }
+  });
 };
 
-// 处理同意操作 expertStatus: 0  包含佣金和预付款比例
-const handleDetail = (row: any) => {
-  detailDialog.value?.open(row.userId);
+const handleExport = async () => {
+  if (!canExport.value) return;
+  const params = { ...(proTable.value?.searchParam || {}) };
+  await exportLawFirmReconciliation(params);
 };
 
 onMounted(() => {
-  initializeContext();
+  refreshTable();
 });
 
 onActivated(() => {
@@ -266,73 +212,6 @@ onActivated(() => {
   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;

+ 295 - 0
src/views/lawyerManagement/reconciliation/lawyerDetail.vue

@@ -0,0 +1,295 @@
+<template>
+  <div class="reconciliation-page">
+    <div class="layout-wrapper">
+      <div class="content-panel">
+        <div class="summary-wrapper">
+          <div v-for="item in summaryCards" :key="item.label" class="summary-card">
+            <div class="summary-label">
+              {{ item.label }}
+            </div>
+            <div class="summary-value">
+              {{ item.value }}
+            </div>
+          </div>
+        </div>
+
+        <ProTable
+          ref="proTable"
+          :columns="columns"
+          :request-api="getTableList"
+          :data-callback="dataCallback"
+          :request-auto="false"
+          :data-export="handleExport"
+          :export-disabled="!canExport"
+        >
+          <template #tableHeader>
+            <el-button type="primary" :icon="Download" :disabled="!canExport" @click="handleExport"> 导出 </el-button>
+          </template>
+          <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, onMounted, computed } from "vue";
+import { useRoute } from "vue-router";
+import type { Course } from "@/api/interface";
+import ProTable from "@/components/ProTable/index.vue";
+import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import DetailDialog from "./detailDialog.vue";
+import { getLawyerList, getOverview, exportLawFirmReconciliation } from "@/api/modules/lawyer";
+import { useUserStore } from "@/stores/modules/user";
+import { Download, EditPen } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+
+const proTable = ref<ProTableInstance>();
+const detailDialog = ref<any>(null);
+const userStore = useUserStore();
+const route = useRoute();
+const canExport = computed(() => {
+  const table = proTable.value;
+  if (!table) return false;
+  const data = table.tableData as any;
+  if (Array.isArray(data)) return data.length > 0;
+  if (data && typeof data === "object" && Array.isArray((data as any).list)) return (data as any).list.length > 0;
+  return false;
+});
+
+const summaryData = ref({
+  totalOrderCount: 0,
+  totalOrderAmountYuan: 0,
+  platformServiceFeeYuan: 0
+});
+const isAdmin = computed(() => userStore.userInfo?.name === "admin");
+const routeFirmId = computed(() => (route.query.firmId as string) || "");
+const fallbackFirmId = computed(() => userStore.userInfo?.firmId || "");
+const currentFirmId = computed(() => {
+  if (routeFirmId.value) return routeFirmId.value;
+  if (isAdmin.value) return fallbackFirmId.value;
+  return fallbackFirmId.value;
+});
+
+const columns = reactive<ColumnProps<Course.ReqCourseParams>[]>([
+  { label: "律师信息", prop: "lawyerName", minWidth: 280, search: { el: "input", props: { placeholder: "请输入律师姓名" } } },
+  { label: "订单数量", prop: "totalOrderCount", width: 160, align: "center" },
+  { label: "订单金额", prop: "totalOrderAmountYuan", width: 200, align: "center" },
+  { label: "平台信息服务费", prop: "platformServiceFeeYuan", width: 220, align: "center" },
+  {
+    label: "订单日期",
+    prop: "orderTime",
+    width: 180,
+    isShow: false,
+    search: {
+      el: "date-picker",
+      props: {
+        type: "monthrange",
+        valueFormat: "YYYY-MM",
+        format: "YYYY-MM",
+        startPlaceholder: "开始月份",
+        endPlaceholder: "结束月份",
+        rangeSeparator: "至",
+        unlinkPanels: true
+      }
+    }
+  },
+  { label: "操作", prop: "operation", width: 160, align: "center", fixed: "right" }
+]);
+const buildQueryParams = (params: any) => {
+  const tempParams = { ...params };
+  delete tempParams.time;
+  const newParams = {
+    ...tempParams,
+    pageNum: tempParams.pageNum,
+    pageSize: tempParams.pageSize
+  };
+  if (params.orderTime) {
+    newParams.startTime = params.orderTime[0] + "-00";
+    newParams.endTime = params.orderTime[1] + "-31";
+  }
+  delete newParams.orderTime;
+  if (currentFirmId.value) {
+    newParams.firmId = currentFirmId.value;
+  }
+  return newParams;
+};
+
+const fetchOverview = async (params: any) => {
+  try {
+    const res: any = await getOverview(params);
+    const overview = res?.data;
+    summaryData.value = {
+      totalOrderCount: overview.totalOrderCount || 0,
+      totalOrderAmountYuan: overview.totalOrderAmountYuan || 0,
+      platformServiceFeeYuan: 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([getLawyerList(queryParams), fetchOverview(queryParams)]);
+  return listRes;
+};
+const dataCallback = (data: any) => {
+  return {
+    list: data.records,
+    total: data.total
+  };
+};
+
+const filters = reactive({
+  lawyerName: "",
+  dateRange: [] as string[]
+});
+
+const summaryCards = computed(() => [
+  { label: "总订单数量", value: summaryData.value.totalOrderCount },
+  { label: "总订单金额", value: formatCurrency(summaryData.value.totalOrderAmountYuan) },
+  { label: "平台信息服务费", value: formatCurrency(summaryData.value.platformServiceFeeYuan) }
+]);
+
+const refreshTable = () => {
+  proTable.value?.getTableList();
+};
+
+const handleSearch = () => {
+  if (!proTable.value) return;
+  if (filters.lawyerName) proTable.value.searchParam.lawyerName = filters.lawyerName;
+  else delete proTable.value.searchParam.lawyerName;
+
+  if (filters.dateRange?.length === 2) {
+    proTable.value.searchParam.time = filters.dateRange;
+  } else {
+    delete proTable.value.searchParam.time;
+  }
+  proTable.value.search();
+};
+
+const formatCurrency = (val?: number | string) => {
+  return `¥${val || 0}`;
+};
+
+const handleDetail = (row: any) => {
+  detailDialog.value?.open(row.lawyerId);
+};
+
+const handleExport = async () => {
+  if (!currentFirmId.value) {
+    ElMessage.warning("缺少律所信息,无法导出");
+    return;
+  }
+  if (!canExport.value) {
+    ElMessage.warning("暂无可导出的数据");
+    return;
+  }
+  const params = { ...(proTable.value?.searchParam || {}), firmId: currentFirmId.value };
+  await exportLawFirmReconciliation(params);
+};
+
+onMounted(() => {
+  refreshTable();
+});
+
+onActivated(() => {
+  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;
+}
+.content-panel {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 0;
+}
+.page-heading {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1c2130;
+}
+.page-heading .heading-label {
+  margin-right: 6px;
+  font-weight: normal;
+  color: #7a7f87;
+}
+.summary-wrapper {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+  gap: 16px;
+}
+.summary-card {
+  padding: 16px;
+  background: #ffffff;
+  border: 1px solid #e2e6ef;
+  border-radius: 12px;
+}
+.summary-label {
+  margin-bottom: 4px;
+  color: #7a7f87;
+}
+.summary-value {
+  font-size: 24px;
+  font-weight: 600;
+  color: #1f2532;
+}
+.lawyer-info {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+.lawyer-meta {
+  display: flex;
+  flex-direction: column;
+  font-size: 13px;
+  color: #7e7f8c;
+}
+.lawyer-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1c2130;
+}
+.lawyer-extra {
+  font-size: 12px;
+  color: #a5a7b3;
+}
+</style>

+ 290 - 0
src/views/refund/detailDialog.vue

@@ -0,0 +1,290 @@
+<template>
+  <el-dialog v-model="dialogShow" title="尾款详情" draggable width="860px" @close="handleClose">
+    <!-- 合并指标区域为一行 -->
+    <div class="metrics-sections">
+      <div class="metrics-section">
+        <div class="section-header">预计指标</div>
+        <div class="metrics-cards">
+          <div class="metric-card">
+            <div class="metric-label">GMV(元)</div>
+            <div class="metric-value">
+              {{ formatAmount(detailData.orderGmv) }}
+            </div>
+          </div>
+          <div class="metric-card">
+            <div class="metric-label">播放量</div>
+            <div class="metric-value">
+              {{ detailData.orderPlayCount || 0 }}
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="metrics-section">
+        <div class="section-header">实际完成</div>
+        <div class="metrics-cards">
+          <div class="metric-card">
+            <div class="metric-label">GMV(元)</div>
+            <div class="metric-value">
+              {{ formatAmount(detailData.actualGmv) }}
+            </div>
+          </div>
+          <div class="metric-card">
+            <div class="metric-label">播放量</div>
+            <div class="metric-value">
+              {{ detailData.actualPlayCount || 0 }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 详情展示区域 -->
+    <div class="review-container">
+      <div class="review-detail">
+        <el-descriptions :column="2" border>
+          <!-- 第一列:重要信息 -->
+          <el-descriptions-item label="套餐名称">
+            {{ detailData.name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="订单编号">
+            {{ detailData.orderNo }}
+          </el-descriptions-item>
+          <el-descriptions-item label="达人ID">
+            {{ detailData.expertId }}
+          </el-descriptions-item>
+          <el-descriptions-item label="达人昵称">
+            {{ detailData.userName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="姓名">
+            {{ detailData.realName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="联系电话">
+            {{ detailData.userPhone }}
+          </el-descriptions-item>
+          <el-descriptions-item label="身份证号码" :span="2">
+            {{ detailData.idCard }}
+          </el-descriptions-item>
+
+          <!-- 第二列:财务信息 -->
+          <el-descriptions-item label="订单金额(元)" :span="2">
+            {{ formatAmount(detailData.orderMoney) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="尾款比例"> {{ detailData.endPaymentRate || 0 }}% </el-descriptions-item>
+          <el-descriptions-item label="尾款(元)">
+            {{ formatAmount(detailData.endPayment) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="预付款比例"> {{ detailData.advanceRate || 0 }}% </el-descriptions-item>
+          <el-descriptions-item label="预付款(元)">
+            {{ formatAmount(detailData.advance) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="佣金比例"> {{ detailData.commissionRate || 0 }}% </el-descriptions-item>
+          <el-descriptions-item label="佣金(元)">
+            {{ formatAmount(detailData.commission) }}
+          </el-descriptions-item>
+
+          <!-- 底部重要信息 -->
+          <el-descriptions-item label="推广模块" :span="2">
+            <el-tag
+              v-for="(item, index) in getPromoteTypes(detailData.promoteType)"
+              :key="index"
+              style="margin-right: 5px; margin-bottom: 5px"
+            >
+              {{ item }}
+            </el-tag>
+          </el-descriptions-item>
+
+          <el-descriptions-item label="状态" :span="2">
+            <el-tag v-if="detailData.advancePaymentStatus == 0" type="success"> 审核通过 </el-tag>
+            <el-tag v-if="detailData.advancePaymentStatus == 1" type="success"> 待完成 </el-tag>
+            <el-tag v-if="detailData.advancePaymentStatus == 2" type="primary"> 已完成 </el-tag>
+          </el-descriptions-item>
+
+          <el-descriptions-item label="下单时间">
+            {{ detailData.orderTime || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="支付时间">
+            {{ detailData.payTime || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="完成时间" v-if="detailData.completeTime">
+            {{ detailData.completeTime || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="申请时间" v-if="detailData.endFundsTime">
+            {{ detailData.endFundsTime || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="审核时间" v-if="detailData.endPaymentTime">
+            {{ detailData.endPaymentTime }}
+          </el-descriptions-item>
+          <el-descriptions-item label="驳回原因" v-if="detailData.endPaymentRefusal">
+            {{ detailData.endPaymentRefusal }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, defineExpose, defineEmits } from "vue";
+
+// 定义弹窗状态
+const dialogShow = ref(false);
+
+// 详情数据
+const detailData: any = ref({});
+// 处理推广板块字符串
+const getPromoteTypes = (promoteType: string) => {
+  if (!promoteType) return [];
+  return promoteType.split(",");
+};
+
+// 金额格式化
+const formatAmount = (amount: number | string) => {
+  const num = typeof amount === "string" ? parseFloat(amount) : amount;
+  return isNaN(num) ? "¥0.00" : `¥${num.toFixed(2)}`;
+};
+
+// 事件触发
+const emit = defineEmits(["close"]);
+
+// 显示弹窗方法
+const open = (data: any) => {
+  detailData.value = {
+    ...data
+  };
+  dialogShow.value = true;
+};
+
+// 关闭弹窗
+const handleClose = () => {
+  dialogShow.value = false;
+  emit("close");
+};
+
+// 暴露给父组件的方法
+defineExpose({
+  open,
+  close: handleClose
+});
+</script>
+
+<style lang="scss" scoped>
+.section-header {
+  padding-left: 5px;
+  margin: 10px 0 12px;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  border-left: 3px solid #409eff;
+}
+.metrics-sections {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 15px;
+
+  /* 在小屏幕上自动堆叠 */
+  @media (width <=600px) {
+    flex-direction: column;
+    gap: 10px;
+  }
+}
+.metrics-section {
+  flex: 1;
+  min-width: 0;
+  .section-header {
+    padding-left: 5px;
+    margin: 0 0 8px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    border-left: 3px solid #409eff;
+  }
+}
+.metrics-cards {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 8px;
+}
+.metric-card {
+  padding: 8px 10px;
+  background: #f8f9fa;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  transition: all 0.2s ease;
+  &:hover {
+    background: #f0f7ff;
+    border-color: #d6e4ff;
+  }
+}
+.metric-label {
+  margin-bottom: 4px;
+  font-size: 12px;
+  font-weight: 500;
+  color: #606266;
+}
+.metric-value {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+.review-container {
+  margin-bottom: 15px;
+
+  /* 滚动条样式优化 */
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+  }
+  &::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+  }
+}
+.review-detail {
+  .introduction {
+    line-height: 1.5;
+    word-break: break-word;
+    white-space: pre-wrap;
+  }
+}
+
+/* 隐藏预览时的操作栏 */
+:deep(.el-image-viewer__btn) {
+  display: none !important;
+}
+
+/* 或者更精确地隐藏操作栏区域 */
+:deep(.el-image-viewer__actions) {
+  display: none !important;
+}
+.image-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+  gap: 8px;
+}
+.image-item {
+  aspect-ratio: 1;
+  overflow: hidden;
+}
+.gallery-image {
+  width: 100%;
+  height: 100%;
+  cursor: pointer;
+  object-fit: cover;
+}
+
+/* 调整描述项的间距 */
+:deep(.el-descriptions__label) {
+  font-weight: 600;
+  background-color: #fafafa;
+}
+:deep(.el-descriptions__cell) {
+  padding: 10px 20px;
+}
+</style>

+ 299 - 0
src/views/refund/index.vue

@@ -0,0 +1,299 @@
+<template>
+  <div class="table-box">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :data-callback="dataCallback">
+      <template #endFundsButton="scope">
+        <el-tag v-if="scope.row.endFundsButton == 1" type="success"> 审核中 </el-tag>
+        <el-tag v-if="scope.row.endFundsButton == 2" type="success"> 已通过 </el-tag>
+        <el-tag v-if="scope.row.endFundsButton == 3" type="primary"> 已驳回 </el-tag>
+      </template>
+      <template #promoteType="scope">
+        <el-tag v-for="(item, index) in getPromoteTypes(scope.row.promoteType)" :key="index">
+          {{ item }}
+        </el-tag>
+      </template>
+
+      <template #operation="scope">
+        <el-button type="primary" :icon="Search" link @click="handleDetail(scope.row)"> 查看详情 </el-button>
+        <el-button type="primary" :icon="Setting" v-if="scope.row.endFundsButton == 1" link @click="handleReview(scope.row)">
+          审核
+        </el-button>
+      </template>
+    </ProTable>
+
+    <ReviewDialog ref="reviewDialog" @approve="handleApprove" @reject="handleReject" />
+    <DetailDialog ref="detailDialog" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import ReviewDialog from "./reviewDialog.vue";
+import DetailDialog from "./detailDialog.vue";
+import { ref, reactive, onActivated } from "vue";
+import type { Course } from "@/api/interface";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import { Search, Setting } from "@element-plus/icons-vue";
+
+const proTable = ref<ProTableInstance>();
+
+const reviewDialog = ref<any>(null);
+const detailDialog = ref<any>(null);
+
+const columns = reactive<ColumnProps<Course.ReqCourseParams>[]>([
+  {
+    label: "序号",
+    type: "index",
+    width: 60,
+    align: "center",
+    fixed: "left" // 固定在左侧
+  },
+  {
+    label: "订单编号",
+    prop: "orderNo",
+    width: 240,
+    search: { el: "input", tooltip: "请输入订单编号" },
+    fixed: "left", // 固定在左侧
+    showOverflowTooltip: true
+  },
+  {
+    label: "姓名",
+    prop: "realName",
+    width: 160,
+    search: { el: "input", tooltip: "请输入姓名" },
+    showOverflowTooltip: true
+  },
+  {
+    label: "订单金额(元)",
+    prop: "orderMoney",
+    width: 200,
+    align: "right"
+  },
+  {
+    label: "状态",
+    prop: "endFundsButton",
+    width: 100,
+    search: {
+      el: "select",
+      tooltip: "请选择状态",
+      props: { clearable: true }
+    },
+    enum: [
+      { value: 1, label: "审核中" },
+      { value: 2, label: "已通过" },
+      { value: 3, label: "已驳回" }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    label: "下单时间",
+    prop: "orderTime"
+  },
+  {
+    label: "申请时间",
+    prop: "endFundsTime"
+  },
+  {
+    label: "下单时间",
+    prop: "time1",
+    isShow: false, // 关键:不在表格中显示
+    search: {
+      el: "date-picker",
+      props: {
+        type: "datetimerange",
+        valueFormat: "YYYY-MM-DD HH:mm:ss",
+        rangeSeparator: "至",
+        startPlaceholder: "开始时间",
+        endPlaceholder: "结束时间"
+      }
+    }
+  },
+  {
+    label: "申请时间",
+    prop: "time4",
+    isShow: false, // 关键:不在表格中显示
+    search: {
+      el: "date-picker",
+      props: {
+        type: "datetimerange",
+        valueFormat: "YYYY-MM-DD HH:mm:ss",
+        rangeSeparator: "至",
+        startPlaceholder: "开始时间",
+        endPlaceholder: "结束时间"
+      }
+    }
+  },
+  {
+    label: "操作",
+    prop: "operation",
+    width: 200,
+    fixed: "right" // 固定在右侧
+  }
+]);
+
+const getTableList = async (params: any) => {
+  let tempParams = JSON.parse(JSON.stringify(params));
+  delete tempParams.time1;
+  delete tempParams.time2;
+  delete tempParams.time3;
+  delete tempParams.time4;
+  // 深拷贝原始参数
+  let newParams = JSON.parse(JSON.stringify(tempParams));
+  newParams.page = newParams.pageNum;
+  newParams.size = newParams.pageSize;
+  delete newParams.pageNum;
+  delete newParams.pageSize;
+  if (params.time1) {
+    newParams.orderCreatedTime = params.time1[0];
+    newParams.orderEndTime = params.time1[1];
+  }
+  // todo  后端 现在下单时间和支付时间是一个字段,后续调整 在改动
+  if (params.time2) {
+    newParams.payCreatedTime = params.time2[0];
+    newParams.payEndTime = params.time2[1];
+  }
+  if (params.time3) {
+    newParams.firstCompleteTime = params.time3[0];
+    newParams.endCompleteTime = params.time3[1];
+  }
+  if (params.time4) {
+    newParams.createdEndPaymentTime = params.time4[0];
+    newParams.endEndPaymentTime = params.time4[1];
+  }
+  const res = await getFinalPaymentList(newParams);
+  return res;
+};
+
+const dataCallback = (data: any) => {
+  return {
+    list: data.records,
+    total: data.total
+  };
+};
+
+// 处理推广板块字符串
+const getPromoteTypes = (promoteType: string) => {
+  if (!promoteType) return [];
+  return promoteType.split(",");
+};
+
+//详情
+const handleDetail = row => {
+  detailDialog.value?.open(row);
+};
+
+// 审核
+const handleReview = (row: any) => {
+  reviewDialog.value?.open(row);
+};
+
+// 处理同意操作 endFundsButton: 2
+const handleApprove = async (payload: {
+  id: number;
+  name: string;
+  orderMoney: number;
+  orderNo: string;
+  storeTel: string;
+  userPhone: string;
+  endPaymentRefusal: string;
+  endPayment: number | string;
+}) => {
+  try {
+    const res: any = await getFinalPaymentReview({
+      ...payload,
+      endFundsButton: 2
+    });
+    if (res.data.status == 0) {
+      ElMessage.success(res.msg);
+    }
+    if (res.data.status == 1) {
+      ElMessage.error(res.data.codeMsg);
+    }
+    proTable.value?.getTableList();
+  } catch (error) {
+    console.error("审核通过失败:", error);
+    ElMessage.error("审核通过失败");
+  }
+};
+
+// 处理驳回操作 endFundsButton: 3
+const handleReject = async (payload: {
+  id: number;
+  name: string;
+  orderMoney: number;
+  orderNo: string;
+  storeTel: string;
+  userPhone: string;
+  endPaymentRefusal: string;
+  endPayment: number | string;
+}) => {
+  try {
+    const res: any = await getFinalPaymentReview({
+      ...payload,
+      endFundsButton: 3
+    });
+    if (res.data.status == 0) {
+      ElMessage.success(res.msg);
+    }
+    if (res.data.status == 1) {
+      ElMessage.error(res.data.codeMsg);
+    }
+    proTable.value?.getTableList();
+  } catch (error) {
+    console.error("审核驳回失败:", error);
+    ElMessage.error("审核驳回失败");
+  }
+};
+
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+</script>
+
+<style scoped lang="scss">
+/* 操作列按钮间距优化 */
+:deep(.operation-column .cell) {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  .el-button {
+    height: auto;
+    padding: 5px 10px;
+    &.is-link {
+      min-width: auto;
+      padding: 0;
+    }
+  }
+}
+
+/* 金额列高亮显示 */
+:deep(.financial-column .cell) {
+  font-weight: 600;
+  color: #e6a23c; // Element Plus 橙色
+  text-align: center;
+}
+
+/* 表格行悬停效果增强 */
+:deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
+  background-color: #f5f9ff !important;
+  transition: background-color 0.3s;
+}
+
+/* 表头样式优化 */
+:deep(.el-table__header-wrapper th) {
+  padding: 10px 0;
+  font-weight: 600;
+  color: #303133;
+  background-color: #f8f9fa;
+  .cell {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+/* 单元格内边距优化 */
+:deep(.el-table__cell) {
+  padding: 8px 0;
+}
+</style>

+ 401 - 0
src/views/refund/reviewDialog.vue

@@ -0,0 +1,401 @@
+<template>
+  <el-dialog v-model="dialogShow" title="尾款审核" draggable width="860px" @close="handleClose">
+    <!-- 合并指标区域为一行 -->
+    <div class="metrics-sections">
+      <div class="metrics-section">
+        <div class="section-header">预计指标</div>
+        <div class="metrics-cards">
+          <div class="metric-card">
+            <div class="metric-label">GMV(元)</div>
+            <div class="metric-value">
+              {{ formatAmount(detailData.orderGmv) }}
+            </div>
+          </div>
+          <div class="metric-card">
+            <div class="metric-label">播放量</div>
+            <div class="metric-value">
+              {{ detailData.orderPlayCount || 0 }}
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="metrics-section">
+        <div class="section-header">实际完成</div>
+        <div class="metrics-cards">
+          <div class="metric-card">
+            <div class="metric-label">GMV(元)</div>
+            <div class="metric-value">
+              {{ formatAmount(detailData.actualGmv) }}
+            </div>
+          </div>
+          <div class="metric-card">
+            <div class="metric-label">播放量</div>
+            <div class="metric-value">
+              {{ detailData.actualPlayCount || 0 }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 详情展示区域 -->
+    <div class="review-container">
+      <div class="review-detail">
+        <el-descriptions :column="2" border>
+          <!-- 第一列:重要信息 -->
+          <el-descriptions-item label="套餐名称">
+            {{ detailData.name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="订单编号">
+            {{ detailData.orderNo }}
+          </el-descriptions-item>
+          <el-descriptions-item label="达人ID">
+            {{ detailData.expertId }}
+          </el-descriptions-item>
+          <el-descriptions-item label="达人昵称">
+            {{ detailData.userName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="姓名">
+            {{ detailData.realName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="联系电话">
+            {{ detailData.userPhone }}
+          </el-descriptions-item>
+          <el-descriptions-item label="身份证号码" :span="2">
+            {{ detailData.idCard }}
+          </el-descriptions-item>
+
+          <!-- 第二列:财务信息 -->
+          <el-descriptions-item label="订单金额(元)" :span="2">
+            {{ formatAmount(detailData.orderMoney) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="尾款比例"> {{ detailData.endPaymentRate || 0 }}% </el-descriptions-item>
+          <el-descriptions-item label="尾款(元)">
+            {{ formatAmount(detailData.endPayment) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="预付款比例"> {{ detailData.advanceRate || 0 }}% </el-descriptions-item>
+          <el-descriptions-item label="预付款(元)">
+            {{ formatAmount(detailData.advance) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="佣金比例"> {{ detailData.commissionRate || 0 }}% </el-descriptions-item>
+          <el-descriptions-item label="佣金(元)">
+            {{ formatAmount(detailData.commission) }}
+          </el-descriptions-item>
+
+          <!-- 底部重要信息 -->
+          <el-descriptions-item label="推广模块" :span="2">
+            <el-tag
+              v-for="(item, index) in getPromoteTypes(detailData.promoteType)"
+              :key="index"
+              style="margin-right: 5px; margin-bottom: 5px"
+            >
+              {{ item }}
+            </el-tag>
+          </el-descriptions-item>
+
+          <el-descriptions-item label="状态" :span="2">
+            <el-tag v-if="detailData.advancePaymentStatus == 0" type="success"> 审核通过 </el-tag>
+            <el-tag v-if="detailData.advancePaymentStatus == 1" type="success"> 待完成 </el-tag>
+            <el-tag v-if="detailData.advancePaymentStatus == 2" type="primary"> 已完成 </el-tag>
+          </el-descriptions-item>
+
+          <el-descriptions-item label="下单时间">
+            {{ detailData.orderTime || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="支付时间">
+            {{ detailData.payTime || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="完成时间" v-if="detailData.completeTime">
+            {{ detailData.completeTime }}
+          </el-descriptions-item>
+          <el-descriptions-item label="申请时间" v-if="detailData.endFundsTime">
+            {{ detailData.endFundsTime }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+    </div>
+
+    <!-- 操作按钮区域 -->
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button type="success" @click="confirmApprove"> 同意 </el-button>
+        <el-button type="warning" @click="openRejectDialog"> 驳回 </el-button>
+        <el-button @click="handleClose">关闭</el-button>
+      </span>
+    </template>
+
+    <!-- 驳回原因弹窗 -->
+    <el-dialog title="驳回原因" v-model="rejectDialogVisible" width="520px" append-to-body>
+      <el-form :model="rejectForm" :rules="rejectRules" ref="rejectFormRef" label-width="80px">
+        <el-form-item label="原因" prop="reasonRefusal">
+          <el-input v-model="rejectForm.reasonRefusal" type="textarea" :rows="3" placeholder="请输入驳回原因" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="cancelRejectDialog">取消</el-button>
+          <el-button type="primary" @click="confirmReject">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, defineExpose, defineEmits } from "vue";
+import { ElMessageBox } from "element-plus";
+import type { FormInstance, FormRules } from "element-plus";
+
+// 定义弹窗状态
+const dialogShow = ref(false);
+const rejectDialogVisible = ref(false);
+
+// 详情数据
+const detailData: any = ref({});
+// 处理推广板块字符串
+const getPromoteTypes = (promoteType: string) => {
+  if (!promoteType) return [];
+  return promoteType.split(",");
+};
+
+// 金额格式化
+const formatAmount = (amount: number | string) => {
+  const num = typeof amount === "string" ? parseFloat(amount) : amount;
+  return isNaN(num) ? "¥0.00" : `¥${num.toFixed(2)}`;
+};
+
+// 驳回表单
+const rejectForm = reactive({
+  reasonRefusal: ""
+});
+
+// 驳回表单验证规则
+const rejectRules: FormRules = {
+  reasonRefusal: [
+    { required: true, message: "请输入驳回原因", trigger: "blur" },
+    { min: 2, max: 200, message: "驳回原因长度为2-200个字符", trigger: "blur" }
+  ]
+};
+
+// 表单引用
+const rejectFormRef = ref<FormInstance>();
+
+// 事件触发
+const emit = defineEmits(["approve", "reject", "close", "refresh"]);
+
+// 显示弹窗方法
+const open = (data: any) => {
+  detailData.value = {
+    ...data
+  };
+  dialogShow.value = true;
+};
+
+// 关闭弹窗
+const handleClose = () => {
+  dialogShow.value = false;
+  emit("close");
+};
+
+// 直接确认同意发放尾款
+const confirmApprove = async () => {
+  try {
+    await ElMessageBox.confirm("确认同意发放尾款?", "确认同意", {
+      confirmButtonText: "确认",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+
+    // 触发同意事件
+    emit("approve", {
+      id: detailData.value.id,
+      name: detailData.value.name,
+      orderMoney: detailData.value.orderMoney,
+      orderNo: detailData.value.orderNo,
+      storeTel: detailData.value.storeTel,
+      userPhone: detailData.value.userPhone,
+      endPayment: detailData.value.endPayment
+    });
+
+    // 关闭弹窗并通知刷新
+    dialogShow.value = false;
+    emit("refresh");
+  } catch (error) {
+    // 用户取消,不执行操作
+  }
+};
+
+// 打开驳回原因弹窗
+const openRejectDialog = () => {
+  rejectForm.reasonRefusal = "";
+  rejectDialogVisible.value = true;
+};
+
+// 取消驳回
+const cancelRejectDialog = () => {
+  rejectDialogVisible.value = false;
+};
+
+// 确认驳回(二次确认)
+const confirmReject = async () => {
+  if (!rejectFormRef.value) return;
+
+  const valid = await rejectFormRef.value.validate();
+  if (!valid) return;
+
+  try {
+    await ElMessageBox.confirm("确认取消发放尾款?", "确认驳回", {
+      confirmButtonText: "确认",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+
+    // 触发驳回事件
+    emit("reject", {
+      id: detailData.value.id,
+      name: detailData.value.name,
+      orderMoney: detailData.value.orderMoney,
+      orderNo: detailData.value.orderNo,
+      storeTel: detailData.value.storeTel,
+      userPhone: detailData.value.userPhone,
+      endPayment: detailData.value.endPayment,
+      endPaymentRefusal: rejectForm.reasonRefusal
+    });
+
+    // 关闭所有弹窗并通知刷新
+    rejectDialogVisible.value = false;
+    dialogShow.value = false;
+    emit("refresh");
+  } catch (error) {
+    // 用户取消二次确认
+  }
+};
+
+// 暴露给父组件的方法
+defineExpose({
+  open,
+  close: handleClose
+});
+</script>
+
+<style lang="scss" scoped>
+.section-header {
+  padding-left: 5px;
+  margin: 10px 0 12px;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  border-left: 3px solid #409eff;
+}
+.metrics-sections {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 15px;
+
+  /* 在小屏幕上自动堆叠 */
+  @media (width <=600px) {
+    flex-direction: column;
+    gap: 10px;
+  }
+}
+.metrics-section {
+  flex: 1;
+  min-width: 0;
+  .section-header {
+    padding-left: 5px;
+    margin: 0 0 8px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    border-left: 3px solid #409eff;
+  }
+}
+.metrics-cards {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 8px;
+}
+.metric-card {
+  padding: 8px 10px;
+  background: #f8f9fa;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  transition: all 0.2s ease;
+  &:hover {
+    background: #f0f7ff;
+    border-color: #d6e4ff;
+  }
+}
+.metric-label {
+  margin-bottom: 4px;
+  font-size: 12px;
+  font-weight: 500;
+  color: #606266;
+}
+.metric-value {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+.review-container {
+  margin-bottom: 15px;
+
+  /* 滚动条样式优化 */
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+  }
+  &::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+  }
+}
+.review-detail {
+  .introduction {
+    line-height: 1.5;
+    word-break: break-word;
+    white-space: pre-wrap;
+  }
+}
+
+/* 隐藏预览时的操作栏 */
+:deep(.el-image-viewer__btn) {
+  display: none !important;
+}
+
+/* 或者更精确地隐藏操作栏区域 */
+:deep(.el-image-viewer__actions) {
+  display: none !important;
+}
+.image-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+  gap: 8px;
+}
+.image-item {
+  aspect-ratio: 1;
+  overflow: hidden;
+}
+.gallery-image {
+  width: 100%;
+  height: 100%;
+  cursor: pointer;
+  object-fit: cover;
+}
+
+/* 调整描述项的间距 */
+:deep(.el-descriptions__label) {
+  font-weight: 600;
+  background-color: #fafafa;
+}
+:deep(.el-descriptions__cell) {
+  padding: 10px 20px;
+}
+</style>