m-AIWriting.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667
  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="请在这里输入您的写作要求..."></div>
  34. <div class="input-actions">
  35. <div class="left-actions">
  36. <button class="attachment-btn" @click="triggerFileUpload" :disabled="isSending">
  37. <img src="@/assets/AIWriting/4.png" alt="附件" class="action-icon" />
  38. </button>
  39. <!-- 文件预览区域 - 显示在输入框右边 -->
  40. <div v-if="selectedFile" class="file-preview-inline">
  41. <div class="file-info-inline">
  42. <span class="file-icon-inline">{{ selectedFile.icon }}</span>
  43. <span class="file-name-inline">{{ selectedFile.name }}</span>
  44. <button class="remove-file-inline" @click="removeSelectedFile">
  45. <span class="remove-icon">×</span>
  46. </button>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="right-actions">
  51. <button class="voice-btn" @click="handleVoiceClick" :disabled="isSending" :class="{ 'recording': isListening }">
  52. <div class="icon-container">
  53. <img src="@/assets/Chat/18.png" alt="语音" class="voice-icon" />
  54. <div v-if="isListening" class="recording-indicator"></div>
  55. </div>
  56. </button>
  57. <div class="divider"></div>
  58. <button class="send-btn" @click="sendAIWritingRequest" :disabled="isGenerating || isSending">
  59. <img
  60. :src="hasInputContent ? sendIconFilled : sendIconEmpty"
  61. alt="发送"
  62. class="send-icon"
  63. />
  64. <span v-if="isGenerating" class="generating-text">生成中...</span>
  65. </button>
  66. </div>
  67. </div>
  68. <p class="hint-text">提示:请输入关键字,AI将根据关键字生成文档</p>
  69. </div>
  70. </div>
  71. <!-- 文档模板区域 - 完全复用PC端 -->
  72. <div class="document-templates">
  73. <div class="template-tabs">
  74. <div
  75. v-for="tab in tabs"
  76. :key="tab.key"
  77. :class="['tab-item', { active: activeTab === tab.key }]"
  78. @click="switchTab(tab.key)"
  79. >
  80. {{ tab.name }}
  81. </div>
  82. </div>
  83. <div class="template-cards">
  84. <div
  85. v-for="template in filteredTemplates"
  86. :key="template.id"
  87. :class="['template-card', template.buttonClass]"
  88. >
  89. <div class="template-row">
  90. <div :class="['template-icon', getIconClass(template.buttonClass)]">
  91. <img :src="template.image" :alt="template.name" class="template-icon-img" />
  92. </div>
  93. <div class="template-info">
  94. <div class="template-title">{{ template.name }}</div>
  95. <div class="template-desc">{{ getDescription(template.name) }}</div>
  96. </div>
  97. <button
  98. :class="['use-template-btn', template.buttonClass]"
  99. @click="useTemplate(template.name)"
  100. >
  101. 使用此模板
  102. </button>
  103. </div>
  104. </div>
  105. </div>
  106. </div>
  107. </div>
  108. <!-- 步骤二:富文本编辑器 -->
  109. <div v-if="currentView === 'editor'" class="editor-view">
  110. <!-- 编辑器Loading遮罩 - 只覆盖编辑器区域 -->
  111. <div v-if="isGenerating" class="editor-loading-overlay">
  112. <div class="loading-spinner"></div>
  113. <div class="loading-text">正在生成文档,请稍候...</div>
  114. </div>
  115. <div class="editor-header">
  116. <div class="editor-title">
  117. <h3>文档预览</h3>
  118. <p class="editor-subtitle" v-if="!isGenerating">编辑请去电脑端</p>
  119. <p class="editor-subtitle" v-else>AI正在生成内容,请稍候...</p>
  120. </div>
  121. <div class="editor-actions">
  122. <button class="download-btn" @click="downloadDocument" :disabled="isGenerating">
  123. <img src="@/assets/Exam/13.png" alt="下载Word" class="download-icon" />
  124. </button>
  125. </div>
  126. </div>
  127. <!-- 富文本编辑器 -->
  128. <div class="editor-container">
  129. <div
  130. ref="richEditor"
  131. class="rich-text-editor"
  132. v-html="editorContent"
  133. contenteditable="false"
  134. ></div>
  135. </div>
  136. </div>
  137. </div>
  138. <!-- 去除弹窗方式,改为直接录音,保留占位注释以便回滚 -->
  139. <!-- 移动端Toast提示组件 -->
  140. <MobileToast
  141. :visible="showToast"
  142. :message="toastMessage"
  143. :duration="2000"
  144. @close="closeToast"
  145. />
  146. </div>
  147. </template>
  148. <script setup>
  149. import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue'
  150. import { useRouter } from 'vue-router'
  151. import MobileHeader from '@/components/MobileHeader.vue'
  152. import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
  153. import MobileToast from '@/components/MobileToast.vue'
  154. import VoiceModal from '@/components/VoiceModal.vue'
  155. // 移除 element-plus 的 ElMessage,改用移动端 Toast
  156. import { apis } from '@/request/apis.js'
  157. // ===== 已删除:getUserId - 不再需要,改用token =====
  158. // import { getUserId } from '@/utils/userManager.js'
  159. import { useSpeechRecognition } from '@/composables/useSpeechRecognition'
  160. // 完全复用PC端的导入
  161. import sendIconEmpty from '@/assets/Chat/15.png'
  162. import sendIconFilled from '@/assets/Chat/16.png'
  163. import announcementIcon from '@/assets/AIWriting/20.png'
  164. import notificationIcon from '@/assets/AIWriting/21.png'
  165. import summaryIcon from '@/assets/AIWriting/22.png'
  166. import meetingIcon from '@/assets/AIWriting/20.png'
  167. import speechIcon from '@/assets/AIWriting/21.png'
  168. const router = useRouter()
  169. // 响应式数据 - 复用PC端逻辑
  170. const currentView = ref('main') // 'main' | 'editor'
  171. const showHistory = ref(false)
  172. const isGenerating = ref(false)
  173. const isSending = ref(false)
  174. // 语音识别(与PC端保持一致)
  175. const {
  176. isSupported: speechSupported,
  177. isListening,
  178. transcript,
  179. error: speechError,
  180. startListening,
  181. stopListening,
  182. } = useSpeechRecognition()
  183. const templateContent = ref('')
  184. const editorContent = ref('')
  185. const selectedFile = ref(null)
  186. const showVoiceModal = ref(false)
  187. const richEditor = ref(null)
  188. const activeTab = ref("all")
  189. // 历史记录相关数据
  190. const historyData = ref([])
  191. const historyTotal = ref(0)
  192. const isLoadingHistory = ref(false)
  193. const ai_conversation_id = ref(0)
  194. // Toast相关状态
  195. const showToast = ref(false)
  196. const toastMessage = ref('')
  197. // 移动端Toast提示方法
  198. const displayToast = (message, duration = 2000) => {
  199. toastMessage.value = message
  200. showToast.value = true
  201. setTimeout(() => {
  202. closeToast()
  203. }, duration)
  204. }
  205. // 关闭Toast
  206. const closeToast = () => {
  207. showToast.value = false
  208. }
  209. // 完全复用PC端的模板配置
  210. const tabs = [
  211. { key: "all", name: "全部" },
  212. { key: "announcement", name: "公告" },
  213. { key: "notification", name: "通知" },
  214. { key: "summary", name: "总结" },
  215. { key: "meeting", name: "会议" },
  216. { key: "speech", name: "决定" },
  217. ];
  218. const templates = [
  219. {
  220. id: 1,
  221. name: "公告模板",
  222. image: announcementIcon,
  223. category: "announcement",
  224. buttonClass: "announcement-btn"
  225. },
  226. {
  227. id: 2,
  228. name: "通知模板",
  229. image: notificationIcon,
  230. category: "notification",
  231. buttonClass: "notification-btn"
  232. },
  233. {
  234. id: 3,
  235. name: "工作汇报模板",
  236. image: summaryIcon,
  237. category: "summary",
  238. buttonClass: "report-btn"
  239. },
  240. {
  241. id: 4,
  242. name: "会议纪要模版",
  243. image: meetingIcon,
  244. category: "meeting",
  245. buttonClass: "announcement-btn"
  246. },
  247. {
  248. id: 5,
  249. name: "决定模版",
  250. image: speechIcon,
  251. category: "speech",
  252. buttonClass: "notification-btn"
  253. }
  254. ];
  255. // 复用PC端的计算属性
  256. const filteredTemplates = computed(() => {
  257. if (activeTab.value === "all") {
  258. return templates;
  259. }
  260. return templates.filter(template => template.category === activeTab.value);
  261. });
  262. // 获取模板描述与图标类,保持与原配色一致
  263. const getDescription = (name) => {
  264. switch (name) {
  265. case '公告模板':
  266. return '适用于各类信息公告'
  267. case '通知模板':
  268. return '适用于各类通知公文'
  269. case '工作汇报模板':
  270. return '适用于各类工作汇报'
  271. case '会议纪要模版':
  272. return '适用于正式会议的记录'
  273. case '决定模版':
  274. return '适用于各类专业的决定文稿'
  275. default:
  276. return '常用办公文档模板'
  277. }
  278. }
  279. const getIconClass = (buttonClass) => {
  280. // 与按钮配色映射
  281. return {
  282. 'announcement-btn': 'icon-announcement',
  283. 'notification-btn': 'icon-notification',
  284. 'report-btn': 'icon-report'
  285. }[buttonClass] || 'icon-announcement'
  286. }
  287. const getEmoji = (name) => {
  288. switch (name) {
  289. case '公告模板': return '📢'
  290. case '通知模板': return '📣'
  291. case '工作汇报模板': return '📝'
  292. case '会议纪要模版': return '🗓️'
  293. case '决定模版': return '✅'
  294. default: return '📄'
  295. }
  296. }
  297. const hasInputContent = computed(() => {
  298. return templateContent.value && templateContent.value.trim().length > 0
  299. });
  300. // 切换标签
  301. const switchTab = (tabKey) => {
  302. activeTab.value = tabKey
  303. }
  304. // 使用模板 - 复用PC端的useTemplate方法
  305. const useTemplate = (templateName) => {
  306. let content = ''
  307. switch (templateName) {
  308. case '公告模板':
  309. content = "<span class=\"editable-highlight\" contenteditable=\"true\">公告主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">发文单位:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">核心内容:</span>\n\n请帮我生成一份正式的公告,要求格式规范、语言严谨,具体参考以上内容,按照标准公告格式生成全文,包括标题、正文、落款等所有要素。"
  310. break
  311. case '通知模板':
  312. content = "<span class=\"editable-highlight\" contenteditable=\"true\">通知主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">通知对象:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">具体事项:</span>\n\n请帮我生成一份正式的通知,要求格式规范、语言严谨,具体参考以上内容,按照标准公文格式生成完整通知,包括文号、标题、正文、落款等所有要素。"
  313. break
  314. case '工作汇报模板':
  315. content = "<span class=\"editable-highlight\" contenteditable=\"true\">总结主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">总结时间:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">主要内容:</span>\n\n请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。"
  316. break
  317. case '会议纪要模版':
  318. content = "<span class=\"editable-highlight\" contenteditable=\"true\">会议主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">会议时间:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">主要议题:</span>\n\n请帮我生成一份正式的会议纪要,要求格式规范、语言严谨,具体参考以上内容,按照标准会议纪要格式生成全文,包含标题、导语、议定事项和落款。"
  319. break
  320. case '决定模版':
  321. content = "<span class=\"editable-highlight\" contenteditable=\"true\">决定主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">决定依据:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">决定内容:</span>\n\n请帮我生成一份正式的决定,要求格式规范、语言严谨,具体参考以上内容,按照标准决定公文格式生成完整文件。"
  322. break
  323. default:
  324. content = "<span class=\"editable-highlight\" contenteditable=\"true\">文档主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">主要内容:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">具体要求:</span>\n\n请帮我生成一份正式的文档,要求格式规范、语言严谨,具体参考以上内容。"
  325. }
  326. // 设置模板内容到输入框
  327. const inputElement = document.querySelector('.template-input-container')
  328. if (inputElement) {
  329. inputElement.innerHTML = content
  330. templateContent.value = content.replace(/<[^>]*>/g, '')
  331. // 确保高亮元素的样式和交互功能
  332. nextTick(() => {
  333. const highlights = inputElement.querySelectorAll('.editable-highlight')
  334. highlights.forEach(highlight => {
  335. highlight.style.backgroundColor = '#3E7BFA10'
  336. highlight.style.color = '#3E7BFA'
  337. highlight.style.padding = '4px 8px'
  338. highlight.style.borderRadius = '6px'
  339. highlight.style.fontWeight = '500'
  340. highlight.style.cursor = 'text'
  341. highlight.style.border = '1px solid transparent'
  342. highlight.style.display = 'inline-block'
  343. highlight.style.minWidth = '20px'
  344. highlight.style.margin = '6px 8px 6px 0'
  345. // 添加点击编辑功能
  346. highlight.addEventListener('click', () => {
  347. highlight.contentEditable = 'true'
  348. highlight.focus()
  349. })
  350. highlight.addEventListener('blur', () => {
  351. highlight.contentEditable = 'false'
  352. })
  353. })
  354. })
  355. }
  356. }
  357. // 处理输入框输入
  358. const handleTemplateInput = (event) => {
  359. const element = event.target
  360. templateContent.value = element.textContent || ''
  361. // 确保高亮元素的样式
  362. nextTick(() => {
  363. const highlights = element.querySelectorAll('.editable-highlight')
  364. highlights.forEach(highlight => {
  365. highlight.style.backgroundColor = '#3E7BFA10'
  366. highlight.style.color = '#3E7BFA'
  367. highlight.style.padding = '4px 8px'
  368. highlight.style.borderRadius = '6px'
  369. highlight.style.fontWeight = '500'
  370. highlight.style.cursor = 'text'
  371. highlight.style.border = '1px solid transparent'
  372. highlight.style.display = 'inline-block'
  373. highlight.style.minWidth = '20px'
  374. highlight.style.margin = '6px 8px 6px 0'
  375. })
  376. })
  377. }
  378. // 处理复制事件
  379. const handleCopy = (event) => {
  380. event.preventDefault()
  381. const selection = window.getSelection()
  382. if (selection.toString().trim()) {
  383. navigator.clipboard.writeText(selection.toString())
  384. }
  385. }
  386. // 处理文件上传
  387. const triggerFileUpload = () => {
  388. const input = document.createElement('input')
  389. input.type = 'file'
  390. input.accept = '.pdf,.doc,.docx,.txt,.jpg,.jpeg,.png'
  391. input.onchange = (event) => {
  392. const file = event.target.files[0]
  393. if (file) {
  394. selectedFile.value = {
  395. name: file.name,
  396. size: file.size,
  397. icon: getFileIcon(file.type)
  398. }
  399. }
  400. }
  401. input.click()
  402. }
  403. // 获取文件图标
  404. const getFileIcon = (fileType) => {
  405. if (fileType.includes('pdf')) return '📄'
  406. if (fileType.includes('word') || fileType.includes('document')) return '📝'
  407. if (fileType.includes('image')) return '🖼️'
  408. return '📎'
  409. }
  410. // 移除文件
  411. const removeSelectedFile = () => {
  412. selectedFile.value = null
  413. }
  414. // 处理语音点击(直接开始/停止录音,与PC端一致)
  415. const handleVoiceClick = () => {
  416. if (!speechSupported.value) {
  417. displayToast('当前浏览器不支持语音识别')
  418. return
  419. }
  420. if (isListening.value) {
  421. stopListening()
  422. } else {
  423. // 开始前清空上次结果
  424. // 由组合函数内部管理 transcript
  425. startListening()
  426. }
  427. }
  428. // 监听语音识别转写内容,实时写入输入框
  429. watch(transcript, (val) => {
  430. if (!val) return
  431. const inputElement = document.querySelector('.template-input-container')
  432. if (inputElement) {
  433. inputElement.textContent = val
  434. templateContent.value = val
  435. }
  436. })
  437. // 监听语音识别错误
  438. watch(speechError, (err) => {
  439. if (err) displayToast(String(err))
  440. })
  441. // 发送AI写作请求 - 调用真实API接口
  442. const sendAIWritingRequest = async () => {
  443. if (!hasInputContent.value) {
  444. console.log("请输入内容");
  445. return;
  446. }
  447. // 直接进入富文本编辑器并显示loading
  448. currentView.value = 'editor';
  449. isGenerating.value = true;
  450. try {
  451. console.log('开始调用AI写作API...');
  452. console.log('对话ID:', ai_conversation_id.value);
  453. console.log('消息内容:', templateContent.value);
  454. // 调用真实的AI写作接口 - 与PC端保持一致
  455. const response = await apis.sendDeepseekMessage({
  456. // ===== 已删除:user_id - 后端从token解析 =====
  457. ai_conversation_id: ai_conversation_id.value,
  458. message: templateContent.value,
  459. business_type: 2 // AI写作类型
  460. });
  461. console.log('AI写作API响应:', response);
  462. if (response.statusCode === 200) {
  463. // 设置对话ID
  464. if (response.data && response.data.ai_conversation_id) {
  465. ai_conversation_id.value = response.data.ai_conversation_id;
  466. }
  467. // 设置文档内容 - 与PC端保持一致
  468. const aiReply = response.data.reply;
  469. if (aiReply) {
  470. // 直接使用AI回复内容
  471. editorContent.value = aiReply;
  472. } else {
  473. // 如果没有AI回复,生成默认内容
  474. const mockContent = generateMockContent(templateContent.value);
  475. editorContent.value = mockContent;
  476. }
  477. console.log('AI写作成功,文档内容已设置');
  478. } else {
  479. console.error('AI写作失败:', response);
  480. displayToast('生成失败,请重试');
  481. // 生成模拟内容作为后备
  482. const mockContent = generateMockContent(templateContent.value);
  483. editorContent.value = mockContent;
  484. }
  485. isGenerating.value = false;
  486. } catch (error) {
  487. console.error('AI写作API调用失败:', error);
  488. displayToast('网络错误,请重试');
  489. // 生成模拟内容作为后备
  490. const mockContent = generateMockContent(templateContent.value);
  491. editorContent.value = mockContent;
  492. isGenerating.value = false;
  493. }
  494. }
  495. // 生成模拟内容
  496. const generateMockContent = (inputText) => {
  497. return `
  498. <div class="document-content">
  499. <h1>总结报告</h1>
  500. <div class="content-body">
  501. <h2>一、工作总结</h2>
  502. <p>根据您的要求,以下是详细的总结报告内容:</p>
  503. <p>${inputText}</p>
  504. <h2>二、主要成果</h2>
  505. <ul>
  506. <li>完成项目目标,达成率100%</li>
  507. <li>团队协作效率提升30%</li>
  508. <li>客户满意度达到95%</li>
  509. </ul>
  510. <h2>三、存在的问题</h2>
  511. <p>在执行过程中,我们发现了以下问题和不足:</p>
  512. <ul>
  513. <li>时间安排有待优化</li>
  514. <li>资源配置需要调整</li>
  515. <li>沟通机制需要完善</li>
  516. </ul>
  517. <h2>四、下阶段计划</h2>
  518. <p>基于前期工作总结,下阶段将重点关注以下几个方面:</p>
  519. <ul>
  520. <li>优化工作流程,提高效率</li>
  521. <li>加强团队协作,改善沟通</li>
  522. <li>完善制度体系,确保执行</li>
  523. </ul>
  524. </div>
  525. </div>
  526. `;
  527. }
  528. // 下载文档 - 复用PC端的实现
  529. const downloadDocument = async () => {
  530. if (!editorContent.value) {
  531. displayToast('没有可下载的内容')
  532. return
  533. }
  534. try {
  535. console.log('开始下载AI写作文档...')
  536. // 提取文档标题
  537. const documentTitle = extractDocumentTitle(editorContent.value);
  538. // 创建改进的HTML格式Word文档内容(与PC端保持一致)
  539. const wordContent = createImprovedWordContent(editorContent.value, documentTitle);
  540. // 创建Blob对象 - 使用Word兼容的MIME类型
  541. const blob = new Blob([wordContent], {
  542. type: 'application/msword'
  543. });
  544. // 下载文件
  545. const url = URL.createObjectURL(blob)
  546. const a = document.createElement('a')
  547. a.href = url
  548. a.download = `${documentTitle}.doc`
  549. document.body.appendChild(a)
  550. a.click()
  551. document.body.removeChild(a)
  552. URL.revokeObjectURL(url)
  553. displayToast('Word文档下载成功!')
  554. console.log('✅ AI写作文档下载成功')
  555. } catch (error) {
  556. console.error('下载Word文档失败:', error)
  557. displayToast('下载失败,请重试')
  558. }
  559. }
  560. // 提取文档标题
  561. const extractDocumentTitle = (content) => {
  562. // 先移除HTML标签,获取纯文本
  563. const textContent = content.replace(/<[^>]*>/g, '');
  564. // 尝试从内容中提取标题
  565. // 1. 查找第一个段落或标题
  566. const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
  567. if (h1Match) {
  568. return h1Match[1].trim();
  569. }
  570. // 2. 查找第一个h2标题
  571. const h2Match = content.match(/<h2[^>]*>([^<]+)<\/h2>/i);
  572. if (h2Match) {
  573. return h2Match[1].trim();
  574. }
  575. // 3. 查找第一个h3标题
  576. const h3Match = content.match(/<h3[^>]*>([^<]+)<\/h3>/i);
  577. if (h3Match) {
  578. return h3Match[1].trim();
  579. }
  580. // 4. 如果没有标题标签,取前50个字符作为标题
  581. const firstSentence = textContent.trim().split(/[。!?]/)[0];
  582. if (firstSentence && firstSentence.length > 0) {
  583. return firstSentence.length > 30 ? firstSentence.substring(0, 30) + '...' : firstSentence;
  584. }
  585. // 5. 最后兜底,使用日期作为标题
  586. return `AI写作文档_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}`;
  587. };
  588. // 创建改进的HTML格式Word文档内容 - 与PC端保持一致
  589. const createImprovedWordContent = (content, title = 'AI生成的文档') => {
  590. // 创建HTML格式的文档内容,兼容Microsoft Office Word
  591. let htmlContent = `<!DOCTYPE html>
  592. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  593. xmlns:w="urn:schemas-microsoft-com:office:word"
  594. xmlns="http://www.w3.org/TR/REC-html40">
  595. <head>
  596. <meta charset="utf-8">
  597. <meta name="ProgId" content="Word.Document">
  598. <meta name="Generator" content="Microsoft Word 15">
  599. <meta name="Originator" content="Microsoft Word 15">
  600. <title>${title}</title>
  601. <!--[if gte mso 9]>
  602. <xml>
  603. <w:WordDocument>
  604. <w:View>Print</w:View>
  605. <w:Zoom>100</w:Zoom>
  606. <w:DoNotOptimizeForBrowser/>
  607. <w:ValidateAgainstSchemas/>
  608. <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>
  609. <w:IgnoreMixedContent>false</w:IgnoreMixedContent>
  610. <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>
  611. <w:Compatibility>
  612. <w:BreakWrappedTables/>
  613. <w:SnapToGridInCell/>
  614. <w:WrapTextWithPunct/>
  615. <w:UseAsianBreakRules/>
  616. <w:DontGrowAutofit/>
  617. </w:Compatibility>
  618. </w:WordDocument>
  619. </xml>
  620. <![endif]-->
  621. <style>
  622. @page {
  623. size: 21cm 29.7cm;
  624. margin: 2.5cm 2cm 2.5cm 2cm;
  625. }
  626. body {
  627. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  628. font-size: 14px;
  629. line-height: 1.6;
  630. margin: 24px;
  631. color: #000;
  632. }
  633. .header {
  634. text-align: center;
  635. margin-bottom: 1.5cm;
  636. page-break-after: avoid;
  637. }
  638. h1 {
  639. font-size: 16pt;
  640. font-weight: bold;
  641. color: #000;
  642. margin-top: 24pt;
  643. margin-bottom: 12pt;
  644. text-align: center;
  645. }
  646. h2 {
  647. font-size: 14pt;
  648. font-weight: bold;
  649. color: #000;
  650. margin-top: 18pt;
  651. margin-bottom: 9pt;
  652. }
  653. h3 {
  654. font-size: 12pt;
  655. font-weight: bold;
  656. color: #000;
  657. margin-top: 14pt;
  658. margin-bottom: 7pt;
  659. }
  660. p {
  661. margin-bottom: 12pt;
  662. text-align: justify;
  663. text-indent: 2em;
  664. }
  665. ul, ol {
  666. margin-left: 20pt;
  667. margin-bottom: 12pt;
  668. }
  669. li {
  670. margin-bottom: 6pt;
  671. }
  672. strong, b {
  673. font-weight: bold;
  674. }
  675. em, i {
  676. font-style: italic;
  677. }
  678. table {
  679. border-collapse: collapse;
  680. width: 100%;
  681. margin: 12pt 0;
  682. }
  683. th, td {
  684. border: 1pt solid #000;
  685. padding: 6pt;
  686. text-align: left;
  687. }
  688. .no-indent {
  689. text-indent: 0;
  690. }
  691. </style>
  692. </head>
  693. <body>
  694. <div>
  695. ${content}
  696. </div>
  697. </body>
  698. </html>`;
  699. return htmlContent;
  700. }
  701. // 处理编辑器输入
  702. const handleEditorInput = (event) => {
  703. editorContent.value = event.target.innerHTML
  704. }
  705. // 生成对话标题
  706. const generateConversationTitle = (content) => {
  707. if (!content) return 'AI写作对话'
  708. // 取前30个字符作为标题
  709. const title = content.replace(/<[^>]*>/g, '').trim()
  710. return title.length > 30 ? title.substring(0, 30) + '...' : title
  711. }
  712. // 历史记录相关方法
  713. const showHistoryDrawer = () => {
  714. if (!isGenerating.value) {
  715. showHistory.value = true
  716. // 每次打开都刷新,避免回显旧数据
  717. getHistoryRecordList()
  718. }
  719. // AI处理中时不执行任何操作,不记录点击意图
  720. }
  721. // 获取历史记录列表
  722. const getHistoryRecordList = async () => {
  723. try {
  724. console.log('📋 开始获取AI写作历史记录列表...')
  725. isLoadingHistory.value = true
  726. const startTime = performance.now()
  727. const response = await apis.getHistoryRecord({
  728. // ===== 已删除:user_id - 后端从token解析 =====
  729. ai_conversation_id: 0, // 0表示获取对话列表
  730. business_type: 2 // AI写作类型
  731. })
  732. const endTime = performance.now()
  733. console.log(`📋 AI写作历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
  734. console.log('📋 AI写作历史记录列表响应:', response)
  735. if (response.statusCode === 200) {
  736. // 设置历史记录总数
  737. historyTotal.value = response.total || 0
  738. // 转换后端数据为前端格式
  739. historyData.value = response.data.map(conversation => ({
  740. id: conversation.id,
  741. title: generateConversationTitle(conversation.content),
  742. time: formatTime(conversation.updated_at),
  743. businessType: conversation.business_type,
  744. isActive: false,
  745. // 保存原始数据用于后续查询
  746. rawData: conversation
  747. }))
  748. // 高亮当前会话
  749. if (ai_conversation_id.value) {
  750. historyData.value.forEach(item => { item.isActive = item.id === ai_conversation_id.value })
  751. }
  752. console.log(`✅ AI写作历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
  753. } else {
  754. console.error('❌ 获取AI写作历史记录列表失败:', response.statusCode)
  755. }
  756. } catch (error) {
  757. console.error('❌ 获取AI写作历史记录列表失败:', error)
  758. } finally {
  759. isLoadingHistory.value = false
  760. }
  761. }
  762. // 时间解析与格式化(与其他移动端一致,容错更强)
  763. const parseToDate = (input) => {
  764. if (!input) return null
  765. if (typeof input === 'number') {
  766. const ms = input < 1e12 ? input * 1000 : input
  767. return new Date(ms)
  768. }
  769. if (typeof input === 'string') {
  770. let d = new Date(input)
  771. if (!isNaN(d)) return d
  772. const normalized = input.replace(/-/g, '/').replace('T', ' ')
  773. d = new Date(normalized)
  774. if (!isNaN(d)) return d
  775. }
  776. return new Date(input)
  777. }
  778. const formatTime = (timeString) => {
  779. const date = parseToDate(timeString)
  780. if (!date || isNaN(date)) return '未知时间'
  781. const now = new Date()
  782. const isToday = date.toDateString() === now.toDateString()
  783. const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
  784. const isYesterday = date.toDateString() === yesterday.toDateString()
  785. if (isToday) {
  786. return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  787. }
  788. if (isYesterday) {
  789. return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  790. }
  791. const month = date.getMonth() + 1
  792. const day = date.getDate()
  793. const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  794. return `${month}月${day}日 ${time}`
  795. }
  796. const goBack = () => {
  797. router.go(-1)
  798. }
  799. const createNewTask = () => {
  800. historyData.value.forEach(item => {
  801. item.isActive = false
  802. })
  803. currentView.value = 'main'
  804. // 重要:重置对话ID为新对话
  805. ai_conversation_id.value = 0
  806. // 重置到默认模板内容
  807. const defaultTemplate = "<span class=\"editable-highlight\" contenteditable=\"true\">总结主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">总结时间:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">主要内容:</span>\n\n请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。"
  808. // 重新设置输入框内容
  809. const inputElement = document.querySelector('.template-input-container')
  810. if (inputElement) {
  811. inputElement.innerHTML = defaultTemplate
  812. templateContent.value = defaultTemplate.replace(/<[^>]*>/g, '') // 提取纯文本作为templateContent
  813. // 重新为高亮元素添加样式和交互功能
  814. nextTick(() => {
  815. const highlights = inputElement.querySelectorAll('.editable-highlight')
  816. highlights.forEach(highlight => {
  817. highlight.style.backgroundColor = '#3E7BFA10'
  818. highlight.style.color = '#3E7BFA'
  819. highlight.style.padding = '4px 8px'
  820. highlight.style.borderRadius = '6px'
  821. highlight.style.fontWeight = '500'
  822. highlight.style.cursor = 'text'
  823. highlight.style.border = '1px solid transparent'
  824. highlight.style.display = 'inline-block'
  825. highlight.style.minWidth = '20px'
  826. highlight.style.margin = '6px 8px 6px 0'
  827. highlight.addEventListener('click', () => {
  828. highlight.contentEditable = 'true'
  829. highlight.focus()
  830. })
  831. highlight.addEventListener('blur', () => {
  832. highlight.contentEditable = 'false'
  833. })
  834. })
  835. })
  836. }
  837. editorContent.value = ''
  838. selectedFile.value = null
  839. showHistory.value = false
  840. console.log('✅ 新建任务完成,已重置对话ID为0,准备创建新对话')
  841. }
  842. // 获取指定对话的详细消息 - 复用PC端的实现
  843. const getConversationMessages = async (conversationId) => {
  844. try {
  845. console.log('开始获取AI写作对话消息,conversationId:', conversationId)
  846. const response = await apis.getHistoryRecord({
  847. // ===== 已删除:user_id - 后端从token解析 =====
  848. ai_conversation_id: conversationId,
  849. business_type: 2 // AI写作类型
  850. })
  851. console.log('AI写作对话消息响应:', response)
  852. if (response.statusCode === 200) {
  853. if (!response.data || !Array.isArray(response.data)) {
  854. console.error('历史记录数据格式不正确:', response.data)
  855. return false
  856. }
  857. // 解析消息内容 - 修复JSON解析错误
  858. return response.data.map(msg => {
  859. let parsedContent = null
  860. if (msg.content) {
  861. try {
  862. // 尝试解析JSON,如果失败则直接使用原内容
  863. if (typeof msg.content === 'string' && msg.content.startsWith('{')) {
  864. parsedContent = JSON.parse(msg.content)
  865. } else {
  866. // 如果不是JSON格式,创建一个包装对象
  867. parsedContent = {
  868. hasDocument: false,
  869. content: msg.content
  870. }
  871. }
  872. } catch (error) {
  873. console.log('消息内容不是JSON格式,使用原内容:', msg.content)
  874. parsedContent = {
  875. hasDocument: false,
  876. content: msg.content
  877. }
  878. }
  879. }
  880. return {
  881. type: msg.type,
  882. content: msg.content,
  883. time: formatTime(msg.created_at),
  884. parsedContent: parsedContent
  885. }
  886. })
  887. } else {
  888. console.error('获取历史记录失败:', response.statusCode)
  889. return false
  890. }
  891. } catch (error) {
  892. console.error('获取历史记录失败:', error)
  893. return false
  894. }
  895. }
  896. const handleHistoryItem = async (item) => {
  897. console.log("点击历史记录:", item);
  898. // 设置选中状态
  899. historyData.value.forEach(hItem => {
  900. hItem.isActive = hItem.id === item.id
  901. })
  902. // 设置对话ID
  903. ai_conversation_id.value = item.id
  904. // 获取详细消息
  905. const messages = await getConversationMessages(item.id)
  906. if (messages && messages.length > 0) {
  907. // 找到AI消息中的文档内容
  908. const aiMessage = messages.find(msg => msg.type === 'ai')
  909. if (aiMessage && aiMessage.parsedContent && aiMessage.parsedContent.documentContent) {
  910. // 有文档内容,进入编辑器
  911. editorContent.value = aiMessage.parsedContent.documentContent.content
  912. currentView.value = 'editor'
  913. } else if (aiMessage && aiMessage.parsedContent && aiMessage.parsedContent.content) {
  914. // 如果内容在 parsedContent 中
  915. editorContent.value = aiMessage.parsedContent.content
  916. currentView.value = 'editor'
  917. } else if (aiMessage && aiMessage.content) {
  918. // 直接使用AI消息内容
  919. editorContent.value = aiMessage.content
  920. currentView.value = 'editor'
  921. } else {
  922. // 没有文档内容,显示对话
  923. const inputElement = document.querySelector('.template-input-container')
  924. if (inputElement) {
  925. inputElement.innerHTML = `<p>${item.rawData?.content || item.title}</p>`
  926. templateContent.value = item.rawData?.content || item.title
  927. }
  928. currentView.value = 'main'
  929. }
  930. } else {
  931. // 获取消息失败,显示基本信息
  932. displayToast('加载历史记录失败')
  933. }
  934. showHistory.value = false
  935. }
  936. const deleteHistoryItem = async (historyItem, index) => {
  937. try {
  938. console.log('开始删除移动端历史记录:', historyItem)
  939. const response = await apis.deleteHistoryRecord({
  940. // ===== 已删除:user_id - 后端从token解析 =====
  941. ai_conversation_id: historyItem.id
  942. })
  943. if (response.statusCode === 200) {
  944. // 从本地数据中移除
  945. historyData.value.splice(index, 1)
  946. // 如果删除的是当前激活的历史记录,执行新建任务
  947. if (historyItem.isActive) {
  948. console.log('删除激活的历史记录,执行新建任务')
  949. createNewTask()
  950. }
  951. console.log('✅ 移动端历史记录删除成功')
  952. displayToast('删除成功')
  953. } else {
  954. console.error('❌ 删除移动端历史记录失败:', response)
  955. displayToast('删除失败')
  956. }
  957. } catch (error) {
  958. console.error('❌ 删除移动端历史记录失败:', error)
  959. displayToast('删除失败,请稍后重试')
  960. }
  961. }
  962. onMounted(() => {
  963. console.log('Mobile AI Writing Page Loaded')
  964. // 初始化输入框的默认模板内容
  965. nextTick(() => {
  966. const inputElement = document.querySelector('.template-input-container')
  967. if (inputElement) {
  968. // 设置默认模板内容(工作汇报模板)
  969. const defaultTemplate = "<span class=\"editable-highlight\" contenteditable=\"true\">总结主题:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">总结时间:</span>\n<span class=\"editable-highlight\" contenteditable=\"true\">主要内容:</span>\n\n请帮我生成一份正式的总结报告,要求格式规范、语言严谨,具体参考以上内容,按照标准工作总结格式生成全文,包含\"工作总结、问题不足、未来计划\"三部分的完整报告。"
  970. inputElement.innerHTML = defaultTemplate
  971. templateContent.value = defaultTemplate.replace(/<[^>]*>/g, '')
  972. // 为高亮元素添加样式和交互功能
  973. const highlights = inputElement.querySelectorAll('.editable-highlight')
  974. highlights.forEach(highlight => {
  975. highlight.style.backgroundColor = '#3E7BFA10'
  976. highlight.style.color = '#3E7BFA'
  977. highlight.style.padding = '4px 8px'
  978. highlight.style.borderRadius = '6px'
  979. highlight.style.fontWeight = '500'
  980. highlight.style.cursor = 'text'
  981. highlight.style.border = '1px solid transparent'
  982. highlight.style.display = 'inline-block'
  983. highlight.style.minWidth = '20px'
  984. highlight.style.margin = '6px 8px 6px 0'
  985. highlight.addEventListener('click', () => {
  986. highlight.contentEditable = 'true'
  987. highlight.focus()
  988. })
  989. highlight.addEventListener('blur', () => {
  990. highlight.contentEditable = 'false'
  991. })
  992. })
  993. }
  994. })
  995. })
  996. </script>
  997. <style scoped>
  998. /* @import '@/styles/mobile-zoom-fix.css'; */
  999. .mobile-ai-writing {
  1000. min-height: 100vh;
  1001. background: #f5f5f5;
  1002. padding: 0;
  1003. }
  1004. .mobile-content {
  1005. /* padding: 16px; */
  1006. /* padding-top: 64px; */
  1007. overflow-y: auto;
  1008. max-height: calc(100vh - 64px);
  1009. position: relative;
  1010. }
  1011. /* AI写作卡片 - 完全复用PC端样式 */
  1012. .ai-writing-card {
  1013. background: white;
  1014. border-radius: 12px;
  1015. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  1016. overflow: hidden;
  1017. margin-bottom: 16px;
  1018. position: relative;
  1019. }
  1020. /* 卡片内Loading遮罩 */
  1021. .card-loading-overlay {
  1022. position: absolute;
  1023. top: 0;
  1024. left: 0;
  1025. right: 0;
  1026. bottom: 0;
  1027. background: rgba(255, 255, 255, 0.95);
  1028. display: flex;
  1029. flex-direction: column;
  1030. align-items: center;
  1031. justify-content: center;
  1032. z-index: 10;
  1033. }
  1034. /* 文档生成区域 - 复用PC端样式 */
  1035. .document-generation {
  1036. padding: 20px;
  1037. h3 {
  1038. font-size: 20px;
  1039. font-weight: 600;
  1040. color: #1f2937;
  1041. margin-bottom: 8px;
  1042. text-align: center;
  1043. }
  1044. .subtitle {
  1045. font-size: 14px;
  1046. color: #6b7280;
  1047. margin-bottom: 20px;
  1048. text-align: center;
  1049. }
  1050. }
  1051. .input-area {
  1052. width: 100%;
  1053. }
  1054. /* 模板输入容器 - 移动端重点优化 */
  1055. .template-input-container {
  1056. width: 100%;
  1057. background: #ffffff;
  1058. border: 2px solid #e5e7eb;
  1059. border-radius: 12px;
  1060. padding: 16px;
  1061. margin-bottom: 16px;
  1062. font-size: 16px;
  1063. line-height: 1.6;
  1064. color: #374151;
  1065. min-height: 120px;
  1066. overflow-y: auto;
  1067. resize: none;
  1068. outline: none;
  1069. transition: all 0.3s ease;
  1070. box-sizing: border-box;
  1071. white-space: pre-line; /* 保留换行符,确保每个关键信息独占一行 */
  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>