Kaynağa Gözat

first commit

jinx 1 ay önce
işleme
8c4112933a

+ 1 - 0
.env.development

@@ -0,0 +1 @@
+VITE_BASE_URL=https://sit-zhongyuanbwg.4dage.com/

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VITE_BASE_URL=/

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+/public/static/tiles
+*.tsbuildinfo

+ 2 - 0
.npmrc

@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com/
+@dage:registry=http://192.168.20.245:4873/

+ 12 - 0
README.md

@@ -0,0 +1,12 @@
+ZHS2504131-1	湖南自然资源厅-文旅数字化合作项目
+## 线上地址
+https://hn3dreal.org.cn/ysyy/index.html 
+
+气味王国api
+https://github.com/Scentrealm/API/blob/main/wifi.md
+
+## 气味测试地址
+https://houseoss.4dkankan.com/project/qwwgTest/index.html#/test
+
+## 云上衡阳地址(已下线)
+https://hengyang.higozj.com/h5/#/

BIN
favicon.ico


+ 17 - 0
index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="UTF-8" />
+    <!-- <link rel="icon" href="/favicon.ico" /> -->
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
+    />
+    <link rel="shortcut icon" href="./static/favicon.ico" />
+    <title>中原突围纪念馆管理后台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 8 - 0
jsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 5604 - 0
package-lock.json


+ 36 - 0
package.json

@@ -0,0 +1,36 @@
+{
+  "name": "zytw-admin",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
+    "@ant-design/icons-vue": "^7.0.1",
+    "@dage/service": "^1.0.6",
+    "animate.css": "^4.1.1",
+    "ant-design-vue": "^4.2.6",
+    "axios": "^1.10.0",
+    "http-server": "^14.1.1",
+    "lib-flexible": "^0.3.2",
+    "lodash": "^4.17.21",
+    "pinia": "^3.0.1",
+    "swiper": "^5.4.5",
+    "tslib": "^2.8.1",
+    "viewerjs": "^1.11.7",
+    "vue": "^3.5.13",
+    "vue-router": "^4.5.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.2.3",
+    "autoprefixer": "^10.4.21",
+    "postcss-px-to-viewport": "^1.1.1",
+    "sass": "^1.86.3",
+    "vite": "^6.2.4",
+    "vite-plugin-vue-devtools": "^7.7.2"
+  }
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 3296 - 0
pnpm-lock.yaml


+ 22 - 0
postcss.config.js

@@ -0,0 +1,22 @@
+export default {
+  plugins: {
+    autoprefixer: {},
+    // "postcss-px-to-viewport": {
+    //   unitToConvert: "px", // 需要转换的单位,默认为"px"
+    //   viewportWidth: 750, // 设计稿的视口宽度
+    //   unitPrecision: 5, // 单位转换后保留的精度
+    //   propList: ["*"], // 能转化为vw的属性列表
+    //   viewportUnit: "vw", // 希望使用的视口单位
+    //   fontViewportUnit: "vw", // 字体使用的视口单位
+    //   selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
+    //   minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
+    //   mediaQuery: true, // 媒体查询里的单位是否需要转换单位
+    //   replace: true, //  是否直接更换属性值,而不添加备用属性
+    //   exclude: /node_modules/i, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
+    //   include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
+    //   landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
+    //   landscapeUnit: "vw", // 横屏时使用的单位
+    //   landscapeWidth: 750, // 横屏时使用的视口宽度
+    // },
+  },
+};

BIN
public/static/favicon.ico


+ 81 - 0
src/App.vue

@@ -0,0 +1,81 @@
+<template>
+  <template v-if="routerName && routerName == 'login'">
+    <RouterView />
+  </template>
+  <a-layout
+    style="min-height: 100vh"
+    v-if="routerName && routerName != 'login'"
+  >
+    <a-layout-sider :style="siderStyle"><Menu /></a-layout-sider>
+    <a-layout>
+      <a-layout-header :style="headerStyle"><Header /></a-layout-header>
+      <a-layout-content class="layout-content" :style="contentStyle">
+        <a-config-provider
+          :locale="zhCN"
+          :theme="{ token: { colorPrimary: '#AE1F0A' } }"
+        >
+          <RouterView /></a-config-provider
+      ></a-layout-content>
+      <!-- <a-layout-footer :style="footerStyle">Footer</a-layout-footer> -->
+    </a-layout>
+  </a-layout>
+</template>
+<script setup>
+import { RouterView } from "vue-router";
+import { nextTick, onMounted, ref, watch, computed } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { getUserInfo } from "@/api";
+import zhCN from "ant-design-vue/es/locale/zh_CN";
+import Menu from "@/components/Menu/index.vue";
+import Header from "@/components/Header/index.vue";
+const route = useRoute();
+import { useStore } from "@/stores";
+const store = useStore();
+// let routerName = store.routerName;
+const routerName = computed(() => store.routerName);
+
+const userInfo = computed(() => {
+  return store.userInfo;
+});
+
+import router from "@/router";
+onMounted(() => {});
+const headerStyle = {
+  textAlign: "center",
+  color: "#fff",
+  height: 64,
+  paddingInline: 50,
+  lineHeight: "64px",
+  backgroundColor: "#fff",
+};
+const contentStyle = {
+  textAlign: "center",
+  minHeight: 120,
+  lineHeight: "120px",
+  color: "#fff",
+  // backgroundColor: "#108ee9",
+};
+const siderStyle = {
+  textAlign: "center",
+  lineHeight: "120px",
+  color: "#fff",
+  backgroundColor: "#BF9A56",
+};
+const footerStyle = {
+  textAlign: "center",
+  color: "#fff",
+  // backgroundColor: "#7dbcea",
+};
+
+</script>
+
+<style scoped lang="scss">
+.layout-content {
+  margin: 10px;
+  // padding: 10px;
+  min-height: 280px;
+  background: #fff;
+  border-radius: 10px;
+}
+</style>
+<style></style>

+ 81 - 0
src/api/index.js

@@ -0,0 +1,81 @@
+import { http } from "./request";
+/**
+ * 登录
+ */
+export const login = (data) => {
+  return http.post("/zytwjng/auth/login", data);
+};
+/**
+ * 获取文物征集列表
+ */
+export const antiqueCollect = (data) => {
+  return http.get("/zytwjng/antiqueCollect", data);
+};
+/**
+ * 删除附件
+ */
+export const deleteCollectFile = (id) => {
+  return http.delete(`/zytwjng/antiqueCollect/deleteFile/${id}`);
+};
+/**
+ * 删除文物征集
+ */
+export const deleteCollect = (id) => {
+  return http.delete(`/zytwjng/antiqueCollect/${id}`);
+};
+/**
+ * 获取附件列表
+ */
+export const getCollectFiles = (id) => {
+  return http.get(`/zytwjng/antiqueCollect/listFile/${id}`);
+};
+
+/**
+ * 导出
+ */
+export const exportFiles = (data) => {
+  return http.getFile(`/zytwjng/antiqueCollect/export`, data);
+};
+
+/**
+ * 用户管理分页接口
+ */
+export const userList = (data) => {
+  return http.get(`/zytwjng/user`, data);
+};
+/**
+ * 新增用户接口
+ */
+export const addUser = (data) => {
+  return http.post(`/zytwjng/user`, data);
+};
+/**
+ * 新增用户接口
+ */
+export const delUser = (id) => {
+  return http.delete(`/zytwjng/user/${id}`);
+};
+/**
+ * 重置密码
+ */
+export const resetUserPassword = (userId) => {
+  return http.patch(`/zytwjng/user/password/reset/${userId}`);
+};
+/**
+ * 修改密码
+ */
+export const changePassword = (data) => {
+  return http.post(`/zytwjng/auth/password`, data);
+};
+/**
+ * 登出
+ */
+export const logout = (data) => {
+  return http.post(`/zytwjng/auth/logout`);
+};
+/**
+ * 获取用户信息
+ */
+export const getUserInfo = (data) => {
+  return http.get(`/zytwjng/user/detail`);
+};

+ 180 - 0
src/api/request.js

