快乐的梦鱼 месяцев назад: 2
Родитель
Сommit
1fb108f3e4

+ 4 - 5
src/common/components/RequireLogin.vue

@@ -1,15 +1,14 @@
 <template>
   <slot v-if="isLogged" />
-  <view v-else class="d-flex flex-column align-center justify-center height-300">
-    <view class="mb-3">
-      <text>{{unLoginMessage}}</text>
-    </view>
+  <FlexCol v-else center :height="300" :gap="30">
+    <text>{{unLoginMessage}}</text>
     <Button :innerStyle="{width: '50%'}" type="primary" @click="goLogin">去登录</Button>
-  </view>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
 import Button from '@/components/basic/Button.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
 import { navTo } from '@/components/utils/PageAction';
 import { useAuthStore } from '@/store/auth';
 import { computed } from 'vue';

+ 17 - 6
src/common/components/SimplePageContentLoader.vue

@@ -1,13 +1,13 @@
 <template>
   <view
     v-if="loader?.loadStatus.value == 'loading'"
-    style="min-height: 200rpx;display: flex;justify-content: center;align-items: center;"
+    class="loader-view center"
   >
     <LoadingPage loadingText="加载中" textSize="18" />
   </view>
   <view
     v-else-if="loader?.loadStatus.value == 'error'"
-    style="min-height: 200rpx"
+    class="loader-view"
   >
     <Empty
       image="error"
@@ -21,7 +21,7 @@
   </template>
   <view
     v-if="showEmpty || loader?.loadStatus.value == 'nomore'"
-    style="min-height: 200rpx"
+    class="loader-view"
   >
     <Empty
       image="search"
@@ -40,7 +40,7 @@
     :lazy-load="true"
     @load="handleLoad"
     @error="handleLoad"
-    src="https://mn.wenlvti.net/uploads/20250313/46adb2f039c6f23a3e69149526eb7e61.png"
+    src="https://mn.wenlvti.net/app_static/empty.jpg"
     style="width:0px;height:0px"
   />
 </template>
@@ -50,7 +50,6 @@ import { onMounted, ref, type PropType } from 'vue';
 import type { ISimplePageContentLoader } from '../composeabe/SimplePageContentLoader';
 import Empty from '@/components/feedback/Empty.vue';
 import Button from '@/components/basic/Button.vue';
