Pārlūkot izejas kodu

添加表格单元格样式和主题

pipipi-pikachu 5 gadi atpakaļ
vecāks
revīzija
604494cd8e

+ 12 - 12
src/mocks/index.ts

@@ -34,22 +34,22 @@ export const slides: Slide[] = [
         },
         data: [
           [
-            { id: '1', colspan: 1, rowspan: 1, text: '' },
-            { id: '2', colspan: 1, rowspan: 1, text: '' },
-            { id: '3', colspan: 1, rowspan: 1, text: '' },
-            { id: '4', colspan: 1, rowspan: 1, text: '' },
+            { id: '1', colspan: 1, rowspan: 1, text: '1' },
+            { id: '2', colspan: 1, rowspan: 1, text: '2' },
+            { id: '3', colspan: 1, rowspan: 1, text: '3' },
+            { id: '4', colspan: 1, rowspan: 1, text: '4' },
           ],
           [
-            { id: '6', colspan: 1, rowspan: 1, text: '' },
-            { id: '7', colspan: 1, rowspan: 1, text: '' },
-            { id: '8', colspan: 1, rowspan: 1, text: '' },
-            { id: '9', colspan: 1, rowspan: 1, text: '' },
+            { id: '6', colspan: 1, rowspan: 1, text: '6' },
+            { id: '7', colspan: 1, rowspan: 1, text: '7' },
+            { id: '8', colspan: 1, rowspan: 1, text: '8' },
+            { id: '9', colspan: 1, rowspan: 1, text: '9' },
           ],
           [
-            { id: '11', colspan: 1, rowspan: 1, text: '' },
-            { id: '12', colspan: 1, rowspan: 1, text: '' },
-            { id: '13', colspan: 1, rowspan: 1, text: '' },
-            { id: '14', colspan: 1, rowspan: 1, text: '' },
+            { id: '11', colspan: 1, rowspan: 1, text: '11' },
+            { id: '12', colspan: 1, rowspan: 1, text: '12' },
+            { id: '13', colspan: 1, rowspan: 1, text: '13' },
+            { id: '14', colspan: 1, rowspan: 1, text: '14' },
           ],
         ],
       },

+ 20 - 9
src/types/slides.ts

@@ -136,21 +136,31 @@ export interface PPTChartElement {
   gridColor?: string;
 }
 
+export interface TableCellStyle {
+  bold?: boolean;
+  em?: boolean;
+  underline?: boolean;
+  strikethrough?: boolean;
+  color?: string;
+  backcolor?: string;
+  fontsize?: string;
+  fontname?: string;
+  align?: string;
+}
 export interface TableCell {
   id: string;
   colspan: number;
   rowspan: number;
   text: string;
-  style?: {
-    color?: string;
-    bgColor?: string;
-    fontSize?: number;
-    fontName?: string;
-    bold?: boolean;
-    italic?: boolean;
-    align?: string;
-  };
+  style?: TableCellStyle;
 }
