soft5566 пре 1 година
родитељ
комит
3144cbf708

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "axios": "1.7.7",
     "dayjs": "1.11.13",
     "element-plus": "2.8.8",
+    "file-saver": "2.0.5",
     "js-cookie": "3.0.5",
     "lodash-es": "4.17.21",
     "mitt": "3.0.1",

+ 8 - 0
pnpm-lock.yaml

@@ -20,6 +20,9 @@ importers:
       element-plus:
         specifier: 2.8.8
         version: 2.8.8(vue@3.5.13(typescript@5.7.2))
+      file-saver:
+        specifier: 2.0.5
+        version: 2.0.5
       js-cookie:
         specifier: 3.0.5
         version: 3.0.5
@@ -1172,6 +1175,9 @@ packages:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
 
+  file-saver@2.0.5:
+    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+
   fill-range@4.0.0:
     resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
     engines: {node: '>=0.10.0'}
@@ -3608,6 +3614,8 @@ snapshots:
     dependencies:
       flat-cache: 3.2.0
 
+  file-saver@2.0.5: {}
+
   fill-range@4.0.0:
     dependencies:
       extend-shallow: 2.0.1

+ 72 - 0
src/api/alumniOrganization/audit-list.js

@@ -0,0 +1,72 @@
+import { request } from "@/utils/service"
+
+/** 审核 */
+export function toExamineClubApi(params) {
+  return request({
+    url: "alumniClubApply/toExamineClub",
+    method: "get",
+    params
+  })
+}
+
+/** 查 表格数据 */
+export function getTableDataApi(params) {
+  return request({
+    url: "alumniClubApply/queryClubProcessPage",
+    method: "get",
+    params
+  })
+}
+
+/** 导出 表格数据 */
+export function queryClubProcessExcelApi(data) {
+  return request({
+    url: "alumniClubApply/queryClubProcessExcel",
+    method: "post",
+    data,
+    responseType: "blob"
+  })
+}
+
+/** 查 审核状态 */
+export function getQueryPassTypesApi() {
+  return request({
+    url: "alumniClub/queryPassTypes",
+    method: "get"
+  })
+}
+
+/** 查 学院 */
+export function getQueryCollegesApi() {
+  return request({
+    url: "alumniOrg/queryColleges",
+    method: "get"
+  })
+}
+
+/** 查 学段 */
+export function getQueryPeriodsApi(params) {
+  return request({
+    url: "alumniOrg/queryPeriods",
+    method: "get",
+    params
+  })
+}
+
+/** 查 专业 */
+export function getQueryMajorsApi(params) {
+  return request({
+    url: "alumniOrg/queryMajors",
+    method: "get",
+    params
+  })
+}
+
+/** 查 班级 */
+export function getQueryClassesApi(params) {
+  return request({
+    url: "alumniOrg/queryClasses",
+    method: "get",
+    params
+  })
+}

+ 43 - 0
src/api/alumniOrganization/classification-management.js

@@ -0,0 +1,43 @@
+import { request } from "@/utils/service"
+
+/**
+ * 删 组织分类
+ */
+export function deleteCategoryApi(params) {
+  return request({
+    url: "alumniCategory/deleteCategory",
+    method: "get",
+    params
+  })
+}
+
+/**
+ * 增 组织分类
+ */
+export function insertCategoryApi(data) {
+  return request({
+    url: "alumniCategory/insertCategory",
+    method: "post",
+    data
+  })
+}
+
+/**
+ * 改 组织分类
+ */
+export function updateCategoryApi(data) {
+  return request({
+    url: "alumniCategory/updateCategory",
+    method: "post",
+    data
+  })
+}
+
+/** 查 表格数据 */
+export function getTableDataApi(params) {
+  return request({
+    url: "alumniCategory/queryCategoryPage",
+    method: "get",
+    params
+  })
+}

+ 41 - 13
src/api/alumniOrganization/index.js

@@ -1,33 +1,44 @@
 import { request } from "@/utils/service"
 
