Selaa lähdekoodia

📦 贴图管理功能

快乐的梦鱼 1 viikko sitten
vanhempi
commit
c4d1e1ad0b

+ 8 - 3
src/api/BaseAppServerRequestModule.ts

@@ -133,15 +133,20 @@ export class BaseAppServerRequestModule<T extends DataModel> extends RequestCore
           let data = {} as any;
 
           //后端返回格式不统一,所以在这里处理格式
-          if (typeof json.data === 'object') {
+          if (typeof json.data !== undefined) {
             data = json.data;
-            message = json.data?.msg || response.statusText;
+            if (typeof json.data === 'object')
+              message = json.data?.msg;
+            else
+              message = json.msg;
           }
           else {
             //否则返回上层对象
             data = json;
-            message = json.msg || response.statusText;
+            message = json.msg;
           }
+          if (!message)
+            message = response.statusText;
           return new RequestApiResult(
             resultModelClass ?? instance.config.modelClassCreator,
             json?.code || response.status,

+ 0 - 65
src/api/light/LightVillageApi.ts

@@ -272,38 +272,6 @@ export interface VillageTreeAnimProps {
   }>,
 }
 
-export class PostMessage extends DataModel<PostMessage> {
-  constructor() {
-    super(PostMessage, "微信贴图");
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      title: { clientSide: 'string', serverSide: 'string' },
-      images: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-      sendTime: { clientSide: 'date', serverSide: 'string' },
-    }
-    this._afterSolveServer = () => {
-      if (!this.images) {
-        this.images = [];
-      }
-      if (this.image && this.images?.length == 0) {
-        this.images = [ this.image ];
-      }
-    }
-  }
-
-  id !: number;
-  title = '';
-  content = '';
-  images = [] as string[];
-  image = '';
-  nickName = '';
-  avatar = '';
-  likeCount = 0;
-  shareCount = 0;
-  sendTime = new Date();
-}
-
 export class LightVillageApi extends AppServerRequestModule<DataModel> {
 
   constructor() {
@@ -418,39 +386,6 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
     return res.requireData();
   }
 
-  async getMessages(page: number, pageSize: number, search?:{
-    keywords?: string;
-    villageId?: number;
-    userId?: number;
-  }) {
-    const res = await this.get<{
-      data: KeyValue[],
-      total: number,
-    }>('/village/collect/wechatContentList', '获取微信贴图列表', {
-      page,
-      pageSize,
-      keywords: search?.keywords,
-      village_id: search?.villageId,
-      user_id: search?.userId,
-    });
-    const data = res.requireData();
-    return {
-      list: transformArrayDataModel<PostMessage>(PostMessage, transformSomeToArray(data.data), '微信贴图列表', true),
-      total: data.total,
-    };
-  }
-
-  async getMessageDetails(id: number) {
-    const res = await this.get<KeyValue>('/village/collect/wechatContentDetail', '获取微信贴图详情', {
-      id: id,
-    });
-    return transformDataModel<PostMessage>(PostMessage, res.requireData());
-  }
-
-  async publishMessage(params: PostMessage) {
-    return await this.post<KeyValue>('/village/collect/wechatContentSave', '发布微信贴图', params.toServerSide());
-  }
-
   async updateVillageGallery(id: number, images: string[]) {
     return await this.post<KeyValue>('/village/village/save', '更新村社相册', {
       id: id,

+ 265 - 0
src/api/light/OfficialApi.ts

@@ -0,0 +1,265 @@
+import { DataModel, transformArrayDataModel, transformDataModel, type KeyValue } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import { transformSomeToArray } from '../Utils';
+
+export class PostMessage extends DataModel<PostMessage> {
+  constructor() {
+    super(PostMessage, "微信贴图");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      title: { clientSide: 'string', serverSide: 'string' },
+      images: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      sendTime: { clientSide: 'date', serverSide: 'string' },
+    }
+    this._afterSolveServer = () => {
+      if (!this.images) {
+        this.images = [];
+      }
+      if (this.image && this.images?.length == 0) {
+        this.images = [ this.image ];
+      }
+      if (!this.nickName)
+        this.nickName = this.nickname as string || '';
+    }
+  }
+
+  id !: number;
+  title = '';
+  content = '';
+  images = [] as string[];
+  image = '';
+  jumpUrl = '';
+  nickName = '';
+  topicName = '';
+  avatar = '';
+  likeCount = 0;
+  shareCount = 0;
+  sendTime = new Date();
+
+  ext?: StickerSearchResult;
+}
+
+export class TopicItem extends DataModel<TopicItem> {
+  constructor() {
+    super(TopicItem, '话题');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      interactionCount: { clientSide: 'number', serverSide: 'number' },
+      itemCount: { clientSide: 'number', serverSide: 'number' },
+      readCount: { clientSide: 'number', serverSide: 'number' },
+      readCountInWxa: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  /** 话题名称 */
+  topic = '';
+  /** 互动数 */
+  interactionCount = 0;
+  /** 是否API发布汇总: 0=否 */
+  isApiPubSummary = 0;
+  /** 内容数 */
+  itemCount = 0;
+  /** 阅读数 */
+  readCount = 0;
+  /** 小程序内阅读数 */
+  readCountInWxa = 0;
+}
+
+export class TopicListSummary extends DataModel<TopicListSummary> {
+  constructor() {
+    super(TopicListSummary, '话题汇总');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      interactionCount: { clientSide: 'number', serverSide: 'number' },
+      itemCount: { clientSide: 'number', serverSide: 'number' },
+      readCount: { clientSide: 'number', serverSide: 'number' },
+      readCountInWxa: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  /** 总互动数 */
+  interactionCount = 0;
+  /** 总内容数 */
+  itemCount = 0;
+  /** 总阅读数 */
+  readCount = 0;
+  /** 小程序内总阅读数 */
+  readCountInWxa = 0;
+}
+
+export class StickerPictureInfo extends DataModel<StickerPictureInfo> {
+  constructor() {
+    super(StickerPictureInfo, '贴图图片信息');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      width: { clientSide: 'number', serverSide: 'number' },
+      height: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  cdnUrl = '';
+  width = 0;
+  height = 0;
+  shareCover = [] as string[];
+}
+
+export class StickerMsgId extends DataModel<StickerMsgId> {
+  constructor() {
+    super(StickerMsgId, '贴图消息ID');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      bizuin: { clientSide: 'number', serverSide: 'number' },
+      idx: { clientSide: 'number', serverSide: 'number' },
+      msgid: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  bizuin = 0;
+  idx = 0;
+  msgid = 0;
+}
+
+export interface StickerSearchResult {
+  /** 贴图标题 */
+  title: string;
+  /** 是否置顶: 0=否, 1=是 */
+  isTop: number;
+  /** 是否拉黑: 0=否, 1=是 */
+  isBlock: number;
+  /** 图片信息 */
+  pictureInfo: StickerPictureInfo;
+  /** 消息ID */
+  id: StickerMsgId;
+}
+
+export class OfficialApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  /**
+   * 获取微信贴图列表
+   */
+  async getMessages(page: number, pageSize: number, search?:{
+    keywords?: string;
+    villageId?: number;
+    userId?: number;
+  }) {
+    const res = await this.get<{
+      data: KeyValue[],
+      total: number,
+    }>('/village/collect/wechatContentList', '获取微信贴图列表', {
+      page,
+      pageSize,
+      keywords: search?.keywords,
+      village_id: search?.villageId,
+      user_id: search?.userId,
+    });
+    const data = res.requireData();
+    return {
+      list: transformArrayDataModel<PostMessage>(PostMessage, transformSomeToArray(data.data), '微信贴图列表', true),
+      total: data.total,
+    };
+  }
+
+  /**
+   * 获取贴图详情
+   */
+  async getMessageDetails(id: number) {
+    const res = await this.get<KeyValue>('/village/collect/wechatContentDetail', '获取微信贴图详情', {
+      id: id,
+    });
+    return transformDataModel<PostMessage>(PostMessage, res.requireData());
+  }
+
+  /**
+   * 发布贴图
+   */
+  async publishMessage(params: PostMessage) {
+    return await this.post<KeyValue>('/village/collect/wechatContentSave', '发布微信贴图', params.toServerSide());
+  }
+
+  /**
+   * 获取话题列表
+   */
+  async getTopicList(page: number, pageSize: number, keywords?: string) {
+    const res = await this.post<{
+      data: KeyValue,
+      success: boolean,
+    }>('/village/topic/topicList', '获取话题列表', {
+      current: page,
+      offset: pageSize,
+      keywords,
+    });
+    const raw = res.requireData();
+    const data = raw.data as KeyValue;
+    return {
+      topics: transformArrayDataModel<TopicItem>(TopicItem, transformSomeToArray(data.topics), '话题列表', true),
+      summary: transformDataModel<TopicListSummary>(TopicListSummary, data.summary as KeyValue),
+      total: data.total as number,
+      keywords: (data.keywords || []) as string[],
+    };
+  }
+
+  /**
+   * 搜索贴图详细信息
+   */
+  async stickerSearch(topicName: string, stickerUrl: string) {
+    const res = await this.post<{
+      data: KeyValue,
+      success: boolean,
+    }>('/village/topic/stickerSearch', '搜索贴图详细信息', {
+      topic_name: topicName,
+      sticker_url: stickerUrl,
+    });
+    const raw = res.requireData();
+    const msg = (raw.data as KeyValue).msg as KeyValue;
+    return {
+      title: msg.title as string,
+      isTop: msg.is_top as number,
+      isBlock: msg.is_block as number,
+      pictureInfo: transformDataModel<StickerPictureInfo>(StickerPictureInfo, msg.picture_info as KeyValue),
+      id: transformDataModel<StickerMsgId>(StickerMsgId, msg.id as KeyValue),
+    } as StickerSearchResult;
+  }
+
+  /**
+   * 贴图置顶 / 取消置顶
+   * @param action top=置顶, cancel=取消置顶
+   */
+  async stickerTop(topicName: string, stickerUrl: string, action: 'top' | 'cancel') {
+    await this.post<KeyValue>('/village/topic/stickerTop', '贴图置顶', {
+      topic_name: topicName,
+      sticker_url: stickerUrl,
+      action,
+    });
+  }
+
+  /**
+   * 贴图拉黑 / 取消拉黑
+   * @param action block=拉黑, cancel=取消拉黑
+   */
+  async stickerBlock(topicName: string, stickerUrl: string, action: 'block' | 'cancel') {
+    await this.post<KeyValue>('/village/topic/stickerBlock', '贴图拉黑', {
+      topic_name: topicName,
+      sticker_url: stickerUrl,
+      action,
+    });
+  }
+
+  /**
+   * 检查贴图管理权限
+   * @returns 是否有权限
+   */
+  async checkTopicRule(villageId: number) {
+    const res = await this.post<boolean>('/village/topic/checkTopicRule', '检查贴图管理权限', {
+      village_id: villageId,
+    });
+    return res.requireData();
+  }
+}
+
+
+export default new OfficialApi();

+ 11 - 1
src/common/components/CommonTopBanner.vue

@@ -8,7 +8,13 @@
       minHeight: '100vh',
     }">
       <StatusBarSpace  />
-      <NavBar v-if="showNav" :title="title" leftButton="back" />
+      <NavBar 
+        v-if="showNav"
+        :title="title" 
+        leftButton="back" 
+        :customBack="customBack" 
+        @backPressed="emit('backPressed')" 
+      />
       <slot />
     </FlexCol>
   </CommonRoot>
@@ -23,9 +29,13 @@ import NavBar from '@/components/nav/NavBar.vue';
 withDefaults(defineProps<{
   title: string;
   showNav?: boolean;
+  customBack?: boolean;
 }>(), {
   title: '',
   showNav: true,
+  customBack: false,
 });
 
+const emit = defineEmits(['backPressed']);
+
 </script>

+ 10 - 1
src/components/nav/NavBar.vue

@@ -169,6 +169,11 @@ export interface NavBarProps {
    * 自定义类名
    */
   innerClass?: any;
+  /**
+   * 是否自定义返回逻辑。如果为true,则点击返回按钮后 backPressed 事件触发
+   * @default false
+   */
+  customBack?: boolean;
 }
 
 function getButton(type: NavBarButtonTypes|string) {
@@ -183,7 +188,7 @@ function getButton(type: NavBarButtonTypes|string) {
   return button;
 }
 
-const emit = defineEmits([  'leftButtonPressed', 'rightButtonPressed' ]);
+const emit = defineEmits([  'leftButtonPressed', 'rightButtonPressed', 'backPressed' ]);
 
 const theme = useTheme();
 
@@ -216,6 +221,10 @@ const titleTextStyle = theme.useThemeStyle({
 
 function handleButtonNavBack(button: NavBarButtonTypes|string, callback: () => void) {
   if (button === 'back') {
+    if (props.customBack) {
+      emit('backPressed');
+      return;
+    }
     if (isTopLevelPage()) {
       uni.reLaunch({
         url: theme.getVar('AppHomePage', '/pages/index/index'),

+ 14 - 0
src/pages.json

@@ -218,6 +218,20 @@
       }
     },
     {
+      "path": "pages/home/village/post/management-topic",
+      "style": {
+        "navigationBarTitleText": "话题管理",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/home/village/post/management-list",
+      "style": {
+        "navigationBarTitleText": "贴图管理",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/article/details",
       "style": {
         "navigationBarTitleText": "新闻详情"

+ 3 - 2
src/pages/home/chat/dependent/post/publish.vue

@@ -86,13 +86,14 @@ import ProvideVar from '@/components/theme/ProvideVar.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Popup from '@/components/dialog/Popup.vue';
 import Agent from './components/agent.vue';
-import LightVillageApi, { PostMessage } from '@/api/light/LightVillageApi';
+import OfficialApi, { PostMessage } from '@/api/light/OfficialApi';
 import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
 import PrimaryButton from '@/common/components/PrimaryButton.vue';
 import Tbutton from './components/tbutton.vue';
 import Height from '@/components/layout/space/Height.vue';
 import type { UploaderAction, UploaderItem } from '@/components/form/Uploader';
 import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
+import LightVillageApi from '@/api/light/LightVillageApi';
 
 const { querys } = useLoadQuerys({
   tag: '',
@@ -209,7 +210,7 @@ function publish() {
   (uni as any).shareToOfficialAccount({
     ...data,
     success: (postObj: { postUrl: string }) => {
-      LightVillageApi.publishMessage(new PostMessage().setSelfValues({
+      OfficialApi.publishMessage(new PostMessage().setSelfValues({
         title: title.value,
         content: content.value,
         images: images.value.map((image) => image.url),

+ 2 - 2
src/pages/home/composeabe/OfficialAccount.ts

@@ -1,10 +1,10 @@
-import LightVillageApi, { PostMessage } from "@/api/light/LightVillageApi";
+import OfficialApi, { PostMessage } from "@/api/light/OfficialApi";
 
 export function useOfficialAccount(getInfoObj: () => any) {
 
   function onPublishSuccess(postObj: { postUrl: string }) {
     const res = getInfoObj();
-    LightVillageApi.publishMessage(new PostMessage().setSelfValues({
+    OfficialApi.publishMessage(new PostMessage().setSelfValues({
       path: `/pages/index`,
       villageId: res.villageId,
       userId: res.userId,

+ 22 - 4
src/pages/home/village/introd/card.vue

@@ -285,7 +285,7 @@ import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLo
 import { useVillageStore } from '@/store/village';
 import { useRequireLogin } from '@/common/composeabe/RequireLogin';
 import { useFollow } from '../composeabe/Follow';
-import { ArrayUtils } from '@imengyu/imengyu-utils';
+import { ArrayUtils, assertNotNull } from '@imengyu/imengyu-utils';
 import { navTo } from '@/components/utils/PageAction';
 import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import Icon from '@/components/basic/Icon.vue';
@@ -312,8 +312,9 @@ import Button from '@/components/basic/Button.vue';
 import BubbleTip from '@/components/feedback/BubbleTip.vue';
 import MemoryTimeOut from '@/components/composeabe/MemoryTimeOut';
 import TextEllipsis from '@/components/display/TextEllipsis.vue';
-import { toast } from '@/components/dialog/CommonRoot';
+import { confirm, toast } from '@/components/dialog/CommonRoot';
 import { useGetNotice } from '../composeabe/GetNotice';
+import OfficialApi from '@/api/light/OfficialApi';
 
 const authStore = useAuthStore();
 const { getIsVolunteer } = useUserTools();
@@ -460,8 +461,25 @@ function handleGoGallery() {
     maxCount: villageStore.currentVillage?.imageLimit || 3,
   });
 }
-function handleGoOfficalManage() {
-  toast('暂未开放,敬请期待');
+async function handleGoOfficalManage() {
+  assertNotNull(villageStore.currentVillage?.id)
+  /* const isAdmin = await OfficialApi.checkTopicRule(villageStore.currentVillage.id);
+  if (!isAdmin) {
+    const goUpgrade = await confirm({
+      title: '提示',
+      content: '您还不是管理员,无法管理贴图哦',
+      confirmText: '去升级',
+      cancelText: '取消',
+    });
+    if (goUpgrade) {
+      upgradeRef.value?.show();
+    }
+    return;
+  } */
+  navTo('/pages/home/village/post/management-list', {
+    villageId: villageStore.currentVillage.id,
+    topic: recommendTagName.value,
+  });
 }
 
 const { currentNoticeContent, noticeListLoader } = useGetNotice(() => villageStore.currentVillage?.id || 0);

+ 84 - 0
src/pages/home/village/post/components/PostItem.vue

@@ -0,0 +1,84 @@
+<template>
+  <BackgroundBox
+    backgroundImage="https://xy.wenlvti.net/app_static/images/village/BoxDark.png"
+    :backgroundCutBorder="[6,6,6,6]"
+    :backgroundCutBorderSize="[10,10,10,10]"
+  >
+    <Touchable
+      direction="row"
+      justify="space-between"
+      align="center"
+      gap="gap.md"
+      :padding="[15,25]"
+      @click="emit('click', item)"
+    >
+      <FlexRow align="center" gap="gap.lg">
+        <Image 
+          v-if="image"
+          :src="image" 
+          defaultImage="https://xy.wenlvti.net/app_static/images/village/PlaceholderVillage.jpg"
+          :width="160"
+          :height="110"
+          mode="aspectFill"
+          radius="radius.md"
+        />
+        <FlexCol>
+          <Text v-if="topicName" :text="`#${topicName}`" :maxWidth="400" :lines="2" fontConfig="contentText" />
+          <Text v-if="title" :text="title" :maxWidth="400" :lines="2" fontConfig="contentText" />
+          <Text v-if="userName" :text="`发布人:${userName}`" fontConfig="contentText" />
+        </FlexCol>
+      </FlexRow>
+      <FlexRow align="center" gap="gap.md">
+        <IconButton 
+          v-if="showPinned"
+          icon="rise-filling"
+          text="已置顶"
+          size="26"
+        />
+        <IconButton 
+          v-if="showBlocked"
+          icon="delete-filling"
+          color="danger"
+          text="已屏蔽"
+          size="26"
+        />
+        <FrameButton 
+          v-if="showManagement"
+          text="管理"
+          size="small"
+          @click="emit('management', item)"
+        />
+      </FlexRow>
+    </Touchable>
+  </BackgroundBox>
+</template>
+
+<script setup lang="ts">  
+import Touchable from '@/components/feedback/Touchable.vue';
+import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
+import Text from '@/components/basic/Text.vue';
+import Image from '@/components/basic/Image.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import type { PostMessage } from '@/api/light/OfficialApi';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import IconButton from '@/components/basic/IconButton.vue';
+
+const props = withDefaults(defineProps<{  
+  item: any,
+  image?: string;
+  topicName?: string;
+  title?: string;
+  userName?: string;
+  showManagement?: boolean;
+  showPinned?: boolean;
+  showBlocked?: boolean;
+}>(), {
+  showManagement: true,
+});
+
+const emit = defineEmits<{
+  (e: 'click', item: any): void;
+  (e: 'management', item: any): void;
+}>();
+</script>

+ 165 - 0
src/pages/home/village/post/management-list.vue

@@ -0,0 +1,165 @@
+<template>
+  <CommonTopBanner title="贴图管理" @backPressed="handleBack">
+    <FlexCol padding="padding.md">
+      <SearchBar
+        v-model="searchText"
+        placeholder="搜一搜" 
+        @search="listLoader.reload()"
+      />
+      <Height :height="20" />
+      <SimplePageListLoader :loader="listLoader">
+        <FlexCol gap="gap.lg">
+          <PostItem
+            v-for="item in listLoader.list.value"
+            :key="item.id"
+            :item="item"
+            :image="item.image || ImagesUrls.defaultImage"
+            :topicName="item.topicName"
+            :title="item.title || item.content"
+            :showPinned="item.ext?.isTop === 1"
+            :showBlocked="item.ext?.isBlock === 1"
+            :userName="item.nickName"
+            @click="goDetail(item)"
+            @management="manage(item)"
+          />
+        </FlexCol>
+      </SimplePageListLoader>
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
+import { ref } from 'vue';
+import { alert, actionSheet, toast } from '@/components/dialog/CommonRoot';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePageListLoader';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Height from '@/components/layout/space/Height.vue';
+import OfficialApi, { type PostMessage } from '@/api/light/OfficialApi';
+import SearchBar from '@/components/form/SearchBar.vue';
+import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
+import PostItem from './components/PostItem.vue';
+import ImagesUrls from '@/common/config/ImagesUrls';
+import { formatError } from '@imengyu/imengyu-utils';
+
+const searchText = ref('');
+const listManaged = ref(false);
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+  topic: '',
+}, async (querys) => {
+  listLoader.reload();
+})
+
+const listLoader = useSimplePageListLoader(40, async (page, pageSize) => {
+  let res = (await OfficialApi.getMessages(
+    page,
+    pageSize,
+    {
+      keywords: searchText.value,
+      villageId: querys.value.villageId,
+    }
+  ));
+  const list = res.list
+    .filter((item) => item.topicName === querys.value.topic)
+    .map((item) => {
+      return item
+    })
+
+  for (const element of list) {
+    if (!element.jumpUrl)
+      continue;
+    const res = await OfficialApi.stickerSearch(element.topicName, element.jumpUrl);
+    element.ext = res;
+  }
+
+  return {
+    list: list,
+    total: res.total,
+  };
+}, false);
+
+function goDetail(item: PostMessage) {
+  if (!item.jumpUrl) {
+    alert({
+      title: '提示',
+      content: '数据错误。本贴图暂无跳转链接',
+    });
+    return;
+  }
+  uni.openOfficialAccountArticle({
+    url: item.jumpUrl,
+    fail: (err) => {
+      console.log(err);
+    }
+  })
+}
+async function manage(item: PostMessage) {
+  if (!item.jumpUrl || !item.ext) {
+    alert({
+      title: '提示',
+      content: '数据错误。链接丢失',
+    });
+    return;
+  }
+
+  const res = await actionSheet({
+    title: '管理贴图',
+    actions: [
+      {
+        name: item.ext?.isTop === 1 ? '取消置顶' : '置顶',
+        subname: '在村社首页优先展示优秀内容',
+      },
+      {
+        name: item.ext?.isBlock === 1 ? '取消屏蔽' : '屏蔽',
+        subname: '屏蔽劣质贴图,维护村社首页型象',
+      },
+    ],
+    showCancel: true,
+  });
+  try {
+    uni.showLoading({
+      title: '操作中...',
+    });
+    switch (res) {
+      case 0:
+        if (item.ext.isTop === 1) {
+          await OfficialApi.stickerTop(item.topicName, item.jumpUrl, 'cancel');
+          item.ext.isTop = 0;
+          toast('取消置顶成功');
+        } else {
+          await OfficialApi.stickerTop(item.topicName, item.jumpUrl, 'top');
+          item.ext.isTop = 1;
+          toast('置顶成功');
+        }
+        break;
+      case 1:
+        if (item.ext.isBlock === 1) {
+          await OfficialApi.stickerBlock(item.topicName, item.jumpUrl, 'cancel');
+          item.ext.isBlock = 0;
+          toast('取消屏蔽成功');
+        } else {
+          await OfficialApi.stickerBlock(item.topicName, item.jumpUrl, 'block');
+          item.ext.isBlock = 1;
+          toast('屏蔽成功');
+        }
+        break;
+    }
+  } catch (e) {
+    toast('操作失败' + formatError(e));
+  } finally {
+    uni.hideLoading();
+  }
+}
+function handleBack() {
+  if (listManaged.value) {
+    backAndCallOnPageBack('refreshOfficialAccount', { })
+    return;
+  }
+  back();
+}
+
+</script>

+ 66 - 0
src/pages/home/village/post/management-topic.vue

@@ -0,0 +1,66 @@
+<template>
+  <CommonTopBanner title="贴图管理">
+    <FlexCol v-if="!isAdmin" :padding="[80,20]" :gap="10" center>
+      <Image src="https://mn.wenlvti.net/app_static/xiangyuan/images/home/UnOpenIcon.png" width="300" mode="widthFix" />
+      <Text textAlign="center">您还不是管理员,无法管理贴图哦</Text>
+      <Height :size="20" />
+      <Button size="large" type="primary" @click="back">
+        返回
+      </Button>
+    </FlexCol>
+    <FlexCol v-else padding="padding.md">
+      <SimplePageContentLoader :loader="topicsListLoader">
+        <FlexCol gap="gap.lg">
+          <PostItem
+            v-for="item in topicsListLoader.content.value?.topics"
+            :key="item.topic"
+            :item="item"
+            :title="`文章数:${item.itemCount} 阅读数:${item.readCount}`"
+            :topicName="item.topic"
+            @click="goTopicManagement(item.topic)"
+            @management="goTopicManagement(item.topic)"
+          />
+        </FlexCol>
+      </SimplePageContentLoader>
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { navTo, back } from '@/components/utils/PageAction';
+import { ref } from 'vue';
+import { alert } from '@/components/dialog/CommonRoot';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import Height from '@/components/layout/space/Height.vue';
+import Button from '@/components/basic/Button.vue';
+import OfficialApi from '@/api/light/OfficialApi';
+import PostItem from './components/PostItem.vue';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+
+const isAdmin = ref(false);
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+  topic: '',
+}, async (querys) => {
+  isAdmin.value = true//await OfficialApi.checkTopicRule(querys.villageId);
+  topicsListLoader.reload();
+})
+
+const topicsListLoader = useSimpleDataLoader(async () => {
+  return (await OfficialApi.getTopicList(1, 100, querys.value.topic));
+}, false);
+
+function goTopicManagement(topic: string) {
+  navTo('/pages/home/village/post/management-list', { 
+    topic,
+    villageId: querys.value.villageId,
+  });
+}
+</script>
+

+ 6 - 2
src/pages/index.vue

@@ -38,7 +38,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, watch } from 'vue';
 import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
 import { useTheme } from '@/components/theme/ThemeDefine';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
@@ -125,6 +125,10 @@ async function paySuccessAndRefresh() {
   }
 }
 
+watch(tabIndex, () => {
+  uni.setStorageSync('lastTabIndex', tabIndex.value);
+});
+
 
 defineExpose({
   onPageBack: (name: string, data: Record<string, unknown>) => {
@@ -162,7 +166,7 @@ onShareTimeline(() => {
 })
 onMounted(() => {
   if (isDevEnv) {
-    //tabIndex.value = 1;
+    tabIndex.value = Number(uni.getStorageSync('lastTabIndex') ?? 0);
   }
 })
 </script>