浏览代码

志愿者审核页面

快乐的梦鱼 1 月之前
父节点
当前提交
2902118055

+ 14 - 1
src/api/inhert/VillageApi.ts

@@ -99,6 +99,10 @@ export class VolunteerInfo extends DataModel<VolunteerInfo> {
     }
     }
   }
   }
 
 
+  static STATUS_APPROVED = 1;
+  static STATUS_PENDING = 0;
+  static STATUS_REJECTED = -1;
+
   id !: number;
   id !: number;
   mainBodyId !: number;
   mainBodyId !: number;
   type = '';
   type = '';
@@ -273,13 +277,22 @@ export class VillageApi extends AppServerRequestModule<DataModel> {
       id,
       id,
     }, '获取志愿者信息', undefined, VolunteerInfo)).data as VolunteerInfo
     }, '获取志愿者信息', undefined, VolunteerInfo)).data as VolunteerInfo
   }
   }
-  async getVillageVolunteerList(villageId?: number) {
+  async getVillageVolunteerList(villageId?: number, status?: number) {
     return (this.post('/village/volunteer/getList', {
     return (this.post('/village/volunteer/getList', {
       village_id: villageId,
       village_id: villageId,
+      status,
     }, '获取志愿者列表')) 
     }, '获取志愿者列表')) 
       .then(res => transformArrayDataModel<VolunteerInfo>(VolunteerInfo, res.data2 || [], ``, true))
       .then(res => transformArrayDataModel<VolunteerInfo>(VolunteerInfo, res.data2 || [], ``, true))
       .catch(e => { throw e });
       .catch(e => { throw e });
   }
   }
+  async reviewVillageVolunteer(villageId: number, volunteerId: number, status: number) {
+    return (this.post('/village/village/claimReview', {
+      village_id: villageId,
+      village_volunteer_id: volunteerId,
+      status,
+    }, '审核志愿者')) ;
+  }
+
   async getCollectModuleList() {
   async getCollectModuleList() {
     return (this.get('/village/volunteer/getCollectModuleList', '获取采集版块列表', {
     return (this.get('/village/volunteer/getCollectModuleList', '获取采集版块列表', {
     })) 
     })) 

+ 7 - 0
src/components/dialog/CommonRoot.ts

@@ -39,6 +39,13 @@ export function notify(options: ToastShowProps) {
   return NaDialogRoot().notify(options);
   return NaDialogRoot().notify(options);
 }
 }
 
 
+let startZIndex = 1010;
+
+export function getCurrentZIndex() {
+  startZIndex += 3;
+  return startZIndex;
+}
+
 export default {
 export default {
   install(app: App) {
   install(app: App) {
     const na : ICommonRoot = {
     const na : ICommonRoot = {

+ 16 - 7
src/components/dialog/Popup.vue

@@ -34,6 +34,7 @@
       right: inset[1] ? `${themeContext.resolveThemeSize(inset[1])}` : undefined,
       right: inset[1] ? `${themeContext.resolveThemeSize(inset[1])}` : undefined,
       bottom: inset[2] ? `${themeContext.resolveThemeSize(inset[2])}` : undefined,
       bottom: inset[2] ? `${themeContext.resolveThemeSize(inset[2])}` : undefined,
       left: inset[3] ? `${themeContext.resolveThemeSize(inset[3])}` : undefined,
       left: inset[3] ? `${themeContext.resolveThemeSize(inset[3])}` : undefined,
+      zIndex: popupZIndex,
     }"
     }"
   >
   >
     <view 
     <view 
@@ -41,6 +42,7 @@
       :style="{
       :style="{
         backgroundColor: mask ? themeContext.resolveThemeColor(maskColor) : '',
         backgroundColor: mask ? themeContext.resolveThemeColor(maskColor) : '',
         transitionDuration: `${duration}ms`,
         transitionDuration: `${duration}ms`,
+        zIndex: popupZIndex + 1,
       }"
       }"
       @mousedown.stop="handleClose"
       @mousedown.stop="handleClose"
       @touchstart.stop="handleClose"
       @touchstart.stop="handleClose"
@@ -81,6 +83,7 @@
             minWidth: dialogSize,
             minWidth: dialogSize,
           },
           },
         }),
         }),
