Bladeren bron

修改首页细节问题,增加志愿者删除,修改图片

快乐的梦鱼 1 maand geleden
bovenliggende
commit
f31ba9d96d

+ 2 - 0
src/App.vue

@@ -39,6 +39,8 @@ configTheme(false, (theme, defaultDarkTheme) => {
   theme.colorConfigs.default.primary = '#00b66a';
   theme.colorConfigs.pressed.primary = '#00814b';
   theme.colorConfigs.background.primary = '#dcfff0';
+
+  theme.varOverrides['ImageDefaultImage'] = 'https://mn.wenlvti.net/app_static/minnan/EmptyImage.png';
   return [theme, defaultDarkTheme];
 });
 </script>

+ 6 - 0
src/api/inhert/VillageApi.ts

@@ -239,6 +239,12 @@ export class VillageApi extends AppServerRequestModule<DataModel> {
     console.log('updateVolunteer', data.toServerSide());
     return (this.post('/village/volunteer/save', data.toServerSide(), '更新志愿者')) ;
   }
+  async deleteVolunteer(id: number, villageId: number) {
+    return (this.post('/village/volunteer/del', {
+      id,
+      village_id: villageId,
+    }, '删除志愿者')) ;
+  }
   async getVolunteerInfoByIdAdmin(id: number) {
     return (await this.post('/village/volunteer/info', {
       id,

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

@@ -1,4 +1,4 @@
-import { DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { DataModel, transformArrayDataModel, transformDataModel } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from '../RequestModules';
 import CommonContent from '../CommonContent';
 
@@ -189,6 +189,18 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
     return CommonContent.getCategoryChildList(pid);
   }
 
+  getOverview(
+    villageId: number,
+    villageVolunteerId: number,
+  ) {
+    return (this.post(`/village/overview/getInfo`, {
+      village_id: villageId,
+      village_volunteer_id: villageVolunteerId,
+    }, '获取村社概览'))
+      .then(res => transformDataModel(CommonInfoModel, res.data2))
+      .catch(e => { throw e });
+  }
+
   async getInfo<T extends DataModel>(
     collectModuleId: number|undefined,
     subType: string,

+ 3 - 3
src/common/components/SimplePageContentLoader.vue

@@ -72,9 +72,9 @@ const props = defineProps({
   emptyView: {
     type: Object as PropType<{
       text: string,
-      buttonText: string,
-      button: boolean,
-      buttonClick: () => void,
+      buttonText?: string,
+      button?: boolean,
+      buttonClick?: () => void,
     }>,
     default: null,
   },

+ 1 - 1
src/components/README.md

@@ -6,7 +6,7 @@ NaEasy UI 是一款简单的 UniApp 移动端UI组件库。
 
 ## 版本
 
-当前版本:INDEV 0.0.3-25121601
+当前版本:INDEV 0.0.4-25121701
 
 ## 版权说明
 

+ 4 - 4
src/components/basic/Image.vue

@@ -115,10 +115,10 @@ defineOptions({
 })
 const props = withDefaults(defineProps<ImageProps>(), {
   src: '',
-  failedImage: '',
-  defaultImage: '',
-  showLoading: true,
-  showFailed: true,
+  failedImage: () => propGetThemeVar('ImageFailedImage', ''),
+  defaultImage: () => propGetThemeVar('ImageDefaultImage', ''),
+  showLoading: () => propGetThemeVar('ImageShowLoading', true),
+  showFailed: () => propGetThemeVar('ImageShowFailed', true),
   showGrey: () => propGetThemeVar('ImageShowGrey', false),
   loading: false,
   loadingColor: () => propGetThemeVar('ImageLoadingColor', 'border.default'),

+ 8 - 0
src/components/basic/Text.vue

@@ -76,6 +76,11 @@ export interface TextProps {
    */
   selectable?: boolean,
   /**
+   * 是否允许换行
+   * @default true
+   */
+  wrap?: boolean,
+  /**
    * 行数限制
    */
   lines?: number,
@@ -136,6 +141,7 @@ const props = withDefaults(defineProps<TextProps>(), {
   lineThrough: false,
   shadow: false,
   touchable: false,
+  wrap: true,
 });
 const emit = defineEmits([ 'click' ])
 
@@ -186,6 +192,8 @@ const style = computed(() => {
     for(const k in fontStyle)
       o.fontStyle = k;
   }
+  if (!props.wrap) o.whiteSpace = 'nowrap';
+
   if (fontFamily) o.fontFamily = fontFamily;
   if (props.textAlign) {
     o.textAlign = props.textAlign;

+ 19 - 2
src/components/dialog/Dialog.vue

@@ -6,9 +6,11 @@
     @close="onClose"
   >
     <DialogInner 
-      v-bind="$props"
-      :onConfirm="$props.onConfirm"
+      ref="dialogInner"
+      v-bind="props"
+      :onConfirm="$props.onConfirm""
       :onCancel="$props.onCancel"
+      :confirmCountDownTime="props.confirmCountDown"
       :topSlots="{
         default: Boolean($slots?.default),
         bottomContent: Boolean($slots?.bottomContent),
@@ -42,6 +44,7 @@
 </template>
 
 <script setup lang="ts">
+import { ref, watch } from 'vue';
 import DialogInner from './DialogInner.vue';
 import Popup from './Popup.vue';
 import type { PopupProps } from './Popup.vue';
@@ -106,6 +109,11 @@ export interface DialogProps extends Omit<PopupProps, 'onClose'|'position'|'rend
    */
   confirmColor?: string|undefined;
   /**
+   * 确定按扭文字倒计时,单位秒
+   * @default undefined
+   */
+  confirmCountDown?: number|undefined;
+  /**
    * 取消按扭文字的颜色
    * @default text.content
    */
@@ -144,6 +152,7 @@ export interface DialogProps extends Omit<PopupProps, 'onClose'|'position'|'rend
 }
 export type DialogConfirmProps = Omit<DialogProps, 'show'|'showCancel'|'onClose'>;
 
+const dialogInner = ref();
 const props = withDefaults(defineProps<DialogProps>(), {
   mask: true,
   showConfirm: true,
@@ -151,6 +160,14 @@ const props = withDefaults(defineProps<DialogProps>(), {
 });
 const emit = defineEmits([ 'close', 'update:show' ]);
 
+watch(() => props.show, (newVal) => {
+  if (newVal) {
+    setTimeout(() => {
+      dialogInner.value?.startConfirmCountDown();
+    }, 200);
+  }
+});
+
 function onClose() {
   emit('close');
   emit('update:show', false);

+ 7 - 1
src/components/dialog/DialogButton.vue

@@ -5,7 +5,7 @@
       ...vertical ? {} : themeStyles.dialogButtonHorz.value,
     }"
     :pressedColor="themeContext.resolveThemeColor(pressedColor)"
-    touchable
+    :touchable="touchable"
     direction="row"
     @click="loading ? undefined : $emit('click')"
   >
@@ -46,6 +46,11 @@ export interface DialogButtonProps {
   */
   loading?: boolean,
   /**
+  * 是否可点击
+  * @default true
+  */
+  touchable?: boolean|undefined,
+  /**
   * 按钮文字
   * @default false
   */
@@ -94,6 +99,7 @@ const props = withDefaults(defineProps<DialogButtonProps>(), {
   loading: false,
   vertical: false,
   buttonColor: undefined,
+  touchable: true,
   pressedColor: () => propGetThemeVar('DialogButtonPressedColor', 'pressed.white'),
 });
 

+ 27 - 5
src/components/dialog/DialogInner.vue

@@ -60,8 +60,9 @@
         v-if="showConfirm"
         key="confirm"
         :vertical="bottomVertical"
-        :text="confirmText"
+        :text="confirmCountDownValue > 0 ? `${confirmText} (${confirmCountDownValue})` : confirmText"
         :loading="buttomLoadingState.confirm"
+        :touchable="confirmCountDownValue <= 0"
         :buttonColor="confirmColor"
         @click="onConfirmClick('confirm')"
       />
@@ -70,14 +71,14 @@
 </template>
 
 <script setup lang="ts">
-import FlexView from '../layout/FlexView.vue';
-import FlexCol from '../layout/FlexCol.vue';
+import { onMounted, ref } from 'vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize, DynamicVar } from '../theme/ThemeTools';
-import type { DialogProps } from './Dialog.vue';
+import FlexView from '../layout/FlexView.vue';
+import FlexCol from '../layout/FlexCol.vue';
 import Icon from '../basic/Icon.vue';
-import { ref } from 'vue';
 import DialogButton from './DialogButton.vue';
+import type { DialogProps } from './Dialog.vue';
 
 const themeContext = useTheme();
 const themeStyles = themeContext.useThemeStyles({
@@ -112,6 +113,7 @@ const themeStyles = themeContext.useThemeStyles({
 
 export interface DialogInnerProps extends Omit<DialogProps, 'show'> {
   topSlots?: Record<string, boolean>,
+  confirmCountDownTime?: number,
 }
 
 const emit = defineEmits([ 'close' ]);
@@ -130,6 +132,7 @@ const props = withDefaults(defineProps<DialogInnerProps>(), {
 });
 
 const buttomLoadingState = ref<Record<string, boolean>>({});
+const confirmCountDownValue = ref<number>(0);
 
 function setButtonLoadingStateByName(name: string, state: boolean) {
   buttomLoadingState.value[name] = state;
@@ -141,6 +144,17 @@ function checkAnyButtonLoading() {
   }
   return false;
 }
+function startConfirmCountDown() {
+  if (!props.confirmCountDownTime)
+    return;
+  confirmCountDownValue.value = props.confirmCountDownTime;
+  const interval = setInterval(() => {
+    confirmCountDownValue.value--;
+    if (confirmCountDownValue.value <= 0) {
+      clearInterval(interval);
+    }
+  }, 1000);
+}
   
 function onPopupClose() {
   emit('close');
@@ -182,5 +196,13 @@ function onConfirmClick(name: string) {
   } else onPopupClose();
 }
 
+onMounted(() => {
+  setTimeout(() => {
+    startConfirmCountDown();
+  }, 200);
+});
+defineExpose({
+  startConfirmCountDown,
+})
 
 </script>

+ 3 - 7
src/components/dialog/DialogRoot.vue

@@ -29,12 +29,10 @@ defineExpose<DialogAlertRoot>({
 
     const onConfirm = _options.onConfirm;
     const onCancel = _options.onCancel;
-    const onClose = _options.onClose;
-
+    
     return new Promise<boolean>((resolve) => {
-      _options.onClose = () => {
+      (_options as any).onClose = () => {
         show.value = false;
-        onClose?.();
         resolve(false);
       };
       _options.onCancel = () => {
@@ -57,12 +55,10 @@ defineExpose<DialogAlertRoot>({
     options.value = _options;
 
     const onConfirm = _options.onConfirm;
-    const onClose = _options.onClose;
 
     return new Promise<void>((resolve) => {
-      _options.onClose = () => {
+      (_options as any).onClose = () => {
         show.value = false;
-        onClose?.();
         resolve();
       };
       _options.onConfirm = () => {

+ 306 - 0
src/components/feedback/BubbleBox.vue

@@ -0,0 +1,306 @@
+<template>
+  <view 
+    class="nana-bubble-box"
+    :style="outerStyle"
+    @mouseenter="handleHover(true)"
+    @mouseleave="handleHover(false)"
+  >
+    <view v-if="trigger === 'click'" @click.native.capture="handleClick">
+      <slot />
+    </view>
+    <slot v-else />
+    <SimpleTransition name="bubble-box" :show="showState" :duration="200">
+      <template #show="{ classNames }">
+        <view class="nana-bubble-box-popup-mask" @click="hide" />
+        <FlexView
+          v-if="items?.length"
+          position="absolute"
+          :direction="direction"
+          :backgroundColor="backgroundColor"
+          :radius="radius"
+          :gap="10"
+          :padding="10"
+          :zIndex="1001"
+          :margin="selectObjectByType(position, 'left', {
+            top: [arrowWidth,0],
+            bottom: [arrowWidth,0],
+            left: [0,arrowWidth],
+            right: [0,arrowWidth],
+          })"
+          :innerClass="['nana-bubble-box-popup',position,classNames]"
+          shadow="light"
+          v-bind="innerProps"
+          :innerStyle="innerStyle"
+        >
+          <view 
+            class="nana-bubble-box-arrow" 
+            :style="{ 
+              borderWidth: theme.resolveThemeSize(arrowWidth),
+              borderColor: backgroundColor,
+              borderRightColor: 'transparent',
+              borderBottomColor: 'transparent',
+              borderLeftColor: 'transparent',
+            }"
+          />
+          <Touchable
+            v-for="item in items"
+            :key="item.text"
+            direction="row"
+            align-items="center"
+            :gap="10"
+            :padding="[5, 20]"
+            v-bind="itemProps"
+            @click="handleItemClick(item)"
+          >
+            <Icon
+              :name="item.icon"
+              :size="44"
+              :color="item.textColor || itemTextColor"
+              v-bind="{ ...itemIconProps, ...item.iconProps }"
+            />
+            <Text 
+              :wrap="false" 
+              v-bind="itemTextProps" 
+              :color="item.textColor || itemTextColor"
+              :text="item.text" 
+            />
+          </Touchable>
+        </FlexView>
+      </template>
+    </SimpleTransition>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
+import { selectObjectByType } from '../theme/ThemeTools';
+import type { FlexProps } from '../layout/FlexView.vue';
+import type { TextProps } from '../basic/Text.vue';
+import Icon, { type IconProps } from '../basic/Icon.vue';
+import FlexView from '../layout/FlexView.vue';
+import Text from '../basic/Text.vue';
+import Touchable from './Touchable.vue';
+import SimpleTransition from '../anim/SimpleTransition.vue';
+
+export interface BubbleBoxItem {
+  text: string,
+  textColor?: string,
+  icon?: string,
+  iconProps?: IconProps,
+  onClick: () => void,
+}
+export interface BubbleBoxProps {
+  /**
+   * 气泡框位置
+   * @default top
+   */
+  position?: 'left' | 'right' | 'top' | 'bottom',
+  /**
+   * 触发点击事件模式
+   * @default click
+   */
+  trigger?: 'click'|'hover'|'none',
+  /**
+   * 气泡框按钮排列方向
+   * @default column
+   */
+  direction?: 'column' | 'row',
+  /**
+   * 是否禁用
+   * @default false
+   */
+  disabled?: boolean,
+  /**
+   * 气泡框按钮数组
+   */
+  items?: BubbleBoxItem[],
+  /**
+   * 气泡框按钮文本颜色
+   * @default 'text.content'
+   */
+  itemTextColor?: string,
+  /**
+   * 气泡框按钮文本样式
+   */
+  itemTextProps?: TextProps,
+  /**
+   * 气泡框按钮图标样式
+   */
+  itemIconProps?: IconProps,
+  /**
+   * 气泡框按钮样式
+   */
+  itemProps?: FlexProps,
+  /**
+   * 气泡框外层容器样式
+   */
+  outerStyle?: Record<string, string>,
+  /**
+   * 气泡框背景颜色
+   * @default white
+   */
+  backgroundColor?: string,
+  /**
+   * 气泡框箭头宽度
+   * @default 12
+   */
+  arrowWidth?: number,
+  /**
+   * 气泡框圆角半径
+   * @default 12
+   */
+  radius?: number,
+  /**
+   * 气泡框内层容器样式
+   */
+  innerProps?: FlexProps,
+  /**
+   * 气泡框内层容器样式
+   */
+  innerStyle?: Record<string, string>,
+}
+export interface BubbleBoxExpose {
+  show: () => void,
+  hide: () => void,
+}
+const theme = useTheme();
+const props = withDefaults(defineProps<BubbleBoxProps>(), {
+  position: 'top',
+  trigger: 'click',
+  direction: 'column',
+  arrowWidth: () => propGetThemeVar('BubbleBoxArrowWidth', 12),
+  items: () => [],
+  itemTextColor: () => propGetThemeVar('BubbleBoxItemTextColor', 'text.content'),
+  backgroundColor: () => propGetThemeVar('BubbleBoxBackgroundColor', 'white'),
+  radius: () => propGetThemeVar('BubbleBoxRadius', 12),
+});
+
+const backgroundColor = computed(() => theme.resolveThemeColor(props.backgroundColor));
+const showState = ref(false);
+const lock = ref(false);
+
+function handleItemClick(item: BubbleBoxItem) {
+  hide();
+  item.onClick();
+}
+function handleClick() {
+  if (lock.value) return;
+  if (props.trigger === 'click' && !props.disabled)  {
+     enterLock();
+    showState.value = !showState.value;
+  }
+}
+function handleHover(show: boolean) {
+  if (lock.value) return;
+  if (props.trigger === 'hover' && !props.disabled)
+    showState.value = show;
+}
+
+function enterLock() {
+  lock.value = true;
+  setTimeout(() => {
+    lock.value = false;
+  }, 300);
+}
+function show() { 
+  showState.value = true;
+  enterLock();
+}
+function hide() { 
+  showState.value = false; 
+  enterLock();
+}
+
+defineExpose<BubbleBoxExpose>({
+  show,
+  hide,
+})
+
+defineOptions({
+  options: {
+    virtualHost: true,
+    styleIsolation: "shared",
+  }
+})
+</script>
+
+<style lang="scss">
+.nana-bubble-box {
+  position: relative;
+  overflow: visible;
+
+  .nana-bubble-box-popup {
+    position: absolute;
+    transition: opacity ease-in-out 0.2s;
+
+    &.left {
+      top: 50%;
+      right: 100%;
+      transform: translateY(-50%) translateX(0);
+
+      .nana-bubble-box-arrow {
+        top: 50%;
+        left: 100%;
+        transform: translateY(-50%) rotate(-90deg);
+      }
+    }
+    &.right {
+      top: 50%;
+      left: 100%;
+      transform: translateY(-50%);
+
+      .nana-bubble-box-arrow {
+        top: 50%;
+        left: 0;
+        transform: translateX(-100%) translateY(-50%) rotate(90deg);
+      }
+    }
+    &.top {
+      bottom: -100%;
+      left: 50%;
+      transform: translateX(-50%) translateY(-100%);
+
+      .nana-bubble-box-arrow {
+        top: 100%;
+        left: 50%;
+        transform: translateX(-50%) rotate(0);
+      }
+    }
+    &.bottom {
+      top: 100%;
+      left: 50%;
+      transform: translateX(-50%);
+
+      .nana-bubble-box-arrow {
+        top: 0;
+        left: 50%;
+        transform: translateX(-50%) translateY(-100%) rotate(180deg);
+      }
+    }
+ 
+    &.bubble-box-enter-active,
+    &.bubble-box-leave-active {
+      opacity: 1;
+    }
+    &.bubble-box-enter-from,
+    &.bubble-box-leave-to {
+      opacity: 0;
+    }
+  }
+  .nana-bubble-box-popup-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 1000;
+  }
+  .nana-bubble-box-arrow {
+    position: absolute;
+    width: 0;
+    height: 0;
+    border-style: solid;
+  }
+}
+</style>

+ 26 - 0
src/components/feedback/Touchable.ts

@@ -0,0 +1,26 @@
+import type { FlexProps } from "../layout/FlexView.vue";
+
+export interface TouchableFlexProps extends FlexProps {
+  /**
+   * 是否可以点击
+   * @default true
+   */
+  touchable?: boolean,
+  /**
+   * 按下时的颜色
+   * @default 'background.press'
+   */
+  pressedColor?: string,
+  /**
+   * 按下时的透明度(仅在 pressedColor 未设置时有效)
+   * @default 0.7
+   */
+  activeOpacity?: number,
+  /**
+   * 是否设置鼠标指针为指针
+   * @default true
+   */
+  setCursor?: boolean,
+}
+
+export const TouchableClickEventInceptorKey = Symbol('TouchableClickEventInceptor');

+ 9 - 27
src/components/feedback/Touchable.vue

@@ -14,7 +14,7 @@
     @touchend="handleTouchEnd"
     @click.native.stop="handleClick"
   >
-    <slot></slot>
+    <slot />
   </view>
 </template>
 
@@ -23,34 +23,11 @@
 /**
  * 组件说明:Flex组件,用于一些布局中快速写容器,是一系列盒子的基础组件。
  */
-import { computed, getCurrentInstance, onMounted, ref } from 'vue';
+import { computed, getCurrentInstance, inject, onMounted, ref, type Ref } from 'vue';
 import { useTheme } from '../theme/ThemeDefine';
 import { RandomUtils } from '@imengyu/imengyu-utils';
 import { useBaseViewStyleBuilder } from '../layout/BaseView';
-import type { FlexProps } from '../layout/FlexView.vue';
-
-export interface TouchableFlexProps extends FlexProps {
-  /**
-   * 是否可以点击
-   * @default true
-   */
-  touchable?: boolean,
-  /**
-   * 按下时的颜色
-   * @default 'background.press'
-   */
-  pressedColor?: string,
-  /**
-   * 按下时的透明度(仅在 pressedColor 未设置时有效)
-   * @default 0.7
-   */
-  activeOpacity?: number,
-  /**
-   * 是否设置鼠标指针为指针
-   * @default true
-   */
-  setCursor?: boolean,
-}
+import { TouchableClickEventInceptorKey, type TouchableFlexProps } from './Touchable';
 
 const props = withDefaults(defineProps<TouchableFlexProps>(), {
   activeOpacity: 0.7,
@@ -87,7 +64,8 @@ defineOptions({
   }
 })
 const emit = defineEmits([ "click", "state" ]);
-const isPressed = ref(false)
+const isPressed = ref(false);
+const clickEventInceptor = inject<(Ref<() => void>)|null>(TouchableClickEventInceptorKey, null);
 
 function handleTouchStart() {
   if (props.touchable) {
@@ -104,6 +82,10 @@ function handleMouseLeave() {
     emit('state', 'default')
 }
 function handleClick(e: Event) {
+  if (clickEventInceptor && clickEventInceptor.value) {
+    clickEventInceptor.value();
+    return;
+  }
   if (props.touchable) {
     emit('state', 'default')
     emit('click', e);

+ 4 - 1
src/components/layout/BaseView.ts

@@ -2,6 +2,7 @@ import { computed } from "vue";
 import { useTheme } from "../theme/ThemeDefine";
 import type { FlexProps } from "./FlexView.vue";
 import { configMargin, configPadding } from "../theme/ThemeTools";
+import { ObjectUtils } from "@imengyu/imengyu-utils";
 
 export function useBaseViewStyleBuilder(props: FlexProps) {
   
@@ -30,7 +31,6 @@ export function useBaseViewStyleBuilder(props: FlexProps) {
       borderStyle: props.borderStyle,
       boxShadow: props.shadow ? themeContext.getVar('shadow.' + props.shadow, undefined) : undefined,
       zIndex: props.zIndex,
-      ...(props.innerStyle ? props.innerStyle : {}),
     }
 
     //内边距样式
@@ -38,6 +38,9 @@ export function useBaseViewStyleBuilder(props: FlexProps) {
     //外边距样式
     configMargin(obj, themeContext.theme.value, props.margin as any);
 
+    if (props.innerStyle)
+      ObjectUtils.cloneValuesToObject(props.innerStyle, obj);
+
     if (obj.paddingVertical) {
       if (obj.paddingTop === undefined)
         obj.paddingTop = obj.paddingVertical;

+ 1 - 1
src/components/layout/FlexView.vue

@@ -115,7 +115,7 @@ export interface FlexProps {
    */
   shadow?: string,
   /**
-   * 阴影。使用主题中的阴影预设。
+   * 边框。使用主题中的边框预设。
    */
   border?: string,
   /**

+ 7 - 0
src/pages.json

@@ -88,6 +88,13 @@
       }
     },
     {
+      "path": "pages/dig/forms/submits",
+      "style": {
+        "navigationBarTitleText": "我的投稿",
+        "enablePullDownRefresh": true
+      }
+    },
+    {
       "path": "pages/editor/editor",
       "style": {
         "navigationBarTitleText": "编辑文章",

+ 118 - 60
src/pages/dig/admin.vue

@@ -1,67 +1,99 @@
 <template>
-  <FlexCol :padding="30">
-    <FlexRow justify="space-between">
-      <SearchBar
-        v-model="searchText"
-        placeholder="搜一搜" 
-        :innerStyle="{ width: '460rpx' }"
-        @confirm="search"
-      />
-      <NButton type="primary" @click="newData">+ 新增</NButton>
-    </FlexRow>
-    <FlexCol :gap="20" :margin="[20,0,0,0]">
+  <CommonRoot>
+    <FlexCol :padding="30">
+      <FlexRow justify="space-between">
+        <SearchBar
+          v-model="searchText"
+          placeholder="搜一搜" 
+          :innerStyle="{ width: '460rpx' }"
+          @confirm="search"
+        />
+        <NButton type="primary" @click="newData">+ 新增</NButton>
+      </FlexRow>
+      <FlexCol :gap="20" :margin="[20,0,0,0]">
+
+        <button class="remove-button-style" open-type="share">
+          <Touchable 
+            direction="column"
+            center 
+            :padding="20"
+            :height="300"
+            :innerStyle="{
+              backgroundImage: `url('https://mn.wenlvti.net/app_static/xiangyuan/images/share-btn.jpg')`,
+              backgroundSize: 'cover',
+            }"
+          >
+            <FlexCol position="relative">
+              <Icon icon="smile-filling" color="primary" :size="156" />
+              <Icon icon="share" color="warning" :size="56" :innerStyle="{ position: 'absolute', bottom: 0, right: '-40rpx' }" />
+            </FlexCol>
+            <Height :height="20" />
+            <Text :fontSize="26" color="primary" text="分享给志愿者注册,加入志愿者队伍" />
+          </Touchable>
+        </button>
 
-      <button class="remove-button-style" open-type="share">
-        <Touchable 
-          direction="column"
-          center 
+        <FlexRow 
+          v-for="item in listLoader.list.value"
+          :key="item.id" 
+          backgroundColor="white" 
+          radius="20"
+          justify="space-between"
+          align="center"
           :padding="20"
-          :height="300"
-          :innerStyle="{
-            backgroundImage: `url('https://mn.wenlvti.net/app_static/xiangyuan/images/share-btn.jpg')`,
-            backgroundSize: 'cover',
-          }"
-          @click=""
+          :gap="20"
         >
-          <FlexCol position="relative">
-            <Icon icon="smile-filling" color="primary" :size="156" />
-            <Icon icon="share" color="warning" :size="56" :innerStyle="{ position: 'absolute', bottom: 0, right: '-40rpx' }" />
-          </FlexCol>
-          <Height :height="20" />
-          <Text :fontSize="26" color="primary" text="分享给志愿者注册,加入志愿者队伍" />
-        </Touchable>
-      </button>
-
-      <FlexRow 
-        v-for="item in listLoader.list.value"
-        :key="item.id" 
-        backgroundColor="white" 
-        radius="20"
-        justify="space-between"
-        :padding="20"
-        :gap="20"
-      >
-        <Image :src="item.image" width="150rpx" height="150rpx" round radius="30" />
-        <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 align="center" :flexShrink="0">
-          <NButton @click="goDetail(item.id, true)">改密码</NButton>
-          <NButton type="primary" @click="goDetail(item.id)">编辑</NButton>
+          <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>
-      </FlexRow>
+      </FlexCol>
+      <SimplePageListLoader :loader="listLoader" :noEmpty="true">
+        <template #empty>
+          <Empty image="search" text="暂无数据,点击按钮新增数据">
+            <NButton type="primary" @click="newData">+ 新增数据</NButton>
+          </Empty>
+        </template>
+      </SimplePageListLoader>
     </FlexCol>
-    <SimplePageListLoader :loader="listLoader" :noEmpty="true">
-      <template #empty>
-        <Empty image="search" text="暂无数据,点击按钮新增数据">
-          <Button type="primary" @click="newData">+ 新增数据</Button>
-        </Empty>
-      </template>
-    </SimplePageListLoader>
-  </FlexCol>
+  </CommonRoot>
 </template>
 
 <script setup lang="ts">
@@ -69,7 +101,9 @@ import { ref } from 'vue';
 import { navTo } from '@/components/utils/PageAction';
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
 import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
-import VillageApi from '@/api/inhert/VillageApi';
+import { onShareAppMessage } from '@dcloudio/uni-app';
+import { alert, confirm, toast } from '@/components/dialog/CommonRoot';
+import VillageApi, { VolunteerInfo } from '@/api/inhert/VillageApi';
 import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
 import NButton from '@/components/basic/Button.vue';
 import Icon from '@/components/basic/Icon.vue';
@@ -81,13 +115,16 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Touchable from '@/components/feedback/Touchable.vue';
 import Height from '@/components/layout/space/Height.vue';
-import { onShareAppMessage } from '@dcloudio/uni-app';
+import BubbleBox from '@/components/feedback/BubbleBox.vue';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import UserHead from '@/static/images/user/avatar.png';
 
 const { querys } = useLoadQuerys({ 
   id: 0,  
   name: '',
   points: 0,
   level: 0,
+  villageId: 0,
   villageVolunteerId: 0,
 }, () => {
   listLoader.loadData(undefined, true);
@@ -131,6 +168,27 @@ function goDetail(id: number, onlyPassword: boolean = false) {
     onlyPassword,
   });
 }
+function doDeleteUser(item: VolunteerInfo) {
+  confirm({
+    title: '确认要删除用户?',
+    content: `请注意:删除${item.name}后将无法继续登录,请确认此账号不再继续使用。`,
+    confirmColor: 'danger',
+    confirmCountDown: 10,
+  }).then((res) => {
+    if (res) {
+      VillageApi.deleteVolunteer(item.id, querys.value.villageId).then(() => {
+        listLoader.loadData(undefined, true);
+        toast({ content: '删除成功' })
+      }).catch((e) => {
+        alert({
+          title: '删除失败',
+          content: '请联系管理员处理。' + e,
+          icon: 'delete-filling',
+        })
+      });
+    }
+  });
+}
 function search() {
   listLoader.loadData(undefined, true);
 }

+ 0 - 2
src/pages/dig/forms/list.vue

@@ -25,8 +25,6 @@
       >
         <Image 
           :src="item.image"
-          defaultImage="https://mn.wenlvti.net/app_static/minnan/EmptyImage.png"
-          failedImage="https://mn.wenlvti.net/app_static/minnan/EmptyImage.png"
           :showFailed="false"
           :width="100"
           :height="100"

+ 138 - 0
src/pages/dig/forms/submits.vue

@@ -0,0 +1,138 @@
+<template>
+  <FlexCol :padding="30">
+    <SearchBar
+      v-model="searchText"
+      placeholder="搜一搜" 
+      @confirm="search"
+    />
+    <Height :height="20" />
+    <SimplePageListLoader :loader="listLoader" :noEmpty="true">
+      <template #empty>
+        <Empty image="search" description="这里还没提交过投稿,快来去写吧!">
+        </Empty>
+      </template>
+      <FlexCol :gap="20">
+        <Touchable 
+          v-for="item in listLoader.list.value"
+          :key="item.id" 
+          :gap="20"
+          :padding="[15,20]"
+          :radius="15"
+          align="center"
+          backgroundColor="white"
+          direction="row"
+          touchable
+          @click="goDetail(item.id)"
+        >
+          <Image 
+            :src="item.image"
+            :showFailed="false"
+            :width="100"
+            :height="100"
+            :radius="10"
+            mode="aspectFill"
+            round
+          />
+          <FlexCol>
+            <H4 :size="36">{{ item.title }}</H4>
+            <Text :size="23">{{ item.desc }}</Text>
+          </FlexCol>
+        </Touchable>
+      </FlexCol>
+    </SimplePageListLoader>
+    <XBarSpace />
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { onPullDownRefresh } from '@dcloudio/uni-app';
+import { DataDateUtils } from '@imengyu/js-request-transform';
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { useCollectStore } from '@/store/collect';
+import { useAuthStore } from '@/store/auth';
+import { navTo } from '@/components/utils/PageAction';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import VillageInfoApi from '@/api/inhert/VillageInfoApi';
+import Image from '@/components/basic/Image.vue';
+import Empty from '@/components/feedback/Empty.vue';
+import SearchBar from '@/components/form/SearchBar.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Text from '@/components/basic/Text.vue';
+import Height from '@/components/layout/space/Height.vue';
+import H4 from '@/components/typography/H4.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import XBarSpace from '@/components/layout/space/XBarSpace.vue';
+
+const searchText = ref('');
+const authStore = useAuthStore();
+
+const listLoader = useSimplePageListLoader<{
+  id: number,
+  image: string,
+  title: string,
+  desc: string
+}, {
+  villageId: number,  
+  villageVolunteerId: number,
+}>(8, async (page, pageSize, params) => {
+  if (!params )
+    throw new Error("未传入参数,当前页面需要参数");
+  let res = await VillageInfoApi.getList(
+    undefined,
+    '',
+    undefined,
+    undefined,
+    params.villageId,
+    params.villageVolunteerId,
+    undefined,
+    page,
+    pageSize,
+  )
+  if (searchText.value)
+    res = res.filter((p) => p.title.includes(searchText.value));
+  const list = res.map((item) => {
+    return {
+      id: item.id,
+      image: item.image,
+      title: item.title,
+      desc: DataDateUtils.formatDate(item.updatedAt, 'YYYY-MM-dd') + (
+        authStore.isAdmin ? (' 投稿人:' + item.villageVolunteerName) : ''
+      )
+    }
+  })
+  return {
+    list: list,
+    total: list.length,
+  };
+});
+function goDetail(id: number) {
+  navTo('common', { 
+    id,
+    villageId: querys.value.villageId,
+    villageVolunteerId: querys.value.villageVolunteerId,
+  });
+}
+function search() {
+  listLoader.loadData(undefined, true);
+}
+
+const { querys } = useLoadQuerys({ 
+  villageId: 0,  
+  villageVolunteerId: 0,
+}, async (querys) => {
+  listLoader.loadData(querys)
+});
+
+onPullDownRefresh(() => {
+  listLoader.loadData(undefined, true);
+});
+
+defineExpose({
+  onPageBack(name: string, param: any) {
+    if (param && param.needRefresh)
+      listLoader.loadData(undefined, true);
+  }
+})
+</script>

+ 40 - 8
src/pages/dig/index.vue

@@ -2,7 +2,7 @@
   <FlexCol>
     <Image 
       mode="aspectFill" 
-      src="https://mn.wenlvti.net/app_static/xiangan/banner_submit.jpg"
+      src="https://mn.wenlvti.net/app_static/xiangyuan/images/index-banner.jpg"
       width="100%"
     />
     <FlexCol :padding="30">
@@ -12,10 +12,8 @@
           :loader="villageListLoader"
           :showEmpty="villageListLoader.content.value?.length == 0"
           :emptyView="{
-            text: '你还没有认领的村社',
-            button: true,
-            buttonText: '联系管理员认领',
-            buttonClick: () => {},
+            text: '你还没有认领的村社,请联系管理员认领',
+            button: false,
           }"
         >
           <FlexCol :gap="10">
@@ -38,9 +36,34 @@
                 <H3>{{ item.villageName }}</H3>
               </FlexRow>
               <ButtonGroup direction="row" align="center" :gap="0">
-                <Button v-if="authStore.isAdmin" icon="edit-filling" size="small" @click="goManagePage(item)">管理</Button>
-                <Button v-if="authStore.isAdmin" size="small" icon="browse" @click="goPreviewDigPage(item)">预览</Button>
-                <Button type="primary" size="small" icon="edit-filling" @click="goSubmitDigPage(item)">采编</Button>
+                <BubbleBox :items="[
+                  {
+                    icon: 'edit-filling',
+                    text: '管理',
+                    onClick: () => goManagePage(item),
+                  },
+                  {
+                    icon: 'browse',
+                    text: '预览',
+                    onClick: () => goPreviewDigPage(item),
+                  },
+                ]">
+                  <Button v-if="authStore.isAdmin" icon="edit-filling" size="small">管理</Button>
+                </BubbleBox>
+                <BubbleBox :items="[
+                  {
+                    icon: 'edit-filling',
+                    text: '采编',
+                    onClick: () => goSubmitDigPage(item),
+                  },
+                  {
+                    icon: 'browse',
+                    text: '我的投稿',
+                    onClick: () => goMyDigPage(item),
+                  },
+                ]">
+                  <Button type="primary" size="small" icon="edit-filling">采编</Button>
+                </BubbleBox>
               </ButtonGroup>
             </FlexRow>
           </FlexCol>
@@ -82,6 +105,7 @@ import H3 from '@/components/typography/H3.vue';
 import Text from '@/components/basic/Text.vue';
 import Height from '@/components/layout/space/Height.vue';
 import ButtonGroup from '@/components/basic/ButtonGroup.vue';
+import BubbleBox from '@/components/feedback/BubbleBox.vue';
 
 const authStore = useAuthStore();
 const collectStore = useCollectStore();
@@ -102,11 +126,19 @@ function goManagePage(item: VillageListItem) {
   navTo('./dig/admin', { 
     id: item.villageId,
     name: item.villageName,
+    villageId: item.villageId,
     villageVolunteerId: item.villageVolunteerId,
     points: volunteerInfoLoader.content.value?.points,
     level: volunteerInfoLoader.content.value?.level,
   })
 }
+function goMyDigPage(item: VillageListItem) {
+  navTo('./dig/forms/submits', { 
+    villageName: item.villageName,
+    villageId: item.villageId,
+    villageVolunteerId: item.villageVolunteerId,
+  })
+}
 function goPreviewDigPage(item: VillageListItem) {
   navTo('./dig/admin/preview', { 
     villageName: item.villageName,

+ 6 - 1
src/pages/user/index.vue

@@ -10,6 +10,8 @@
     >
       <Image 
         :src="userInfo?.avatar || UserHead"
+        :defaultImage="UserHead"
+        :failedImage="UserHead"
         mode="aspectFill" 
         class="avatar" 
         width="100rpx"
@@ -23,9 +25,12 @@
     </Touchable>
     <Height :height="50" />
     <CellGroup round>
-      <Cell icon="/static/images/user/icon-edit.png" title="我的投稿" showArrow touchable @click="$emit('goSubmit')" />
+      <Cell icon="/static/images/user/icon-edit.png" title="我的投稿" showArrow touchable @click="navTo('/pages/dig/forms/submits')" />
       <Cell icon="/static/images/user/icon-profile.png" title="编辑资料" showArrow touchable @click="goUserProfile" />
       <Cell icon="/static/images/user/icon-function.png" title="关于我们" showArrow touchable @click="navTo('/pages/home/about/about')" />
+      <button open-type="contact" class="remove-button-style">
+        <Cell icon="/static/images/user/icon-service.png" title="联系客服" showArrow touchable />
+      </button>
       <Cell icon="/static/images/user/icon-chat.png" title="商务合作" showArrow touchable @click="navTo('/pages/home/about/contract')" />
       <Cell v-if="userInfo" icon="/static/images/user/icon-quit.png" title="退出登录" showArrow touchable @click="doLogout" />
     </CellGroup>

BIN
src/static/images/user/icon-service.png