-/**  */
-export function createTableDataApi(data) {
+/** 查 导出组织 */
+export function getAlumniClubExcelApi(data) {
   return request({
-    url: "table",
+    url: "alumniClub/getAlumniClubExcel",
     method: "post",
-    data
+    data,
+    responseType: "blob"
+  })
+}
+
+/** 删 删除组织 */
+export function deleteClubDataApi(params) {
+  return request({
+    url: "alumniClub/deleteClubById",
+    method: "get",
+    params
   })
 }
 
-/** 删 */
-export function deleteTableDataApi(id) {
+/** 改 置顶组织 */
+export function clubTopupApi(data) {
   return request({
-    url: `table/${id}`,
-    method: "delete"
+    url: "alumniClub/clubTopup",
+    method: "post",
+    data
   })
 }
 
-/** 改 */
-export function updateTableDataApi(data) {
+/** 改 更新组织 */
+export function updateClubDataApi(data) {
   return request({
-    url: "table",
-    method: "put",
+    url: "alumniClub/updateClubData",
+    method: "post",
     data
   })
 }
 
 /**
- * 创建组织
+ * 创建组织
  */
 export function insertClubDataApi(data) {
   return request({
@@ -54,3 +65,20 @@ export function getQueryCategoryPageApi(params) {
     params
   })
 }
+
+/** 查 管理员部门树 */
+export function getQueryOrgTreeApi() {
+  return request({
+    url: "alumniOrg/queryOrgTree",
+    method: "get"
+  })
+}
+
+/** 查 用户列表 */
+export function getQueryPageUserApi(params) {
+  return request({
+    url: "alumniUser/queryPageUser",
+    method: "get",
+    params
+  })
+}

+ 6 - 6
src/styles/element-plus.scss

@@ -92,7 +92,7 @@
   border-radius: 10px;
   overflow: hidden;
   .el-dialog__headerbtn {
-    height: 61.5px;
+    height: 60px;
     font-size: 18px;
     .el-dialog__close {
       color: #808080;
@@ -100,17 +100,17 @@
   }
   .el-dialog__header {
     background-color: #edf1f5;
-    padding-left: 35px;
-    padding-top: 21px;
+    padding-left: 20px;
+    padding-top: 20px;
     font-weight: 500;
     .el-dialog__title {
-      font-size: 20px;
+      font-size: 18px;
     }
   }
   .el-dialog__body {
-    padding: 40px 60px;
+    padding: 20px;
   }
   .el-dialog__footer {
-    padding: 0 60px 40px 60px;
+    padding: 0 20px 30px 20px;
   }
 }

+ 5 - 5
src/views/alumni-management/index.vue

@@ -99,7 +99,7 @@ const getQueryMajors = () => {
     })
 }
 /**
- * 获取专业列表
+ * 获取班级列表
  */
 const getQueryClasses = () => {
   if (searchData.major === undefined) {
@@ -150,10 +150,10 @@ const getQueryUsersPage = () => {
 const handleSearch = () => {
   paginationData.currentPage === 1 ? getQueryUsersPage() : (paginationData.currentPage = 1)
 }
-const resetSearch = () => {
-  searchFormRef.value?.resetFields()
-  handleSearch()
-}
+// const resetSearch = () => {
+//   searchFormRef.value?.resetFields()
+//   handleSearch()
+// }
 //#endregion
 
 /** 监听分页参数的变化 */

+ 271 - 62
src/views/alumni-organization/audit-list.vue

@@ -1,6 +1,15 @@
 <script setup>
-import { reactive, ref, watch } from "vue"
-import { getTableDataApi } from "@/api/table"
+import { reactive, ref, watch, onBeforeMount } from "vue"
+import {
+  getTableDataApi,
+  getQueryCollegesApi,
+  getQueryPeriodsApi,
+  getQueryMajorsApi,
+  getQueryClassesApi,
+  getQueryPassTypesApi,
+  toExamineClubApi,
+  queryClubProcessExcelApi
+} from "@/api/alumniOrganization/audit-list"
 import { ElMessageBox } from "element-plus"
 import { usePagination } from "@/hooks/usePagination"
 
@@ -11,32 +20,150 @@ const { paginationData, handleCurrentChange, handleSizeChange } = usePagination(
 const tableData = ref([])
 const searchFormRef = ref(null)
 const searchData = reactive({
-  organizationName: undefined,
-  applicant: undefined,
+  userName: undefined,
+  createTime: null,
   status: undefined,
+  statusOptions: undefined,
+  name: undefined,
   college: undefined,
-  stage: undefined,
+  collegeOptions: undefined,
+  period: undefined,
+  periodOptions: undefined,
   major: undefined,
+  majorOptions: undefined,
   class: undefined,
-  createTime: null
+  classOptions: undefined
 })
+/**
+ * 获取 审核状态列表
+ */
+const getQueryPassTypes = () => {
+  loading.value = true
+  getQueryPassTypesApi()
+    .then(({ data }) => {
+      searchData.statusOptions = data
+    })
+    .catch(() => {
+      searchData.statusOptions = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取 学院列表
+ */
+const getQueryColleges = () => {
+  loading.value = true
+  getQueryCollegesApi()
+    .then(({ data }) => {
+      searchData.collegeOptions = data
+    })
+    .catch(() => {
+      searchData.collegeOptions = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取 学段列表
+ */
+const getQueryPeriods = () => {
+  if (searchData.college === undefined) {
+    searchData.periodOptions = []
+    searchData.period = ""
+
+    searchData.majorOptions = []
+    searchData.major = ""
+
+    searchData.classOptions = []
+    searchData.class = ""
+    return
+  }
+  loading.value = true
+  getQueryPeriodsApi({
+    collegeId: searchData.college
+  })
+    .then(({ data }) => {
+      searchData.periodOptions = data
+    })
+    .catch(() => {
+      searchData.periodOptions = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取 专业列表
+ */
+const getQueryMajors = () => {
+  if (searchData.period === undefined) {
+    searchData.majorOptions = []
+    searchData.major = ""
+
+    searchData.classOptions = []
+    searchData.class = ""
+    return
+  }
+  loading.value = true
+  getQueryMajorsApi({
+    periodId: searchData.period
+  })
+    .then(({ data }) => {
+      searchData.majorOptions = data
+    })
+    .catch(() => {
+      searchData.majorOptions = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取 班级列表
+ */
+const getQueryClasses = () => {
+  if (searchData.major === undefined) {
+    searchData.classOptions = []
+    searchData.class = ""
+    return
+  }
+  loading.value = true
+  getQueryClassesApi({
+    majorId: searchData.major
+  })
+    .then(({ data }) => {
+      searchData.classOptions = data
+    })
+    .catch(() => {
+      searchData.classOptions = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取 表格数据
+ */
 const getTableData = () => {
   loading.value = true
   getTableDataApi({
     currentPage: paginationData.currentPage,
-    size: paginationData.pageSize,
-    organizationName: searchData.organizationName,
-    applicant: searchData.applicant,
-    status: searchData.applicant,
+    pageCount: paginationData.pageSize,
+    name: searchData.name,
+    userName: searchData.userName,
+    isPass: searchData.status,
     college: searchData.college,
-    stage: searchData.stage,
+    period: searchData.period,
     major: searchData.major,
     class: searchData.class,
-    starTime: searchData.createTime ? searchData.createTime[0] : undefined,
+    startTime: searchData.createTime ? searchData.createTime[0] : undefined,
     endTime: searchData.createTime ? searchData.createTime[1] : undefined
   })
     .then(({ data }) => {
-      paginationData.total = data.total
+      paginationData.total = data.totalCount
       tableData.value = data.list
     })
     .catch(() => {
@@ -46,47 +173,113 @@ const getTableData = () => {
       loading.value = false
     })
 }
+/**
+ * 导出 表格数据
+ */
+const handleDownload = async () => {
+  loading.value = true
+  const res = await queryClubProcessExcelApi({
+    name: searchData.name,
+    userName: searchData.userName,
+    isPass: searchData.status,
+    college: searchData.college,
+    period: searchData.period,
+    major: searchData.major,
+    class: searchData.class,
+    startTime: searchData.createTime ? searchData.createTime[0] : undefined,
+    endTime: searchData.createTime ? searchData.createTime[1] : undefined
+  })
+  // console.log(res)
+  // 请求成功返回后,获取到Excel文件的二进制数据
+  const blob = new Blob([res], { type: "application/vnd.ms-excel" })
+  // 创建下载链接
+  const downloadUrl = URL.createObjectURL(blob)
+  // 创建一个隐藏的a标签,设置下载链接和文件名,模拟点击下载
+  const link = document.createElement("a")
+  link.style.display = "none"
+  link.href = downloadUrl
+  link.download = `审核列表数据_下载时间_${getCurrentDateTime()}.xlsx`
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
+  loading.value = false
+}
+/**
+ * 获取日期时间
+ */
+const getCurrentDateTime = () => {
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = String(now.getMonth() + 1).padStart(2, "0") // 月份从0开始,需要加1
+  const day = String(now.getDate()).padStart(2, "0")
+  const hours = String(now.getHours()).padStart(2, "0")
+  const minutes = String(now.getMinutes()).padStart(2, "0")
+  const seconds = String(now.getSeconds()).padStart(2, "0")
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
 const handleSearch = () => {
   paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
 }
-const resetSearch = () => {
-  searchFormRef.value?.resetFields()
-  handleSearch()
-}
+// const resetSearch = () => {
+//   searchFormRef.value?.resetFields()
+//   handleSearch()
+// }
 //#endregion
 
 /** 监听分页参数的变化 */
 watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
 
-/** 导出 */
-const handleDownload = () => {
-  // todo 调用导出接口
-  console.log("导出")
-}
-
 /** 通过 */
 const handlePass = (row) => {
   ElMessageBox.confirm("确认通过?", "提示", {
     confirmButtonText: "确定",
     cancelButtonText: "取消",
-    type: "warning"
+    type: "warning",
+    closeOnPressEscape: false,
+    closeOnClickModal: false
   }).then(() => {
-    // todo 调用通过接口
-    console.log(row)
+    // 调用通过接口
+    toExamineClubApi({
+      id: row.id,
+      isPass: 2
+    })
+      .then(() => {
+        ElMessage.success("已审核通过")
+        getTableData()
+      })
+      .catch(() => {})
   })
 }
 
 /** 拒绝 */
 const handleReject = (row) => {
-  ElMessageBox.confirm("确认拒绝?", "提示", {
+  ElMessageBox.prompt("确认拒绝?", "提示", {
     confirmButtonText: "确定",
     cancelButtonText: "取消",
-    type: "warning"
-  }).then(() => {
-    // todo 调用拒绝接口
-    console.log(row)
+    inputPattern: /[\u4e00-\u9fa5a-zA-Z0-9_-]{2,100}/,
+    inputErrorMessage: "请输入拒绝理由",
+    closeOnPressEscape: false,
+    closeOnClickModal: false
+  }).then((content) => {
+    // 调用拒绝接口
+    toExamineClubApi({
+      id: row.id,
+      isPass: 3,
+      passValue: content.value
+    })
+      .then(() => {
+        ElMessage.success("已拒绝")
+        getTableData()
+      })
+      .catch(() => {})
   })
 }
+
+onBeforeMount(() => {
+  getQueryPassTypes()
+  getQueryColleges()
+})
 </script>
 
 <template>
@@ -94,35 +287,53 @@ const handleReject = (row) => {
     <el-card v-loading="loading" header="审核列表">
       <div class="toolbar-wrapper">
         <el-form ref="searchFormRef" :inline="true" :model="searchData">
-          <el-form-item prop="organizationName" label="组织名称">
-            <el-input v-model="searchData.organizationName" placeholder="请输入" />
+          <el-form-item prop="name" label="组织名称">
+            <el-input v-model="searchData.name" clearable placeholder="请输入" style="width: 150px" />
           </el-form-item>
-          <el-form-item prop="applicant" label="申请人">
-            <el-input v-model="searchData.applicant" placeholder="请输入" />
+          <el-form-item prop="userName" label="申请人">
+            <el-input v-model="searchData.userName" clearable placeholder="请输入" style="width: 150px" />
           </el-form-item>
           <el-form-item prop="status" label="审核状态">
-            <el-select v-model="searchData.status" placeholder="请选择" style="width: 178px">
-              <el-option label="todo" value="todo" />
+            <el-select v-model="searchData.status" placeholder="请选择" style="width: 150px">
+              <el-option v-for="item in searchData.statusOptions" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </el-form-item>
           <el-form-item prop="college" label="学院">
-            <el-select v-model="searchData.college" placeholder="请选择" style="width: 178px">
-              <el-option label="todo" value="todo" />
+            <el-select
+              v-model="searchData.college"
+              placeholder="请选择"
+              @change="getQueryPeriods"
+              clearable
+              style="width: 178px"
+            >
+              <el-option v-for="item in searchData.collegeOptions" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </el-form-item>
-          <el-form-item prop="stage" label="学段">
-            <el-select v-model="searchData.stage" placeholder="请选择" style="width: 178px">
-              <el-option label="todo" value="todo" />
+          <el-form-item prop="period" label="学段">
+            <el-select
+              v-model="searchData.period"
+              placeholder="请选择"
+              @change="getQueryMajors"
+              clearable
+              style="width: 178px"
+            >
+              <el-option v-for="item in searchData.periodOptions" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </el-form-item>
           <el-form-item prop="major" label="专业">
-            <el-select v-model="searchData.major" placeholder="请选择" style="width: 178px">
-              <el-option label="todo" value="todo" />
+            <el-select
+              v-model="searchData.major"
+              placeholder="请选择"
+              @change="getQueryClasses"
+              clearable
+              style="width: 178px"
+            >
+              <el-option v-for="item in searchData.majorOptions" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </el-form-item>
           <el-form-item prop="class" label="班级">
-            <el-select v-model="searchData.class" placeholder="请选择" style="width: 178px">
-              <el-option label="todo" value="todo" />
+            <el-select v-model="searchData.class" placeholder="请选择" clearable style="width: 178px">
+              <el-option v-for="item in searchData.classOptions" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </el-form-item>
           <el-form-item prop="createTime" label="创建时间">
@@ -136,26 +347,24 @@ const handleReject = (row) => {
           </el-form-item>
           <el-form-item>
             <el-button type="primary" @click="handleSearch">查询</el-button>
-            <el-button @click="resetSearch" plain>重置</el-button>
+            <!-- <el-button @click="resetSearch" plain>重置</el-button> -->
+            <el-button plain @click="handleDownload">导出</el-button>
           </el-form-item>
         </el-form>
-        <div>
-          <el-button plain @click="handleDownload">导出</el-button>
-        </div>
       </div>
       <div class="table-wrapper">
-        <el-table :data="tableData" max-height="500">
-          <el-table-column type="index" label="序号" width="100" align="center" />
-          <el-table-column prop="applicant" label="申请人" align="center" />
-          <el-table-column prop="college" label="学院" align="center" />
-          <el-table-column prop="stage" label="学段" align="center" />
-          <el-table-column prop="major" label="专业" align="center" />
-          <el-table-column prop="class" label="班级" align="center" />
-          <el-table-column prop="organizationName" label="申请组织" align="center" />
-          <el-table-column prop="todo" label="审核人" align="center" />
-          <el-table-column prop="status" label="审核状态" align="center" />
-          <el-table-column prop="todo" label="审核时间" align="center" />
-          <el-table-column prop="createTime" label="创建时间" align="center" />
+        <el-table :data="tableData" max-height="450" height="450">
+          <el-table-column type="index" label="序号" width="80" align="center" />
+          <el-table-column prop="applyName" label="申请人" align="center" />
+          <el-table-column prop="collegeName" label="学院" align="center" />
+          <el-table-column prop="periodName" label="学段" align="center" />
+          <el-table-column prop="majorName" label="专业" align="center" />
+          <el-table-column prop="className" label="班级" align="center" />
+          <el-table-column prop="name" label="申请组织" align="center" />
+          <el-table-column prop="applyUserName" label="审核人" align="center" />
+          <el-table-column prop="passName" label="审核状态" align="center" />
+          <el-table-column prop="passTime" label="审核时间" align="center" width="160" />
+          <el-table-column prop="createTime" label="创建时间" align="center" width="160" />
           <el-table-column fixed="right" label="操作" width="200" align="center">
             <template #default="scope">
               <el-link type="danger" @click="handleReject(scope.row)">拒绝</el-link>

+ 34 - 18
src/views/alumni-organization/classification-management.vue

@@ -1,9 +1,15 @@
 <script setup>
 import { reactive, ref, watch } from "vue"
-import { createTableDataApi, deleteTableDataApi, updateTableDataApi, getTableDataApi } from "@/api/table"
+import {
+  getTableDataApi,
+  insertCategoryApi,
+  updateCategoryApi,
+  deleteCategoryApi
+} from "@/api/alumniOrganization/classification-management"
 import { ElMessage, ElMessageBox } from "element-plus"
 import { usePagination } from "@/hooks/usePagination"
 import { cloneDeep } from "lodash-es"
+// import { id } from "element-plus/es/locales.mjs"
 
 const loading = ref(false)
 const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
@@ -23,7 +29,7 @@ const handleCreateOrUpdate = () => {
   formRef.value?.validate((valid, fields) => {
     if (!valid) return console.error("表单校验不通过", fields)
     loading.value = true
-    const api = formData.value.id === undefined ? createTableDataApi : updateTableDataApi
+    const api = formData.value.id === undefined ? insertCategoryApi : updateCategoryApi
     api(formData.value)
       .then(() => {
         ElMessage.success("操作成功")
@@ -46,9 +52,13 @@ const handleDelete = (row) => {
   ElMessageBox.confirm("确认删除?", "提示", {
     confirmButtonText: "确定",
     cancelButtonText: "取消",
+    closeOnClickModal: false,
+    closeOnPressEscape: false,
     type: "warning"
   }).then(() => {
-    deleteTableDataApi(row.id).then(() => {
+    deleteCategoryApi({
+      id: row.id
+    }).then(() => {
       ElMessage.success("删除成功")
       getTableData()
     })
@@ -75,14 +85,14 @@ const getTableData = () => {
   loading.value = true
   getTableDataApi({
     currentPage: paginationData.currentPage,
-    size: paginationData.pageSize,
+    pageCount: paginationData.pageSize,
     name: searchData.name,
-    creator: searchData.creator,
-    starTime: searchData.createTime ? searchData.createTime[0] : undefined,
+    userId: searchData.creator,
+    startTime: searchData.createTime ? searchData.createTime[0] : undefined,
     endTime: searchData.createTime ? searchData.createTime[1] : undefined
   })
     .then(({ data }) => {
-      paginationData.total = data.total
+      paginationData.total = data.totalCount
       tableData.value = data.list
     })
     .catch(() => {
@@ -95,10 +105,10 @@ const getTableData = () => {
 const handleSearch = () => {
   paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
 }
-const resetSearch = () => {
-  searchFormRef.value?.resetFields()
-  handleSearch()
-}
+// const resetSearch = () => {
+//   searchFormRef.value?.resetFields()
+//   handleSearch()
+// }
 //#endregion
 
 /** 监听分页参数的变化 */
@@ -111,10 +121,10 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
       <div class="toolbar-wrapper">
         <el-form ref="searchFormRef" :inline="true" :model="searchData">
           <el-form-item prop="name" label="分类名称">
-            <el-input v-model="searchData.name" placeholder="请输入" />
+            <el-input v-model="searchData.name" placeholder="请输入" clearable style="width: 150px" />
           </el-form-item>
           <el-form-item prop="creator" label="创建人">
-            <el-input v-model="searchData.creator" placeholder="请输入" />
+            <el-input v-model="searchData.creator" placeholder="请输入" clearable style="width: 150px" />
           </el-form-item>
           <el-form-item prop="createTime" label="创建时间">
             <el-date-picker
@@ -127,7 +137,7 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
           </el-form-item>
           <el-form-item>
             <el-button type="primary" @click="handleSearch">查询</el-button>
-            <el-button @click="resetSearch" plain>重置</el-button>
+            <!-- <el-button @click="resetSearch" plain>重置</el-button> -->
           </el-form-item>
         </el-form>
         <div>
@@ -135,11 +145,11 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
         </div>
       </div>
       <div class="table-wrapper">
-        <el-table :data="tableData" max-height="500">
+        <el-table :data="tableData" max-height="450" height="450">
           <el-table-column type="index" label="序号" width="100" align="center" />
           <el-table-column prop="name" label="分类名称" align="center" />
           <el-table-column prop="createTime" label="创建时间" align="center" />
-          <el-table-column prop="creator" label="创建人" align="center" />
+          <el-table-column prop="createUserName" label="创建人" align="center" />
           <el-table-column fixed="right" label="操作" width="200" align="center">
             <template #default="scope">
               <el-link type="primary" @click="handleUpdate(scope.row)">编辑</el-link>
@@ -166,7 +176,10 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
       v-model="dialogVisible"
       :title="formData.id === undefined ? '创建分类' : '编辑分类'"
       @closed="resetForm"
-      width="50%"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      width="500"
+      align-center
     >
       <el-form ref="formRef" :model="formData" :rules="formRules" label-width="auto" size="large">
         <el-form-item prop="name" label="分类名称">
@@ -175,7 +188,10 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleCreateOrUpdate" :loading="loading">立即创建</el-button>
+        <el-button v-if="formData.id === undefined" type="primary" @click="handleCreateOrUpdate" :loading="loading"
+          >立即创建</el-button
+        >
+        <el-button v-else type="primary" @click="handleCreateOrUpdate" :loading="loading">立即编辑</el-button>
       </template>
     </el-dialog>
   </div>

+ 322 - 51
src/views/alumni-organization/index.vue

@@ -1,15 +1,19 @@
 <script setup>
-import { reactive, ref, watch, onMounted } from "vue"
+import { reactive, ref, watch, onBeforeMount } from "vue"
 import {
-  createTableDataApi,
-  deleteTableDataApi,
-  updateTableDataApi,
+  clubTopupApi,
+  insertClubDataApi,
+  deleteClubDataApi,
+  updateClubDataApi,
   getQueryCategoryPageApi,
+  getQueryOrgTreeApi,
   getTableDataApi,
-  insertClubDataApi
+  getQueryPageUserApi,
+  getAlumniClubExcelApi
 } from "@/api/alumniOrganization"
 import { ElMessage, ElMessageBox } from "element-plus"
 import { usePagination } from "@/hooks/usePagination"
+import { Search } from "@element-plus/icons-vue"
 import { cloneDeep } from "lodash-es"
 
 const loading = ref(false)
@@ -18,23 +22,81 @@ const { paginationData, handleCurrentChange, handleSizeChange } = usePagination(
 //#region 增
 const DEFAULT_FORM_DATA = {
   id: undefined,
-  name: "",
-  classification: undefined,
-  administrator: [],
-  introduction: "",
-  about: ""
+  name: undefined,
+  categoryId: undefined,
+  categoryName: undefined,
+  number: 6,
+  description: undefined,
+  contact: undefined,
+  contacts: undefined,
+  phone: undefined,
+  email: undefined,
+  address: undefined,
+  categoryOption: undefined,
+  admin: undefined,
+  admins: undefined
 }
+const isEdit = ref(false)
+const orgTreeData = ref([])
+const userData = ref([])
+const checkUserList = ref([])
+const selectAdminDialogFormVisible = ref(false)
 const dialogVisible = ref(false)
 const formRef = ref(null)
 const formData = ref(cloneDeep(DEFAULT_FORM_DATA))
 const formRules = {
-  name: [{ required: true, trigger: "blur", message: "必填" }]
+  name: [{ required: true, trigger: "blur", message: "请输入组织名称" }],
+  categoryId: [{ required: true, trigger: "blur", message: "请选择组织分类" }],
+  number: [{ required: true, trigger: "blur", message: "请设置人数" }],
+  admin: [{ required: true, trigger: "blur", message: "请选择管理员" }],
+  admins: [{ required: true, trigger: "blur", message: "请选择管理员" }],
+  description: [{ required: true, trigger: "blur", message: "请输入描述" }],
+  contact: [{ required: true, trigger: "blur", message: "请输入联系人" }],
+  phone: [
+    { required: true, trigger: "blur", message: "请输入联系方式" },
+    {
+      pattern: /^(?:\+86)?1[3-9]\d{9}|(?:\+86)?\d{3,4}-?\d{7,8}$/,
+      trigger: "blur",
+      message: "请输入有效的手机号或座机号码"
+    }
+  ],
+  email: [
+    { required: false, trigger: "blur" }, // 不强制要求填写
+    {
+      pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
+      trigger: "blur",
+      message: "请输入有效的邮箱地址"
+    }
+  ],
+  address: [
+    { required: false, trigger: "blur" }, // 不是必填项
+    {
+      pattern: /^[\u4e00-\u9fa5a-zA-Z0-9\s,-.]+$/, // 基本地址模式
+      trigger: "blur",
+      message: "请输入有效的地址" // 无效地址时的提示信息
+    }
+  ]
+}
+/**
+ * 转化数据
+ */
+const transformData = (data) => {
+  return data.map((item) => ({
+    label: item.name,
+    id: item.id,
+    parentId: item.parentId ? item.parentId : null,
+    cardNumber: item.cardNumber ? item.cardNumber : null,
+    children: item.children ? transformData(item.children) : null
+  }))
 }
+/**
+ * 创建 或 更新 组织
+ */
 const handleCreateOrUpdate = () => {
   formRef.value?.validate((valid, fields) => {
     if (!valid) return console.error("表单校验不通过", fields)
     loading.value = true
-    const api = formData.value.id === undefined ? createTableDataApi : updateTableDataApi
+    const api = formData.value.id === undefined ? insertClubDataApi : updateClubDataApi
     api(formData.value)
       .then(() => {
         ElMessage.success("操作成功")
@@ -46,9 +108,32 @@ const handleCreateOrUpdate = () => {
       })
   })
 }
+const handleCategoryChange = (value) => {
+  const selected = searchData.categoryOption.find((item) => item.id === value)
+  if (selected) {
+    formData.value.categoryId = selected.id
+    formData.value.categoryName = selected.name
+  } else {
+    formData.value.categoryId = undefined
+    formData.value.categoryName = undefined
+  }
+}
+/**
+ * 创建组织弹窗
+ */
+const handleCreateDialog = () => {
+  dialogVisible.value = true
+  isEdit.value = false
+}
+/**
+ * 重置表单
+ */
 const resetForm = () => {
   formRef.value?.clearValidate()
   formData.value = cloneDeep(DEFAULT_FORM_DATA)
+  checkUserList.value = undefined
+  formData.value.admin = undefined
+  userData.value = []
 }
 //#endregion
 
@@ -59,7 +144,9 @@ const handleDelete = (row) => {
     cancelButtonText: "取消",
     type: "warning"
   }).then(() => {
-    deleteTableDataApi(row.id).then(() => {
+    deleteClubDataApi({
+      id: row.id
+    }).then(() => {
       ElMessage.success("删除成功")
       getTableData()
     })
@@ -71,10 +158,27 @@ const handleDelete = (row) => {
 const handleUpdate = (row) => {
   dialogVisible.value = true
   formData.value = cloneDeep(row)
+  isEdit.value = true
+  formData.value.admins = row.admins
+  formData.value.admin = row.admins.map((item) => item.id)
+  userData.value = transformData(row.admins)
+  // 默认勾选
+  checkUserList.value = row.admins.map((item) => item.id)
 }
 const handleTop = (row) => {
-  // todo 调用置顶接口
-  console.log(row)
+  loading.value = true
+  clubTopupApi({
+    id: row.id,
+    isTop: 1
+  })
+    .then(() => {
+      ElMessage.success("置顶成功")
+      getTableData()
+    })
+    .catch(() => ({}))
+    .finally(() => {
+      loading.value = false
+    })
 }
 //#endregion
 
@@ -85,8 +189,8 @@ const searchData = reactive({
   name: undefined,
   creator: undefined,
   createTime: null,
-  classification: undefined,
-  classificationOptions: undefined
+  categoryId: undefined,
+  categoryOption: undefined
 })
 /**
  * 获取表格数据
@@ -100,7 +204,7 @@ const getTableData = () => {
     userName: searchData.creator,
     startTime: searchData.createTime ? searchData.createTime[0] : undefined,
     endTime: searchData.createTime ? searchData.createTime[1] : undefined,
-    categoryId: searchData.classification
+    categoryId: searchData.categoryId
   })
     .then(({ data }) => {
       paginationData.total = data.totalCount
@@ -119,14 +223,30 @@ const getTableData = () => {
 const getQueryCategoryPage = () => {
   loading.value = true
   getQueryCategoryPageApi({
-    currentPage: paginationData.currentPage,
-    pageCount: paginationData.pageSize
+    currentPage: 1,
+    pageCount: 1000
   })
     .then(({ data }) => {
-      searchData.classificationOptions = data.list
+      searchData.categoryOption = data.list
     })
     .catch(() => {
-      searchData.classificationOptions = []
+      searchData.categoryOption = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取部门树
+ */
+const getQueryOrgTree = () => {
+  loading.value = true
+  getQueryOrgTreeApi()
+    .then(({ data }) => {
+      orgTreeData.value = transformData(data)
+    })
+    .catch(() => {
+      orgTreeData.value = []
     })
     .finally(() => {
       loading.value = false
@@ -135,23 +255,93 @@ const getQueryCategoryPage = () => {
 const handleSearch = () => {
   paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
 }
-const resetSearch = () => {
-  searchFormRef.value?.resetFields()
-  handleSearch()
-}
+// const resetSearch = () => {
+//   searchFormRef.value?.resetFields()
+//   handleSearch()
+// }
 //#endregion
 
 /** 监听分页参数的变化 */
 watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
 
 /** 导出 */
-const handleDownload = () => {
-  // todo 调用导出接口
-  console.log("导出")
+const handleDownload = async () => {
+  loading.value = true
+  const res = await getAlumniClubExcelApi({
+    name: searchData.name,
+    userName: searchData.creator,
+    startTime: searchData.createTime ? searchData.createTime[0] : undefined,
+    endTime: searchData.createTime ? searchData.createTime[1] : undefined,
+    categoryId: searchData.categoryId
+  })
+  // console.log(res)
+  // 请求成功返回后,获取到Excel文件的二进制数据
+  const blob = new Blob([res], { type: "application/vnd.ms-excel" })
+  // 创建下载链接
+  const downloadUrl = URL.createObjectURL(blob)
+  // 创建一个隐藏的a标签,设置下载链接和文件名,模拟点击下载
+  const link = document.createElement("a")
+  link.style.display = "none"
+  link.href = downloadUrl
+  link.download = `校友组织数据_下载时间_${getCurrentDateTime()}.xlsx`
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
+  loading.value = false
+}
+/**
+ * 获取日期时间
+ */
+const getCurrentDateTime = () => {
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = String(now.getMonth() + 1).padStart(2, "0") // 月份从0开始,需要加1
+  const day = String(now.getDate()).padStart(2, "0")
+  const hours = String(now.getHours()).padStart(2, "0")
+  const minutes = String(now.getMinutes()).padStart(2, "0")
+  const seconds = String(now.getSeconds()).padStart(2, "0")
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+/**
+ * 选择管理员
+ */
+const handleNodeClick = (e) => {
+  loading.value = true
+  // 获取用户列表
+  getQueryPageUserApi({
+    currentPage: 1,
+    pageCount: 1000,
+    departmentId: e.id
+  })
+    .then(({ data }) => {
+      formData.value.admins = data.list.map((item) => ({
+        id: item.id,
+        name: item.name,
+        cardNumber: item.cardNumber
+      }))
+      userData.value = transformData(data.list)
+    })
+    .catch(() => {
+      userData.value = []
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+/**
+ * 获取用户列表
+ */
+const handleCheckUser = () => {
+  if (isEdit.value) {
+    formData.value.admin = checkUserList.value
+  } else {
+    formData.value.admin = checkUserList.value
+  }
 }
 
-onMounted(() => {
+onBeforeMount(() => {
   getQueryCategoryPage()
+  getQueryOrgTree()
 })
 </script>
 
@@ -175,14 +365,9 @@ onMounted(() => {
               value-format="x"
             />
           </el-form-item>
-          <el-form-item prop="classification" label="分类">
-            <el-select v-model="searchData.classification" placeholder="选择分类" clearable style="width: 178px">
-              <el-option
-                v-for="item in searchData.classificationOptions"
-                :key="item.id"
-                :label="item.name"
-                :value="item.id"
-              />
+          <el-form-item prop="categoryId" label="分类">
+            <el-select v-model="searchData.categoryId" placeholder="选择分类" clearable style="width: 178px">
+              <el-option v-for="item in searchData.categoryOption" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </el-form-item>
           <el-form-item>
@@ -191,7 +376,7 @@ onMounted(() => {
           </el-form-item>
         </el-form>
         <div>
-          <el-button type="primary" @click="dialogVisible = true">创建组织</el-button>
+          <el-button type="primary" @click="handleCreateDialog">创建组织</el-button>
           <el-button plain @click="handleDownload">导出</el-button>
         </div>
       </div>
@@ -235,32 +420,93 @@ onMounted(() => {
       v-model="dialogVisible"
       :title="formData.id === undefined ? '创建组织' : '编辑组织'"
       @closed="resetForm"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
       width="50%"
+      align-center
     >
       <el-form ref="formRef" :model="formData" :rules="formRules" label-width="auto" size="large">
         <el-form-item prop="name" label="组织名称">
           <el-input v-model="formData.name" placeholder="请输入组织名称" />
         </el-form-item>
-        <el-form-item prop="classification" label="组织分类">
-          <el-select v-model="formData.classification" placeholder="请选择分类">
-            <el-option label="todo" value="todo" />
+        <el-form-item prop="categoryId" label="组织分类">
+          <el-select v-model="formData.categoryId" clearable placeholder="请选择分类" @change="handleCategoryChange">
+            <el-option v-for="item in searchData.categoryOption" :key="item.id" :label="item.name" :value="item.id" />
           </el-select>
         </el-form-item>
-        <el-form-item prop="administrator" label="管理员">
-          <el-select v-model="formData.administrator" placeholder="请选择管理员" multiple>
-            <el-option label="todo" value="todo" />
-          </el-select>
+        <el-form-item prop="admin" label="管理员">
+          <div style="display: flex; align-items: flex-start; width: 100%">
+            <el-button
+              type="primary"
+              @click="selectAdminDialogFormVisible = true"
+              style="margin-right: 8px"
+              :icon="Search"
+            />
+            <el-select v-model="formData.admin" placeholder="请选择管理员" disabled multiple>
+              <el-option v-for="item in formData.admins" :key="item.id" :label="item.name" :value="item.id" />
+            </el-select>
+          </div>
+        </el-form-item>
+        <el-form-item prop="number" label="人数">
+          <el-input-number v-model="formData.number" type="number" :min="1" :max="1000" />
         </el-form-item>
-        <el-form-item prop="introduction" label="本会简介">
-          <el-input v-model="formData.introduction" type="textarea" :rows="5" placeholder="请输入" />
+        <el-form-item prop="description" label="本会简介">
+          <el-input v-model="formData.description" type="textarea" :rows="5" placeholder="请输入简介" />
         </el-form-item>
-        <el-form-item prop="about" label="联系我们">
-          <el-input v-model="formData.about" type="textarea" :rows="5" placeholder="请输入" />
+        <!-- <el-form-item prop="contact" label="联系我们" /> -->
+        <el-form-item prop="contacts" label="联系人">
+          <el-input v-model="formData.contacts" type="text" placeholder="请输入联系人" />
+        </el-form-item>
+        <el-form-item prop="phone" label="电话">
+          <el-input v-model="formData.phone" type="text" placeholder="请输入电话" />
+        </el-form-item>
+        <el-form-item prop="email" label="邮箱">
+          <el-input v-model="formData.email" type="text" placeholder="请输入邮箱" />
+        </el-form-item>
+        <el-form-item prop="address" label="地址">
+          <el-input v-model="formData.address" type="text" placeholder="请输入地址" />
         </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleCreateOrUpdate" :loading="loading">立即创建</el-button>
+        <el-button v-if="formData.id === undefined" type="primary" @click="handleCreateOrUpdate" :loading="loading"
+          >立即创建</el-button
+        >
+        <el-button v-else type="primary" @click="handleCreateOrUpdate" :loading="loading">立即修改</el-button>
+      </template>
+    </el-dialog>
+    <!-- 选择管理员 -->
+    <el-dialog
+      v-model="selectAdminDialogFormVisible"
+      title="选择管理员"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      width="500"
+      draggable
+      style="margin-top: 10%"
+    >
+      <div class="custom-scroll" style="height: 300px; overflow: auto; display: flex; justify-content: space-between">
+        <el-tree
+          class="custom-scroll"
+          style="height: 300px; overflow: auto; width: 50%"
+          :data="orgTreeData"
+          default-expand-all
+          @node-click="handleNodeClick"
+        />
+        <el-checkbox-group v-model="checkUserList" style="height: 300px; overflow: auto; padding: 10px; width: 50%">
+          <el-checkbox
+            v-for="item in userData"
+            :key="item.id"
+            :label="item.label"
+            :value="item.id"
+            @change="handleCheckUser"
+          />
+        </el-checkbox-group>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="selectAdminDialogFormVisible = false">确定</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -282,4 +528,29 @@ onMounted(() => {
   display: flex;
   justify-content: flex-end;
 }
+
+.custom-scroll {
+  overflow-y: scroll; /* 允许垂直滚动 */
+  border: 1px solid #ddd;
+  padding: 0;
+  background-color: #ffffff;
+}
+
+/* 自定义滚动条样式 */
+.custom-scroll::-webkit-scrollbar {
+  width: 1px; /* 滚动条的宽度 */
+}
+
+.custom-scroll::-webkit-scrollbar-track {
+  background: #f1f1f1; /* 滚动条轨道的颜色 */
+}
+
+.custom-scroll::-webkit-scrollbar-thumb {
+  background: #0061ff; /* 滚动条的颜色 */
+  border-radius: 1px; /* 圆角 */
+}
+
+.custom-scroll::-webkit-scrollbar-thumb:hover {
+  background: #0061ff; /* 滚动条悬停时的颜色 */
+}
 </style>

+ 38 - 1
src/views/login/index.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { reactive, ref } from "vue"
+import { reactive, ref, onMounted, onBeforeUnmount } from "vue"
 import { useRouter } from "vue-router"
 import { useUserStore } from "@/store/modules/user"
 import Logo from "@/assets/login/logo.svg?component"
@@ -52,6 +52,43 @@ const handleLogin = () => {
     }
   })
 }
+const preventDefaultActions = (e) => {
+  e.preventDefault()
+}
+
+const preventZoom = (e) => {
+  if (e.ctrlKey || e.metaKey) {
+    e.preventDefault()
+  }
+}
+
+const preventTextSelection = (e) => {
+  e.preventDefault()
+}
+
+onMounted(() => {
+  document.addEventListener("contextmenu", preventDefaultActions)
+  document.addEventListener("keydown", (e) => {
+    if (e.ctrlKey && (e.key === "A" || e.key === "a")) {
+      preventDefaultActions(e)
+    }
+    if (e.ctrlKey && (e.key === "=" || e.key === "-")) {
+      preventDefaultActions(e)
+    }
+    if (e.ctrlKey && e.shiftKey && (e.key === "+" || e.key === "_")) {
+      preventDefaultActions(e)
+    }
+  })
+  document.addEventListener("wheel", preventZoom, { passive: false })
+  document.addEventListener("mousedown", preventTextSelection)
+})
+
+onBeforeUnmount(() => {
+  document.removeEventListener("contextmenu", preventDefaultActions)
+  document.removeEventListener("keydown", preventDefaultActions)
+  document.removeEventListener("wheel", preventZoom)
+  document.removeEventListener("mousedown", preventTextSelection)
+})
 </script>
 
 <template>