@@ -0,0 +1,180 @@
+/*
+ * @Author: Rindy
+ * @Date: 2021-04-25 15:58:21
+ * @LastEditors: Rindy
+ * @LastEditTime: 2021-05-08 15:49:54
+ * @Description: 注释
+ */
+
+import axios from "axios";
+import browser from "@/utils/browser";
+import router from "@/router";
+// TextDecoder polyfills for lower browser
+if (undefined === window.TextEncoder) {
+  window.TextEncoder = class _TextEncoder {
+    encode(s) {
+      return unescape(encodeURIComponent(s))
+        .split("")
+        .map(function (val) {
+          return val.charCodeAt();
+        });
+    }
+  };
+  window.TextDecoder = class _TextDecoder {
+    decode(code_arr) {
+      return decodeURIComponent(
+        escape(String.fromCharCode.apply(null, code_arr))
+      );
+    }
+  };
+}
+
+const fetch = axios.create();
+
+fetch.interceptors.request.use(
+  (config) => {
+    if (config.url.indexOf("/zytwjng/") != -1) {
+      let token =
+        browser.valueFromUrl("token") || localStorage.getItem("token") || "";
+      if (token) {
+        config.headers["token"] = token;
+      }
+    }
+    return config;
+  },
+  (error) => {
+    return Promise.reject(error);
+  }
+);
+fetch.interceptors.response.use(
+  (response) => {
+    // 正常的文件流
+    if (!/json/gi.test(response.headers["content-type"])) {
+      return response.data;
+    }
+
+    // 以文件流方式请求但是返回json,需要解析为JSON对象
+    if (response.request.responseType === "arraybuffer") {
+      let enc = new TextDecoder("utf-8");
+      let res = JSON.parse(enc.decode(new Uint8Array(response.data)));
+      return res;
+    }
+    // console.error(response);
+    // if (response.data.code == 401) {
+    //   router.replace("/login");
+    // }
+    return response.data;
+  },
+  (error) => {
+    if (error.status == 401) {
+      localStorage.removeItem("token");
+      router.replace("/login");
+    }
+  }
+);
+
+const http = {
+  retry(func, retries = 0, delay = 1000) {
+    return new Promise((resolve, reject) => {
+      func()
+        .then(resolve)
+        .catch((error) => {
+          if (retries <= 1) {
+            reject(error);
+          } else {
+            setTimeout(() => {
+              http
+                .retry(func, retries - 1, delay)
+                .then(resolve)
+                .catch(reject);
+            }, delay);
+          }
+        });
+    });
+  },
+  get(url, data) {
+    if (data && typeof data === "object") {
+      if (url.indexOf("?") == -1) {
+        url += "?";
+      } else {
+        url += "&";
+      }
+      url += new URLSearchParams(data).toString();
+    }
+    return fetch.get(url);
+  },
+  getFile(url, data) {
+    if (data && typeof data === "object") {
+      if (url.indexOf("?") == -1) {
+        url += "?";
+      } else {
+        url += "&";
+      }
+      url += new URLSearchParams(data).toString();
+    }
+    return fetch.get(url, { responseType: "blob" });
+  },
+  delete(url) {
+    return fetch.delete(url);
+  },
+  patch(url) {
+    return fetch.patch(url);
+  },
+  getImage(url, retries = 3) {
+    return http.retry(
+      () =>
+        new Promise((resolve, reject) => {
+          let img = new Image();
+          img.src = url;
+          img.crossOrigin = "anonymous";
+          img.onload = function () {
+            resolve(img);
+          };
+          img.onerror = function () {
+            reject(`[${url}] load fail`);
+          };
+        }),
+      retries
+    );
+  },
+  getBueffer(url) {
+    return fetch.get(url, {
+      responseType: "arraybuffer",
+    });
+  },
+  getBlob(url) {
+    return fetch.get(url, {
+      responseType: "blob",
+    });
+  },
+  post(url, data) {
+    return fetch.post(url, data);
+  },
+  postFile(url, data) {
+    const form = new FormData();
+    let cb = null;
+    if (data.onUploadProgress) {
+      cb = data.onUploadProgress;
+      delete data.onUploadProgress;
+    }
+    for (let key in data) {
+      // if (key === 'files' && data[key].length > 0) {
+      //     for (let i = 0; i < data[key].length; i++) {
+      //         form.append(key, data[key][i])
+      //     }
+      // } else {
+      //     form.append(key, data[key])
+      // }
+      form.append(key, data[key]);
+    }
+
+    return fetch.post(url, form, {
+      headers: {
+        "Content-Type": "multipart/form-data",
+      },
+      onUploadProgress: cb,
+    });
+  },
+};
+
+export { http, fetch };

BIN
src/assets/images/admin-bg.jpg


BIN
src/assets/images/favicon.ico


BIN
src/assets/images/icon_ac@2x.png


BIN
src/assets/images/icon_eyes@2x.png


BIN
src/assets/images/icon_pw@2x.png


BIN
src/assets/images/slider-logo.png


BIN
src/assets/images/title.png


BIN
src/assets/images/title@2x.png


+ 154 - 0
src/assets/main.css

