Explorar el Código

📦 管理员审核页和阿里云上传

快乐的梦鱼 hace 2 meses
padre
commit
eb9698eb83

+ 56 - 0
package-lock.json

@@ -27,12 +27,14 @@
         "quill-image-uploader": "^1.3.0",
         "tslib": "^2.8.1",
         "vue": "^3.5.18",
+        "vue-clipboard3": "^2.0.0",
         "vue-router": "^4.5.1",
         "vue3-carousel": "^0.15.0"
       },
       "devDependencies": {
         "@inquirer/prompts": "^7.5.3",
         "@tsconfig/node22": "^22.0.2",
+        "@types/ali-oss": "^6.16.11",
         "@types/node": "^22.16.5",
         "@types/nprogress": "^0.2.3",
         "@vitejs/plugin-vue": "^6.0.1",
@@ -4325,6 +4327,13 @@
         "tslib": "^2.4.0"
       }
     },
+    "node_modules/@types/ali-oss": {
+      "version": "6.16.11",
+      "resolved": "https://registry.npmjs.org/@types/ali-oss/-/ali-oss-6.16.11.tgz",
+      "integrity": "sha512-/AyemPZy93ZXGzEokMsoPFgjH37snpzH4X/fwans/n63HLaCleriCG3PyrkHCPkgHEc9vj9Uo6paqsBN3vJ3OA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/estree": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -5877,6 +5886,17 @@
         "node": ">= 12"
       }
     },
+    "node_modules/clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "license": "MIT",
+      "dependencies": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
     "node_modules/clipboardy": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz",
@@ -6688,6 +6708,12 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
+      "license": "MIT"
+    },
     "node_modules/denque": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -8030,6 +8056,15 @@
         "node": ">=0.6.0"
       }
     },
+    "node_modules/good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "license": "MIT",
+      "dependencies": {
+        "delegate": "^3.1.2"
+      }
+    },
     "node_modules/gopd": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -11991,6 +12026,12 @@
         "get-ready": "~1.0.0"
       }
     },
+    "node_modules/select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
+      "license": "MIT"
+    },
     "node_modules/semver": {
       "version": "6.3.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -12868,6 +12909,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
+      "license": "MIT"
+    },
     "node_modules/tiny-invariant": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -14164,6 +14211,15 @@
         "ufo": "^1.6.1"
       }
     },
+    "node_modules/vue-clipboard3": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/vue-clipboard3/-/vue-clipboard3-2.0.0.tgz",
+      "integrity": "sha512-Q9S7dzWGax7LN5iiSPcu/K1GGm2gcBBlYwmMsUc5/16N6w90cbKow3FnPmPs95sungns4yvd9/+JhbAznECS2A==",
+      "license": "MIT",
+      "dependencies": {
+        "clipboard": "^2.0.6"
+      }
+    },
     "node_modules/vue-devtools-stub": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz",

+ 2 - 0
package.json

@@ -33,12 +33,14 @@
     "quill-image-uploader": "^1.3.0",
     "tslib": "^2.8.1",
     "vue": "^3.5.18",