-import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 
 const props = defineProps({	
@@ -98,4 +97,16 @@ function handleLoad() {
   loaded.value = true;
   props.loader.loadData(undefined);
 }
-</script>
+</script>
+
+<style lang="scss">
+.loader-view {
+  min-height: 200rpx;
+
+  &.center {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+</style>

+ 74 - 47
src/common/components/parts/Box2LineLargeImageUserShadow.vue

@@ -1,72 +1,99 @@
 <template>
-  <view 
-    :class="[
-      'position-relative grid4-item',
-      'd-flex flex-col shadow-l radius-l bg-base p-2 mb-2 overflow-hidden',
-      'border-all-light-light-primary',
+  <Touchable 
+    :padding="20"
+    :flexShrink="fixSize ? 0 : undefined"
+    :flexGrow="fixSize ? undefined : 1"
+    :gap="10"
+    :radius="20"
+    overflow="hidden"
+    position="relative"
+    :innerClass="[
+      'grid4-item',
       classNames,
-      fixSize ? 'flex-shrink-0' : ' flex-grow-1',
     ]"
-    
-    :style="{ 
+    :innerStyle="{ 
       height: 'calc(100% - 20rpx)',
       width: fixSize ? undefined : 'calc(100% - 10rpx)',
     }"
+    direction="column"
     @click="$emit('click')"
   >
-    <image 
+    <Image 
       v-if="image" 
-      class="w-100 height-300 radius-base" 
+      :width="100" 
+      :height="300" 
+      :radius="20"
       :src="image" 
       mode="aspectFill" 
     />
-    <image 
+    <Image 
       v-if="videoMark" 
-      class="width-60 mr-2 video-mark" 
-      :src="PlayVideo" mode="widthFix" 
+      :width="60" 
+      :src="PlayVideo" 
+      class="video-mark"
+      mode="widthFix" 
     />
-    <view v-if="userName" class="d-flex flex-row align-center mt-2">
-      <image class="width-60 mr-2" :src="userHead" mode="widthFix" />
-      <text class="size-s">{{ userName }}</text>
-    </view>
-    <text 
-      :class="[
-        `color-${titleColor}`,
-        title1 || desc ? 'text-lines-1' : 'text-lines-2',
-        'mt-2',
-      ]"
+    <FlexRow v-if="userName" align="center" :gap="20">
+      <Image width="60" :src="userHead" mode="widthFix" />
+      <Text fontConfig="subText">{{ userName }}</Text>
+    </FlexRow>
+    <Text 
+      :color="titleColor"
+      :fontConfig="title1 || desc ? 'h4' : 'subText'"
+      :lines="title1 || desc ? 1 : 2"
     >
       {{ title }}
-    </text>
-    <text v-if="badge" class="position-absolute color-primary-text size-s bg-light-primary radius-base p-1 radius-s text-lines-1 r-0 t-0 mr-3 mt-3">{{ badge }}</text>
-    <text v-if="desc" class="color-second text-lines-2 mt-2">{{ desc }}</text>
-    <view v-if="likes !== undefined && comment !== undefined" class="d-flex flex-row mt-2">
-      <image class="width-40 mr-2" :src="IconHeart" mode="widthFix" />
-      <text class="size-s mr-3">{{ likes }}</text>
-      <image class="width-40 mr-2" :src="IconChat" mode="widthFix" />
-      <text class="size-s">{{ comment }}</text>
-    </view>
+    </Text>
+    <FlexRow
+      position="absolute"
+      :top="0"
+      :right="0"
+      :margin="15"
+      :radius="20" 
+      :padding="15"
+      backgroundColor="background.primary" 
+    >
+      <Text 
+        v-if="badge" 
+        color="primary" 
+        fontConfig="subText"
+        :lines="1"
+        :text="badge" 
+      />
+    </FlexRow>
+    <Text v-if="desc" color="second" :lines="2">{{ desc }}</Text>
+    <FlexRow v-if="likes !== undefined && comment !== undefined" :gap="20">
+      <Image width="40" :src="IconHeart" mode="widthFix" />
+      <Text fontConfig="subText">{{ likes }}</Text>
+      <Image width="40" :src="IconChat" mode="widthFix" />
+      <Text fontConfig="subText">{{ comment }}</Text>
+    </FlexRow>
     <RoundTags v-if="tags" :tags="tags" small />
-    <view v-if="bottomTime" class="d-flex flex-row mt-2">
-      <image class="width-40 mr-2" :src="IconTime" mode="widthFix" />
-      <text class="size-s mr-3">{{ bottomTime }}</text>
-    </view>
-    <view v-if="bottomLocate" class="d-flex flex-row justify-between mt-2">
-      <view class="d-flex flex-row align-center">
-        <image class="width-40 mr-2" :src="IconLocation" mode="widthFix" />
-        <text class="size-s">{{ bottomLocate }}</text>
-      </view>
-      <view class="d-flex flex-row align-center">
-        <image class="width-40 mr-2" :src="IconStar" mode="widthFix" />
-        <text class="size-s">{{ bottomScore }}</text>
-      </view>
-    </view> 
-  </view>
+    <FlexRow v-if="bottomTime" :gap="20">
+      <Image width="40" :src="IconTime" mode="widthFix" />
+      <Text fontConfig="subText">{{ bottomTime }}</Text>
+    </FlexRow>
+    <FlexRow v-if="bottomLocate" justify="space-between" :gap="20">
+      <FlexRow align="center" :gap="20">
+        <Image width="40" :src="IconLocation" mode="widthFix" />
+        <Text fontConfig="subText">{{ bottomLocate }}</Text>
+      </FlexRow>
+      <FlexRow align="center" :gap="20">
+        <Image width="40" :src="IconStar" mode="widthFix" />
+        <Text fontConfig="subText">{{ bottomScore }}</Text>
+      </FlexRow>
+    </FlexRow> 
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import type { PropType } from 'vue';
 import RoundTags from './RoundTags.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const IconHeart = 'https://mncdn.wenlvti.net/app_static/minnan/images/discover/IconHeart.png';
 const IconChat = 'https://mncdn.wenlvti.net/app_static/minnan/images/discover/IconChat.png';

+ 3 - 2
src/common/components/parts/ImageGrid.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="w-100 d-flex flex-row flex-wrap" :style="{ gap: `${gap}rpx` }">
+  <FlexRow width="100%" wrap :gap="gap">
     <image 
       v-for="(v, k) in images"
       :key="k"
@@ -12,10 +12,11 @@
       mode="aspectFill"
       @click="itemClick(v, k)"
     />
-  </view>
+  </FlexRow>
 </template>
 
 <script setup lang="ts">
+import FlexRow from '@/components/layout/FlexRow.vue';
 import type { PropType } from 'vue';
 
 const props = defineProps({	

+ 18 - 18
src/common/components/parts/RoundTags.vue

@@ -1,29 +1,29 @@
 <template>
-  <view 
-    class="d-flex flex-row flex-wrap mt-2"
-  >
-    <view 
+  <FlexRow wrap :gap="10">
+    <template
       v-for="(tag, k) in tags"
       :key="k" 
-      class="bg-place mr-2 mb-2"
-      :style="{
-        maxWidth: '160rpx',
-        overflow: 'hidden',
-        textOverflow: 'ellipsis',
-        whiteSpace: 'nowrap',
-      }"
-      :class="[
-        tag ? '' : 'd-none',
-        small ? 'radius-l p-2 pt-0 pb-1' : 'radius-ll p-25 pt-1',
-      ]"
     >
-      <text class="color-text-content-second size-ss">{{ tag }}</text>
-    </view>
-  </view>
+      <Tag 
+        v-if="tag"
+        :innerStyle="{
+          maxWidth: '160rpx',
+          overflow: 'hidden',
+          textOverflow: 'ellipsis',
+          whiteSpace: 'nowrap',
+        }"
+        :size="small ? 'small' : 'medium'"
+        :text="tag"
+        scheme="light"
+      />
+    </template>
+  </FlexRow>
 </template>
 
 <script setup lang="ts">
 import { computed, type PropType } from 'vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Tag from '@/components/display/Tag.vue';
 
 const props = defineProps({
   tags: {

+ 2 - 2
src/components/form/Uploader.vue

@@ -393,13 +393,13 @@ function onItemDeletePress(item: UploaderItem) {
     });
 }
 //更新列表条目
-function updateListItem(item: UploaderItem) {
+function updateListItem(item: UploaderItem) {/* 
   currentUpladList.value = ((prev) => {
     const newList = prev.concat();
     const index = prev.findIndex((k) => k.filePath === item.filePath);
     index >= 0 ? newList[index] = { ...item } : newList.push(item);
     return newList;
-  })(currentUpladList.value);
+  })(currentUpladList.value); */
   emit('updateList', currentUpladList.value);
 }
 //删除列表条目

+ 11 - 2
src/components/form/UploaderField.vue

@@ -49,13 +49,22 @@ const {
   //() => { /*uploaderRef.value?.pick()*/ },
 );
 
+let nextNoChangeList = false;
+
 watch(() => props.modelValue, (newVal) => {
-  if (props.autoUpdateUploadList)
+  if (props.autoUpdateUploadList) {
+    if (nextNoChangeList) {
+      nextNoChangeList = false;
+      return;
+    }
     setUploaderList();
+  }
 })
 
 function handleListChange(list: UploaderItem[]) {
-  updateValue(list.map((item) => item.uploadedPath).filter((item) => item) as string[]);
+  nextNoChangeList = true;
+  const res = list.map((item) => item.uploadedPath).filter((item) => item) as string[];
+  updateValue(props.single ? res[0] : res);
 }
 function setUploaderList() {
   uploaderRef.value?.setList(props.single || typeof value.value === 'string' 

+ 8 - 5
src/components/form/UploaderListItem.vue

@@ -80,7 +80,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, watch } from 'vue';
 import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize, DynamicSize2, selectStyleType } from '../theme/ThemeTools';
 import { StringUtils } from '@imengyu/imengyu-utils';
@@ -200,10 +200,13 @@ const iconSize = computed(() => themeContext.resolveThemeSize('UploaderListItemI
 const loadingSize = computed(() => themeContext.resolveThemeSize('UploaderListItemLoadingSize', 35));
 
 const imageExts = [ 'jpg', 'jpeg', 'png', 'webp', 'gif' ]
-const isImage = computed(() => props.item.isImage 
-  || (props.item.previewPath && imageExts.includes(StringUtils.path.getFileExt(props.item.previewPath)))
-  || imageExts.includes(StringUtils.path.getFileExt(props.item.filePath || ''))
-);
+const isImage = computed(() => {
+  return props.item.isImage 
+    || (props.item.previewPath && imageExts.includes(StringUtils.path.getFileExt(props.item.previewPath)))
+    || (props.item.filePath && imageExts.includes(StringUtils.path.getFileExt(props.item.filePath || ''))
+  )
+});
+
 const fileIcon = computed(() => {
   const ext = StringUtils.path.getFileExt(props.item.filePath || '');
   switch (ext) {

+ 49 - 63
src/pages/article/common/CommonListPage.vue

@@ -1,36 +1,30 @@
 <template>
   <!-- 通用列表页 -->
-  <view 
-    :class="[
-      'common-list-page d-flex flex-column', 
-      hasBg ? 'bg-base p-3' : ''
-    ]"
+  <FlexCol 
+    :padding="30"
+    :backgroundColor="hasBg ? 'background.page' : ''"
   >
-    <view v-if="tabs" class="top-tab bg-base">
-      <Tabs
-        :tabs="tabs" 
-        :width="700"
-        v-model:currentIndex="tabCurrentIndex"
-        :autoScroll="false"
-        @click="handleTabClick"
-      />
-    </view>
+    <Tabs
+      v-if="tabs"
+      :tabs="tabs" 
+      :width="700"
+      v-model:currentIndex="tabCurrentIndex"
+      :autoScroll="false"
+      @click="handleTabClick"
+    />
     <!-- 搜索 -->
-    <view v-if="showSearch" class="d-flex flex-col">
-      <SearchBar
-        v-model="searchValue"
-        :placeholder="`输入关键词搜索${title}`" 
-        @search="doSearch"
-        @cancel="doSearch"
-      />
-    </view>
+    <SearchBar
+      v-if="showSearch"
+      v-model="searchValue"
+      :placeholder="`输入关键词搜索${title}`" 
+      @search="doSearch"
+      @cancel="doSearch"
+    />
     <!-- 下拉框 -->
-    <view 
+    <FlexRow
       v-if="dropDownNames.length > 0" 
-      class="d-flex flex-row justify-between align-center mt-2"
-      :class="[
-        dropDownVisibleCount >= 3 ? 'justify-around' : ('justify-between')
-      ]"
+      :justify="dropDownVisibleCount >= 3 ? 'space-around' : 'space-between'"
+      align="center"
     >
       <template v-for="(drop, k) in dropDownNames" :key="k" >
         <SimpleDropDownPicker 
@@ -41,34 +35,29 @@
           @update:modelValue="(v) => handleChangeDropDownValue(k, v)"
         />
       </template>
-      <view 
-        v-if="(showTotal && dropDownVisibleCount < 3)" 
-        class="d-flex flex-row align-center mt-3 size-s color-primary text-bold"
-      >
-        <text>总共有 {{ listLoader.total }} 个</text>
-      </view>
-    </view>
-    <view 
-      v-if="(dropDownVisibleCount >= 3 || dropDownVisibleCount == 0)" 
-      class="d-flex flex-row justify-center align-center mt-3 size-s color-primary text-bold"
-    >
-      <text>总共有 {{ listLoader.total }} 个</text>
-    </view>
+      <FlexRow v-if="(showTotal && dropDownVisibleCount < 3)" center>
+        <Text bold color="primary" :text="`总共有 ${ listLoader.total.value } 个`" />
+      </FlexRow>
+    </FlexRow>
+    <FlexRow v-if="(dropDownVisibleCount >= 3 || dropDownVisibleCount == 0)" center :padding="20">
+      <Text bold color="primary" :text="`总共有 ${ listLoader.total.value } 个`" />
+    </FlexRow>
     
     <!-- 列表 -->
-    <view class="position-relative d-flex flex-row flex-wrap justify-between align-stretch mt-3">
-      <view
+    <FlexRow 
+      position="relative"
+      wrap justify="space-between" align="stretch"
+    >
+      <FlexCol
         v-for="(item, i) in listLoader.list.value"
         :key="item.id"
-        :class="[
-          'position-relative d-flex flex-grow-1',
-          itemType.endsWith('-2') ? 'width-1-2' : 'w-100'
-        ]"
+        :flexGrow="1"
+        :width="itemType.endsWith('-2') ? '50%' : '100%'"
       >
         <Box2LineLargeImageUserShadow 
           v-if="itemType.startsWith('image-large')"
-          class="w-100"
-          titleColor="title-text"
+          :width="100"
+          titleColor="black"
           :classNames="getItemClass(i)"
           :image="getImage(item)"
           :titleBox="item.titleBox"
@@ -80,8 +69,8 @@
         />
         <Box2LineImageRightShadow 
           v-else-if="itemType.startsWith('article-common')"
-          class="w-100"
-          titleColor="title-text"
+          :width="100"
+          titleColor="black"
           :titleBox="item.titleBox"
           :classNames="getItemClass(i)"
           :image="getImage(item)"
@@ -94,10 +83,10 @@
         />
         <Box2LineImageRightShadow 
           v-else-if="itemType.startsWith('article-character')"
-          class="w-100"
+          :width="100"
           :classNames="getItemClass(i)"
           :image="getImage(item)"
-          titleColor="title-text"
+          titleColor="black"
           :title="item.title"
           :titleBox="item.titleBox"
           :tags="item.bottomTags || item.keywords"
@@ -106,11 +95,11 @@
           @click="goDetails(item, item.id)"
         />
 
-      </view>
-      <view v-if="itemType.endsWith('-2') && listLoader.list.value.length % 2 != 0" class="width-1-2" />
-    </view>
+      </FlexCol>
+      <view v-if="itemType.endsWith('-2') && listLoader.list.value.length % 2 != 0" style="width:50%;" />
+    </FlexRow>
     <SimplePageListLoader :loader="listLoader" />
-  </view>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
@@ -124,6 +113,9 @@ import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/co
 import AppCofig from '@/common/config/AppCofig';
 import Tabs from '@/components/nav/Tabs.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Text from '@/components/basic/Text.vue';
 
 function getImage(item: any) {
   return item.thumbnail || item.image || AppCofig.defaultImage
@@ -356,10 +348,4 @@ onMounted(() => {
   if (props.loadMounted)
     listLoader.loadData(undefined, true);
 });
-</script>
-
-<style lang="scss">
-.common-list-page {
-  min-height: 100vh; 
-}
-</style>
+</script>

+ 0 - 214
src/pages/article/common/DetailTabPage.vue

@@ -1,214 +0,0 @@
-<template>
-  <!-- TAB分页的详情页 -->
-  <view class="d-flex flex-col bg-base">
-    <SimplePageContentLoader :loader="loader">
-      <template v-if="loader.content.value">
-        <view class="d-flex flex-col">
-
-          <!-- 轮播大图 -->
-          <ImageSwiper 
-            v-if="showHead" 
-            :images="loader.content.value.images"
-          />
-
-          <!-- 标题区域 -->
-          <view class="d-flex flex-col mt-3 p-3">
-            <slot name="title" :content="loader.content.value">
-              <view class="d-flex flex-col">
-                <view class="d-flex flex-row align-center">
-                  <text :class="'size-lll font-songti font-bold color-text-content flex-shrink-1 mr-2' + (loader.content.value.titleBox ? ' border-all-text' : '')">
-                    {{ loader.content.value.title }}
-                  </text>
-                  <slot name="titleEnd" :content="loader.content.value" />
-                </view>
-                <text class="size-base color-text-content-second mt-2">{{ loader.content.value.desc }}</text>
-                <text v-if="loader.content.value.from" class="size-s color-text-content-second">来源:{{ loader.content.value.from }}</text>
-              </view>
-            </slot>
-            <slot name="titleExtra" :content="loader.content.value" />
-          </view>
-
-          <!-- 内容切换标签 -->
-          <view class="ml-2 mr-2">
-            <Tabs
-              :tabs="tabs" 
-              v-model:currentIndex="tabCurrentIndex"
-              :autoScroll="true"
-              :autoItemWidth="false"
-              :defaultIndicatorWidth="130"
-              class="top-tab"
-            />
-          </view>
-
-          <view class="d-flex flex-col radius-l bg-light p-25 mt-3" style="min-height:70vh">
-            <!-- 简介 -->
-            <template v-if="tabCurrentId == 0">
-              <Parse
-                v-if="loader.content.value.intro"
-                :content="loader.content.value.intro"
-                :tagStyle="commonParserStyle"
-              />
-              <Parse
-                v-if="loader.content.value.content"
-                :content="loader.content.value.content"
-                :tagStyle="commonParserStyle"
-              />
-              <text v-if="emptyContent">暂无简介</text>
-            </template>
-            <!-- 图片 -->
-            <template v-else-if="tabCurrentId == 1">
-              <slot name="imagesPrefix" />
-              <ImageGrid
-                :images="loader.content.value.images"
-                :rowCount="2"
-                :preview="true"
-                imageHeight="200rpx"
-              />
-            </template>
-            <!-- 视频 -->
-            <template v-else-if="tabCurrentId == 2">
-              <video
-                v-if="loader.content.value.video"
-                class="w-100 video"
-                autoplay
-                :poster="loader.content.value.image"
-                :src="loader.content.value.video"
-                controls
-              />
-            </template>
-            <!-- 音频 -->
-            <template v-else-if="tabCurrentId == 3">
-              <video 
-                v-if="loader.content.value.audio"
-                class="w-100 video"
-                autoplay
-                :poster="loader.content.value.image"
-                :src="loader.content.value.audio"
-                controls
-              />
-            </template>
-            <!-- 其他tab -->
-            <slot v-else name="extraTabs" :tabCurrentId="tabCurrentId" :content="loader.content.value" />
-          </view>
-          <ContentNote />
-        </view>
-        <LikeFooter :content="loader.content.value" />
-      </template>
-    </SimplePageContentLoader>
-  </view>
-</template>
-<script setup lang="ts">
-import type { GetContentDetailItem } from "@/api/CommonContent";
-import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
-import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
-import { useTabControl, type TabControlItem } from "@/common/composeabe/TabControl";
-import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
-import ImageGrid from "@/pages/parts/ImageGrid.vue";
-import ImageSwiper from "@/pages/parts/ImageSwiper.vue";
-import ContentNote from "@/pages/parts/ContentNote.vue";
-import commonParserStyle from "@/common/style/commonParserStyle";
-import { computed, type PropType, type Ref } from "vue";
-import Parse from "@/components/display/parse/Parse.vue";
-import Tabs from "@/components/nav/Tabs.vue";
-import LikeFooter from "@/pages/parts/LikeFooter.vue";
-
-const props = defineProps({
-  load: {
-    type: Function as PropType<(id: number, tabsArray: Ref<TabControlItem[]>) => Promise<GetContentDetailItem>>,
-    default: null,
-  },
-  extraTabs: {
-    type: Array as PropType<TabControlItem[]>,
-    default: () => [],
-  },
-  showHead: {
-    type: Boolean,
-    default: true,
-  },
-})
-
-const emit = defineEmits([
-  "tabChange"
-])
-
-const emptyContent = computed(() => {
-  return !(loader.content.value?.intro as string || '').trim() && !(loader.content.value?.content || '').trim();
-})
-
-const loader = useSimplePageContentLoader<
-  GetContentDetailItem, 
-  { id: number }
->(async (params) => {
-  if (!params)
-    throw new Error("!params");
-  const d = await props.load(params.id, tabsArray);
-  tabsArray.value[1].visible = Boolean(d.images && d.images.length > 1);
-  tabsArray.value[2].visible = Boolean(d.video);
-  tabsArray.value[3].visible = Boolean(d.audio);
-
-  if (d.title)
-    uni.setNavigationBarTitle({ title: d.title });
-  setTimeout(() => {
-    if (emptyContent.value) {
-      if (d.video)
-        tabCurrentIndex.value = 1;
-      else if (d.video)
-        tabCurrentIndex.value = 2;
-    }
-  }, 200);
-  return d;
-});
-
-const { 
-  tabCurrentId,
-  tabCurrentIndex,
-  tabsArray,
-  tabs,
-} = useTabControl({
-  tabs: [
-    {
-      id: 0,
-      text: '简介',
-      visible: true,
-    },
-    {
-      id: 1,
-      text: '图片',
-      visible: true,
-    },
-    {
-      id: 2,
-      text: '视频',
-      visible: true,
-    },
-    {
-      id: 3,
-      text: '音频',
-      visible: true,
-    },
-    ...props.extraTabs,
-  ],
-  onTabChange(a, b) {
-    emit("tabChange", a, b);
-  },
-})
-
-useLoadQuerys({ id : 0 }, (p) => loader.loadData(p));
-
-defineExpose({
-  getPageShareData() {
-    const content = loader.content.value;
-    if (!content)
-      return {};
-    const res = {
-      title: content.title,
-      imageUrl: content.image,
-    };
-    return res;
-  } 
-})
-</script>
-
-<style lang="scss">
-
-</style>

+ 30 - 104
src/pages/article/common/IntroBlock.vue

@@ -1,36 +1,43 @@
 <template>
-  <view :class="[
-    'intro-block',
-    small ? 'small' : '',
-  ]">
+  <FlexCol>
     <SubTitle v-if="title" :title="title" />
-    <view class="desc no-indent">
-      <view v-if="address" class="navigation">
-        <view class="address">
-          <text class="iconfont icon-navigation"></text>
-          <text>{{ address }}</text>
-        </view>
-        <view class="link" @click="emit('navTo')">
-          去这里 <text class="iconfont icon-go"></text>
-        </view>
-      </view>
-      <view 
+    <FlexCol :gap="20">
+
+      <Cell 
+        v-if="address"
+        :label="address"
+        icon="map-filling"
+        :iconProps="{ color: 'primary' }"
+        value="去这里"
+        touchable
+        @click="emit('navTo')"
+      />
+
+      <template
         v-for="(it, k) in descItems" 
         :key="k"
-        :class="['entry',Boolean(it.value)?'':'hidden']"
       >
-        <view class="label">{{ it.label }}</view>
-        <view class="value">{{ it.value }}</view>
-      </view>
+        <FlexRow 
+          v-if="Boolean(it.value)"
+          justify="space-between"
+        >
+          <Text :fontSize="26" color="text.second">{{ it.label }}</Text>
+          <Text :fontSize="26" color="text.second">{{ it.value }}</Text>
+        </FlexRow>
+      </template>
       <slot name="lastDesc" />
-    </view>
+    </FlexCol>
     <slot />
-  </view>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
+import Cell from '@/components/basic/Cell.vue';
+import Text from '@/components/basic/Text.vue';
 import SubTitle from '@/components/display/title/SubTitle.vue';
-import type { PropType } from 'vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import { type PropType } from 'vue';
 
 const props = defineProps({	
   title: {
@@ -45,90 +52,9 @@ const props = defineProps({
     type: Array as PropType<Array<{ label: string, value: any }>>,
     default: () => []
   },
-  small: {
-    type: Boolean,
-    default: false
-  }
 })
 
 const emit = defineEmits([	
   "navTo"	
 ])
-</script>
-
-<style lang="scss">
-.intro-block {
-  margin-bottom: 38rpx;
-
-  &.small {
-    margin-bottom: 0rpx;
-
-    .desc{
-      line-height: inherit;
-      padding-bottom: 10rpx;
-    }
-  }
-
-  .entry {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    margin-bottom: 10rpx;
-
-    &.hidden {
-      display: none;
-    }
-
-    .label {
-      color: #666666;
-      font-weight: 400;
-      font-size: 30rpx;
-      flex-shrink: 0;
-    }
-    .value {
-      font-size: 30rpx;
-      color: #312520;
-      font-weight: 400;
-      text-align: right;
-      max-width: 500rpx;
-      flex-shrink: 1;
-    }
-  }
-  .sub-title{
-    margin-left: 20rpx;
-    margin-top: 10rpx;
-    font-size: 35rpx;
-    font-weight: 600;
-  }
-  .desc{
-    padding: 30rpx 0;
-  }
-  .navigation{
-    display: flex;
-    align-items: center;
-    margin-bottom: 28rpx;
-    .address{
-      flex:1;
-      height: auto;
-      background: #F9F6EB;
-      border-radius: 28rpx;
-      font-weight: 400;
-      font-size: 24rpx;
-      color: #000000;
-      line-height: 48rpx;
-      padding-left: 30rpx;
-      display: flex;
-      align-items: center;
-      text.iconfont{
-        display: inline-block;
-        font-size: 36rpx;
-        margin-right: 8rpx;
-      }
-    }
-    .link{
-      margin-left: 20rpx;
-      color:#FF8719;
-    }
-  }
-}
-</style>
+</script>

+ 38 - 31
src/pages/article/details.vue

@@ -1,8 +1,8 @@
 <template>
-  <view class="d-flex flex-column bg-base pb-45">
+  <FlexCol :padding="[0,0,50,0]">
     <SimplePageContentLoader :loader="loader">
       <template v-if="loader.content.value">
-        <view class="d-flex flex-col">
+        <FlexCol :gap="20">
           <swiper 
             v-if="loader.content.value.images.length > 0"
             circular 
@@ -10,61 +10,62 @@
             :autoplay="true"
             :interval="3000"
             :duration="1000"
-            class="height-500"
+            :style="{ height: '500rpx' }"
           >
             <swiper-item v-for="(item, key) in loader.content.value.images" :key="key">
-              <view class="item">
-                <image 
-                  :src="item" 
-                  class="w-100 height-500 radius-base"
-                  mode="aspectFill" 
-                  @click="onPreviewImage(key)"
-                />
-              </view>
+              <Image 
+                :src="item" 
+                width="100%"
+                :height="500"
+                :radius="20"
+                mode="aspectFill"
+                touchable
+                @click="onPreviewImage(key)"
+              />
             </swiper-item>
           </swiper>
-          <image 
+          <Image 
             v-else-if="loader.content.value.image"
-            class="w-100 radius-base"
+            :radius="20"
             :src="loader.content.value.image"
             mode="widthFix"
+            width="100%"
           />
-          <view class="d-flex flex-col p-3">
-            <view class="size-ll color-title-text">{{ loader.content.value.title }}</view>
-            <view class="d-flex flex-row mt-2">
-              <text v-if="loader.content.value.from" class="size-s color-text-content-second mr-2 ">来源:{{ loader.content.value.from }}</text>
-              <text class="size-s color-text-content-second">{{ DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd') }}</text>
-            </view>
-          </view>
-          <view class="p-3 radius-ll bg-light mt-3">
+          <FlexCol :padding="20" :gap="14">
+            <H2>{{ loader.content.value.title }}</H2>
+            <FlexRow>
+              <Text v-if="loader.content.value.from" :text="`来源:${loader.content.value.from}`" fontConfig="subText" />
+              <Text :text="DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd')" fontConfig="subText" />
+            </FlexRow>
+          </FlexCol>
+          <FlexCol :padding="20" :radius="20">
             <Parse
               v-if="loader.content.value.content"
               :content="loader.content.value.content"
               :tagStyle="commonParserStyle"
             />
-            <text v-if="emptyContent">暂无简介</text>
-          </view>
+            <Text v-if="emptyContent" fontConfig="subText">暂无简介</Text>
+          </FlexCol>
           
           <!-- 推荐 -->
-          <view v-if="recommendListLoader.content.value?.length" class="d-flex flex-col p-3">
-            <text class="size-base text-bold mb-3">相关推荐</text>
+          <FlexCol v-if="recommendListLoader.content.value?.length" :padding="20">
+            <H4>相关推荐</H4>
             <Box2LineImageRightShadow
-              class="w-100"
-              titleColor="title-text"
+              titleColor="black"
               v-for="item in recommendListLoader.content.value"
               :key="item.id"
               :image="item.thumbnail || item.image || AppCofig.defaultImage"
               :title="item.title"
               :desc="item.desc"
-              :badge="item.badge"
+              :badge="(item.badge as string)"
               :wideImage="true"
               @click="goDetails(item.id)"
             />
-          </view>
-        </view>
+          </FlexCol>
+        </FlexCol>
       </template>
     </SimplePageContentLoader>
-  </view>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
@@ -83,6 +84,12 @@ import Parse from "@/components/display/parse/Parse.vue";
 import CommonContent, { GetContentListParams } from "@/api/CommonContent";
 import Box2LineImageRightShadow from "@/common/components/parts/Box2LineImageRightShadow.vue";
 import AppCofig from "@/common/config/AppCofig";
+import FlexCol from "@/components/layout/FlexCol.vue";
+import Image from "@/components/basic/Image.vue";
+import H4 from "@/components/typography/H4.vue";
+import Text from "@/components/basic/Text.vue";
+import H2 from "@/components/typography/H2.vue";
+import FlexRow from "@/components/layout/FlexRow.vue";
 
 const loader = useSimplePageContentLoader<
   GetContentDetailItem, 

+ 2 - 2
src/pages/article/web/ewebview.vue

@@ -1,6 +1,6 @@
 <template>
   <web-view 
-    class="w-100 h-100vh"
+    :style="{ width: '100%', height: '100%' }"
     :src="finalUrl"
   />
 </template>
@@ -10,7 +10,7 @@ import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
 import { ref } from 'vue';
 
 const finalUrl = ref('')
-const { querys } = useLoadQuerys({
+useLoadQuerys({
   url: '',
 }, ({ url }) => {
   finalUrl.value = decodeURIComponent(url)

+ 3 - 12
src/pages/dig/admin/volunteer.vue

@@ -36,7 +36,6 @@ 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 { 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';
 
@@ -101,16 +100,6 @@ const formDefine : IDynamicFormOptions = {
       } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
       rules: [{ required: true, message: '请选择区域' }],
     },
-    { 
-      label: '所属村社', name: 'villageId', type: 'select-id',
-      additionalProps: {
-        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: '请选择所属村社' }],
-      show: { callback: () => isNew.value },
-    },
     {
       label: '性别', name: 'sex', type: 'radio-value',
       additionalProps: {
@@ -124,6 +113,7 @@ const formDefine : IDynamicFormOptions = {
       label: '头像', name: 'image', type: 'uploader',
       additionalProps: {
         single: true,
+        
         maxFileSize: 1024 * 1024 * 10,
         upload: useAliOssUploadCo('xiangyuan/volunteer/images')
       } as UploaderFieldProps,
@@ -168,7 +158,7 @@ const formDefine : IDynamicFormOptions = {
 }
 
 async function submit() {
-  if (!formRef.value)
+  if (!formRef.value || !formModel.value)
     return;
   try {
     await formRef.value.validate();
@@ -178,6 +168,7 @@ async function submit() {
   }
   try {
     loading.value = true;
+    formModel.value!.villageId = querys.value.villageId;
     if (querys.value.id >= 0) {
       await VillageApi.updateVolunteer(formModel.value!);
     } else {

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

@@ -38,7 +38,7 @@
                 <H3>{{ item.villageName }}</H3>
               </FlexRow>
               <FlexRow align="center" :gap="20">
-                <Button type="primary" size="small" icon="work-filling" @click="goManagePage(item)">管理</Button>
+                <Button v-if="authStore.isAdmin" type="primary" size="small" icon="work-filling" @click="goManagePage(item)">管理</Button>
                 <Button type="default" size="small" icon="edit-filling" @click="goSubmitDigPage(item)">采编</Button>
               </FlexRow>
             </FlexRow>

+ 7 - 5
src/pages/editor/editor.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="d-flex flex-column h-100vh">
+  <FlexCol height="100vh">
     <sp-editor
       :toolbar-config="{
         excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck'],
@@ -10,14 +10,14 @@
       @input="inputOver"
       @upinImage="upinImage"
       @overMax="overMax"
-    ></sp-editor>
+    />
     
-    <view class="d-flex flex-row align-center gap-s p-3">
+    <FlexRow align="center" :padding="18" :gap="15">
       <Button :innerStyle="{flex:1}" type="danger" @click="cancel">取消</Button>
       <Button :innerStyle="{flex:1}" type="primary" @click="save">保存</Button>
-    </view>
+    </FlexRow>
     <XBarSpace />
-  </view>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
@@ -29,6 +29,8 @@ import spEditor from '@/uni_modules/sp-editor/components/sp-editor/sp-editor.vue
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import Button from '@/components/basic/Button.vue';
 import CommonContent from '@/api/CommonContent';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
 
 const maxLength = ref(-1);
 

+ 4 - 3
src/pages/editor/preview.vue

@@ -1,15 +1,16 @@
 <template>
-  <view>
+  <FlexCol :padding="20">
     <Empty v-if="!content" image="search" description="空内容,请先编写内容后再预览" />
-    <view v-else class="p-3">
+    <view v-else>
       <Parse :content="content" />
     </view>
-  </view>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
 import Parse from '@/components/display/parse/Parse.vue';
 import Empty from '@/components/feedback/Empty.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
 import { onLoad } from '@dcloudio/uni-app';
 import { ref } from 'vue';
 

+ 2 - 0
src/pages/home/discover/details.vue

@@ -47,6 +47,8 @@
             <FlexRow :padding="20" :gap="20" align="center">
               <Image
                 :src="loader.content.value.image"
+                :failedImage="AppCofig.defaultImage"
+                :defaultImage="AppCofig.defaultImage"
                 width="80"
                 height="80"
                 radius="10"

+ 50 - 40
src/pages/home/village/details.vue

@@ -1,30 +1,31 @@
 <template>
   <SimplePageContentLoader :loader="contentLoader">
-    <view 
-      v-if="contentLoader.loadStatus.value == 'finished'"
-      class="d-flex flex-column bg-base"
-    >
+    <FlexCol v-if="contentLoader.loadStatus.value == 'finished'">
       <swiper 
         circular
-        class="height-500"
         :indicator-dots="false"
         :autoplay="true"
         :interval="2000"
         :duration="1000"
+        :style="{
+          height: '500rpx',
+        }"
       >
         <swiper-item v-for="(item, k) in data.images" :key="k">
-          <image 
-            class="w-100 height-500 radius-l-top" 
+          <Image 
             :src="item" 
+            :radius="20"
+            :height="500"
             mode="aspectFill"
+            touchable
             @click="onPreviewImage(k)"
           />
         </swiper-item>
       </swiper>
 
-      <view class="d-flex flex-col p-3 radius-l-top p-3 bg-light">
+      <FlexCol :padding="20" :radius="20">
 
-        <view class="d-flex flex-col">
+        <FlexCol>
           <SubTitle :title="data.villageName" />
           <IntroBlock 
             small
@@ -43,46 +44,50 @@
               },
             ]"
           />
-          <view class="mt-3 color-text-content">
+          <FlexCol :margin="[20, 0, 0, 0]">
             <Parse :content="data.overview" :tagStyle="commonParserStyle" />
-            <text v-if="!data.overview" >无内容,请添加内容! {{ data.overview }}</text>
-          </view>
-        </view>
+            <Text v-if="!data.overview" fontConfig="subText">无内容,请添加内容! </Text>
+          </FlexCol>
+        </FlexCol>
 
-        <view class="d-flex flex-row flex-wrap mt-3">
-          <view 
+        <FlexCol :margin="[20, 0, 0, 0]" wrap>
+          <FlexCol 
             v-for="(tag, key) in tagsData"
             :key="key"
-            class="w-20 d-flex flex-column align-center"
+            width="20%"
+            align="center"
             @click="goList(tag)"
           >
-            <image :src="tag.image" class="width-100 mt-2" mode="widthFix"></image>
-            <view class="text-align-center color-text-content size-ss">{{ tag.title }}</view>
-          </view>
-        </view>
+            <Image :src="tag.image" width="100%" mode="widthFix"></Image>
+            <Text fontConfig="subText" textAlign="center">{{ tag.title }}</Text>
+          </FlexCol>
+        </FlexCol>
 
-        <view class="d-flex flex-col mt-3 mb-3">
+        <FlexCol :margin="[20, 0, 30, 0]">
           <SubTitle title="地理位置" />
-          <div class="d-flex flex-column radius-base bg-white mt-3">
-            <map id="map"
-              class="w-100 height-350"
+          <FlexCol :radius="20" backgroundColor="white" :margin="[20, 0, 0, 0]">
+            <map 
+              id="map"
               :latitude="center[1]"
               :longitude="center[0]"
               :markers="markers"
               :scale="15"
+              :style="{
+                width: '100%',
+                height: '350rpx',
+              }"
             />
-            <view class="d-flex flex-row justify-between p-2 mt-1">
-              <view>
-                <text class="iconfont icon-navigation"></text>
-                <text class="address">{{ data.address }}</text>
-              </view>
-              <view class="d-flex flex-row align-center" @click="goAddress">
-                <text class="color-orange">去这里</text>
-                <text class="iconfont icon-arrow-right"></text>
-              </view>
-            </view>
-          </div>
-        </view>
+            <Cell
+              v-if="data.address"
+              :label="data.address"
+              icon="map-filling"
+              :iconProps="{ color: 'primary' }"
+              value="去这里"
+              touchable
+              @click="goAddress"
+            />
+          </FlexCol>
+        </FlexCol>
 
         <template 
           v-for="(tag, index) in tagsDataRecommend"
@@ -90,7 +95,7 @@
         >
           <SubTitle :title="tag.title" showMore @moreClicked="tag.goList()" />
           <SimplePageContentLoader :loader="tag.loader" >
-            <view class="d-flex flex-col">
+            <FlexCol>
               <Box2LineLargeImageUserShadow 
                 v-for="(item, i) in tag.loader.content.value"
                 :key="i"
@@ -101,12 +106,12 @@
                 :comment="item.comments"
                 @click="tag.goDetail(item.id)"
               />
-            </view>
+            </FlexCol>
           </SimplePageContentLoader>
         </template>
-      </view>
+      </FlexCol>
 
-    </view>
+    </FlexCol>
   </SimplePageContentLoader>
 </template>
 
@@ -126,6 +131,11 @@ import VillageApi from '@/api/inhert/VillageApi';
 import commonParserStyle from '@/common/style/commonParserStyle';
 import ImagesUrls from '@/common/config/ImagesUrls';
 import SubTitle from '@/components/display/title/SubTitle.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Cell from '@/components/basic/Cell.vue';
 
 const EmptyImage = 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png';