@@ -0,0 +1,154 @@
+:root {
+  --z-index-normal: 1;
+  --z-index-top: 1000;
+  --z-index-popper: 2000;
+  --z-hot-popper: 3000;
+}
+
+body,
+ol,
+ul,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+th,
+td,
+dl,
+dd,
+form,
+fieldset,
+legend,
+input,
+textarea,
+select {
+  margin: 0;
+  padding: 0;
+}
+* {
+  box-sizing: border-box;
+  user-select: none;
+  /* line-height: normal; */
+}
+body {
+  color: #333333;
+  text-align: justify;
+  font-family: "SourceHanSerifSC-Regular";
+  -webkit-tap-highlight-color: transparent;
+}
+html,
+body,
+#app {
+  width: 100%;
+  height: 100%;
+  background: #f5f5f5;
+}
+a {
+  color: #fff;
+  cursor: pointer;
+  text-decoration: none;
+}
+em {
+  font-style: normal;
+}
+li {
+  list-style: none;
+}
+img {
+  border: 0;
+  vertical-align: middle;
+}
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+p {
+  word-wrap: break-word;
+}
+iframe {
+  border: none;
+}
+
+/* @font-face {
+  font-family: "SourceHanSerifSC-Bold";
+  src: url("./fonts/SOURCEHANSERIFCN-BOLD.otf");
+}
+@font-face {
+  font-family: "SourceHanSerifSC-Regular";
+  src: url("./fonts/SOURCEHANSERIFCN-REGULAR.otf");
+} */
+
+.limit-line {
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+  word-break: break-all;
+  word-wrap: break-word;
+}
+
+.line-2 {
+  -webkit-line-clamp: 2;
+}
+
+.line-3 {
+  -webkit-line-clamp: 3;
+}
+
+.hidden {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+.darkGlass {
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.message-outer {
+  position: absolute;
+  display: table;
+  height: 100%;
+  width: 100%;
+
+  * {
+    transition: all 0.3s;
+  }
+}
+::-webkit-scrollbar {
+  width: 4px;
+  height: 1px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 4px;
+  box-shadow: inset 0 0 5px rgba(106, 73, 52, 1);
+  background: #ccc;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: #999;
+}
+
+::-webkit-scrollbar-track {
+  box-shadow: inset 0 0 5px transparent;
+  border-radius: 4px;
+  /* background: #000000; */
+}
+input {
+  background: none;
+}
+.amap-logo,
+.amap-copyright {
+  display: none !important;
+}
+.amap-info-close {
+  display: none;
+}
+.amap-info-content {
+  padding: 0;
+  border-radius: 0.16rem;
+}

+ 99 - 0
src/components/Header/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="header-content">
+    <div class="user-avatar">
+      <a-dropdown placement="bottomRight" arrow>
+        <span class="user-name">
+          {{ store.userInfo?.username }} {{}}<DownOutlined
+        /></span>
+
+        <template #overlay>
+          <a-menu class="user-info-dropdown">
+            <a-menu-item @click="open = true">
+              <a>修改密码</a>
+            </a-menu-item>
+            <a-menu-item>
+              <a @click="showConfirm">退出登录</a>
+            </a-menu-item>
+          </a-menu>
+        </template>
+      </a-dropdown>
+    </div>
+  </div>
+  <Reset :open="open" @close="open = false" />
+</template>
+<script setup>
+import { ref, createVNode, onMounted, nextTick } from "vue";
+import Reset from "./reset/index.vue";
+import { DownOutlined, ExclamationCircleOutlined } from "@ant-design/icons-vue";
+import { Modal } from "ant-design-vue";
+import { useStore } from "@/stores";
+import { logout, getUserInfo } from "@/api";
+import router from "@/router";
+const store = useStore();
+const open = ref(false);
+const showConfirm = () => {
+  Modal.confirm({
+    title: "是否登出账号?",
+    icon: createVNode(ExclamationCircleOutlined),
+    content: createVNode("div", {
+      style: "color:red;",
+    }),
+    okText: "确定",
+    cancelText: "取消",
+    onOk() {
+      logout()
+        .then((res) => {
+          if (res.code == 0) {
+            localStorage.removeItem("token");
+            router.replace("/login");
+          }
+        })
+        .catch((err) => {});
+    },
+    onCancel() {
+      console.log("Cancel");
+    },
+    class: "login-out-modal",
+  });
+};
+
+const getInfo = () => {
+  getUserInfo()
+    .then((res) => {
+      if (res.code == 0) {
+        store.setUserInfo(res.data);
+      }
+    })
+    .catch((err) => {});
+};
+onMounted(() => {
+    getInfo();
+});
+</script>
+<style lang="scss" scoped>
+.header-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  color: #000;
+  .user-avatar {
+    // cursor: pointer;
+    .user-name {
+      cursor: pointer;
+    }
+  }
+}
+:deep(.ant-dropdown) {
+}
+</style>
+<style>
+.user-info-dropdown {
+  .ant-dropdown-menu-title-content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+</style>

+ 194 - 0
src/components/Header/reset/index.vue

@@ -0,0 +1,194 @@
+<template>
+  <div>
+    <!-- :keyboard="false" -->
+    <!-- :maskClosable="false" -->
+    <a-modal
+      v-model:open="props.open"
+      title="修改密码"
+      width="480px"
+      @cancel="handleCancel"
+    >
+      <template #footer>
+        <a-button key="back" @click="handleCancel">取消</a-button>
+        <a-button
+          key="submit"
+          type="primary"
+          :loading="loading"
+          @click="handleOk"
+          >确认</a-button
+        >
+      </template>
+      <a-form
+        :label-col="labelCol"
+        ref="formRef"
+        :rules="rules"
+        :model="formState"
+        @finish="onFinish"
+        @finishFailed="onFinishFailed"
+      >
+        <a-form-item label="旧密码" name="oldPassword">
+          <div class="un-box">
+            <a-input-password
+              v-model:value="formState.oldPassword"
+              placeholder="请输入内容 6-15字"
+              maxlength="15"
+            />
+
+            <p v-if="showError" class="error">密码错误</p>
+          </div>
+        </a-form-item>
+        <a-form-item label="新密码" name="newPassword">
+          <div class="un-box">
+            <a-input-password
+              v-model:value="formState.newPassword"
+              placeholder="请输入内容 6-15字"
+              maxlength="15"
+            />
+          </div>
+        </a-form-item>
+        <a-form-item label="确认密码" name="confirmPassword">
+          <div class="un-box">
+            <a-input-password
+              v-model:value="formState.confirmPassword"
+              placeholder="请输入内容 6-15字"
+              maxlength="15"
+            />
+          </div>
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+<script setup>
+import {
+  ref,
+  reactive,
+  onMounted,
+  nextTick,
+  defineProps,
+  defineEmits,
+} from "vue";
+import { changePassword } from "@/api";
+import { message } from "ant-design-vue";
+const emits = defineEmits(["close"]);
+const props = defineProps({
+  open: {
+    type: Boolean,
+    default: false,
+  },
+});
+const labelCol = {
+  style: {
+    width: "100px",
+  },
+};
+const formState = reactive({
+  oldPassword: "",
+  newPassword: "",
+  confirmPassword: "",
+});
+const showError = ref(false);
+const rules = {
+  oldPassword: [
+    {
+      required: true,
+      validator: (_, value) => {
+        if (!value || value.length < 6 || value.length > 15) {
+          return Promise.reject(new Error("请输入内容 6-15字"));
+        }
+        showError.value = false;
+        return Promise.resolve();
+      },
+      trigger: ["blur", "change"],
+    },
+  ],
+  newPassword: [
+    {
+      required: true,
+      validator: (_, value) => {
+        if (!value || value.length < 6 || value.length > 15) {
+          return Promise.reject(new Error("请输入内容 6-15字"));
+        }
+
+        showError.value = false;
+        return Promise.resolve();
+      },
+      trigger: ["blur", "change"],
+    },
+  ],
+  confirmPassword: [
+    {
+      required: true,
+      validator: (_, value) => {
+        if (value !== formState.newPassword) {
+          return Promise.reject(new Error("确认新密码与新密码不一致"));
+        }
+        // if (!value || value.length < 6 || value.length > 15) {
+        //   return Promise.reject(new Error("请输入内容 6-15字"));
+        // }
+        showError.value = false;
+        return Promise.resolve();
+      },
+      trigger: ["blur", "change"],
+    },
+  ],
+};
+const formRef = ref(null);
+const handleCancel = (e) => {
+  emits("close");
+  formState.oldPassword = "";
+  formState.newPassword = "";
+  formState.confirmPassword = "";
+  formRef.value.clearValidate();
+  showError.value = false;
+};
+const loading = ref(false);
+const handleOk = (e) => {
+  formRef.value
+    .validate()
+    .then(() => {
+      // 提交表单
+      console.log("提交表单", formState);
+      loading.value = true;
+      changePassword({
+        newPassword: formState.newPassword,
+        oldPassword: formState.oldPassword,
+      })
+        .then((res) => {
+          loading.value = false;
+          if (res.code == 10004) {
+            showError.value = true;
+            return;
+          }
+          if (res.code == 0) {
+            handleCancel();
+            message.success("操作成功");
+          } else {
+            message.error(res.message);
+          }
+        })
+        .catch((err) => {
+          loading.value = false;
+        });
+
+      //   setTimeout(() => {
+      //     showError.value = true;
+      //   }, 1000);
+    })
+    .catch((errorInfo) => {
+      console.log("验证失败:", errorInfo);
+    });
+};
+</script>
+<style lang="scss" scoped>
+.un-box {
+  position: relative;
+  .error {
+    position: absolute;
+    bottom: -100%;
+    left: 0;
+    font-size: 12px;
+    color: red;
+  }
+}
+</style>

+ 125 - 0
src/components/Menu/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="menu-content" v-if="routerName">
+    <div class="logo"></div>
+    <!-- <div class="menu-list">
+      <div class="menu-item">文物征集</div>
+      <div class="menu-item">留言选登</div>
+      <div class="menu-item">用户管理</div>
+    </div> -->
+    <a-menu
+      v-if="store.userInfo"
+      class="menu-list"
+      v-model:selectedKeys="state.selectedKeys"
+      mode="inline"
+      theme="dark"
+      :items="items"
+    ></a-menu>
+  </div>
+</template>
+<script setup>
+import { ref, reactive, h, onMounted, watch, nextTick, computed } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import router from "@/router";
+import { UserOutlined, TagOutlined } from "@ant-design/icons-vue";
+import { useStore } from "@/stores";
+const store = useStore();
+
+// let routerName = store.routerName;
+const routerName = computed(() => {
+  if (store.routerName == "collect") {
+    state.selectedKeys = [""];
+  } else {
+    state.selectedKeys = [store.routerName];
+  }
+
+  return store.routerName;
+});
+const state = reactive({
+  selectedKeys: [""],
+});
+const items = computed(() => {
+  let list = [
+    {
+      key: "",
+      icon: () => h(TagOutlined),
+      label: "文物征集",
+      title: "collect",
+    },
+    //   {
+    //     key: "messages",
+    //     // icon: () => h(DesktopOutlined),
+    //     label: "留言选登",
+    //     title: "messages",
+    //   },
+  ];
+
+  if (store.userInfo?.isAdmin) {
+    list.push({
+      key: "users",
+      icon: () => h(UserOutlined),
+      label: "用户管理",
+      title: "users",
+    });
+  }
+  return list;
+});
+watch(
+  () => state.selectedKeys,
+  (newVal) => {
+    console.log("Selected keys changed:", newVal);
+    router.push(`/${newVal[0]}`);
+  }
+);
+onMounted(() => {
+  //   state.selectedKeys = route.name;
+});
+</script>
+<style lang="scss" scoped>
+.menu-content {
+  height: 100%;
+  width: 200px;
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  flex-direction: column;
+  .logo {
+    width: 166px;
+    height: 63px;
+    background: url("@/assets/images/slider-logo.png") no-repeat;
+    background-size: 100% auto;
+    margin-bottom: 30px;
+    margin-top: 30px;
+  }
+  .menu-list {
+    background: none;
+    font-size: 20px;
+    font-weight: bold;
+    color: #fff;
+    :deep(.ant-menu-item) {
+      width: 100%;
+      height: 60px;
+      margin: 0;
+      padding-left: 0 !important;
+      border-radius: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      //   margin-bottom: 40px;
+      .ant-menu-item-icon {
+        font-size: 20px;
+        // position: absolute;
+        // left: 0;
+      }
+      .ant-menu-title-content {
+        flex: none;
+      }
+    }
+  }
+}
+</style>
+<style>
+.menu-content {
+  .ant-menu-title-content {
+  }
+}
+</style>

+ 23 - 0
src/configure.js

@@ -0,0 +1,23 @@
+import { compose, initial } from "@dage/service";
+
+initial({
+  fetch: window.fetch.bind(window),
+  baseURL: import.meta.env.VITE_BASE_URL,
+  interceptor: compose(async (request, next) => {
+    request.headers["Content-Type"] = "application/json";
+
+    const response = await next();
+    const { showError = true } = request.meta;
+
+    if (response.code !== 0 && request.name.indexOf("someData") < 0) {
+      const message = response.__raw__.data.msg ?? "系统出差中";
+      // 错误信息映射
+      response.errorMessage = message;
+      if (showError) {
+        console.log(message);
+      }
+    }
+
+    return response;
+  }),
+});

+ 16 - 0
src/main.js

@@ -0,0 +1,16 @@
+import "./assets/main.css";
+import "./configure";
+
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+import App from "./App.vue";
+import router from "./router";
+import Antd from "ant-design-vue";
+import "ant-design-vue/dist/reset.css";
+
+const app = createApp(App);
+app.use(createPinia());
+app.use(router);
+
+// app.mount("#app");
+app.use(Antd).mount("#app");

+ 55 - 0
src/router/index.js

@@ -0,0 +1,55 @@
+import {
+  createRouter,
+  createWebHistory,
+  createWebHashHistory,
+} from "vue-router";
+import { useStore } from "@/stores";
+
+// const store = useStore();
+const router = createRouter({
+  // history: createWebHistory(import.meta.env.BASE_URL),
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: "/",
+      name: "collect",
+
+      meta: { title: "文物征集" },
+      component: () => import("../views/collect/index.vue"),
+    },
+    {
+      path: "/login",
+      name: "login",
+      meta: { title: "登录页" },
+      component: () => import("../views/login/index.vue"),
+    },
+    {
+      path: "/messages",
+      name: "messages",
+      meta: { title: "留言选登" },
+      component: () => import("../views/messages/index.vue"),
+    },
+    {
+      path: "/users",
+      name: "users",
+      meta: { title: "用户管理" },
+      component: () => import("../views/users/index.vue"),
+    },
+  ],
+});
+router.beforeEach(async (to, from, next) => {
+  // let token = localStorage.getItem("token");
+  // if (to.name !== "login" && !token) {
+  //   return next({ name: "login" });
+  // }
+  let store = useStore();
+
+  store.setRouterName(to.name);
+
+  next();
+});
+
+router.afterEach((to, from, failure) => {
+ 
+});
+export default router;

