ソースを参照

📦 采编富文本编辑器接入AI功能

快乐的梦鱼 13 時間 前
コミット
0a0277930c
共有30 個のファイルを変更した729 個の追加280 個の削除を含む
  1. 0 2
      src/api/map/AMapApi.ts
  2. 72 37
      src/common/components/form/RichTextEditor.vue
  3. 3 2
      src/common/components/upload/AliOssUploadCo.ts
  4. 15 13
      src/components/form/Uploader.vue
  5. 8 7
      src/pages.json
  6. 0 114
      src/pages/dig/editor/editor.vue
  7. 45 5
      src/pages/dig/forms/data/building.ts
  8. 28 6
      src/pages/dig/forms/data/common.ts
  9. 86 8
      src/pages/dig/forms/data/element.ts
  10. 107 10
      src/pages/dig/forms/data/environment.ts
  11. 22 2
      src/pages/dig/forms/data/food.ts
  12. 56 6
      src/pages/dig/forms/data/overview.ts
  13. 43 4
      src/pages/dig/forms/data/relic.ts
  14. 0 0
      src/pages/home/chat/components/sp-editor/changelog.md
  15. 0 0
      src/pages/home/chat/components/sp-editor/components/sp-editor/color-picker.vue
  16. 0 0
      src/pages/home/chat/components/sp-editor/components/sp-editor/fab-tool.vue
  17. 0 0
      src/pages/home/chat/components/sp-editor/components/sp-editor/link-edit.vue
  18. 0 0
      src/pages/home/chat/components/sp-editor/components/sp-editor/sp-editor.vue
  19. 0 0
      src/pages/home/chat/components/sp-editor/icons/custom-icon.css
  20. 0 0
      src/pages/home/chat/components/sp-editor/icons/editor-icon.css
  21. 0 0
      src/pages/home/chat/components/sp-editor/package.json
  22. 0 0
      src/pages/home/chat/components/sp-editor/readme.md
  23. 0 0
      src/pages/home/chat/components/sp-editor/static/image-resize.min.js
  24. 0 0
      src/pages/home/chat/components/sp-editor/static/quill.min.js
  25. 0 0
      src/pages/home/chat/components/sp-editor/utils/index.js
  26. 7 17
      src/pages/home/chat/dependent/post/components/agent.vue
  27. 2 2
      src/pages/home/chat/dependent/post/components/tbutton.vue
  28. 3 32
      src/pages/home/chat/dependent/post/composables/agentTools.ts
  29. 203 0
      src/pages/home/chat/dependent/post/editor.vue
  30. 29 13
      src/pages/home/chat/dependent/post/publish.vue

+ 0 - 2
src/api/map/AMapApi.ts

@@ -44,8 +44,6 @@ export class AMapApi extends AMapServerRequestModule<DataModel> {
       location: `${lng},${lat}`,
       ...querys,
     }));
