快乐的梦鱼 дней назад: 6
Родитель
Сommit
25b9ec36da

+ 65 - 1
src/api/RequestModules.ts

@@ -5,9 +5,10 @@
  */
 
 import ApiCofig from "@/common/config/ApiCofig";
-import type { DataModel } from "@imengyu/js-request-transform";
+import type { DataModel, NewDataModel } from "@imengyu/js-request-transform";
 import { BaseAppServerRequestModule } from "./BaseAppServerRequestModule";
 import { isDev } from "@/common/config/AppCofig";
+import { defaultResponseDataHandlerCatch, RequestApiError, RequestApiResult, RequestCoreInstance, RequestOptions, RequestResponse, type RequestApiInfoStruct } from "@imengyu/imengyu-utils";
 
 /**
  * 主应用服务请求模块
@@ -16,4 +17,67 @@ export class AppServerRequestModule<T extends DataModel> extends BaseAppServerRe
   constructor() {
     super(isDev ? ApiCofig.server.Dev : ApiCofig.server.Prod);
   }
+}
+
+/**
+ * 更新服务请求模块
+ */
+export class UpdateServerRequestModule<T extends DataModel> extends BaseAppServerRequestModule<T> {
+  constructor() {
+    super("https://update-server1.imengyu.top");
+    this.config.requestInterceptor = undefined;
+    this.config.responseDataHandler = async function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel | undefined, instance: RequestCoreInstance<T>, apiInfo: RequestApiInfoStruct): Promise<RequestApiResult<T>> {
+      const method = req.method || 'GET';
+      try {
+        const json = await response.json();
+        if (response.ok) {
+          if (!json) {
+            throw new RequestApiError(
+              'businessError',
+              '后端未返回数据',
+              '',
+              response.status,
+              null,
+              null,
+              response.headers,
+              apiInfo
+            );
+          }
+          if (!json.success)
+            throw new RequestApiError(
+              'businessError',
+              json.message,
+              json.code.toString(),
+              json.code,
+              json,
+              json,
+              response.headers,
+              apiInfo
+            );
+          
+          return new RequestApiResult(
+            resultModelClass ?? instance.config.modelClassCreator,
+            json?.code || response.status,
+            json.message,
+            json.data,
+            json,
+            response.headers,
+            apiInfo
+          );
+        }
+        else {
+          throw json;
+        }
+
+      } catch (err) {
+        if (err instanceof RequestApiError) {
+          throw response;
+        }
+        //错误统一处理
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          defaultResponseDataHandlerCatch(method, req, response, null, err as any, apiInfo, response.url, reject, instance);
+        });
+      }
+    };
+  }
 }

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

@@ -7,7 +7,6 @@ export default {
     Dev: 'https://mn.wenlvti.net/api',
     Prod: 'https://mn.wenlvti.net/api',
   },