+        zIndex: popupZIndex + 2,
         backgroundColor: themeContext.resolveThemeColor(backgroundColor),
         backgroundColor: themeContext.resolveThemeColor(backgroundColor),
         margin: `${themeContext.resolveThemeSize(margin[0])} ${themeContext.resolveThemeSize(margin[1])} ${themeContext.resolveThemeSize(margin[2])} ${themeContext.resolveThemeSize(margin[3])}`,
         margin: `${themeContext.resolveThemeSize(margin[0])} ${themeContext.resolveThemeSize(margin[1])} ${themeContext.resolveThemeSize(margin[2])} ${themeContext.resolveThemeSize(margin[3])}`,
         ...innerStyle,
         ...innerStyle,
@@ -121,6 +124,7 @@ import { selectStyleType } from '../theme/ThemeTools';
 import { SimpleDelay } from '@imengyu/imengyu-utils';
 import { SimpleDelay } from '@imengyu/imengyu-utils';
 import PopupTitle from './PopupTitle.vue';
 import PopupTitle from './PopupTitle.vue';
 import SafeAreaPadding from '../layout/space/SafeAreaPadding.vue';
 import SafeAreaPadding from '../layout/space/SafeAreaPadding.vue';
+import { getCurrentZIndex } from './CommonRoot';
 
 
 /**
 /**
  * Popup 的显示位置
  * Popup 的显示位置
@@ -208,6 +212,11 @@ export interface PopupProps {
    * @default '30%'
    * @default '30%'
    */
    */
   size?: string|number;
   size?: string|number;
+  /**
+   * 指定弹出层的 z-index 层级,默认是 1010
+   * @default 0
+   */
+  zIndex?: number;
 }
 }
 
 
 const emit = defineEmits([ 'update:show', 'close', 'closeAnimFinished' ])
 const emit = defineEmits([ 'update:show', 'close', 'closeAnimFinished' ])
@@ -223,6 +232,7 @@ const props = withDefaults(defineProps<PopupProps>(), {
   backgroundColor: 'white',
   backgroundColor: 'white',
   safeArea: true,
   safeArea: true,
   duration: 230,
   duration: 230,
+  zIndex: 0,
   size: '30%',
   size: '30%',
 });
 });
 
 
@@ -245,6 +255,7 @@ const show2 = ref(false);
 const showAnimState = ref(false);
 const showAnimState = ref(false);
 const radius = computed(() => props.round ? themeContext.resolveThemeSize('PopupRadius', 30) : 0);
 const radius = computed(() => props.round ? themeContext.resolveThemeSize('PopupRadius', 30) : 0);
 const dialogSize = computed(() => themeContext.resolveThemeSize(props.size));
 const dialogSize = computed(() => themeContext.resolveThemeSize(props.size));
+const popupZIndex = ref(props.zIndex);
 
 
 let lateStopTimer : SimpleDelay|undefined;
 let lateStopTimer : SimpleDelay|undefined;
 
 