+ 19 - 0
src/stores/index.js

@@ -0,0 +1,19 @@
+// stores/counter.js
+import { nextTick, onMounted, ref } from "vue";
+import { defineStore } from "pinia";
+
+export const useStore = defineStore("store", {
+  state: () => {
+    return { routerName: "", userInfo: null };
+  },
+  // 也可以定义为
+  // state: () => ({ count: 0 })
+  actions: {
+    setRouterName(payload) {
+      this.routerName = payload;
+    },
+    setUserInfo(payload) {
+      this.userInfo = payload;
+    },
+  },
+});

+ 125 - 0
src/utils/browser.js

@@ -0,0 +1,125 @@
+export default {
+  hasURLParam: function (key) {
+    let querys = window.location.search.substring(1).split("&");
+    for (let i = 0; i < querys.length; i++) {
+      let keypair = querys[i].split("=");
+      if (keypair[0] == key) {
+        return true;
+      }
+    }
+    return false;
+  },
+  getLang() {
+    return this.getURLParam("lang") || "zh";
+  },
+  getURLParam: function (key) {
+    let querys = window.location.search.substring(1).split("&");
+    for (let i = 0; i < querys.length; i++) {
+      let keypair = querys[i].split("=");
+      if (keypair.length === 2 && keypair[0] === key) {
+        try {
+          return decodeURIComponent(keypair[1]);
+        } catch (error) {
+          return keypair[1];
+        }
+      }
+    }
+    return "";
+  },
+  valueFromUrl(key) {
+    return this.urlHasValue(key, true);
+  },
+  urlHasValue: function (key, isGetValue) {
+    if (
+      key === "m" &&
+      window.__ProjectNum &&
+      window.__ProjectNum != "__ProjectNum__"
+    ) {
+      return window.__ProjectNum;
+    }
+
+    let querys = window.location.search.substr(1).split("&");
+    if (isGetValue) {
+      for (let i = 0; i < querys.length; i++) {
+        let keypair = querys[i].split("=");
+        if (keypair.length === 2 && keypair[0] === key) {
+          return keypair[1];
+        }
+      }
+      return "";
+    } else {
+      //return window.location.search.match("&" + key + "|\\?" + key) != null  有bug
+      for (let i = 0; i < querys.length; i++) {
+        let keypair = querys[i].split("=");
+        if (keypair[0] == key) {
+          return true;
+        }
+      }
+      return false;
+    }
+  },
+  detectWeixin: function () {
+    //微信 包括PC的微信
+    return (
+      window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) ==
+      "micromessenger"
+    );
+  },
+  detectWeixinMiniProgram: function () {
+    return window.navigator.userAgent.match("miniProgram");
+  },
+  detectIOS: function () {
+    return this.detectIPhone() || this.detectIPad() || this.detectIPod();
+  },
+  detectIPhone: function () {
+    var e = window.navigator.userAgent,
+      t = /iPhone/;
+    return t.test(e);
+  },
+  detectIPad: function () {
+    if (
+      window.navigator.platform === "MacIntel" &&
+      window.navigator.maxTouchPoints > 1
+    ) {
+      return true;
+    }
+    var e = window.navigator.userAgent,
+      t = /iPad/;
+    return t.test(e);
+  },
+  detectIPod: function () {
+    var e = window.navigator.userAgent,
+      t = /iPod/;
+    return t.test(e);
+  },
+  detectAndroid: function () {
+    var e = window.navigator.userAgent;
+    return e.indexOf("Android") !== -1;
+  },
+  detectIE: function () {
+    var e = window.navigator.userAgent,
+      t = e.indexOf("MSIE ");
+    return t !== -1 || !!navigator.userAgent.match(/Trident.*rv\:11\./);
+  },
+  detectOpera: function () {
+    var e = window.navigator.userAgent;
+    return e.indexOf("OPR") !== -1;
+  },
+  detectSafari: function () {
+    var e = window.navigator.userAgent,
+      t = e.indexOf("Safari");
+    return t !== -1 && !this.detectOpera() && !this.detectChrome(); //xzw add detectOpera
+  },
+  detectFirefox: function () {
+    var e = window.navigator.userAgent;
+    return e.indexOf("Firefox") !== -1;
+  },
+  detectChrome: function () {
+    var e = window.navigator.userAgent;
+    return e.indexOf("Chrome") !== -1 && !this.detectOpera();
+  },
+  detectAndroidMobile: function () {
+    var e = window.navigator.userAgent;
+    return this.detectAndroid() && e.indexOf("Mobile") !== -1;
+  },
+};

+ 32 - 0
src/utils/download.js