-  dynamicCategoryConfigServer: 'https://mn.wenlvti.net/app_static/minnan/data/dynamicCategoryConfig.json',
   mainBodyId: 1,
   platformId: 327,
   /**

+ 2 - 1
src/pages/article/data/CommonCategoryGlobalLoader.ts

@@ -4,6 +4,7 @@ import { showError } from "@/common/composeabe/ErrorDisplay";
 import ApiCofig from "@/common/config/ApiCofig";
 import DefaultCofig from "./DefaultCategory.json";
 import type { IHomeCommonCategoryDefine } from "./CommonCategoryDefine";
+import CommonCategoryApi from "./api/CommonCategoryApi";
 
 // 全局加载默认分类
 
@@ -29,7 +30,7 @@ export function useCommonCategoryGlobalLoader() {
         commonCategoryData.value = DefaultCofig as IHomeCommonCategoryDefine;
         return;
       }
-      const category = (await NotConfigue.get<IHomeCommonCategoryDefine>(ApiCofig.dynamicCategoryConfigServer, '加载默认分类')).data;
+      const category = (await CommonCategoryApi.getConfig()) as any as IHomeCommonCategoryDefine;
       if (category)
         commonCategoryData.value = category;
       else

+ 106 - 0
src/pages/article/data/api/CommonCategoryApi.ts

@@ -0,0 +1,106 @@
+import { UpdateServerRequestModule } from '@/api/RequestModules';
+import { DataModel } from '@imengyu/js-request-transform';
+import type { IHomeCommonCategoryDefine } from '../CommonCategoryDefine';
+
+export const CommonCategoryConfig = {
+  /**
+   * 应用id
+   */
+  appId: 2,
+  appConfigId: 3,
+  accessKey: '3hSiTCiGNiF2c3yyB6JNtA4eEf2jX8Yi2w87W2F6FYxH2W7e',
+}
+
+export interface ICommonCategoryConfigItem {
+  activeHistoryId: number;
+  data: IHomeCommonCategoryDefine;
+}
+export interface ICommonCategoryConfigHistoryItem {
+  data: IHomeCommonCategoryDefine;
+}
+
+export class CommonCategoryApi extends UpdateServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  /**
+   * 获取当前配置,有缓存,会根据激活的历史版本获取对应配置
+   * @returns 
+   */
+  async getConfig() {
+    return (await this.get<ICommonCategoryConfigHistoryItem>('/app-configuration-get', '获取配置', {
+      name: CommonCategoryConfig.appConfigId,
+      appId: CommonCategoryConfig.appId,
+      accessKey: CommonCategoryConfig.accessKey,
+    })).data!.data as IHomeCommonCategoryDefine;
+  }
+  /**
+   * 获取顶级配置,不使用缓存
+   * @returns 
+   */
+  async getConfigWithoutCache() {
+    return (await this.get<ICommonCategoryConfigItem>('/app-configuration/' + CommonCategoryConfig.appConfigId, '获取配置')).data;
+  }
+  /**
+   * 编辑配置
+   * @param json 配置数据
+   * @param saveToHistoryId 保存到历史版本id:为0时创建新历史版本;为空不保存到历史版本;为其他值时保存到指定ID历史版本
+   * @param name 历史版本名称:为空时使用默认名称
+   * @returns 
+   */
+  async editConfig(json: ICommonCategoryConfigHistoryItem['data'], name?: string, saveToHistoryId?: number) {
+    return (await this.post(`/app-configuration-edit/${CommonCategoryConfig.appConfigId}?saveToHistoryId=${saveToHistoryId}`, '编辑配置', {
+      id: CommonCategoryConfig.appConfigId,
+      name,
+      data: json,
+      accessKey: CommonCategoryConfig.accessKey,
+    })).data;
+  }
+  /**
+   * 获取配置历史版本列表
+   * @param page 页码
+   * @param pageSize 页大小
+   * @returns 
+   */
+  async getConfigHistoryList(page: number, pageSize = 10) {
+    return (await this.get<{
+      allCount: number;
+      allPage: number;
+      empty: boolean;
+      items: ICommonCategoryConfigHistoryItem[];
+      pageIndex: number;
+      pageSize: number;
+    }>(
+      `/app-configuration-history/${page}/${pageSize}?appConfigurationId=${CommonCategoryConfig.appConfigId}`,
+      '获取配置历史版本列表'
+    )).data;
+  }
+  /**
+   * 删除配置历史版本
+   * @param id 历史版本id
+   * @returns 
+   */
+  async deleteConfigHistory(id: number) {
+    return (await this.delete(`/app-configuration-history/${id}`, '删除配置历史版本', {
+      accessKey: CommonCategoryConfig.accessKey,
+      appConfigurationId: CommonCategoryConfig.appConfigId,
+    })).data;
+  }
+  /**
+   * 设置配置历史版本为活动版本
+   * @param historyId 历史版本id
+   * @returns 
+   */
+  async setActiveConfigHistory(historyId: number) {
+    return (await this.post(`/app-configuration-set-active-history`, '设置配置历史版本为活动版本', {
+      accessKey: CommonCategoryConfig.accessKey,
+      id: CommonCategoryConfig.appConfigId,
+      historyId,
+    })).data;
+  }
+
+}
+
+export default new CommonCategoryApi();

+ 229 - 10
src/pages/article/data/editor/MiniProgramEditor.vue

@@ -3,10 +3,77 @@
     <div class="miniprogram-editor">
       <div class="editor-toolbar">
         <a-space>