-
-    console.log(data);
     return data.raw.regeocode.addressComponent as {
       country: string,
       province: string,

+ 72 - 37
src/common/components/form/RichTextEditor.vue

@@ -14,6 +14,8 @@
 </template>
 
 <script setup lang="ts">
+import { computed, type PropType } from 'vue';
+import { propGetThemeVar, useTheme } from '@/components/theme/ThemeDefine';
 import { onPageShow } from '@dcloudio/uni-app';
 import { navTo } from '@/components/utils/PageAction';
 import Parse from '@/components/display/parse/Parse.vue';
@@ -21,34 +23,41 @@ import Button from '@/components/basic/Button.vue';
 import Text from '@/components/basic/Text.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
-import { propGetThemeVar, useTheme } from '@/components/theme/ThemeDefine';
-import { computed } from 'vue';
 
-const props = defineProps({	
-  modelValue: { 
-    type: String,
-    default: null 
-  },
-  maxLength: {
-    type: Number,
-    default: -1,
-  },
-  placeholder: {
-    type: String,
-    default: '未编写内容,点击编写',
+export interface SpEditorProps {
+  modelValue: string;
+  maxLength: number;
+  placeholder: string;
+  disabled: boolean;
+  readonly: boolean;
+  enableAi: boolean;
+  extraProps: {
+    get: () => {
+      title: string,
+      images: string[],
+    },
+    update: (extraProps: {
+      title: string,
+      images: string[],
+    }) => void,
+    storageInfo?: {
+      villageId: number,
+      uploadPath: string,
+    },
   },
-  disabled: {
-    type: Boolean,
-    default: false,
-  },
-  readonly: {
-    type: Boolean,
-    default: false,
-  },
-  maskColor: {
-    type: String,
-    default: () => propGetThemeVar('RichTextEditorMaskColor', 'white'),
-  }
+  getExtraAiInfo: () => string[]|Promise<string[]>,
+  maskColor: string;
+}
+
+const props = withDefaults(defineProps<SpEditorProps>(), {
+  maxLength: -1,
+  placeholder: '未编写内容,点击编写',
+  extraProps: () => ({
+    get: () => ({ title: '', images: [] }),
+    update: () => {},
+    storageInfo: undefined,
+  }),
+  maskColor: () => propGetThemeVar('RichTextEditorMaskColor', '#fcf4e4'),
 })
 const emit = defineEmits(['update:modelValue'])
 
@@ -61,32 +70,58 @@ const maskStyle = computed(() => ({
 }));
 
 function preview() {
+  const extraProps = props.extraProps.get();
   uni.setStorage({
     key: 'editorContent',
-    data: props.modelValue,
+    data: {
+      title: extraProps.title,
+      content: props.modelValue,
+      images: extraProps.images,
+    },
     success: () => navTo('/pages/dig/editor/preview'),
   })
 }
-function edit() {
+async function edit() {
   editorOpened = true;
+  const extraProps = props.extraProps.get();
+  const extraAiInfo = await props.getExtraAiInfo();
+
   uni.setStorage({
-    key: 'editorMaxLength',
-    data: props.maxLength,
+    key: 'editorContent',
+    data: {
+      title: extraProps.title,
+      content: props.modelValue,
+      images: extraProps.images,
+    },
+    success: () => navTo('/pages/home/chat/dependent/post/editor', {  
+      maxLength: props.maxLength,
+      enableAi: props.enableAi,
+      extraAiInfo: extraAiInfo.join(','),
+      uploadSubpath: props.extraProps.storageInfo?.uploadPath,
+      villageId: props.extraProps.storageInfo?.villageId,
+    }),
   })
-  uni.setStorage({
+}
+function doSaveContent() {
+  uni.getStorage({
     key: 'editorContent',
-    data: props.modelValue,
-    success: () => navTo('/pages/dig/editor/editor'),
+    success: (success) => {
+      const content = success.data.content;
+      const images = success.data.images;
+      const title = success.data.title;
+      emit('update:modelValue', content)
+      props.extraProps.update({
+        title: title,
+        images: images,
+      })
+    }
   })
 }
 
 onPageShow(() => {
   if (editorOpened) {
     editorOpened = false;
-    uni.getStorage({
-      key: 'editorContent',
-      success: (success) => emit('update:modelValue', success.data),
-    })
+    doSaveContent();
   }
 })
 </script>

+ 3 - 2
src/common/components/upload/AliOssUploadCo.ts

@@ -2,6 +2,7 @@ import { Base64Utils, RandomUtils, StringUtils } from "@imengyu/imengyu-utils";
 import { hmacSha1Base64 } from "./hmac";
 import type { UploaderAction } from "@/components/form/Uploader";
 import LightVillageApi from "@/api/light/LightVillageApi";
+import type { Ref } from "vue";
 
 const client = {
   region: 'oss-cn-shenzhen',
@@ -77,7 +78,7 @@ function uploadOSS(uploadPath: string, filePath: string, cancelHandler: { cancel
   })
 }
 
-export function useAliOssUploadCo(subPath: string, storageManage?: {
+export function useAliOssUploadCo(subPath: string|Ref<string>, storageManage?: {
   getVillageId: () => number,
   overflow: () => void,
 }) {
@@ -107,7 +108,7 @@ export function useAliOssUploadCo(subPath: string, storageManage?: {
 
     function doUpload() {
       const name = StringUtils.path.getFileName(action.item.filePath);
-      const uploadPath = `${subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(name)}`;  
+      const uploadPath = `${typeof subPath === 'object' ? subPath.value : subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(name)}`;  
       uploadOSS(uploadPath, action.item.filePath, cancelHandler,(progress) => {
         action.onProgress?.(progress)
       }).then((res) => {

+ 15 - 13
src/components/form/Uploader.vue

@@ -244,7 +244,7 @@ export interface UploaderInstance {
    * * 单选情况下会替换已上传列表中的条目。
    * * 多选情况下会添加到末尾。
    */
-  addItemAndUpload: (item: UploaderItem) => void;
+  addItemAndUpload: (item: UploaderItem) => Promise<UploaderItem>;
   /**
    * 开始手动上传所有条目
    */
@@ -252,7 +252,7 @@ export interface UploaderInstance {
   /**
    * 开始手动上传指定条目
    */
-  startUpload: (item: UploaderItem) => Promise<void>;
+  startUpload: (item: UploaderItem) => Promise<UploaderItem>;
   /**
    * 获取现在是否全部条目处于已上传并且完成状态
    */
@@ -515,13 +515,13 @@ function isImagePath(path: string) {
 
 
 //开始上传条目
-function startUploadItem(item: UploaderItem) {
+async function startUploadItem(item: UploaderItem) {
   if (item.state === 'uploading')
-    return;
-  return new Promise<void>((resolve, reject) => {
+    return item;
+  return await new Promise<UploaderItem>((resolve, reject) => {
     if (item.state === 'success') {
-      resolve();
-      return;
+      resolve(item);
+      return item;
     }
     LogUtils.printLog(TAG, 'message', `调用上传文件 ${item.filePath}`);
 
@@ -550,7 +550,7 @@ function startUploadItem(item: UploaderItem) {
         if (result.previewUrl)
           item.previewPath = result.previewUrl;
         updateListItem(item);
-        resolve();
+        resolve(item);
         LogUtils.printLog(TAG, 'success', `上传文件 ${item.filePath} 成功,上传路径:${result.uploadedUrl}`);
       },
       onProgress(precent) {
@@ -567,14 +567,16 @@ function startUploadItem(item: UploaderItem) {
   });
 }
 //开始上传条目
-async function startUploadMulitItem(items: UploaderItem[]) : Promise<void> {
+async function startUploadMulitItem(items: UploaderItem[]) {
   if (props.uploadQueueMode === 'sequential')
     await items.reduce((promiseChain, currentItem) =>
-      promiseChain.then(() => startUploadItem(currentItem)).catch(() => startUploadItem(currentItem)),
-      Promise.resolve()
+      promiseChain
+        .then(() => startUploadItem(currentItem))
+        .catch(() => startUploadItem(currentItem)),
+      Promise.resolve() as Promise<any>
     );
   else
-    await Promise.all(items.map(item => startUploadItem(item))) as unknown as Promise<void>;
+    await Promise.all(items.map(item => startUploadItem(item)));
   emit('allUploaded');
 }
 
@@ -612,7 +614,7 @@ defineExpose<UploaderInstance>({
     if (props.maxUploadCount === 1 && currentUpladList.value.length > 0)
       deleteListItem(currentUpladList.value[0]);
     currentUpladList.value.push(item);
-    startUploadItem(item);
+    return startUploadItem(item);
   },
   pick() {
     onUploadPress();

+ 8 - 7
src/pages.json

@@ -351,13 +351,6 @@
       "root": "pages/dig",
       "pages": [
         {
-          "path": "editor/editor",
-          "style": {
-            "navigationBarTitleText": "编辑文章",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
           "path": "editor/preview",
           "style": {
             "navigationBarTitleText": "预览文章",
@@ -487,6 +480,14 @@
             "navigationStyle": "custom",
             "enablePullDownRefresh": false
           }
+        },
+        {
+          "path": "dependent/post/editor",
+          "style": {
+            "navigationBarTitleText": "AI智能编辑器",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": false
+          }
         }
       ]
     },

+ 0 - 114
src/pages/dig/editor/editor.vue

@@ -1,114 +0,0 @@
-<template>
-  <FlexCol height="100vh">
-    <sp-editor
-      :toolbar-config="{
-        excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck'],
-        iconSize: '18px'
-      }"
-      :maxlength="maxLength"
-      @init="initEditor"
-      @input="inputOver"
-      @upinImage="upinImage"
-      @overMax="overMax"
-    />
-    
-    <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>
-    </FlexRow>
-    <XBarSpace />
-  </FlexCol>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue';
-import { confirm } from '@/components/utils/DialogAction';
-import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
-import { showError } from '@/common/composeabe/ErrorDisplay';
-import spEditor from '../components/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);
-
-function cancel() {
-  confirm({
-    title: '提示',
-    content: '是否放弃编辑?',
-  }).then((res) => {
-    if (res)
-      back();
-  })
-}
-function save() {
-  uni.setStorage({
-    key: 'editorContent',
-    data: currentContent,
-    success: () => backAndCallOnPageBack('editor', {}),
-    fail: (e) => showError(e),
-  })
-}
-
-let currentContent = '';
-
-/**
-* 获取输入内容
-*/
-function inputOver(e: { html: string; text: string; }) {
-  // 可以在此处获取到编辑器已编辑的内容
-  currentContent = e.html;
-}
-/**
- * 超出最大内容限制
- * @param {Object} e {html,text} 内容的html文本,和text文本
- */
-function overMax(e: { html: string; text: string; }) {
-  // 若设置了最大字数限制,可在此处触发超出限制的回调
-  console.log('==== overMax :', e)
-}
-function initEditor(editor: any) {
-  uni.getStorage({
-    key: 'editorMaxLength',
-    success: (success) => {
-      maxLength.value = parseInt(success.data);
-    },
-  })
-  uni.getStorage({
-    key: 'editorContent',
-    success: (success) => {
-      editor.setContents({
-        html: success.data
-      })
-    },
-  })
-}
-/**
- * 直接运行示例工程插入图片无法正常显示的看这里
- * 因为插件默认采用云端存储图片的方式
- * 以$emit('upinImage', tempFiles, this.editorCtx)的方式回调
- * @param {Object} tempFiles
- * @param {Object} editorCtx
- */
-function upinImage(tempFiles: any, editorCtx: any) {
-  let path;
-  // #ifdef MP-WEIXIN
-  path = tempFiles[0].tempFilePath; 
-  // #endif
-  // #ifndef MP-WEIXIN
-  path = tempFiles[0].path;
-  // #endif
-
-  console.log('==== upinImage :', path);
-  
-  CommonContent.uploadFile(path, 'image', 'file').then((res) => {
-    editorCtx.insertImage({
-      src: res.fullurl,
-      width: '80%',
-      success: function () {}
-    })
-  }).catch((e) => showError(e));
-}
-</script>

+ 45 - 5
src/pages/dig/forms/data/building.ts

@@ -6,6 +6,7 @@ import type { SingleForm } from "../forms";
 import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import { useAliOssUploadCo } from "@/common/components/upload/AliOssUploadCo";
 import { villageCommonContent } from "./common";
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
 
 export function villageInfoBuildingForm(title: string) : SingleForm  {
   return [VillageBulidingInfo, (r) => ({
@@ -110,8 +111,27 @@ export function villageInfoBuildingForm(title: string) : SingleForm  {
             additionalProps: {
               placeholder: '请输入建筑中的故事',
               maxLength: 5000,
-              showWordLimit: true, 
-            },
+              enableAi: true,
+              extraProps: {
+                get() {
+                  return {
+                    title: r.value.getValueByPath('name'),
+                    images: r.value.getValueByPath('images'),
+                  }
+                },
+                update(extraProps) {
+                  r.value.setValueByPath('images', extraProps.images);
+                  r.value.setValueByPath('name', extraProps.title);
+                },
+                storageInfo: {
+                  villageId: r.value.getGlobalParams().villageId,
+                  uploadPath: 'xiangyuan/building',
+                },
+              },
+              getExtraAiInfo() {
+                return [ '用户正在栏目'+title+',编辑建筑 '+ r.value.getValueByPath('name') +' 建筑中的故事']
+              },
+            } as SpEditorProps,
           },
           {
             label: '功能特点', 
@@ -121,14 +141,34 @@ export function villageInfoBuildingForm(title: string) : SingleForm  {
             additionalProps: {
               placeholder: '请输入功能特点',
               maxLength: 5000,
-              showWordLimit: true, 
-            },
+              enableAi: true,
+              extraProps: {
+                get() {
+                  return {
+                    title: r.value.getValueByPath('name'),
+                    images: r.value.getValueByPath('images'),
+                  }
+                },
+                update(extraProps) {
+                  r.value.setValueByPath('images', extraProps.images);
+                  r.value.setValueByPath('name', extraProps.title);
+                },
+                storageInfo: {
+                  villageId: r.value.getGlobalParams().villageId,
+                  uploadPath: 'xiangyuan/building',
+                },
+              },
+              getExtraAiInfo() {
+                return [ '用户正在栏目'+title+',编辑建筑 '+ r.value.getValueByPath('name') +' 功能特点']
+              },
+            } as SpEditorProps,
             rules:  [] 
           },
           ...villageCommonContent(r, {
             title: title,
             showContent: false,
             showTitle: false,
+            uploadPath: 'xiangyuan/building',
           }).formItems
         ]
       },
@@ -498,13 +538,13 @@ export const villageInfoDistributionForm : SingleForm = [CommonInfoModel, (r) =>
       additionalProps: {
         placeholder: '请输入营造智慧',
         maxLength: 5000,
-        showWordLimit: true, 
       },
     }, 
     ...villageCommonContent(r, {
       title: '建筑分布',
       showContent: false,
       showTitle: false,
+      uploadPath: 'xiangyuan/building',
     }).formItems
   ] 
 }), { title: '建筑分布', typeName: '', }]

+ 28 - 6
src/pages/dig/forms/data/common.ts

@@ -1,17 +1,19 @@
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
 import { useAliOssUploadCo } from "@/common/components/upload/AliOssUploadCo";
 import type { IDynamicFormOptions, IDynamicFormRef } from "@/components/dynamic";
 import type { PickerIdFieldProps } from "@/components/dynamic/wrappers/PickerIdField";
-import type { FieldProps } from "@/components/form/Field.vue";
 import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { Ref } from "vue";
 
 export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: { 
   title: string,
   showTitle?: boolean,
+  titleKey?: string,
   showContent?: boolean,
   noType?: boolean,
   noCloseExtra?: boolean,
   contentKey?: string,
+  uploadPath?: string,
 } = {
   title: '文章',
   showTitle: true,
@@ -45,7 +47,27 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             additionalProps: {
               placeholder: '请输入介绍内容正文',
               maxLength: 5000,
-            },
+              enableAi: true,
+              extraProps: {
+                get() {
+                  return {
+                    title: ref.value.getValueByPath(options.titleKey || 'name'),
+                    images: ref.value.getValueByPath('images'),
+                  }
+                },
+                update(extraProps) {
+                  ref.value.setValueByPath('images', extraProps.images);
+                  ref.value.setValueByPath(options.titleKey || 'name', extraProps.title);
+                },
+                storageInfo: {
+                  villageId: ref.value.getGlobalParams().villageId,
+                  uploadPath: options.uploadPath,
+                },
+              },
+              getExtraAiInfo() {
+                return [ '用户正在栏目' + options.title + ',编辑 ' + ref.value.getValueByPath(options.titleKey || 'name') +' 的介绍内容']
+              },
+            } as SpEditorProps,
             rules: [{
               required: true,
               message: '请输入介绍内容',
@@ -95,7 +117,7 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/common', {
+              upload: useAliOssUploadCo((options.uploadPath || 'xiangyuan') + '/images', {
                 getVillageId: () => ref.value.getGlobalParams().villageId,
                 overflow: () => ref.value.emitMessage('storageOverflow'),
               }),
@@ -113,7 +135,7 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/video', {
+              upload: useAliOssUploadCo((options.uploadPath || 'xiangyuan') + '/video', {
                 getVillageId: () => ref.value.getGlobalParams().villageId,
                 overflow: () => ref.value.emitMessage('storageOverflow'),
               }),
@@ -148,7 +170,7 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/archives', {
+              upload: useAliOssUploadCo((options.uploadPath || 'xiangyuan') + '/archives', {
                 getVillageId: () => ref.value.getGlobalParams().villageId,
                 overflow: () => ref.value.emitMessage('storageOverflow'),
               }),
@@ -167,7 +189,7 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/annex', {
+              upload: useAliOssUploadCo((options.uploadPath || 'xiangyuan') + '/annex', {
                 getVillageId: () => ref.value.getGlobalParams().villageId,
                 overflow: () => ref.value.emitMessage('storageOverflow'),
               }),

+ 86 - 8
src/pages/dig/forms/data/element.ts

@@ -6,6 +6,7 @@ import type { StepperProps } from "@/components/form/Stepper.vue";
 import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { SingleForm } from "../forms";
 import { villageCommonContent } from "./common";
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
 
 export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
   formItems: [
@@ -91,8 +92,27 @@ export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
           additionalProps: {
             placeholder: '请输入环境特点',
             maxLength: 1000,
-            showWordLimit: true, 
-          } as FieldProps,
+            enableAi: true,
+            extraProps: {
+              get() {
+                return {
+                  title: r.value.getValueByPath('name'),
+                  images: r.value.getValueByPath('images'),
+                }
+              },
+              update(extraProps) {
+                r.value.setValueByPath('images', extraProps.images);
+                r.value.setValueByPath('name', extraProps.title);
+              },
+              storageInfo: {
+                villageId: r.value.getGlobalParams().villageId,
+                uploadPath: 'xiangyuan/element',
+              },
+            },
+            getExtraAiInfo() {
+              return [ '用户正在栏目环境要素,编辑 '+ r.value.getValueByPath('name') +' 环境特点']
+            },
+          } as SpEditorProps,
           rules:  [] 
         },
         {
@@ -103,8 +123,27 @@ export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
           additionalProps: {
             placeholder: '请输入功能特点',
             maxLength: 1000,
-            showWordLimit: true, 
-          } as FieldProps,
+            enableAi: true,
+            extraProps: {
+              get() {
+                return {
+                  title: r.value.getValueByPath('name'),
+                  images: r.value.getValueByPath('images'),
+                }
+              },
+              update(extraProps) {
+                r.value.setValueByPath('images', extraProps.images);
+                r.value.setValueByPath('name', extraProps.title);
+              },
+              storageInfo: {
+                villageId: r.value.getGlobalParams().villageId,
+                uploadPath: 'xiangyuan/element',
+              },
+            },
+            getExtraAiInfo() {
+              return [ '用户正在栏目环境要素,编辑 '+ r.value.getValueByPath('name') +' 功能特点']
+            },
+          } as SpEditorProps,
           rules:  [] 
         },
         {
@@ -115,8 +154,27 @@ export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
           additionalProps: {
             placeholder: '请输入保存状况',
             maxLength: 1000,
-            showWordLimit: true, 
-          } as FieldProps,
+            enableAi: true,
+            extraProps: {
+              get() {
+                return {
+                  title: r.value.getValueByPath('name'),
+                  images: r.value.getValueByPath('images'),
+                }
+              },
+              update(extraProps) {
+                r.value.setValueByPath('images', extraProps.images);
+                r.value.setValueByPath('name', extraProps.title);
+              },
+              storageInfo: {
+                villageId: r.value.getGlobalParams().villageId,
+                uploadPath: 'xiangyuan/element',
+              },
+            },
+            getExtraAiInfo() {
+              return [ '用户正在栏目环境要素,编辑 '+ r.value.getValueByPath('name') +' 保存状况']
+            },
+          } as SpEditorProps,
           rules:  [] 
         },
         {
@@ -127,14 +185,34 @@ export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
           additionalProps: {
             placeholder: '请输入文化故事',
             maxLength: 5000,
-            showWordLimit: true, 
-          } as FieldProps,
+            enableAi: true,
+            extraProps: {
+              get() {
+                return {
+                  title: r.value.getValueByPath('name'),
+                  images: r.value.getValueByPath('images'),
+                }
+              },
+              update(extraProps) {
+                r.value.setValueByPath('images', extraProps.images);
+                r.value.setValueByPath('name', extraProps.title);
+              },
+              storageInfo: {
+                villageId: r.value.getGlobalParams().villageId,
+                uploadPath: 'xiangyuan/element',
+              },
+            },
+            getExtraAiInfo() {
+              return [ '用户正在栏目环境要素,编辑 '+ r.value.getValueByPath('name') +' 文化故事']
+            },
+          } as SpEditorProps,
           rules:  [] 
         },
         ...villageCommonContent(r, {
           title: '环境要素',
           showContent: false,
           showTitle: false,
+          uploadPath: 'xiangyuan/element',
         }).formItems
       ]
     },

+ 107 - 10
src/pages/dig/forms/data/environment.ts

@@ -1,6 +1,7 @@
 import { CommonInfoModel } from "@/api/inhert/VillageInfoApi";
 import type { SingleForm } from "../forms";
 import { villageCommonContent } from "./common";
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
 
 export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => ({
   formItems: [
@@ -25,8 +26,27 @@ export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => (
       additionalProps: {
         placeholder: '请输入自然环境',
         maxLength: 5000,
-        showWordLimit: true, 
-      },
+        enableAi: true,
+        extraProps: {
+          get() {
+            return {
+              title: r.value.getValueByPath('name'),
+              images: r.value.getValueByPath('images'),
+            }
+          },
+          update(extraProps) {
+            r.value.setValueByPath('images', extraProps.images);
+            r.value.setValueByPath('name', extraProps.title);
+          },
+          storageInfo: {
+            villageId: r.value.getGlobalParams().villageId,
+            uploadPath: 'xiangyuan/environment',
+          },
+        },
+        getExtraAiInfo() {
+          return [ '用户正在栏目环境格局,编辑 '+ r.value.getValueByPath('name') +' 自然环境特点']
+        },
+      } as SpEditorProps,
       rules:  [{
         required: true,
         message: '请输入自然环境',
@@ -40,8 +60,27 @@ export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => (
       additionalProps: {
         placeholder: '请输入选址',
         maxLength: 5000,
-        showWordLimit: true, 
-      },
+        enableAi: true,
+        extraProps: {
+          get() {
+            return {
+              title: r.value.getValueByPath('name'),
+              images: r.value.getValueByPath('images'),
+            }
+          },
+          update(extraProps) {
+            r.value.setValueByPath('images', extraProps.images);
+            r.value.setValueByPath('name', extraProps.title);
+          },
+          storageInfo: {
+            villageId: r.value.getGlobalParams().villageId,
+            uploadPath: 'xiangyuan/environment',
+          },
+        },
+        getExtraAiInfo() {
+          return [ '用户正在栏目环境格局,编辑 '+ r.value.getValueByPath('name') +' 自然环境特点']
+        },
+      } as SpEditorProps,
     }, 
     { 
       label: '格局', 
@@ -51,8 +90,27 @@ export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => (
       additionalProps: {
         placeholder: '请输入格局',
         maxLength: 5000,
-        showWordLimit: true, 
-      },
+        enableAi: true,
+        extraProps: {
+          get() {
+            return {
+              title: r.value.getValueByPath('name'),
+              images: r.value.getValueByPath('images'),
+            }
+          },
+          update(extraProps) {
+            r.value.setValueByPath('images', extraProps.images);
+            r.value.setValueByPath('name', extraProps.title);
+          },
+          storageInfo: {
+            villageId: r.value.getGlobalParams().villageId,
+            uploadPath: 'xiangyuan/environment',
+          },
+        },
+        getExtraAiInfo() {
+          return [ '用户正在栏目环境格局,编辑 '+ r.value.getValueByPath('name') +' 自然环境格局']
+        },
+      } as SpEditorProps,
     }, 
     { 
       label: '整体风貌', 
@@ -62,8 +120,27 @@ export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => (
       additionalProps: {
         placeholder: '请输入整体风貌',
         maxLength: 5000,
-        showWordLimit: true, 
-      },
+        enableAi: true,
+        extraProps: {
+          get() {
+            return {
+              title: r.value.getValueByPath('name'),
+              images: r.value.getValueByPath('images'),
+            }
+          },
+          update(extraProps) {
+            r.value.setValueByPath('images', extraProps.images);
+            r.value.setValueByPath('name', extraProps.title);
+          },
+          storageInfo: {
+            villageId: r.value.getGlobalParams().villageId,
+            uploadPath: 'xiangyuan/environment',
+          },
+        },
+        getExtraAiInfo() {
+          return [ '用户正在栏目环境格局,编辑 '+ r.value.getValueByPath('name') +' 整体风貌特点']
+        },
+      } as SpEditorProps,
     }, 
     { 
       label: '农业遗产', 
@@ -73,13 +150,33 @@ export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => (
       additionalProps: {
         placeholder: '请输入农业遗产',
         maxLength: 5000,
-        showWordLimit: true, 
-      },
+        enableAi: true,
+        extraProps: {
+          get() {
+            return {
+              title: r.value.getValueByPath('name'),
+              images: r.value.getValueByPath('images'),
+            }
+          },
+          update(extraProps) {
+            r.value.setValueByPath('images', extraProps.images);
+            r.value.setValueByPath('name', extraProps.title);
+          },
+          storageInfo: {
+            villageId: r.value.getGlobalParams().villageId,
+            uploadPath: 'xiangyuan/environment',
+          },
+        },
+        getExtraAiInfo() {
+          return [ '用户正在栏目环境格局,编辑 '+ r.value.getValueByPath('name') +' 农业遗产信息']
+        },
+      } as SpEditorProps,
     }, 
     ...villageCommonContent(r, {
       title: '环境格局',
       showContent: false,
       showTitle: false,
+      uploadPath: 'xiangyuan/environment',
     }).formItems
   ] 
 }), { title: '环境格局', typeName: '', }]

+ 22 - 2
src/pages/dig/forms/data/food.ts

@@ -1,6 +1,7 @@
 import { VillageBulidingInfo } from "@/api/inhert/VillageInfoApi";
 import type { SingleForm } from "../forms";
 import { villageCommonContent } from "./common";
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
 
 export function villageInfoFoodProductsForm(title: string) : SingleForm {
   return [VillageBulidingInfo, (r) => ({
@@ -26,8 +27,27 @@ export function villageInfoFoodProductsForm(title: string) : SingleForm {
         additionalProps: {
           placeholder: '请输入详情',
           maxLength: 5000,
-          showWordLimit: true, 
-        },
+          enableAi: true,
+          extraProps: {
+            get() {
+              return {
+                title: r.value.getValueByPath('name'),
+                images: r.value.getValueByPath('images'),
+              }
+            },
+            update(extraProps) {
+              r.value.setValueByPath('images', extraProps.images);
+              r.value.setValueByPath('name', extraProps.title);
+            },
+            storageInfo: {
+              villageId: r.value.getGlobalParams().villageId,
+              uploadPath: 'xiangyuan/environment',
+            },
+          },
+          getExtraAiInfo() {
+            return [ '用户正在栏目' + title + ',编辑 '+ r.value.getValueByPath('name') +' 内容详情']
+          },
+        } as SpEditorProps,
         rules:  [{
           required: true,
           message: '请输入详情',

+ 56 - 6
src/pages/dig/forms/data/overview.ts

@@ -9,6 +9,8 @@ import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { RowProps } from "@/components/layout/grid/Row.vue";
 import type { GroupForm } from "../forms";
 import { villageCommonContent } from "./common";
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
+import LightVillageApi from "@/api/light/LightVillageApi";
 
 export const villageInfoOverviewForm : GroupForm = {
   [1]: [CommonInfoModel, (form) => ({
@@ -76,7 +78,7 @@ export const villageInfoOverviewForm : GroupForm = {
       },
     ]
   }), { title: '行政区划', typeName: '', order: 1 }],
-  [5]: [CommonInfoModel, (f) => ({
+  [5]: [CommonInfoModel, (r) => ({
     formItems: [
       {
         name: '',
@@ -91,8 +93,32 @@ export const villageInfoOverviewForm : GroupForm = {
             additionalProps: {
               placeholder: '请输入整体概括信息',
               maxLength: 1000,
-              showWordLimit: true, 
-            } as FieldProps,
+              enableAi: true,
+              extraProps: {
+                get() {
+                  return {
+                    title: r.value.getValueByPath('name'),
+                    images: r.value.getValueByPath('images'),
+                  }
+                },
+                update(extraProps) {
+                  r.value.setValueByPath('images', extraProps.images);
+                  r.value.setValueByPath('name', extraProps.title);
+                },
+                storageInfo: {
+                  villageId: r.value.getGlobalParams().villageId,
+                  uploadPath: 'xiangyuan/environment',
+                },
+              },
+              async getExtraAiInfo() {
+                const villageInfo = await LightVillageApi.getVillageDetails(r.value.getGlobalParams().villageId);
+                return [ 
+                  '用户正在栏目村落信息,编辑 '+ r.value.getValueByPath('name') +' 整体概括信息',
+                  '村社信息:' + villageInfo.name + ', ' + villageInfo.desc,
+                  '地址:' + villageInfo.address,
+                ]
+              },
+            } as SpEditorProps,
           },
           {
             label: '突出价值',
@@ -102,12 +128,36 @@ export const villageInfoOverviewForm : GroupForm = {
             additionalProps: {
               placeholder: '请输入突出价值信息',
               maxLength: 1000,
-              showWordLimit: true, 
-            } as FieldProps,
+              enableAi: true,
+              extraProps: {
+                get() {
+                  return {
+                    title: r.value.getValueByPath('name'),
+                    images: r.value.getValueByPath('images'),
+                  }
+                },
+                update(extraProps) {
+                  r.value.setValueByPath('images', extraProps.images);
+                  r.value.setValueByPath('name', extraProps.title);
+                },
+                storageInfo: {
+                  villageId: r.value.getGlobalParams().villageId,
+                  uploadPath: 'xiangyuan/environment',
+                },
+              },
+              async getExtraAiInfo() {
+                const villageInfo = await LightVillageApi.getVillageDetails(r.value.getGlobalParams().villageId);
+                return [ 
+                  '用户正在栏目村落信息,编辑 '+ r.value.getValueByPath('name') +' 突出价值',
+                  '村社信息:' + villageInfo.name + ', ' + villageInfo.desc,
+                  '地址:' + villageInfo.address,
+                ]
+              },
+            } as SpEditorProps,
           },
         ]
       },
-      ...(villageCommonContent(f, { 
+      ...(villageCommonContent(r, { 
         title: '综述',
         showContent: false,
         showTitle: false,

+ 43 - 4
src/pages/dig/forms/data/relic.ts

@@ -6,6 +6,7 @@ import type { StepperProps } from "@/components/form/Stepper.vue";
 import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { SingleForm } from "../forms";
 import { villageCommonContent } from "./common";
+import type { SpEditorProps } from "@/common/components/form/RichTextEditor.vue";
 
 export const villageInfoRelicForm : SingleForm = [CommonInfoModel, (r) => ({
   formItems: [
@@ -110,8 +111,27 @@ export const villageInfoRelicForm : SingleForm = [CommonInfoModel, (r) => ({
           additionalProps: {
             placeholder: '请输入简介',
             maxLength: 5000,
-            showWordLimit: true, 
-          },
+            enableAi: true,
+            extraProps: {
+              get() {
+                return {
+                  title: r.value.getValueByPath('name'),
+                  images: r.value.getValueByPath('images'),
+                }
+              },
+              update(extraProps) {
+                r.value.setValueByPath('images', extraProps.images);
+                r.value.setValueByPath('name', extraProps.title);
+              },
+              storageInfo: {
+                villageId: r.value.getGlobalParams().villageId,
+                uploadPath: 'xiangyuan/environment',
+              },
+            },
+            getExtraAiInfo() {
+              return [ '用户正在栏目非遗文物,编辑 '+ r.value.getValueByPath('name') +' 内容详情']
+            },
+          } as SpEditorProps,
           rules:  [{
             required: true,
             message: '请输入描述',
@@ -140,8 +160,27 @@ export const villageInfoRelicForm : SingleForm = [CommonInfoModel, (r) => ({
           additionalProps: { 
             placeholder: '输入关于此文物的历史文化故事',
             maxLength: 5000,
-            showWordLimit: true, 
-          } as FieldProps,
+            enableAi: true,
+            extraProps: {
+              get() {
+                return {
+                  title: r.value.getValueByPath('name'),
+                  images: r.value.getValueByPath('images'),
+                }
+              },
+              update(extraProps) {
+                r.value.setValueByPath('images', extraProps.images);
+                r.value.setValueByPath('name', extraProps.title);
+              },
+              storageInfo: {
+                villageId: r.value.getGlobalParams().villageId,
+                uploadPath: 'xiangyuan/environment',
+              },
+            },
+            getExtraAiInfo() {
+              return [ '用户正在栏目非遗文物,编辑 '+ r.value.getValueByPath('name') +' 关于此文物的历史文化故事']
+            },
+          } as SpEditorProps,
           rules: []
         },
         ...villageCommonContent(r, {

src/pages/dig/components/sp-editor/changelog.md → src/pages/home/chat/components/sp-editor/changelog.md


src/pages/dig/components/sp-editor/components/sp-editor/color-picker.vue → src/pages/home/chat/components/sp-editor/components/sp-editor/color-picker.vue


src/pages/dig/components/sp-editor/components/sp-editor/fab-tool.vue → src/pages/home/chat/components/sp-editor/components/sp-editor/fab-tool.vue



src/pages/dig/components/sp-editor/components/sp-editor/sp-editor.vue → src/pages/home/chat/components/sp-editor/components/sp-editor/sp-editor.vue


src/pages/dig/components/sp-editor/icons/custom-icon.css → src/pages/home/chat/components/sp-editor/icons/custom-icon.css


src/pages/dig/components/sp-editor/icons/editor-icon.css → src/pages/home/chat/components/sp-editor/icons/editor-icon.css


src/pages/dig/components/sp-editor/package.json → src/pages/home/chat/components/sp-editor/package.json


src/pages/dig/components/sp-editor/readme.md → src/pages/home/chat/components/sp-editor/readme.md


src/pages/dig/components/sp-editor/static/image-resize.min.js → src/pages/home/chat/components/sp-editor/static/image-resize.min.js


src/pages/dig/components/sp-editor/static/quill.min.js → src/pages/home/chat/components/sp-editor/static/quill.min.js


src/pages/dig/components/sp-editor/utils/index.js → src/pages/home/chat/components/sp-editor/utils/index.js


+ 7 - 17
src/pages/home/chat/dependent/post/components/agent.vue

@@ -167,12 +167,11 @@ const interfaceManager: ChatInterfaceManager = {
 const props = defineProps<{
   title: string;
   content: string;
-  images: {
+  images: ({
     url: string;
     localUrl: string;
-  }[];
-  villageInfo?: VillageListItem | undefined;
-  tag?: string;
+  }|string)[];
+  extraInfoFoAi: string[];
   showAgentPopup: boolean;
 }>();
 const emit = defineEmits([ 
@@ -304,7 +303,7 @@ async function generateAiImageDescs() {
   if (props.images.length === 0) return;
 
   const newImages = props.images.filter(
-    img => !uploadedImageDescs.some(desc => desc.srcImage === img.localUrl)
+    img => !uploadedImageDescs.some(desc => desc.srcImage === img || (typeof img === 'object' && desc.srcImage === img.localUrl))
   );
   if (newImages.length === 0) return;
 
@@ -313,6 +312,7 @@ async function generateAiImageDescs() {
 
     const uploadedUrls = await Promise.all(
       newImages.map(async (img) => {
+        if (typeof img === 'string') return { localUrl: img, remoteUrl: img };
         if (img.url) return { localUrl: img.localUrl, remoteUrl: img.url };
         const res = await CommonContent.uploadFile(img.localUrl, 'image', 'file');
         return { localUrl: img.localUrl, remoteUrl: res.fullurl };
@@ -398,10 +398,7 @@ const contentRef = computed({
   get: () => props.content,
   set: (v: string) => emit('update:content', v),
 });
-const imagesRef = computed({
-  get: () => props.images,
-  set: (v: { url: string; localUrl: string }[]) => emit('update:images', v as any),
-});
+const imagesRef = computed<any[]>(() => props.images);
 
 const { registerTools } = useAgentTools({
   title: titleRef,
@@ -448,14 +445,7 @@ const chatManager = useChat({
     onGetAppendSystemMessages: () => {
       const result = [] as string[];
       result.push('## 编写内容');
-      result.push('用户正在编写话题:' + 
-        //亮乡源话题中的“广场”意思是话题广场,AI容易曲解为现实中的地标广场,此处需要去除
-        props.tag?.replace('广场', '')
-      );
-      if (props.villageInfo) {
-        result.push('用户编写内容与此村社有关:' + props.villageInfo.name);
-        result.push('地址:' + props.villageInfo.address);
-      }
+      result.push(...props.extraInfoFoAi);
       if (uploadedImageDescs.length > 0) {
         result.push('## 相关图片');
         result.push('用户上传了以下图片,请参考图片说明内容进行写作:');

+ 2 - 2
src/pages/home/chat/dependent/post/components/tbutton.vue

@@ -31,10 +31,10 @@ import { onMounted, onUnmounted, ref, watch } from 'vue';
 const props = defineProps<{
   title: string;
   content: string;
-  images: {
+  images: ({
     url: string;
     localUrl: string;
-  }[];
+  }|string)[];
 }>();
 
 const emit = defineEmits([ 'showAi' ]);

+ 3 - 32
src/pages/home/chat/dependent/post/composables/agentTools.ts

@@ -1,10 +1,10 @@
-import type { Ref } from "vue";
+import type { ComputedRef, Ref } from "vue";
 import type { ChatToolsManager } from "../../../core/Tools";
 
 export function useAgentTools(options: {
   title: Ref<string>;
   content: Ref<string>;
-  images: Ref<{ url: string; localUrl: string }[]>;
+  images: ComputedRef<any[]>;
 }) {
   function setTitle(title: string) {
     options.title.value = title;
@@ -14,10 +14,6 @@ export function useAgentTools(options: {
     options.content.value = content;
   }
 
-  function setImages(images: string[]) {
-    options.images.value = images.map((url) => ({ url, localUrl: url }));
-  }
-
   function requireNonEmptyText(value: unknown, fieldName: string) {
     if (typeof value !== "string" || value.trim() === "") {
       throw new Error(`${fieldName} 不能为空`);
@@ -74,7 +70,7 @@ export function useAgentTools(options: {
           title: options.title.value,
           content: includeContent ? options.content.value : undefined,
           contentLength: options.content.value.length,
-          images: options.images.value.map((item) => item.url),
+          images: options.images.value.map((item) => typeof item === 'string' ? item : item.url),
         };
       },
     });
@@ -283,31 +279,6 @@ export function useAgentTools(options: {
         return buildResult({ changed: "content", mode: "deleteOne", occurrence });
       },
     });
-
-    tools.registerTool({
-      name: "article_set_images",
-      description: "设置文章配图 URL 列表。",
-      parameters: {
-        type: "object",
-        properties: {
-          images: {
-            type: "array",
-            items: { type: "string" },
-            description: "完整的图片 URL 数组(会覆盖原有配图)。",
-          },
-        },
-        required: ["images"],
-      },
-      generateFriendlyName: (args) => `设置配图(${Array.isArray(args?.images) ? args.images.length : 0}张)`,
-      handler: (args) => {
-        if (!Array.isArray(args?.images)) {
-          throw new Error("images 必须是字符串数组");
-        }
-        const imageUrls = args.images.filter((item: unknown) => typeof item === "string");
-        setImages(imageUrls);
-        return buildResult({ changed: "images", images: imageUrls });
-      },
-    });
   }
 
   return {

+ 203 - 0
src/pages/home/chat/dependent/post/editor.vue

@@ -0,0 +1,203 @@
+<template>
+  <CommonTopBanner title="文章编辑" customBack @backPressed="cancel">
+    <FlexCol height="100vh">
+      <sp-editor
+        :toolbar-config="{
+          excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck'],
+          iconSize: '18px'
+        }"
+        :maxlength="maxLength"
+        @init="initEditor"
+        @input="inputOver"
+        @upinImage="upinImage"
+        @overMax="overMax"
+      />
+      <FlexCol v-if="enableAi" padding="padding.md">
+        <Uploader 
+          ref="uploader"
+          listType="grid"
+          :maxUploadCount="9" 
+          :upload="aliOss"
+          @updateList="onUpdateList"
+          @allUploaded="onAllUploaded"
+        />
+        <FlexRow justify="flex-end" align="center" gap="gap.md">
+          <Button text="AI看图写作" @click="openImageWriting" />
+          <Tbutton
+            :content="currentContent"
+            :title="currentTitle"
+            :images="currentImages"
+            @showAi="showAgentPopup = true" 
+          />
+        </FlexRow>
+      </FlexCol>
+      <Height :height="50" />
+      <FlexRow center>
+        <PrimaryButton
+          text="保存"
+          width="500rpx"
+          @click="save"
+        />
+      </FlexRow>
+      <XBarSpace />
+      <Agent 
+        ref="agentRef"
+        v-model:showAgentPopup="showAgentPopup"
+        v-model:title="currentTitle"
+        v-model:content="currentContent"
+        v-model:images="currentImages"
+        :extraInfoFoAi="extraAiInfo"
+        @upload="uploader?.pick()"
+        @close="showAgentPopup = false" 
+      />
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useAuthStore } from '@/store/auth';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { confirm } from '@/components/utils/DialogAction';
+import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { useAliOssUploadCo } from '@/common/components/upload/AliOssUploadCo';
+import spEditor from '../../components/sp-editor/components/sp-editor/sp-editor.vue';
+import XBarSpace from '@/components/layout/space/XBarSpace.vue';
+import Button from '@/components/basic/Button.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import Height from '@/components/layout/space/Height.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
+import Tbutton from './components/tbutton.vue';
+import Agent from './components/agent.vue';
+import Uploader, { type UploaderInstance } from '@/components/form/Uploader.vue';
+import type { UploaderItem } from '@/components/form/Uploader';
+
+const { querys } = useLoadQuerys({
+  enableAi: true,
+  extraAiInfo: '',
+  uploadSubpath: 'common',
+  villageId: 0,
+  maxLength: 1000,
+}, () => {
+  maxLength.value = querys.value.maxLength;
+  enableAi.value = querys.value.enableAi;
+  uploadSubpath.value = querys.value.uploadSubpath;
+  extraAiInfo.value = querys.value.extraAiInfo.split(',');
+  load();
+});
+
+const enableAi = ref(false);
+const maxLength = ref(-1);
+const uploadSubpath = ref('');
+const currentTitle = ref('');
+const currentContent = ref('');
+const currentImages = ref<string[]>([]);
+
+const authStore = useAuthStore();
+const uploader = ref<UploaderInstance>();
+const agentRef = ref<InstanceType<typeof Agent>>();
+
+function cancel() {
+  confirm({
+    title: '提示',
+    content: '是否放弃编辑?',
+  }).then((res) => {
+    if (res)
+      back();
+  })
+}
+function save() {
+  uni.setStorage({
+    key: 'editorContent',
+    data: {
+      title: currentTitle.value,
+      images: currentImages.value,
+      content: currentContent.value,
+    },
+    success: () => backAndCallOnPageBack('editor', {}),
+    fail: (e) => showError(e),
+  })
+}
+function load() {
+  uni.getStorage({
+    key: 'editorContent',
+    success: (success) => {
+      currentTitle.value = success.data.title;
+      currentContent.value = success.data.content;
+      currentImages.value = success.data.images;
+      uploader.value?.setList(currentImages.value.map((image) => ({
+        url: image,
+        type: 'image',
+        filePath: image,
+        state: image ? 'success' : 'notstart',
+      })));
+    },
+  })
+}
+
+const showAgentPopup = ref(false);
+const extraAiInfo = ref<string[]>([]);
+const aliOss = useAliOssUploadCo(uploadSubpath, {
+  getVillageId: () => querys.value.villageId,
+  overflow: () => {
+    //todo
+  },
+});
+
+function openImageWriting() {
+  agentRef.value?.openImageWriting();
+}
+function onAllUploaded() {
+  agentRef.value?.openImageWritingDialogIfLastClicked();
+}
+function onUpdateList(list: UploaderItem[]) {
+  currentImages.value = list.map((item) => item.uploadedPath || '');
+}
+
+
+/**
+* 获取输入内容
+*/
+function inputOver(e: { html: string; text: string; }) {
+  // 可以在此处获取到编辑器已编辑的内容
+  currentContent.value = e.html;
+}
+/**
+ * 超出最大内容限制
+ * @param {Object} e {html,text} 内容的html文本,和text文本
+ */
+function overMax(e: { html: string; text: string; }) {
+  // 若设置了最大字数限制,可在此处触发超出限制的回调
+  console.log('==== overMax :', e)
+}
+function initEditor(editor: any) {
+  editor.setContents({
+    html: currentContent.value
+  })
+}
+function upinImage(tempFiles: any, editorCtx: any) {
+  let path;
+  // #ifdef MP-WEIXIN
+  path = tempFiles[0].tempFilePath; 
+  // #endif
+  // #ifndef MP-WEIXIN
+  path = tempFiles[0].path;
+  // #endif
+
+  //上传
+  uploader.value?.addItemAndUpload({
+    previewPath: path,
+    filePath: path,
+    state: 'notstart',
+  }).then((res) => {  
+    editorCtx.insertImage({
+      src: res.uploadedPath,
+      width: '80%',
+      success: function () {}
+    })
+  }).catch((e) => showError(e, '上传图片失败'));
+}
+</script>

+ 29 - 13
src/pages/home/chat/dependent/post/publish.vue

@@ -63,8 +63,7 @@
         v-model:title="title"
         v-model:content="content"
         v-model:images="images"
-        :villageInfo="villageInfoForAI.content.value ?? undefined"
-        :tag="querys.tag"
+        :extraInfoFoAi="extraInfoFoAi"
         @upload="uploader?.pick()"
         @close="showAgentPopup = false" 
       />
@@ -73,32 +72,31 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch } from 'vue';
+import { computed, onMounted, ref, watch } from 'vue';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
 import { Debounce, formatError } from '@imengyu/imengyu-utils';
-import { confirm, toast, alert } from '@/components/dialog/CommonRoot';
+import { confirm, toast } from '@/components/dialog/CommonRoot';
 import { useAuthStore } from '@/store/auth';
 import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
-import { envVersion } from '@/common/config/AppCofig';
-import CommonContent from '@/api/CommonContent';
+import { envVersion, isDevEnv } from '@/common/config/AppCofig';
+import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
+import { confirm as uniConfirm } from '@/components/utils/DialogAction';
+import type { UploaderAction, UploaderItem } from '@/components/form/Uploader';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import Field from '@/components/form/Field.vue';
 import Uploader, { type UploaderInstance } from '@/components/form/Uploader.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 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 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 { confirm as uniConfirm } from '@/components/utils/DialogAction';
-import LightVillageApi from '@/api/light/LightVillageApi';
 import Button from '@/components/basic/Button.vue';
+import OfficialApi, { PostMessage } from '@/api/light/OfficialApi';
+import LightVillageApi from '@/api/light/LightVillageApi';
+import CommonContent from '@/api/CommonContent';
 
 const { querys } = useLoadQuerys({
   tag: '',
@@ -108,7 +106,12 @@ const { querys } = useLoadQuerys({
   const info = uni.getSystemInfoSync();
   console.log(info.platform);
   
-  if (!(uni as any).shareToOfficialAccount || (info.platform !== 'android' && info.platform !== 'ios')) {
+  if (
+    !isDevEnv && (
+      !(uni as any).shareToOfficialAccount 
+      || (info.platform !== 'android' && info.platform !== 'ios')
+    )
+  ) {
     uniConfirm({
       title: '提示',
       content: '抱歉,微信目前不支持在电脑端编写贴图,您可以在手机版微信中编写贴图哦!',
@@ -149,6 +152,19 @@ const villageInfoForAI = useSimpleDataLoader(async () => {
   return null;
 }, false);
 
+const extraInfoFoAi = computed(() => {
+  const result = [] as string[];
+  result.push('用户正在编写话题:' + 
+    //亮乡源话题中的“广场”意思是话题广场,AI容易曲解为现实中的地标广场,此处需要去除
+    querys.value.tag?.replace('广场', '')
+  );
+  if (villageInfoForAI.content.value) {
+    result.push('用户编写内容与此村社有关:' + villageInfoForAI.content.value.name);
+    result.push('地址:' + villageInfoForAI.content.value.address);
+  }
+  return result;
+});
+
 watch(title, () => saveDraftDebunce.executeWithDelay());
 watch(content, () => saveDraftDebunce.executeWithDelay());
 watch(images, () => saveDraftDebunce.executeWithDelay());