Bläddra i källkod

校友组织管理,广场信息管理已接完接口,系统设置上传图片新增尺寸截图限制

hzj18279462576@163.com 3 månader sedan
förälder
incheckning
1f7b53ff57

+ 86 - 1
package-lock.json

@@ -8,7 +8,6 @@
       "name": "pack",
       "version": "0.0.0",
       "dependencies": {
-        "@popperjs/core": "^2.11.8",
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^5.1.12",
         "axios": "^1.8.4",
@@ -19,6 +18,7 @@
         "pinia": "^3.0.2",
         "v-viewer": "^3.0.11",
         "vue": "^3.5.13",
+        "vue-img-cutter": "^3.0.7",
         "vue-router": "^4.5.0"
       },
       "devDependencies": {
@@ -520,6 +520,47 @@
       "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.9.tgz",
       "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
     },
+    "node_modules/@intlify/core-base": {
+      "version": "9.14.4",
+      "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.4.tgz",
+      "integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==",
+      "dependencies": {
+        "@intlify/message-compiler": "9.14.4",
+        "@intlify/shared": "9.14.4"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/message-compiler": {
+      "version": "9.14.4",
+      "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
+      "integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
+      "dependencies": {
+        "@intlify/shared": "9.14.4",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/shared": {
+      "version": "9.14.4",
+      "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.4.tgz",
+      "integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.13",
       "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -2037,6 +2078,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/core-js": {
+      "version": "3.48.0",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.48.0.tgz",
+      "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
+      "hasInstallScript": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/cors": {
       "version": "2.8.5",
       "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
@@ -6477,6 +6528,40 @@
         }
       }
     },
+    "node_modules/vue-i18n": {
+      "version": "9.14.4",
+      "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.4.tgz",
+      "integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==",
+      "dependencies": {
+        "@intlify/core-base": "9.14.4",
+        "@intlify/shared": "9.14.4",
+        "@vue/devtools-api": "^6.5.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/vue-i18n/node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
+    },
+    "node_modules/vue-img-cutter": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/vue-img-cutter/-/vue-img-cutter-3.0.7.tgz",
+      "integrity": "sha512-fNw3kimawg9XVXDZCw2bI74NI+Jq+H42wjymatZVVSY46wuBty6LbQsu4GeVfo/yzpS9AHY0tzckpYzX3D2fmA==",
+      "dependencies": {
+        "core-js": "^3.20.3",
+        "vue": "^3.2.29",
+        "vue-i18n": "^9.1.10"
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.5.0",
       "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",

+ 1 - 1
package.json

@@ -9,7 +9,6 @@
     "preview": "vite preview"
   },
   "dependencies": {
-    "@popperjs/core": "^2.11.8",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "axios": "^1.8.4",
@@ -20,6 +19,7 @@
     "pinia": "^3.0.2",
     "v-viewer": "^3.0.11",
     "vue": "^3.5.13",
+    "vue-img-cutter": "^3.0.7",
     "vue-router": "^4.5.0"
   },
   "devDependencies": {

+ 158 - 0
src/api/alumni-organization.js

@@ -0,0 +1,158 @@
+import request from "@/utils/request"; // 你的axios封装
+
+// 【【【【【校友组织列表】】】】】
+// 8.15.	创建或编辑组织弹窗中部门树形图列表
+export function getQueryOrgTree() {
+  return request({
+    url: "/api/alumniOrg/queryOrgTree",
+    method: "get",
+  });
+}
+
+// 获取省
+export function getProvinceLevel() {
+  return request({
+    url: "/api/alumniCity/getProvinceLevel",
+    method: "get",
+  });
+}
+
+// 获取市
+export function getCityLevel(params) {
+  return request({
+    url: "/api/alumniCity/getCityLevel",
+    method: "get",
+    params: params,
+  });
+}
+
+// 8.16.	导出校友组织数据
+export function getAlumniClubExcel(data) {
+  return request({
+    url: "/api/alumniClub/getAlumniClubExcel",
+    method: "post",
+    data: data,
+  });
+}
+
+// 8.1.	获取组织分类下拉列表数据
+export function getQueryCategoryDatas() {
+  return request({
+    url: "/api/alumniCategory/queryCategoryDatas",
+    method: "get",
+  });
+}
+// 8.2.	组织分页数据
+export function getQueryClubPage(params) {
+  return request({
+    url: "/api/alumniClub/queryClubPage",
+    method: "get",
+    params: params,
+  });
+}
+// 8.3.	创建组织
+export function insertClubData(data) {
+  return request({
+    url: "/api/alumniClub/insertClubData",
+    method: "post",
+    data: data,
+  });
+}
+// 8.4.	更新组织
+export function updateClubData(data) {
+  return request({
+    url: "/api/alumniClub/updateClubData",
+    method: "post",
+    data: data,
+  });
+}
+
+// 删除校友相册数据
+export function deleteClubById(params) {
+  return request({
+    url: `/api/alumniClub/deleteClubById`,
+    method: "get",
+    params: params,
+  });
+}
+
+// 8.5.	置顶组织
+export function clubTopup(data) {
+  return request({
+    url: `/api/alumniClub/clubTopup`,
+    method: "post",
+    data: data,
+  });
+}
+
+// 【【【【【组织分类管理】】】】】】
+// 8.7.	获取组织分类分页数据
+export function getQueryCategoryPage(params) {
+  return request({
+    url: "/api/alumniCategory/queryCategoryPage",
+    method: "get",
+    params: params,
+  });
+}
+
+// 8.8.	新增组织分类数据
+export function insertCategory(data) {
+  return request({
+    url: "/api/alumniCategory/insertCategory",
+    method: "post",
+    data: data,
+  });
+}
+// 8.9.	更新组织分类数据
+export function updateCategory(data) {
+  return request({
+    url: "/api/alumniCategory/updateCategory",
+    method: "post",
+    data: data,
+  });
+}
+
+// 删除相册分类数据
+export function deleteCategory(params) {
+  return request({
+    url: `/api/alumniCategory/deleteCategory`,
+    method: "get",
+    params: params,
+  });
+}
+
+// 【【【【【入会申请审核】】】】】】
+// 8.12.	获取审核状态下拉列表数据
+export function getQueryPassTypes() {
+  return request({
+    url: "/api/alumniClub/queryPassTypes",
+    method: "get",
+  });
+}
+
+// 8.13.	组织入会审核分页数据
+export function getQueryClubProcessPage(params) {
+  return request({
+    url: "/api/alumniClubApply/queryClubProcessPage",
+    method: "get",
+    params: params,
+  });
+}
+
+// 8.14.	审核入会数据
+export function toExamineClub(params) {
+  return request({
+    url: "/api/alumniClubApply/toExamineClub",
+    method: "get",
+    params: params,
+  });
+}
+
+// 8.17.	导出校友入会审核数据
+export function getQueryClubProcessExcel(data) {
+  return request({
+    url: "/api/alumniClubApply/queryClubProcessExcel",
+    method: "post",
+    data: data,
+  });
+}

+ 56 - 0
src/api/alumni-square.js

@@ -0,0 +1,56 @@
+// 广场信息
+// 引入axios请求实例(已封装好baseURL、请求拦截器携带token)
+import request from '@/utils/request'
+
+// 17.1.	发布动态
+export function insertPosts(data) {
+  return request({
+    url: '/api/alumniPosts/insertPosts',
+    method: 'post',
+    data
+  })
+}
+
+// 17.2.	获取组织分类下拉列表数据
+export function queryCategoryDatas() {
+  return request({
+    url: '/api/alumniCategory/queryCategoryDatas',
+    method: 'get'
+  })
+}
+
+// 17.3.	审核帖子数据
+export function toExaminePosts(params) {
+  return request({
+    url: '/api/alumniPosts/toExaminePosts',
+    method: 'get',
+    params // 传菜单对象:{permCode, permName, parentId...}
+  })
+}
+
+// 17.4.	删除帖子数据
+export function deletePostsById(params) {
+  return request({
+    url: '/api/alumniPosts/deletePostsById',
+    method: 'get',
+    params // 传修改后的菜单对象,必须带id
+  })
+}
+
+// 17.5.	获取帖子分页数据
+export function queryPostsPage(params) {
+  return request({
+    url: `/api/alumniPosts/queryPostsPage`,
+    method: 'get',
+    params
+  })
+}
+
+// 17.6.	获取评论数据
+export function queryCommetns(params) {
+  return request({
+    url: `/api/alumniComment/queryCommetns`,
+    method: 'get',
+    params
+  })
+}

BIN
src/assets/img/dianzan.png


BIN
src/assets/img/pinglun.png


BIN
src/assets/img/yuandian.png


BIN
src/assets/img/yuandian2.png


BIN
src/assets/img/yuandian3.png


+ 216 - 0
src/components/CropImage.vue

@@ -0,0 +1,216 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="裁剪图片(固定尺寸:1200x800)"
+    width="95%"
+    top="2vh"
+    append-to-body
+    :close-on-click-modal="false"
+    @close="handleClose"
+  >
+    <!-- 裁剪容器 -->
+    <div class="crop-container">
+      <img ref="cropImage" :src="imageUrl" alt="待裁剪图片" />
+    </div>
+
+    <!-- 弹窗底部按钮 -->
+    <template #footer>
+      <el-button @click="visible = false">取消</el-button>
+      <el-button type="primary" @click="handleConfirmCrop">确认裁剪</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, watch, nextTick, onUnmounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import Cropper from 'cropperjs'
+import 'cropperjs/dist/cropper.min.css'
+
+// 接收父组件传值
+const props = defineProps({
+  // 是否显示弹窗
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  // 待裁剪图片的URL(base64或远程地址)
+  imageUrl: {
+    type: String,
+    default: ''
+  },
+  // 裁剪宽高比(默认1200/800=3:2)
+  aspectRatio: {
+    type: Number,
+    default: 1200 / 800
+  },
+  // 最终输出的宽度
+  outputWidth: {
+    type: Number,
+    default: 1200
+  },
+  // 最终输出的高度
+  outputHeight: {
+    type: Number,
+    default: 800
+  }
+})
+
+// 向父组件传值
+const emit = defineEmits([
+  'close', // 弹窗关闭
+  'confirm', // 裁剪确认(返回blob和base64)
+  'error' // 裁剪失败
+])
+
+// 裁剪相关变量
+const cropImage = ref(null)
+const cropperInstance = ref(null)
+
+// 监听弹窗显示状态,初始化/销毁裁剪器
+watch(
+  () => props.visible,
+  (newVal) => {
+    if (newVal && props.imageUrl) {
+      nextTick(() => {
+        setTimeout(initCropper, 200) // 延迟确保DOM加载完成
+      })
+    } else {
+      destroyCropper()
+    }
+  },
+  { immediate: true }
+)
+
+// 初始化裁剪器
+const initCropper = () => {
+  // 先销毁旧实例
+  destroyCropper()
+
+  if (!cropImage.value || !props.imageUrl) return
+
+  // 确保图片加载完成
+  cropImage.value.onload = () => {
+    try {
+      cropperInstance.value = new Cropper(cropImage.value, {
+        aspectRatio: props.aspectRatio, // 宽高比
+        viewMode: 0, // 允许裁剪框超出容器,解决画面过小
+        dragMode: 'move', // 拖动模式:移动图片
+        autoCropArea: 0.8, // 初始裁剪区域占80%
+        cropBoxResizable: false, // 禁止调整裁剪框大小
+        cropBoxMovable: true, // 允许移动裁剪框
+        background: false, // 关闭背景网格
+        center: true, // 裁剪框居中
+        zoomOnWheel: true, // 允许滚轮缩放
+        autoCrop: true, // 自动裁剪
+        responsive: true, // 响应式
+        restore: false, // 禁止恢复
+        checkOrientation: false // 关闭方向检查
+      })
+
+      // 强制放大图片,解决显示过小问题
+      setTimeout(() => {
+        if (cropperInstance.value) {
+          try {
+            const containerData = cropperInstance.value.getContainerData()
+            const imageData = cropperInstance.value.getImageData()
+            const scale = containerData.width / imageData.width * 0.9
+            cropperInstance.value.zoomTo(scale)
+          } catch (e) {
+            cropperInstance.value.zoomTo(1.2) // 降级放大
+          }
+        }
+      }, 300)
+    } catch (e) {
+      ElMessage.error('裁剪器初始化失败:' + e.message)
+      emit('error', e)
+    }
+  }
+}
+
+// 销毁裁剪器
+const destroyCropper = () => {
+  if (cropperInstance.value) {
+    cropperInstance.value.destroy()
+    cropperInstance.value = null
+  }
+}
+
+// 确认裁剪
+const handleConfirmCrop = () => {
+  if (!cropperInstance.value) {
+    ElMessage.error('裁剪器未初始化,请重试')
+    return
+  }
+
+  try {
+    // 获取裁剪后的canvas
+    const canvas = cropperInstance.value.getCroppedCanvas({
+      width: props.outputWidth,
+      height: props.outputHeight,
+      imageSmoothingQuality: 'high' // 高清裁剪
+    })
+
+    // 转为blob(用于上传)和base64(用于预览)
+    canvas.toBlob((blob) => {
+      if (blob) {
+        const base64 = canvas.toDataURL('image/png', 0.9)
+        emit('confirm', { blob, base64 }) // 向父组件返回结果
+        visible.value = false // 关闭弹窗
+      } else {
+        ElMessage.error('图片裁剪失败')
+        emit('error', new Error('canvas转blob失败'))
+      }
+    }, 'image/png', 0.9)
+  } catch (e) {
+    ElMessage.error('裁剪失败:' + e.message)
+    emit('error', e)
+  }
+}
+
+// 弹窗关闭时的处理
+const handleClose = () => {
+  destroyCropper()
+  emit('close')
+}
+
+// 组件卸载时销毁裁剪器
+onUnmounted(() => {
+  destroyCropper()
+})
+</script>
+
+<style scoped lang="scss">
+.crop-container {
+  width: 100%;
+  height: 700px; // 足够大的裁剪区域
+  overflow: hidden;
+  margin: 0 auto;
+
+  img {
+    display: block;
+    max-width: none !important;
+    max-height: none !important;
+  }
+}
+
+// 裁剪器样式穿透
+:deep(.cropper-container) {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+:deep(.cropper-crop-box) {
+  border: 2px solid #409eff;
+}
+
+:deep(.cropper-view-box) {
+  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px !important;
+  height: 750px;
+  overflow: hidden;
+}
+</style>

+ 43 - 0
src/components/ImgCutter.vue

@@ -0,0 +1,43 @@
+<script setup>
+import ImgCutter from "vue-img-cutter";
+import { uploadFile } from "@/api/uploadFile.js";
+
+let emits = defineEmits(["getUrl"]);
+let cutDown = (data) => {
+  let formData = new FormData();
+  console.log(data,"图片信息");
+  
+  let { file } = data;
+   // 校验文件类型
+  const isImage = file.type.startsWith("image/");
+  if (!isImage) {
+    ElMessage.error("只能上传图片格式文件");
+    return false;
+  }
+  // 校验文件大小(2MB)
+  const isLt2M = file.size / 1024 / 1024 < 4;
+  if (!isLt2M) {
+    ElMessage.error("图片大小不能超过4MB");
+    return false;
+  }
+
+  formData.append("file", file);
+  uploadFile(formData).then((res) => {
+    emits("getUrl", res);
+  });
+};
+</script>
+
+<template>
+  <ImgCutter
+    :boxWidth="1200"
+    :boxHeight="600"
+    :cutWidth="750"
+    :cutHeight="480" 
+    :sizeChange="false"
+    :originalGraph="true"
+    @cutDown="cutDown"
+  ></ImgCutter>
+</template>
+
+<style lang="scss" scoped></style>

+ 1 - 2
src/main.js

@@ -12,13 +12,12 @@ import { createPinia } from "pinia";
 import { useCounterStore } from "@/stores/index";
 
 // 导入 SVG 图标样式和注册逻辑
-import 'virtual:svg-icons-register'
+import "virtual:svg-icons-register";
 import SvgIcon from "@/components/SvgIcon.vue"; // 导入SvgIcon组件
 
 import Viewer from "v-viewer";
 import "viewerjs/dist/viewer.css";
 
-
 const addRouter = async () => {
   const store = useCounterStore();
   await store.ROLEAdd();

+ 1 - 1
src/views/alumni-album/PhotoAudit.vue

@@ -178,7 +178,7 @@
       ref="ruleFormRef"
       :model="ruleForm"
       :rules="rules"
-      label-width="80px"
+      label-width="90px"
       class="demo-ruleForm"
       :size="formSize"
       label-position="right"

+ 3 - 2
src/views/alumni-album/PhotoList.vue

@@ -266,8 +266,8 @@ const store = useCounterStore();
 // 为避免解构时失去响应性
 const { name, age, isCollapse, realAge, collegeRole } = storeToRefs(store);
 
-const handleDelete = (file) => {
-  console.log(file);
+const handleDelete = (index) => {
+  ruleForm.images.splice(index, 1);
 };
 
 const handlePreview = (images, index) => {
@@ -491,6 +491,7 @@ const submitAdd = lodash.debounce(async (formEl) => {
 
 const cancelAdd = () => {
   addDialogVisible.value = false;
+  getList()
 };
 
 // 多选框功能

+ 277 - 108
src/views/alumni-organization/ApplicationAudit.vue

@@ -6,7 +6,7 @@
           <span>发布人 :</span>
           <el-input
             clearable
-            v-model.trim="searchInput.name"
+            v-model.trim="searchInput.userName"
             class="w-50 m-2"
             placeholder="请输入发布人"
             style="width: 180px"
@@ -16,11 +16,11 @@
           <span>申请组织 :</span>
           <el-select
             clearable
-            v-model="searchInput.name"
+            v-model="searchInput.clubId"
             placeholder="请选择申请组织"
           >
             <el-option
-              v-for="i in collegeData"
+              v-for="i in categoryData"
               :key="i.id"
               :label="i.name"
               :value="i.id"
@@ -31,11 +31,11 @@
           <span>审核状态 :</span>
           <el-select
             clearable
-            v-model="searchInput.name"
+            v-model="searchInput.isPass"
             placeholder="请选择审核状态"
           >
             <el-option
-              v-for="i in collegeData"
+              v-for="i in typeData"
               :key="i.id"
               :label="i.name"
               :value="i.id"
@@ -83,38 +83,58 @@
       >
         <!-- <el-table-column type="selection" align="center" width="55" /> -->
         <el-table-column type="index" align="center" label="序号" width="60" />
-        <el-table-column align="center" prop="school" label="申请人" />
-        <el-table-column align="center" prop="school" label="联系电话" />
-        <el-table-column align="center" prop="school" label="工作单位" />
-        <el-table-column align="center" prop="school" label="工作地区" />
-        <el-table-column align="center" prop="school" label="职务" />
-        <el-table-column align="center" prop="school" label="申请组织" />
+        <el-table-column align="center" prop="userName" label="申请人" />
+        <el-table-column align="center" prop="phone" label="联系电话" />
+        <el-table-column align="center" prop="employer" label="工作单位" />
+        <el-table-column align="center" prop="workloca" label="工作地区" />
+        <el-table-column align="center" prop="position" label="职务" />
+        <el-table-column align="center" prop="clubName" label="申请组织" />
         <el-table-column width="100" align="center" label="组织城市">
           <template #default="{ row }">
-            <span v-if="row.retentionState == 2">是</span>
-            <span v-if="row.retentionState == 1">否</span>
+            <span>{{ row.clubCity }}</span>
           </template>
         </el-table-column>
         <el-table-column
           show-overflow-tooltip
           align="center"
-          prop="dormitory"
+          prop="remark"
           label="申请理由"
         />
-        <el-table-column align="center" prop="major" label="申请时间" />
-        <el-table-column align="center" prop="sex" label="审核状态" />
-        <el-table-column align="center" prop="college" label="审核人" />
-        <el-table-column align="center" prop="college" label="审核时间" />
+        <el-table-column align="center" prop="createTime" label="申请时间" />
+        <el-table-column align="center" prop="passName" label="审核状态">
+          <template #default="{ row }">
+            <span v-if="row.passName == '已通过'" style="color: #67c23a">{{
+              row.passName
+            }}</span>
+            <span v-if="row.passName == '待审核'" style="color: #409eff">{{
+              row.passName
+            }}</span>
+            <span v-if="row.passName == '已拒绝'" style="color: #f56c6c">{{
+              row.passName
+            }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column align="center" prop="applyUserName" label="审核人" />
+        <el-table-column align="center" prop="passTime" label="审核时间" />
         <el-table-column align="center" label="操作" fixed="right" width="200">
           <template #default="{ row }">
-            <el-button type="primary" @click="updateS(row)" link
+            <el-button type="primary" @click="infoClick(row)" link
               >详情</el-button
             >
-            <el-button type="primary" @click="updateS(row)" link
+            <el-button
+              type="primary"
+              v-if="row.passName == '待审核'"
+              @click="operationPassChange(row)"
+              link
               >通过</el-button
             >
-            <el-button type="danger" @click="deleteS(row)" link>拒绝</el-button>
-            <el-button type="danger" @click="deleteS(row)" link>删除</el-button>
+            <el-button
+              type="danger"
+              v-if="row.passName == '待审核'"
+              @click="operationRefuseChange(row)"
+              link
+              >拒绝</el-button
+            >
           </template>
         </el-table-column>
       </el-table>
@@ -142,7 +162,7 @@
     :close-on-press-escape="false"
     :title="dialongTitle"
     align-center
-    width="560"
+    width="540"
     :before-close="cancelAdd"
     destroy-on-close
     draggable
@@ -151,39 +171,19 @@
       ref="ruleFormRef"
       :model="ruleForm"
       :rules="rules"
-      label-width="100px"
+      label-width="90px"
       class="demo-ruleForm"
       :size="formSize"
       label-position="right"
       status-icon
     >
-      <el-form-item label="新闻标题 :" prop="dormitory">
-        <el-input
-          clearable
-          v-model.trim="ruleForm.dormitory"
-          class="w-50 m-2"
-          placeholder="请输入新闻标题"
-        />
-      </el-form-item>
-      <el-form-item label="新闻分类 :" prop="school">
-        <el-select
-          @change="schoolFormChange"
-          v-model="ruleForm.school"
-          clearable
-          placeholder="请选择新闻分类"
-        >
-          <el-option
-            v-for="i in schoolData"
-            :key="i.id"
-            :label="i.school"
-            :value="`${i.school},${i.id}`"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="新闻内容 :" prop="bedNumber">
+      <el-form-item
+        label="审批意见 :"
+        :prop="dialongTitle == '拒绝审批' ? 'passValue' : ''"
+      >
         <el-input
-          v-model.trim="ruleForm.bedNumber"
-          placeholder="请输入新闻内容"
+          v-model.trim="ruleForm.passValue"
+          placeholder="请输入审批意见"
           clearable
           :autosize="{ minRows: 4 }"
           type="textarea"
@@ -250,6 +250,59 @@
       >
     </div>
   </el-dialog>
+
+  <el-dialog
+    class="projectImport"
+    v-model="infoVisible"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    title="详情"
+    align-center
+    width="600"
+    :before-close="cancelInfo"
+    draggable
+  >
+    <div style="margin-bottom: 15px">
+      <el-descriptions :column="1">
+        <el-descriptions-item label="申请人 :">{{
+          infoData.userName
+        }}</el-descriptions-item>
+        <el-descriptions-item label="联系电话 :">{{
+          infoData.phone
+        }}</el-descriptions-item>
+        <el-descriptions-item label="工作单位 :">{{
+          infoData.employer
+        }}</el-descriptions-item>
+        <el-descriptions-item label="工作地区 :">{{
+          infoData.workloca
+        }}</el-descriptions-item>
+        <el-descriptions-item label="职务 :">{{
+          infoData.position
+        }}</el-descriptions-item>
+        <el-descriptions-item label="申请组织 :">{{
+          infoData.clubName
+        }}</el-descriptions-item>
+        <el-descriptions-item label="组织城市 :">{{
+          infoData.clubCity
+        }}</el-descriptions-item>
+        <el-descriptions-item label="申请理由 :">{{
+          infoData.remark
+        }}</el-descriptions-item>
+        <el-descriptions-item label="申请时间 :">{{
+          infoData.createTime
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核状态 :">{{
+          infoData.passName
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核人 :">{{
+          infoData.applyUserName
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核时间 :">{{
+          infoData.passTime
+        }}</el-descriptions-item>
+      </el-descriptions>
+    </div>
+  </el-dialog>
 </template>
 
 <script setup>
@@ -269,6 +322,14 @@ import lodash from "lodash";
 import { storeToRefs } from "pinia";
 import { useCounterStore } from "@/stores/index";
 
+import {
+  getQueryPassTypes,
+  getQueryCategoryDatas,
+  getQueryClubProcessPage,
+  toExamineClub,
+  getQueryClubProcessExcel,
+} from "@/api/alumni-organization.js";
+
 const router = useRouter();
 const store = useCounterStore();
 
@@ -284,11 +345,10 @@ const activeIndex = ref(); // 默认跳转路由
 const dialongTitle = ref("新增账号"); // 弹窗标题
 
 const searchInput = reactive({
-  name: "",
-  year: null,
-  college: null,
-  major: null,
-  class: null,
+  userName: "",
+  clubId: null,
+  isPass: null,
+  createTime: null,
 }); // 搜索按钮数据
 
 const currentPage = ref(1); // 当前页
@@ -297,82 +357,137 @@ const total = ref(0); // 当前总数
 const selectIds = ref([]); // 勾选的全部数据
 
 const addDialogVisible = ref(false); // 控制添加账号弹窗
+const typeData = ref(); // 审核状态下拉列表
+const categoryData = ref(); // 组织分类下拉列表
 
 // 表单数据
 const formSize = ref("default");
 const ruleFormRef = ref();
 const ruleForm = reactive({
-  school: "靖安校区", //校区名称
-  build: "", //楼栋名称
-  dormitory: "", //寝室号
-  sex: "男", //寝室性别
-  college: "", //所属学院
-  major: "", //所属专业
-  bedNumber: "", //床位数
-  // "gradestr": "",              //所属年级
-  remark: "", //备注
+  passValue: "",
   id: "",
 });
 // 表单验证
 const rules = reactive({
-  school: [{ required: true, message: "校区名称不能为空", trigger: "blur" }],
-  build: [{ required: true, message: "楼栋名称不能为空", trigger: "blur" }],
-  dormitory: [{ required: true, message: "寝室号不能为空", trigger: "blur" }],
-  sex: [{ required: true, message: "寝室性别不能为空", trigger: "blur" }],
-  college: [{ required: true, message: "所属学院不能为空", trigger: "blur" }],
-  major: [{ required: true, message: "专业不能为空", trigger: "blur" }],
-  bedNumber: [{ required: true, message: "床位数不能为空", trigger: "blur" }],
+  passValue: [{ required: true, message: "审批意见不能为空", trigger: "blur" }],
 });
 
+const infoVisible = ref(false);
+const infoData = ref({});
+
 // 获取账户列表
 const getList = async () => {
   loading.value = true;
   let params = {
     currentPage: currentPage.value, // 当前页
     pageCount: pageSize.value, // 一页数据条数
-    schoolId: searchInput.schoolId,
-    buildId: searchInput.buildId,
+    userName: searchInput.userName,
+    clubId: searchInput.clubId,
+    isPass: searchInput.isPass,
   };
+  if (searchInput.createTime) {
+    params.startTime = searchInput.createTime[0];
+    params.endTime = searchInput.createTime[1];
+  }
+  getQueryClubProcessPage(params).then((res) => {
+    console.log(res, "组织入会审核分页数据");
+    if (res.code == 200) {
+      tableData.list = res.data.list;
+      total.value = res.data.totalCount;
+      loading.value = false;
+    } else {
+      loading.value = false;
+    }
+  });
 };
 
 // 搜索功能
-const searchBtn = lodash.debounce(async () => {}, 300);
-const resetBtn = lodash.debounce(async () => {}, 300);
-
-// 添加账号
-const addlist = () => {
-  dialongTitle.value = "发布新闻";
+const searchBtn = lodash.debounce(async () => {
+  currentPage.value = 1;
+  getList();
+}, 300);
+const resetBtn = lodash.debounce(async () => {
+  searchInput.userName = "";
+  searchInput.clubId = "";
+  searchInput.isPass = null;
+  searchInput.createTime = null;
+  currentPage.value = 1;
+  getList();
+}, 300);
+
+
+// 通过
+// const operationPassChange = async (row) => {
+//   let data = {
+//     id: row.id,
+//     isPass: 2,
+//   };
+//   let res = await toExamineClub(data);
+//   if (res.code == 200) {
+//     ElMessage({
+//       type: "success",
+//       showClose: true,
+//       message: res.message,
+//       center: true,
+//     });
+//     getList();
+//   } else {
+//     ElMessage({
+//       type: "error",
+//       showClose: true,
+//       message: res.message,
+//       center: true,
+//     });
+//   }
+// };
+// // 拒绝
+// const operationRefuseChange = async (row) => {
+//   let data = {
+//     id: row.id,
+//     isPass: 3,
+//   };
+//   let res = await toExamineClub(data);
+//   if (res.code == 200) {
+//     ElMessage({
+//       type: "success",
+//       showClose: true,
+//       message: res.message,
+//       center: true,
+//     });
+//     getList();
+//   } else {
+//     ElMessage({
+//       type: "error",
+//       showClose: true,
+//       message: res.message,
+//       center: true,
+//     });
+//   }
+// };
+// 通过
+const operationPassChange = async (row) => {
+  dialongTitle.value = "通过审批";
   addDialogVisible.value = true;
-  ruleForm.school = null;
+  ruleForm.passValue = null;
+  ruleForm.id = row.id;
 };
-// 添加账号
-const updateS = (row) => {
-  console.log(row);
-  dialongTitle.value = "编辑新闻";
+// 拒绝
+const operationRefuseChange = async (row) => {
+  dialongTitle.value = "拒绝审批";
   addDialogVisible.value = true;
+  ruleForm.passValue = null;
+  ruleForm.id = row.id;
 };
-const deleteS = async (row) => {
-  ElMessageBox.confirm("是否删除此数据?", "提示!!!", {
-    confirmButtonText: "确认",
-    cancelButtonText: "取消",
-    type: "warning",
-  })
-    .then(async () => {
-      loading.value = true;
-      let data = {
-        dormitoryId: row.id, // 当前页
-      };
-    })
-    .catch(() => {
-      loading.value = false;
-    });
+
+const infoClick = (row) => {
+  console.log(row);
+
+  infoVisible.value = true;
+  infoData.value = row;
 };
-// 保留
-const retainS = async (row, flag) => {
-  let data = {
-    dormitoryIds: [row.id], // 当前页
-    retentionState: flag, //保留状态 1:开放,2:保留
-  };
+
+const cancelInfo = () => {
+  infoVisible.value = false;
 };
 
 // 确认添加员工
@@ -380,13 +495,34 @@ const submitAdd = lodash.debounce(async (formEl) => {
   if (!formEl) return;
   await formEl.validate(async (valid, fields) => {
     if (valid) {
-      console.log(data);
+      let data = {
+        id: ruleForm.id,
+        isPass: dialongTitle.value == "通过审批" ? 2 : 3,
+        passValue: ruleForm.passValue,
+      };
+      let res = await toExamineClub(data);
+      if (res.code == 200) {
+        addDialogVisible.value = false;
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+        getList();
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     } else {
       console.log("error submit!", fields);
     }
   });
 }, 1000);
-
 const cancelAdd = () => {
   addDialogVisible.value = false;
 };
@@ -426,7 +562,40 @@ const cancelProjectImport = () => {
   }
 };
 
-onBeforeMount(() => {});
+const typeList = async () => {
+  let res = await getQueryPassTypes();
+  console.log(res, "审核状态下拉列表");
+  if (res.code == 200) {
+    typeData.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+const categoryList = async () => {
+  let res = await getQueryCategoryDatas();
+  // console.log(res, "相册分类下拉列表数据");
+  if (res.code == 200) {
+    categoryData.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+onBeforeMount(() => {
+  getList();
+  typeList();
+  categoryList();
+});
 onUnmounted(() => {
   // document.removeEventListener("keyup", Enters);
 });

+ 139 - 54
src/views/alumni-organization/CategoryManage.vue

@@ -6,11 +6,11 @@
           <span>分类名称 :</span>
           <el-select
             clearable
-            v-model="searchInput.year"
+            v-model="searchInput.categoryId"
             placeholder="请选择分类名称"
           >
             <el-option
-              v-for="i in yearData"
+              v-for="i in categoryData"
               :key="i.id"
               :label="i.name"
               :value="i.id"
@@ -21,7 +21,7 @@
           <span>创建人 :</span>
           <el-input
             clearable
-            v-model.trim="searchInput.name"
+            v-model.trim="searchInput.userName"
             class="w-50 m-2"
             placeholder="请输入创建人"
             style="width: 180px"
@@ -67,20 +67,31 @@
     <div class="footer" v-loading="loading">
       <div class="card_all">
         <el-space wrap>
-          <el-card style="max-width: 480px" v-for="o in 16" :key="o">
+          <el-card
+            style="max-width: 480px"
+            v-for="i in tableData.list"
+            :key="i.id"
+          >
             <template #header>
               <div class="card-header">
-                <span>校友企业家联谊会</span>
-                <span><span class="number">1 </span>个组织</span>
+                <span>{{ i.name }}</span>
+                <span
+                  ><span class="number">{{ i.photoNum ? i.photoNum : 0 }} </span
+                  >个组织</span
+                >
               </div>
             </template>
-            <p>创建时间: 2026-01-06 14:26:00</p>
-            <p>创建人: 张三</p>
+            <p>创建时间: {{ i.createTime }}</p>
+            <p>创建人: {{ i.userName }}</p>
             <template #footer>
               <div class="card-footer">
-                <el-button type="primary" link>编辑</el-button>
+                <el-button type="primary" @click="updateS(i)" link
+                  >编辑</el-button
+                >
                 <span class="line">|</span>
-                <el-button type="danger" link>删除</el-button>
+                <el-button type="danger" @click="deleteS(i)" link
+                  >删除</el-button
+                >
               </div>
             </template>
           </el-card>
@@ -110,7 +121,7 @@
     :close-on-press-escape="false"
     :title="dialongTitle"
     align-center
-    width="450"
+    width="440"
     :before-close="cancelAdd"
     destroy-on-close
     draggable
@@ -119,16 +130,16 @@
       ref="ruleFormRef"
       :model="ruleForm"
       :rules="rules"
-      label-width="100px"
+      label-width="90px"
       class="demo-ruleForm"
       :size="formSize"
       label-position="right"
       status-icon
     >
-      <el-form-item label="分类名称 :" prop="dormitory">
+      <el-form-item label="分类名称 :" prop="name">
         <el-input
           clearable
-          v-model.trim="ruleForm.dormitory"
+          v-model.trim="ruleForm.name"
           class="w-50 m-2"
           placeholder="请输入分类名称"
         />
@@ -165,6 +176,14 @@ import lodash from "lodash";
 import { storeToRefs } from "pinia";
 import { useCounterStore } from "@/stores/index";
 
+import {
+  getQueryCategoryPage,
+  insertCategory,
+  updateCategory,
+  deleteCategory,
+  getQueryCategoryDatas
+} from "@/api/alumni-organization.js";
+
 const router = useRouter();
 const store = useCounterStore();
 
@@ -180,11 +199,9 @@ const activeIndex = ref(); // 默认跳转路由
 const dialongTitle = ref("新增账号"); // 弹窗标题
 
 const searchInput = reactive({
-  name: "",
-  year: null,
-  college: null,
-  major: null,
-  class: null,
+  categoryId: null,
+  userName: "",
+  createTime: null,
 }); // 搜索按钮数据
 
 const currentPage = ref(1); // 当前页
@@ -192,32 +209,20 @@ const pageSize = ref(10);
 const total = ref(0); // 当前总数
 const selectIds = ref([]); // 勾选的全部数据
 
+const categoryData = ref();
+
 const addDialogVisible = ref(false); // 控制添加账号弹窗
 
 // 表单数据
 const formSize = ref("default");
 const ruleFormRef = ref();
 const ruleForm = reactive({
-  school: "靖安校区", //校区名称
-  build: "", //楼栋名称
-  dormitory: "", //寝室号
-  sex: "男", //寝室性别
-  college: "", //所属学院
-  major: "", //所属专业
-  bedNumber: "", //床位数
-  // "gradestr": "",              //所属年级
-  remark: "", //备注
+  name: "", //校区名称
   id: "",
 });
 // 表单验证
 const rules = reactive({
-  school: [{ required: true, message: "校区名称不能为空", trigger: "blur" }],
-  build: [{ required: true, message: "楼栋名称不能为空", trigger: "blur" }],
-  dormitory: [{ required: true, message: "寝室号不能为空", trigger: "blur" }],
-  sex: [{ required: true, message: "寝室性别不能为空", trigger: "blur" }],
-  college: [{ required: true, message: "所属学院不能为空", trigger: "blur" }],
-  major: [{ required: true, message: "专业不能为空", trigger: "blur" }],
-  bedNumber: [{ required: true, message: "床位数不能为空", trigger: "blur" }],
+  name: [{ required: true, message: "分类名称不能为空", trigger: "blur" }],
 });
 
 // 获取账户列表
@@ -226,26 +231,52 @@ const getList = async () => {
   let params = {
     currentPage: currentPage.value, // 当前页
     pageCount: pageSize.value, // 一页数据条数
-    schoolId: searchInput.schoolId,
-    buildId: searchInput.buildId,
+    categoryId: searchInput.categoryId,
+    userName: searchInput.userName,
   };
+  if (searchInput.createTime) {
+    params.startTime = searchInput.createTime[0];
+    params.endTime = searchInput.createTime[1];
+  }
+  getQueryCategoryPage(params).then((res) => {
+    console.log(res, "组织分类分页数据");
+    if (res.code == 200) {
+      tableData.list = res.data.list;
+      total.value = res.data.totalCount;
+      loading.value = false;
+    } else {
+      loading.value = false;
+    }
+  });
 };
 
 // 搜索功能
-const searchBtn = lodash.debounce(async () => {}, 300);
-const resetBtn = lodash.debounce(async () => {}, 300);
+const searchBtn = lodash.debounce(async () => {
+  currentPage.value = 1;
+  getList();
+}, 300);
+const resetBtn = lodash.debounce(async () => {
+  searchInput.categoryId = "";
+  searchInput.userName = "";
+  searchInput.createTime = null;
+  currentPage.value = 1;
+  getList();
+}, 300);
 
 // 添加账号
 const addlist = () => {
   dialongTitle.value = "创建分类";
   addDialogVisible.value = true;
-  ruleForm.school = null;
+  ruleForm.name = null;
+  ruleForm.id = null;
 };
 // 添加账号
 const updateS = (row) => {
   console.log(row);
   dialongTitle.value = "编辑分类";
   addDialogVisible.value = true;
+  ruleForm.name = row.name;
+  ruleForm.id = row.id;
 };
 const deleteS = async (row) => {
   ElMessageBox.confirm("是否删除此数据?", "提示!!!", {
@@ -254,29 +285,65 @@ const deleteS = async (row) => {
     type: "warning",
   })
     .then(async () => {
-      loading.value = true;
-      let data = {
-        dormitoryId: row.id, // 当前页
+      let params = {
+        id: row.id,
       };
+      let res = await deleteCategory(params);
+      if (res.code == 200) {
+        getList();
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     })
     .catch(() => {
       loading.value = false;
     });
 };
-// 保留
-const retainS = async (row, flag) => {
-  let data = {
-    dormitoryIds: [row.id], // 当前页
-    retentionState: flag, //保留状态 1:开放,2:保留
-  };
-};
 
 // 确认添加员工
 const submitAdd = lodash.debounce(async (formEl) => {
   if (!formEl) return;
   await formEl.validate(async (valid, fields) => {
     if (valid) {
-      console.log(data);
+      let data = {
+        name: ruleForm.name,
+      };
+      let res = "";
+      if (dialongTitle.value == "创建分类") {
+        res = await insertCategory(data);
+      } else {
+        data.id = ruleForm.id;
+        res = await updateCategory(data);
+      }
+      if (res.code == 200) {
+        addDialogVisible.value = false;
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+        getList();
+        categoryList()
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     } else {
       console.log("error submit!", fields);
     }
@@ -322,7 +389,25 @@ const cancelProjectImport = () => {
   }
 };
 
-onBeforeMount(() => {});
+const categoryList = async () => {
+  let res = await getQueryCategoryDatas();
+  // console.log(res, "相册分类下拉列表数据");
+  if (res.code == 200) {
+    categoryData.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+onBeforeMount(() => {
+  getList();
+  categoryList();
+});
 onUnmounted(() => {
   // document.removeEventListener("keyup", Enters);
 });
@@ -383,10 +468,10 @@ onUnmounted(() => {
 
       .el-card {
         width: 360px !important;
-        // height: 184px;
+        height: 184px;
       }
       :deep(.el-card__header) {
-        padding: 20px 20px 0;
+        padding: 10px 20px 0;
         border: 0;
         font-size: 20px;
         .card-header {

+ 397 - 181
src/views/alumni-organization/OrganizationList.vue

@@ -16,11 +16,11 @@
           <span>组织分类 :</span>
           <el-select
             clearable
-            v-model="searchInput.name"
+            v-model="searchInput.categoryId"
             placeholder="请选择组织分类"
           >
             <el-option
-              v-for="i in collegeData"
+              v-for="i in categoryData"
               :key="i.id"
               :label="i.name"
               :value="i.id"
@@ -31,7 +31,7 @@
           <span>联系人 :</span>
           <el-input
             clearable
-            v-model.trim="searchInput.name"
+            v-model.trim="searchInput.contractPerson"
             class="w-50 m-2"
             placeholder="请输入联系人"
             style="width: 180px"
@@ -74,33 +74,44 @@
       >
         <!-- <el-table-column type="selection" align="center" width="55" /> -->
         <el-table-column type="index" align="center" label="序号" width="60" />
-        <el-table-column align="center" prop="school" label="组织名称" />
-        <el-table-column width="100" align="center" label="组织分类">
+        <el-table-column align="center" prop="name" label="组织名称" />
+        <el-table-column
+          width="100"
+          align="center"
+          prop="categoryName"
+          label="组织分类"
+        >
+        </el-table-column>
+        <el-table-column align="center" prop="dormitory" label="组织城市">
           <template #default="{ row }">
-            <span v-if="row.retentionState == 2">是</span>
-            <span v-if="row.retentionState == 1">否</span>
+            {{ row.province }}-{{ row.city }}
           </template>
         </el-table-column>
         <el-table-column
           show-overflow-tooltip
           align="center"
-          prop="dormitory"
-          label="组织城市"
-        />
-        <el-table-column
-          show-overflow-tooltip
-          align="center"
-          prop="dormitory"
+          prop="description"
           label="组织介绍"
         />
-        <el-table-column align="center" prop="sex" label="分会标识" />
-        <el-table-column align="center" prop="college" label="成员人数" />
-        <el-table-column align="center" prop="major" label="联系人" />
-        <el-table-column align="center" prop="major" label="联系方式" />
-        <el-table-column align="center" prop="major" label="创建时间" />
+        <el-table-column align="center" label="分会标识">
+          <template #default="{ row }">
+            <img
+              v-viewer
+              v-if="row.branchIde"
+              :src="row.branchIde"
+              alt="图片预览"
+              class="preview-img"
+              style="width: 60px; height: 60px; margin: 10px 0"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" prop="clubNum" label="成员人数" />
+        <el-table-column align="center" prop="contractPerson" label="联系人" />
+        <el-table-column align="center" prop="contractInfo" label="联系方式" />
+        <el-table-column align="center" prop="createTime" label="创建时间" />
         <el-table-column align="center" label="操作" fixed="right" width="200">
           <template #default="{ row }">
-            <el-button type="primary" @click="updateS(row)" link
+            <el-button type="primary" @click="clubTopupClick(row)" link
               >置顶</el-button
             >
             <el-button type="primary" @click="updateS(row)" link
@@ -149,92 +160,114 @@
       label-position="right"
       status-icon
     >
-      <el-form-item label="组织名称 :" prop="dormitory">
+      <el-form-item label="组织名称 :" prop="name">
         <el-input
           clearable
-          v-model.trim="ruleForm.dormitory"
+          v-model.trim="ruleForm.name"
           class="w-50 m-2"
           placeholder="请输入组织名称"
         />
       </el-form-item>
-      <el-form-item label="组织分类 :" prop="school">
+      <el-form-item label="组织分类 :" prop="categoryId">
         <el-select
-          @change="schoolFormChange"
-          v-model="ruleForm.school"
+          v-model="ruleForm.categoryId"
           clearable
           placeholder="请选择组织分类"
         >
           <el-option
-            v-for="i in schoolData"
+            v-for="i in categoryData"
             :key="i.id"
-            :label="i.school"
-            :value="`${i.school},${i.id}`"
+            :label="i.name"
+            :value="`${i.id}-${i.name}`"
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="组织城市 :" prop="bedNumber">
-        <el-cascader v-model="ruleForm.bedNumber" :options="options" />
+      <el-form-item label="组织城市 :" prop="cityId">
+        <div>
+          <el-select
+            v-model="ruleForm.provinceId"
+            placeholder="请选择省"
+            style="width: 140px"
+            @change="provincesChange"
+          >
+            <el-option
+              v-for="item in provinceList"
+              :key="item.id"
+              :label="item.name"
+              :value="`${item.id}-${item.name}`"
+            />
+          </el-select>
+          <span> - </span>
+          <el-select
+            v-model="ruleForm.cityId"
+            placeholder="请选择市"
+            style="width: 140px"
+          >
+            <el-option
+              v-for="item in cityList"
+              :key="item.id"
+              :label="item.name"
+              :value="`${item.id}-${item.name}`"
+            />
+          </el-select>
+        </div>
       </el-form-item>
-      <el-form-item label="组织介绍 :" prop="bedNumber">
+      <el-form-item label="组织介绍 :" prop="description">
         <el-input
-          v-model.trim="ruleForm.bedNumber"
+          v-model.trim="ruleForm.description"
           placeholder="请输入组织介绍"
           clearable
           :autosize="{ minRows: 4 }"
           type="textarea"
         />
       </el-form-item>
-      <el-form-item label="联系人 :" prop="dormitory">
+      <el-form-item label="联系人 :" prop="contractPerson">
         <el-input
           clearable
-          v-model.trim="ruleForm.dormitory"
+          v-model.trim="ruleForm.contractPerson"
           class="w-50 m-2"
           placeholder="请输入联系人"
         />
       </el-form-item>
-      <el-form-item label="手机号码 :" prop="dormitory">
+      <el-form-item label="手机号码 :" prop="contractInfo">
         <el-input
           clearable
-          v-model.trim="ruleForm.dormitory"
+          v-model.trim="ruleForm.contractInfo"
           class="w-50 m-2"
           placeholder="请输入手机号码"
         />
       </el-form-item>
-      <el-form-item label="分会标识 :" prop="dormitory">
-        <el-upload
-          action="#"
-          list-type="picture-card"
-          :auto-upload="false"
-          :file-list="fileList"
-          :on-change="handleFileChange"
-          :before-upload="handleBeforeUpload"
-        >
-          <el-icon><Plus /></el-icon>
-
-          <template #file="{ file }">
-            <div>
-              <img
-                class="el-upload-list__item-thumbnail"
-                :src="file.url"
-                alt=""
-              />
-              <span class="el-upload-list__item-actions">
-                <span
-                  class="el-upload-list__item-preview"
-                  @click="handlePictureCardPreview(file)"
-                >
-                  <el-icon><ZoomIn /></el-icon>
-                </span>
-                <span
-                  class="el-upload-list__item-delete"
-                  @click="handleRemove(file)"
-                >
-                  <el-icon><Delete /></el-icon>
-                </span>
-              </span>
-            </div>
-          </template>
-        </el-upload>
+      <el-form-item label="分会标识 :" prop="branchIde">
+        <div class="img-preview-list">
+          <div class="img-item" v-if="ruleForm.branchIde">
+            <img
+              :src="ruleForm.branchIde"
+              alt="图片预览"
+              class="preview-img"
+              v-viewer
+            />
+            <span class="delete-btn" @click="handleDelete">
+              <el-icon><Delete /></el-icon>
+            </span>
+          </div>
+          <el-upload
+            action="#"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :before-upload="beforeUpload"
+            class="upload-btn"
+            accept="image/*"
+          >
+            <el-icon><Plus /></el-icon>
+            <span class="upload-txt">点击上传图片</span>
+          </el-upload>
+        </div>
+      </el-form-item>
+      <el-form-item label="置顶 :" prop="isTop">
+        <el-radio-group v-model="ruleForm.isTop">
+          <el-radio :value="1">是</el-radio>
+          <el-radio :value="2">否</el-radio>
+        </el-radio-group>
       </el-form-item>
       <el-form-item class="options">
         <el-button @click="cancelAdd">取消</el-button>
@@ -317,37 +350,72 @@ import { storeToRefs } from "pinia";
 import { useCounterStore } from "@/stores/index";
 import { api as viewerApi } from "v-viewer";
 
+import {
+  getQueryClubPage,
+  getQueryOrgTree,
+  getAlumniClubExcel,
+  getQueryCategoryDatas,
+  insertClubData,
+  updateClubData,
+  deleteClubById,
+  clubTopup,
+  getProvinceLevel,
+  getCityLevel,
+} from "@/api/alumni-organization.js";
+import { uploadFile } from "@/api/uploadFile";
+
 const router = useRouter();
 const store = useCounterStore();
 
 // 为避免解构时失去响应性
 const { name, age, isCollapse, realAge, collegeRole } = storeToRefs(store);
 
-// 维护当前的文件列表
-const fileList = ref([]);
+const handleDelete = () => {
+  ruleForm.branchIde = "";
+};
 
-// 核心:文件变化时直接覆盖
-const handleFileChange = (newFile) => {
-  // 直接用新文件替换整个列表,实现覆盖
-  fileList.value = [newFile];
+const handlePreview = (images) => {
+  // 传给预览器,此时插件显示的第一张就是点击的图片
+  viewerApi({
+    images: images,
+    zIndex: 3000,
+  });
 };
 
-// 上传前检查:如果已有文件,给出提示
-const handleBeforeUpload = () => {
-  if (fileList.value.length > 0) {
-    ElMessage.info("新图片将覆盖原有图片");
+// 上传前的校验
+const beforeUpload = (file) => {
+  // 校验文件类型
+  const isImage = file.type.startsWith("image/");
+  if (!isImage) {
+    ElMessage.error("只能上传图片格式文件");
+    return false;
+  }
+  // 校验文件大小(2MB)
+  const isLt2M = file.size / 1024 / 1024 < 2;
+  if (!isLt2M) {
+    ElMessage.error("图片大小不能超过2MB");
+    return false;
   }
   return true;
 };
 
-const handleRemove = () => {
-  fileList.value = [];
-  console.log("删除图片");
-};
-
-// 图片预览
-const handlePictureCardPreview = (file) => {
-  viewerApi({ images: fileList.value,zIndex: 2020 });
+// 文件选择变化时触发
+const handleFileChange = async (file, newFileList) => {
+  console.log(file);
+  let formData = new FormData();
+  formData.append("file", file.raw);
+  let res = await uploadFile(formData);
+  // console.log(res.data.fileUrl);
+  if (res.code == 200) {
+    ruleForm.branchIde = res.data.fileUrl; // 赋值给响应式fileList,页面自动刷新
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
 };
 
 // 表格数据
@@ -360,10 +428,8 @@ const dialongTitle = ref("新增账号"); // 弹窗标题
 
 const searchInput = reactive({
   name: "",
-  year: null,
-  college: null,
-  major: null,
-  class: null,
+  categoryId: null,
+  contractPerson: null,
 }); // 搜索按钮数据
 
 const currentPage = ref(1); // 当前页
@@ -371,76 +437,45 @@ const pageSize = ref(10);
 const total = ref(0); // 当前总数
 const selectIds = ref([]); // 勾选的全部数据
 
+const categoryData = ref();
+const provinceList = ref([]);
+const cityList = ref([]);
+
 const addDialogVisible = ref(false); // 控制添加账号弹窗
-const options = ref([
-  {
-    value: "guide",
-    label: "Guide",
-    children: [
-      {
-        value: "disciplines",
-        label: "Disciplines",
-        children: [
-          {
-            value: "consistency",
-            label: "Consistency",
-          },
-          {
-            value: "feedback",
-            label: "Feedback",
-          },
-          {
-            value: "efficiency",
-            label: "Efficiency",
-          },
-          {
-            value: "controllability",
-            label: "Controllability",
-          },
-        ],
-      },
-      {
-        value: "navigation",
-        label: "Navigation",
-        children: [
-          {
-            value: "side nav",
-            label: "Side Navigation",
-          },
-          {
-            value: "top nav",
-            label: "Top Navigation",
-          },
-        ],
-      },
-    ],
-  },
-]);
 
 // 表单数据
 const formSize = ref("default");
 const ruleFormRef = ref();
 const ruleForm = reactive({
-  school: "靖安校区", //校区名称
-  build: "", //楼栋名称
-  dormitory: "", //寝室号
-  sex: "男", //寝室性别
-  college: "", //所属学院
-  major: "", //所属专业
-  bedNumber: "", //床位数
-  // "gradestr": "",              //所属年级
-  remark: "", //备注
+  name: "",
+  categoryId: "",
+  provinceId: "",
+  cityId: "",
+  description: "",
+  contractPerson: "",
+  contractInfo: "",
+  branchIde: "",
+  isTop: 2, // 是:1   否:2
   id: "",
 });
 // 表单验证
 const rules = reactive({
-  school: [{ required: true, message: "校区名称不能为空", trigger: "blur" }],
-  build: [{ required: true, message: "楼栋名称不能为空", trigger: "blur" }],
-  dormitory: [{ required: true, message: "寝室号不能为空", trigger: "blur" }],
-  sex: [{ required: true, message: "寝室性别不能为空", trigger: "blur" }],
-  college: [{ required: true, message: "所属学院不能为空", trigger: "blur" }],
-  major: [{ required: true, message: "专业不能为空", trigger: "blur" }],
-  bedNumber: [{ required: true, message: "床位数不能为空", trigger: "blur" }],
+  name: [{ required: true, message: "组织名称不能为空", trigger: "blur" }],
+  categoryId: [
+    { required: true, message: "所属分类不能为空", trigger: "blur" },
+  ],
+  provinceId: [{ required: true, message: "省名称不能为空", trigger: "blur" }],
+  cityId: [{ required: true, message: "市名称不能为空", trigger: "blur" }],
+  description: [
+    { required: true, message: "组织介绍不能为空", trigger: "blur" },
+  ],
+  contractPerson: [
+    { required: true, message: "联系人不能为空", trigger: "blur" },
+  ],
+  contractInfo: [
+    { required: true, message: "联系方式不能为空", trigger: "blur" },
+  ],
+  branchIde: [{ required: true, message: "分会标识不能为空", trigger: "blur" }],
 });
 
 // 获取账户列表
@@ -449,29 +484,35 @@ const getList = async () => {
   let params = {
     currentPage: currentPage.value, // 当前页
     pageCount: pageSize.value, // 一页数据条数
-    schoolId: searchInput.schoolId,
-    buildId: searchInput.buildId,
+    categoryId: searchInput.categoryId,
+    name: searchInput.name,
+    contractPerson: searchInput.contractPerson,
   };
+  getQueryClubPage(params).then((res) => {
+    console.log(res, "校友组织分页数据");
+    if (res.code == 200) {
+      tableData.list = res.data.list;
+      total.value = res.data.totalCount;
+      loading.value = false;
+    } else {
+      loading.value = false;
+    }
+  });
 };
 
 // 搜索功能
-const searchBtn = lodash.debounce(async () => {}, 300);
-const resetBtn = lodash.debounce(async () => {}, 300);
+const searchBtn = lodash.debounce(async () => {
+  currentPage.value = 1;
+  getList();
+}, 300);
+const resetBtn = lodash.debounce(async () => {
+  searchInput.categoryId = "";
+  searchInput.name = "";
+  searchInput.contractPerson = null;
+  currentPage.value = 1;
+  getList();
+}, 300);
 
-const top = () => {};
-
-// 添加账号
-const addlist = () => {
-  dialongTitle.value = "创建组织";
-  addDialogVisible.value = true;
-  ruleForm.school = null;
-};
-// 添加账号
-const updateS = (row) => {
-  console.log(row);
-  dialongTitle.value = "编辑组织";
-  addDialogVisible.value = true;
-};
 const deleteS = async (row) => {
   ElMessageBox.confirm("是否删除此数据?", "提示!!!", {
     confirmButtonText: "确认",
@@ -479,21 +520,84 @@ const deleteS = async (row) => {
     type: "warning",
   })
     .then(async () => {
-      loading.value = true;
-      let data = {
-        dormitoryId: row.id, // 当前页
+      let params = {
+        id: row.id,
       };
+      let res = await deleteClubById(params);
+      if (res.code == 200) {
+        getList();
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     })
     .catch(() => {
       loading.value = false;
     });
 };
-// 保留
-const retainS = async (row, flag) => {
-  let data = {
-    dormitoryIds: [row.id], // 当前页
-    retentionState: flag, //保留状态 1:开放,2:保留
+// 添加账号
+const addlist = () => {
+  dialongTitle.value = "创建组织";
+  addDialogVisible.value = true;
+  ruleForm.name = "";
+  ruleForm.categoryId = null;
+  ruleForm.provinceId = null;
+  ruleForm.cityId = null;
+  cityList.value = [];
+  ruleForm.description = null;
+  ruleForm.contractPerson = null;
+  ruleForm.contractInfo = null;
+  ruleForm.branchIde = null;
+  ruleForm.isTop = 2;
+  ruleForm.id = null;
+};
+// 添加账号
+const updateS = async (row) => {
+  console.log(row);
+  dialongTitle.value = "编辑组织";
+  addDialogVisible.value = true;
+  ruleForm.name = row.name;
+  ruleForm.categoryId = `${row.categoryId}-${row.categoryName}`;
+  if (row.provinceId) {
+    ruleForm.provinceId = `${row.provinceId}-${row.province}`;
+    getCityLevel({ provinceId: row.provinceId }).then((res) => {
+      cityList.value = res.data;
+    });
+    ruleForm.cityId = `${row.cityId}-${row.city}`;
+  } else {
+    ruleForm.provinceId = [];
+    ruleForm.cityId = [];
+  }
+  ruleForm.description = row.description;
+  ruleForm.contractPerson = row.contractPerson;
+  ruleForm.contractInfo = row.contractInfo;
+  ruleForm.branchIde = row.branchIde;
+  ruleForm.isTop = row.isTop;
+  ruleForm.id = row.id;
+  let params = {
+    provinceId: row.cityId,
+  };
+  getCityLevel(params).then((res) => {});
+};
+const provincesChange = async (val) => {
+  console.log(val);
+  ruleForm.cityId = null;
+  let params = {
+    provinceId: val.split("-")[0],
   };
+  let res = await getCityLevel(params);
+  console.log(res, "市数据");
+  cityList.value = res.data;
 };
 
 // 确认添加员工
@@ -501,7 +605,44 @@ const submitAdd = lodash.debounce(async (formEl) => {
   if (!formEl) return;
   await formEl.validate(async (valid, fields) => {
     if (valid) {
-      console.log(data);
+      let data = {
+        name: ruleForm.name,
+        categoryId: ruleForm.categoryId.split("-")[0],
+        categoryName: ruleForm.categoryId.split("-")[1],
+        province: ruleForm.provinceId.split("-")[1],
+        provinceId: ruleForm.provinceId.split("-")[0],
+        city: ruleForm.cityId.split("-")[1],
+        cityId: ruleForm.cityId.split("-")[0],
+        description: ruleForm.description,
+        contractPerson: ruleForm.contractPerson,
+        contractInfo: ruleForm.contractInfo,
+        branchIde: ruleForm.branchIde,
+        isTop: ruleForm.isTop,
+      };
+      let res = "";
+      if (dialongTitle.value == "创建组织") {
+        res = await insertClubData(data);
+      } else {
+        data.id = ruleForm.id;
+        res = await updateClubData(data);
+      }
+      if (res.code == 200) {
+        addDialogVisible.value = false;
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+        getList();
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     } else {
       console.log("error submit!", fields);
     }
@@ -512,6 +653,30 @@ const cancelAdd = () => {
   addDialogVisible.value = false;
 };
 
+const clubTopupClick = async (row) => {
+  let params = {
+    id: row.id,
+    isTop: 1,
+  };
+  let res = await clubTopup(params);
+  if (res.code == 200) {
+    getList();
+    ElMessage({
+      type: "success",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
 // 多选框功能
 const handleSelectionChange = (val) => {
   // console.log(val);
@@ -547,7 +712,58 @@ const cancelProjectImport = () => {
   }
 };
 
-onBeforeMount(() => {});
+const categoryList = async () => {
+  let res = await getQueryCategoryDatas();
+  // console.log(res, "相册分类下拉列表数据");
+  if (res.code == 200) {
+    categoryData.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+const provinceData = async () => {
+  let res = await getProvinceLevel();
+  console.log(res, "省数据");
+  if (res.code == 200) {
+    provinceList.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+const cityData = async (provinceId) => {
+  let params = {
+    provinceId,
+  };
+  let res = await getCity(params);
+  console.log(res, "市数据");
+  if (res.code == 200) {
+    return res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+onBeforeMount(() => {
+  getList();
+  categoryList();
+  provinceData();
+});
 onUnmounted(() => {
   // document.removeEventListener("keyup", Enters);
 });

+ 1 - 1
src/views/alumni-organization/alumni-organization.vue

@@ -35,7 +35,7 @@ const store = useCounterStore();
 // 为避免解构时失去响应性
 const { name, age, isCollapse, realAge, collegeRole } = storeToRefs(store);
 
-const activeName = ref("third");
+const activeName = ref("first");
 
 const handleClick = (tab, event) => {
   console.log(tab.props.name, event);

+ 494 - 99
src/views/alumni-square/alumni-square.vue

@@ -11,7 +11,7 @@
             <span>发帖人 :</span>
             <el-input
               clearable
-              v-model.trim="searchInput.name"
+              v-model.trim="searchInput.userName"
               class="w-50 m-2"
               placeholder="请输入发帖人"
               style="width: 180px"
@@ -21,15 +21,12 @@
             <span>审核状态 :</span>
             <el-select
               clearable
-              v-model="searchInput.year"
+              v-model="searchInput.isPass"
               placeholder="请选择审核状态"
             >
-              <el-option
-                v-for="i in yearData"
-                :key="i.id"
-                :label="i.name"
-                :value="i.id"
-              />
+              <el-option label="待审核" :value="1" />
+              <el-option label="已通过" :value="2" />
+              <el-option label="已拒绝" :value="3" />
             </el-select>
           </div>
           <div class="condition">
@@ -72,35 +69,85 @@
       <div class="footer" v-loading="loading">
         <div class="card_all">
           <el-space wrap>
-            <el-card style="max-width: 480px" v-for="o in 16" :key="o">
+            <el-card
+              style="max-width: 480px"
+              v-for="i in tableData.list"
+              :key="i.id"
+            >
               <template #header>
                 <div class="card-header">
-                  <img src="../../assets/img/nanchang.png" alt="" />
+                  <img v-viewer :src="i.image" alt="" />
                   <div class="issuer">
-                    <p>发帖人</p>
-                    <span>更新时间: 2016-12-12 12:30:00</span>
+                    <p>{{ i.userName }}</p>
+                    <span>{{ i.updateTime }}</span>
                   </div>
                 </div>
               </template>
-              <p @click="viewDetails">
-                北国风光,千里冰封,万里雪飘。
-                望长城内外,惟余莽莽;大河上下,顿失滔滔。
-                山舞银蛇,原驰蜡象,欲与天公试比高。
-                须晴日,看红装素裹,分外妖娆。 江山如此多娇,引无数英雄竞折腰。
-                惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。
-                一代天骄,成吉思汗,只识弯弓射大雕。
-                俱往矣,数风流人物,还看今朝。
+              <p @click="viewDetails(i)" class="content">
+                {{ i.content }}
               </p>
+              <div class="dianzan">
+                <div class="icon">
+                  <img src="../../assets/img/pinglun.png" alt="" />
+                  <span>{{ i.commentNum }}</span>
+                </div>
+                <div class="icon">
+                  <img src="../../assets/img/dianzan.png" alt="" />
+                  <span>{{ i.likeNum }}</span>
+                </div>
+              </div>
               <template #footer>
                 <div class="card-footer">
-                  <span>待审批 2016-12-12</span>
-                  <el-dropdown placement="bottom">
+                  <div class="footer_left">
+                    <img
+                      v-if="i.passName == '待审核'"
+                      style="width: 25px; margin-right: 5px"
+                      src="../../assets/img/yuandian.png"
+                      alt=""
+                    />
+                    <img
+                      v-if="i.passName == '已通过'"
+                      style="width: 25px; margin-right: 5px"
+                      src="../../assets/img/yuandian2.png"
+                      alt=""
+                    />
+                    <img
+                      v-if="i.passName == '已拒绝'"
+                      style="width: 25px; margin-right: 5px"
+                      src="../../assets/img/yuandian3.png"
+                      alt=""
+                    />
+                    <span style="margin-right: 20px">{{ i.passName }}</span>
+                    <span>{{ i.passTime }}</span>
+                  </div>
+                  <el-dropdown placement="bottom" :hide-on-click="false">
                     <el-icon :size="20"><MoreFilled /></el-icon>
                     <template #dropdown>
-                      <el-dropdown-menu>
-                        <el-dropdown-item>通过</el-dropdown-item>
-                        <el-dropdown-item>拒绝</el-dropdown-item>
-                        <el-dropdown-item>删除</el-dropdown-item>
+                      <el-dropdown-menu v-if="i.passName == '待审核'">
+                        <el-dropdown-item @click="operationPassChange(i)"
+                          ><span style="color: #0095ff"
+                            >通过</span
+                          ></el-dropdown-item
+                        >
+                        <el-dropdown-item @click="operationRefuseChange(i)"
+                          ><span style="color: #ff8d1a"
+                            >拒绝</span
+                          ></el-dropdown-item
+                        >
+                        <el-dropdown-item @click="deleteS(i)"
+                          ><span style="color: #d43030"
+                            >删除</span
+                          ></el-dropdown-item
+                        >
+                      </el-dropdown-menu>
+                      <el-dropdown-menu
+                        v-if="i.passName == '已通过' || i.passName == '已拒绝'"
+                      >
+                        <el-dropdown-item @click="deleteS(i)"
+                          ><span style="color: #d43030"
+                            >删除</span
+                          ></el-dropdown-item
+                        >
                       </el-dropdown-menu>
                     </template>
                   </el-dropdown>
@@ -148,24 +195,23 @@
         label-position="right"
         status-icon
       >
-        <el-form-item label="组织分类 :" prop="school">
+        <el-form-item label="组织分类 :" prop="categoryId">
           <el-select
-            @change="schoolFormChange"
-            v-model="ruleForm.school"
+            v-model="ruleForm.categoryId"
             clearable
             placeholder="请选择组织分类"
           >
             <el-option
-              v-for="i in schoolData"
+              v-for="i in categoryData"
               :key="i.id"
-              :label="i.school"
+              :label="i.name"
               :value="i.id"
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="内容 :" prop="bedNumber">
+        <el-form-item label="内容 :" prop="content">
           <el-input
-            v-model.trim="ruleForm.bedNumber"
+            v-model.trim="ruleForm.content"
             placeholder="请输入内容"
             clearable
             :autosize="{ minRows: 4 }"
@@ -186,6 +232,54 @@
       </el-form>
     </el-dialog>
 
+    <el-dialog
+      class="addStaff"
+      v-model="operationVisible"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :title="operationTitle"
+      align-center
+      width="540"
+      :before-close="cancelOperation"
+      destroy-on-close
+      draggable
+    >
+      <el-form
+        ref="operationRef"
+        :model="operationRuleForm"
+        :rules="operationRules"
+        label-width="90px"
+        class="demo-ruleForm"
+        :size="formSize"
+        label-position="right"
+        status-icon
+      >
+        <el-form-item
+          label="审批意见 :"
+          :prop="operationTitle == '拒绝审批' ? 'passValue' : ''"
+        >
+          <el-input
+            v-model.trim="operationRuleForm.passValue"
+            placeholder="请输入审批意见"
+            clearable
+            :autosize="{ minRows: 4 }"
+            type="textarea"
+          />
+        </el-form-item>
+        <el-form-item class="options">
+          <el-button @click="cancelOperation">取消</el-button>
+          <el-button
+            color="rgba(0, 97, 255, 1)"
+            class="queding"
+            type="primary"
+            @click="operationSubmitAdd(operationRef)"
+          >
+            确定
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-dialog>
+
     <!-- 详情弹窗 -->
     <el-dialog
       class="details"
@@ -201,23 +295,79 @@
     >
       <div>
         <div class="card-header">
-          <img src="../../assets/img/nanchang.png" alt="" />
+          <img style="border-radius: 50%" :src="detailData.image" alt="" />
           <div class="issuer">
-            <p>发帖人</p>
-            <span>更新时间: 2016-12-12 12:30:00</span>
+            <p>{{ detailData.userName }}</p>
+            <span>{{ detailData.updateTime }}</span>
           </div>
         </div>
-        <p @click="viewDetails">
-          北国风光,千里冰封,万里雪飘。
-          望长城内外,惟余莽莽;大河上下,顿失滔滔。
-          山舞银蛇,原驰蜡象,欲与天公试比高。 须晴日,看红装素裹,分外妖娆。
-          江山如此多娇,引无数英雄竞折腰。
-          惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。
-          一代天骄,成吉思汗,只识弯弓射大雕。 俱往矣,数风流人物,还看今朝。
+        <p style="text-indent: 2em; line-height: 1.8">
+          {{ detailData.content }}
         </p>
-        <div class="card-footer">
-          <p class="comment-count">1条评论数</p>
-          <div class="user">
+        <p class="comment-count">{{ detailData.commentNum }}条评论数</p>
+        <div class="card-footer" v-loading="commentLoading">
+          <div
+            class="user"
+            v-for="(i, index) in commentData"
+            :key="i.id"
+          >
+            <img style="border-radius: 0" :src="i.image" alt="" />
+            <div class="user-info">
+              <div class="upvote">
+                <span class="username">{{ i.commentName }}</span>
+                <div class="num">
+                  <img src="../../assets/img/upvote.png" alt="" />
+                  <span>{{ i.likeNum }}</span>
+                </div>
+              </div>
+              <div class="comment">
+                <p style="margin: 5px 0">{{ i.content }}</p>
+                <span style="font-size: 12px; margin-bottom: 10px">{{
+                  i.createTime
+                }}</span>
+                <div
+                  v-if="i.commentNum > 0 && !i.children"
+                  style="
+                    font-size: 12px;
+                    margin-bottom: 10px;
+                    cursor: pointer;
+                    display: flex;
+                    align-items: center;
+                  "
+                  @click="openComment(i, index)"
+                >
+                  查看全部 {{ i.commentNum }} 回复
+                  <el-icon><ArrowRightBold /></el-icon>
+                </div>
+                <div v-else>
+                  <div
+                    v-loading="commentLoading"
+                    class="user"
+                    v-for="j in i.children"
+                    :key="j.id"
+                  >
+                    <img style="border-radius: 0;margin: 10px 10px 0 0;" :src="j.image" alt="" />
+                    <div class="user-info">
+                      <div class="upvote">
+                        <span class="username">{{ j.commentName }}</span>
+                        <div class="num">
+                          <img src="../../assets/img/upvote.png" alt="" />
+                          <span>{{ j.likeNum }}</span>
+                        </div>
+                      </div>
+                      <div class="comment">
+                        <p style="margin: 5px 0">{{ j.content }}</p>
+                        <span style="font-size: 12px; margin-bottom: 10px">{{
+                          j.createTime
+                        }}</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <!-- <div class="user">
             <img src="../../assets/img/nanchang.png" alt="" />
             <div class="user-info">
               <div class="upvote">
@@ -232,7 +382,7 @@
                 <span>2016-12-12 12:30:00</span>
               </div>
             </div>
-          </div>
+          </div> -->
         </div>
       </div>
     </el-dialog>
@@ -252,9 +402,17 @@ import { useRouter } from "vue-router";
 import { genFileId, ElMessage, ElMessageBox } from "element-plus";
 import { dayjs } from "element-plus";
 import lodash from "lodash";
-import { MoreFilled } from "@element-plus/icons-vue";
+import { MoreFilled, ArrowRightBold } from "@element-plus/icons-vue";
 import { storeToRefs } from "pinia";
 import { useCounterStore } from "@/stores/index";
+import {
+  insertPosts,
+  queryCategoryDatas,
+  toExaminePosts,
+  deletePostsById,
+  queryPostsPage,
+  queryCommetns,
+} from "@/api/alumni-square";
 
 const router = useRouter();
 const store = useCounterStore();
@@ -271,11 +429,9 @@ const activeIndex = ref(); // 默认跳转路由
 const dialongTitle = ref("新增账号"); // 弹窗标题
 
 const searchInput = reactive({
-  name: "",
-  year: null,
-  college: null,
-  major: null,
-  class: null,
+  userName: "",
+  isPass: null,
+  createTime: null,
 }); // 搜索按钮数据
 
 const currentPage = ref(1); // 当前页
@@ -289,29 +445,34 @@ const addDialogVisible = ref(false); // 控制添加账号弹窗
 const formSize = ref("default");
 const ruleFormRef = ref();
 const ruleForm = reactive({
-  school: "靖安校区", //校区名称
-  build: "", //楼栋名称
-  dormitory: "", //寝室号
-  sex: "男", //寝室性别
-  college: "", //所属学院
-  major: "", //所属专业
-  bedNumber: "", //床位数
-  // "gradestr": "",              //所属年级
-  remark: "", //备注
+  categoryId: "",
+  content: "",
   id: "",
 });
 // 表单验证
 const rules = reactive({
-  school: [{ required: true, message: "校区名称不能为空", trigger: "blur" }],
-  build: [{ required: true, message: "楼栋名称不能为空", trigger: "blur" }],
-  dormitory: [{ required: true, message: "寝室号不能为空", trigger: "blur" }],
-  sex: [{ required: true, message: "寝室性别不能为空", trigger: "blur" }],
-  college: [{ required: true, message: "所属学院不能为空", trigger: "blur" }],
-  major: [{ required: true, message: "专业不能为空", trigger: "blur" }],
-  bedNumber: [{ required: true, message: "床位数不能为空", trigger: "blur" }],
+  categoryId: [
+    { required: true, message: "组织分类不能为空", trigger: "blur" },
+  ],
+  content: [{ required: true, message: "内容不能为空", trigger: "blur" }],
 });
 
 const detailsVisible = ref(false); // 详情弹窗
+const detailData = ref();
+const commentData = ref();
+const commentLoading = ref(false);
+
+const operationVisible = ref();
+const operationTitle = ref();
+const operationRef = ref(); // 操作按钮
+const operationRuleForm = reactive({
+  passValue: "",
+  id: "",
+});
+// 表单验证
+const operationRules = reactive({
+  passValue: [{ required: true, message: "审批意见不能为空", trigger: "blur" }],
+});
 
 // 获取账户列表
 const getList = async () => {
@@ -319,27 +480,62 @@ const getList = async () => {
   let params = {
     currentPage: currentPage.value, // 当前页
     pageCount: pageSize.value, // 一页数据条数
-    schoolId: searchInput.schoolId,
-    buildId: searchInput.buildId,
+    userName: searchInput.userName,
+    isPass: searchInput.isPass,
   };
+  if (searchInput.createTime) {
+    params.startTime = searchInput.createTime[0];
+    params.endTime = searchInput.createTime[1];
+  }
+  queryPostsPage(params).then((res) => {
+    console.log(res, "获取帖子分页数据");
+    if (res.code == 200) {
+      tableData.list = res.data.list;
+      total.value = res.data.totalCount;
+      loading.value = false;
+    } else {
+      loading.value = false;
+    }
+  });
+};
+
+const categoryData = ref();
+const categoryList = async () => {
+  let res = await queryCategoryDatas();
+  if (res.code == 200) {
+    categoryData.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
 };
 
 // 搜索功能
-const searchBtn = lodash.debounce(async () => {}, 300);
-const resetBtn = lodash.debounce(async () => {}, 300);
+const searchBtn = lodash.debounce(async () => {
+  currentPage.value = 1;
+  getList();
+}, 300);
+const resetBtn = lodash.debounce(async () => {
+  searchInput.userName = "";
+  searchInput.isPass = null;
+  searchInput.createTime = null;
+  currentPage.value = 1;
+  getList();
+}, 300);
 
 // 添加账号
 const addlist = () => {
   dialongTitle.value = "发布动态";
   addDialogVisible.value = true;
-  ruleForm.school = null;
-};
-// 添加账号
-const updateS = (row) => {
-  console.log(row);
-  dialongTitle.value = "编辑寝室信息";
-  addDialogVisible.value = true;
+  ruleForm.categoryId = "";
+  ruleForm.content = "";
+  ruleForm.id = "";
 };
+
 const deleteS = async (row) => {
   ElMessageBox.confirm("是否删除此数据?", "提示!!!", {
     confirmButtonText: "确认",
@@ -347,29 +543,59 @@ const deleteS = async (row) => {
     type: "warning",
   })
     .then(async () => {
-      loading.value = true;
-      let data = {
-        dormitoryId: row.id, // 当前页
+      let params = {
+        id: row.id,
       };
+      let res = await deletePostsById(params);
+      if (res.code == 200) {
+        getList();
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     })
     .catch(() => {
       loading.value = false;
     });
 };
-// 保留
-const retainS = async (row, flag) => {
-  let data = {
-    dormitoryIds: [row.id], // 当前页
-    retentionState: flag, //保留状态 1:开放,2:保留
-  };
-};
 
 // 确认添加员工
 const submitAdd = lodash.debounce(async (formEl) => {
   if (!formEl) return;
   await formEl.validate(async (valid, fields) => {
     if (valid) {
-      console.log(data);
+      let data = {
+        categoryId: ruleForm.categoryId,
+        content: ruleForm.content,
+      };
+      let res = await insertPosts(data);
+      if (res.code == 200) {
+        addDialogVisible.value = false;
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+        getList();
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
     } else {
       console.log("error submit!", fields);
     }
@@ -381,8 +607,103 @@ const cancelAdd = () => {
 };
 
 // 查看详情
-const viewDetails = () => {
+const viewDetails = async (i) => {
+  console.log(i, "查看详情");
+  commentLoading.value = true;
   detailsVisible.value = true;
+  detailData.value = i;
+  let params = {
+    postsId: i.id,
+    commentId: 0,
+  };
+  let res = await queryCommetns(params);
+  if (res.code == 200) {
+    commentLoading.value = false;
+    console.log(res, "评论");
+    commentData.value = res.data;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+// 查看更多回复
+const openComment = async (i, index) => {
+  commentLoading.value = true;
+  let params = {
+    postsId: i.id,
+    commentId: i.id,
+  };
+  let res = await queryCommetns(params);
+  if (res.code == 200) {
+    // console.log(res, "回复评论");
+    commentLoading.value = false;
+    commentData.value[index].children = res.data;
+    console.log(commentData.value, "全部评论");
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+// 通过
+const operationPassChange = async (row) => {
+  operationTitle.value = "通过审批";
+  operationVisible.value = true;
+  operationRuleForm.passValue = null;
+  operationRuleForm.id = row.id;
+};
+// 拒绝
+const operationRefuseChange = async (row) => {
+  operationTitle.value = "拒绝审批";
+  operationVisible.value = true;
+  operationRuleForm.passValue = null;
+  operationRuleForm.id = row.id;
+};
+
+// 确认审批
+const operationSubmitAdd = lodash.debounce(async (formEl) => {
+  if (!formEl) return;
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      let data = {
+        id: operationRuleForm.id,
+        isPass: operationTitle.value == "通过审批" ? 2 : 3,
+        passValue: operationRuleForm.passValue,
+      };
+      let res = await toExaminePosts(data);
+      if (res.code == 200) {
+        operationVisible.value = false;
+        ElMessage({
+          type: "success",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+        getList();
+      } else {
+        ElMessage({
+          type: "error",
+          showClose: true,
+          message: res.message,
+          center: true,
+        });
+      }
+    } else {
+      console.log("error submit!", fields);
+    }
+  });
+}, 1000);
+
+const cancelOperation = () => {
+  operationVisible.value = false;
 };
 
 // 每页显示条数
@@ -397,8 +718,8 @@ const handleCurrentChange = (value) => {
 };
 
 onBeforeMount(() => {
-  //   getList();
-  //   schoolList();
+  getList();
+  categoryList();
   //   collegeList();
 });
 onUnmounted(() => {
@@ -504,6 +825,7 @@ onUnmounted(() => {
               width: 50px;
               height: 50px;
               margin-right: 20px;
+              border-radius: 50%;
             }
             .issuer {
               p {
@@ -518,9 +840,39 @@ onUnmounted(() => {
           }
         }
         :deep(.el-card__body) {
-          cursor: pointer;
           padding: 0 20px 10px;
           font-size: 13px;
+          .content {
+            height: 78px;
+            overflow: hidden;
+            display: -webkit-box;
+            -webkit-line-clamp: 3;
+            -webkit-box-orient: vertical;
+            line-height: 1.6;
+            cursor: pointer;
+            font-size: 16px;
+          }
+          .dianzan {
+            margin: 0;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .icon {
+              margin: 0 15px;
+              display: flex;
+              align-items: center;
+              justify-content: space-around;
+              img {
+                // cursor: pointer;
+                width: 18px;
+                height: 18px;
+                margin-right: 8px;
+              }
+              span {
+                font-size: 16px;
+              }
+            }
+          }
         }
         :deep(.el-card__footer) {
           padding: 10px 20px;
@@ -534,6 +886,10 @@ onUnmounted(() => {
               outline: 2px solid rgba(38, 151, 255, 0.5) !important;
               // outline-offset: 2px;
             }
+            .footer_left {
+              display: flex;
+              align-items: center;
+            }
           }
         }
       }
@@ -656,7 +1012,16 @@ onUnmounted(() => {
         }
       }
     }
+    .comment-count {
+        padding: 10px ;
+        margin: 0;
+        font-weight: 800;
+        background-color: #f5f7fa;
+        border-bottom: 0.5px solid #e0e0e0;
+      }
     .card-footer {
+      max-height: 450px;
+      overflow: auto;
       background-color: #f5f7fa;
       min-height: 100px;
       margin-bottom: 20px;
@@ -671,18 +1036,20 @@ onUnmounted(() => {
         img {
           width: 40px;
           height: 40px;
-          margin: 0 20px;
+          margin: 10px 15px;
         }
         .user-info {
           flex: 1;
-          margin-bottom: 10px;
+          // margin-bottom: 10px;
           .upvote {
             display: flex;
             justify-content: space-between;
             align-items: center;
-            margin: 8px 0;
-            .username{
+            margin: 8px 0 0;
+            .username {
               font-weight: 800;
+              // color: #2A82E4;
+              // color: #aaabad;
             }
             img {
               width: 19px;
@@ -695,9 +1062,37 @@ onUnmounted(() => {
               margin-right: 20px;
             }
           }
+          .comment {
+            display: flex;
+            flex-direction: column;
+          }
         }
       }
     }
+    /* WebKit 内核浏览器(Chrome、Edge、Safari)的滚动条样式 */
+    .card-footer::-webkit-scrollbar {
+      /* 滚动条宽度 */
+      width: 6px;
+      height: 6px;
+    }
+
+    /* 滚动条轨道 */
+    .card-footer::-webkit-scrollbar-track {
+      background: #f5f7fa;
+      border-radius: 3px;
+    }
+
+    /* 滚动条滑块 */
+    .card-footer::-webkit-scrollbar-thumb {
+      background: #c0c4cc;
+      border-radius: 3px;
+      /* 鼠标悬停时滑块变色 */
+      transition: background 0.3s;
+    }
+
+    .card-footer::-webkit-scrollbar-thumb:hover {
+      background: #909399;
+    }
     .el-textarea {
       width: 400px;
     }

+ 4 - 0
src/views/permission/Role.vue

@@ -223,6 +223,7 @@
                 <div>
                   <span>组织城市 : </span>
                   <el-select
+                  clearable
                     v-model="i.provinces"
                     placeholder="请选择省"
                     style="width: 140px"
@@ -237,6 +238,7 @@
                   </el-select>
                   <span> - </span>
                   <el-select
+                  clearable
                     v-model="i.citys"
                     placeholder="请选择市"
                     style="width: 140px"
@@ -436,6 +438,7 @@
                 <div>
                   <span>组织城市 : </span>
                   <el-select
+                  clearable
                     v-model="i.provinces"
                     placeholder="请选择省"
                     style="width: 140px"
@@ -450,6 +453,7 @@
                   </el-select>
                   <span> - </span>
                   <el-select
+                  clearable
                     v-model="i.citys"
                     placeholder="请选择市"
                     style="width: 140px"

+ 451 - 0
src/views/system-setting/cropper.vue

@@ -0,0 +1,451 @@
+<template>
+	<div>
+		<el-dialog v-model="dialogVisible" title="剪裁图片" width="1000">
+			<!-- 裁剪框 -->
+			<div class="cropper">
+				<img ref="cropimg" :src="imgSrc" alt />
+			</div>
+			<div class="cutter-tool">
+				<el-button
+					icon="ZoomIn"
+					class="tool-but"
+					style="border-radius: 5px 0 0 5px !important; border-right: none"
+					@click="scale(1)" />
+				<el-button icon="ZoomOut" class="tool-but" @click="scale(-1)" />
+				<el-button icon="ArrowLeft" class="tool-but" @click="moveImg(0)" />
+				<el-button icon="ArrowRight" class="tool-but" @click="moveImg(1)" />
+				<el-button icon="ArrowUp" class="tool-but" @click="moveImg(2)" />
+				<el-button icon="ArrowDown" class="tool-but" @click="moveImg(3)" />
+				<el-button icon="Sort" class="tool-but rotate" @click="flipHorizontal" />
+				<el-button icon="Sort" class="tool-but" @click="flipVertically" />
+				<el-button icon="RefreshRight" class="tool-but" @click="turnImg(90)" />
+				<el-button
+					icon="RefreshLeft"
+					style="border-radius: 0 !important; border-radius: 0 5px 5px 0 !important; margin-left: 0"
+					@click="turnImg(-90)" />
+			</div>
+			<div style="width: calc(45px * 10); margin: 20px auto">
+				<el-slider
+					v-model="rotationAngle"
+					show-input
+					:min="-180"
+					:max="180"
+					:marks="marks"
+					@input="sliderInput" />
+			</div>
+ 
+			<template #footer>
+				<div class="dialog-footer">
+					<el-tag type="primary">原图 {{ quality }}</el-tag>
+					<div style="display: flex">
+						<div style="display: flex; align-items: center">
+							<span style="font-weight: bold">品质</span>
+							<div style="cursor: pointer; margin: 0 5px; display: flex; align-items: center">
+								<el-tooltip
+									class="box-item"
+									effect="dark"
+									content="调低该值可缩小图片体积 调高该值可提升清晰度"
+									placement="top">
+									<el-icon><QuestionFilled /></el-icon>
+								</el-tooltip>
+							</div>
+							<div style="width: 100px; margin: 0 20px">
+								<el-slider v-model="imageQuality" :min="0" :max="10" :format-tooltip="formatTooltip" />
+							</div>
+						</div>
+						<el-button type="info" @click="reset">重置</el-button>
+						<el-button @click="close">取消</el-button>
+						<el-button type="primary" @click="cropImage"> 确定 </el-button>
+					</div>
+				</div>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+ 
+<script setup>
+import { ref, nextTick, watch } from 'vue';
+import Cropper from 'cropperjs';
+import 'cropperjs/dist/cropper.css';
+ 
+//给父组件传递方法
+const emit = defineEmits(['cropImage', 'closeImage']);
+ 
+const props = defineProps({
+	corImg: {
+		type: [Object, String],
+		default: {},
+	},
+	// 裁剪框的宽高比
+	aspectRatio: {
+		type: String,
+		default: '',
+	},
+	// 图片格式
+	imgFormat: {
+		type: String,
+		default: 'jpeg',
+	},
+});
+const imgSrc = ref('');
+const aspectRatio = ref(NaN);
+const imgFormat = ref('image/png');
+watch(
+	() => props.aspectRatio,
+	(newVal) => {
+		if (newVal == '') {
+			aspectRatio.value = NaN;
+		} else {
+			aspectRatio.value = Number(newVal.split(':')[0]) / Number(newVal.split(':')[1]);
+		}
+		console.log(aspectRatio.value);
+	},
+	{
+		immediate: true,
+		deep: true,
+	},
+);
+const imgFormatMap = {
+	jpeg: 'image/jpeg',
+	png: 'image/png',
+	webp: 'image/webp',
+	'': 'image/jpeg', // 默认值
+};
+watch(
+	() => props.imgFormat,
+	(newVal) => {
+		imgFormat.value = imgFormatMap[newVal] || 'image/jpeg';
+	},
+	{
+		immediate: true,
+		deep: true,
+	},
+);
+// 裁剪框是否显示
+const dialogVisible = ref(false);
+// 图片元素引用
+const cropimg = ref(null);
+// 裁剪实例引用
+const cropper = ref(null);
+const rotationAngle = ref(0);
+const quality = ref('');
+// 压缩图片的品质值
+const imageQuality = ref(10);
+const marks = ref({
+	'-180': '-180°',
+	0: '0°',
+	180: '180°',
+});
+/**
+ * 将传入的值除以10并返回结果
+ *
+ * @param val 需要被除的值
+ * @returns 返回除以10后的结果
+ */
+const formatTooltip = (val) => {
+	return val / 10;
+};
+ 
+/**
+ * 关闭对话框
+ *
+ * @description 关闭对话框,并将对话框的显示状态设置为false,同时触发'closeImage'事件并传递null作为参数
+ */
+const close = () => {
+	dialogVisible.value = false;
+	emit('closeImage', props.corImg);
+};
+watch(
+	() => props.corImg,
+	(newVal) => {
+		if (newVal) {
+			dialogVisible.value = true;
+			setTimeout(() => {
+				// 判断newVal是字符串还是对象,如果是字符串则直接赋值给imgSrc.value
+				if (typeof newVal === 'string' && cropper.value) {
+					cropper.value.replace(newVal);
+				} else {
+					if (cropper.value && newVal.url) {
+						cropper.value.replace(newVal.url);
+						getQuality(newVal.size);
+					}
+				}
+			}, 500);
+		}
+	},
+	{
+		immediate: true,
+	},
+);
+ 
+watch(
+	() => dialogVisible.value,
+	(newVal) => {
+		if (!newVal) {
+			cropper.value?.destroy();
+			imageQuality.value = 10;
+			rotationAngle.value = 0;
+		} else {
+			nextTick(() => {
+				getCropper();
+			});
+		}
+	},
+	{
+		immediate: true,
+	},
+);
+ 
+/**
+ * 根据文件大小计算文件质量,并以合适的单位显示
+ *
+ * @param {number} size - 文件大小(以字节为单位)
+ * @returns {void}
+ */
+const getQuality = (size) => {
+	quality.value = size / 1024;
+	if (quality.value >= 1024) {
+		// 超过1M
+		quality.value = (quality.value / 1024).toFixed(2) + 'M';
+	} else if (quality.value >= 1) {
+		// 1K到1M之间
+		quality.value = quality.value.toFixed(2) + 'K';
+	} else {
+		// 小于1K
+		quality.value = (quality.value * 1024).toFixed(2) + 'B'; // 可以选择转换为字节,或者保持小数形式
+	}
+};
+ 
+/**
+ * 获取裁剪器实例
+ *
+ * @returns 裁剪器实例
+ */
+const getCropper = () => {
+	cropper.value = new Cropper(cropimg.value, {
+		/*
+			定义裁剪器的拖动模式
+				move: 移动图片
+				crop: 创建一个新的裁剪框
+				none: 什么也不做
+		*/
+		dragMode: 'move',
+		aspectRatio: aspectRatio.value, // 设置裁剪框的宽高比。默认值是 NaN,意味着比例自由
+		initialAspectRatio: NaN, // 定义裁剪框的初始宽高比。默认和图片容器的宽高比相同。只有在 aspectRatio 的值为 NaN时才可以设置
+		data: null, // 之前存储的裁剪后的数据,将在初始化时传递给setData方法 只有在 autoCrop 的值为 true时可用
+		preview: '', // 添加额外的元素(容器)以供预览
+		responsive: true, // 在窗口大小变化后,重新渲染裁剪器
+		restore: false, // 在窗口大小变化后,恢复被裁剪的区域
+		checkCrossOrigin: false, // 检查当前图片是否为跨域图片
+		checkOrientation: true, // 检查当前图片的 EXIF 信息,并根据相关信息旋转图片
+		modal: true, // 是否在图片和裁剪框之间显示黑色蒙版。
+		guides: true, // 是否显示裁剪框的辅助线
+		center: true, // 是否显示裁剪框中心的指示器。
+		highlight: true, // 是否显示裁剪框上面的白色蒙版(突出显示裁剪框)。
+		background: true, // 是否显示裁剪框的背景。
+		autoCrop: true, // 是否在初始化时自动裁剪图片。默认值为 true
+		autoCropArea: 1, // 设置裁剪框的初始大小。默认值为 auto,即自动调整为图片的 80%
+		movable: true, // 是否可以移动图片。默认为 true,即允许移动
+		rotatable: true, // 是否可以旋转图片。默认为 true,即允许旋转
+		scalable: true, // 是否可以缩放图片(以图片中心点为原点进行缩放)。
+		zoomable: true, // 是否可以缩放图片(以图片左上角为原点进行缩放)。
+		zoomOnTouch: true, // 是否允许通过拖动来缩放图片。
+		zoomOnWheel: true, // 是否可以通过鼠标滚轮来缩放图片。默认为 true,即允许通过鼠标滚轮进行缩放
+		wheelZoomRatio: 0.1, // 鼠标滚轮缩放图片的比例。默认为 0.1,即每次滚动时缩小或放大 10%
+		cropBoxMovable: true, // 是否可以移动裁剪框。默认为 true,即允许移动
+		cropBoxResizable: true, // 是否可以改变裁剪框的大小。默认为 true,即允许改变大小
+		toggleDragModeOnDblclick: false, // 当点击两次时可以在“crop”和“move”之间切换拖拽模式 需要浏览器支持 dblclick 事件。
+		/*
+			裁剪框的视图模式:
+				0 无限制
+				1 限制裁剪框不能超出图片的范围
+				2 限制裁剪框不能超出图片的范围且图片填充模式为 cover 最长边填充
+				3 限制裁剪框不能超出图片的范围 且图片填充模式为 contain 最短边填充
+		*/
+		viewMode: 0,
+		minContainerWidth: 200, // 设置裁剪器容器的最小宽度。默认为 200px
+		minContainerHeight: 100, // 设置裁剪器容器的最小高度。默认为 150px
+		minCanvasWidth: 0, // 设置图片容器的最小宽度。默认为 0,即不限制
+		minCanvasHeight: 0, // 设置图片容器的最小高度。默认为 0,即不限制
+		minCropBoxWidth: 0, // 设置裁剪框的最小宽度。默认为 0  注: 这个尺寸是相对于页面的,而不是图片。
+		minCropBoxHeight: 0, // 设置裁剪框的最小高度。默认为 0  注: 这个尺寸是相对于页面的,而不是图片。
+		ready: ready(), // ready 事件的快捷写法。 在初始化完成后触发。
+		cropstart: cropstart(), // cropstart 事件的快捷写法。 在开始裁剪时触发。
+		cropmove: null, // cropmove 事件的快捷写法。 在裁剪时触发。
+		cropend: null, // cropend 事件的快捷写法。 在结束裁剪时触发。
+		crop: null, // crop 事件的快捷写法。 在完成裁剪后触发。
+		zoom: null, // zoom 事件的快捷写法。 当图片缩放时触发。
+	});
+};
+ 
+const ready = (env) => {
+	// console.log('初始化', env);
+};
+const cropstart = (env) => {
+	// console.log('开始裁剪', env);
+};
+ 
+/**
+ * 根据传入的参数调整图片缩放比例
+ *
+ * @param env 缩放方向参数,大于0表示放大,小于等于0表示缩小
+ */
+const scale = (env) => {
+	if (env > 0) {
+		// 放大图片
+		cropper.value.zoom(0.1);
+	} else {
+		// 缩小图片
+		cropper.value.zoom(-0.1);
+	}
+};
+ 
+/**
+ * 移动图片位置
+ *
+ * @param env 环境变量,决定图片移动的方向
+ *             0: 向左移动
+ *             1: 向右移动
+ *             2: 向上移动
+ *             3: 向下移动
+ */
+const moveImg = (env) => {
+	if (env == 0) {
+		// 向左移动图片
+		cropper.value.move(-10, 0);
+	} else if (env == 1) {
+		// 向右移动图片
+		cropper.value.move(10, 0);
+	} else if (env == 2) {
+		// 向上移动图片
+		cropper.value.move(0, -10);
+	} else if (env == 3) {
+		// 向下移动图片
+		cropper.value.move(0, 10);
+	}
+};
+ 
+const scaleX = ref(1); // 水平翻转的缩放值
+const scaleY = ref(1); // 垂直翻转的缩放值
+/**
+ * 水平翻转图片
+ */
+const flipHorizontal = () => {
+	if (scaleX.value == 1) {
+		scaleX.value = -1;
+	} else {
+		scaleX.value = 1;
+	}
+	cropper.value.scale(scaleX.value, scaleY.value);
+};
+ 
+/**
+ * 垂直翻转函数
+ *
+ */
+const flipVertically = () => {
+	if (scaleY.value == 1) {
+		scaleY.value = -1;
+	} else {
+		scaleY.value = 1;
+	}
+	cropper.value.scale(scaleX.value, scaleY.value);
+};
+ 
+/**
+ * 根据环境变量旋转图片
+ *
+ * @param {Object} env - 包含旋转角度的环境变量对象
+ * @returns {void}
+ */
+const turnImg = (env) => {
+	cropper.value.rotate(env);
+};
+ 
+const sliderInput = (val) => {
+	cropper.value.rotateTo(Number(val));
+};
+const reset = () => {
+	cropper.value.reset();
+};
+ 
+const cropImage = () => {
+	const canvas = cropper.value.getCroppedCanvas(); // 获取裁剪后的canvas元素
+	// 减少图片体积,压缩图片 0-1之间,1为原始大小
+	canvas.toBlob(
+		(blob) => {
+			const url = URL.createObjectURL(blob); // 创建临时url地址
+			const file = new File([blob], props.corImg.name || 'croppedImage.jpg', {
+				type: blob.type,
+				lastModified: Date.now(),
+			});
+			const data = {
+				name: file.name,
+				size: file.size,
+				raw: file,
+				url: url,
+				uid: props.corImg.uid,
+			};
+			dialogVisible.value = false;
+			emit('cropImage', data);
+		},
+		imgFormat.value,
+		imageQuality.value / 10, // 动态调整压缩质量
+	);
+};
+ 
+/**
+ * 将Data URL转换为Blob对象
+ *
+ * @param dataurl Data URL字符串
+ * @returns 返回转换后的Blob对象
+ */
+const dataURLtoBlob = (dataurl) => {
+	var arr = dataurl.split(','),
+		mime = arr[0].match(/:(.*?);/)[1],
+		bstr = atob(arr[1]),
+		n = bstr.length,
+		u8arr = new Uint8Array(n);
+	while (n--) {
+		u8arr[n] = bstr.charCodeAt(n);
+	}
+	return new Blob([u8arr], { type: mime });
+};
+</script>
+ 
+<style lang="scss" scoped>
+.cropper {
+	width: 100%;
+	height: 40vh;
+ 
+	img {
+		max-height: 100%;
+	}
+}
+ 
+::v-deep .i-dialog-footer {
+	display: none !important;
+}
+ 
+.cutter-tool {
+	display: flex;
+	justify-content: center;
+	margin-top: 30px;
+	.tool-but {
+		margin-left: 0;
+		border-radius: 0 !important;
+		border-right: none;
+	}
+	::v-deep .rotate {
+		.el-icon {
+			transform: rotate(90deg);
+		}
+	}
+}
+.dialog-footer {
+	width: 100%;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+</style>

+ 307 - 0
src/views/system-setting/system-setting copy.vue

@@ -0,0 +1,307 @@
+<template>
+  <div class="content-box">
+    <div class="left">
+      <!-- <el-icon :size="23" class="camera"><VideoCameraFilled /></el-icon> -->
+      <span class="cameratxt">系统设置</span>
+    </div>
+    <div class="scroll">
+      <h4>小程序首页封面轮播图设置</h4>
+      <p>可上传多张图片</p>
+      <div class="img-upload-wrap">
+        <!-- 2. 自己循环纯链接数组fileList,展示图片(核心:自定义渲染) -->
+        <div class="img-preview-list" v-if="fileList.length > 0">
+          <div class="img-item" v-for="(url, index) in fileList" :key="index">
+            <!-- 替换原生img为el-image,新增preview相关属性 -->
+            <img
+              :src="url"
+              alt="图片预览"
+              class="preview-img"
+              @click="handlePreview(fileList, index)"
+            />
+            <span
+              v-if="!disabled"
+              class="delete-btn"
+              @click="handleDelete(index)"
+            >
+              <el-icon><Delete /></el-icon>
+            </span>
+          </div>
+          <el-upload
+            v-if="!disabled"
+            action="#"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :before-upload="beforeUpload"
+            class="upload-btn"
+            accept="image/*"
+          >
+            <el-icon><Plus /></el-icon>
+            <span class="upload-txt">点击上传图片</span>
+          </el-upload>
+        </div>
+      </div>
+
+      <h4>联系我们设置</h4>
+      <p>
+        <span>联系地址 : </span>
+        <el-input
+          :disabled="disabled"
+          clearable
+          v-model.trim="address"
+          class="w-50 m-2"
+          placeholder="请输入联系地址"
+          style="width: 220px; margin-left: 15px"
+        />
+      </p>
+      <p>
+        <span>联系邮箱 : </span>
+        <el-input
+          :disabled="disabled"
+          clearable
+          v-model.trim="email"
+          class="w-50 m-2"
+          placeholder="请输入联系邮箱"
+          style="width: 220px; margin-left: 15px"
+        />
+      </p>
+      <p>
+        <span>联系电话 : </span>
+        <el-input
+          :disabled="disabled"
+          clearable
+          v-model.trim="phone"
+          class="w-50 m-2"
+          placeholder="请输入联系电话"
+          style="width: 220px; margin-left: 15px"
+        />
+      </p>
+      <p>
+        <el-button v-if="disabled" @click="edit" type="primary">编辑</el-button>
+        <el-button v-if="!disabled" @click="confirm" type="primary"
+          >确认</el-button
+        >
+        <el-button v-if="!disabled" @click="cancel">取消</el-button>
+      </p>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import {
+  ref,
+  watch,
+  reactive,
+  nextTick,
+  onBeforeMount,
+  onUnmounted,
+} from "vue";
+import { useRouter } from "vue-router";
+import { genFileId, ElMessage, ElMessageBox } from "element-plus";
+import { dayjs } from "element-plus";
+
+import { storeToRefs } from "pinia";
+import { useCounterStore } from "@/stores/index";
+
+import { api as viewerApi } from "v-viewer";
+
+import { getQueryAlumniSystem, updateAlumniSystem } from "@/api/system.js";
+import { uploadFile } from "@/api/uploadFile.js";
+
+const router = useRouter();
+const store = useCounterStore();
+
+// 为避免解构时失去响应性
+import { Delete, Download, Plus, ZoomIn } from "@element-plus/icons-vue";
+
+const { name, age, isCollapse, realAge, collegeRole } = storeToRefs(store);
+
+const id = ref("");
+const address = ref("");
+const email = ref("");
+const phone = ref("");
+const fileList = ref([]);
+
+const disabled = ref(true);
+
+const getList = async () => {
+  let res = await getQueryAlumniSystem();
+  console.log(res);
+
+  if (res.code == 200) {
+    address.value = res.data.address;
+    email.value = res.data.email;
+    id.value = res.data.id;
+    phone.value = res.data.phone;
+    fileList.value = res.data.files;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+const handleDelete = (index) => {
+  fileList.value.splice(index, 1);
+};
+
+const handlePreview = (images, index) => {
+  // 核心:复制数组,把点击的图片移到第一位
+  const newImages = [...images]; // 拷贝原数组,避免修改原数据
+  const currentImg = newImages.splice(index, 1)[0]; // 取出当前点击的图片
+  newImages.unshift(currentImg); // 放到数组开头
+
+  // 传给预览器,此时插件显示的第一张就是点击的图片
+  viewerApi({
+    images: newImages,
+    zIndex: 3000,
+    initial: index // 添加这一行
+  });
+};
+
+// 上传前的校验
+const beforeUpload = (file) => {
+  // 校验文件类型
+  const isImage = file.type.startsWith("image/");
+  if (!isImage) {
+    ElMessage.error("只能上传图片格式文件");
+    return false;
+  }
+  // 校验文件大小(2MB)
+  const isLt2M = file.size / 1024 / 1024 < 2;
+  if (!isLt2M) {
+    ElMessage.error("图片大小不能超过2MB");
+    return false;
+  }
+  return true;
+};
+
+// 文件选择变化时触发
+const handleFileChange = async (file, newFileList) => {
+  console.log(file);
+  let formData = new FormData();
+  formData.append("file", file.raw);
+  let res = await uploadFile(formData);
+  // console.log(res.data.fileUrl);
+  if (res.code == 200) {
+    fileList.value.push(res.data.fileUrl); // 赋值给响应式fileList,页面自动刷新
+    // ElMessage({
+    //   type: "success",
+    //   showClose: true,
+    //   message: res.message,
+    //   center: true,
+    // });
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+
+const edit = () => {
+  disabled.value = false;
+};
+const confirm = async () => {
+  let res = await updateAlumniSystem({
+    id: id.value,
+    address: address.value,
+    email: email.value,
+    phone: phone.value,
+    files: fileList.value,
+  });
+  console.log(res);
+  if (res.code == 200) {
+    getList();
+    ElMessage({
+      type: "success",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+    disabled.value = true;
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
+const cancel = () => {
+  disabled.value = true;
+  getList();
+};
+
+onBeforeMount(() => {
+  getList();
+});
+onUnmounted(() => {
+  // document.removeEventListener("keyup", Enters);
+});
+</script>
+
+<style scoped lang="scss">
+.content-box {
+  width: calc(100% - 40px);
+  height: calc(100% - 105px);
+  margin: 20px auto;
+  background-color: #fff;
+  color: #000;
+  display: flex;
+  flex-direction: column;
+
+  .svg {
+    width: 22px;
+    height: 22px;
+  }
+
+  .left {
+    width: calc(100% - 60px);
+    height: 60px;
+    margin: 0 auto;
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid #ccc;
+    color: #000;
+    font-size: 18px;
+    font-weight: 600;
+    .camera {
+      margin-right: 15px;
+      color: #4392f7;
+    }
+  }
+  .scroll {
+    width: calc(100% - 60px);
+    height: calc(100% - 61px);
+    margin: 0 auto;
+    // display: flex;
+    // flex-direction: column;
+    overflow: auto;
+    h4 {
+      margin: 20px 0;
+      font-size: 18px;
+    }
+    p {
+      margin: 0 0 15px 0;
+    }
+    .img-upload-wrap {
+      width: 100%;
+      padding: 0 10px 0;
+    }
+    /* 上传按钮样式:自定义,可根据需求修改 */
+    :deep(.el-upload) {
+      width: 140px;
+      height: 140px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+}
+</style>

+ 26 - 5
src/views/system-setting/system-setting.vue

@@ -26,7 +26,8 @@
               <el-icon><Delete /></el-icon>
             </span>
           </div>
-          <el-upload
+          <ImgCutter v-if="!disabled" @getUrl="getUrl" />
+          <!-- <el-upload
             v-if="!disabled"
             action="#"
             :auto-upload="false"
@@ -37,7 +38,7 @@
           >
             <el-icon><Plus /></el-icon>
             <span class="upload-txt">点击上传图片</span>
-          </el-upload>
+          </el-upload> -->
         </div>
       </div>
 
@@ -107,6 +108,8 @@ import { api as viewerApi } from "v-viewer";
 import { getQueryAlumniSystem, updateAlumniSystem } from "@/api/system.js";
 import { uploadFile } from "@/api/uploadFile.js";
 
+import ImgCutter from "@/components/ImgCutter.vue";
+
 const router = useRouter();
 const store = useCounterStore();
 
@@ -157,7 +160,7 @@ const handlePreview = (images, index) => {
   viewerApi({
     images: newImages,
     zIndex: 3000,
-    initial: index // 添加这一行
+    initial: index, // 添加这一行
   });
 };
 
@@ -170,9 +173,9 @@ const beforeUpload = (file) => {
     return false;
   }
   // 校验文件大小(2MB)
-  const isLt2M = file.size / 1024 / 1024 < 2;
+  const isLt2M = file.size / 1024 / 1024 < 4;
   if (!isLt2M) {
-    ElMessage.error("图片大小不能超过2MB");
+    ElMessage.error("图片大小不能超过4MB");
     return false;
   }
   return true;
@@ -202,6 +205,24 @@ const handleFileChange = async (file, newFileList) => {
     });
   }
 };
+const getUrl = (res) => {
+  if (res.code == 200) {
+    fileList.value.push(res.data.fileUrl); // 赋值给响应式fileList,页面自动刷新
+    // ElMessage({
+    //   type: "success",
+    //   showClose: true,
+    //   message: res.message,
+    //   center: true,
+    // });
+  } else {
+    ElMessage({
+      type: "error",
+      showClose: true,
+      message: res.message,
+      center: true,
+    });
+  }
+};
 
 const edit = () => {
   disabled.value = false;