-          <a-button type="primary" @click="loadEditorJson">加载</a-button>
-          <a-button @click="saveEditorJson">保存</a-button>
+          <a-dropdown>
+            <a-button>
+              加载
+              <DownOutlined />
+            </a-button>
+            <template #overlay>
+              <a-menu v-if="historyList.length">
+                <a-menu-item @click="onSelectDefault">
+                  选择默认配置
+                  <a-badge v-if="currentConfig?.activeHistoryId === 0" count="激活" />
+                </a-menu-item>
+                <a-menu-item
+                  v-for="(item, index) in historyList"
+                  :key="index"
+                  @click="onSelectHistory(item.id!)"
+                >
+                  {{ item.name }}
+                  <a-badge v-if="item.id === currentConfig?.activeHistoryId" count="激活" />
+                </a-menu-item>
+              </a-menu>
+              <a-menu v-else>
+                <a-menu-item disabled>暂无历史版本</a-menu-item>
+              </a-menu>
+            </template>
+          </a-dropdown>
+          <a-dropdown-button type="primary" @click="saveEditorJson">
+            保存
+            <template #overlay>
+              <a-menu>
+                <a-menu-item @click="openSaveAsModal">另存为历史版本</a-menu-item>
+              </a-menu>
+            </template>
+          </a-dropdown-button>
+
+          <div>
+            <InfoCircleFilled />
+            当前显示配置:
+            <span>{{ currentShowConfigName }}</span>
+          </div>
+
+          <a-button v-if="currentConfig" @click="setActiveHistory">
+            {{ currentConfig.activeHistoryId === currentHistoryId ? '已经是激活版本' : '设置为激活版本' }}
+          </a-button>
+          <a-button v-if="currentConfig && currentHistoryId !== 0" danger @click="deleteHistory">删除历史版本</a-button>
         </a-space>
+        <a-button @click="exportToJsonFile">
+          导出为JSON文件
+          <DownloadOutlined />
+        </a-button>
       </div>
+
+      <!-- 另存为历史版本弹框 -->
+      <a-modal
+        v-model:open="saveAsModalVisible"
+        title="另存为历史版本"
+        ok-text="保存"
+        cancel-text="取消"
+        :confirm-loading="saveAsLoading"
+        @ok="confirmSaveAs"
+      >
+        <a-form layout="vertical" class="save-as-form">
+          <a-form-item label="版本名称">
+            <a-input
+              v-model:value="saveAsVersionName"
+              placeholder="请输入版本名称"
+              allow-clear
+            />
+          </a-form-item>
+        </a-form>
+      </a-modal>
+
       <div class="editor-body">
         <!-- 左一:页面列表 -->
         <div class="panel panel-pages">
@@ -56,29 +123,178 @@
 </template>
 
 <script setup lang="ts">
-import { computed, provide, ref } from 'vue';
+import { computed, onMounted, provide, ref } from 'vue';
 import { ObjectUtils } from '@imengyu/imengyu-utils';
-import type { IHomeCommonCategoryDefine } from '../article/data/CommonCategoryDefine';
-import DefaultEditorJson from '../article/data/DefaultCategory.json';
+import { DownOutlined, DownloadOutlined, InfoCircleFilled } from '@ant-design/icons-vue';
+import { message, Modal } from 'ant-design-vue';
+import type { IHomeCommonCategoryDefine } from '../CommonCategoryDefine';
+import DefaultEditorJson from '../DefaultCategory.json';
 import PropsEditorTree from './subpart/PropsEditorTree.vue';
 import EditorPreview from './subpart/EditorPreview.vue';
 import zhCN from 'ant-design-vue/es/locale/zh_CN';
+import CommonCategoryApi, { type ICommonCategoryConfigItem } from '../api/CommonCategoryApi';
+
+/** 历史版本列表项(接口返回的 items 元素) */
+interface IHistoryListItem {
+  id?: number;
+  data?: IHomeCommonCategoryDefine;
+  createTime?: string;
+  [key: string]: any;
+}
 
 const currentEditorJson = ref<IHomeCommonCategoryDefine>(DefaultEditorJson as IHomeCommonCategoryDefine);
 const selectedPage = ref<(IHomeCommonCategoryDefine['page'][0]) | null>(null);
 const pageList = computed(() => currentEditorJson.value.page || []);
+const historyList = ref<IHistoryListItem[]>([]);
+const currentConfig = ref<ICommonCategoryConfigItem>();
+const currentHistoryId = ref<number>(0);
+const currentShowConfigName = computed(() => {
+  if (currentHistoryId.value === 0)
+    return '默认配置';
+  else
+    return historyList.value.find(item => item.id === currentHistoryId.value)?.name ?? '未知';
+});
+
+const saveAsModalVisible = ref(false);
+const saveAsVersionName = ref('');
+const saveAsLoading = ref(false);
 
 provide('pageList', pageList);
 
