BoxMask.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <template>
  2. <transition name="fade-in">
  3. <div class="code-preview-wrapper" v-show="visible" @keydown.s.ctrl.prevent="handleModifyFileContent">
  4. <!-- 顶部信息栏 & 工具栏 -->
  5. <div class="tip-wrapper" v-if="visible">
  6. <div class="name" :title="$file.getFileNameComplete(fileInfo)">
  7. {{ $file.getFileNameComplete(fileInfo) }}
  8. <span class="un-save" v-show="isModify && !codeMirrorOptions.readOnly">(未保存)</span>
  9. </div>
  10. <div class="editor-preveiw">在线预览{{ codeMirrorOptions.readOnly ? '' : ' & 编辑' }}</div>
  11. <div class="tool-wrapper">
  12. <a
  13. class="item download-link"
  14. target="_blank"
  15. :href="$file.getDownloadFilePath(fileInfo)"
  16. :download="$file.getFileNameComplete(fileInfo)"
  17. >
  18. <DownloadOutlined title="下载" />
  19. </a>
  20. <a-tooltip placement="bottom">
  21. <template #title>
  22. 操作提示: <br />
  23. 1. 按 Esc 键可退出查看;<br />
  24. 2. 支持在线编辑、保存、下载
  25. </template>
  26. <div class="item text-wrapper">
  27. <span class="text">操作提示</span>
  28. <BulbOutlined />
  29. </div>
  30. </a-tooltip>
  31. <CloseOutlined class="item" title="关闭预览" @click="closeCodePreview" />
  32. </div>
  33. </div>
  34. <!-- 代码编辑区域 -->
  35. <div class="code-editor-wrapper">
  36. <div class="operate-wrapper">
  37. <SaveOutlined
  38. class="save-icon"
  39. title="保存(ctrl+s)"
  40. v-show="isModify && !codeMirrorOptions.readOnly"
  41. @click="handleModifyFileContent"
  42. />
  43. <a-form class="editor-set-form" :model="codeMirrorOptions" layout="inline" size="small" label-align="right">
  44. <a-form-item class="line-wrapper">
  45. <a-checkbox v-model:checked="codeMirrorOptions.lineWrapping" @change="handleChangeCodeMirrorOption"
  46. >自动换行</a-checkbox
  47. >
  48. </a-form-item>
  49. <a-form-item class="font-size">
  50. <a-select v-model:value="codeMirrorCustomOptions.fontSize" filterable>
  51. <a-select-option v-for="(item, index) in fontSizeList" :key="index" :value="item"
  52. >{{ item }} px</a-select-option
  53. >
  54. </a-select>
  55. </a-form-item>
  56. <a-form-item label="代码语言" class="lanaguage">
  57. <a-select v-model:value="codeMirrorLanguage" filterable @change="handleChangeCodeMirrorOption">
  58. <a-select-option v-for="[key, value] in fileSuffixCodeModeMap.entries()" :key="key" :value="key">{{
  59. value.language
  60. }}</a-select-option>
  61. </a-select>
  62. </a-form-item>
  63. <a-form-item label="主题" class="theme">
  64. <a-select v-model:value="codeMirrorTheme" filterable @change="handleChangeCodeMirrorOption">
  65. <a-select-option value="default">default</a-select-option>
  66. <a-select-option v-for="(item, index) in codeMirrorThemeList" :key="index" :value="item">{{
  67. item
  68. }}</a-select-option>
  69. </a-select>
  70. </a-form-item>
  71. </a-form>
  72. </div>
  73. <a-spin :spinning="codeMirrorLoading">
  74. <Codemirror
  75. class="code-editor"
  76. ref="codemirrorRef"
  77. v-model="codeMirrorText"
  78. :style="{ fontSize: `${codeMirrorCustomOptions.fontSize}px` }"
  79. :extensions="extensions"
  80. :disabled="codeMirrorOptions.readOnly"
  81. :indent-with-tab="true"
  82. :tab-size="codeMirrorOptions.tabSize"
  83. :placeholder="'请输入代码...'"
  84. v-if="isShow"
  85. />
  86. </a-spin>
  87. </div>
  88. </div>
  89. </transition>
  90. </template>
  91. <script setup>
  92. import { ref, reactive, computed, watch, onMounted, onUnmounted, getCurrentInstance } from 'vue'
  93. import { message } from 'ant-design-vue'
  94. import { DownloadOutlined, BulbOutlined, CloseOutlined, SaveOutlined } from '@ant-design/icons-vue'
  95. import { Codemirror } from 'vue-codemirror'
  96. import { basicSetup } from 'codemirror'
  97. import { oneDark } from '@codemirror/theme-one-dark'
  98. import { javascript } from '@codemirror/lang-javascript'
  99. import { html } from '@codemirror/lang-html'
  100. import { css } from '@codemirror/lang-css'
  101. import { json } from '@codemirror/lang-json'
  102. import { xml } from '@codemirror/lang-xml'
  103. // import { EditorView } from '@codemirror/view'
  104. import { fontSizeList, fileSuffixCodeModeMap, codeMirrorThemeList } from '@/libs/map.js'
  105. import { useMyResourceStore } from '@/store/myResource.js'
  106. import { getFilePreview, modifyFileContent } from '@/api/myResource/file.js'
  107. import tool from '@/utils/tool'
  108. const props = defineProps({
  109. fileInfo: Object,
  110. isEdit: Boolean,
  111. callback: Function
  112. })
  113. const myResource = useMyResourceStore()
  114. const visible = ref(false)
  115. defineExpose({ visible })
  116. const originalCodeText = ref('')
  117. const codeMirrorText = ref('')
  118. const codeMirrorLoading = ref(false)
  119. const isShow = ref(true)
  120. const codemirrorRef = ref(null)
  121. const codeMirrorLanguage = ref('html')
  122. const codeMirrorTheme = ref('default')
  123. // CodeMirror 配置
  124. const codeMirrorOptions = reactive({
  125. tabSize: 4,
  126. mode: 'text/html',
  127. theme: 'default',
  128. readOnly: true,
  129. lineNumbers: true,
  130. line: true,
  131. autoCloseBrackets: true,
  132. foldGutter: true,
  133. lineWrapping: true,
  134. gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers']
  135. })
  136. const codeMirrorCustomOptions = reactive({
  137. fontSize: 14
  138. })
  139. // 根据语言和主题生成扩展
  140. const extensions = computed(() => {
  141. const exts = [basicSetup]
  142. // 添加语言支持
  143. switch (codeMirrorLanguage.value) {
  144. case 'js':
  145. case 'javascript':
  146. exts.push(javascript())
  147. break
  148. case 'html':
  149. exts.push(html())
  150. break
  151. case 'css':
  152. exts.push(css())
  153. break
  154. case 'json':
  155. exts.push(json())
  156. break
  157. case 'xml':
  158. exts.push(xml())
  159. break
  160. default:
  161. exts.push(html())
  162. }
  163. // 添加主题
  164. if (codeMirrorTheme.value === 'one-dark') {
  165. exts.push(oneDark)
  166. } else {
  167. // 默认亮色主题
  168. exts.push(
  169. // EditorView.theme({
  170. // '&': {
  171. // height: '100%',
  172. // fontSize: `${codeMirrorCustomOptions.fontSize}px`,
  173. // color: '#e0e0e0' // 添加默认文本颜色为浅灰色
  174. // },
  175. // '.cm-scroller': {
  176. // overflow: 'auto'
  177. // },
  178. // '.cm-content': {
  179. // color: '#e0e0e0' // 确保内容区域文本为浅灰色
  180. // },
  181. // '.cm-line': {
  182. // color: '#e0e0e0' // 确保每行文本为浅灰色
  183. // },
  184. // // 语法高亮颜色
  185. // '.cm-comment': { color: '#6a9955' }, // 注释
  186. // '.cm-keyword': { color: '#569cd6' }, // 关键字
  187. // '.cm-string': { color: '#ce9178' }, // 字符串
  188. // '.cm-number': { color: '#b5cea8' }, // 数字
  189. // '.cm-property': { color: '#9cdcfe' }, // 属性
  190. // '.cm-operator': { color: '#d4d4d4' }, // 操作符
  191. // '.cm-meta': { color: '#dcdcaa' }, // 元数据
  192. // '.cm-atom': { color: '#4ec9b0' }, // 原子
  193. // '.cm-variable': { color: '#9cdcfe' }, // 变量
  194. // '.cm-tag': { color: '#569cd6' }, // 标签
  195. // '.cm-attribute': { color: '#9cdcfe' } // 属性
  196. // })
  197. )
  198. }
  199. // 添加行号
  200. if (codeMirrorOptions.lineNumbers) {
  201. // exts.push(EditorView.lineWrapping)
  202. }
  203. return exts
  204. })
  205. const screenWidth = computed(() => myResource.screenWidth)
  206. const isModify = computed(() => originalCodeText.value !== codeMirrorText.value)
  207. watch(visible, (val) => {
  208. if (val) {
  209. let fileSuffix = props.fileInfo.extendName.toLowerCase()
  210. if (fileSuffix === 'yaml') {
  211. fileSuffix = 'yml'
  212. }
  213. if (fileSuffixCodeModeMap.has(fileSuffix)) {
  214. codeMirrorLanguage.value = fileSuffix
  215. codeMirrorOptions.mode = fileSuffixCodeModeMap.get(fileSuffix).mime
  216. }
  217. codeMirrorOptions.readOnly = !props.isEdit
  218. codeMirrorTheme.value = localStorage.getItem('qiwen_file_codemirror_theme') || 'default'
  219. getCodeText()
  220. document.addEventListener('keyup', handleEscKey)
  221. } else {
  222. document.removeEventListener('keyup', handleEscKey)
  223. }
  224. })
  225. watch(
  226. () => codeMirrorTheme.value,
  227. (val) => {
  228. localStorage.setItem('qiwen_file_codemirror_theme', val)
  229. }
  230. )
  231. const getCodeText = () => {
  232. codeMirrorLoading.value = true
  233. getFilePreview({
  234. userFileId: props.fileInfo.userFileId,
  235. isMin: false,
  236. shareBatchNum: props.fileInfo.shareBatchNum,
  237. extractionCode: props.fileInfo.extractionCode,
  238. token: tool.data.get('TOKEN')
  239. })
  240. .then((res) => {
  241. codeMirrorLoading.value = false
  242. originalCodeText.value = typeof res === 'object' ? JSON.stringify(res) : res
  243. codeMirrorText.value = originalCodeText.value + ''
  244. })
  245. .catch(() => {
  246. codeMirrorLoading.value = false
  247. })
  248. }
  249. const handleModifyFileContent = () => {
  250. if (!isModify.value || codeMirrorOptions.readOnly) {
  251. return false
  252. }
  253. codeMirrorLoading.value = true
  254. modifyFileContent({
  255. userFileId: props.fileInfo.userFileId,
  256. fileContent: codeMirrorText.value
  257. })
  258. .then((res) => {
  259. codeMirrorLoading.value = false
  260. if (res.success) {
  261. message.success('已保存')
  262. getCodeText()
  263. } else {
  264. message.error(res.message)
  265. }
  266. })
  267. .catch((err) => {
  268. codeMirrorLoading.value = false
  269. message.error(err.message)
  270. })
  271. }
  272. const handleChangeCodeMirrorOption = () => {
  273. isShow.value = false
  274. setTimeout(() => {
  275. isShow.value = true
  276. }, 0)
  277. }
  278. const closeCodePreview = () => {
  279. visible.value = false
  280. props.callback('cancel')
  281. }
  282. const handleEscKey = (e) => {
  283. if (e.keyCode === 27) {
  284. closeCodePreview()
  285. }
  286. }
  287. onMounted(() => {
  288. if (visible.value) {
  289. document.addEventListener('keyup', handleEscKey)
  290. }
  291. })
  292. onUnmounted(() => {
  293. document.removeEventListener('keyup', handleEscKey)
  294. })
  295. </script>
  296. <style lang="less" scoped>
  297. @import '@/style/myResource/varibles.less';
  298. @import '@/style/myResource/mixins.less';
  299. .code-preview-wrapper {
  300. position: fixed;
  301. top: 0;
  302. right: 0;
  303. bottom: 0;
  304. left: 0;
  305. overflow: auto;
  306. width: 100%;
  307. height: 100vh;
  308. z-index: 2;
  309. display: flex;
  310. align-items: center;
  311. animation: imgPreviewAnimation 0.3s;
  312. -webkit-animation: imgPreviewAnimation 0.3s; /* Safari and Chrome */
  313. animation-iteration-count: 0.3;
  314. -webkit-animation-iteration-count: 0.3;
  315. animation-fill-mode: forwards;
  316. -webkit-animation-fill-mode: forwards; /* Safari 和 Chrome */
  317. @keyframes imgPreviewAnimation {
  318. 0% {
  319. background: transparent;
  320. }
  321. 100% {
  322. background: rgba(0, 0, 0, 0.8);
  323. }
  324. }
  325. .tip-wrapper {
  326. position: fixed;
  327. top: 0;
  328. left: 0;
  329. z-index: 2;
  330. background: rgba(0, 0, 0, 0.5);
  331. padding: 0 48px;
  332. width: 100%;
  333. height: 48px;
  334. line-height: 48px;
  335. color: #fff;
  336. font-size: 16px;
  337. display: flex;
  338. justify-content: space-between;
  339. .name {
  340. flex: 1;
  341. padding-right: 16px;
  342. text-align: left;
  343. overflow: hidden;
  344. text-overflow: ellipsis;
  345. white-space: nowrap;
  346. .un-save {
  347. color: @warning-color;
  348. font-size: 14px;
  349. }
  350. }
  351. .tool-wrapper {
  352. flex: 1;
  353. display: flex;
  354. justify-content: flex-end;
  355. .item {
  356. margin-left: 16px;
  357. height: 48px;
  358. line-height: 48px;
  359. cursor: pointer;
  360. &:hover {
  361. opacity: 0.7;
  362. }
  363. }
  364. .download-link {
  365. color: inherit;
  366. font-size: 18px;
  367. }
  368. .text-wrapper {
  369. .text {
  370. margin-right: 8px;
  371. }
  372. }
  373. }
  374. }
  375. .code-editor-wrapper {
  376. margin: 56px auto 0 auto;
  377. width: 90vw;
  378. height: calc(100vh - 80px);
  379. .operate-wrapper {
  380. display: flex;
  381. justify-content: space-between;
  382. align-items: center;
  383. border-radius: 8px 8px 0 0;
  384. border-bottom: 1px solid @border-color-base;
  385. padding: 8px 16px;
  386. background: #1e1e1e; /* 修改为暗色背景 */
  387. color: #e0e0e0; /* 添加文字颜色为浅色 */
  388. .save-icon {
  389. font-size: 20px;
  390. cursor: pointer;
  391. color: @Info;
  392. font-weight: 550;
  393. &:hover {
  394. opacity: 0.5;
  395. }
  396. }
  397. .editor-set-form {
  398. flex: 1;
  399. text-align: right;
  400. :deep(.ant-form-item) {
  401. margin-bottom: 0;
  402. /* 修改表单项文字颜色 */
  403. color: #e0e0e0;
  404. .ant-form-item-label > label {
  405. color: #e0e0e0;
  406. }
  407. .ant-checkbox-wrapper {
  408. color: #e0e0e0;
  409. }
  410. &.font-size {
  411. .ant-form-item-control-input-content {
  412. .ant-select {
  413. width: 96px;
  414. }
  415. }
  416. }
  417. &.lanaguage {
  418. .ant-form-item-control-input-content {
  419. .ant-select {
  420. width: 120px;
  421. }
  422. }
  423. }
  424. &.theme {
  425. .ant-form-item-control-input-content {
  426. .ant-select {
  427. width: 190px;
  428. }
  429. }
  430. }
  431. }
  432. }
  433. }
  434. .code-editor {
  435. height: calc(100vh - 129px);
  436. background: #1e1e1e; /* 添加暗色背景 */
  437. :deep(.cm-editor) {
  438. border-radius: 0 0 8px 8px;
  439. height: inherit;
  440. font-size: inherit;
  441. background: #1e1e1e; /* 添加暗色背景 */
  442. color: #e0e0e0; /* 确保编辑器文本为浅灰色 */
  443. * {
  444. font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace !important;
  445. }
  446. .cm-scroller {
  447. width: 100%;
  448. padding: 8px 0 0 0;
  449. line-height: 1.5;
  450. font-size: inherit;
  451. overflow: auto;
  452. background: #1e1e1e; /* 添加暗色背景 */
  453. }
  454. .cm-content {
  455. color: #e0e0e0; /* 确保内容区域文本为浅灰色 */
  456. }
  457. .cm-line {
  458. color: #e0e0e0; /* 确保每行文本为浅灰色 */
  459. }
  460. .cm-gutters {
  461. background: #1e1e1e; /* 行号区域背景 */
  462. border-right: 1px solid #333; /* 行号右侧边框 */
  463. color: #858585; /* 行号颜色 */
  464. }
  465. .cm-activeLineGutter {
  466. background-color: #2c2c2c; /* 当前行的行号背景 */
  467. }
  468. }
  469. }
  470. }
  471. }
  472. </style>