m-AIWriting.vue 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663
  1. <template>
  2. <div class="mobile-ai-writing">
  3. <!-- 使用移动端通用头部组件 -->
  4. <MobileHeader title="AI写作" @back="goBack" @menu="showHistoryDrawer" />
  5. <div class="mobile-content">
  6. <!-- 使用移动端通用历史记录抽屉 -->
  7. <MobileHistoryDrawer
  8. :visible="!isGenerating && showHistory"
  9. title="历史记录"
  10. :historyData="historyData"
  11. :loading="isLoadingHistory"
  12. @close="showHistory = false"
  13. @createNewTask="createNewTask"
  14. @handleHistoryItem="handleHistoryItem"
  15. @deleteHistoryItem="deleteHistoryItem"
  16. />
  17. <!-- 步骤一:AI写作主页面 - 完全复用PC端结构 -->
  18. <div v-if="currentView === 'main'" class="ai-writing-card">
  19. <!-- Loading遮罩 - 只覆盖当前卡片 -->
  20. <div v-if="isGenerating" class="card-loading-overlay">
  21. <div class="loading-spinner"></div>
  22. <div class="loading-text">正在生成文档,请稍候...</div>
  23. </div>
  24. <!-- 文档生成区域 -->
  25. <div class="document-generation">
  26. <h3>帮我写作</h3>
  27. <p class="subtitle">智能生成办公文档,提升办公效能,高效创作</p>
  28. <div class="input-area">
  29. <div class="template-input-container"
  30. contenteditable="true"
  31. @input="handleTemplateInput"
  32. @copy="handleCopy"
  33. placeholder="请在这里输入您的写作要求...">
  34. 请帮我生成一份正式的总结报告,要求格式规范、语言严谨。具体内容包括<span class="editable-highlight" contenteditable="true">总结主题:</span>、<span class="editable-highlight" contenteditable="true">总结时间:</span>、<span class="editable-highlight" contenteditable="true">主要业绩和成果:</span>、<span class="editable-highlight" contenteditable="true">存在的问题和不足:</span>、<span class="editable-highlight" contenteditable="true">下一阶段工作计划:</span>的内容。请按照标准工作总结格式生成全文,包含"工作总结、问题不足、未来计划"三部分的完整报告。
  35. </div>
  36. <div class="input-actions">
  37. <div class="left-actions">
  38. <button class="attachment-btn" @click="triggerFileUpload" :disabled="isSending">
  39. <img src="@/assets/AIWriting/4.png" alt="附件" class="action-icon" />
  40. </button>
  41. <!-- 文件预览区域 - 显示在输入框右边 -->
  42. <div v-if="selectedFile" class="file-preview-inline">
  43. <div class="file-info-inline">
  44. <span class="file-icon-inline">{{ selectedFile.icon }}</span>
  45. <span class="file-name-inline">{{ selectedFile.name }}</span>
  46. <button class="remove-file-inline" @click="removeSelectedFile">
  47. <span class="remove-icon">×</span>
  48. </button>
  49. </div>
  50. </div>
  51. </div>
  52. <div class="right-actions">
  53. <button class="voice-btn" @click="handleVoiceClick" :disabled="isSending" :class="{ 'recording': isListening }">
  54. <div class="icon-container">
  55. <img src="@/assets/chat/18.png" alt="语音" class="voice-icon" />
  56. <div v-if="isListening" class="recording-indicator"></div>
  57. </div>
  58. </button>
  59. <div class="divider"></div>
  60. <button class="send-btn" @click="sendAIWritingRequest" :disabled="isGenerating || isSending">
  61. <img
  62. :src="hasInputContent ? sendIconFilled : sendIconEmpty"
  63. alt="发送"
  64. class="send-icon"
  65. />
  66. <span v-if="isGenerating" class="generating-text">生成中...</span>
  67. </button>
  68. </div>
  69. </div>
  70. <p class="hint-text">提示:请输入关键字,AI将根据关键字生成文档</p>
  71. </div>
  72. </div>
  73. <!-- 文档模板区域 - 完全复用PC端 -->
  74. <div class="document-templates">
  75. <div class="template-tabs">
  76. <div
  77. v-for="tab in tabs"
  78. :key="tab.key"
  79. :class="['tab-item', { active: activeTab === tab.key }]"
  80. @click="switchTab(tab.key)"
  81. >
  82. {{ tab.name }}
  83. </div>
  84. </div>
  85. <div class="template-cards">
  86. <div
  87. v-for="template in filteredTemplates"
  88. :key="template.id"
  89. :class="['template-card', template.buttonClass]"
  90. >
  91. <div class="template-row">
  92. <div :class="['template-icon', getIconClass(template.buttonClass)]">
  93. <img :src="template.image" :alt="template.name" class="template-icon-img" />
  94. </div>
  95. <div class="template-info">
  96. <div class="template-title">{{ template.name }}</div>
  97. <div class="template-desc">{{ getDescription(template.name) }}</div>
  98. </div>
  99. <button
  100. :class="['use-template-btn', template.buttonClass]"
  101. @click="useTemplate(template.name)"
  102. >
  103. 使用此模板
  104. </button>
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. <!-- 步骤二:富文本编辑器 -->
  111. <div v-if="currentView === 'editor'" class="editor-view">
  112. <!-- 编辑器Loading遮罩 - 只覆盖编辑器区域 -->
  113. <div v-if="isGenerating" class="editor-loading-overlay">
  114. <div class="loading-spinner"></div>
  115. <div class="loading-text">正在生成文档,请稍候...</div>
  116. </div>
  117. <div class="editor-header">
  118. <div class="editor-title">
  119. <h3>文档预览</h3>
  120. <p class="editor-subtitle" v-if="!isGenerating">编辑请去电脑端</p>
  121. <p class="editor-subtitle" v-else>AI正在生成内容,请稍候...</p>
  122. </div>
  123. <div class="editor-actions">
  124. <button class="download-btn" @click="downloadDocument" :disabled="isGenerating">
  125. <img src="@/assets/Exam/13.png" alt="下载Word" class="download-icon" />
  126. </button>
  127. </div>
  128. </div>
  129. <!-- 富文本编辑器 -->
  130. <div class="editor-container">
  131. <div
  132. ref="richEditor"
  133. class="rich-text-editor"
  134. v-html="editorContent"
  135. contenteditable="false"
  136. ></div>
  137. </div>
  138. </div>
  139. </div>
  140. <!-- 去除弹窗方式,改为直接录音,保留占位注释以便回滚 -->
  141. <!-- 移动端Toast提示组件 -->
  142. <MobileToast
  143. :visible="showToast"
  144. :message="toastMessage"
  145. :duration="2000"
  146. @close="closeToast"
  147. />
  148. </div>
  149. </template>
  150. <script setup>
  151. import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue'
  152. import { useRouter } from 'vue-router'
  153. import MobileHeader from '@/components/MobileHeader.vue'
  154. import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
  155. import MobileToast from '@/components/MobileToast.vue'
  156. import VoiceModal from '@/components/VoiceModal.vue'
  157. // 移除 element-plus 的 ElMessage,改用移动端 Toast
  158. import { apis } from '@/request/apis.js'
  159. // ===== 已删除:getUserId - 不再需要,改用token =====
  160. // import { getUserId } from '@/utils/userManager.js'
  161. import { useSpeechRecognition } from '@/composables/useSpeechRecognition'
  162. // 完全复用PC端的导入
  163. import sendIconEmpty from '@/assets/chat/15.png'
  164. import sendIconFilled from '@/assets/chat/16.png'
  165. import announcementIcon from '@/assets/AIWriting/20.png'
  166. import notificationIcon from '@/assets/AIWriting/21.png'
  167. import summaryIcon from '@/assets/AIWriting/22.png'
  168. import meetingIcon from '@/assets/AIWriting/20.png'
  169. import speechIcon from '@/assets/AIWriting/21.png'
  170. const router = useRouter()
  171. // 响应式数据 - 复用PC端逻辑
  172. const currentView = ref('main') // 'main' | 'editor'
  173. const showHistory = ref(false)
  174. const isGenerating = ref(false)
  175. const isSending = ref(false)
  176. // 语音识别(与PC端保持一致)
  177. const {
  178. isSupported: speechSupported,
  179. isListening,
  180. transcript,
  181. error: speechError,
  182. startListening,
  183. stopListening,
  184. } = useSpeechRecognition()
  185. const templateContent = ref('')
  186. const editorContent = ref('')
  187. const selectedFile = ref(null)
  188. const showVoiceModal = ref(false)
  189. const richEditor = ref(null)
  190. const activeTab = ref("all")
  191. // 历史记录相关数据
  192. const historyData = ref([])
  193. const historyTotal = ref(0)
  194. const isLoadingHistory = ref(false)
  195. const ai_conversation_id = ref(0)
  196. // Toast相关状态
  197. const showToast = ref(false)
  198. const toastMessage = ref('')
  199. // 移动端Toast提示方法
  200. const displayToast = (message, duration = 2000) => {
  201. toastMessage.value = message
  202. showToast.value = true
  203. setTimeout(() => {
  204. closeToast()
  205. }, duration)
  206. }
  207. // 关闭Toast
  208. const closeToast = () => {
  209. showToast.value = false
  210. }
  211. // 完全复用PC端的模板配置
  212. const tabs = [
  213. { key: "all", name: "全部" },
  214. { key: "announcement", name: "公告" },
  215. { key: "notification", name: "通知" },
  216. { key: "summary", name: "总结" },
  217. { key: "meeting", name: "会议" },
  218. { key: "speech", name: "决定" },
  219. ];
  220. const templates = [
  221. {
  222. id: 1,
  223. name: "公告模板",
  224. image: announcementIcon,
  225. category: "announcement",
  226. buttonClass: "announcement-btn"
  227. },
  228. {
  229. id: 2,
  230. name: "通知模板",
  231. image: notificationIcon,
  232. category: "notification",
  233. buttonClass: "notification-btn"
  234. },
  235. {
  236. id: 3,
  237. name: "工作汇报模板",
  238. image: summaryIcon,
  239. category: "summary",
  240. buttonClass: "report-btn"
  241. },
  242. {
  243. id: 4,
  244. name: "会议纪要模版",
  245. image: meetingIcon,
  246. category: "meeting",
  247. buttonClass: "announcement-btn"
  248. },
  249. {
  250. id: 5,
  251. name: "决定模版",
  252. image: speechIcon,
  253. category: "speech",
  254. buttonClass: "notification-btn"
  255. }
  256. ];
  257. // 复用PC端的计算属性
  258. const filteredTemplates = computed(() => {
  259. if (activeTab.value === "all") {
  260. return templates;
  261. }
  262. return templates.filter(template => template.category === activeTab.value);
  263. });
  264. // 获取模板描述与图标类,保持与原配色一致
  265. const getDescription = (name) => {
  266. switch (name) {
  267. case '公告模板':
  268. return '适用于各类信息公告'
  269. case '通知模板':
  270. return '适用于各类通知公文'
  271. case '工作汇报模板':
  272. return '适用于各类工作汇报'
  273. case '会议纪要模版':
  274. return '适用于正式会议的记录'
  275. case '决定模版':
  276. return '适用于各类专业的决定文稿'
  277. default:
  278. return '常用办公文档模板'
  279. }
  280. }
  281. const getIconClass = (buttonClass) => {
  282. // 与按钮配色映射
  283. return {
  284. 'announcement-btn': 'icon-announcement',
  285. 'notification-btn': 'icon-notification',
  286. 'report-btn': 'icon-report'
  287. }[buttonClass] || 'icon-announcement'
  288. }
  289. const getEmoji = (name) => {
  290. switch (name) {
  291. case '公告模板': return '📢'
  292. case '通知模板': return '📣'
  293. case '工作汇报模板': return '📝'
  294. case '会议纪要模版': return '🗓️'
  295. case '决定模版': return '✅'
  296. default: return '📄'
  297. }
  298. }
  299. const hasInputContent = computed(() => {
  300. return templateContent.value && templateContent.value.trim().length > 0
  301. });
  302. // 切换标签
  303. const switchTab = (tabKey) => {
  304. activeTab.value = tabKey
  305. }
  306. // 使用模板 - 复用PC端的useTemplate方法
  307. const useTemplate = (templateName) => {
  308. let content = ''
  309. switch (templateName) {
  310. case '公告模板':
  311. content = "请帮我生成一份正式的公告,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告编号:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发布背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">公告核心条款:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">发文日期:</span>等内容。请按照标准公告格式生成全文,包括标题、正文、落款等所有要素。"
  312. break
  313. case '通知模板':
  314. content = "请帮我生成一份正式的通知,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">通知对象:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">通知内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">时间安排:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">联系方式:</span>等内容。请按照标准通知格式生成全文,确保表达清楚、信息准确。"
  315. break
  316. case '工作汇报模板':
  317. content = "请帮我生成一份正式的工作汇报,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">汇报主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">汇报时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要工作内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">完成情况:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">存在问题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">下步计划:</span>等内容。请按照标准工作汇报格式生成全文,确保内容全面、数据准确。"
  318. break
  319. case '会议纪要模版':
  320. content = "请帮我生成一份正式的会议纪要,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">会议主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">会议时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">参会人员:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">会议议程:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">议题讨论:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决议事项:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">后续安排:</span>等内容。请按照标准会议纪要格式生成全文,确保记录准确、要点清晰。"
  321. break
  322. case '决定模版':
  323. content = "请帮我生成一份正式的决定,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">决定主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定背景:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定依据:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">决定内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">执行要求:</span>等内容。请按照标准决定文件格式生成全文,确保表述准确、要求明确。"
  324. break
  325. default:
  326. content = "请帮我生成一份正式的文档,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">文档主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要内容:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">具体要求:</span>等内容。"
  327. }
  328. // 设置模板内容到输入框
  329. const inputElement = document.querySelector('.template-input-container')
  330. if (inputElement) {
  331. inputElement.innerHTML = content
  332. templateContent.value = content.replace(/<[^>]*>/g, '')
  333. // 确保高亮元素的样式和交互功能
  334. nextTick(() => {
  335. const highlights = inputElement.querySelectorAll('.editable-highlight')
  336. highlights.forEach(highlight => {
  337. highlight.style.backgroundColor = '#3E7BFA10'
  338. highlight.style.color = '#3E7BFA'
  339. highlight.style.padding = '4px 8px'
  340. highlight.style.borderRadius = '6px'
  341. highlight.style.fontWeight = '500'
  342. highlight.style.cursor = 'text'
  343. highlight.style.border = '1px solid transparent'
  344. highlight.style.display = 'inline-block'
  345. highlight.style.minWidth = '20px'
  346. highlight.style.margin = '6px 8px 6px 0'
  347. // 添加点击编辑功能
  348. highlight.addEventListener('click', () => {
  349. highlight.contentEditable = 'true'
  350. highlight.focus()
  351. })
  352. highlight.addEventListener('blur', () => {
  353. highlight.contentEditable = 'false'
  354. })
  355. })
  356. })
  357. }
  358. }
  359. // 处理输入框输入
  360. const handleTemplateInput = (event) => {
  361. const element = event.target
  362. templateContent.value = element.textContent || ''
  363. // 确保高亮元素的样式
  364. nextTick(() => {
  365. const highlights = element.querySelectorAll('.editable-highlight')
  366. highlights.forEach(highlight => {
  367. highlight.style.backgroundColor = '#3E7BFA10'
  368. highlight.style.color = '#3E7BFA'
  369. highlight.style.padding = '4px 8px'
  370. highlight.style.borderRadius = '6px'
  371. highlight.style.fontWeight = '500'
  372. highlight.style.cursor = 'text'
  373. highlight.style.border = '1px solid transparent'
  374. highlight.style.display = 'inline-block'
  375. highlight.style.minWidth = '20px'
  376. highlight.style.margin = '6px 8px 6px 0'
  377. })
  378. })
  379. }
  380. // 处理复制事件
  381. const handleCopy = (event) => {
  382. event.preventDefault()
  383. const selection = window.getSelection()
  384. if (selection.toString().trim()) {
  385. navigator.clipboard.writeText(selection.toString())
  386. }
  387. }
  388. // 处理文件上传
  389. const triggerFileUpload = () => {
  390. const input = document.createElement('input')
  391. input.type = 'file'
  392. input.accept = '.pdf,.doc,.docx,.txt,.jpg,.jpeg,.png'
  393. input.onchange = (event) => {
  394. const file = event.target.files[0]
  395. if (file) {
  396. selectedFile.value = {
  397. name: file.name,
  398. size: file.size,
  399. icon: getFileIcon(file.type)
  400. }
  401. }
  402. }
  403. input.click()
  404. }
  405. // 获取文件图标
  406. const getFileIcon = (fileType) => {
  407. if (fileType.includes('pdf')) return '📄'
  408. if (fileType.includes('word') || fileType.includes('document')) return '📝'
  409. if (fileType.includes('image')) return '🖼️'
  410. return '📎'
  411. }
  412. // 移除文件
  413. const removeSelectedFile = () => {
  414. selectedFile.value = null
  415. }
  416. // 处理语音点击(直接开始/停止录音,与PC端一致)
  417. const handleVoiceClick = () => {
  418. if (!speechSupported.value) {
  419. displayToast('当前浏览器不支持语音识别')
  420. return
  421. }
  422. if (isListening.value) {
  423. stopListening()
  424. } else {
  425. // 开始前清空上次结果
  426. // 由组合函数内部管理 transcript
  427. startListening()
  428. }
  429. }
  430. // 监听语音识别转写内容,实时写入输入框
  431. watch(transcript, (val) => {
  432. if (!val) return
  433. const inputElement = document.querySelector('.template-input-container')
  434. if (inputElement) {
  435. inputElement.textContent = val
  436. templateContent.value = val
  437. }
  438. })
  439. // 监听语音识别错误
  440. watch(speechError, (err) => {
  441. if (err) displayToast(String(err))
  442. })
  443. // 发送AI写作请求 - 调用真实API接口
  444. const sendAIWritingRequest = async () => {
  445. if (!hasInputContent.value) {
  446. console.log("请输入内容");
  447. return;
  448. }
  449. // 直接进入富文本编辑器并显示loading
  450. currentView.value = 'editor';
  451. isGenerating.value = true;
  452. try {
  453. console.log('开始调用AI写作API...');
  454. console.log('对话ID:', ai_conversation_id.value);
  455. console.log('消息内容:', templateContent.value);
  456. // 调用真实的AI写作接口 - 与PC端保持一致
  457. const response = await apis.sendDeepseekMessage({
  458. // ===== 已删除:user_id - 后端从token解析 =====
  459. ai_conversation_id: ai_conversation_id.value,
  460. message: templateContent.value,
  461. business_type: 2 // AI写作类型
  462. });
  463. console.log('AI写作API响应:', response);
  464. if (response.statusCode === 200) {
  465. // 设置对话ID
  466. if (response.data && response.data.ai_conversation_id) {
  467. ai_conversation_id.value = response.data.ai_conversation_id;
  468. }
  469. // 设置文档内容 - 与PC端保持一致
  470. const aiReply = response.data.reply;
  471. if (aiReply) {
  472. // 直接使用AI回复内容
  473. editorContent.value = aiReply;
  474. } else {
  475. // 如果没有AI回复,生成默认内容
  476. const mockContent = generateMockContent(templateContent.value);
  477. editorContent.value = mockContent;
  478. }
  479. console.log('AI写作成功,文档内容已设置');
  480. } else {
  481. console.error('AI写作失败:', response);
  482. displayToast('生成失败,请重试');
  483. // 生成模拟内容作为后备
  484. const mockContent = generateMockContent(templateContent.value);
  485. editorContent.value = mockContent;
  486. }
  487. isGenerating.value = false;
  488. } catch (error) {
  489. console.error('AI写作API调用失败:', error);
  490. displayToast('网络错误,请重试');
  491. // 生成模拟内容作为后备
  492. const mockContent = generateMockContent(templateContent.value);
  493. editorContent.value = mockContent;
  494. isGenerating.value = false;
  495. }
  496. }
  497. // 生成模拟内容
  498. const generateMockContent = (inputText) => {
  499. return `
  500. <div class="document-content">
  501. <h1>总结报告</h1>
  502. <div class="content-body">
  503. <h2>一、工作总结</h2>
  504. <p>根据您的要求,以下是详细的总结报告内容:</p>
  505. <p>${inputText}</p>
  506. <h2>二、主要成果</h2>
  507. <ul>
  508. <li>完成项目目标,达成率100%</li>
  509. <li>团队协作效率提升30%</li>
  510. <li>客户满意度达到95%</li>
  511. </ul>
  512. <h2>三、存在的问题</h2>
  513. <p>在执行过程中,我们发现了以下问题和不足:</p>
  514. <ul>
  515. <li>时间安排有待优化</li>
  516. <li>资源配置需要调整</li>
  517. <li>沟通机制需要完善</li>
  518. </ul>
  519. <h2>四、下阶段计划</h2>
  520. <p>基于前期工作总结,下阶段将重点关注以下几个方面:</p>
  521. <ul>
  522. <li>优化工作流程,提高效率</li>
  523. <li>加强团队协作,改善沟通</li>
  524. <li>完善制度体系,确保执行</li>
  525. </ul>
  526. </div>
  527. </div>
  528. `;
  529. }
  530. // 下载文档 - 复用PC端的实现
  531. const downloadDocument = async () => {
  532. if (!editorContent.value) {
  533. displayToast('没有可下载的内容')
  534. return
  535. }
  536. try {
  537. console.log('开始下载AI写作文档...')
  538. // 提取文档标题
  539. const documentTitle = extractDocumentTitle(editorContent.value);
  540. // 创建改进的HTML格式Word文档内容(与PC端保持一致)
  541. const wordContent = createImprovedWordContent(editorContent.value, documentTitle);
  542. // 创建Blob对象 - 使用Word兼容的MIME类型
  543. const blob = new Blob([wordContent], {
  544. type: 'application/msword'
  545. });
  546. // 下载文件
  547. const url = URL.createObjectURL(blob)
  548. const a = document.createElement('a')
  549. a.href = url
  550. a.download = `${documentTitle}.doc`
  551. document.body.appendChild(a)
  552. a.click()
  553. document.body.removeChild(a)
  554. URL.revokeObjectURL(url)
  555. displayToast('Word文档下载成功!')
  556. console.log('✅ AI写作文档下载成功')
  557. } catch (error) {
  558. console.error('下载Word文档失败:', error)
  559. displayToast('下载失败,请重试')
  560. }
  561. }
  562. // 提取文档标题
  563. const extractDocumentTitle = (content) => {
  564. // 先移除HTML标签,获取纯文本
  565. const textContent = content.replace(/<[^>]*>/g, '');
  566. // 尝试从内容中提取标题
  567. // 1. 查找第一个段落或标题
  568. const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
  569. if (h1Match) {
  570. return h1Match[1].trim();
  571. }
  572. // 2. 查找第一个h2标题
  573. const h2Match = content.match(/<h2[^>]*>([^<]+)<\/h2>/i);
  574. if (h2Match) {
  575. return h2Match[1].trim();
  576. }
  577. // 3. 查找第一个h3标题
  578. const h3Match = content.match(/<h3[^>]*>([^<]+)<\/h3>/i);
  579. if (h3Match) {
  580. return h3Match[1].trim();
  581. }
  582. // 4. 如果没有标题标签,取前50个字符作为标题
  583. const firstSentence = textContent.trim().split(/[。!?]/)[0];
  584. if (firstSentence && firstSentence.length > 0) {
  585. return firstSentence.length > 30 ? firstSentence.substring(0, 30) + '...' : firstSentence;
  586. }
  587. // 5. 最后兜底,使用日期作为标题
  588. return `AI写作文档_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}`;
  589. };
  590. // 创建改进的HTML格式Word文档内容 - 与PC端保持一致
  591. const createImprovedWordContent = (content, title = 'AI生成的文档') => {
  592. // 创建HTML格式的文档内容,兼容Microsoft Office Word
  593. let htmlContent = `<!DOCTYPE html>
  594. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  595. xmlns:w="urn:schemas-microsoft-com:office:word"
  596. xmlns="http://www.w3.org/TR/REC-html40">
  597. <head>
  598. <meta charset="utf-8">
  599. <meta name="ProgId" content="Word.Document">
  600. <meta name="Generator" content="Microsoft Word 15">
  601. <meta name="Originator" content="Microsoft Word 15">
  602. <title>${title}</title>
  603. <!--[if gte mso 9]>
  604. <xml>
  605. <w:WordDocument>
  606. <w:View>Print</w:View>
  607. <w:Zoom>100</w:Zoom>
  608. <w:DoNotOptimizeForBrowser/>
  609. <w:ValidateAgainstSchemas/>
  610. <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>
  611. <w:IgnoreMixedContent>false</w:IgnoreMixedContent>
  612. <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>
  613. <w:Compatibility>
  614. <w:BreakWrappedTables/>
  615. <w:SnapToGridInCell/>
  616. <w:WrapTextWithPunct/>
  617. <w:UseAsianBreakRules/>
  618. <w:DontGrowAutofit/>
  619. </w:Compatibility>
  620. </w:WordDocument>
  621. </xml>
  622. <![endif]-->
  623. <style>
  624. @page {
  625. size: 21cm 29.7cm;
  626. margin: 2.5cm 2cm 2.5cm 2cm;
  627. }
  628. body {
  629. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  630. font-size: 14px;
  631. line-height: 1.6;
  632. margin: 24px;
  633. color: #000;
  634. }
  635. .header {
  636. text-align: center;
  637. margin-bottom: 1.5cm;
  638. page-break-after: avoid;
  639. }
  640. h1 {
  641. font-size: 16pt;
  642. font-weight: bold;
  643. color: #000;
  644. margin-top: 24pt;
  645. margin-bottom: 12pt;
  646. text-align: center;
  647. }
  648. h2 {
  649. font-size: 14pt;
  650. font-weight: bold;
  651. color: #000;
  652. margin-top: 18pt;
  653. margin-bottom: 9pt;
  654. }
  655. h3 {
  656. font-size: 12pt;
  657. font-weight: bold;
  658. color: #000;
  659. margin-top: 14pt;
  660. margin-bottom: 7pt;
  661. }
  662. p {
  663. margin-bottom: 12pt;
  664. text-align: justify;
  665. text-indent: 2em;
  666. }
  667. ul, ol {
  668. margin-left: 20pt;
  669. margin-bottom: 12pt;
  670. }
  671. li {
  672. margin-bottom: 6pt;
  673. }
  674. strong, b {
  675. font-weight: bold;
  676. }
  677. em, i {
  678. font-style: italic;
  679. }
  680. table {
  681. border-collapse: collapse;
  682. width: 100%;
  683. margin: 12pt 0;
  684. }
  685. th, td {
  686. border: 1pt solid #000;
  687. padding: 6pt;
  688. text-align: left;
  689. }
  690. .no-indent {
  691. text-indent: 0;
  692. }
  693. </style>
  694. </head>
  695. <body>
  696. <div>
  697. ${content}
  698. </div>
  699. </body>
  700. </html>`;
  701. return htmlContent;
  702. }
  703. // 处理编辑器输入
  704. const handleEditorInput = (event) => {
  705. editorContent.value = event.target.innerHTML
  706. }
  707. // 生成对话标题
  708. const generateConversationTitle = (content) => {
  709. if (!content) return 'AI写作对话'
  710. // 取前30个字符作为标题
  711. const title = content.replace(/<[^>]*>/g, '').trim()
  712. return title.length > 30 ? title.substring(0, 30) + '...' : title
  713. }
  714. // 历史记录相关方法
  715. const showHistoryDrawer = () => {
  716. if (!isGenerating.value) {
  717. showHistory.value = true
  718. // 每次打开都刷新,避免回显旧数据
  719. getHistoryRecordList()
  720. }
  721. // AI处理中时不执行任何操作,不记录点击意图
  722. }
  723. // 获取历史记录列表
  724. const getHistoryRecordList = async () => {
  725. try {
  726. console.log('📋 开始获取AI写作历史记录列表...')
  727. isLoadingHistory.value = true
  728. const startTime = performance.now()
  729. const response = await apis.getHistoryRecord({
  730. // ===== 已删除:user_id - 后端从token解析 =====
  731. ai_conversation_id: 0, // 0表示获取对话列表
  732. business_type: 2 // AI写作类型
  733. })
  734. const endTime = performance.now()
  735. console.log(`📋 AI写作历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
  736. console.log('📋 AI写作历史记录列表响应:', response)
  737. if (response.statusCode === 200) {
  738. // 设置历史记录总数
  739. historyTotal.value = response.total || 0
  740. // 转换后端数据为前端格式
  741. historyData.value = response.data.map(conversation => ({
  742. id: conversation.id,
  743. title: generateConversationTitle(conversation.content),
  744. time: formatTime(conversation.updated_at),
  745. businessType: conversation.business_type,
  746. isActive: false,
  747. // 保存原始数据用于后续查询
  748. rawData: conversation
  749. }))
  750. // 高亮当前会话
  751. if (ai_conversation_id.value) {
  752. historyData.value.forEach(item => { item.isActive = item.id === ai_conversation_id.value })
  753. }
  754. console.log(`✅ AI写作历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
  755. } else {
  756. console.error('❌ 获取AI写作历史记录列表失败:', response.statusCode)
  757. }
  758. } catch (error) {
  759. console.error('❌ 获取AI写作历史记录列表失败:', error)
  760. } finally {
  761. isLoadingHistory.value = false
  762. }
  763. }
  764. // 时间解析与格式化(与其他移动端一致,容错更强)
  765. const parseToDate = (input) => {
  766. if (!input) return null
  767. if (typeof input === 'number') {
  768. const ms = input < 1e12 ? input * 1000 : input
  769. return new Date(ms)
  770. }
  771. if (typeof input === 'string') {
  772. let d = new Date(input)
  773. if (!isNaN(d)) return d
  774. const normalized = input.replace(/-/g, '/').replace('T', ' ')
  775. d = new Date(normalized)
  776. if (!isNaN(d)) return d
  777. }
  778. return new Date(input)
  779. }
  780. const formatTime = (timeString) => {
  781. const date = parseToDate(timeString)
  782. if (!date || isNaN(date)) return '未知时间'
  783. const now = new Date()
  784. const isToday = date.toDateString() === now.toDateString()
  785. const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
  786. const isYesterday = date.toDateString() === yesterday.toDateString()
  787. if (isToday) {
  788. return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  789. }
  790. if (isYesterday) {
  791. return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  792. }
  793. const month = date.getMonth() + 1
  794. const day = date.getDate()
  795. const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  796. return `${month}月${day}日 ${time}`
  797. }
  798. const goBack = () => {
  799. router.go(-1)
  800. }
  801. const createNewTask = () => {
  802. historyData.value.forEach(item => {
  803. item.isActive = false
  804. })
  805. currentView.value = 'main'
  806. // 重要:重置对话ID为新对话
  807. ai_conversation_id.value = 0
  808. // 重置到默认模板内容
  809. const defaultTemplate = "请帮我生成一份正式的总结报告,要求格式规范、语言严谨。具体内容包括<span class=\"editable-highlight\" contenteditable=\"true\">总结主题:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">总结时间:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">主要业绩和成果:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">存在的问题和不足:</span>、<span class=\"editable-highlight\" contenteditable=\"true\">下一阶段工作计划:</span>的内容。请按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。"
  810. // 重新设置输入框内容
  811. const inputElement = document.querySelector('.template-input-container')
  812. if (inputElement) {
  813. inputElement.innerHTML = defaultTemplate
  814. templateContent.value = defaultTemplate.replace(/<[^>]*>/g, '') // 提取纯文本作为templateContent
  815. // 重新为高亮元素添加样式和交互功能
  816. nextTick(() => {
  817. const highlights = inputElement.querySelectorAll('.editable-highlight')
  818. highlights.forEach(highlight => {
  819. highlight.style.backgroundColor = '#3E7BFA10'
  820. highlight.style.color = '#3E7BFA'
  821. highlight.style.padding = '4px 8px'
  822. highlight.style.borderRadius = '6px'
  823. highlight.style.fontWeight = '500'
  824. highlight.style.cursor = 'text'
  825. highlight.style.border = '1px solid transparent'
  826. highlight.style.display = 'inline-block'
  827. highlight.style.minWidth = '20px'
  828. highlight.style.margin = '6px 8px 6px 0'
  829. highlight.addEventListener('click', () => {
  830. highlight.contentEditable = 'true'
  831. highlight.focus()
  832. })
  833. highlight.addEventListener('blur', () => {
  834. highlight.contentEditable = 'false'
  835. })
  836. })
  837. })
  838. }
  839. editorContent.value = ''
  840. selectedFile.value = null
  841. showHistory.value = false
  842. console.log('✅ 新建任务完成,已重置对话ID为0,准备创建新对话')
  843. }
  844. // 获取指定对话的详细消息 - 复用PC端的实现
  845. const getConversationMessages = async (conversationId) => {
  846. try {
  847. console.log('开始获取AI写作对话消息,conversationId:', conversationId)
  848. const response = await apis.getHistoryRecord({
  849. // ===== 已删除:user_id - 后端从token解析 =====
  850. ai_conversation_id: conversationId,
  851. business_type: 2 // AI写作类型
  852. })
  853. console.log('AI写作对话消息响应:', response)
  854. if (response.statusCode === 200) {
  855. if (!response.data || !Array.isArray(response.data)) {
  856. console.error('历史记录数据格式不正确:', response.data)
  857. return false
  858. }
  859. // 解析消息内容 - 修复JSON解析错误
  860. return response.data.map(msg => {
  861. let parsedContent = null
  862. if (msg.content) {
  863. try {
  864. // 尝试解析JSON,如果失败则直接使用原内容
  865. if (typeof msg.content === 'string' && msg.content.startsWith('{')) {
  866. parsedContent = JSON.parse(msg.content)
  867. } else {
  868. // 如果不是JSON格式,创建一个包装对象
  869. parsedContent = {
  870. hasDocument: false,
  871. content: msg.content
  872. }
  873. }
  874. } catch (error) {
  875. console.log('消息内容不是JSON格式,使用原内容:', msg.content)
  876. parsedContent = {
  877. hasDocument: false,
  878. content: msg.content
  879. }
  880. }
  881. }
  882. return {
  883. type: msg.type,
  884. content: msg.content,
  885. time: formatTime(msg.created_at),
  886. parsedContent: parsedContent
  887. }
  888. })
  889. } else {
  890. console.error('获取历史记录失败:', response.statusCode)
  891. return false
  892. }
  893. } catch (error) {
  894. console.error('获取历史记录失败:', error)
  895. return false
  896. }
  897. }
  898. const handleHistoryItem = async (item) => {
  899. console.log("点击历史记录:", item);
  900. // 设置选中状态
  901. historyData.value.forEach(hItem => {
  902. hItem.isActive = hItem.id === item.id
  903. })
  904. // 设置对话ID
  905. ai_conversation_id.value = item.id
  906. // 获取详细消息
  907. const messages = await getConversationMessages(item.id)
  908. if (messages && messages.length > 0) {
  909. // 找到AI消息中的文档内容
  910. const aiMessage = messages.find(msg => msg.type === 'ai')
  911. if (aiMessage && aiMessage.parsedContent && aiMessage.parsedContent.documentContent) {
  912. // 有文档内容,进入编辑器
  913. editorContent.value = aiMessage.parsedContent.documentContent.content
  914. currentView.value = 'editor'
  915. } else if (aiMessage && aiMessage.parsedContent && aiMessage.parsedContent.content) {
  916. // 如果内容在 parsedContent 中
  917. editorContent.value = aiMessage.parsedContent.content
  918. currentView.value = 'editor'
  919. } else if (aiMessage && aiMessage.content) {
  920. // 直接使用AI消息内容
  921. editorContent.value = aiMessage.content
  922. currentView.value = 'editor'
  923. } else {
  924. // 没有文档内容,显示对话
  925. const inputElement = document.querySelector('.template-input-container')
  926. if (inputElement) {
  927. inputElement.innerHTML = `<p>${item.rawData?.content || item.title}</p>`
  928. templateContent.value = item.rawData?.content || item.title
  929. }
  930. currentView.value = 'main'
  931. }
  932. } else {
  933. // 获取消息失败,显示基本信息
  934. displayToast('加载历史记录失败')
  935. }
  936. showHistory.value = false
  937. }
  938. const deleteHistoryItem = async (historyItem, index) => {
  939. try {
  940. console.log('开始删除移动端历史记录:', historyItem)
  941. const response = await apis.deleteHistoryRecord({
  942. // ===== 已删除:user_id - 后端从token解析 =====
  943. ai_conversation_id: historyItem.id
  944. })
  945. if (response.statusCode === 200) {
  946. // 从本地数据中移除
  947. historyData.value.splice(index, 1)
  948. // 如果删除的是当前激活的历史记录,执行新建任务
  949. if (historyItem.isActive) {
  950. console.log('删除激活的历史记录,执行新建任务')
  951. createNewTask()
  952. }
  953. console.log('✅ 移动端历史记录删除成功')
  954. displayToast('删除成功')
  955. } else {
  956. console.error('❌ 删除移动端历史记录失败:', response)
  957. displayToast('删除失败')
  958. }
  959. } catch (error) {
  960. console.error('❌ 删除移动端历史记录失败:', error)
  961. displayToast('删除失败,请稍后重试')
  962. }
  963. }
  964. onMounted(() => {
  965. console.log('Mobile AI Writing Page Loaded')
  966. // 初始化输入框的默认文本到响应式状态,确保 hasInputContent 初始即正确
  967. nextTick(() => {
  968. const inputElement = document.querySelector('.template-input-container')
  969. if (inputElement) {
  970. const text = (inputElement.textContent || '').trim()
  971. if (text) {
  972. templateContent.value = text
  973. }
  974. // 挂载后为默认模板中的高亮占位元素补齐样式
  975. const highlights = inputElement.querySelectorAll('.editable-highlight')
  976. highlights.forEach(highlight => {
  977. highlight.style.backgroundColor = '#3E7BFA10'
  978. highlight.style.color = '#3E7BFA'
  979. highlight.style.padding = '4px 8px'
  980. highlight.style.borderRadius = '6px'
  981. highlight.style.fontWeight = '500'
  982. highlight.style.cursor = 'text'
  983. highlight.style.border = '1px solid transparent'
  984. highlight.style.display = 'inline-block'
  985. highlight.style.minWidth = '20px'
  986. highlight.addEventListener('click', () => {
  987. highlight.contentEditable = 'true'
  988. highlight.focus()
  989. })
  990. highlight.addEventListener('blur', () => {
  991. highlight.contentEditable = 'false'
  992. })
  993. })
  994. }
  995. })
  996. })
  997. </script>
  998. <style scoped>
  999. /* @import '@/styles/mobile-zoom-fix.css'; */
  1000. .mobile-ai-writing {
  1001. min-height: 100vh;
  1002. background: #f5f5f5;
  1003. padding: 0;
  1004. }
  1005. .mobile-content {
  1006. /* padding: 16px; */
  1007. /* padding-top: 64px; */
  1008. overflow-y: auto;
  1009. max-height: calc(100vh - 64px);
  1010. position: relative;
  1011. }
  1012. /* AI写作卡片 - 完全复用PC端样式 */
  1013. .ai-writing-card {
  1014. background: white;
  1015. border-radius: 12px;
  1016. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  1017. overflow: hidden;
  1018. margin-bottom: 16px;
  1019. position: relative;
  1020. }
  1021. /* 卡片内Loading遮罩 */
  1022. .card-loading-overlay {
  1023. position: absolute;
  1024. top: 0;
  1025. left: 0;
  1026. right: 0;
  1027. bottom: 0;
  1028. background: rgba(255, 255, 255, 0.95);
  1029. display: flex;
  1030. flex-direction: column;
  1031. align-items: center;
  1032. justify-content: center;
  1033. z-index: 10;
  1034. }
  1035. /* 文档生成区域 - 复用PC端样式 */
  1036. .document-generation {
  1037. padding: 20px;
  1038. h3 {
  1039. font-size: 20px;
  1040. font-weight: 600;
  1041. color: #1f2937;
  1042. margin-bottom: 8px;
  1043. text-align: center;
  1044. }
  1045. .subtitle {
  1046. font-size: 14px;
  1047. color: #6b7280;
  1048. margin-bottom: 20px;
  1049. text-align: center;
  1050. }
  1051. }
  1052. .input-area {
  1053. width: 100%;
  1054. }
  1055. /* 模板输入容器 - 移动端重点优化 */
  1056. .template-input-container {
  1057. width: 100%;
  1058. background: #ffffff;
  1059. border: 2px solid #e5e7eb;
  1060. border-radius: 12px;
  1061. padding: 16px;
  1062. margin-bottom: 16px;
  1063. font-size: 16px;
  1064. line-height: 1.6;
  1065. color: #374151;
  1066. min-height: 120px;
  1067. overflow-y: auto;
  1068. resize: none;
  1069. outline: none;
  1070. transition: all 0.3s ease;
  1071. box-sizing: border-box;
  1072. &:focus {
  1073. border-color: #3e7bfa;
  1074. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  1075. }
  1076. &[contenteditable="true"] {
  1077. font-size: 18px !important;
  1078. line-height: 1.6 !important;
  1079. -webkit-text-size-adjust: none !important;
  1080. transform: translateZ(0) !important;
  1081. -webkit-appearance: none !important;
  1082. -moz-appearance: none !important;
  1083. appearance: none !important;
  1084. }
  1085. }
  1086. /* 高亮可编辑元素 */
  1087. .editable-highlight {
  1088. background-color: #3E7BFA10 !important;
  1089. color: #3E7BFA !important;
  1090. padding: 4px 8px !important;
  1091. border-radius: 6px !important;
  1092. font-weight: 500 !important;
  1093. cursor: text !important;
  1094. border: 1px solid transparent !important;
  1095. display: inline-block !important;
  1096. min-width: 20px !important;
  1097. margin: 6px 8px 6px 0 !important;
  1098. transition: all 0.2s ease !important;
  1099. &:hover {
  1100. background-color: #3E7BFA20 !important;
  1101. border-color: #3E7BFA !important;
  1102. }
  1103. &:focus {
  1104. background-color: #ffffff !important;
  1105. border-color: #3E7BFA !important;
  1106. box-shadow: 0 0 0 2px rgba(62, 123, 250, 0.2) !important;
  1107. outline: none !important;
  1108. }
  1109. }
  1110. /* 操作按钮区域 - 移动端适配 */
  1111. .input-actions {
  1112. display: flex;
  1113. justify-content: space-between;
  1114. align-items: center;
  1115. margin-bottom: 12px;
  1116. }
  1117. .left-actions,
  1118. .right-actions {
  1119. display: flex;
  1120. align-items: center;
  1121. gap: 12px;
  1122. }
  1123. .attachment-btn,
  1124. .voice-btn {
  1125. width: 40px;
  1126. height: 40px;
  1127. border: none;
  1128. border-radius: 8px;
  1129. background: none;
  1130. cursor: pointer;
  1131. display: flex;
  1132. align-items: center;
  1133. justify-content: center;
  1134. transition: all 0.2s ease;
  1135. }
  1136. .action-icon {
  1137. width: 45px;
  1138. height: 45px;
  1139. object-fit: contain;
  1140. }
  1141. /* 语音按钮专用图标尺寸 */
  1142. .voice-icon {
  1143. width: 30px;
  1144. height: 30px;
  1145. object-fit: contain;
  1146. }
  1147. .icon-container {
  1148. position: relative;
  1149. }
  1150. .recording-indicator {
  1151. position: absolute;
  1152. top: -2px;
  1153. right: -2px;
  1154. width: 8px;
  1155. height: 8px;
  1156. background: #dc3545;
  1157. border-radius: 50%;
  1158. animation: pulse 1s infinite;
  1159. }
  1160. .divider {
  1161. width: 1px;
  1162. height: 31px;
  1163. background: #d6d5de;
  1164. }
  1165. .send-btn {
  1166. background: none;
  1167. border: none;
  1168. padding: 0;
  1169. cursor: pointer;
  1170. position: relative;
  1171. display: flex;
  1172. align-items: center;
  1173. justify-content: center;
  1174. transition: opacity 0.3s ease;
  1175. &:disabled {
  1176. cursor: not-allowed;
  1177. opacity: 0.6;
  1178. }
  1179. }
  1180. .send-icon {
  1181. width: 90px;
  1182. height: 40px;
  1183. object-fit: contain;
  1184. }
  1185. .generating-text {
  1186. position: absolute;
  1187. bottom: -20px;
  1188. left: 50%;
  1189. transform: translateX(-50%);
  1190. font-size: 12px;
  1191. color: #007bff;
  1192. white-space: nowrap;
  1193. }
  1194. .hint-text {
  1195. text-align: left;
  1196. font-size: 12px;
  1197. color: #9ca3af;
  1198. margin: 0;
  1199. }
  1200. /* 文件预览 */
  1201. .file-preview-inline {
  1202. margin-left: 8px;
  1203. }
  1204. .file-info-inline {
  1205. display: flex;
  1206. align-items: center;
  1207. gap: 4px;
  1208. background: #f3f4f6;
  1209. padding: 4px 8px;
  1210. border-radius: 6px;
  1211. font-size: 12px;
  1212. }
  1213. .file-icon-inline {
  1214. font-size: 14px;
  1215. }
  1216. .file-name-inline {
  1217. max-width: 80px;
  1218. overflow: hidden;
  1219. text-overflow: ellipsis;
  1220. white-space: nowrap;
  1221. }
  1222. .remove-file-inline {
  1223. background: none;
  1224. border: none;
  1225. cursor: pointer;
  1226. padding: 0;
  1227. margin-left: 4px;
  1228. }
  1229. .remove-icon {
  1230. color: #dc2626;
  1231. font-size: 16px;
  1232. line-height: 1;
  1233. }
  1234. /* 文档模板区域 - 移动端适配的PC端样式 */
  1235. .document-templates {
  1236. padding: 0 20px 20px;
  1237. }
  1238. .template-tabs {
  1239. display: flex;
  1240. gap: 20px;
  1241. margin-bottom: 20px;
  1242. border-bottom: 1px solid #e5e7eb;
  1243. overflow-x: auto;
  1244. .tab-item {
  1245. padding: 5px 0;
  1246. font-size: 16px;
  1247. font-weight: 500;
  1248. color: #4b5563;
  1249. cursor: pointer;
  1250. transition: all 0.3s ease;
  1251. border-bottom: 3px solid transparent;
  1252. min-width: 50px;
  1253. text-align: center;
  1254. white-space: nowrap;
  1255. &.active {
  1256. color: #2563eb;
  1257. border-bottom-color: #2563eb;
  1258. }
  1259. }
  1260. }
  1261. .template-cards {
  1262. display: flex;
  1263. flex-direction: column;
  1264. gap: 12px;
  1265. }
  1266. .template-card {
  1267. position: relative;
  1268. /* background: white; */
  1269. border-radius: 8px;
  1270. overflow: hidden;
  1271. /* box-shadow: 0 2px 4px rgba(0,0,0,0.1); */
  1272. transition: transform 0.2s ease;
  1273. &:hover {
  1274. transform: translateY(-2px);
  1275. }
  1276. }
  1277. /* 卡片底色与描边(按类型) */
  1278. .template-card.announcement-btn { background: rgb(222,236,254); border: 1px solid #60A5FA; }
  1279. .template-card.notification-btn { background: rgb(250,245,255); border: 1px solid #C084FC; }
  1280. .template-card.report-btn { background: rgb(250,247,237); border: 1px solid #FB923C; }
  1281. /* 会议纪要同公告、决定同通知 → 已复用按钮类 */
  1282. .template-row {
  1283. display: flex;
  1284. align-items: center;
  1285. gap: 12px;
  1286. padding: 12px 12px 12px 12px;
  1287. background: #ffffff;
  1288. border: 1px solid #E5E8EB;
  1289. border-radius: 10px;
  1290. box-shadow: 0 2px 6px rgba(0,0,0,0.05);
  1291. }
  1292. .template-icon {
  1293. width: 40px;
  1294. height: 40px;
  1295. border-radius: 10px;
  1296. display: flex;
  1297. align-items: center;
  1298. justify-content: center;
  1299. flex-shrink: 0;
  1300. }
  1301. .template-icon-img {
  1302. width: 100%;
  1303. height: 100%;
  1304. object-fit: contain;
  1305. }
  1306. .icon-announcement { background: transparent; border: none; }
  1307. .icon-notification { background: transparent; border: none; }
  1308. .icon-report { background: transparent; border: none; }
  1309. .template-info { flex: 1; text-align: left; }
  1310. .template-title { font-size: 16px; font-weight: 600; color: #1f2937; }
  1311. .template-desc { font-size: 8px; color: #6b7280; margin-top: 4px; }
  1312. .use-template-btn {
  1313. position: absolute;
  1314. right: 12px;
  1315. top: 50%;
  1316. transform: translateY(-50%);
  1317. background: #ffffff !important;
  1318. color: #374151 !important;
  1319. border: 1px solid #E5E8EB !important;
  1320. padding: 8px 12px;
  1321. font-size: 12px;
  1322. font-weight: 600;
  1323. border-radius: 18px;
  1324. cursor: pointer;
  1325. transition: background 0.3s ease, transform 0.2s ease;
  1326. &:hover {
  1327. background: #F9FAFB !important;
  1328. transform: translateY(-50%) scale(1.02);
  1329. }
  1330. /* 按钮跟随配色:文字与描边用主色,底色主色10%透明 */
  1331. &.announcement-btn { color: #1D4ED8 !important; border-color: #1D4ED8 !important; background: rgba(29,78,216,0.06) !important; }
  1332. &.notification-btn { color: #C084FC !important; border-color: #C084FC !important; background: rgba(192,132,252,0.06) !important; }
  1333. &.report-btn { color: #FB923C !important; border-color: #FB923C !important; background: rgba(251,146,60,0.06) !important; }
  1334. }
  1335. /* 编辑器视图 */
  1336. .editor-view {
  1337. background: white;
  1338. border-radius: 12px;
  1339. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1340. overflow: hidden;
  1341. margin: 16px;
  1342. position: relative; /* 为内部遮罩做准备 */
  1343. }
  1344. .editor-loading-overlay {
  1345. position: absolute;
  1346. top: 100px; /* 从编辑器容器开始 */
  1347. left: 16px;
  1348. right: 16px;
  1349. bottom: 0;
  1350. background: rgba(255, 255, 255, 0.95);
  1351. display: flex;
  1352. flex-direction: column;
  1353. align-items: center;
  1354. justify-content: center;
  1355. z-index: 10;
  1356. border-radius: 8px;
  1357. }
  1358. .loading-spinner {
  1359. width: 40px;
  1360. height: 40px;
  1361. border: 3px solid #f3f3f3;
  1362. border-top: 3px solid #007bff;
  1363. border-radius: 50%;
  1364. animation: spin 1s linear infinite;
  1365. margin-bottom: 16px;
  1366. }
  1367. .loading-text {
  1368. font-size: 16px;
  1369. color: #374151;
  1370. font-weight: 500;
  1371. }
  1372. .editor-header {
  1373. display: flex;
  1374. justify-content: space-between;
  1375. align-items: center;
  1376. padding: 16px 20px;
  1377. border-bottom: 1px solid #e5e7eb;
  1378. }
  1379. .editor-title h3 {
  1380. font-size: 18px;
  1381. font-weight: 600;
  1382. color: #1f2937;
  1383. margin: 0 0 4px 0;
  1384. }
  1385. .editor-subtitle {
  1386. font-size: 14px;
  1387. color: #6b7280;
  1388. margin: 0;
  1389. }
  1390. .download-btn {
  1391. border: none;
  1392. background: transparent;
  1393. cursor: pointer;
  1394. transition: opacity 0.3s ease;
  1395. padding: 0;
  1396. &:hover:not(:disabled) {
  1397. opacity: 0.8;
  1398. }
  1399. &:disabled {
  1400. opacity: 0.6;
  1401. cursor: not-allowed;
  1402. }
  1403. }
  1404. .download-icon {
  1405. width: 107px;
  1406. height: 34px;
  1407. }
  1408. /* 富文本编辑器 */
  1409. .editor-container {
  1410. padding: 20px 20px 20px 25px;
  1411. min-height: 400px;
  1412. max-height: calc(100vh - 160px);
  1413. overflow-y: auto;
  1414. background: #fafafa;
  1415. border-radius: 8px;
  1416. margin: 0;
  1417. position: relative; /* 为遮罩定位做准备 */
  1418. }
  1419. .rich-text-editor {
  1420. font-size: 16px;
  1421. line-height: 1.6;
  1422. color: #374151;
  1423. min-height: 350px;
  1424. outline: none;
  1425. /* 优化文档结构显示 */
  1426. h1 {
  1427. color: #1f2937;
  1428. border-bottom: 2px solid #3e7bfa;
  1429. padding-bottom: 8px;
  1430. margin-bottom: 20px;
  1431. font-size: 20px;
  1432. font-weight: 600;
  1433. }
  1434. h2 {
  1435. color: #374151;
  1436. margin-top: 24px;
  1437. margin-bottom: 12px;
  1438. font-size: 18px;
  1439. font-weight: 600;
  1440. }
  1441. h3 {
  1442. color: #374151;
  1443. margin-top: 20px;
  1444. margin-bottom: 10px;
  1445. font-size: 16px;
  1446. font-weight: 500;
  1447. }
  1448. p {
  1449. margin-bottom: 12px;
  1450. text-align: justify;
  1451. }
  1452. ul, ol {
  1453. margin: 12px 0;
  1454. padding-left: 20px;
  1455. li {
  1456. margin-bottom: 6px;
  1457. line-height: 1.7;
  1458. }
  1459. }
  1460. /* 序号标题样式优化 */
  1461. .section-number {
  1462. color: #6b7280;
  1463. font-weight: 600;
  1464. }
  1465. /* 确保内容不超出卡片边缘 */
  1466. max-width: 100%;
  1467. word-wrap: break-word;
  1468. overflow-wrap: break-word;
  1469. }
  1470. /* 动画 */
  1471. @keyframes spin {
  1472. to { transform: rotate(360deg); }
  1473. }
  1474. @keyframes pulse {
  1475. 0%, 100% { opacity: 1; }
  1476. 50% { opacity: 0.5; }
  1477. }
  1478. </style>