-async function loadEditorJson() {
-  // 临时使用本地文件,后续接入后端接口
-  currentEditorJson.value = ObjectUtils.clone(DefaultEditorJson) as IHomeCommonCategoryDefine;
+async function loadEditorJson(selectDefault = false) {
+  try {
+    //加载基础配置
+    currentConfig.value = await CommonCategoryApi.getConfigWithoutCache();
+    if (!currentConfig.value)
+      throw new Error('加载基础配置失败');
+    if (selectDefault) 
+      currentHistoryId.value = currentConfig.value.activeHistoryId;
+    //根据activeHistoryId选择当前激活的历史版本
+    if (currentHistoryId.value > 0) {
+      const activeHistory = historyList.value.find(item => item.id === currentHistoryId.value);
+      if (activeHistory) 
+        currentEditorJson.value = ObjectUtils.clone(activeHistory.data!) as IHomeCommonCategoryDefine;
+      else
+        throw new Error('当前激活的历史版本不存在');
+    } else {
+      currentEditorJson.value = ObjectUtils.clone(currentConfig.value.data) as IHomeCommonCategoryDefine;
+    }
+    message.success('加载分类成功');
+  } catch (error) {
+    Modal.error({
+      title: '加载分类失败',
+      content: '' + error,
+    });
+  }
+}
+async function loadEditorJsonHistorys() {
+  try {
+    const res = await CommonCategoryApi.getConfigHistoryList(1, 10);
+    const items = res?.items ?? [];
+    historyList.value = Array.isArray(items) ? items : [];
+  } catch (error) {
+    historyList.value = [];
+    Modal.error({
+      title: '加载历史版本列表失败',
+      content: '' + error,
+    });
+  }
 }
 
+function onSelectDefault() {
+  currentHistoryId.value = 0;
+  loadEditorJson(true);
+}
+function onSelectHistory(id: number) {
+  currentHistoryId.value = id;
+  loadEditorJson(false);
+}
+
+/** 默认保存:根据 currentHistoryId 保存到默认配置或指定历史版本 */
 async function saveEditorJson() {
-  // 临时测试,后续接入后端接口
-  console.log(currentEditorJson.value);
+  try {
+    const saveToHistoryId = currentHistoryId.value === 0 ? undefined : currentHistoryId.value;
+    await CommonCategoryApi.editConfig(
+      currentEditorJson.value,
+      undefined,
+      saveToHistoryId
+    );
+    message.success('保存成功');
+  } catch (error) {
+    Modal.error({
+      title: '保存失败',
+      content: '' + error,
+    });
+  }
+}
+
+function openSaveAsModal() {
+  saveAsVersionName.value = '';
+  saveAsModalVisible.value = true;
+}
+
+async function confirmSaveAs() {
+  const name = saveAsVersionName.value?.trim();
+  if (!name) {
+    message.warning('请输入版本名称');
+    return;
+  }
+  saveAsLoading.value = true;
+  try {
+    await CommonCategoryApi.editConfig(
+      currentEditorJson.value,
+      name,
+      0
+    );
+    message.success('已另存为历史版本');
+    saveAsModalVisible.value = false;
+    await loadEditorJsonHistorys();
+  } catch (error) {
+    Modal.error({
+      title: '另存为失败',
+      content: '' + error,
+    });
+  } finally {
+    saveAsLoading.value = false;
+  }
+}
+
+async function setActiveHistory() {
+  await CommonCategoryApi.setActiveConfigHistory(currentHistoryId.value);
+  message.success('设置为激活版本成功');
 }
+async function deleteHistory() {
+  Modal.confirm({
+    title: '删除历史版本',
+    content: '确定要删除该历史版本吗?',
+    onOk: async () => {
+      await CommonCategoryApi.deleteConfigHistory(currentHistoryId.value);
+      message.success('删除历史版本成功');
+      await loadEditorJsonHistorys();
+      await loadEditorJson(true);
+    },
+  });
+}
+
+function exportToJsonFile() {
+  const json = JSON.stringify(currentEditorJson.value);
+  const blob = new Blob([json], { type: 'application/json' });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = 'editor.json';
+  a.click();
+}
+
+onMounted(async () => {
+  await loadEditorJsonHistorys();
+  await loadEditorJson(true);
+  window.addEventListener('beforeunload', function(e) {
+    const message = '你还有未保存的内容,确定要离开吗?';
+    e.returnValue = message;
+    return message;
+  });
+})
 </script>
 
 <style scoped>
@@ -92,6 +308,9 @@ async function saveEditorJson() {
   padding: 8px 16px;
   background: #fff;
   border-bottom: 1px solid #eee;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 }
 .editor-body {
   flex: 1;

+ 1 - 0
src/pages/article/data/editor/editors/HomePropsEditor.vue

@@ -50,6 +50,7 @@ import type { IHomeCommonCategoryHomeDefine, IHomeCommonCategoryListTabNestCateg
 import LinkPathEditor from '../components/LinkPathEditor.vue';
 import IconEditor from '../components/IconEditor.vue';
 import DynamicDataEditor from '../components/DynamicDataEditor.vue';
+import NestCategoryEditor from '../subpart/NestCategoryEditor.vue';
 
 const props = defineProps<{
   props: IHomeCommonCategoryHomeDefine['props'];

+ 64 - 8
src/pages/article/data/editor/subpart/NestCategoryEditor.vue

@@ -3,6 +3,13 @@
     <div v-for="(cat, i) in categorys" :key="i" class="nested-item">
       <a-collapse>
         <a-collapse-panel :key="i" :header="cat.text || `子分类 ${i + 1}`">
+          <template #extra>
+            <div class="nest-category-editor-buttons">
+              <ArrowUpOutlined title="上移" @click.stop="moveUp(i)" />
+              <ArrowDownOutlined title="下移" @click.stop="moveDown(i)" />
+              <CopyOutlined title="复制" @click.stop="copy(i)" />
+            </div>
+          </template>
           <a-form :labelCol="{ span: 6 }" size="small">
             <a-form-item label="显示标题">
               <a-checkbox 
@@ -57,7 +64,10 @@
         </a-collapse-panel>
       </a-collapse>
     </div>
-    <a-button type="dashed" block size="small" @click="add">+ 添加子分类</a-button>
+    <div class="nest-category-editor-footer">
+      <a-button class="large" type="dashed" block size="small" @click="add">+ 添加子分类</a-button>
+      <a-button class="small" type="dashed" block size="small" @click="paste">粘贴</a-button>
+    </div>
   </div>
 </template>
 
@@ -69,6 +79,8 @@ import DynamicDataEditor from '../components/DynamicDataEditor.vue';
 import DataSolveEditor from '../components/DataSolveEditor.vue';
 import { CommonCategoryBlockType } from '../../CommonCategoryBlocks';
 import ItemTypeEditor from '../components/ItemTypeEditor.vue';
+import { ArrowUpOutlined, ArrowDownOutlined, CopyOutlined } from '@ant-design/icons-vue';
+import { ArrayUtils } from '@imengyu/imengyu-utils';
 
 const props = defineProps<{
   categorys: IHomeCommonCategoryListTabNestCategoryItemDefine[];
@@ -83,13 +95,40 @@ function add() {
     data: undefined as unknown as IHomeCommonCategoryDynamicData,
   });
 }
-
 function remove(i: number) {
   props.categorys.splice(i, 1);
 }
+async function paste() {
+  const data = await uni.getClipboardData();
+  if (typeof data === 'string') {
+    const json = JSON.parse(data);
+    if (json.type === 'Copy:NestCategoryItem') {
+      props.categorys.push(json.data);
+    }
+  }
+}
+
+function moveUp(i: number) {
+  ArrayUtils.upData(props.categorys, i);
+}
+function moveDown(i: number) {
+  ArrayUtils.downData(props.categorys, i);
+}
+function copy(i: number) {
+  uni.setClipboardData({
+    data: JSON.stringify({
+      type: 'Copy:NestCategoryItem',
+      data: props.categorys[i]
+    }),
+  });
+  uni.showToast({
+    title: '复制成功',
+    icon: 'success',
+  });
+}
 </script>
 
-<style scoped>
+<style lang="scss" scoped>
 .nest-category-editor {
   font-size: 12px;
 }
@@ -98,12 +137,29 @@ function remove(i: number) {
   padding: 8px;
   background: #fafafa;
   border-radius: 4px;
+
+  :deep(.ant-collapse) {
+    border: none;
+    background: transparent;
+  }
+  :deep(.ant-collapse-item) {
+    border: none;
+  }
 }
-.nested-item :deep(.ant-collapse) {
-  border: none;
-  background: transparent;
+.nest-category-editor-buttons {
+  display: flex;
+  flex-direction: row;
+  gap: 8px;
 }
-.nested-item :deep(.ant-collapse-item) {
-  border: none;
+.nest-category-editor-footer {
+  display: flex;
+  flex-direction: row;
+
+  .small {
+    flex: 1;
+  }
+  .large {
+    flex: 4
+  }
 }
 </style>