TextStylePanel.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <template>
  2. <div class="text-style-panel">
  3. <div class="preset-style">
  4. <div
  5. class="preset-style-item"
  6. v-for="item in presetStyles"
  7. :key="item.label"
  8. :style="item.style"
  9. @click="emitBatchRichTextCommand(item.cmd)"
  10. >{{item.label}}</div>
  11. </div>
  12. <Divider />
  13. <InputGroup compact class="row">
  14. <Select
  15. style="flex: 3;"
  16. :value="richTextAttrs.fontname"
  17. @change="value => emitRichTextCommand('fontname', value)"
  18. >
  19. <template #suffixIcon><IconFontSize /></template>
  20. <SelectOptGroup label="系统字体">
  21. <SelectOption v-for="font in availableFonts" :key="font.en" :value="font.en">
  22. <span :style="{ fontFamily: font.en }">{{font.zh}}</span>
  23. </SelectOption>
  24. </SelectOptGroup>
  25. <SelectOptGroup label="在线字体">
  26. <SelectOption v-for="font in webFonts" :key="font.name" :value="font.name">
  27. <span>{{font.name}}</span>
  28. </SelectOption>
  29. </SelectOptGroup>
  30. </Select>
  31. <Select
  32. style="flex: 2;"
  33. :value="richTextAttrs.fontsize"
  34. @change="value => emitRichTextCommand('fontsize', value)"
  35. >
  36. <template #suffixIcon><IconAddText /></template>
  37. <SelectOption v-for="fontsize in fontSizeOptions" :key="fontsize" :value="fontsize">
  38. {{fontsize}}
  39. </SelectOption>
  40. </Select>
  41. </InputGroup>
  42. <ButtonGroup class="row">
  43. <Popover trigger="click">
  44. <template #content>
  45. <ColorPicker
  46. :modelValue="richTextAttrs.color"
  47. @update:modelValue="value => emitRichTextCommand('color', value)"
  48. />
  49. </template>
  50. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
  51. <Button class="text-color-btn" style="flex: 1;">
  52. <IconText />
  53. <div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
  54. </Button>
  55. </Tooltip>
  56. </Popover>
  57. <Popover trigger="click">
  58. <template #content>
  59. <ColorPicker
  60. :modelValue="richTextAttrs.backcolor"
  61. @update:modelValue="value => emitRichTextCommand('backcolor', value)"
  62. />
  63. </template>
  64. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
  65. <Button class="text-color-btn" style="flex: 1;">
  66. <IconBackgroundColor />
  67. <div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
  68. </Button>
  69. </Tooltip>
  70. </Popover>
  71. <Popover trigger="click">
  72. <template #content>
  73. <ColorPicker
  74. :modelValue="fill"
  75. @update:modelValue="value => updateFill(value)"
  76. />
  77. </template>
  78. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文本框填充">
  79. <Button class="text-color-btn" style="flex: 1;">
  80. <IconFill />
  81. <div class="text-color-block" :style="{ backgroundColor: fill }"></div>
  82. </Button>
  83. </Tooltip>
  84. </Popover>
  85. </ButtonGroup>
  86. <CheckboxButtonGroup class="row">
  87. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="加粗">
  88. <CheckboxButton
  89. style="flex: 1;"
  90. :checked="richTextAttrs.bold"
  91. @click="emitRichTextCommand('bold')"
  92. ><IconTextBold /></CheckboxButton>
  93. </Tooltip>
  94. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="斜体">
  95. <CheckboxButton
  96. style="flex: 1;"
  97. :checked="richTextAttrs.em"
  98. @click="emitRichTextCommand('em')"
  99. ><IconTextItalic /></CheckboxButton>
  100. </Tooltip>
  101. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="下划线">
  102. <CheckboxButton
  103. style="flex: 1;"
  104. :checked="richTextAttrs.underline"
  105. @click="emitRichTextCommand('underline')"
  106. ><IconTextUnderline /></CheckboxButton>
  107. </Tooltip>
  108. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除线">
  109. <CheckboxButton
  110. style="flex: 1;"
  111. :checked="richTextAttrs.strikethrough"
  112. @click="emitRichTextCommand('strikethrough')"
  113. ><IconStrikethrough /></CheckboxButton>
  114. </Tooltip>
  115. </CheckboxButtonGroup>
  116. <CheckboxButtonGroup class="row">
  117. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="上标">
  118. <CheckboxButton
  119. style="flex: 1;"
  120. :checked="richTextAttrs.superscript"
  121. @click="emitRichTextCommand('superscript')"
  122. ><IconUpOne /></CheckboxButton>
  123. </Tooltip>
  124. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="下标">
  125. <CheckboxButton
  126. style="flex: 1;"
  127. :checked="richTextAttrs.subscript"
  128. @click="emitRichTextCommand('subscript')"
  129. ><IconDownOne /></CheckboxButton>
  130. </Tooltip>
  131. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="行内代码">
  132. <CheckboxButton
  133. style="flex: 1;"
  134. :checked="richTextAttrs.code"
  135. @click="emitRichTextCommand('code')"
  136. ><IconCode /></CheckboxButton>
  137. </Tooltip>
  138. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="引用">
  139. <CheckboxButton
  140. style="flex: 1;"
  141. :checked="richTextAttrs.blockquote"
  142. @click="emitRichTextCommand('blockquote')"
  143. ><IconQuote /></CheckboxButton>
  144. </Tooltip>
  145. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
  146. <CheckboxButton
  147. style="flex: 1;"
  148. @click="emitRichTextCommand('clear')"
  149. ><IconFormat /></CheckboxButton>
  150. </Tooltip>
  151. </CheckboxButtonGroup>
  152. <Divider />
  153. <RadioGroup
  154. class="row"
  155. button-style="solid"
  156. :value="richTextAttrs.align"
  157. @change="e => emitRichTextCommand('align', e.target.value)"
  158. >
  159. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="左对齐">
  160. <RadioButton value="left" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
  161. </Tooltip>
  162. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="居中">
  163. <RadioButton value="center" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
  164. </Tooltip>
  165. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="右对齐">
  166. <RadioButton value="right" style="flex: 1;"><IconAlignTextRight /></RadioButton>
  167. </Tooltip>
  168. </RadioGroup>
  169. <CheckboxButtonGroup class="row">
  170. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="项目符号">
  171. <CheckboxButton
  172. style="flex: 1;"
  173. :checked="richTextAttrs.bulletList"
  174. @click="emitRichTextCommand('bulletList')"
  175. ><IconList /></CheckboxButton>
  176. </Tooltip>
  177. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="编号">
  178. <CheckboxButton
  179. style="flex: 1;"
  180. :checked="richTextAttrs.orderedList"
  181. @click="emitRichTextCommand('orderedList')"
  182. ><IconOrderedList /></CheckboxButton>
  183. </Tooltip>
  184. </CheckboxButtonGroup>
  185. <Divider />
  186. <div class="row">
  187. <div style="flex: 2;">行间距:</div>
  188. <Select style="flex: 3;" :value="lineHeight" @change="value => updateLineHeight(value)">
  189. <template #suffixIcon><IconRowHeight /></template>
  190. <SelectOption v-for="item in lineHeightOptions" :key="item" :value="item">{{item}}倍</SelectOption>
  191. </Select>
  192. </div>
  193. <div class="row">
  194. <div style="flex: 2;">字间距:</div>
  195. <Select style="flex: 3;" :value="wordSpace" @change="value => updateWordSpace(value)">
  196. <template #suffixIcon><IconFullwidth /></template>
  197. <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}px</SelectOption>
  198. </Select>
  199. </div>
  200. <Divider />
  201. <ElementOutline />
  202. <Divider />
  203. <ElementShadow />
  204. <Divider />
  205. <ElementOpacity />
  206. </div>
  207. </template>
  208. <script lang="ts">
  209. import { computed, defineComponent, onUnmounted, ref, watch } from 'vue'
  210. import { useStore } from 'vuex'
  211. import { MutationTypes, State } from '@/store'
  212. import { PPTTextElement } from '@/types/slides'
  213. import emitter, { EmitterEvents } from '@/utils/emitter'
  214. import { TextAttrs } from '@/prosemirror/utils'
  215. import { WEB_FONTS } from '@/configs/font'
  216. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  217. import ElementOpacity from '../common/ElementOpacity.vue'
  218. import ElementOutline from '../common/ElementOutline.vue'
  219. import ElementShadow from '../common/ElementShadow.vue'
  220. const presetStyles = [
  221. {
  222. label: '大标题',
  223. style: {
  224. fontSize: '26px',
  225. fontWeight: 700,
  226. },
  227. cmd: [
  228. { command: 'clear' },
  229. { command: 'fontsize', value: '48px' },
  230. { command: 'align', value: 'center' },
  231. { command: 'bold' },
  232. ],
  233. },
  234. {
  235. label: '小标题',
  236. style: {
  237. fontSize: '22px',
  238. fontWeight: 700,
  239. },
  240. cmd: [
  241. { command: 'clear' },
  242. { command: 'fontsize', value: '36px' },
  243. { command: 'align', value: 'center' },
  244. { command: 'bold' },
  245. ],
  246. },
  247. {
  248. label: '正文',
  249. style: {
  250. fontSize: '20px',
  251. },
  252. cmd: [
  253. { command: 'clear' },
  254. { command: 'fontsize', value: '20px' },
  255. ],
  256. },
  257. {
  258. label: '正文[小]',
  259. style: {
  260. fontSize: '18px',
  261. },
  262. cmd: [
  263. { command: 'clear' },
  264. { command: 'fontsize', value: '18px' },
  265. ],
  266. },
  267. {
  268. label: '注释 1',
  269. style: {
  270. fontSize: '16px',
  271. fontStyle: 'italic',
  272. },
  273. cmd: [
  274. { command: 'clear' },
  275. { command: 'fontsize', value: '16px' },
  276. { command: 'em' },
  277. ],
  278. },
  279. {
  280. label: '注释 2',
  281. style: {
  282. fontSize: '16px',
  283. textDecoration: 'underline',
  284. },
  285. cmd: [
  286. { command: 'clear' },
  287. { command: 'fontsize', value: '16px' },
  288. { command: 'underline' },
  289. ],
  290. },
  291. ]
  292. const webFonts = WEB_FONTS
  293. interface CommandPayload {
  294. command: string;
  295. value?: string;
  296. }
  297. export default defineComponent({
  298. name: 'text-style-panel',
  299. components: {
  300. ElementOpacity,
  301. ElementOutline,
  302. ElementShadow,
  303. },
  304. setup() {
  305. const store = useStore<State>()
  306. const handleElement = computed<PPTTextElement>(() => store.getters.handleElement)
  307. const fill = ref<string>()
  308. const lineHeight = ref<number>()
  309. const wordSpace = ref<number>()
  310. watch(handleElement, () => {
  311. if(!handleElement.value || handleElement.value.type !== 'text') return
  312. fill.value = handleElement.value.fill || '#000'
  313. lineHeight.value = handleElement.value.lineHeight || 1.5
  314. wordSpace.value = handleElement.value.wordSpace || 0
  315. }, { deep: true, immediate: true })
  316. const richTextAttrs = ref<TextAttrs>({
  317. bold: false,
  318. em: false,
  319. underline: false,
  320. strikethrough: false,
  321. superscript: false,
  322. subscript: false,
  323. code: false,
  324. color: '#000',
  325. backcolor: '#000',
  326. fontsize: '12px',
  327. fontname: '微软雅黑',
  328. align: 'left',
  329. bulletList: false,
  330. orderedList: false,
  331. blockquote: false,
  332. })
  333. const availableFonts = computed(() => store.state.availableFonts)
  334. const fontSizeOptions = [
  335. '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
  336. '36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '80px',
  337. ]
  338. const lineHeightOptions = [0.5, 1.0, 1.2, 1.5, 1.8, 2.0, 3.0]
  339. const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 8]
  340. const updateRichTextAttrs = (attr: TextAttrs) => richTextAttrs.value = attr
  341. emitter.on(EmitterEvents.UPDATE_TEXT_STATE, attr => updateRichTextAttrs(attr))
  342. onUnmounted(() => {
  343. emitter.off(EmitterEvents.UPDATE_TEXT_STATE, attr => updateRichTextAttrs(attr))
  344. })
  345. const emitRichTextCommand = (command: string, value?: string) => {
  346. emitter.emit(EmitterEvents.EXEC_TEXT_COMMAND, { command, value })
  347. }
  348. const emitBatchRichTextCommand = (payload: CommandPayload[]) => {
  349. emitter.emit(EmitterEvents.EXEC_TEXT_COMMAND, payload)
  350. }
  351. const { addHistorySnapshot } = useHistorySnapshot()
  352. const updateLineHeight = (value: number) => {
  353. const props = { lineHeight: value }
  354. store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
  355. addHistorySnapshot()
  356. }
  357. const updateWordSpace = (value: number) => {
  358. const props = { wordSpace: value }
  359. store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
  360. addHistorySnapshot()
  361. }
  362. const updateFill = (value: string) => {
  363. const props = { fill: value }
  364. store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
  365. addHistorySnapshot()
  366. }
  367. return {
  368. fill,
  369. lineHeight,
  370. wordSpace,
  371. richTextAttrs,
  372. availableFonts,
  373. webFonts,
  374. fontSizeOptions,
  375. lineHeightOptions,
  376. wordSpaceOptions,
  377. updateLineHeight,
  378. updateWordSpace,
  379. updateFill,
  380. emitRichTextCommand,
  381. emitBatchRichTextCommand,
  382. presetStyles,
  383. }
  384. },
  385. })
  386. </script>
  387. <style lang="scss" scoped>
  388. .text-style-panel {
  389. user-select: none;
  390. }
  391. .row {
  392. width: 100%;
  393. display: flex;
  394. align-items: center;
  395. margin-bottom: 10px;
  396. }
  397. .preset-style {
  398. display: flex;
  399. flex-wrap: wrap;
  400. margin-bottom: 10px;
  401. }
  402. .preset-style-item {
  403. width: 50%;
  404. height: 50px;
  405. border: solid 1px #d6d6d6;
  406. box-sizing: border-box;
  407. display: flex;
  408. justify-content: center;
  409. align-items: center;
  410. position: relative;
  411. cursor: pointer;
  412. transition: all .2s;
  413. &:hover {
  414. border-color: $themeColor;
  415. color: $themeColor;
  416. z-index: 1;
  417. }
  418. &:nth-child(2n) {
  419. margin-left: -1px;
  420. }
  421. &:nth-child(n+3) {
  422. margin-top: -1px;
  423. }
  424. }
  425. .text-color-btn {
  426. display: flex;
  427. flex-direction: column;
  428. justify-content: center;
  429. align-items: center;
  430. }
  431. .text-color-block {
  432. width: 16px;
  433. height: 3px;
  434. margin-top: 1px;
  435. }
  436. </style>