@@ -260,6 +271,7 @@ watch(() => props.show, (v) => {
     }, props.duration)
     }, props.duration)
     lateStopTimer.start();
     lateStopTimer.start();
   } else {
   } else {
+    popupZIndex.value = props.zIndex > 0 ? props.zIndex : getCurrentZIndex();
     if (lateStopTimer)
     if (lateStopTimer)
       lateStopTimer.stop();
       lateStopTimer.stop();
     lateStopTimer = new SimpleDelay(undefined, () => {
     lateStopTimer = new SimpleDelay(undefined, () => {
@@ -278,7 +290,6 @@ watch(() => props.show, (v) => {
   left: 0;
   left: 0;
   right: 0; 
   right: 0; 
   bottom: 0;
   bottom: 0;
-  z-index: 1010;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   pointer-events: none;
   pointer-events: none;
@@ -286,12 +297,12 @@ watch(() => props.show, (v) => {
 
 
   &.show2 {
   &.show2 {
     pointer-events: auto;
     pointer-events: auto;
-    .nana-popup-mask {
+    > .nana-popup-mask {
       pointer-events: auto;
       pointer-events: auto;
     }
     }
   }
   }
   &.no-mask {
   &.no-mask {
-    .nana-popup-mask {
+    > .nana-popup-mask {
       pointer-events: none;
       pointer-events: none;
     }
     }
   }
   }
@@ -305,14 +316,12 @@ watch(() => props.show, (v) => {
     left: 0;
     left: 0;
     right: 0;
     right: 0;
     bottom: 0;
     bottom: 0;
-    z-index: 1011;
     pointer-events: none;
     pointer-events: none;
     opacity: 0;
     opacity: 0;
     transition: opacity ease-in-out 0.3s;
     transition: opacity ease-in-out 0.3s;
   }
   }
   .nana-popup-content {
   .nana-popup-content {
     position: relative;
     position: relative;
-    z-index: 1012;
     overflow: hidden;
     overflow: hidden;
     transition: all ease-in-out 0.3s;
     transition: all ease-in-out 0.3s;
     opacity: 0.3;
     opacity: 0.3;
@@ -336,10 +345,10 @@ watch(() => props.show, (v) => {
   }
   }
 
 
   &.show {
   &.show {
-    .nana-popup-mask {
+    > .nana-popup-mask {
       opacity: 1;
       opacity: 1;
     }
     }
-    .nana-popup-content {
+    > .nana-popup-content {
       opacity: 1;
       opacity: 1;
       transform: translateX(0) translateY(0);
       transform: translateX(0) translateY(0);
     }
     }

+ 7 - 0
src/pages.json

@@ -67,6 +67,13 @@
       }
       }
     },
     },
     {
     {
+      "path": "pages/dig/admin/review",
+      "style": {
+        "navigationBarTitleText": "注册审核",
+        "enablePullDownRefresh": true
+      }
+    },
+    {
       "path": "pages/dig/forms/task",
       "path": "pages/dig/forms/task",
       "style": {
       "style": {
         "navigationBarTitleText": "村社文化资源挖掘平台",
         "navigationBarTitleText": "村社文化资源挖掘平台",

+ 47 - 0
src/pages/dig/admin/components/VolunteerItem.vue

@@ -0,0 +1,47 @@
+<template>
+  <FlexRow
+    :key="item.id" 
+    backgroundColor="white" 
+    radius="20"
+    justify="space-between"
+    align="center"
+    :padding="20"
+    :gap="20"
+  >
+    <FlexRow :gap="20">
+      <Image 
+        :src="item.image"
+        width="100rpx"
+        height="100rpx"
+        :defaultImage="UserHead"
+        :failedImage="UserHead"
+        round 
+      />
+      <FlexCol>
+        <Text :fontSize="36" bold :text="`${item.name} ${item.sex === 0 ? '男' : '女'}`" />
+        <Text :fontSize="26" :text="`手机:${item.mobile}`" />
+        <Text :fontSize="26" :text="`地址:${item.address || '暂无'}`" />
+        <Text :fontSize="26" :text="`可采编:${item.collectModuleText || '暂无'}`" />
+      </FlexCol>
+    </FlexRow>
+    
+    <slot name="action" :item="item" />
+  </FlexRow>
+</template>
+
+<script setup lang="ts">
+import type { VolunteerInfo } from '@/api/inhert/VillageApi';
+import type { PropType } from 'vue'
+import UserHead from '@/static/images/user/avatar.png';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Image from '@/components/basic/Image.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Text from '@/components/basic/Text.vue';
+
+defineProps({
+  item: {
+    type: Object as PropType<VolunteerInfo>,
+    default: () => {},
+  },
+})
+</script>

+ 154 - 0
src/pages/dig/admin/data/volunteer.ts

@@ -0,0 +1,154 @@
+import CommonContent from "@/api/CommonContent";
+import VillageApi from "@/api/inhert/VillageApi";
+import { useAliOssUploadCo } from "@/common/components/upload/AliOssUploadCo";
+import type { IDynamicFormItemCallbackAdditionalProps, IDynamicFormOptions, IDynamicFormRef } from "@/components/dynamic";
+import type { CheckBoxTreeListProps } from "@/components/dynamic/wrappers/CheckBoxTreeList.vue";
+import type { PickerIdFieldProps } from "@/components/dynamic/wrappers/PickerIdField";
+import type { RadioValueProps } from "@/components/dynamic/wrappers/RadioValue";
+import type { FieldProps } from "@/components/form/Field.vue";
+import type { FormProps } from "@/components/form/Form.vue";
+import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
+import type { RuleItem } from "async-validator";
+import type { Ref } from "vue";
+
+export function getVolunteerForm(options: {
+  canSetCatalog: boolean,
+  villageId: number,
+  onlyPassword?: boolean,
+  noPassword?: boolean,
+  isNew: Ref<boolean>,
+  formRef: Ref<IDynamicFormRef|undefined>,
+}) : IDynamicFormOptions {
+  return {
+    formItems: [
+      {
+        name: 'groupBase',
+        type: 'flat-simple',
+        show: { callback: () => !options.noPassword },
+        children: [
+          { 
+            label: '登录账号', name: 'username', type: 'text',
+            additionalProps: { 
+              placeholder: '请输入登录账号',
+            },
+            rules: [{ required: true, message: '请输入登录账号' }],
+            show: { callback: () => options.isNew.value },
+          },
+          {
+            label: '密码',
+            name: 'password',
+            type: 'text',
+            additionalProps: { 
+              placeholder: '请输入密码',
+              type: 'password',
+            } as FieldProps,
+            rules: [{ required: true, message: '请输入密码' }],
+            show: { callback: () => options.isNew.value || options.onlyPassword === true },
+          },
+          {
+            label: '确认密码',
+            name: 'passwordRepeat',
+            type: 'text',
+            additionalProps: { 
+              placeholder: '请再输入一次密码',
+              type: 'password',
+            } as FieldProps,
+            rules: [
+              { required: true, message: '请再输入一次密码' },
+              {
+                async validator(rule, value) {
+                  if (value != options.formRef.value?.getValueByPath('password'))
+                    throw '两次输入密码不一致,请检查';
+                },
+              }
+            ] as RuleItem[],
+            show: { callback: () => options.isNew.value || options.onlyPassword === true },
+          },
+        ]
+      },
+      {
+        name: 'groupExtra',
+        type: 'flat-simple',
+        show: { callback: () => options.onlyPassword !== true },
+        childrenColProps: {
+          span: 24,
+        },
+        children: [
+          {
+            label: '真实名称', name: 'name', type: 'text',
+            additionalProps: { placeholder: '请输入真实名称' },
+            rules: [{ required: true, message: '请输入真实名称' }],
+          },
+          {
+            label: '手机号', name: 'mobile', type: 'text',
+            additionalProps: { placeholder: '请输入手机号' },
+            rules: [{ required: true, message: '请输入手机号' }],
+          },
+          { 
+            label: '区域', name: 'regionId', type: 'select-id',
+            additionalProps: {
+              placeholder: '请选择区域',
+              disabled: { callback: () => !options.isNew.value },
+              loadData: async () => (await CommonContent.getCategoryList(1)).map(p => ({ text: p.title, value: p.id, raw: p }))
+            } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
+            rules: [{ required: true, message: '请选择区域' }],
+          },
+          {
+            label: '性别', name: 'sex', type: 'radio-value',
+            additionalProps: {
+              options: [
+                { text: '男', value: 1 },
+                { text: '女', value: 2 }
+              ]
+            } as RadioValueProps,
+          },
+          { 
+            label: '头像', name: 'image', type: 'uploader',
+            additionalProps: {
+              single: true,
+              maxFileSize: 1024 * 1024 * 10,
+              upload: useAliOssUploadCo('xiangyuan/volunteer/images')
+            } as UploaderFieldProps,
+          },
+          { label: '现居地址', name: 'address', type: 'text', additionalProps: { placeholder: '请输入现居地址' } },
+          { 
+            label: '个人介绍', 
+            name: 'intro', 
+            type: 'textarea', 
+            additionalProps: { 
+              placeholder: '请输入个人介绍',
+              showWordLimit: true,
+              maxLength: 200,
+            } as FieldProps,
+          },
+          ...(options.canSetCatalog ? [{ 
+            label: '采集版块', name: 'catalogIds', type: 'check-box-tree', 
+            additionalProps: { 
+              placeholder: '请选择采集版块',
+              vertical: true,
+              multiple: true,
+              loadData: async (pid) => (await VillageApi.getCatalogList(options.villageId, pid)).map((p) => ({
+                text: p.title,
+                value: p.id,
+                hasChildren: p.haschild,
+              })),
+            } as CheckBoxTreeListProps,
+          }]: []),
+          { 
+            label: '村落认领说明', name: 'claimReason', type: 'text', 
+            additionalProps: { placeholder: '请输入村落认领说明' } ,
+            show: { callback: () => options.isNew.value },
+          },
+        ]
+      },
+    ],
+    formAdditionaProps: {
+      labelWidth: '160rpx',
+      labelAlign: 'right',
+      innerStyle: {
+        radius: '10rpx',
+      },
+    } as Omit<FormProps, 'model'>,
+  }
+
+}

+ 36 - 55
src/pages/dig/admin/index.vue

@@ -5,13 +5,15 @@
         <SearchBar
         <SearchBar
           v-model="searchText"
           v-model="searchText"
           placeholder="搜一搜" 
           placeholder="搜一搜" 
-          :innerStyle="{ width: '460rpx' }"
+          :innerStyle="{ width: '300rpx' }"
           @confirm="search"
           @confirm="search"
         />
         />
-        <NButton type="primary" @click="newData">+ 新增</NButton>
+        <ButtonGroup>
+          <NButton :radius="40" size="small" @click="navTo('review', { villageId: querys.villageId })">待审核志愿者</NButton>
+          <NButton :radius="40" size="small" type="primary" @click="newData">+ 新增</NButton>
+        </ButtonGroup>
       </FlexRow>
       </FlexRow>
       <FlexCol :gap="20" :margin="[20,0,0,0]">
       <FlexCol :gap="20" :margin="[20,0,0,0]">
-
         <button class="remove-button-style" open-type="share">
         <button class="remove-button-style" open-type="share">
           <Touchable 
           <Touchable 
             direction="column"
             direction="column"
@@ -31,59 +33,38 @@
             <Text :fontSize="26" color="primary" text="分享给志愿者注册,加入志愿者队伍" />
             <Text :fontSize="26" color="primary" text="分享给志愿者注册,加入志愿者队伍" />
           </Touchable>
           </Touchable>
         </button>
         </button>
-
-        <FlexRow 
+        <VolunteerItem 
           v-for="item in listLoader.list.value"
           v-for="item in listLoader.list.value"
           :key="item.id" 
           :key="item.id" 
-          backgroundColor="white" 
-          radius="20"
-          justify="space-between"
-          align="center"
-          :padding="20"
-          :gap="20"
+          :item="(item as unknown as VolunteerInfo)"
         >
         >
-          <FlexRow :gap="20">
-            <Image 
-              :src="item.image"
-              width="100rpx"
-              height="100rpx"
-              :defaultImage="UserHead"
-              :failedImage="UserHead"
-              round 
-            />
-            <FlexCol>
-              <Text :fontSize="36" bold :text="`${item.name} ${item.sex === 0 ? '男' : '女'}`" />
-              <Text :fontSize="26" :text="`手机:${item.mobile}`" />
-              <Text :fontSize="26" :text="`地址:${item.address || ''}`" />
-              <Text :fontSize="26" :text="`可采编:${item.collectModuleText || '暂无'}`" />
-            </FlexCol>
-          </FlexRow>
-          
-          <BubbleBox 
-            :innerStyle="{ left: '0' }"
-            position="bottom"
-            :items="[
-              {
-                icon: 'edit-filling',
-                text: '编辑信息',
-                onClick: () => goDetail(item.id),
-              },
-              {
-                icon: 'browse',
-                text: '修改密码',
-                onClick: () => goDetail(item.id, true),
-              },
-              {
-                icon: 'trash',
-                text: '删除账号',
-                textColor: 'danger',
-                onClick: () => doDeleteUser(item as unknown as VolunteerInfo),
-              },
-            ]"
-          >
-            <NButton type="primary" icon="edit-filling" :radius="40" />
-          </BubbleBox>
-        </FlexRow>
+          <template #action>
+            <BubbleBox 
+              :innerStyle="{ left: '0' }"
+              position="bottom"
+              :items="[
+                {
+                  icon: 'edit-filling',
+                  text: '编辑信息',
+                  onClick: () => goDetail(item.id),
+                },
+                {
+                  icon: 'browse',
+                  text: '修改密码',
+                  onClick: () => goDetail(item.id, true),
+                },
+                {
+                  icon: 'trash',
+                  text: '删除账号',
+                  textColor: 'danger',
+                  onClick: () => doDeleteUser(item as unknown as VolunteerInfo),
+                },
+              ]"
+            >
+              <NButton type="primary" icon="edit-filling" :radius="40" />
+            </BubbleBox>
+          </template>
+        </VolunteerItem>
       </FlexCol>
       </FlexCol>
       <SimplePageListLoader :loader="listLoader" :noEmpty="true">
       <SimplePageListLoader :loader="listLoader" :noEmpty="true">
         <template #empty>
         <template #empty>
@@ -107,7 +88,6 @@ import VillageApi, { VolunteerInfo } from '@/api/inhert/VillageApi';
 import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
 import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
 import NButton from '@/components/basic/Button.vue';
 import NButton from '@/components/basic/Button.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Icon from '@/components/basic/Icon.vue';
-import Image from '@/components/basic/Image.vue';
 import Text from '@/components/basic/Text.vue';
 import Text from '@/components/basic/Text.vue';
 import Empty from '@/components/feedback/Empty.vue';
 import Empty from '@/components/feedback/Empty.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
@@ -117,7 +97,8 @@ import Touchable from '@/components/feedback/Touchable.vue';
 import Height from '@/components/layout/space/Height.vue';
 import Height from '@/components/layout/space/Height.vue';
 import BubbleBox from '@/components/feedback/BubbleBox.vue';
 import BubbleBox from '@/components/feedback/BubbleBox.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
-import UserHead from '@/static/images/user/avatar.png';
+import VolunteerItem from './components/VolunteerItem.vue';
+import ButtonGroup from '@/components/basic/ButtonGroup.vue';
 
 
 const { querys } = useLoadQuerys({ 
 const { querys } = useLoadQuerys({ 
   id: 0,  
   id: 0,  

+ 168 - 0
src/pages/dig/admin/review.vue

@@ -0,0 +1,168 @@
+<template>
+  <CommonRoot>
+    <FlexCol :padding="30">
+      <SearchBar
+        v-model="searchText"
+        placeholder="搜一搜" 
+        @confirm="search"
+      />
+
+      <SimplePageListLoader :loader="listLoader" :noEmpty="true">
+        <template #empty>
+          <Empty image="search" text="暂无待审核志愿者" />
+        </template>
+        <FlexCol :gap="20" :margin="[20,0,0,0]">
+          <VolunteerItem
+            v-for="item in listLoader.list.value"
+            :key="item.id" 
+            :item="item"
+          >
+            <FlexRow :gap="20">
+              <Image 
+                :src="item.image"
+                width="100rpx"
+                height="100rpx"
+                :defaultImage="UserHead"
+                :failedImage="UserHead"
+                round 
+              />
+              <FlexCol>
+                <Text :fontSize="36" bold :text="`${item.name} ${item.sex === 0 ? '男' : '女'}`" />
+                <Text :fontSize="26" :text="`手机:${item.mobile}`" />
+                <Text :fontSize="26" :text="`地址:${item.address || ''}`" />
+                <Text :fontSize="26" :text="`可采编:${item.collectModuleText || '暂无'}`" />
+              </FlexCol>
+            </FlexRow>
+            <template #action>
+              <Button type="primary" icon="task-filling" :radius="40" @click="handleShowDetail(item)" />
+            </template>
+          </VolunteerItem>
+        </FlexCol>
+      </SimplePageListLoader>
+
+      <Popup
+        v-model:show="reviewPopup"
+        :title="`审核 ${reviewItem?.title}`"
+        size="80%"
+        round
+        position="bottom"
+        closeable
+      >
+        <FlexCol :gap="30" :padding="30">
+          <DynamicForm
+            v-if="reviewItem"
+            :model="reviewItem"
+            :options="reviewFormDefine"
+            :formGlobalParams="querys"
+          />
+          <Button type="success" @click="handleReview(VolunteerInfo.STATUS_APPROVED)">通过审核</Button>
+          <Button type="danger" @click="handleReview(VolunteerInfo.STATUS_REJECTED)">驳回审核</Button>
+        </FlexCol>
+      </Popup>
+    </FlexCol>
+  </CommonRoot>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import { closeToast, confirm, toast } from '@/components/dialog/CommonRoot';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { getVolunteerForm } from './data/volunteer';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
+import VillageApi, { VolunteerInfo } from '@/api/inhert/VillageApi';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import Button from '@/components/basic/Button.vue';
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import Empty from '@/components/feedback/Empty.vue';
+import SearchBar from '@/components/form/SearchBar.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import UserHead from '@/static/images/user/avatar.png';
+import VolunteerItem from './components/VolunteerItem.vue';
+import Popup from '@/components/dialog/Popup.vue';
+import DynamicForm from '@/components/dynamic/DynamicForm.vue';
+
+const { querys } = useLoadQuerys({ 
+  villageId: 0,
+}, () => {
+  listLoader.loadData(undefined, true);
+});
+
+const searchText = ref('');
+const listLoader = useSimplePageListLoader(8, async (page, pageSize, params) => {
+  if (page === 1) {
+    let res = await VillageApi.getVillageVolunteerList(querys.value.villageId, VolunteerInfo.STATUS_PENDING);
+    if (searchText.value)
+      res = res.filter((p) => p.name.includes(searchText.value));
+    res.forEach((item) => {
+      item.title = `${item.name} ${item.sex === 0 ? '男' : '女'}`;
+      item.desc = ` 手机号:${item.mobile} 地址:${item.address || ''} 认领说明:${item.claimReason || ''}`;
+    })
+    return {
+      page,
+      total: res.length,
+      list: res,
+    }
+  }
+  return {
+    page,
+    total: 0,
+    list: [],
+  }
+});
+
+const reviewPopup = ref(false);
+const reviewItem = ref<VolunteerInfo>();
+const reviewFormRef = ref<IDynamicFormRef>();
+const reviewFormDefine = ref<IDynamicFormOptions>();
+
+function handleShowDetail(item: VolunteerInfo) {
+  reviewItem.value = item;
+  reviewPopup.value = true;
+}
+async function handleReview(status: number) {
+  if (!reviewItem.value)
+    return;
+
+  if (!await confirm({
+    title: '确认审核吗?',
+    content: `确认审核${status === VolunteerInfo.STATUS_APPROVED ? '通过' : '驳回'}${reviewItem.value.title}吗?`,
+  }))
+    return;
+
+  reviewPopup.value = false;
+  toast({
+    type: 'loading',  
+    content: '请稍后',
+  })
+  try {
+    await VillageApi.reviewVillageVolunteer(querys.value.villageId, reviewItem.value.id, status);
+    toast({  type: 'success', content: '审核成功' });
+  } catch (e) {
+    showError(e);
+  } finally {
+    closeToast();
+  }
+}
+function search() {
+  listLoader.loadData(undefined, true);
+}
+
+onMounted(() => {
+  reviewFormDefine.value = {
+    ...getVolunteerForm({
+      canSetCatalog: false,
+      villageId: querys.value.villageId,
+      noPassword: true,
+      isNew: ref(false),
+      formRef: reviewFormRef,
+    }),
+    readonly: true,
+    disabled: true,
+  }
+})
+</script>

+ 10 - 143
src/pages/dig/admin/volunteer.vue

@@ -17,11 +17,12 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, nextTick, ref } from 'vue';
 import { computed, nextTick, ref } from 'vue';
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
-import { useAliOssUploadCo } from '@/common/components/upload/AliOssUploadCo';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import { alert, toast } from '@/components/dialog/CommonRoot';
 import { alert, toast } from '@/components/dialog/CommonRoot';
 import { backAndCallOnPageBack } from '@/components/utils/PageAction';
 import { backAndCallOnPageBack } from '@/components/utils/PageAction';
 import { RequestApiError } from '@imengyu/imengyu-utils';
 import { RequestApiError } from '@imengyu/imengyu-utils';
+import { getVolunteerForm } from './data/volunteer';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 import Button from '@/components/basic/Button.vue';
 import Button from '@/components/basic/Button.vue';
@@ -29,150 +30,12 @@ import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Height from '@/components/layout/space/Height.vue';
 import Height from '@/components/layout/space/Height.vue';
 import VillageApi, { VolunteerInfo } from '@/api/inhert/VillageApi';
 import VillageApi, { VolunteerInfo } from '@/api/inhert/VillageApi';
-import CommonContent from '@/api/CommonContent';
-import type { RuleItem } from 'async-validator';
-import type { IDynamicFormOptions, IDynamicFormRef, IDynamicFormItemCallbackAdditionalProps } from '@/components/dynamic';
-import type { UploaderFieldProps } from '@/components/form/UploaderField.vue';
-import type { FieldProps } from '@/components/form/Field.vue';
-import type { PickerIdFieldProps } from '@/components/dynamic/wrappers/PickerIdField';
-import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
-import type { FormProps } from '@/components/form/Form.vue';
-import type { CheckBoxTreeListProps } from '@/components/dynamic/wrappers/CheckBoxTreeList.vue';
 
 
 const loading = ref(false);
 const loading = ref(false);
 
 
 const formRef = ref<IDynamicFormRef>();
 const formRef = ref<IDynamicFormRef>();
 const formModel = ref<VolunteerInfo>();
 const formModel = ref<VolunteerInfo>();
-const formDefine : IDynamicFormOptions = {
-  formItems: [
-    {
-      name: 'groupBase',
-      type: 'flat-simple',
-      children: [
-        { 
-          label: '用户名', name: 'username', type: 'text',
-          additionalProps: { 
-            placeholder: '请输入用户名',
-          },
-          rules: [{ required: true, message: '请输入用户名' }],
-          show: { callback: () => isNew.value },
-        },
-        {
-          label: '密码',
-          name: 'password',
-          type: 'text',
-          additionalProps: { 
-            placeholder: '请输入密码',
-            type: 'password',
-          } as FieldProps,
-          rules: [{ required: true, message: '请输入密码' }],
-          show: { callback: () => isNew.value || querys.value.onlyPassword },
-        },
-        {
-          label: '确认密码',
-          name: 'passwordRepeat',
-          type: 'text',
-          additionalProps: { 
-            placeholder: '请再输入一次密码',
-            type: 'password',
-          } as FieldProps,
-          rules: [
-            { required: true, message: '请再输入一次密码' },
-            {
-              async validator(rule, value) {
-                if (value != formRef.value?.getValueByPath('password'))
-                  throw '两次输入密码不一致,请检查';
-              },
-            }
-          ] as RuleItem[],
-          show: { callback: () => isNew.value || querys.value.onlyPassword },
-        },
-      ]
-    },
-    {
-      name: 'groupExtra',
-      type: 'flat-simple',
-      show: { callback: () => querys.value.onlyPassword == false },
-      childrenColProps: {
-        span: 24,
-      },
-      children: [
-        {
-          label: '真实名称', name: 'name', type: 'text',
-          additionalProps: { placeholder: '请输入真实名称' },
-          rules: [{ required: true, message: '请输入真实名称' }],
-        },
-        {
-          label: '手机号', name: 'mobile', type: 'text',
-          additionalProps: { placeholder: '请输入手机号' },
-          rules: [{ required: true, message: '请输入手机号' }],
-        },
-        { 
-          label: '区域', name: 'regionId', type: 'select-id',
-          additionalProps: {
-            placeholder: '请选择区域',
-            disabled: { callback: () => !isNew.value },
-            loadData: async () => (await CommonContent.getCategoryList(1)).map(p => ({ text: p.title, value: p.id, raw: p }))
-          } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
-          rules: [{ required: true, message: '请选择区域' }],
-        },
-        {
-          label: '性别', name: 'sex', type: 'radio-value',
-          additionalProps: {
-            options: [
-              { text: '男', value: 1 },
-              { text: '女', value: 2 }
-            ]
-          } as RadioValueProps,
-        },
-        { 
-          label: '头像', name: 'image', type: 'uploader',
-          additionalProps: {
-            single: true,
-            maxFileSize: 1024 * 1024 * 10,
-            upload: useAliOssUploadCo('xiangyuan/volunteer/images')
-          } as UploaderFieldProps,
-        },
-        { label: '地址', name: 'address', type: 'text', additionalProps: { placeholder: '请输入地址' } },
-        { 
-          label: '介绍', 
-          name: 'intro', 
-          type: 'textarea', 
-          additionalProps: { 
-            placeholder: '请输入介绍',
-            showWordLimit: true,
-            maxLength: 200,
-          } as FieldProps,
-        },
-        { 
-          label: '采集版块', name: 'catalogIds', type: 'check-box-tree', 
-          additionalProps: { 
-            placeholder: '请选择采集版块',
-            vertical: true,
-            multiple: true,
-            loadData: async (pid) => (await VillageApi.getCatalogList(querys.value.villageId, pid)).map((p) => ({
-              text: p.title,
-              value: p.id,
-              hasChildren: p.haschild,
-            })),
-          } as CheckBoxTreeListProps,
-        },
-        { 
-          label: '村落认领说明', name: 'claimReason', type: 'text', 
-          additionalProps: { placeholder: '请输入村落认领说明' } ,
-          show: { callback: () => isNew.value },
-        },
-      ]
-    },
-  ],
-  formAdditionaProps: {
-    labelWidth: '160rpx',
-    labelAlign: 'right',
-    innerStyle: {
-      radius: '10rpx',
-    },
-  } as Omit<FormProps, 'model'>,
-}
+const formDefine = ref<IDynamicFormOptions>();
 
 
 async function submit() {
 async function submit() {
   if (!formRef.value || !formModel.value)
   if (!formRef.value || !formModel.value)
@@ -223,6 +86,13 @@ const { querys } = useLoadQuerys({
   loading.value = true;
   loading.value = true;
   if (!formRef.value)
   if (!formRef.value)
     return;
     return;
+  formDefine.value = getVolunteerForm({
+    canSetCatalog: true,
+    villageId: querys.villageId,
+    onlyPassword: querys.onlyPassword,
+    isNew,
+    formRef,
+  });
 
 
   let formData = undefined;
   let formData = undefined;
 
 
@@ -243,9 +113,6 @@ const { querys } = useLoadQuerys({
    
    
   await nextTick();
   await nextTick();
   formRef.value.initDefaultValuesToModel();
   formRef.value.initDefaultValuesToModel();
-
-  console.log('formModel.value', formModel.value);
-  
 });
 });
 const isNew = computed(() => querys.value.id < 0);
 const isNew = computed(() => querys.value.id < 0);