@@ -0,0 +1,32 @@
+export const downloadFile = (file, name) => {
+  return new Promise(async (resolve, reject) => {
+    if (file) {
+      let objectUrl = window.URL.createObjectURL(file);
+      let a = document.createElement("a");
+      a.href = objectUrl;
+      a.download = name;
+      a.click();
+      a.remove();
+      resolve(true);
+    } else {
+      reject(false);
+    }
+  });
+};
+export const downloadUrl = (url, name) => {
+  return new Promise(async (resolve, reject) => {
+    let response = await fetch(url); // 内容转变成blob地址
+    let blob = await response.blob(); // 创建隐藏的可下载链接
+    if (blob) {
+      let objectUrl = window.URL.createObjectURL(blob);
+      let a = document.createElement("a");
+      a.href = objectUrl;
+      a.download = name;
+      a.click();
+      a.remove();
+      resolve(true);
+    } else {
+      reject(false);
+    }
+  });
+};

+ 136 - 0
src/utils/file.js

@@ -0,0 +1,136 @@
+// import { i18n } from "@/lang/index"
+// 媒体名称
+export const mediaTypes = {
+    // image: i18n.t("common.photo"),
+    // video: i18n.t("common.video"),
+    // audio: i18n.t("common.voice"),
+}
+
+// 媒体扩展类型
+export const mediaMimes = {
+    image: ['jpg', 'png', 'jpeg', 'bmp', 'gif'],
+    audio: ['mp3', 'aac', 'ogg', 'wav' /* , "m4a" */],
+    video: ['mp4', 'mov', 'quicktime', 'webm' /* "rmvb", "wmv" */] //ios:mov
+}
+
+// 媒体大小显示(MB)
+export const mediaMaxSize = {
+    image: 10,
+    video: 20,
+    audio: 5
+}
+
+/**
+ * 获取媒体扩展类型
+ * @param {Stirng} filename 文件名称
+ */
+export const getMime = filename => {
+    if (!filename || filename.indexOf('.') === -1) {
+        return ''
+    }
+
+    return filename
+        .split('.')
+        .pop()
+        .toLowerCase()
+}
+
+/**
+ * 在路径中获取文件名
+ * @param {*} path
+ */
+export const getFilename = path => {
+    const segment = (path || '').split('/')
+    return segment[segment.length - 1]
+}
+
+/**
+ * 检测媒体文件是否超过预设限制
+ * @param {String} type 媒体类型
+ * @param {Number} size 文件大小
+ */
+export const checkSizeLimit = (type, size) => {
+    size = size / 1024 / 1024
+
+    return size <= mediaMaxSize[type]
+}
+
+export const checkSizeLimitFree = (size, limit) => {
+    size = size / 1024 / 1024
+
+    return size <= limit
+}
+
+/**
+ * 检测媒体类型
+ * @param {String} type 媒体类型
+ * @param {String} filename 文件名称
+ */
+export const checkMediaMime = (type, filename) => {
+    const mime = getMime(filename)
+    const find = mediaMimes[type]
+    if (!find) {
+        return false
+    }
+
+    return find.indexOf(mime) !== -1
+}
+
+export const checkMediaMimeByAccept = (accept, filename) => {
+    let mime = getMime(filename)
+    let type = accept
+    if (type && type.indexOf('jpg') == -1 && type.indexOf('jpeg') != -1) {
+        type += ',image/jpg'
+    }
+    return (type || '').indexOf(mime) != -1
+}
+
+export const base64ToBlob = base64 => {
+    let arr = base64.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 })
+}
+
+export const base64ToDataURL = base64 => {
+    return window.URL.createObjectURL(base64ToBlob(base64))
+}
+
+export const blobToDataURL = blob => {
+    return window.URL.createObjectURL(blob)
+}
+
+export const blobToBase64 = function(blob) {
+    return new Promise(resolve => {
+        var reader = new FileReader()
+        reader.onload = function() {
+            resolve(reader.result)
+        }
+        reader.readAsDataURL(blob)
+    })
+}
+
+export function convertBlob2File(blob, name) {
+    return new File([blob], name, {
+        type: blob.type
+    })
+}
+
+export function dataURLtoFile(dataurl, filename = 'file', type) {
+    let arr = dataurl.split(',')
+    let bstr = window.atob(arr[1])
+    !type && (type = arr[0].replace('data:', '').replace(';base64', ''))
+    let n = bstr.length,
+        u8arr = new Uint8Array(n)
+    while (n--) {
+        u8arr[n] = bstr.charCodeAt(n)
+    }
+    return new File([u8arr], filename, {
+        type
+    })
+}

+ 210 - 0
src/views/collect/index.vue

