Procházet zdrojové kódy

📦 选择身份对话框,加入接口修改

快乐的梦鱼 před 1 měsícem
rodič
revize
83a98b74c9

+ 14 - 0
.cursor/rules/typescript-type-imports.mdc

@@ -0,0 +1,14 @@
+---
+description: TypeScript 类型导入使用顶层 import type,禁止内联 import('pkg').Type
+globs: **/*.{ts,vue}
+alwaysApply: false
+---
+
+# TypeScript 类型导入规范
+
+自动补全或手写类型引用时:
+
+- **使用**:`import type { RuleItem } from 'async-validator'`,再在类型处写 `RuleItem[]`
+- **禁止**:在类型注解里写 `import('async-validator').RuleItem[]` 等内联模块类型
+
+若缺少导入,应添加文件顶部的 `import type { ... } from '...'`,不要用 `import('...').` 内联形式。

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+  "typescript.preferences.preferTypeOnlyAutoImports": true,
+}

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

@@ -133,16 +133,12 @@ export class VillageClaimInfo extends DataModel<VillageClaimInfo> {
     this._convertTable = {
       villageId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
       sex: { clientSide: 'number', serverSide: 'number' },
-      type: [
-        { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-        { clientSide: 'array', clientSideChildDataModel: 'string', serverSide: 'original' },
-      ],
     };
   }
 
   villageId = 0;
   name = '';
-  type = ['villager'] as string[];
+  type = 'villager';
   mobile = '';
   address = '';
   claimReason = '';

+ 9 - 4
src/common/components/CommonDialog.vue

@@ -20,14 +20,14 @@
         <slot name="titlePre"></slot>
         <FlexCol center gap="gap.lg">
           <H3 fontConfig="primaryTitle">{{ props.title }}</H3>
-          <CommonDivider />
+          <CommonDivider v-if="props.showDivider" />
         </FlexCol>
       </template>
       <slot></slot>
       <Height :size="24" />
     </BackgroundBox>
     <Height :size="24" />
-    <ImageButton 
+    <ImageButton v-if="props.showCloseButton"
       src="https://xy.wenlvti.net/app_static/images/ButtonClose.png"
       :width="80"
       :height="80"
@@ -45,9 +45,14 @@ import H3 from '@/components/typography/H3.vue';
 import CommonDivider from './CommonDivider.vue';
 import ImageButton from '@/components/basic/ImageButton.vue';
 
-const props = defineProps<DialogProps & {
+const props = withDefaults(defineProps<DialogProps & {
   title?: string;
-}>();
+  showCloseButton?: boolean;
+  showDivider?: boolean;
+}>(), {
+  showCloseButton: true,
+  showDivider: true,
+});
 const emit = defineEmits<{
   (e: 'update:show', value: boolean): void;
 }>();

+ 5 - 0
src/common/components/FrameButton.vue

@@ -6,14 +6,18 @@
     :backgroundCutBorderSize="36"
     :padding="padding"
     :width="width"
+    :touchable="!loading"
     center
+    gap="gap.md"
     @click="emit('click')"
   >
+    <ActivityIndicator v-if="loading" :size="36" :color="primary ? 'white' : 'black'" />
     <Text :text="text" fontConfig="lightTitle" :fontSize="36" :color="primary ? 'white' : 'black'" />
   </BackgroundImageButton>
 </template>
 
 <script setup lang="ts">
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
 import BackgroundImageButton from '@/components/basic/BackgroundImageButton.vue';
 import Text from '@/components/basic/Text.vue';
 import { computed } from 'vue';
@@ -24,6 +28,7 @@ const props = withDefaults(defineProps<{
   size?: 'large' | 'small';
   width?: string|number;
   innerStyle?: object;
+  loading?: boolean;
 }>(), {
   text: '去发布',
   primary: false,

+ 1 - 1
src/pages/home/index.vue

@@ -326,7 +326,7 @@ function handleGoAI() {
   }, '暂时需要登录后才能使用AI助手');
 }
 async function handleLightVillage() {
-  requireLogin(async () => navTo('/pages/home/light/submit-map', { city: currentCity }), '登录后才能点亮村社哦!');
+  requireLogin(async () => navTo('/pages/home/light/submit-map', { city: currentCity.value }), '登录后才能点亮村社哦!');
 }
 
 async function loadInfo() {

+ 5 - 12
src/pages/home/light/form/claim.ts

@@ -23,11 +23,7 @@ export function fillClaimFromVolunteer(model: VillageClaimInfo, volunteer: Volun
   model.job = (volunteer as VolunteerInfo & { job?: string }).job || '';
   model.unit = (volunteer as VolunteerInfo & { unit?: string }).unit || '';
   model.claimReason = (volunteer as VolunteerInfo & { claimReason?: string }).claimReason || '';
-  if (volunteer.type) {
-    model.type = volunteer.type.includes(',')
-      ? volunteer.type.split(',').map((s) => s.trim()).filter(Boolean)
-      : [volunteer.type];
-  }
+  model.type = volunteer.type;
 }
 
 export function getClaimVillageForm(options: {
@@ -50,18 +46,15 @@ export function getClaimVillageForm(options: {
           {
             label: '认领类型',
             name: 'type',
-            type: 'check-box-list',
-            defaultValue: ['villager'],
+            type: 'radio-value',
+            defaultValue: 'villager',
             additionalProps: {
-              multiple: true,
-              vertical: true,
-              useCell: true,
-              loadData: async () => [
+              options: [
                 { text: '村民', value: 'villager' },
                 { text: '志愿者', value: 'volunteer' },
                 { text: '管理人员', value: 'staff' },
               ],
-            } as CheckBoxListProps,
+            } as RadioValueProps,
             rules: [{ required: true, message: '请选择认领类型' }],
           },
           {

+ 14 - 14
src/pages/home/light/submit.vue

@@ -66,6 +66,19 @@
 </template>
 
 <script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { useAppInit } from '@/common/composeabe/AppInit';
+import { useAuthStore } from '@/store/auth';
+import { useUserTools } from '@/common/composeabe/UserTools';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { UserApi } from '@/api/auth/UserApi';
+import { waitTimeOut } from '@imengyu/imengyu-utils';
+import { fillClaimFromVolunteer, getClaimVillageForm } from './form/claim';
+import { getVolunteerForm } from './form/volunteer';
+import { back } from '@/components/utils/PageAction';
+import { closeToast, toast } from '@/components/dialog/CommonRoot';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
 import Button from '@/components/basic/Button.vue';
 import Result from '@/components/feedback/Result.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
@@ -74,21 +87,8 @@ import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
-import { useAppInit } from '@/common/composeabe/AppInit';
-import { UserApi } from '@/api/auth/UserApi';
-import { useAuthStore } from '@/store/auth';
-import { back } from '@/components/utils/PageAction';
-import { closeToast, toast } from '@/components/dialog/CommonRoot';
-import { showError } from '@/common/composeabe/ErrorDisplay';
-import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
-import { onMounted, ref } from 'vue';
-import VillageApi, { VillageClaimInfo, VolunteerInfo } from '@/api/inhert/VillageApi';
-import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
-import { waitTimeOut } from '@imengyu/imengyu-utils';
-import { fillClaimFromVolunteer, getClaimVillageForm } from './form/claim';
-import { getVolunteerForm } from './form/volunteer';
 import Alert from '@/components/feedback/Alert.vue';
-import { useUserTools } from '@/common/composeabe/UserTools';
+import VillageApi, { VillageClaimInfo, VolunteerInfo } from '@/api/inhert/VillageApi';
 
 /**
  * 点亮村社页面

+ 278 - 0
src/pages/home/village/dialogs/JoinDialog.vue

@@ -0,0 +1,278 @@
+<template>
+  <CommonDialog v-model:show="show" title="加入村社" :showDivider="false" :showCloseButton="false">
+    <FlexCol gap="gap.lg" padding="padding.md" width="600rpx">
+      
+      <template v-if="step === 'chooseType'">
+        <FlexCol gap="gap.md">
+          <Text text="加入村社后,您即可为村社做贡献。请完善身份信息,解锁更多村社专属功能" fontConfig="contentSpeicalText" />
+          <Height :height="10" />
+
+          <BackgroundBox 
+            v-for="(item, k) in typeChoices"
+            :key="k"
+            backgroundImage="https://xy.wenlvti.net/app_static/images/village/BoxMid.png"
+            :backgroundCutBorder="32"
+            :backgroundCutBorderSize="36"
+            :padding="24"
+            direction="row"
+          >
+            <Touchable direction="row" gap="gap.md"center @click="handleChooseType(item.value)">
+              <Image :src="item.image" :radius="20" width="100" height="100" mode="aspectFill" />
+              <Width :width="20" />
+              <Text text="我是" fontConfig="lightImportantTitle" />
+              <Text :text="item.label" fontConfig="lightGoldTitle" />
+            </Touchable>
+          </BackgroundBox>
+        </FlexCol>
+      </template>
+      <template v-else-if="step === 'chooseIdentity'">
+        <FlexCol gap="gap.md">
+          <Text textAlign="center" text="请选择你的村民身份" fontConfig="contentSpeicalText" />
+          <Height :height="10" />
+
+          <BackgroundBox 
+            v-for="(item, k) in identityChoices"
+            :key="k"
+            backgroundImage="https://xy.wenlvti.net/app_static/images/village/BoxMid.png"
+            :backgroundCutBorder="32"
+            :backgroundCutBorderSize="36"
+            :padding="24"
+            direction="row"
+          >
+            <Touchable direction="row" gap="gap.md"center @click="handleChooseIdentity(item.value)">
+              <Image :src="item.image" :radius="20" width="100" height="100" mode="aspectFill" />
+              <Width :width="20" />
+              <Text :text="item.label" fontConfig="lightGoldTitle" />
+            </Touchable>
+          </BackgroundBox>
+        </FlexCol>
+      </template>
+      <template v-else-if="step === 'form'">
+        <Text textAlign="center" text="请完善身份信息,解锁更多村社专属功能" fontConfig="contentSpeicalText" />
+        <Height :height="10" />
+        
+        <ProvideVar :vars="{
+          FieldBackgroundColor: 'transparent',
+        }">
+          <BackgroundBox
+            backgroundImage="https://xy.wenlvti.net/app_static/images/village/BoxMid.png"
+            :backgroundCutBorder="32"
+            :backgroundCutBorderSize="36"
+            :padding="24"
+          >
+            <DynamicForm
+              ref="addFormRef"
+              :model="addFormModel"
+              :options="addFormDefine"
+            />
+          </BackgroundBox>
+        </ProvideVar>
+        <Height :height="20" />
+        <FlexRow justify="space-around" gap="gap.md">
+          <FrameButton text="返回" @click="step = 'chooseType'" width="220rpx" />
+          <FrameButton primary text="提交" @click="addSubmit" :loading="addFormLoading" width="220rpx" />
+        </FlexRow>
+      </template>
+      <template v-else-if="step === 'finished'">
+        <Result
+          status="success"
+          title="提交成功"
+          description="等待管理员审核,通过后即可为村社做贡献,您可以先逛逛学习吧"
+        />
+        <FlexRow justify="space-around" gap="gap.md">
+          <FrameButton primary text="完成" @click="show = false" width="220rpx" />
+        </FlexRow>
+      </template>
+    </FlexCol>
+  </CommonDialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { toast } from '@/components/dialog/CommonRoot';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import VillageApi, { VillageClaimInfo } from '@/api/inhert/VillageApi';
+import CommonDialog from '@/common/components/CommonDialog.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import DynamicForm from '@/components/dynamic/DynamicForm.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Height from '@/components/layout/space/Height.vue';
+import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
+import type { RuleItem } from 'async-validator';
+import type { FieldProps } from '@/components/form/Field.vue';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
+import Result from '@/components/feedback/Result.vue';
+import Width from '@/components/layout/space/Width.vue';
+import ProvideVar from '@/components/theme/ProvideVar.vue';
+
+const show = ref(false);
+const props = defineProps<{
+  villageId: number;
+}>();
+const emit = defineEmits(['apply']);
+
+const step = ref('chooseType');
+
+const typeChoices = ref([
+  {
+    label: '志愿者',
+    value: 'volunteer',
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeExternal.png',
+  },
+  {
+    label: '管理人员',
+    value: 'staff',
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeLocal.png',
+  },
+  {
+    label: '村民',
+    value: 'villager',
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeWork.png',
+  },
+]);
+
+function handleChooseType(type: string) {
+  step.value = 'chooseIdentity';
+  addFormModel.value.type = type;
+}
+
+const identityChoices = ref([
+  {
+    label: '在村劳作',
+    value: 1,
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeWork.png',
+  },
+  {
+    label: '社区居民',
+    value: 2,
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeLocal.png',
+  },
+  {
+    label: '在外游子',
+    value: 3,
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeOut.png',
+  },
+  {
+    label: '退休人员',
+    value: 4,
+    image: 'https://xy.wenlvti.net/app_static/images/home/volunteer/IconTypeOld.png',
+  },
+]);
+
+function handleChooseIdentity(identity: number) {
+  step.value = 'form';
+  addFormModel.value.identity = identity;
+}
+
+const addFormLoading = ref(false);
+const addFormRef = ref<IDynamicFormRef>();
+const addFormModel = ref<VillageClaimInfo>(new VillageClaimInfo());
+const addFormDefine : IDynamicFormOptions = {
+  formAdditionaProps: {},
+  formItems: [
+    {
+      label: '真实姓名',
+      name: 'name',
+      type: 'text',
+      additionalProps: { placeholder: '请输入真实姓名' },
+      rules: [{ required: true, message: '请输入真实姓名' }],
+    },
+    {
+      label: '联系方式',
+      name: 'mobile',
+      type: 'text',
+      additionalProps: { placeholder: '请输入手机号' },
+      rules: [{ required: true, message: '请输入联系方式' }],
+    },
+    {
+      label: '性别',
+      name: 'sex',
+      type: 'radio-value',
+      defaultValue: 3,
+      additionalProps: {
+        options: [
+          { text: '男', value: 1 },
+          { text: '女', value: 2 },
+          { text: '未知', value: 3 },
+        ],
+      } as RadioValueProps,
+    },
+    {
+      label: '居住地址',
+      name: 'address',
+      type: 'text',
+      additionalProps: { placeholder: '请输入居住地址(选填)' },
+    },
+    {
+      label: '工作单位',
+      name: 'unit',
+      type: 'text',
+      additionalProps: { placeholder: '请输入工作单位(选填)' },
+      show: { callback: () => addFormModel.value.type === 'staff' },
+      rules: [{
+        required: true,
+        message: '请输入工作单位',
+      }] as RuleItem[],
+    },
+    {
+      label: '职位',
+      name: 'job',
+      type: 'text',
+      additionalProps: { placeholder: '请输入职位' },
+      show: { callback: () => addFormModel.value.type === 'staff' },
+      rules: [{
+        required: true,
+        message: '请输入职位',
+      }] as RuleItem[],
+    },
+    {
+      label: '申请理由',
+      name: 'claimReason',
+      type: 'textarea',
+      additionalProps: {
+        placeholder: '请说明认领该村社的原因(选填),可以加快管理员审核速度',
+        showWordLimit: true,
+        maxLength: 200,
+      } as FieldProps,
+    },
+  ]
+}
+
+async function addSubmit() {
+  if (!addFormRef.value || !addFormModel.value)
+    return;
+  try {
+    await addFormRef.value.validate();
+  } catch (e) {
+    toast({ content: '有必填项未填写,请检查' });
+    return;
+  }
+
+  try {
+    addFormLoading.value = true;
+    addFormModel.value.villageId = props.villageId;
+    await VillageApi.claimVallage(addFormModel.value as VillageClaimInfo);
+    toast({ content: '提交成功' });
+    show.value = false;
+  } catch (e) {
+    showError(e);
+  } finally {
+    addFormLoading.value = false;
+  }
+}
+
+defineExpose({
+  show: () => {
+    show.value = true;
+    step.value = 'chooseType';
+    addFormModel.value = new VillageClaimInfo();
+    addFormModel.value.type = 'villager';
+    addFormModel.value.villageId = props.villageId;
+  },
+});
+</script>

+ 6 - 6
src/pages/home/village/introd/card.vue

@@ -157,7 +157,7 @@
     <RoundTags v-model:active="rankActiveTag" :tags="['乡源果', '志愿者', '乡源光']" />
     <VillageUserRankList 
       :list="villageUserRankListLoader.content.value ?? []" 
-      @goDetails="navTo('../volunteer/detail', { id: $event.id })"
+      @goDetails="navTo('/pages/home/village/volunteer/detail', { id: $event.id })"
     />
 
     <!-- 魅力乡源 -->
@@ -244,6 +244,7 @@
     @publishsuccess="onPublishSuccess"
   />
 
+  <JoinDialog ref="joinDialog" :villageId="villageStore.currentVillage?.id ?? 0" />
   <ApplyGoodsDialog ref="applyGoodsDialog" />
 </template>
 
@@ -280,6 +281,7 @@ import LightVillageApi from '@/api/light/LightVillageApi';
 import Construction from '@/common/components/Construction.vue';
 import OfficialAccountPublishWrap from '@/common/components/OfficialAccountPublishWrap.vue';
 import ApplyGoodsDialog from '../dialogs/ApplyGoodsDialog.vue';
+import JoinDialog from '../dialogs/JoinDialog.vue';
 
 const authStore = useAuthStore();
 const { getIsVolunteer } = useUserTools();
@@ -314,6 +316,8 @@ const emit = defineEmits<{
   (e: 'goTree'): void;
 }>();
 
+const joinDialog = ref();
+
 const rankActiveTag = ref('乡源果');
 const listActiveTag = ref('广场');
 
@@ -351,11 +355,7 @@ const recommendTagName = computed(() => {
 function handleGoJoin() {
   if (isJoined.value)
     return;
-  navTo('/pages/home/light/submit', {
-    villageId: villageStore.currentVillage?.id ?? undefined,
-    unit: (villageStore.currentVillage?.city as string) + (villageStore.currentVillage?.district as string) + (villageStore.currentVillage?.township as string),
-    regionId: villageStore.currentVillage?.region ?? undefined,
-  });
+  joinDialog.value.show();
 }
 function handleGoNew() {
   navTo('/pages/home/light/help/new');

+ 2 - 2
src/pages/index.vue

@@ -88,7 +88,7 @@ onShareTimeline(() => {
   }
 })
 onMounted(() => {
-  //if (isDevEnv)
-    //tabIndex.value = 1;
+  if (isDevEnv)
+    tabIndex.value = 1;
 })
 </script>