Prechádzať zdrojové kódy

按要求修改和优化

快乐的梦鱼 2 mesiacov pred
rodič
commit
ef9d50c125

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

@@ -226,10 +226,16 @@ export class VillageApi extends AppServerRequestModule<DataModel> {
     return (this.post('/village/volunteer/add', data.toServerSide(), '添加志愿者')) ;
   }
   async updateVolunteer(data: VolunteerInfo) {
-    return (this.post('/village/volunteer/update', data.toServerSide(), '更新志愿者')) ;
+    return (this.post('/village/volunteer/save', data.toServerSide(), '更新志愿者')) ;
+  }
+  async getVolunteerInfoByIdAdmin(id: number) {
+    return (await this.post('/village/volunteer/info', {
+      id,
+    }, '管理员获取志愿者信息', undefined, VolunteerInfo)).data as VolunteerInfo
   }
   async getVolunteerInfoById(id: number) {
     return (await this.post('/village/volunteer/getInfo', {
+      id,
     }, '获取志愿者信息', undefined, VolunteerInfo)).data as VolunteerInfo
   }
   async getVillageVolunteerList(villageId?: number) {

+ 1 - 1
src/api/inhert/VillageInfoApi.ts

@@ -113,7 +113,7 @@ export class VillageEnvInfo extends DataModel<VillageEnvInfo> {
 }
 export class VillageListItem extends DataModel<VillageListItem> {
   constructor() {
-    super(VillageListItem, "村信息");
+    super(VillageListItem, "村信息");
     this.setNameMapperCase('Camel', 'Snake');
     this._convertTable = {
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },

+ 1 - 1
src/common/config/ApiCofig.ts

@@ -6,6 +6,6 @@ export default {
   serverDev: 'https://xy.wenlvti.net/api',
   serverProd: 'https://xy.wenlvti.net/api',
   amapServerKey: '8fd09264c33678141f609588c432df0e',
-  mainBodyId: 2,
+  mainBodyId: 1,
   platformId: 330,
 }

+ 5 - 0
src/components/basic/Icon.vue

@@ -66,6 +66,10 @@ export interface IconProps {
    */
   color?: string;
   /**
+   * 图标旋转角度
+   */
+  rotate?: number;
+  /**
    * 自定义样式
    */
   innerStyle?: object,
@@ -110,6 +114,7 @@ const style = computed(() => {
     height: size,
     color: theme.resolveThemeColor(props.color, 'text.content'),
     fill: theme.resolveThemeColor(props.color, 'text.content'),
+    transform: props.rotate ? `rotate(${props.rotate}deg)` : undefined,
     ...props.innerStyle,
   };
 });

+ 9 - 3
src/components/display/CollapseBox.vue

@@ -4,8 +4,8 @@
     class="nana-collapse-box" 
     :style="{
       display: realOpenState ? '' : 'none',
-      height: animDuration > 0 && targetHeight >= 0 ? `${targetHeight}px` : undefined,
-      transition: animDuration > 0 ? `height ${animDuration}ms ease-in-out` : undefined,
+      height: anim && animDuration > 0 && targetHeight >= 0 ? `${targetHeight}px` : undefined,
+      transition: anim && animDuration > 0 ? `height ${animDuration}ms ease-in-out` : undefined,
     }"
   >
     <slot />
@@ -22,6 +22,11 @@ export interface CollapseBoxProps {
    */
   open: boolean;
   /**
+   * 是否开启动画
+   * @default true
+   */
+  anim?: boolean;
+  /**
    * 动画时长(ms),为0时禁用动画
    * @default 300
    */
@@ -35,6 +40,7 @@ export interface CollapseBoxProps {
 const id = computed(() => `nana-collapse-box-${props.name}-${RandomUtils.genNonDuplicateIDHEX(16)}`);
 const props = withDefaults(defineProps<CollapseBoxProps>(), {
   animDuration: 300,
+  anim: true,
 });
 
 const realOpenState = ref(false);
@@ -43,7 +49,7 @@ const instance = getCurrentInstance();
 let isAnimWorking = false;
 
 watch(() => props.open, (newVal) => {
-  if (props.animDuration <= 0) {
+  if (props.animDuration <= 0 || !props.anim) {
     realOpenState.value = newVal;
     return;
   }

+ 3 - 12
src/components/display/CollapseItem.vue

@@ -40,7 +40,8 @@
         <template #rightIcon>
           <Icon 
             icon="arrow-down" 
-            :class="['nana-collapse-item-icon', {'open': state}]"
+            :rotate="state ? 180 : 0"
+            :innerStyle="{ transition: 'transform 0.3s ease-in-out' }"
           />
         </template>
       </Cell>
@@ -105,14 +106,4 @@ const id = computed(() => props.name || position.value);
 const state = computed(() => context.activeName.value.includes(id.value));
 
 const { position } = useChildLinkChild(() => context.getPosition(props.name));
-</script>
-
-<style lang="scss">
-.nana-collapse-item-icon {
-  transition: transform 0.3s ease-in-out;
-
-  &.open {
-    transform: rotate(180deg);
-  }
-}
-</style>
+</script>

+ 9 - 0
src/components/dynamic/DynamicFormControl.vue

@@ -152,6 +152,14 @@
           v-bind="(params)"
         />
       </template>
+      <template v-else-if="item.type === 'check-box-tree'"> 
+        <CheckBoxTreeList
+          ref="itemRef"
+          :modelValue="model"
+          @update:modelValue="onValueChanged"
+          v-bind="(params as any as CheckBoxTreeListProps)"
+        />
+      </template>
       <template v-else-if="item.type === 'check-box-int'"> 
         <CheckBoxToInt
           ref="itemRef"
@@ -251,6 +259,7 @@ import PickerAddressField from './wrappers/PickerAddressField.vue';
 import Button from '../basic/Button.vue';
 import Alert from '../feedback/Alert.vue';
 import Image from '../basic/Image.vue';
+import CheckBoxTreeList, { type CheckBoxTreeListProps } from './wrappers/CheckBoxTreeList.vue';
 
 export interface FormCeilProps {
   model: unknown,

+ 89 - 0
src/components/dynamic/wrappers/CheckBoxTreeList.vue

@@ -0,0 +1,89 @@
+<template>
+  <FlexView :direction="vertical ? 'column' : 'row'" align="center" :gap="10" wrap>
+    <ActivityIndicator v-if="loadStatus === 'loading'" />
+    <Alert
+      v-else-if="loadStatus === 'error'" 
+      message="加载失败" 
+      description="点击重新加载" 
+      type="error" 
+      @click="handleLoadData"
+    />
+    <CheckBoxGroup 
+      v-else 
+      :modelValue="modelValue"
+      :disabled="disabled"
+      :multiple="multiple"
+      @update:modelValue="handleChange" 
+    >
+      <FlexCol>
+        <CheckBoxTreeListItem
+          v-for="item in data2"
+          :key="item.value"
+          :item="item"
+        />
+      </FlexCol>
+    </CheckBoxGroup>
+  </FlexView>
+</template>
+
+<script setup lang="ts">
+import { onMounted, provide, ref } from 'vue';
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
+import Alert from '@/components/feedback/Alert.vue';
+import CheckBoxGroup from '@/components/form/CheckBoxGroup.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexView from '@/components/layout/FlexView.vue';
+import CheckBoxTreeListItem from './CheckBoxTreeListItem.vue';
+
+export interface CheckBoxTreeListItem {
+  text: string;
+  value: any;
+  disable?: boolean;
+  hasChildren?: boolean;
+  children?: CheckBoxTreeListItem[];
+}
+export interface CheckBoxTreeListProps {
+  multiple?: boolean,
+  disabled?: boolean,
+  vertical?: boolean,
+  className?: string,
+  loadData: (pid?: number) => Promise<CheckBoxTreeListItem[]>;
+}
+
+const props = defineProps<CheckBoxTreeListProps & {
+  modelValue?: string[],
+}>();
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const loadStatus = ref<'loading' | 'error' | 'success'>('loading');
+const loadError = ref('');
+const data2 = ref<CheckBoxTreeListItem[]>([]);
+
+provide('loadData', props.loadData);
+
+const handleChange = (checkedValues: any[]) => {
+  emit('update:modelValue', checkedValues);
+  emit('change', checkedValues);
+}
+const handleLoadData = () => {
+  loadStatus.value = 'loading';
+  loadError.value = '';
+  
+  props.loadData().then((v) => {
+    data2.value = v;
+    loadStatus.value = 'success';
+  }).catch((e) => {
+    loadError.value = e.message || '加载失败';
+    loadStatus.value = 'error';
+  });
+}
+const reload = () => {
+  handleLoadData();
+}
+
+defineExpose({ reload });
+
+onMounted(() => {
+  handleLoadData();
+});
+</script>

+ 64 - 0
src/components/dynamic/wrappers/CheckBoxTreeListItem.vue

@@ -0,0 +1,64 @@
+<template>
+  <FlexCol>
+    <FlexRow align="center" :gap="20">
+      <IconButton
+        v-if="item.hasChildren !== false"
+        icon="arrow-right" 
+        :rotate="open ? 90 : 0"
+        :innerStyle="{ transition: 'transform 0.3s ease-in-out' }"
+        @click="toggleOpen"
+      />
+      <CheckBox
+        :key="item.value"
+        :name="item.value"
+        :text="item.text" 
+        :disabled="item.disable"
+      />
+    </FlexRow>
+    <CollapseBox :open="open" :anim="false">
+      <FlexCol :padding="[10,0,10,30]">
+        <CheckBoxTreeListItemWrapper
+          v-for="child in item.children"
+          :key="child.value"
+          :item="child"
+        />
+      </FlexCol>
+    </CollapseBox>
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { inject, ref } from 'vue';
+import type { CheckBoxTreeListItem } from './CheckBoxTreeList.vue';
+import CollapseBox from '@/components/display/CollapseBox.vue';
+import CheckBox from '@/components/form/CheckBox.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import IconButton from '@/components/basic/IconButton.vue';
+import CheckBoxTreeListItemWrapper from './CheckBoxTreeListItemWrapper.vue';
+
+const props = defineProps<{
+  item: CheckBoxTreeListItem,
+}>();
+
+const open = ref(false);
+const loadData = inject('loadData') as (pid?: number) => Promise<CheckBoxTreeListItem[]>;
+
+function toggleOpen() {
+  open.value = !open.value;
+  if (open.value && (!props.item.children || props.item.children.length === 0))
+    loadData(props.item.value).then((children) => {
+      props.item.children = children;
+    });
+}
+</script>
+
+<style lang="scss">
+.nana-collapse-item-icon {
+  transition: transform 0.3s ease-in-out;
+
+  &.open {
+    transform: rotate(180deg);
+  }
+}
+</style>

+ 14 - 0
src/components/dynamic/wrappers/CheckBoxTreeListItemWrapper.vue

@@ -0,0 +1,14 @@
+<template>
+  <CheckBoxTreeListItemComponent
+    :item="item"
+  />
+</template>
+
+<script setup lang="ts">
+import type { CheckBoxTreeListItem } from './CheckBoxTreeList.vue';
+import CheckBoxTreeListItemComponent from './CheckBoxTreeListItem.vue';
+
+const props = defineProps<{
+  item: CheckBoxTreeListItem,
+}>();
+</script>

+ 1 - 0
src/components/form/CalendarField.vue

@@ -146,6 +146,7 @@ const {
   [],
   props.shouldUpdateValueImmediately,
   props.beforeConfirm,
+  popupShow,
 );
 
 watch(tempValue, (v) => {

+ 2 - 0
src/components/form/CascadePickerField.vue

@@ -116,6 +116,8 @@ const {
   emit as any,
   [],
   props.shouldUpdateValueImmediately,
+  undefined,
+  popupShow,
 );
 
 defineOptions({

+ 1 - 0
src/components/form/CascaderField.vue

@@ -127,6 +127,7 @@ const {
   [],
   props.shouldUpdateValueImmediately,
   props.beforeConfirm,
+  popupShow,
 );
 
 function onPickEnd() {

+ 2 - 0
src/components/form/DatePickerField.vue

@@ -116,6 +116,8 @@ const {
   emit as any,
   new Date(),
   props.shouldUpdateValueImmediately,
+  undefined,
+  popupShow,
 );
 
 defineOptions({

+ 2 - 0
src/components/form/DateTimePickerField.vue

@@ -119,6 +119,8 @@ const {
   emit as any,
   new Date(),
   props.shouldUpdateValueImmediately,
+  undefined,
+  popupShow,
 );
 
 defineOptions({

+ 2 - 0
src/components/form/PickerField.vue

@@ -111,6 +111,8 @@ const {
   emit as any,
   [],
   props.shouldUpdateValueImmediately,
+  undefined,
+  popupShow,
 );
 
 defineOptions({

+ 50 - 6
src/components/form/PickerUtils.ts

@@ -1,5 +1,19 @@
-import { ref, watch, type Ref } from "vue";
+import { nextTick, ref, watch, type Ref } from "vue";
 
+/**
+ * 选择器字段临时存储数据的组合式函数
+ * 用于管理选择器组件的临时值、选择文本和交互逻辑
+ * 
+ * @template T - 值的类型
+ * @param 当前值的响应式引用
+ * @param 更新值的回调函数
+ * @param 关闭弹窗的回调函数
+ * @param 事件发射器函数
+ * @param 默认的新值
+ * @param 是否立即更新值
+ * @param 确认前的回调函数,返回true时取消确认
+ * @param 弹窗显示状态的响应式引用
+ */
 export function usePickerFieldTempStorageData<T>(
   value: Ref<T>, 
   updateValue: (d: T) => void,
@@ -8,24 +22,43 @@ export function usePickerFieldTempStorageData<T>(
   defaultNewValue: T,
   shouldUpdateValueImmediately: boolean,
   beforeConfirm?: ((value: T) => Promise<boolean>) | undefined,
+  popupShow?: Ref<boolean>,
 ) {
 
   let tempSelectText = '';
+  let tempLastSelectText = '';
+  // 临时值的响应式引用,初始值为当前值或默认新值
   const tempValue = ref(value.value ?? defaultNewValue) as Ref<T>;
+  // 显示的文本的响应式引用
   const selectText = ref('');
 
+  /**
+   * 当选择文本变化时的处理函数
+   * @param 新的选择文本
+   * @param 是否强制更新显示的选择文本
+   */
   function onSelectTextChange(t: string, forceUpdate = false) {
     tempSelectText = t;
     emit('selectTextChange', t);
     if (forceUpdate)
       selectText.value = t;
   }
+
+  /**
+   * 取消选择的处理函数
+   */
   function onCancel() {
     closePopup();
     emit('cancel');
+    selectText.value = tempLastSelectText;
   }
+
+  /**
+   * 确认选择的处理函数
+   */
   async function onConfirm() {
     closePopup();
+    // 如果有确认前回调且返回true,则取消确认
     if (beforeConfirm && await beforeConfirm(tempValue.value))
       return;
     selectText.value = tempSelectText;
@@ -42,12 +75,23 @@ export function usePickerFieldTempStorageData<T>(
       updateValue(tempValue.value);
   });
 
+  // 如果提供了弹窗显示状态引用,则监听其变化
+  if (popupShow) {
+    watch(popupShow, (v) => {
+      if (v) {
+        // 弹窗显示时,记录当前的选择文本作为上一次的选择文本
+        nextTick(() => {
+          tempLastSelectText = tempSelectText;
+        });
+      }
+    })
+  }
 
   return {
-    onSelectTextChange,
-    onCancel,
-    onConfirm,
-    selectText,
-    tempValue,
+    onSelectTextChange, // 选择文本变化处理函数
+    onCancel, // 取消处理函数
+    onConfirm, // 确认处理函数
+    selectText, // 显示的选择文本
+    tempValue, // 临时值
   }
 }

+ 2 - 0
src/components/form/TimePickerField.vue

@@ -112,6 +112,8 @@ const {
   emit as any,
   new Date(),
   props.shouldUpdateValueImmediately,
+  undefined,
+  popupShow,
 );
 
 defineOptions({

+ 12 - 1
src/pages/article/common/CommonListPage.vue

@@ -174,6 +174,13 @@ const props = defineProps({
     default: true,
   },
   /**
+   * 初始搜索文本
+   */
+  intitalSearch: {
+    type: String,
+    default: '',
+  },
+  /**
    * 显示总数
    */
   showTotal: {
@@ -257,7 +264,7 @@ const dropDownVisibleCount = computed(() => {
   return c;
 })
 const dropDownValues = ref<any>([]);
-const searchValue = ref('');
+const searchValue = ref(props.intitalSearch);
 const listLoader = useSimplePageListLoader(props.pageSize, async (page, pageSize) => {
   return await props.load(
     page, pageSize, 
@@ -268,6 +275,10 @@ const listLoader = useSimplePageListLoader(props.pageSize, async (page, pageSize
 });
 const tabCurrentIndex = ref(0)
 
+watch(() => props.intitalSearch, (newValue) => {
+  searchValue.value = newValue;
+});
+
 function handleChangeDropDownValue(index: number, value: number) {
   dropDownValues.value[index] = value;
   listLoader.loadData(undefined, true);

+ 2 - 0
src/pages/article/common/list.vue

@@ -8,6 +8,7 @@
       mainBodyColumnId: querys.mainBodyColumnId || undefined,
       modelId: querys.modelId || undefined,
     }"
+    :intitalSearch="querys.search"
   />
 </template>
 
@@ -23,6 +24,7 @@ const { querys } = useLoadQuerys({
   detailsPage: '',
   title: '',
   region: '',
+  search: '',
 });
 
 async function loadData(

+ 13 - 21
src/pages/dig/admin/volunteer.vue

@@ -38,6 +38,7 @@ import type { PickerIdFieldProps } from '@/components/dynamic/wrappers/PickerIdF
 import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
 import type { CheckBoxListItem, CheckBoxListProps } from '@/components/dynamic/wrappers/CheckBoxList.vue';
 import type { FormProps } from '@/components/form/Form.vue';
+import type { CheckBoxTreeListProps } from '@/components/dynamic/wrappers/CheckBoxTreeList.vue';
 
 const loading = ref(false);
 
@@ -101,13 +102,14 @@ const formDefine : IDynamicFormOptions = {
       rules: [{ required: true, message: '请选择区域' }],
     },
     { 
-      label: '所属村', name: 'villageId', type: 'select-id',
+      label: '所属村', name: 'villageId', type: 'select-id',
       additionalProps: {
-        placeholder: '请选择所属村',
+        placeholder: '请选择所属村',
         disabled: { callback: () => !isNew.value },
         loadData: async () => (await VillageApi.getClaimedVallageList()).map(p => ({ text: p.title, value: p.id, raw: p })),
       } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
-      rules: [{ required: true, message: '请选择所属村庄' }],
+      rules: [{ required: true, message: '请选择所属村社' }],
+      show: { callback: () => isNew.value },
     },
     {
       label: '性别', name: 'sex', type: 'radio-value',
@@ -138,27 +140,17 @@ const formDefine : IDynamicFormOptions = {
       } as FieldProps,
     },
     { 
-      label: '采集版块', name: 'catalogIds', type: 'check-box-list', 
+      label: '采集版块', name: 'catalogIds', type: 'check-box-tree', 
       additionalProps: { 
         placeholder: '请选择采集版块',
         vertical: true,
         multiple: true,
-        loadData: async () => {
-          async function getCatalogChildren(pid?: number, prefix?: string) {
-            const catalog = (await VillageApi.getCatalogList(querys.value.villageId, pid));
-            const res = [] as CheckBoxListItem[];
-            for (let p of catalog) {
-              res.push({
-                text: (prefix || '') + p.title,
-                value: p.id,
-              });
-              res.push(...(await getCatalogChildren(p.id, (prefix || '') + ' - ')));
-            }
-            return res;
-          }
-          return getCatalogChildren();
-        },
-      } as CheckBoxListProps,
+        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', 
@@ -226,7 +218,7 @@ const { querys } = useLoadQuerys({
   try {
     formModel.value = new VolunteerInfo();
     if (querys.id >= 0)
-      formData = await VillageApi.getVolunteerInfoById(querys.id);
+      formData = await VillageApi.getVolunteerInfoByIdAdmin(querys.id);
   } catch (e) {
     if (!(e instanceof RequestApiError && e.errorMessage.startsWith('请完成')))
       showError(e, undefined, () => backPrev(false));

+ 3 - 3
src/pages/dig/components/CollectModuleList.vue

@@ -63,7 +63,7 @@ async function loadList() {
         try {
           const collectModuleInternalName = getCollectModuleInternalNameById(item.collectModuleId);
           if (!collectModuleInternalName && item.collectModuleId)
-            throw new Error('不存在定义的表单数据');
+            throw new Error('您暂无权限采集该板块');
           const formDefine = collectModuleInternalName ? getVillageInfoForm(collectModuleInternalName, -1) : undefined;
           return {
             ...item,
@@ -87,7 +87,7 @@ async function loadList() {
               } else {
                 alert({
                   title: item.title,
-                  content: '不存在定义的表单数据',
+                  content: '您暂无权限采集该板块',
                 })
               }
             }
@@ -95,7 +95,7 @@ async function loadList() {
         } catch (e) {
           return {
             ...item,
-            desc: '' + e,
+            desc: '' + (e instanceof Error ? e.message : e),
             enable: false,
           }
         }

+ 1 - 1
src/pages/dig/details.vue

@@ -33,7 +33,7 @@
       <Icon icon="edit-filling" color="primary" :size="100" />
       <Height :height="20" />
       <Text :fontSize="34">写随手记</Text>
-      <Text :fontSize="22" color="gray">写随手记,记录下村文化发现和思考,自动分类</Text>
+      <Text :fontSize="22" color="gray">写随手记,记录下村文化发现和思考,自动分类</Text>
       <Text :fontSize="22" color="gray">也可点击下方进入指定的分类采集信息</Text>
     </Touchable>
 

+ 3 - 3
src/pages/dig/index.vue

@@ -6,13 +6,13 @@
       width="100%"
     />
     <FlexCol :padding="30">
-      <SubTitle title="我的村" />
-      <RequireLogin unLoginMessage="登录后查看我认领的村">
+      <SubTitle title="我的村" />
+      <RequireLogin unLoginMessage="登录后查看我认领的村">
         <SimplePageContentLoader
           :loader="villageListLoader"
           :showEmpty="villageListLoader.content.value?.length == 0"
           :emptyView="{
-            text: '你还没有认领的村',
+            text: '你还没有认领的村',
             button: true,
             buttonText: '联系管理员认领',
             buttonClick: () => {},

+ 3 - 2
src/pages/home/store/index.vue

@@ -1,6 +1,6 @@
 <template>
   <FlexCol :gap="20" :padding="20">
-    <SearchBar placeholder="输入关键词搜索" />
+    <SearchBar placeholder="输入关键词搜索" @search="goList(undefined, '知识库 · 全部', $event)" />
     <FlexCol :radius="15" overflow="hidden">
       <ImageSwiper 
         :height="300"
@@ -97,11 +97,12 @@ function goDetail(item: GetContentListItem) {
     id: item.id,
   });
 }
-function goList(mainBodyColumnId: number|undefined, title: string) {
+function goList(mainBodyColumnId: number|undefined, title: string, search?: string) {
   navTo('/pages/article/common/list', {
     modelId: 16,
     mainBodyColumnId,
     title,
+    search,
   });
 }
 </script>

+ 5 - 1
src/pages/user/login.vue

@@ -140,7 +140,11 @@ function loginMobile() {
       type: 'success',  
       content: '登录成功',
     });
-    setTimeout(() => redirectToIndex(), 200);
+    setTimeout(() => {
+      //加载采集板块信息
+      collectStore.loadCollectableModules();
+      redirectToIndex()
+    }, 200);
   }).catch((e) => { 
     closeToast()
     showError(e);