+export interface TableTheme {
+  color: string;
+  rowHeader: boolean;
+  rowFooter: boolean;
+  colHeader: boolean;
+  colFooter: boolean;
+} 
 export interface PPTTableElement {
   type: 'table';
   id: string;
@@ -161,6 +171,7 @@ export interface PPTTableElement {
   width: number;
   height: number;
   outline: PPTElementOutline;
+  theme?: TableTheme;
   colWidths: number[];
   data: TableCell[][];
 }

+ 2 - 0
src/utils/emitter.ts

@@ -3,6 +3,8 @@ import mitt, { Emitter } from 'mitt'
 export enum EmitterEvents {
   UPDATE_TEXT_STATE = 'UPDATE_TEXT_STATE',
   EXEC_TEXT_COMMAND = 'EXEC_TEXT_COMMAND',
+  UPDATE_TABLE_TEXT_STATE = 'UPDATE_TABLE_TEXT_STATE',
+  EXEC_TABLE_TEXT_COMMAND = 'EXEC_TABLE_TEXT_COMMAND',
   SCALE_ELEMENT_STATE = 'SCALE_ELEMENT_STATE',
 }
 

+ 4 - 2
src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/index.vue

@@ -9,11 +9,13 @@
     <template v-if="handleElement.chartType === 'line'">
       <div class="row">
         <Checkbox 
-          @change="e => updateOptions({ showArea: e.target.checked })" :checked="showArea" 
+          @change="e => updateOptions({ showArea: e.target.checked })"
+          :checked="showArea" 
           style="flex: 1;"
         >面积图样式</Checkbox>
         <Checkbox 
-          @change="e => updateOptions({ showLine: !e.target.checked })" :checked="!showLine" 
+          @change="e => updateOptions({ showLine: !e.target.checked })"
+          :checked="!showLine" 
           style="flex: 1;"
         >散点图样式</Checkbox>
       </div>

+ 314 - 0
src/views/Editor/Toolbar/ElementStylePanel/TableStylePanel.vue

@@ -0,0 +1,314 @@
+<template>
+  <div class="table-style-panel">
+    <InputGroup compact class="row">
+      <Select
+        style="flex: 3;"
+        :value="textAttrs.fontname"
+        @change="value => emitUpdateTextAttrCommand({ fontname: value })"
+      >
+        <template #suffixIcon><IconFontSize /></template>
+        <SelectOption v-for="font in availableFonts" :key="font.en" :value="font.en">
+          <span :style="{ fontFamily: font.en }">{{font.zh}}</span>
+        </SelectOption>
+      </Select>
+      <Select
+        style="flex: 2;"
+        :value="textAttrs.fontsize"
+        @change="value => emitUpdateTextAttrCommand({ fontsize: value })"
+      >
+        <template #suffixIcon><IconAddText /></template>
+        <SelectOption v-for="fontsize in fontSizeOptions" :key="fontsize" :value="fontsize">
+          {{fontsize}}
+        </SelectOption>
+      </Select>
+    </InputGroup>
+
+    <ButtonGroup class="row">
+      <Popover trigger="click">
+        <template #content>
+          <ColorPicker
+            :modelValue="textAttrs.color"
+            @update:modelValue="value => emitUpdateTextAttrCommand({ color: value })"
+          />
+        </template>
+        <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
+          <Button class="text-color-btn" style="flex: 1;">
+            <IconText />
+            <div class="text-color-block" :style="{ backgroundColor: textAttrs.color }"></div>
+          </Button>
+        </Tooltip>
+      </Popover>
+      <Popover trigger="click">
+        <template #content>
+          <ColorPicker
+            :modelValue="textAttrs.backcolor"
+            @update:modelValue="value => emitUpdateTextAttrCommand({ backcolor: value })"
+          />
+        </template>
+        <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="单元格填充">
+          <Button class="text-color-btn" style="flex: 1;">
+            <IconFill />
+            <div class="text-color-block" :style="{ backgroundColor: textAttrs.backcolor }"></div>
+          </Button>
+        </Tooltip>
+      </Popover>
+    </ButtonGroup>
+
+    <CheckboxButtonGroup class="row">
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="加粗">
+        <CheckboxButton 
+          style="flex: 1;"
+          :checked="textAttrs.bold"
+          @click="emitUpdateTextAttrCommand({ bold: !textAttrs.bold })"
+        ><IconTextBold /></CheckboxButton>
+      </Tooltip>
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="斜体">
+        <CheckboxButton 
+          style="flex: 1;"
+          :checked="textAttrs.em"
+          @click="emitUpdateTextAttrCommand({ em: !textAttrs.em })"
+        ><IconTextItalic /></CheckboxButton>
+      </Tooltip>
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="下划线">
+        <CheckboxButton 
+          style="flex: 1;"
+          :checked="textAttrs.underline"
+          @click="emitUpdateTextAttrCommand({ underline: !textAttrs.underline })"
+        ><IconTextUnderline /></CheckboxButton>
+      </Tooltip>
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除线">
+        <CheckboxButton 
+          style="flex: 1;"
+          :checked="textAttrs.strikethrough"
+          @click="emitUpdateTextAttrCommand({ strikethrough: !textAttrs.strikethrough })"
+        ><IconStrikethrough /></CheckboxButton>
+      </Tooltip>
+    </CheckboxButtonGroup>
+
+    <RadioGroup 
+      class="row" 
+      button-style="solid" 
+      :value="textAttrs.align"
+      @change="e => emitUpdateTextAttrCommand({ align: e.target.value })"
+    >
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="左对齐">
+        <RadioButton value="left" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
+      </Tooltip>
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="居中">
+        <RadioButton value="center" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
+      </Tooltip>
+      <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="右对齐">
+        <RadioButton value="right" style="flex: 1;"><IconAlignTextRight /></RadioButton>
+      </Tooltip>
+    </RadioGroup>
+
+    <Divider />
+
+    <ElementOutline :fixed="true" />
+
+    <Divider />
+
+    <div class="row theme-switch">
+      <div style="flex: 2;">启用主题表格:</div>
+      <div class="switch-wrapper" style="flex: 3;">
+        <Switch 
+          :checked="hasTheme" 
+          @change="checked => toggleTheme(checked)" 
+        />
+      </div>
+    </div>
+
+    <template v-if="hasTheme">
+      <div class="row">
+        <Checkbox 
+          @change="e => updateTheme({ rowHeader: e.target.checked })" 
+          :checked="theme.rowHeader" 
+          style="flex: 1;"
+        >标题行</Checkbox>
+        <Checkbox 
+          @change="e => updateTheme({ rowFooter: e.target.checked })" 
+          :checked="theme.rowFooter" 
+          style="flex: 1;"
+        >汇总行</Checkbox>
+      </div>
+      <div class="row">
+        <Checkbox 
+          @change="e => updateTheme({ colHeader: e.target.checked })" 
+          :checked="theme.colHeader" 
+          style="flex: 1;"
+        >第一列</Checkbox>
+        <Checkbox 
+          @change="e => updateTheme({ colFooter: e.target.checked })" 
+          :checked="theme.colFooter" 
+          style="flex: 1;"
+        >最后一列</Checkbox>
+      </div>
+      <div class="row">
+        <div style="flex: 2;">主题颜色:</div>
+        <Popover trigger="click">
+          <template #content>
+            <ColorPicker
+              :modelValue="theme.color"
+              @update:modelValue="value => updateTheme({ color: value })"
+            />
+          </template>
+          <ColorButton :color="theme.color" style="flex: 3;" />
+        </Popover>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onUnmounted, ref, watch } from 'vue'
+import { useStore } from 'vuex'
+import { MutationTypes, State } from '@/store'
+import { PPTTableElement, TableCellStyle, TableTheme } from '@/types/slides'
+import emitter, { EmitterEvents } from '@/utils/emitter'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+
+import ElementOutline from '../common/ElementOutline.vue'
+import ColorButton from '../common/ColorButton.vue'
+
+export default defineComponent({
+  name: 'table-style-panel',
+  components: {
+    ElementOutline,
+    ColorButton,
+  },
+  setup() {
+    const store = useStore<State>()
+    const handleElement = computed<PPTTableElement>(() => store.getters.handleElement)
+
+    const textAttrs = ref({
+      bold: false,
+      em: false,
+      underline: false,
+      strikethrough: false,
+      color: '#000',
+      backcolor: '#000',
+      fontsize: '12px',
+      fontname: '微软雅黑',
+      align: 'left',
+    })
+
+    const theme = ref<TableTheme>()
+    const hasTheme = ref(false)
+
+    watch(handleElement, () => {
+      if(!handleElement.value) return
+      
+      theme.value = handleElement.value.theme
+      hasTheme.value = !!theme.value
+    }, { deep: true, immediate: true })
+
+    const updateTextAttrs = (style?: Partial<TableCellStyle>) => {
+      if(!style) {
+        textAttrs.value = {
+          bold: false,
+          em: false,
+          underline: false,
+          strikethrough: false,
+          color: '#000',
+          backcolor: '#000',
+          fontsize: '12px',
+          fontname: '微软雅黑',
+          align: 'left',
+        }
+      }
+      else {
+        textAttrs.value = {
+          bold: !!style.bold,
+          em: !!style.em,
+          underline: !!style.underline,
+          strikethrough: !!style.strikethrough,
+          color: style.color || '#000',
+          backcolor: style.backcolor || '#000',
+          fontsize: style.fontsize || '12px',
+          fontname: style.fontname || '微软雅黑',
+          align: style.align || 'left',
+        }
+      }
+    }
+
+    emitter.on(EmitterEvents.UPDATE_TABLE_TEXT_STATE, style => updateTextAttrs(style))
+    onUnmounted(() => {
+      emitter.off(EmitterEvents.UPDATE_TABLE_TEXT_STATE, style => updateTextAttrs(style))
+    })
+
+    const availableFonts = computed(() => store.state.availableFonts)
+    const fontSizeOptions = [
+      '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
+    ]
+
+    const { addHistorySnapshot } = useHistorySnapshot()
+
+    const emitUpdateTextAttrCommand = (textAttrProp: Partial<TableCellStyle>) => {
+      emitter.emit(EmitterEvents.EXEC_TABLE_TEXT_COMMAND, textAttrProp)
+    }
+
+    const updateTheme = (themeProp: Partial<TableTheme>) => {
+      const currentTheme = theme.value || {}
+      const props = { theme: { ...currentTheme, ...themeProp } }
+      store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      addHistorySnapshot()
+    }
+
+    const toggleTheme = (checked: boolean) => {
+      if(checked) {
+        const props = {
+          theme: {
+            color: '#d14424',
+            rowHeader: true,
+            rowFooter: false,
+            colHeader: false,
+            colFooter: false,
+          }
+        }
+        store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      }
+      else {
+        store.commit(MutationTypes.REMOVE_ELEMENT_PROPS, { id: handleElement.value.id, propName: 'theme' })
+      }
+      addHistorySnapshot()
+    }
+
+    return {
+      availableFonts,
+      fontSizeOptions,
+      textAttrs,
+      emitUpdateTextAttrCommand,
+      theme,
+      hasTheme,
+      toggleTheme,
+      updateTheme,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+.theme-switch {
+  margin-bottom: 18px;
+}
+.switch-wrapper {
+  text-align: right;
+}
+.text-color-btn {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+.text-color-block {
+  width: 16px;
+  height: 3px;
+  margin-top: 1px;
+}
+</style>

+ 2 - 0
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -18,6 +18,7 @@ import ImageStylePanel from './ImageStylePanel.vue'
 import ShapeStylePanel from './ShapeStylePanel.vue'
 import LineStylePanel from './LineStylePanel.vue'
 import ChartStylePanel from './ChartStylePanel/index.vue'
+import TableStylePanel from './TableStylePanel.vue'
 
 export default defineComponent({
   name: 'element-style-panel',
@@ -34,6 +35,7 @@ export default defineComponent({
         [ElementTypes.SHAPE]: ShapeStylePanel,
         [ElementTypes.LINE]: LineStylePanel,
         [ElementTypes.CHART]: ChartStylePanel,
+        [ElementTypes.TABLE]: TableStylePanel,
       }
       return panelMap[handleElement.value.type] || null
     })

+ 12 - 4
src/views/Editor/Toolbar/common/ElementOutline.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="element-outline">
-    <div class="row">
+    <div class="row" v-if="!fixed">
       <div style="flex: 2;">启用边框:</div>
       <div class="switch-wrapper" style="flex: 3;">
         <Switch 
@@ -59,6 +59,12 @@ export default defineComponent({
   components: {
     ColorButton,
   },
+  props: {
+    fixed: {
+      type: Boolean,
+      default: false,
+    },
+  },
   setup() {
     const store = useStore<State>()
     const handleElement = computed<PPTElement>(() => store.getters.handleElement)
@@ -81,11 +87,13 @@ export default defineComponent({
     }
 
     const toggleOutline = (checked: boolean) => {
-      let props: { outline?: PPTElementOutline } = { outline: undefined }
       if(checked) {
-        props = { outline: { width: 2, color: '#000', style: 'solid' } }
+        const props = { outline: { width: 2, color: '#000', style: 'solid' } }
+        store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      }
+      else {
+        store.commit(MutationTypes.REMOVE_ELEMENT_PROPS, { id: handleElement.value.id, propName: 'outline' })
       }
-      store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
       addHistorySnapshot()
     }
 

+ 5 - 3
src/views/Editor/Toolbar/common/ElementShadow.vue

@@ -92,11 +92,13 @@ export default defineComponent({
     }
 
     const toggleShadow = (checked: boolean) => {
-      let props: { shadow?: PPTElementShadow } = { shadow: undefined }
       if(checked) {
-        props = { shadow: { h: 1, v: 1, blur: 2, color: '#000' } }
+        const props = { shadow: { h: 1, v: 1, blur: 2, color: '#000' } }
+        store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      }
+      else {
+        store.commit(MutationTypes.REMOVE_ELEMENT_PROPS, { id: handleElement.value.id, propName: 'shadow' })
       }
-      store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
       addHistorySnapshot()
     }
 

+ 1 - 0
src/views/components/element/TableElement/BaseTableElement.vue

@@ -13,6 +13,7 @@
         :width="elementInfo.width"
         :colWidths="elementInfo.colWidths"
         :outline="elementInfo.outline"
+        :theme="elementInfo.theme"
       />
     </div>
   </div>

+ 101 - 8
src/views/components/element/TableElement/EditableTable.vue

@@ -14,7 +14,16 @@
         @mousedown="$event => handleMousedownColHandler($event, index)"
       ></div>
     </div>
-    <table>
+    <table 
+      :class="{
+        'theme': theme,
+        'row-header': theme?.rowHeader,
+        'row-footer': theme?.rowFooter,
+        'col-header': theme?.colHeader,
+        'col-footer': theme?.colFooter,
+      }"
+      :style="`--themeColor: ${theme?.color}; --subThemeColor1: ${subThemeColor[0]}; --subThemeColor2: ${subThemeColor[1]}`"
+    >
       <colgroup>
         <col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
       </colgroup>
@@ -33,6 +42,7 @@
               borderStyle: outline.style,
               borderColor: outline.color,
               borderWidth: outline.width + 'px',
+              ...getTextStyle(cell.style),
             }"
             v-for="(cell, colIndex) in rowCells"
             :key="cell.id"
@@ -61,7 +71,8 @@
 <script lang="ts">
 import { computed, defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
 import debounce from 'lodash/debounce'
-import { PPTElementOutline, TableCell } from '@/types/slides'
+import tinycolor from 'tinycolor2'
+import { PPTElementOutline, TableCell, TableCellStyle, TableTheme } from '@/types/slides'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import { KEYS } from '@/configs/hotkey'
 import { createRandomCode } from '@/utils/common'
@@ -92,6 +103,9 @@ export default defineComponent({
       type: Object as PropType<PPTElementOutline>,
       required: true,
     },
+    theme: {
+      type: Object as PropType<TableTheme>,
+    },
     editable: {
       type: Boolean,
       default: true,
@@ -101,6 +115,19 @@ export default defineComponent({
     const store = useStore<State>()
     const canvasScale = computed(() => store.state.canvasScale)
 
+    const subThemeColor = ref(['', ''])
+    watch(() => props.theme, () => {
+      if(props.theme) {
+        const rgba = tinycolor(props.theme.color).toRgb()
+        const subRgba1 = { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a * 0.3 }
+        const subRgba2 = { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a * 0.1 }
+        subThemeColor.value = [
+          `rgba(${[subRgba1.r, subRgba1.g, subRgba1.b, subRgba1.a].join(',')})`,
+          `rgba(${[subRgba2.r, subRgba2.g, subRgba2.b, subRgba2.a].join(',')})`,
+        ]
+      }
+    }, { immediate: true })
+
     const tableCells = computed<TableCell[][]>({
       get() {
         return props.data
@@ -187,6 +214,10 @@ export default defineComponent({
       return selectedCells
     })
 
+    watch(selectedCells, () => {
+      emit('changeSelectedCells', selectedCells.value)
+    })
+
     const activedCell = computed(() => {
       if(selectedCells.value.length > 1) return null
       return selectedCells.value[0]
@@ -446,6 +477,36 @@ export default defineComponent({
       document.removeEventListener('keydown', keydownListener)
     })
 
+    const getTextStyle = (style?: TableCellStyle) => {
+      if(!style) return {}
+      const {
+        bold,
+        em,
+        underline,
+        strikethrough,
+        color,
+        backcolor,
+        fontsize,
+        fontname,
+        align,
+      } = style
+      
+      return {
+        fontWeight: bold ? 'bold' : 'normal',
+        fontStyle: em ? 'italic' : 'normal',
+        textDecoration: `${underline ? 'underline' : ''} ${strikethrough ? 'line-through' : ''}`,
+        color: color || '#000',
+        backgroundColor: backcolor || '',
+        fontSize: fontsize || '14px',
+        fontFamily: fontname || '微软雅黑',
+        textAlign: align || 'left',
+      }
+    }
+
+    const handleInput = debounce(function() {
+      emit('change', tableCells.value)
+    }, 300, { trailing: true })
+
     const getEffectiveTableCells = () => {
       const effectiveTableCells = []
 
@@ -532,11 +593,8 @@ export default defineComponent({
       ]
     }
 
-    const handleInput = debounce(function() {
-      emit('change', tableCells.value)
-    }, 300, { trailing: true })
-
     return {
+      getTextStyle,
       dragLinePosition,
       tableCells,
       colSizeList,
@@ -552,6 +610,7 @@ export default defineComponent({
       handleMousedownColHandler,
       contextmenus,
       handleInput,
+      subThemeColor,
     }
   },
 })
@@ -572,6 +631,40 @@ table {
   word-wrap: break-word;
   user-select: none;
 
+  --themeColor: $themeColor;
+  --subThemeColor1: $themeColor;
+  --subThemeColor2: $themeColor;
+
+  &.theme {
+    tr:nth-child(2n) .cell {
+      background-color: var(--subThemeColor1);
+    }
+    tr:nth-child(2n + 1) .cell {
+      background-color: var(--subThemeColor2);
+    }
+
+    &.row-header {
+      tr:first-child .cell {
+        background-color: var(--themeColor);
+      }
+    }
+    &.row-footer {
+      tr:last-child .cell {
+        background-color: var(--themeColor);
+      }
+    }
+    &.col-header {
+      tr .cell:first-child {
+        background-color: var(--themeColor);
+      }
+    }
+    &.col-footer {
+      tr .cell:last-child {
+        background-color: var(--themeColor);
+      }
+    }
+  }
+
   tr {
     height: 36px;
   }
@@ -581,6 +674,7 @@ table {
     white-space: normal;
     word-wrap: break-word;
     vertical-align: middle;
+    font-size: 14px;
     cursor: default;
 
     &.selected::after {
@@ -590,7 +684,7 @@ table {
       position: absolute;
       top: 0;
       left: 0;
-      background-color: rgba($color: #888, $alpha: .1);
+      background-color: rgba($color: $themeColor, $alpha: .3);
     }
   }
 
@@ -600,7 +694,6 @@ table {
     border: 0;
     outline: 0;
     line-height: 1.5;
-    font-size: 14px;
     user-select: none;
     cursor: text;
 

+ 92 - 5
src/views/components/element/TableElement/StaticTable.vue

@@ -3,7 +3,16 @@
     class="static-table"
     :style="{ width: totalWidth + 'px' }"
   >
-    <table>
+    <table
+      :class="{
+        'theme': theme,
+        'row-header': theme?.rowHeader,
+        'row-footer': theme?.rowFooter,
+        'col-header': theme?.colHeader,
+        'col-footer': theme?.colFooter,
+      }"
+      :style="`--themeColor: ${theme?.color}; --subThemeColor1: ${subThemeColor[0]}; --subThemeColor2: ${subThemeColor[1]}`"
+    >
       <colgroup>
         <col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
       </colgroup>
@@ -18,6 +27,7 @@
               borderStyle: outline.style,
               borderColor: outline.color,
               borderWidth: outline.width + 'px',
+              ...getTextStyle(cell.style),
             }"
             v-for="(cell, colIndex) in rowCells"
             :key="cell.id"
@@ -25,7 +35,7 @@
             :colspan="cell.colspan"
             v-show="!hideCells.includes(`${rowIndex}_${colIndex}`)"
           >
-            <div class="cell-text" v-html="cell.content" />
+            <div class="cell-text" v-html="cell.text" />
           </td>
         </tr>
       </tbody>
@@ -35,7 +45,8 @@
 
 <script lang="ts">
 import { computed, defineComponent, PropType, ref, watch } from 'vue'
-import { PPTElementOutline, TableCell } from '@/types/slides'
+import tinycolor from 'tinycolor2'
+import { PPTElementOutline, TableCell, TableCellStyle, TableTheme } from '@/types/slides'
 
 export default defineComponent({
   name: 'static-table',
@@ -56,6 +67,9 @@ export default defineComponent({
       type: Object as PropType<PPTElementOutline>,
       required: true,
     },
+    theme: {
+      type: Object as PropType<TableTheme>,
+    },
     editable: {
       type: Boolean,
       default: true,
@@ -93,10 +107,51 @@ export default defineComponent({
       return hideCells
     })
 
+    const subThemeColor = ref(['', ''])
+    watch(() => props.theme, () => {
+      if(props.theme) {
+        const rgba = tinycolor(props.theme.color).toRgb()
+        const subRgba1 = { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a * 0.3 }
+        const subRgba2 = { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a * 0.1 }
+        subThemeColor.value = [
+          `rgba(${[subRgba1.r, subRgba1.g, subRgba1.b, subRgba1.a].join(',')})`,
+          `rgba(${[subRgba2.r, subRgba2.g, subRgba2.b, subRgba2.a].join(',')})`,
+        ]
+      }
+    }, { immediate: true })
+
+    const getTextStyle = (style?: TableCellStyle) => {
+      if(!style) return {}
+      const {
+        bold,
+        em,
+        underline,
+        strikethrough,
+        color,
+        backcolor,
+        fontsize,
+        fontname,
+        align,
+      } = style
+      
+      return {
+        fontWeight: bold ? 'bold' : 'normal',
+        fontStyle: em ? 'italic' : 'normal',
+        textDecoration: `${underline ? 'underline' : ''} ${strikethrough ? 'line-through' : ''}`,
+        color: color || '#000',
+        backgroundColor: backcolor || '',
+        fontSize: fontsize || '14px',
+        fontFamily: fontname || '微软雅黑',
+        textAlign: align || 'left',
+      }
+    }
+
     return {
       colSizeList,
       totalWidth,
       hideCells,
+      getTextStyle,
+      subThemeColor,
     }
   },
 })
@@ -117,6 +172,40 @@ table {
   word-wrap: break-word;
   user-select: none;
 
+  --themeColor: $themeColor;
+  --subThemeColor1: $themeColor;
+  --subThemeColor2: $themeColor;
+
+  &.theme {
+    tr:nth-child(2n) .cell {
+      background-color: var(--subThemeColor1);
+    }
+    tr:nth-child(2n + 1) .cell {
+      background-color: var(--subThemeColor2);
+    }
+
+    &.row-header {
+      tr:first-child .cell {
+        background-color: var(--themeColor);
+      }
+    }
+    &.row-footer {
+      tr:last-child .cell {
+        background-color: var(--themeColor);
+      }
+    }
+    &.col-header {
+      tr .cell:first-child {
+        background-color: var(--themeColor);
+      }
+    }
+    &.col-footer {
+      tr .cell:last-child {
+        background-color: var(--themeColor);
+      }
+    }
+  }
+
   tr {
     height: 36px;
   }
@@ -126,7 +215,6 @@ table {
     white-space: normal;
     word-wrap: break-word;
     vertical-align: middle;
-    cursor: default;
   }
 
   .cell-text {
@@ -137,7 +225,6 @@ table {
     line-height: 1.5;
     font-size: 14px;
     user-select: none;
-    cursor: text;
   }
 }
 </style>

+ 82 - 5
src/views/components/element/TableElement/index.vue

@@ -15,29 +15,35 @@
     >
       <div 
         class="table-mask" 
-        v-if="!editable"
-        @dblclick="editable = true"
+        :class="{ 'lock': elementInfo.lock }"
+        v-if="!editable || elementInfo.lock"
+        @dblclick="startEdit()"
         @mousedown="$event => handleSelectElement($event)"
-      ></div>
+      >
+        <div class="mask-tip" :style="{ transform: `scale(${ 1 / canvasScale })` }">双击编辑</div>
+      </div>
+
       <EditableTable 
         @mousedown.stop
         :data="elementInfo.data"
         :width="elementInfo.width"
         :colWidths="elementInfo.colWidths"
         :outline="elementInfo.outline"
+        :theme="elementInfo.theme"
         :editable="editable"
         @change="data => updateTableCells(data)"
         @changeColWidths="widths => updateColWidths(widths)"
+        @changeSelectedCells="cells => updateSelectedCells(cells)"
       />
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
+import { computed, defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
 import { useStore } from 'vuex'
 import { MutationTypes, State } from '@/store'
-import { PPTTableElement, TableCell } from '@/types/slides'
+import { PPTTableElement, TableCell, TableCellStyle } from '@/types/slides'
 import emitter, { EmitterEvents } from '@/utils/emitter'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
@@ -64,6 +70,8 @@ export default defineComponent({
   },
   setup(props) {
     const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
     const { addHistorySnapshot } = useHistorySnapshot()
 
     const handleSelectElement = (e: MouseEvent) => {
@@ -151,12 +159,64 @@ export default defineComponent({
       addHistorySnapshot()
     }
 
+    const selectedCells = ref<string[]>([])
+
+    const emitUpdateTextAttrsState = () => {
+      let rowIndex = 0
+      let colIndex = 0
+      if(selectedCells.value.length) {
+        const selectedCell = selectedCells.value[0]
+        rowIndex = +selectedCell.split('_')[0]
+        colIndex = +selectedCell.split('_')[1]
+      }
+      emitter.emit(EmitterEvents.UPDATE_TABLE_TEXT_STATE, props.elementInfo.data[rowIndex][colIndex].style)
+    }
+
+    const updateTextAttrs = (textAttrProp: Partial<TableCellStyle>) => {
+      const data: TableCell[][] = JSON.parse(JSON.stringify(props.elementInfo.data))
+
+      for(let i = 0; i < data.length; i++) {
+        for(let j = 0; j < data[i].length; j++) {
+          if(!selectedCells.value.length || selectedCells.value.includes(`${i}_${j}`)) {
+            const style = data[i][j].style || {}
+            data[i][j].style = { ...style, ...textAttrProp }
+          }
+        }
+      }
+
+      store.commit(MutationTypes.UPDATE_ELEMENT, {
+        id: props.elementInfo.id, 
+        props: { data },
+      })
+
+      addHistorySnapshot()
+      nextTick(emitUpdateTextAttrsState)
+    }
+
+    const updateSelectedCells = (cells: string[]) => {
+      selectedCells.value = cells
+      nextTick(emitUpdateTextAttrsState)
+    }
+
+    emitter.on(EmitterEvents.EXEC_TABLE_TEXT_COMMAND, state => updateTextAttrs(state))
+    onUnmounted(() => {
+      emitter.off(EmitterEvents.EXEC_TABLE_TEXT_COMMAND, state => updateTextAttrs(state))
+    })
+
+    const startEdit = () => {
+      if(!props.elementInfo.lock) editable.value = true
+    }
+
     return {
       elementRef,
+      canvasScale,
       handleSelectElement,
       updateTableCells,
       updateColWidths,
       editable,
+      startEdit,
+      selectedCells,
+      updateSelectedCells,
     }
   },
 })
@@ -184,5 +244,22 @@ export default defineComponent({
   left: 0;
   right: 0;
   z-index: 10;
+  opacity: 0;
+  transition: opacity .2s;
+
+  .mask-tip {
+    position: absolute;
+    top: 5px;
+    left: 5px;
+    background-color: rgba($color: #000, $alpha: .5);
+    color: #fff;
+    padding: 6px 12px;
+    font-size: 12px;
+    transform-origin: 0 0;
+  }
+
+  &:hover:not(.lock) {
+    opacity: .9;
+  }
 }
 </style>