@@ -0,0 +1,210 @@
+<template>
+  <div class="collect-content">
+    <div class="router-title">
+      <span>
+        {{ routerTitle }}
+      </span>
+      <div>
+        <a-button :loading="exportLoading" @click="handleExport">导出</a-button>
+      </div>
+    </div>
+    <div class="table-body">
+      <a-table
+        :dataSource="dataSource"
+        :columns="columns"
+        :scroll="{ x: 0, y: 600 }"
+        :pagination="pagination"
+        @change="handleTableChange"
+        :loading="loading"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.key === 'province'">
+            <span>{{ record.province + record.city + record.county }}</span>
+          </template>
+
+          <template v-if="column.key === 'action'">
+            <div class="action-content">
+              <a-button type="text" @click="showModal(record.id)"
+                >附件</a-button
+              >
+              <a-button type="text" @click="showView(record)">查看</a-button>
+
+              <a-popconfirm
+                title="请确认是否删除"
+                ok-text="删除"
+                placement="topRight"
+                cancel-text="取消"
+                @confirm="deleteConfirm(record.id)"
+              >
+                <a-button type="text" danger>删除</a-button>
+              </a-popconfirm>
+            </div>
+          </template>
+        </template>
+      </a-table>
+    </div>
+  </div>
+
+  <Modal
+    v-if="open"
+    :collectId="collectId"
+    :open="open"
+    @handleCancel="handleCancel"
+  />
+  <View :viewData="viewData" @handleViewCancel="handleViewCancel" />
+</template>
+<script setup>
+import { ref, onMounted, nextTick } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { antiqueCollect, deleteCollect, exportFiles } from "@/api";
+import { downloadFile } from "@/utils/download";
+import Modal from "./modal/index.vue";
+import View from "./view/index.vue";
+const route = useRoute();
+const routerTitle = ref(route.meta.title);
+const open = ref(false);
+const viewData = ref(null);
+const collectId = ref(null);
+const loading = ref(false);
+const exportLoading = ref(false);
+const pagination = ref({
+  total: 0,
+  current: 1,
+  pageSize: 10,
+  hideOnSinglePage: true,
+});
+const dataSource = ref([]);
+const columns = ref([
+  {
+    title: "联系人姓名",
+    dataIndex: "name",
+    key: "name",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "联系电话",
+    dataIndex: "phone",
+    key: "phone",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "省市区/县",
+    dataIndex: "province",
+    key: "province",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "详细地址",
+    dataIndex: "address",
+    key: "address",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "文物概况",
+    dataIndex: "description",
+    key: "description",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "提交时间",
+    dataIndex: "createTime",
+    key: "createTime",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "操作",
+    key: "action",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+]);
+
+const showModal = (id) => {
+  collectId.value = id;
+  open.value = true;
+};
+const handleCancel = (e) => {
+  collectId.value = null;
+  open.value = false;
+};
+const showView = (record) => {
+  viewData.value = record;
+};
+const handleViewCancel = (e) => {
+  viewData.value = null;
+};
+const handleTableChange = (pag, filters, sorter) => {
+  console.log("分页回调pag, filters, sorter", pag, filters, sorter);
+
+  pagination.value.current = pag.current;
+  pagination.value.pageSize = pag.pageSize;
+  getData();
+};
+const getData = async () => {
+  loading.value = true;
+  antiqueCollect({
+    pageNo: pagination.value.current,
+    pageSize: pagination.value.pageSize,
+  })
+    .then((res) => {
+      loading.value = false;
+      dataSource.value = res.data.pageData;
+      pagination.value.total = res.data.total;
+      pagination.value.current = res.data.pageNum;
+      pagination.value.pageSize = res.data.pageSize;
+    })
+    .catch((err) => {
+      loading.value = false;
+    });
+};
+const handleExport = () => {
+  exportLoading.value = true;
+  exportFiles({
+    pageNo: pagination.value.current,
+    pageSize: pagination.value.pageSize,
+  })
+    .then((res) => {
+      downloadFile(res, `文物征集-${new Date().getTime()}.xlsx`);
+      exportLoading.value = false;
+    })
+    .catch((err) => {
+      exportLoading.value = false;
+    });
+};
+const deleteConfirm = async (id) => {
+  let res = await deleteCollect(id);
+};
+onMounted(() => {
+  getData();
+});
+</script>
+<style lang="scss" scoped>
+.collect-content {
+  color: #000;
+  width: 100%;
+  height: 100%;
+  .router-title {
+    height: 60px;
+    font-size: 18px;
+    font-weight: bold;
+    line-height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 20px;
+  }
+}
+</style>

+ 257 - 0
src/views/collect/modal/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <div>
+    <a-modal
+      v-model:open="props.open"
+      title="附件管理"
+      @ok="handleOk"
+      @cancel="handleCancel"
+      width="80%"
+      height="500px"
+      :footer="null"
+    >
+      <div>
+        <a-radio-group v-model:value="type">
+          <a-radio-button value="image"
+            ><FileImageOutlined />图片</a-radio-button
+          >
+          <a-radio-button value="other"><FileOutlined />其他</a-radio-button>
+        </a-radio-group>
+      </div>
+
+      <div>
+        <a-table
+          :loading="loading"
+          :dataSource="dataSource"
+          :columns="columns"
+          :scroll="{ x: 0, y: 500 }"
+          :pagination="false"
+        >
+          <template #bodyCell="{ column, record }">
+            <template v-if="column.key === 'path'"
+              ><div class="thumb-image">
+                <div
+                  :id="`image_${record.id}`"
+                  v-for="(item, index) in [record.path]"
+                >
+                  <img
+                    :src="`${baseUrl + item}`"
+                    alt=""
+                    class="viewer-image"
+                    @click="getOnclick(record.id)"
+                    :data-original="`${baseUrl + item}`"
+                  />
+                </div>
+              </div>
+            </template>
+            <template v-if="column.key === 'action'">
+              <div class="action-content">
+                <a-button
+                  @click="downloadUrl(record.path, record.fileName)"
+                  type="text"
+                  >下载</a-button
+                >
+
+                <a-popconfirm
+                  title="请确认是否删除"
+                  ok-text="删除"
+                  cancel-text="取消"
+                  @confirm="deleteConfirm(record.id)"
+                >
+                  <a-button type="text" danger>删除</a-button>
+                </a-popconfirm>
+              </div>
+            </template>
+          </template>
+        </a-table>
+      </div>
+    </a-modal>
+  </div>
+</template>
+<script setup>
+import {
+  ref,
+  onMounted,
+  nextTick,
+  defineEmits,
+  reactive,
+  defineProps,
+  computed,
+} from "vue";
+import { FileImageOutlined, FileOutlined } from "@ant-design/icons-vue";
+import { getCollectFiles, deleteCollectFile } from "@/api";
+import { downloadUrl } from "@/utils/download";
+import { message } from "ant-design-vue";
+import Viewer from "viewerjs";
+import "viewerjs/dist/viewer.css";
+const baseUrl = import.meta.env.VITE_BASE_URL;
+const loading = ref(false);
+const image = ref(null);
+const getOnclick = (id) => {
+  let el = document.getElementById("image_" + id);
+  const viewer = new Viewer(el, {
+    url: "data-original",
+    // toolbar: false,
+    navbar: false,
+    show: function () {
+      viewer.update();
+    },
+    // 相关配置项,详情见下面
+  });
+};
+
+const emit = defineEmits(["handleCancel"]);
+const props = defineProps({
+  open: {
+    type: Boolean,
+    default: false,
+  },
+  collectId: {
+    type: Number,
+    default: null,
+  },
+});
+const deleteConfirm = async (id) => {
+  console.error(123123);
+  deleteCollectFile(id)
+    .then((res) => {
+      if (res.code == 0) {
+        message.success("操作成功");
+        getData();
+      } else {
+        message.error(res.message);
+      }
+    })
+    .catch((err) => {});
+};
+const type = ref("image");
+const pagination = ref({
+  total: 0,
+  current: 1,
+  pageSize: 10,
+  showSizeChanger: false,
+});
+const dataSource = ref([]);
+const columns = computed(() => {
+  if (originData.value) {
+    dataSource.value =
+      type.value === "image" ? originData.value.imgs : originData.value.zips;
+  }
+  return type.value === "image" ? imagesColumns.value : filesColumns.value;
+});
+const imagesColumns = ref([
+  {
+    title: "缩略图",
+    dataIndex: "path",
+    key: "path",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "文件名称",
+    dataIndex: "fileName",
+    key: "fileName",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "文件大小",
+    dataIndex: "fileSize",
+    key: "fileSize",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "提交时间",
+    dataIndex: "createTime",
+    key: "createTime",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+
+  {
+    title: "操作",
+    key: "action",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+]);
+const filesColumns = ref([
+  {
+    title: "文件名称",
+    dataIndex: "fileName",
+    key: "fileName",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "文件大小",
+    dataIndex: "fileSize",
+    key: "fileSize",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "提交时间",
+    dataIndex: "createTime",
+    key: "createTime",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "操作",
+    key: "action",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+]);
+
+const open = ref(false);
+
+const handleOk = (e) => {
+  console.log(e);
+  open.value = false;
+};
+const handleCancel = (e) => {
+  console.log(e);
+  type.value = "image";
+  emit("handleCancel", e);
+};
+const originData = ref(null);
+const getData = async () => {
+  loading.value = true;
+  try {
+    let res = await getCollectFiles(props.collectId);
+    if (res.code == 0) {
+      originData.value = res.data;
+      loading.value = false;
+    }
+  } catch (err) {
+    loading.value = false;
+  }
+};
+onMounted(() => {
+  getData();
+});
+</script>
+<style lang="scss" scoped>
+div {
+  margin: 10px 0;
+}
+.thumb-image {
+  img {
+    width: 60px;
+    height: 60px;
+    object-fit: contain;
+    cursor: pointer;
+  }
+}
+</style>

+ 54 - 0
src/views/collect/view/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div>
+    <a-modal
+      v-model:open="props.viewData"
+      title="查看"
+      @cancel="handleViewCancel"
+      width="480px"
+      :footer="null"
+    >
+      <div>
+        <div>提交时间:{{ props.viewData?.createTime }}</div>
+        <div>联系人姓名:{{ props.viewData?.name }}</div>
+        <div>联系电话:{{ props.viewData?.phone }}</div>
+        <div>
+          省市区/县:{{
+            props.viewData?.province +
+            props.viewData?.city +
+            props.viewData?.county
+          }}
+        </div>
+        <div>详细地址:{{ props.viewData?.address }}</div>
+        <div>文物概况:{{ props.viewData?.description }}</div>
+      </div>
+    </a-modal>
+  </div>
+</template>
+<script setup>
+import {
+  ref,
+  onMounted,
+  nextTick,
+  defineEmits,
+  defineProps,
+  computed,
+} from "vue";
+const emit = defineEmits(["handleViewCancel"]);
+const props = defineProps({
+  viewData: {
+    type: Object,
+    default: () => null,
+  },
+});
+
+const handleViewCancel = (e) => {
+  emit("handleViewCancel", e);
+};
+
+onMounted(() => {});
+</script>
+<style lang="scss" scoped>
+div {
+  margin: 10px 0;
+}
+</style>

+ 182 - 0
src/views/login/index.vue

@@ -0,0 +1,182 @@
+<template>
+  <div class="login-content">
+    <div class="login-form">
+      <div class="logo"></div>
+      <p class="title"></p>
+      <div class="input-item user-name">
+        <div class="icon name-icon"></div>
+        <a-input
+          v-model:value="userName"
+          :bordered="false"
+          maxlength="15"
+          placeholder="请输入用户名"
+        />
+      </div>
+
+      <div class="input-item password">
+        <div class="icon pass-icon"></div>
+        <a-input-password
+          v-model:value="password"
+          placeholder="请输入密码"
+          :bordered="false"
+          maxlength="15"
+          class="pss-ipt"
+        />
+      </div>
+      <a-button :loading class="login-btn" type="primary" @click="handleLogin"
+        >登录</a-button
+      >
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { login, getUserInfo } from "@/api/index";
+import { message } from "ant-design-vue";
+import { useStore } from "@/stores";
+const store = useStore();
+import router from "@/router";
+const loading = ref(false);
+const userName = ref("");
+const password = ref("");
+const handleLogin = async () => {
+  if (userName.value.trim() == "") {
+    message.warn("请输入用户名");
+    return;
+  }
+  if (password.value.trim() == "") {
+    message.warn("请输入密码");
+    return;
+  }
+  console.log("登录", userName.value, window.btoa(password.value));
+  loading.value = true;
+  try {
+    let res = await login({
+      password: window.btoa(password.value),
+      username: userName.value,
+    });
+    loading.value = false;
+    if (res.code == 0) {
+      localStorage.setItem("token", res.data.accessToken);
+      router.replace(`/`);
+    } else {
+      message.error(res.message);
+    }
+  } catch (err) {
+    loading.value = false;
+  }
+};
+
+const onEnterKey = (event) => {
+  if (
+    event.keyCode === 13 &&
+    userName.value.trim() != "" &&
+    password.value.trim() != ""
+  ) {
+    // 在这里写入回车键按下后要执行的代码
+    handleLogin();
+  }
+};
+
+const getInfo = () => {
+  getUserInfo()
+    .then((res) => {
+      if (res.code == 0) {
+        store.setUserInfo(res.data);
+        router.replace("/");
+      }
+    })
+    .catch((err) => {});
+};
+
+onMounted(() => {
+  getInfo();
+
+  document.addEventListener("keyup", onEnterKey);
+});
+onUnmounted(() => {
+  document.removeEventListener("keyup", onEnterKey);
+});
+</script>
+<style lang="scss" scoped>
+.login-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: url("@/assets/images/admin-bg.jpg") no-repeat;
+  background-size: cover;
+  .login-form {
+    width: 590px;
+    height: 760px;
+    background: rgba(250, 246, 236, 0.8);
+    box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.1);
+    border-radius: 20px 20px 20px 20px;
+    position: absolute;
+    top: 135px;
+    left: 189px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: flex-start;
+    .logo {
+      width: 304px;
+      height: 114px;
+      background: url("@/assets/images/title.png") no-repeat;
+      background-size: 100% auto;
+      margin: 60px auto 30px;
+    }
+    .title {
+      width: 426px;
+      height: 60px;
+      background: url("@/assets/images/title@2x.png") no-repeat;
+      background-size: 100% auto;
+      margin: 0 auto 90px;
+    }
+
+    .input-item {
+      width: 426px;
+      border-bottom: 1px solid rgba(70, 70, 70, 0.5);
+      margin: 0 auto 10px;
+      height: 60px;
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: flex-start;
+      .icon {
+        width: 40px;
+        height: 40px;
+        &.name-icon {
+          background: url("@/assets/images/icon_ac@2x.png") no-repeat;
+          background-size: 100% auto;
+        }
+        &.pass-icon {
+          background: url("@/assets/images/icon_pw@2x.png") no-repeat;
+          background-size: 100% auto;
+        }
+      }
+      input,
+      .pss-ipt {
+        width: 100%;
+        // position: absolute;
+        height: 40px;
+        bottom: 0;
+        font-size: 24px;
+        &::placeholder {
+          color: rgba(70, 70, 70, 0.5);
+        }
+      }
+    }
+    .login-btn {
+      width: 424px;
+      height: 70px;
+      background: #ae1f0a;
+      border-radius: 5px 5px 5px 5px;
+      margin-top: 70px;
+      font-size: 24px;
+    }
+  }
+}
+</style>

+ 140 - 0
src/views/messages/index.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="collect-content">
+    <div class="router-title">
+      <span>
+        {{ routerTitle }}
+      </span>
+    
+    </div>
+    <div class="table-body">
+      <a-table :dataSource="dataSource" :columns="columns">
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.key === 'status'">
+            <a-popconfirm
+              :title="
+                record.status === 1 ? '确认撤下该留言?' : '确认选登该留言?'
+              "
+              ok-text="确认"
+              cancel-text="取消"
+            >
+              <a-button type="text">{{
+                record.status === 1 ? "展示" : "不展示"
+              }}</a-button>
+            </a-popconfirm>
+          </template>
+          <template v-if="column.key === 'action'">
+            <div class="action-content">
+              <a-button type="text" @click="showView(record)">查看</a-button>
+
+              <a-popconfirm
+                title="请确认是否删除"
+                ok-text="删除"
+                cancel-text="取消"
+              >
+                <a-button type="text" danger>删除</a-button>
+              </a-popconfirm>
+            </div>
+          </template>
+        </template>
+      </a-table>
+    </div>
+  </div>
+  <View :viewData="viewData" @handleViewCancel="handleViewCancel" />
+</template>
+<script setup>
+import { ref, onMounted, nextTick } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import View from "./view/index.vue";
+const route = useRoute();
+const routerTitle = ref(route.meta.title);
+const open = ref(false);
+const viewData = ref(null);
+const dataSource = ref([
+  {
+    key: "1",
+    name: "胡彦斌",
+    age: 32,
+    address: "西湖区湖底公园1号",
+    status: 0,
+  },
+  {
+    key: "2",
+    name: "胡彦祖",
+    age: 42,
+    address: "西湖区湖底公园1号",
+    status: 1,
+  },
+]);
+const columns = ref([
+  {
+    title: "提交时间",
+    dataIndex: "name",
+    key: "name",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "邮箱",
+    dataIndex: "age",
+    key: "age",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "内容",
+    dataIndex: "address",
+    key: "address",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "状态",
+    dataIndex: "status",
+    key: "status",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+
+  {
+    title: "操作",
+    key: "action",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+]);
+
+const showModal = () => {
+  open.value = true;
+};
+const handleCancel = (e) => {
+  open.value = false;
+};
+const showView = (record) => {
+  viewData.value = record;
+};
+const handleViewCancel = (e) => {
+  viewData.value = null;
+};
+</script>
+<style lang="scss" scoped>
+.collect-content {
+  color: #000;
+  width: 100%;
+  height: 100%;
+  .router-title {
+    height: 60px;
+    font-size: 18px;
+    font-weight: bold;
+    line-height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 20px;
+  }
+}
+</style>

+ 48 - 0
src/views/messages/view/index.vue

@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <a-modal
+      v-model:open="props.viewData"
+      title="查看"
+      @cancel="handleViewCancel"
+      width="480px"
+      :footer="null"
+    >
+      <div>
+        <div>提交时间:{{ props.viewData?.address }}</div>
+        <div>联系人姓名:{{ props.viewData?.name }}</div>
+        <div>联系电话:{{ props.viewData?.age }}</div>
+        <div>省市区/县:{{ props.viewData?.address }}</div>
+        <div>详细地址:{{ props.viewData?.address }}</div>
+        <div>文物概况:{{ props.viewData?.address }}</div>
+      </div>
+    </a-modal>
+  </div>
+</template>
+<script setup>
+import {
+  ref,
+  onMounted,
+  nextTick,
+  defineEmits,
+  defineProps,
+  computed,
+} from "vue";
+const emit = defineEmits(["handleViewCancel"]);
+const props = defineProps({
+  viewData: {
+    type: Object,
+    default: () => null,
+  },
+});
+
+const handleViewCancel = (e) => {
+  emit("handleViewCancel", e);
+};
+
+onMounted(() => {});
+</script>
+<style lang="scss" scoped>
+div {
+  margin: 10px 0;
+}
+</style>

+ 128 - 0
src/views/users/addUser/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div>
+    <!-- :keyboard="false" -->
+    <!-- :maskClosable="false" -->
+    <a-modal
+      v-model:open="props.open"
+      title="新增用户"
+      @cancel="handleCancel"
+      @ok="handleOk"
+      width="480px"
+    >
+      <template #footer>
+        <a-button key="back" @click="handleCancel">取消</a-button>
+        <a-button
+          key="submit"
+          type="primary"
+          :loading="loading"
+          @click="handleOk"
+          >确认</a-button
+        >
+      </template>
+      <a-form
+        ref="formRef"
+        :rules="rules"
+        :model="formState"
+        @finish="onFinish"
+        @finishFailed="onFinishFailed"
+      >
+        <a-form-item label="用户名" name="username">
+          <div class="un-box">
+            <a-input
+              placeholder="请输入内容 6-15字;不能重复"
+              v-model:value="formState.username"
+              maxlength="15"
+            />
+            <p class="error">{{ errorText }}</p>
+          </div>
+        </a-form-item>
+        <a-form-item label="初始密码" name="password">
+          <span>123456</span>
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+<script setup>
+import {
+  ref,
+  reactive,
+  onMounted,
+  nextTick,
+  defineProps,
+  defineEmits,
+} from "vue";
+import { addUser } from "@/api";
+import { message } from "ant-design-vue";
+const emits = defineEmits(["close", "confirm"]);
+const props = defineProps({
+  open: {
+    type: Boolean,
+    default: false,
+  },
+});
+const formState = reactive({
+  username: "",
+});
+const loading = ref(false);
+const errorText = ref("");
+const rules = {
+  username: [
+    {
+      required: true,
+      validator: (_, value) => {
+        if (!value || value.length < 6 || value.length > 15) {
+          return Promise.reject(new Error("请输入内容 6-15字;不能重复"));
+        }
+        errorText.value = "";
+        return Promise.resolve();
+      },
+      trigger: ["blur", "change"],
+    },
+  ],
+};
+const formRef = ref(null);
+const handleCancel = (e) => {
+  emits("close");
+  formState.username = "";
+  errorText.value = "";
+};
+const handleOk = (e) => {
+  formRef.value
+    .validate()
+    .then(async () => {
+      // 提交表单
+      console.log("提交表单", formState);
+      loading.value = true;
+      try {
+        let res = await addUser({ username: formState.username });
+        loading.value = false;
+        if (res.code == 0) {
+          handleCancel();
+          emits("confirm");
+
+          message.success("操作成功");
+        } else if (res.code == 10001) {
+          errorText.value = "已存在重复用户名";
+        }
+      } catch (err) {
+        loading.value = false;
+      }
+    })
+    .catch((errorInfo) => {
+      console.log("验证失败:", errorInfo);
+    });
+};
+</script>
+<style lang="scss" scoped>
+.un-box {
+  position: relative;
+  .error {
+    position: absolute;
+    bottom: -100%;
+    left: 0;
+    font-size: 12px;
+    color: red;
+  }
+}
+</style>

+ 226 - 0
src/views/users/index.vue

@@ -0,0 +1,226 @@
+<template>
+  <div class="collect-content">
+    <div class="router-title">
+      <span>
+        {{ routerTitle }}
+      </span>
+    </div>
+    <div class="controls-box">
+      <div class="scarch-content">
+        <span>搜索项:</span>
+        <div>
+          <a-input-search
+            v-model:value="pagination.username"
+            placeholder="请输入用户名"
+            enter-button
+            @search="onSearch"
+          />
+        </div>
+        <a-button @click="resetSearch">重置</a-button>
+      </div>
+      <a-button type="primary" @click="showModal">新增</a-button>
+    </div>
+    <div class="table-body">
+      <a-config-provider>
+        <a-table
+          :scroll="{ x: 0, y: 600 }"
+          :pagination="pagination"
+          @change="handleTableChange"
+          :loading="loading"
+          :dataSource="dataSource"
+          :columns="columns"
+          emptyText="暂无数据"
+        >
+          <template #bodyCell="{ column, record }">
+            <template v-if="column.key === 'isAdmin'">
+              <span v-if="record.isAdmin == 1">管理员</span>
+              <span v-else>普通成员</span>
+            </template>
+            <template v-if="column.key === 'action'">
+              <div class="action-content" v-if="record.isAdmin != 1">
+                <a-button type="text" @click="resetPassword(record.id)"
+                  >重置密码</a-button
+                >
+
+                <a-popconfirm
+                  title="请确认是否删除"
+                  ok-text="删除"
+                  cancel-text="取消"
+                  @confirm="deleteConfirm(record.id)"
+                >
+                  <a-button type="text" danger>删除</a-button>
+                </a-popconfirm>
+              </div>
+              <span v-else>/</span>
+            </template>
+          </template>
+        </a-table>
+      </a-config-provider>
+    </div>
+  </div>
+
+  <AddUser :open="open" @close="open = false" @confirm="getData" />
+</template>
+<script setup>
+import { ref, reactive, onMounted, nextTick } from "vue";
+
+import { useRouter, useRoute } from "vue-router";
+import { message } from "ant-design-vue";
+import { userList, delUser, resetUserPassword } from "@/api";
+import AddUser from "./addUser/index.vue";
+const route = useRoute();
+const routerTitle = ref(route.meta.title);
+const open = ref(false);
+const loading = ref(false);
+const onFinish = (values) => {
+  console.log("Success:", values);
+};
+const onFinishFailed = (errorInfo) => {
+  console.log("Failed:", errorInfo);
+};
+
+const deleteConfirm = async (id) => {
+  let res = await delUser(id);
+  if (res.code == 0) {
+    getData();
+    message.success("操作成功");
+  }
+};
+
+const handleTableChange = (pag, filters, sorter) => {
+  console.log("分页回调pag, filters, sorter", pag, filters, sorter);
+
+  pagination.value.current = pag.current;
+  pagination.value.pageSize = pag.pageSize;
+  getData();
+};
+const dataSource = ref([]);
+const columns = ref([
+  {
+    title: "用户名",
+    dataIndex: "username",
+    key: "username",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "角色",
+    dataIndex: "isAdmin",
+    key: "isAdmin",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+  {
+    title: "创建时间",
+    dataIndex: "createTime",
+    key: "createTime",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+
+  {
+    title: "操作",
+    key: "action",
+    ellipsis: true,
+    width: "50%",
+    align: "center",
+  },
+]);
+const showModal = () => {
+  open.value = true;
+};
+const resetPassword = (userId) => {
+  loading.value = true;
+  resetUserPassword(userId)
+    .then((res) => {
+      loading.value = false;
+      if (res.code == 0) {
+        message.success("密码已重置为123456");
+      } else {
+        message.success(res.message);
+      }
+    })
+    .catch((err) => {
+      loading.value = false;
+    });
+};
+const onSearch = () => {
+  console.log("搜索内容:", pagination.value.username);
+  getData();
+};
+const resetSearch = () => {
+  pagination.value.username = "";
+  console.log("重置搜索");
+  getData();
+};
+const pagination = ref({
+  total: 0,
+  current: 1,
+  pageSize: 10,
+  username: "",
+  hideOnSinglePage: true,
+});
+const getData = async () => {
+  loading.value = true;
+  let params = {
+    pageNo: pagination.value.current,
+    pageSize: pagination.value.pageSize,
+    username: pagination.value.username,
+  };
+  try {
+    let res = await userList(params);
+    if (res.code == 0) {
+      dataSource.value = res.data.pageData;
+      pagination.value.total = res.data.total;
+      pagination.value.current = res.data.pageNum;
+      pagination.value.pageSize = res.data.pageSize;
+      loading.value = false;
+    }
+  } catch (err) {
+    loading.value = false;
+  }
+};
+onMounted(() => {
+  getData();
+});
+</script>
+<style lang="scss" scoped>
+.collect-content {
+  color: #000;
+  width: 100%;
+  height: 100%;
+  .router-title {
+    height: 60px;
+    font-size: 18px;
+    font-weight: bold;
+    line-height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 20px;
+  }
+}
+.controls-box {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+  height: 60px;
+  padding: 0 20px;
+  .scarch-content {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    line-height: normal;
+    span {
+      margin-right: 8px;
+    }
+    .ant-input-search {
+      width: 200px;
+    }
+  }
+}
+</style>

+ 36 - 0
vite.config.js

@@ -0,0 +1,36 @@
+import { fileURLToPath, URL } from "node:url";
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+
+const isDev = process.env.NODE_ENV === "development";
+// import vueDevTools from 'vite-plugin-vue-devtools'
+
+// https://vite.dev/config/
+export default defineConfig({
+  base: "./",
+  build: {
+    // assetsDir: "assets",
+    // outDir: 'dist', // 输出目录
+    // emptyOutDir: true,
+    // copyPublicDir: false,
+  },
+  plugins: [
+    vue(),
+    // vueDevTools(),
+  ],
+  server: {
+    host: "0.0.0.0",
+    proxy: {
+      "/zytwjng": {
+        target: "http://192.168.0.73:8180", // 后端服务地址
+        changeOrigin: false, // 是否改变源地址
+        // rewrite: (path) => path.replace(/^\/api/, ""),
+      },
+    },
+  },
+  resolve: {
+    alias: {
+      "@": fileURLToPath(new URL("./src", import.meta.url)),
+    },
+  },
+});