+    "vue-clipboard3": "^2.0.0",
     "vue-router": "^4.5.1",
     "vue3-carousel": "^0.15.0"
   },
   "devDependencies": {
     "@inquirer/prompts": "^7.5.3",
     "@tsconfig/node22": "^22.0.2",
+    "@types/ali-oss": "^6.16.11",
     "@types/node": "^22.16.5",
     "@types/nprogress": "^0.2.3",
     "@vitejs/plugin-vue": "^6.0.1",

+ 25 - 5
src/api/auth/UserApi.ts

@@ -1,5 +1,6 @@
 import { DataModel } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from '../RequestModules';
+import AppCofig from '@/common/config/AppCofig';
 
 
 export class LoginResult extends DataModel<LoginResult> {
@@ -8,12 +9,21 @@ export class LoginResult extends DataModel<LoginResult> {
     this._convertTable = {
       token: { clientSide: 'string', clientSideRequired: true },
     };
+    this._beforeSolveServer = (data, self) => {
+      data.token = (data.userinfo as any).token;
+      return data;
+    }
     this._afterSolveServer = () => {
-      this.userInfo.id = this.id;
-      this.userInfo.mobile = this.mobile;
-      this.userInfo.nickname = this.nickname;
-      this.userInfo.avatar = this.avatar;
-      this.userInfo.username = this.username;
+      if (!this.userInfo.id)
+        this.userInfo.id = this.id;
+      if (!this.userInfo.mobile)
+        this.userInfo.mobile = this.mobile;
+      if (!this.userInfo.nickname)
+        this.userInfo.nickname = this.nickname;
+      if (!this.userInfo.avatar)
+        this.userInfo.avatar = this.avatar;
+      if (!this.userInfo.username)
+        this.userInfo.username = this.username;
     }
   }
   id = 0;
@@ -29,6 +39,7 @@ export class LoginResult extends DataModel<LoginResult> {
   expiretime = null as number|null;
   expiresIn = null as number|null;
   inheritorId = null as number|null;
+  loginType = 0;
   userInfo = new UserInfo();
 }
 export class UserInfo extends DataModel<UserInfo> {
@@ -59,6 +70,15 @@ export class UserApi extends AppServerRequestModule<DataModel> {
   }) {
     return (await this.post('/ich/inheritor/login', data, '登录', undefined, LoginResult)).data as LoginResult;
   }
+  async loginAdmin(data: {
+    account: string,
+    password: string,
+  }) {
+    return (await this.post('/user/adminLogin', {
+      account: data?.account,
+      password: data?.password,
+    }, '登录', undefined, LoginResult)).data as LoginResult;
+  }
 
   async refresh() {
     return (await this.post('/ich/inheritor/refresh', {}, '刷新token', undefined, LoginResult)).data as LoginResult;

+ 55 - 20
src/api/inheritor/InheritorContent.ts

@@ -1,6 +1,7 @@
-import { DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { DataModel, transformArrayDataModel, transformDataModel } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from '../RequestModules';
 import dayjs from 'dayjs';
+import { transformSomeToArray } from '../Utils';
 
 export class CommonInfo<T extends DataModel> extends DataModel<T> {
 
@@ -75,6 +76,8 @@ export class IchInfo extends CommonInfo<IchInfo> {
       data.latitude = this.lonlat[1];
     };
     this._beforeSolveClient = (data) => {
+      if (!this.expandInfo)
+        this.expandInfo = new IchExpandInfo();
       this.expandInfo.batch = this.batch;
       this.expandInfo.region = this.region;
       this.expandInfo.image = this.image;
@@ -86,7 +89,7 @@ export class IchInfo extends CommonInfo<IchInfo> {
   }
 
   lonlat = [] as (number|string)[];
-  expandInfo = new IchExpandInfo();
+  expandInfo : IchExpandInfo|null = new IchExpandInfo();
 
   id = 0 as number;
   modelId = 2;
@@ -228,7 +231,7 @@ export class InheritorInfo extends CommonInfo<InheritorInfo> {
     };
   }
 
-  expandInfo = new InheritorExpandInfo();
+  expandInfo : InheritorExpandInfo|null = new InheritorExpandInfo();
 
   id = 0 as number;
   modelId = 7;
@@ -369,7 +372,7 @@ export class SeminarInfo extends CommonInfo<SeminarInfo> {
   }
 
   lonlat = [] as (number|string)[];
-  expandInfo = new SeminarExpandInfo();
+  expandInfo : SeminarExpandInfo|null = new SeminarExpandInfo();
 
   id = 0 as number;
   modelId = 17;
@@ -459,6 +462,21 @@ export class PlanInfo extends DataModel<PlanInfo> {
   ichName = '' as string;
   progressText = '' as string;
 }
+export class InheritorAccountInfo extends DataModel<InheritorAccountInfo> {
+  constructor() {
+    super(InheritorAccountInfo, "传承人账号信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      username: { clientSide: 'string', clientSideRequired: true },
+      password: { clientSide: 'string', clientSideRequired: true },
+    };
+  }
+
+  id = 0 as number;
+  username = '' as string;
+  password = '' as string;
+  nickname = '' as string;
+}
 
 export class InheritorContentApi extends AppServerRequestModule<DataModel> {
 
@@ -466,9 +484,10 @@ export class InheritorContentApi extends AppServerRequestModule<DataModel> {
     super();
   }
 
-  async getBaseInfo<T extends DataModel>(newDataModel: new () => T) {
+  async getBaseInfo<T extends DataModel>(id: number|undefined, newDataModel: new () => T) {
     return (await this.post('/ich/inheritor/baseInfo', {
       model_id: new newDataModel().modelId,
+      id,
     }, '基础表信息', undefined, newDataModel)).data as T;
   }
   /**
@@ -490,10 +509,15 @@ export class InheritorContentApi extends AppServerRequestModule<DataModel> {
   async saveBaseInfo<T extends DataModel>(dataModel: T) {
     return (await this.post('/ich/inheritor/saveBase', dataModel.toServerSide(), '基础内容表采集(非遗,传承人,传习所)'));
   }
-  async getExpandInfo<T extends DataModel>(newDataModel: new () => T) {
-    return (await this.post('/ich/inheritor/expandInfo', {
+  async getExpandInfo<T extends DataModel>(id: number|undefined, newDataModel: new () => T) : Promise<T | null> {
+    return this.post('/ich/inheritor/expandInfo', {
       model_id: new newDataModel().modelId,
-    }, '扩展表信息', undefined, newDataModel)).data as T;
+      id,
+    }, '扩展表信息', undefined).then((res) => {
+      if (!res.data2) 
+        return null;
+      return transformDataModel(newDataModel, res.data2) as T;
+    })
   }
   async saveExpandInfo<T extends DataModel>(dataModel: T) {
     return (await this.post('/ich/inheritor/saveExpand', dataModel.toServerSide(), '扩展内容表采集(非遗,传承人,传习所)'));
@@ -502,23 +526,34 @@ export class InheritorContentApi extends AppServerRequestModule<DataModel> {
     return (await this.post('/ich/inheritor/savePlans', dataModel.toServerSide(), '保存项目五年计划'));
   }
 
-  async getIchInfo() {
-    return await this.getBaseInfo(IchInfo);
+  async getInheritorAccountInfo(contentId: number) {
+    return this.post('/ich/inheritor/getAccount', {
+      content_id: contentId,
+    }, '获取传承人账号信息', undefined).then((res) => {
+      const arr = transformSomeToArray(res.data2);
+      if (arr.length === 0)
+        return null;
+      return transformDataModel(InheritorAccountInfo, arr[0]);
+    })
+  }
+
+  async getIchInfo(id: number|undefined) {
+    return await this.getBaseInfo(id, IchInfo);
   }
-  async getInheritorInfo() {
-    return await this.getBaseInfo(InheritorInfo);
+  async getInheritorInfo(id: number|undefined) {
+    return await this.getBaseInfo(id, InheritorInfo);
   }
-  async getSeminarInfo() {
-    return await this.getBaseInfo(SeminarInfo);
+  async getSeminarInfo(id: number|undefined) {
+    return await this.getBaseInfo(id, SeminarInfo);
   }
-  async getIchExpandInfo() {
-    return await this.getExpandInfo(IchExpandInfo);
+  async getIchExpandInfo(id: number|undefined) {
+    return await this.getExpandInfo(id, IchExpandInfo);
   }
-  async getInheritorExpandInfo() {
-    return await this.getExpandInfo(InheritorExpandInfo);
+  async getInheritorExpandInfo(id: number|undefined) {
+    return await this.getExpandInfo(id, InheritorExpandInfo);
   }
-  async getSeminarExpandInfo() {
-    return await this.getExpandInfo(SeminarExpandInfo);
+  async getSeminarExpandInfo(id: number|undefined) {
+    return await this.getExpandInfo(id, SeminarExpandInfo);
   }
 }
 

+ 2 - 0
src/assets/scss/fix.scss

@@ -1,3 +1,5 @@
+@use "./colors.scss" as *;
+
 .carousel-light {
   --vc-nav-color: #fff;
   --vc-clr-primary: #fff;

+ 7 - 2
src/assets/scss/main.scss

@@ -126,10 +126,10 @@ $small-banner-height: 445px;
   transition: all 0.2s ease; 
 
   &:hover {
-    transform: scale(1.05);
+    transform: scale(1.01);
   }
   &:active {
-    transform: scale(0.95); 
+    transform: scale(0.99); 
   }
 }
 
@@ -269,6 +269,11 @@ $small-banner-height: 445px;
   }
 }
 
+.form-container {
+  max-width: 600px;
+  margin: 0 auto;
+}
+
 //Card box
 
 @media (max-width: 1280px) {

+ 28 - 6
src/assets/scss/news.scss

@@ -67,6 +67,14 @@
     width: 100%;
     text-decoration: none;
 
+    .item-right {
+      flex: 1;
+      display: flex;
+      flex-direction: row;
+      justify-content: flex-end;
+      align-items: center;
+    }
+
     &.row-type2 {
       flex-wrap: wrap;
 
@@ -86,13 +94,21 @@
         height: 180px;
       }
     }
-      &.row-type4 {
-        img {
-          object-fit: contain;
-          width: 270px;
-          height: 150px;
-        }
+    &.row-type4 {
+      img {
+        object-fit: contain;
+        width: 270px;
+        height: 150px;
+      }
+    }
+    &.row-type5 {
+      img {
+        object-fit: cover;
+        border-radius: 50%;
+        width: 80px;
+        height: 80px;
       }
+    }
     &.empty {
       background-color: transparent;
       border: none;
@@ -181,6 +197,12 @@
           height: 110px;
         }
       }
+      &.row-type5 {
+        img {
+          width: 60px;
+          height: 60px;
+        }
+      }
 
       img {
         width: 200px;

+ 50 - 0
src/common/upload/AliOssUploadCo.ts

@@ -0,0 +1,50 @@
+import type { AntUploadRequestOption, UploadCoInterface } from "@/components/dynamicf/UploadImageFormItem";
+import { RandomUtils, StringUtils } from "@imengyu/imengyu-utils";
+import OSS from 'ali-oss';
+
+const client = new OSS({
+  region: 'oss-cn-shenzhen',
+  accessKeyId: 'LTAI5t5e7wAQ1FvUA4LCsNs5',
+  accessKeySecret: 'lhF0SimpatPMHNjmjtIKsWsYwTmJhx',
+  bucket: 'minnanwenhua', 
+});
+
+export function useAliOssUploadCo(subPath: string) : UploadCoInterface {
+  return {
+    requestUploadToken: async (key: string,  bucketNameDa: string, expire ?:number) => {
+      return '';
+    },
+    uploadRequest: async (requestOption: AntUploadRequestOption) => {
+      const uploadPath = `${subPath}/${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(requestOption.file.name)}`;      
+
+      //小于8mb则直接上传,否则分片上传
+      if (requestOption.file.size < 8 * 1024 * 1024) {
+        client.put(uploadPath, requestOption.file).then((res) => {  
+          requestOption.onSuccess?.({
+            url: res.url,
+            key: uploadPath,
+          }, null);
+        }).catch((err) => {
+          requestOption.onError?.(err, {});
+        })
+      } else {
+        client.multipartUpload(requestOption.filename, requestOption.file, {
+          progress(percentage) {
+            requestOption.onProgress({ percent: percentage * 100 })
+          },
+        }).then((res) => {
+          requestOption.onSuccess?.({
+            url: client.generateObjectUrl(uploadPath),
+            key: uploadPath,
+          }, null);
+        }).catch((err) => {
+          requestOption.onError?.(err, {});
+        });
+      }
+    },
+    getUrlByUploadResponse: (response: unknown) => {
+      return (response as any).url as string;
+    },
+  }
+}
+

+ 2 - 1
src/common/upload/ImageUploadCo.ts

@@ -22,4 +22,5 @@ export function useImageSimpleUploadCo(additionData?: Record<string, any>) : Upl
       return (response as any).url as string;
     },
   }
-}
+}
+

+ 2 - 1
src/components/NavBar.vue

@@ -16,7 +16,8 @@
       >
         <div>
           <RouterLink to="/">首页</RouterLink>
-          <RouterLink to="/inheritor">我的</RouterLink>
+          <RouterLink v-if="authStore.loginType === 0" to="/inheritor">我的</RouterLink>
+          <RouterLink v-else-if="authStore.loginType === 1" to="/admin">管理员</RouterLink>
         </div>
       </div>
     </Teleport>

+ 10 - 5
src/components/content/CommonListBlock.vue

@@ -115,6 +115,9 @@
                 </div>
               </template>
             </TitleDescBlock>
+            <div class="item-right">
+              <slot name="itemRight" :index="k" :item="item" />
+            </div>
           </div>
           <div 
             v-for="count of placeholderItemCount"
@@ -135,7 +138,7 @@
 
 <script setup lang="ts">
 import { computed, onMounted, ref, watch, type PropType } from 'vue';
-import { useSSrSimplePagerDataLoader } from '@/composeable/SimplePagerDataLoader';
+import { useSimplePagerDataLoader } from '@/composeable/SimplePagerDataLoader';
 import TagBar from '../content/TagBar.vue';
 import Dropdown from '../controls/Dropdown.vue';
 import SimpleInput from '../controls/SimpleInput.vue';
@@ -143,6 +146,7 @@ import SimplePageContentLoader from '@/components/content/SimplePageContentLoade
 import Pagination from '../controls/Pagination.vue';
 import TitleDescBlock from '../parts/TitleDescBlock.vue';
 import IconSearch from '../icons/IconSearch.vue';
+import { useRoute, useRouter } from 'vue-router';
 
 export interface DropdownCommonItem {
   id: number; 
@@ -264,9 +268,8 @@ const props = defineProps({
 const router = useRouter();
 
 const realRowCount = computed(() => {
-  if (import.meta.client)
-    if (window.innerWidth < 768) 
-      return 1;
+   if (window.innerWidth < 768) 
+    return 1;
   return props.rowCount;
 });
 const rowWidth = computed(() => {
@@ -301,6 +304,8 @@ function handleChangeDropDownValue(index: number, value: number) {
 function handleShowDetail(item: any) {
   if (props.showDetail)
     return props.showDetail(item);
+  if (props.detailsPage === 'none')
+    return;
   router.push({ 
     path: props.detailsPage,
     query: {
@@ -315,7 +320,7 @@ const selectedTag = ref(props.defaultSelectTag);
 const pageSize = ref(props.pageSize);
 const route = useRoute();
 
-const newsLoader = await useSSrSimplePagerDataLoader(route.fullPath + '/list' + props.subName, Number(route.query.page || 1), pageSize, (page, size) => props.load(
+const newsLoader = useSimplePagerDataLoader(pageSize, (page, size) => props.load(
   page, size, 
   selectedTag.value, 
   searchText.value,

+ 0 - 130
src/components/content/CommonListPage.vue

@@ -1,130 +0,0 @@
-<template>
-  <!-- 资讯详情页 -->
-  <div class="main-background">
-    <div class="nav-placeholder"></div>
-    <!-- 新闻 -->
-    <section class="main-section main-background main-background-type0 small-h">
-      <div class="content mb-2">
-        <!-- 路径 -->
-        <a-breadcrumb>
-          <a-breadcrumb-item><a href="javascript:;" @click="navTo('/')">首页</a></a-breadcrumb-item>
-          <a-breadcrumb-item v-if="prevPage"><a href="javascript:;" @click="prevPage.url ? navTo(prevPage.url) : back()">{{ prevPage.title }}</a></a-breadcrumb-item>
-          <a-breadcrumb-item>{{ title }}</a-breadcrumb-item>
-        </a-breadcrumb>
-      </div>
-      <CommonListBlock v-bind="props"></CommonListBlock>
-    </section>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { type PropType } from 'vue';
-import { usePageAction } from '@/composeable/PageAction';
-import CommonListBlock from './CommonListBlock.vue';
-import type { DropdownCommonItem, DropDownNames } from './CommonListBlock.vue';
-
-export type { DropdownCommonItem, DropDownNames }
-
-const { navTo, back } = usePageAction();
-
-const props = defineProps({	
-  title: {
-    type: String,
-    default: '',
-  },
-  prevPage: {
-    type: Object as PropType<{
-      title: string,
-      url?: string,
-    }>,
-    default: null,
-  },
-  dropDownNames: {
-    type: Object as PropType<DropDownNames[]>,
-    default: null,
-  },
-  showSearch: {
-    type: Boolean,
-    default: true,
-  },
-  showTableSwitch: {
-    type: Boolean,
-    default: false, 
-  },
-  tableSwitchOptions: {
-    type: Object,
-    default: () => ({}), 
-  },
-  tagsData: {
-    type: Object as PropType<{
-      id: number,
-      name: string,
-    }[]>,
-    default: null,
-  },
-  pageSize: {
-    type: Number,
-    default: 8,
-  },
-  rowCount: {
-    type: Number,
-    default: 2,
-  },
-  rowType: {
-    type: Number,
-    default: 1,
-  },
-  defaultSelectTag: {
-    type: Number,
-    default: 1,
-  },
-  load: {
-    type: Function as PropType<(
-      page: number, 
-      pageSize: number,
-      selectedTag: number,
-      searchText: string,
-      dropDownValues: number[],
-    ) => Promise<{
-      page: number,
-      total: number,
-      data: any[],
-    }>>,
-    required: true,
-  },
-  showDetail: {
-    type: Function as PropType<(item: any) => void>,
-    default: null,
-  },
-  /**
-   * 点击详情跳转页面路径
-   */
-  detailsPage: {
-    type: String,
-    default: '/news/detail'
-  },
-  /**
-   * 详情跳转页面参数
-   */
-  detailsParams: {
-    type: Object as PropType<Record<string, any>>,
-    default: () => ({})
-  },
-  defaultImage: {
-    type: String,
-    default: 'https://mn.wenlvti.net/app_static/minnan/EmptyImage.png'
-  },
-})
-</script>
-
-<style lang="scss">
-@use "@/assets/scss/colors";
-
-.search-icon {
-  width: 25px;
-  height: 25px;
-  cursor: pointer;
-  color: colors.$primary-color;
-}
-</style>
-

+ 1 - 1
src/components/content/SimplePageContentLoader.vue

@@ -33,8 +33,8 @@
 </template>
 
 <script setup lang="ts">
+import type { ILoaderCommon } from '@/composeable/LoaderCommon';
 import { onMounted, ref, type PropType } from 'vue';
-import type { ILoaderCommon } from '../../composeable/LoaderCommon';
 
 const props = defineProps({	
   loader: {

+ 21 - 9
src/components/dynamicf/Editor/QuillEditorWrapper.vue

@@ -1,13 +1,15 @@
 <template>
-  <QuillEditor
-    :modules="modules" 
-    :toolbar="toolbarOptions" 
-    theme="snow"
-    contentType="html"
-    v-bind="$attrs"
-    :content="props.modelValue"
-    @update:content="(val: string) => emit('update:modelValue', val)"
-  />
+  <div class="quill-editor-wrapper">
+    <QuillEditor
+      :modules="modules" 
+      :toolbar="toolbarOptions" 
+      theme="snow"
+      contentType="html"
+      v-bind="$attrs"
+      :content="props.modelValue"
+      @update:content="(val: string) => emit('update:modelValue', val)"
+    />
+  </div>
 </template>
 
 <script lang="ts" setup>
@@ -96,6 +98,16 @@ const modules = [
 
 <style lang="scss">
 
+.quill-editor-wrapper {
+  position: relative;
+
+  .ql-container {
+    max-height: 600px;
+    overflow: hidden;
+    overflow-y: scroll;
+  }
+}
+
 $sizeList: (
   '12px': '小四',
   '14px': '四号',

+ 3 - 3
src/components/dynamicf/SimpleSelectFormItem.ts

@@ -17,7 +17,7 @@ export interface SimpleSelectFormItemProps {
   /**
    * 是否禁用
    */
-  disabled: boolean;
+  disabled?: boolean;
   /**
    * 选项数据
    */
@@ -25,11 +25,11 @@ export interface SimpleSelectFormItemProps {
   /**
    * 选择值
    */
-  value: unknown;
+  value?: unknown;
   /**
    * a-select 其他自定义参数
    */
-  customProps: SelectProps;
+  customProps?: SelectProps;
   /**
    * 是否自定义渲染option插槽
    */

+ 21 - 3
src/components/dynamicf/UploadImageFormItem.ts

@@ -83,7 +83,7 @@ export interface AntUploadRequestOption {
   headers: { [index: string]: string; };
   withCredentials: boolean;
   method: string;
-  onProgress: (e: number) => void;
+  onProgress: (e: { percent: number }) => void;
   onSuccess: (ret : { url: string, key: string }, xhr : XMLHttpRequest|null) => void;
   onError: (err : Error|null|undefined, ret : unknown) => void;
 }
@@ -93,7 +93,7 @@ export interface AntUploadRequestOption {
  * @param limitSizeMB 限制大小MB.
  * @returns 
  */
-export function useBeforeUploadImageChecker(limitSizeMB = 8) : (file: FileItem) => boolean {
+export function useBeforeUploadImageChecker(limitSizeMB = 32) : (file: FileItem) => boolean {
   return (file) => {
     const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
     if (!isJpgOrPng) 
@@ -110,7 +110,25 @@ export function useBeforeUploadImageChecker(limitSizeMB = 8) : (file: FileItem)
  * @param limitSizeMB 限制大小MB.
  * @returns 
  */
-export function useBeforeUploadVideoChecker(limitSizeMB = 256) : (file: FileItem) => boolean {
+export function useBeforeUploadAudioChecker(limitSizeMB = 256) : (file: FileItem) => boolean {
+  return (file) => {
+    const isVideo = file.type.startsWith('audio/');
+    if (!isVideo) 
+      message.error('请选择音频文件!');
+    const isLt2M = file.size / 1024 / 1024 < limitSizeMB;
+    if (!isLt2M) 
+      message.error(`音频大小不能大于${limitSizeMB}MB!`);
+    return isVideo && isLt2M;
+  };
+}
+
+
+/**
+ * 上传视频大小检查组合代码。
+ * @param limitSizeMB 限制大小MB.
+ * @returns 
+ */
+export function useBeforeUploadVideoChecker(limitSizeMB = 4096) : (file: FileItem) => boolean {
   return (file) => {
     const isVideo = file.type.startsWith('video/');
     if (!isVideo) 

+ 16 - 37
src/components/dynamicf/UploadVideoFormItem.vue

@@ -3,39 +3,17 @@
     v-bind="customProps"
     :disabled="disabled"
     v-model:file-list="uploadSubImgList"
-    list-type="text"
+    list-type="picture"
     :class="uploadClass"
-    :max-count="maxCount"
-    :show-upload-list="!single"
+    :max-count="single ? 1 : maxCount"
     :customRequest="handleUpload"
     :before-upload="beforeUpload"
     @change="handleUploadSubImgChange"
   >
-    <template v-if="single">
-      <div v-if="value != ''" class="ant-upload-video">
-        已选择,点击替换视频
-        <video 
-          :src="(value as string)"
-          alt="avatar"
-          controls
-          :width="singleImageSize.width"
-          :height="singleImageSize.height"
-          :preview="false"
-        />
-      </div>
-      <div v-else :style="{ width: singleImageSize.width, height: singleImageSize.height }" class="ant-upload-video">
-        <loading-outlined v-if="uploadingSubImg"></loading-outlined>
-        <plus-outlined v-else></plus-outlined>
-        <div class="ant-upload-text">上传</div>
-      </div>
-    </template>
-    <template v-else>
-      <div class="ant-upload-video">
-        <loading-outlined v-if="uploadingSubImg"></loading-outlined>
-        <plus-outlined v-else></plus-outlined>
-        上传
-      </div>
-    </template>
+    <a-button>
+      <upload-outlined></upload-outlined>
+      上传
+    </a-button>
   </a-upload>
 </template>
 
@@ -48,7 +26,7 @@ import {
   type AntUploadRequestOption, type FileInfo, type FileItem 
 } from './UploadImageFormItem';
 import { message, type UploadProps } from 'ant-design-vue';
-import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue';
+import { UploadOutlined } from '@ant-design/icons-vue';
 import { type PropType, ref, onMounted, watch } from 'vue';
 import FailImage from '@/assets/images/imageFailed.png';
 
@@ -128,19 +106,20 @@ const emits = defineEmits([
 const uploadSubImgList = ref<FileItem[]>([]);
 const uploadingSubImg = ref(false);
 
+function loadValue() {
+uploadSubImgList.value = stringUrlsToUploadedItems(
+  props.value instanceof Array ? (props.value as string[] || []) : [
+    props.value as string
+  ])
+}
+
 onMounted(() => {
   //将之前上传的图片包括URL设置到已上传列表中
-  if (!props.single) {
-    setTimeout(() => {
-      uploadSubImgList.value = stringUrlsToUploadedItems(props.value instanceof Array ? (props.value as string[] || []) : [])
-    }, 400);
-  }
+  setTimeout(loadValue, 400);
 });
 
 watch(() => props.value, () => {
-  if (!props.single) {
-    uploadSubImgList.value = stringUrlsToUploadedItems(props.value instanceof Array ? (props.value as string[] || []) : [])
-  }
+  loadValue();
 });
 
 function handleUpload(requestOption: AntUploadRequestOption) {

+ 10 - 2
src/components/parts/EmptyToRecord.vue

@@ -13,9 +13,17 @@ defineProps({
     type: String,
     default: '去补充'
   },
+  emptyText: {
+    type: String,
+    default: ''
+  },
   showEdited: {
     type: Boolean,
     default: true
+  },
+  showAdd: {
+    type: Boolean,
+    default: true
   }
 })
 </script>
@@ -25,10 +33,10 @@ defineProps({
     v-if="!model"
     status="404"
     :title="`${title}信息`"
-    :subTitle="`暂无${title}信息,快去补充`"
+    :subTitle="emptyText || `暂无${title}信息,快去补充`"
   > 
     <template #extra>
-      <a-button type="primary" @click="emit('edit')">{{ buttonText }}</a-button>
+      <a-button v-if="showAdd" type="primary" @click="emit('edit')">{{ buttonText }}</a-button>
     </template>
   </a-result>
   <div v-else>

+ 150 - 0
src/components/parts/TitleDescBlock.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="TitleDescBlock">
+    <h3>{{ title }}</h3>
+    <span v-if="date" class="time">{{ date }}</span>
+    <SimpleRichHtml hydrate-never :class="'desc ' + (expand?'expand':'no-expand')" :contents="[desc]" />
+    <slot name="addon" />
+
+    <div class="footer">
+      <div v-if="showExpand && desc.length > 200" :class="'expand'+(expand?' on':'')" @click="expand=!expand">
+        {{expand?'折叠':'展开'}}
+        <img src="@/assets/images/IconArrowRight.png" />
+      </div>
+      <div v-if="more" class="more" @click="moreLink ? undefined : emit('moreClick')">
+        <RouterLink :to="moreLink">
+          更多
+          <img src="@/assets/images/IconArrowRight.png" alt="更多" />
+        </RouterLink>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import SimpleRichHtml from '../display/SimpleRichHtml.vue';
+
+const props = defineProps({	
+  title : {
+    type: String,
+    default: '',
+  },
+  desc: {
+    type: String,
+    default: '',
+  },
+  descLines: {
+    type: Number,
+    default: 3,
+  },
+  more: {
+    type: Boolean,
+    default: false,
+  },
+  moreLink: {
+    type: String,
+    default: '',
+  },
+  showExpand: {
+    type: Boolean,
+    default: false,
+  },
+  date: {
+    type: String,
+    default: '',
+  },
+})
+
+const expand = ref(false)
+
+const emit = defineEmits([	
+  "moreClick"	
+]);
+
+</script>
+
+<style lang="scss">
+@use '@/assets/scss/colors.scss' as *;
+
+.TitleDescBlock {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+
+  h3 {
+    color: $text-content-color;
+    font-size: 1.2rem;
+    margin-top: 0;
+    margin-bottom: 8px;
+  }
+
+  .desc,
+  .time {
+    color: $text-content-second-color;
+    font-size: 0.85rem;
+  }
+
+  .time {
+    margin-bottom: 16px;
+  }
+  > .desc {
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    margin: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+
+    &.expand {
+      -webkit-line-clamp: 100;
+      line-clamp: 100;
+    }
+    &.no-expand {
+      -webkit-line-clamp: 5;
+      line-clamp: 5;
+      max-height: 300px; 
+    }
+  }
+
+  .footer {
+    display: flex;
+    flex-direction: row;
+    align-items: center; 
+    justify-content: space-between;
+
+    > div {
+      display: flex;
+      flex-direction: row;
+      align-items: center; 
+      color: $text-content-second-color;
+      font-size: 0.85rem;
+      margin-top: 25px;
+      cursor: pointer;
+    
+      &:hover {
+        color: $primary-color;
+      }
+    }
+  }
+
+  .expand {
+    img {
+      width: 16px;
+      height: 16px;
+      transform: rotate(90deg);
+    }
+    &.on {
+      img {
+        transform: rotate(270deg); 
+      }
+    }
+  }
+
+  .more {
+    img {
+      width: 16px;
+      height: 16px;
+      margin-left: 8px;
+    }
+  }
+}
+</style>

+ 9 - 0
src/composeable/LoaderCommon.ts

@@ -0,0 +1,9 @@
+import type { Ref } from "vue";
+
+export type LoaderLoadType = 'loading' | 'finished' | 'nomore' | 'error';
+
+export interface ILoaderCommon<P> {
+  loadError: Ref<string>;
+  loadStatus: Ref<LoaderLoadType>;
+  loadData: (params?: P, refresh?: boolean) => Promise<void>;
+}

+ 109 - 0
src/composeable/SimpleDataLoader.ts

@@ -0,0 +1,109 @@
+import { onMounted, ref, type Ref } from "vue";
+import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+
+export interface ISimpleDataLoader<T, P> extends ILoaderCommon<P> {
+  content: Ref<T|null>;
+  getLastParams: () => P | undefined;
+}
+
+export function useSimpleDataLoader<T, P = any>(
+  loader: (params?: P) => Promise<T>,
+  loadWhenMounted = true,
+  emptyIfArrayEmpty = true,
+)  : ISimpleDataLoader<T, P>
+ {
+
+  const content = ref<T|null>(null) as Ref<T|null>;
+  const loadStatus = ref<LoaderLoadType>('loading');
+  const loadError = ref('');
+
+  let lastParams: P | undefined;
+
+  async function loadData(params?: P) {
+    if (params)
+      lastParams = params;
+    loadStatus.value = 'loading';
+    try {
+      const res = (await loader(params ?? lastParams)) as T;
+      content.value = res;
+      if (Array.isArray(res) && emptyIfArrayEmpty && (res as any[]).length === 0)
+        loadStatus.value = 'nomore';
+      else
+        loadStatus.value = 'finished';
+      loadError.value = '';
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+      console.log(e);
+      
+    }
+  }
+
+  onMounted(() => {
+    if (loadWhenMounted) {
+      setTimeout(() => {
+        loadData();
+      }, (0.5 + Math.random()) * 500);
+    }
+  })
+
+  return {
+    content,
+    loadStatus,
+    loadError,
+    loadData,
+    getLastParams: () => lastParams,
+  }
+}
+
+export async function useSSrSimpleDataLoader<T, P = any>(
+  name: string,
+  loader: (params?: P) => Promise<T>,
+  params : P|undefined = undefined,
+  emptyIfArrayEmpty = true,
+)  : Promise<ISimpleDataLoader<T, P>>
+ {
+  const route = useRoute();
+
+  let lastParams: P | undefined = params;
+  const loadStatus = ref<LoaderLoadType>('finished');
+  const loadError = ref('');
+  const { data: content, error } = (await useAsyncData(route.fullPath + '/' + name, () => loader(lastParams)))
+
+
+  async function loadData(params?: P, refresh: boolean = false) {
+    if (!import.meta.client)
+      return;
+    if (params)
+      lastParams = params;
+    loadStatus.value = 'loading';
+    try {
+      const res = await loader(params ?? lastParams) as T;
+      content.value = res as any;
+      if (Array.isArray(res) && emptyIfArrayEmpty && (res as any[]).length === 0)
+        loadStatus.value = 'nomore';
+      else
+        loadStatus.value = 'finished';
+      loadError.value = '';
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+      console.log(e);
+    }
+  }
+
+  watch(error, (e) => {
+    if (e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+    }
+  }, { immediate: true });
+  
+  return {
+    content: content as Ref<T|null>,
+    loadStatus,
+    loadError,
+    loadData,
+    getLastParams: () => lastParams,
+  }
+}

+ 128 - 0
src/composeable/SimplePagerDataLoader.ts

@@ -0,0 +1,128 @@
+import { watch, ref, computed, type Ref } from "vue"
+import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+
+export interface ISimplePageListLoader<T, P> extends ILoaderCommon<P> {
+  list: Ref<T[]>;
+  page: Ref<number>;
+  next: () => Promise<void>;
+  prev: () => Promise<void>;
+  total: Ref<number>;
+  totalPages: Ref<number>;
+}
+
+/**
+ * 简单分页数据封装。
+ * 
+ * 该封装了分页数据的加载、分页、上一页、下一页等功能。当页码发生变化时,会自动调用加载函数。
+ * 简单分页同时只能显示一页数据,重新加载会覆盖之前的数据。
+ * 
+ * 使用示例:
+ * ```ts
+ * const { data, page, total, loading } = useSimplePagerDataLoader(10, async (page, pageSize) => {
+ *   const res = await fetch(`/api/data?page=${page}&pageSize=${pageSize}`);
+ *   const data = await res.json();  
+ *   return {
+ *     data,
+ *     page: res.page,
+ *     total: res.total,
+ *   };
+ * });
+ * ```
+ *
+ * @param pageSize 一页的数量
+ * @param loader 加载函数
+ * @returns 
+ */
+export function useSimplePagerDataLoader<T, P = any>(
+  pageSize: number|Ref<number>, 
+  loader: (page: number, pageSize: number, params?: P) => Promise<{
+    data: T[],
+    total: number,
+  }>)  : ISimplePageListLoader<T, P>
+{
+  const page = ref(0);
+  const list = ref<T[]>([]) as Ref<T[]>;
+  const total = ref(0);
+  const totalPages = computed(() => Math.ceil(total.value / getPageSize()));
+  const loadStatus = ref<LoaderLoadType>('loading');
+  const loadError = ref('');
+
+  function getPageSize() {
+    return typeof pageSize == 'object'? pageSize.value : pageSize;
+  }
+  
+  watch(page, async () => {
+    await loadData(lastParams, false);
+  });
+
+  let lastParams: P | undefined;
+  let loading = false;
+
+  async function loadData(params?: P, refresh: boolean = false) {
+    if (loading) 
+      return;
+    if (params)
+      lastParams = params;
+    if (refresh) {
+      page.value = 1;
+    }
+    list.value = []; 
+    loadStatus.value = 'loading';
+    loading = true;
+
+    try {
+      const res = (await loader(page.value, getPageSize(), lastParams));
+      list.value = list.value.concat(res.data);
+      total.value = res.total;
+      loadStatus.value = res.data.length > 0 ? 'finished' : 'nomore';
+      loadError.value = '';
+      loading = false;
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+      loading = false;
+    }
+  }
+  /**
+   * 下一页
+   */
+  async function next() {
+    if (page.value > total.value)
+      return;
+    page.value++;
+    await loadData(lastParams, false);
+  }
+  /**
+   * 上一页
+   */
+  async function prev() {
+    if (page.value <= 1)
+      return;   
+    page.value--;
+    await loadData(lastParams, false);
+  }
+
+  return {
+    loadData,
+    next,
+    prev,
+    /**
+     * 数据
+     */
+    list,
+    /**
+     * 当前页码
+     */
+    page,
+    /**
+     * 总数据条数
+     */
+    total,
+    /**
+     * 总页数
+     */
+    totalPages,
+    loadError,
+    loadStatus,
+  }
+}

+ 150 - 29
src/pages/admin.vue

@@ -7,32 +7,67 @@
     <section class="main-section ">
       <div class="content">
         <div class="title">
-          <h2>管理列表</h2>
+          <h2>管理员管理</h2>
         </div>
        
         <a-tabs v-model:activeKey="activeKey" centered>
-          <a-tab-pane key="1" tab="传承人">
-            <EmptyToRecord title="传承人" :model="inheritorData?.works" :showEdited="false" @edit="router.push({ name: 'FormWork' })">
-              <div class="d-flex justify-content-end">
+          <a-tab-pane key="1" tab="传承人列表">
+            <EmptyToRecord 
+              title="传承人"
+              emptyText="暂无数据"
+              :model="inheritorData"
+              :showEdited="false"
+              :showAdd="false"
+            >
+              <!-- <div class="d-flex justify-content-end">
                 <a-button type="primary" @click="router.push({ name: 'FormWork' })">+ 新增</a-button>
-              </div>
-              <a-list item-layout="horizontal" :data-source="inheritorData?.works || []">
-                <template #renderItem="{ item }">
-                  <a-list-item>
-                    <a-list-item-meta
-                      :title="item.title"
-                      :description="item.desc"
-                    >
-                      <template #avatar>
-                        <a-avatar :src="item.image" />
-                      </template>
-                    </a-list-item-meta>
-                    <template #actions>
-                      <a key="list-loadmore-edit" @click="router.push({ name: 'FormWork', query: { id: item.id } })">编辑</a>
-                    </template>
-                  </a-list-item>
+              </div> -->
+              <CommonListBlock 
+                :showTotal="true"
+                :rowCount="1"
+                :rowType="5"
+                :dropDownNames="[{
+                  options: categoryData.content.value ?? [],
+                  label: '分类',
+                  defaultSelectedValue: 0,
+                }]"
+                :load="(page: number, pageSize: number, _, searchText: string, dropDownValues: number[]) => loadInheritorData(page, pageSize, dropDownValues, searchText)"  
+                :showDetail="(item) => router.push({ name: 'FormInheritor', query: { id: item.id } })"
+              >
+                <template #itemRight="{ item }">
+                  <a-button type="link">编辑</a-button>
+                  <a-button type="link" @click.stop="handleCopyAccount(item)">传承人账号</a-button>
                 </template>
-              </a-list>
+              </CommonListBlock>
+            </EmptyToRecord>
+          </a-tab-pane>
+          <a-tab-pane key="2" tab="非遗项目列表">
+            <EmptyToRecord 
+              title="非遗项目"
+              emptyText="暂无数据"
+              :model="inheritorData"
+              :showEdited="false"
+              :showAdd="false"
+            >
+              <!-- <div class="d-flex justify-content-end">
+                <a-button type="primary" @click="router.push({ name: 'FormWork' })">+ 新增</a-button>
+              </div> -->
+              <CommonListBlock 
+                :showTotal="true"
+                :rowCount="1"
+                :rowType="5"
+                :dropDownNames="[{
+                  options: categoryData.content.value ?? [],
+                  label: '分类',
+                  defaultSelectedValue: 0,
+                }]"
+                :load="(page: number, pageSize: number, _, searchText: string, dropDownValues: number[]) => loadIchData(page, pageSize, dropDownValues, searchText)"
+                :showDetail="(item) => router.push({ name: 'FormIch', query: { id: item.id } })"
+               >
+                <template #itemRight="{ item }">
+                  <a-button type="link" @click.stop="router.push({ name: 'FormIch', query: { id: item.id } })">编辑</a-button>
+                </template>
+              </CommonListBlock>
             </EmptyToRecord>
           </a-tab-pane>
         </a-tabs>
@@ -41,25 +76,111 @@
   </div>
 </template>
 
-
 <script setup lang="ts">
-import { onMounted, ref, watch } from 'vue';
+import { ref, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
-import type { IchInfo, InheritorInfo, PlanInfo, SeminarInfo } from '@/api/inheritor/InheritorContent';
+import { useAuthStore } from '@/stores/auth';
+import { useSimpleDataLoader } from '@/composeable/SimpleDataLoader';
+import useClipboard from 'vue-clipboard3';
 import EmptyToRecord from '@/components/parts/EmptyToRecord.vue';
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+import CommonListBlock from '@/components/content/CommonListBlock.vue';
+import type { GetContentListItem } from '@/api/CommonContent';
+import InheritorContent from '@/api/inheritor/InheritorContent';
+import { message, Modal } from 'ant-design-vue';
 
+const { toClipboard } = useClipboard();
 const router = useRouter();
 const route = useRoute();
+const authStore = useAuthStore();
 const activeKey = ref(route.query.tab as string || '1');
-const ichData = ref<IchInfo>();
-const inheritorData = ref<InheritorInfo>();
-const seminarData = ref<SeminarInfo>();
-const planData = ref<PlanInfo[]>([]);
+const inheritorData = ref<GetContentListItem[]>([]);
 
 watch(activeKey, (newValue) => {
   router.replace({ query: { tab: newValue } });
 })
-onMounted(() => {
 
+const categoryData = useSimpleDataLoader(async () => {
+  const arr = (await CommonContent.getCategoryList(4)).map((item) => ({
+    id: item.id,
+    name: item.title,
+  }));
+  arr.unshift({
+    id: 0,
+    name: '全部',
+  });
+  return arr;
 })
+
+async function loadInheritorData(page: number, pageSize: number, dropDownValues: number[], searchText: string) {
+  const res = await CommonContent.getContentList(
+    new GetContentListParams()
+      .setModelId(7)
+      .setMainBodyColumnId(38)
+      .setKeywords(searchText)
+      .setSelfValues({
+        ichType: dropDownValues[0] == 0 ? undefined: dropDownValues[0],
+        region: authStore.userInfo?.region,
+      }), 
+    page,
+    pageSize
+  );
+  return {
+    page,
+    total: res.total,
+    data: res.list.map((item) => ({
+      ...item,
+      desc: `${item.ichName} ${item.levelText} ${item.batchText}`
+    })),
+  }
+}
+async function loadIchData(page: number, pageSize: number, dropDownValues: number[], searchText: string) {
+  const res = await CommonContent.getContentList(
+    new GetContentListParams()
+      .setModelId(2)
+      .setKeywords(searchText)
+      .setSelfValues({
+        ichType: dropDownValues[0] == 0 ? undefined: dropDownValues[0],
+        region: authStore.userInfo?.region,
+      }), 
+    page,
+    pageSize
+  );
+  return {
+    page,
+    total: res.total,
+    data: res.list.map((item) => ({
+      ...item,
+      desc: `${item.ichTypeText} - ${item.levelText} ${item.batchText}`
+    })),
+  }
+}
+
+async function handleCopyAccount(item: GetContentListItem) {
+  let result;
+  try {
+    result = await InheritorContent.getInheritorAccountInfo(item.id);
+    if (!result)
+      throw '该传承人没有账号';
+  } catch (e) {
+    Modal.error({
+      title: '获取账号失败',
+      content: '' + e,
+    });
+    return;
+  }
+
+  const resultString = `传承人${item.title}的账号:\n用户名:${result.username}\n密码:${result.password}`;
+
+  try {
+    await toClipboard(resultString);
+    message.success('复制到剪贴板成功');
+  } catch (e) {
+    Modal.error({
+      title: '复制失败',
+      content: '复制到剪贴板失败,可能是浏览器不支持或未授权,可手动复制:' + resultString,
+    });
+  }
+
+}
 </script>

+ 14 - 5
src/pages/forms/form.vue

@@ -51,8 +51,8 @@
 </template>
 
 <script setup lang="ts" generic="T extends DataModel, U extends DataModel">
-import { onMounted, ref, toRefs, type PropType, h } from 'vue';
-import { useRouter } from 'vue-router';
+import { onMounted, ref, toRefs, type PropType, h, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import { useWindowOnUnLoadConfirm } from '@/composeable/WindowOnUnLoad';
 import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
 import { message, Modal, type FormInstance } from 'ant-design-vue';
@@ -86,7 +86,7 @@ const props = defineProps({
     default: null
   },
   load: {
-    type: Function as PropType<() => Promise<void>>,
+    type: Function as PropType<(id: number|undefined) => Promise<void>>,
     default: () => Promise.resolve()
   },
   save: {
@@ -99,13 +99,20 @@ const props = defineProps({
   },
 })
 
-const { formModel, formOptions, load } = toRefs(props);
+const { formModel, formOptions, extendFormOptions, load } = toRefs(props);
 const formBase = ref<IDynamicFormRef>();
 const formExtend = ref<IDynamicFormRef>();
 
 const router = useRouter();
+const route = useRoute();
 const loading = ref(false);
 const loadingData = ref(false);
+const readonly = ref(false);
+
+watch(readonly, (newValue) => {
+  formOptions.value.disabled = newValue;
+  extendFormOptions.value.disabled = newValue;
+})
 
 useWindowOnUnLoadConfirm();
 
@@ -176,9 +183,11 @@ async function handleSubmitExtend() {
 
 async function loadData() {
   loadingData.value = true;
+  readonly.value = Boolean(route.query.readonly);
   try {
-    await load.value();
+    await load.value(route.query.id ? Number(route.query.id) : undefined);
   } catch (error) {
+    console.log(error);
     message.error('加载失败 ' + error);
   } finally {
     loadingData.value = false;

+ 11 - 8
src/pages/forms/ich.vue

@@ -18,9 +18,10 @@ import InheritorContent, { IchExpandInfo, IchInfo } from '@/api/inheritor/Inheri
 import CommonContent from '@/api/CommonContent';
 import type { IDynamicFormOptions, IDynamicFormRef } from '@imengyu/vue-dynamic-form';
 import type { SelectProps } from 'ant-design-vue';
-import type { UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
+import { useBeforeUploadImageChecker, useBeforeUploadVideoChecker, type UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
 import type { AddressItem } from '@/components/dynamicf/Map/AddressSercher.vue';
 import { useAuthStore } from '@/stores/auth';
+import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 
 const authStore = useAuthStore();
 const formRef = ref();
@@ -103,7 +104,7 @@ const formOptions = ref<IDynamicFormOptions>({
         //{ label: '批准时间', name: 'approveTime', type: 'text', additionalProps: { placeholder: '请输入批准时间' } },     
         { 
           label: '非遗项目相关图片', name: 'images', type: 'mulit-image',
-          hidden: { callback: (_, model) => (model as IchInfo).type !== 4 },
+          //hidden: { callback: (_, model) => (model as IchInfo).type !== 4 },
           formProps: {
             extra: '建议分辨率:1920*1080以上',
           },
@@ -111,7 +112,8 @@ const formOptions = ref<IDynamicFormOptions>({
             placeholder: '请上传图片',
             maxCount: 20,
             name: 'file',
-            uploadCo: useImageSimpleUploadCo(),
+            beforeUpload: useBeforeUploadImageChecker(),
+            uploadCo: useAliOssUploadCo('ich/images'),
           } as UploadImageFormItemProps,
         },
         { 
@@ -120,7 +122,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传视频',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadVideoChecker(),
+            uploadCo: useAliOssUploadCo('ich/video'),
           } as UploadImageFormItemProps,  
         },
         { 
@@ -308,10 +311,10 @@ const formExtendOptions = ref<IDynamicFormOptions>({
   }
 });
 
-async function loadData() {
-  formModel.value = await InheritorContent.getIchInfo();
-  formModel.value.expandInfo = await InheritorContent.getIchExpandInfo();
-  formExtendModel.value = formModel.value.expandInfo;
+async function loadData(id: number|undefined) {
+  formModel.value = await InheritorContent.getIchInfo(id);
+  formModel.value.expandInfo = await InheritorContent.getIchExpandInfo(id);
+  formExtendModel.value = formModel.value.expandInfo || new IchExpandInfo();
 }
 
 </script>

+ 23 - 14
src/pages/forms/inheritor.vue

@@ -16,8 +16,9 @@ import InheritorContent, { InheritorExpandInfo, InheritorInfo } from '@/api/inhe
 import CommonContent from '@/api/CommonContent';
 import type { IDynamicFormOptions } from '@imengyu/vue-dynamic-form';
 import type { SelectProps } from 'ant-design-vue';
-import type { UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
+import { useBeforeUploadImageChecker, useBeforeUploadVideoChecker, type UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
 import { useAuthStore } from '@/stores/auth';
+import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 
 const authStore = useAuthStore();
 const formModel = ref(new InheritorInfo()) as Ref<InheritorInfo>;
@@ -113,7 +114,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传视频',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadVideoChecker(),
+            uploadCo: useAliOssUploadCo('inheritor/video'),
           } as UploadImageFormItemProps,  
         },
         { 
@@ -166,7 +168,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传图片',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadImageChecker(),
+            uploadCo: useAliOssUploadCo('inheritor/images'),
           } as UploadImageFormItemProps,
         },
         { 
@@ -184,7 +187,8 @@ const formOptions = ref<IDynamicFormOptions>({
             placeholder: '请上传图片',
             maxCount: 20,
             name: 'file',
-            uploadCo: useImageSimpleUploadCo(),
+            beforeUpload: useBeforeUploadImageChecker(),
+            uploadCo: useAliOssUploadCo('inheritor/images'),
           } as UploadImageFormItemProps,
         },
         { 
@@ -193,7 +197,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传音频',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadAudioChecker(),
+            uploadCo: useAliOssUploadCo('inheritor/audios'),
           } as UploadImageFormItemProps,
         },
         { 
@@ -203,7 +208,7 @@ const formOptions = ref<IDynamicFormOptions>({
             placeholder: '请上传数字档案',
             maxCount: 20,
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            uploadCo: useAliOssUploadCo('inheritor/archives'),
           } as UploadImageFormItemProps,
         },
         { 
@@ -288,7 +293,8 @@ const formExtendOptions = ref<IDynamicFormOptions>({
       type: 'single-image',
       additionalProps: {
         placeholder: '请上传证件照',
-        uploadCo: useImageSimpleUploadCo()
+        beforeUpload: useBeforeUploadImageChecker(),
+        uploadCo: useAliOssUploadCo('inheritor/idcards'),
       } as UploadImageFormItemProps
     },
     {
@@ -504,7 +510,8 @@ const formExtendOptions = ref<IDynamicFormOptions>({
       type: 'mulit-image',
       additionalProps: {
         placeholder: '请上传图片资源',
-        uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadImageChecker(),
+        uploadCo: useAliOssUploadCo('inheritor/images'),
       }
     },
     {
@@ -584,7 +591,8 @@ const formExtendOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             name: 'file',
             placeholder: '请上传图片',
-            uploadCo: useImageSimpleUploadCo(),
+            beforeUpload: useBeforeUploadImageChecker(),
+            uploadCo: useAliOssUploadCo('inheritor/images'),
           } as UploadImageFormItemProps,
         },
       ]
@@ -595,7 +603,8 @@ const formExtendOptions = ref<IDynamicFormOptions>({
       type: 'single-image',
       additionalProps: {
         placeholder: '请上传被推荐人身份证复印件',
-        uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadImageChecker(),
+        uploadCo: useAliOssUploadCo('inheritor/idcards'),
       } as UploadImageFormItemProps
     },
     
@@ -618,10 +627,10 @@ const formExtendOptions = ref<IDynamicFormOptions>({
   }
 });
 
-async function loadData() {
-  formModel.value = await InheritorContent.getInheritorInfo();
-  formModel.value.expandInfo = await InheritorContent.getInheritorExpandInfo();
-  formExtendModel.value = formModel.value.expandInfo;
+async function loadData(id: number|undefined) {
+  formModel.value = await InheritorContent.getInheritorInfo(id);
+  formModel.value.expandInfo = await InheritorContent.getInheritorExpandInfo(id);
+  formExtendModel.value = formModel.value.expandInfo || new InheritorExpandInfo();
 }
 
 </script>

+ 16 - 11
src/pages/forms/seminar.vue

@@ -21,6 +21,7 @@ import type { SelectProps } from 'ant-design-vue';
 import type { UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
 import type { AddressItem } from '@/components/dynamicf/Map/AddressSercher.vue';
 import { useAuthStore } from '@/stores/auth';
+import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 
 const authStore = useAuthStore();
 const formRef = ref();
@@ -152,7 +153,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传图片',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadImageChecker(),
+            uploadCo: useAliOssUploadCo('seminar/images')
           } as UploadImageFormItemProps,
         },
         { 
@@ -170,7 +172,8 @@ const formOptions = ref<IDynamicFormOptions>({
             placeholder: '请上传图片',
             maxCount: 20,
             name: 'file',
-            uploadCo: useImageSimpleUploadCo(),
+            beforeUpload: useBeforeUploadImageChecker(),
+            uploadCo: useAliOssUploadCo('seminar/images'),
           } as UploadImageFormItemProps,
         },
         { 
@@ -179,7 +182,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传音频',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadAudioChecker(),
+            uploadCo: useAliOssUploadCo('seminar/audios')
           } as UploadImageFormItemProps,
         },
         { 
@@ -188,7 +192,8 @@ const formOptions = ref<IDynamicFormOptions>({
           additionalProps: {
             placeholder: '请上传视频',
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            beforeUpload: useBeforeUploadVideoChecker(),
+            uploadCo: useAliOssUploadCo('seminar/videos')
           } as UploadImageFormItemProps,  
         },
         { 
@@ -198,7 +203,7 @@ const formOptions = ref<IDynamicFormOptions>({
             placeholder: '请上传数字档案',
             maxCount: 20,
             name: 'file',
-            uploadCo: useImageSimpleUploadCo()
+            uploadCo: useAliOssUploadCo('seminar/archives')
           } as UploadImageFormItemProps,
         },
         { 
@@ -272,8 +277,8 @@ const formExtendOptions = ref<IDynamicFormOptions>({
       ]
     },
     { label: '成立时间', name: 'openTime', type: 'date-time', additionalProps: { placeholder: '请选择成立时间' } },
-    { label: '图片', name: 'image', type: 'single-image', additionalProps: { placeholder: '请上传图片', uploadCo: useImageSimpleUploadCo() } },
-    { label: '相关图片', name: 'images', type: 'mulit-image', additionalProps: { placeholder: '请上传相关图片', uploadCo: useImageSimpleUploadCo() } },
+    { label: '图片', name: 'image', type: 'single-image', additionalProps: { placeholder: '请上传图片', uploadCo: useAliOssUploadCo('seminar/images') } },
+    { label: '相关图片', name: 'images', type: 'mulit-image', additionalProps: { placeholder: '请上传相关图片', uploadCo: useAliOssUploadCo('seminar/images') } },
     { label: '地址', name: 'address', type: 'text', additionalProps: { placeholder: '请输入地址' } },
     { label: '描述', name: 'intro', type: 'text-area', additionalProps: { placeholder: '请输入描述' } },
     { label: '简介', name: 'desc', type: 'text-area', additionalProps: { placeholder: '请输入简介' } }
@@ -283,10 +288,10 @@ const formExtendOptions = ref<IDynamicFormOptions>({
   }
 });
 
-async function loadData() {
-  formModel.value = await InheritorContent.getSeminarInfo();
-  formModel.value.expandInfo = await InheritorContent.getSeminarExpandInfo();
-  formExtendModel.value = formModel.value.expandInfo;
+async function loadData(id: number|undefined) {
+  formModel.value = await InheritorContent.getSeminarInfo(id);
+  formModel.value.expandInfo = await InheritorContent.getSeminarExpandInfo(id);
+  formExtendModel.value = formModel.value.expandInfo || new SeminarExpandInfo(); 
 }
 
 </script>

+ 19 - 10
src/pages/forms/works.vue

@@ -10,15 +10,15 @@
 
 <script setup lang="ts">
 import { ref, type Ref } from 'vue';
-import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo';
 import Form from './form.vue';
 import InheritorContent, { InheritorWorkInfo } from '@/api/inheritor/InheritorContent';
 import CommonContent from '@/api/CommonContent';
 import type { IDynamicFormOptions } from '@imengyu/vue-dynamic-form';
 import type { SelectProps } from 'ant-design-vue';
-import type { UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
+import { useBeforeUploadAudioChecker, useBeforeUploadImageChecker, useBeforeUploadVideoChecker, type UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
 import { useRoute } from 'vue-router';
 import { useAuthStore } from '@/stores/auth';
+import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 
 const authStore = useAuthStore();
 const formModel = ref(new InheritorWorkInfo()) as Ref<InheritorWorkInfo>;
@@ -49,36 +49,45 @@ const formOptions = ref<IDynamicFormOptions>({
               ] 
             } as SelectProps 
           },
-          { label: '缩略图', name: 'image', type: 'single-image', additionalProps: { placeholder: '请上传图片', uploadCo: useImageSimpleUploadCo(), name: 'file', accept: 'image/*' } as UploadImageFormItemProps },
+          { label: '缩略图', name: 'image', type: 'single-image', additionalProps: { placeholder: '请上传图片', uploadCo: useAliOssUploadCo('inheritor/images'), name: 'file', accept: 'image/*' } as UploadImageFormItemProps },
           { label: '图片说明', name: 'imageDesc', type: 'text', additionalProps: { placeholder: '请输入图片说明' } },
           { 
             label: '组图', name: 'images', type: 'mulit-image', 
             hidden: { callback: (_, model) => (model as InheritorWorkInfo).type !== 4 },
             additionalProps: { 
               placeholder: '请上传组图', 
-              uploadCo: useImageSimpleUploadCo(), name: 'file', accept: 'image/*', maxCount: 20 
+              beforeUpload: useBeforeUploadImageChecker(),
+              uploadCo: useAliOssUploadCo('inheritor/images'), name: 'file', accept: 'image/*', maxCount: 20 
             } as UploadImageFormItemProps 
           },
           { label: '作品/产品介绍', name: 'content', type: 'richtext', additionalProps: { placeholder: '请输入内容介绍' } },
           { 
-            label: '音频', name: 'audio', type: 'single-image', 
+            label: '音频', name: 'audio', type: 'single-video', 
             hidden: { callback: (_, model) => (model as InheritorWorkInfo).type !== 2 },
             additionalProps: { 
               placeholder: '请上传音频', 
-              uploadCo: useImageSimpleUploadCo(), name: 'file' 
+              beforeUpload: useBeforeUploadAudioChecker(),
+              uploadCo: useAliOssUploadCo('inheritor/audios'), 
+              name: 'file' 
             } as UploadImageFormItemProps 
           },
           { 
             label: '视频', name: 'video', type: 'single-video', 
             hidden: { callback: (_, model) => (model as InheritorWorkInfo).type !== 3 },
             additionalProps: { 
-              placeholder: '请上传视频', uploadCo: useImageSimpleUploadCo(), name: 'file' 
+              beforeUpload: useBeforeUploadVideoChecker(),
+              placeholder: '请上传视频', uploadCo: useAliOssUploadCo('inheritor/videos'), name: 'file' 
             } as UploadImageFormItemProps 
           },
           { 
             label: '数字档案', name: 'archives', type: 'mulit-image', 
             hidden: { callback: (_, model) => (model as InheritorWorkInfo).type !== 5 },
-            additionalProps: { placeholder: '请上传数字档案', uploadCo: useImageSimpleUploadCo(), name: 'file', maxCount: 20 } as UploadImageFormItemProps 
+            additionalProps: { 
+              placeholder: '请上传数字档案', 
+              uploadCo: useAliOssUploadCo('inheritor/archives'), 
+              name: 'file', 
+              maxCount: 20 
+            } as UploadImageFormItemProps 
           },
         { 
           label: '审核人员', name: 'text1', type: 'static-text', 
@@ -97,7 +106,7 @@ const formOptions = ref<IDynamicFormOptions>({
         { 
           label: '填报人', name: 'text3', type: 'static-text', 
           additionalProps: {
-            text: authStore.userInfo?.nickname,
+            text: authStore.userInfo?.nickname ,
           }
         },
         ]
@@ -145,7 +154,7 @@ const route = useRoute();
 async function loadData() {
   const id = parseFloat(route.query.id as string);
   if (id) {
-    const works = await InheritorContent.getInheritorInfo();
+    const works = await InheritorContent.getInheritorInfo(undefined);
     formModel.value = works.works.find((item) => item.id === id) || new InheritorWorkInfo();
   }
 }

+ 49 - 10
src/pages/login.vue

@@ -8,18 +8,21 @@
         <div class="title">
           <h2>登录</h2>
         </div>
-        <DynamicForm 
-          ref="form"
-          :model="formModel" 
-          :options="formOptions"
-        />
-        <a-button type="primary" block @click="handleSubmit">登录</a-button>
+        <div class="form-container">
+          <DynamicForm 
+            ref="form"
+            :model="formModel" 
+            :options="formOptions"
+          />
+          <a-button type="primary" block @click="handleSubmit">登录</a-button>
+        </div>
       </div>
     </section>
   </div>
 </template>
 
 <script setup lang="ts">
+import type { SimpleSelectFormItemProps } from '@/components/dynamicf/SimpleSelectFormItem';
 import { useAuthStore } from '@/stores/auth';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
 import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
@@ -31,6 +34,8 @@ const form = ref<IDynamicFormRef>();
 const formModel = ref({
   mobile: '',
   password: '',
+  account: '',
+  type: 0,
 });
 const formOptions = ref<IDynamicFormOptions>({
   formLabelCol: { span: 6 },
@@ -43,11 +48,21 @@ const formOptions = ref<IDynamicFormOptions>({
       label: '手机号',
       name: 'mobile',
       type: 'text',
+      hidden: { callback: (_, m) => (m as any).type == 1 },
       additionalProps: {
         placeholder: '请输入手机号'
       },
     },
     {
+      label: '账号',
+      name: 'account',
+      type: 'text',
+      hidden: { callback: (_, m) => (m as any).type == 0 },
+      additionalProps: {
+        placeholder: '请输入账号'
+      },
+    },
+    {
       label: '密码',
       name: 'password',
       type: 'password',
@@ -55,12 +70,31 @@ const formOptions = ref<IDynamicFormOptions>({
         placeholder: '请输入密码' 
       }
     },
+    {
+      label: '登录类型',
+      name: 'type',
+      type: 'radio-value',
+      additionalProps: {
+        placeholder: '请输入密码',
+        options: [
+          {
+            text: '传承人',
+            value: 0,
+          },
+          {
+            text: '管理员',
+            value: 1,
+          },
+        ],
+      } as SimpleSelectFormItemProps
+    },
   ],
   formRules: {
     mobile: [
       { required: true, message: '请输入手机号' },
-      { min: 11, max: 11, message: '手机号长度必须为11位' },
-      { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号' }
+    ],
+    account: [
+      { required: true, message: '请输入密码' }
     ],
     password: [
       { required: true, message: '请输入密码' }
@@ -79,12 +113,17 @@ async function handleSubmit() {
   }
   try {
     await authStore.login(
-      formModel.value.mobile,
+      formModel.value.type == 1 ? formModel.value.account : formModel.value.mobile,
       formModel.value.password,
+      formModel.value.type,
     );
     message.success('您已成功登录');
     await waitTimeOut(200);
-    router.push('/inheritor');
+    if (authStore.loginType == 0) {
+      router.push('/inheritor');
+    } else {
+      router.push('/admin');
+    }
   } catch (error) {
     Modal.error({
       title: '登录失败',

+ 5 - 0
src/router/index.ts

@@ -36,6 +36,11 @@ const router = createRouter({
       component: () => import('@/pages/forms/plans.vue'),
     },
     {
+      path: '/admin',
+      name: 'Admin',
+      component: () => import('@/pages/admin.vue'),
+    },
+    {
       path: '/login',
       name: 'Login',
       component: () => import('@/pages/login.vue'),

+ 21 - 8
src/stores/auth.ts

@@ -11,6 +11,7 @@ export const useAuthStore = defineStore('auth', {
     expireAt: 0,
     userId: 0,
     userInfo: null as null|UserInfo,
+    loginType: 0,
   }),
   actions: {
     async loadLoginState() {
@@ -23,11 +24,12 @@ export const useAuthStore = defineStore('auth', {
         this.userId = authInfo.userId;
         this.expireAt = authInfo.expireAt;
         this.userInfo = authInfo.userInfo;
+        this.loginType = authInfo.loginType;
 
         // 检查token是否过期,如果快要过期,则刷新token
         if (canRefresh && Date.now() > this.expireAt + 1000 * 3600 * 5) {
           const refreshResult = await UserApi.refresh();
-          this.loginResultHandle(refreshResult);
+          this.loginResultHandle(refreshResult, this.loginType);
         }
       } catch (error) {
         this.token = '';
@@ -37,17 +39,27 @@ export const useAuthStore = defineStore('auth', {
         console.log('loadLoginState', error);
       }
     },
-    async login(mobile: string, password: string) {
-      const loginResult = await UserApi.login({
-        mobile,
-        password,
-      })
-      this.loginResultHandle(loginResult);
+    async login(mobile: string, password: string, loginType: number) {
+      let loginResult;
+      if (loginType == 0) {
+        loginResult = await UserApi.login({
+          mobile,
+          password,
+        })
+      } else if (loginType == 1) {
+        loginResult = await UserApi.loginAdmin({
+          account: mobile,
+          password,
+        })
+      } else
+        throw 'login type error';
+      this.loginResultHandle(loginResult, loginType);
     },
-    async loginResultHandle(loginResult: LoginResult) {
+    async loginResultHandle(loginResult: LoginResult, loginType: number) {
       this.token = loginResult.token;
       this.userId = loginResult.userId!;
       this.userInfo = loginResult.userInfo;
+      this.loginType = loginType;
       this.expireAt = (loginResult.expiresIn || 0) + Date.now();
 
       localStorage.setItem(STORAGE_KEY, 
@@ -56,6 +68,7 @@ export const useAuthStore = defineStore('auth', {
           userId: this.userId ,
           expireAt: this.expireAt,
           userInfo: this.userInfo,
+          loginType,
         }) 
       );
     },