Browse Source

提交海报分享组件

jzy 3 months ago
parent
commit
6376fe6e1f

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "dayjs": "^1.11.11",
     "element-plus": "^2.7.6",
     "exceljs": "^4.4.0",
+    "html2canvas": "^1.4.1",
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
     "splitpanes": "^3.1.5",

File diff suppressed because it is too large
+ 239 - 4225
pnpm-lock.yaml


+ 8 - 6
src/components.d.ts

@@ -11,23 +11,25 @@ declare module 'vue' {
     Area: typeof import('./components/form/Area.vue')['default']
     Baidu: typeof import('./components/FsMapPicker/components/baidu.vue')['default']
     CardItem: typeof import('./components/FsCheckCard/components/CardItem.vue')['default']
+    CardPoster: typeof import('./components/cardPoster/cardPoster.vue')['default']
     Cropper: typeof import('./components/avatar/cropper.vue')['default']
     Dict: typeof import('./components/form/Dict.vue')['default']
     Editor: typeof import('./components/form/Editor.vue')['default']
-    ElArea: typeof import('./components/form/ElArea.vue')['default']
-    ElDict: typeof import('./components/form/ElDict.vue')['default']
-    ElEditor: typeof import('./components/form/ElEditor.vue')['default']
-    ElEmployees: typeof import('./components/form/ElEmployees.vue')['default']
+    ElArea: (typeof import('./components/form/ElArea.vue'))['default']
+    ElDict: (typeof import('./components/form/ElDict.vue'))['default']
+    ElEditor: (typeof import('./components/form/ElEditor.vue'))['default']
+    ElEmployees: (typeof import('./components/form/ElEmployees.vue'))['default']
     Employees: typeof import('./components/form/Employees.vue')['default']
     Exception: typeof import('./components/Exception.vue')['default']
     FsCheckCard: typeof import('./components/FsCheckCard/index.vue')['default']
     FsCity: (typeof import('./components/FsCity/index.vue'))['default']
-    FsCitySelect: typeof import('./components/FsCitySelect/index.vue')['default']
+    FsCitySelect: (typeof import('./components/FsCitySelect/index.vue'))['default']
+    FsComplexSelect: typeof import('./components/FsComplexSelect/FsComplexSelect.vue')['default']
     FsImageUpload: (typeof import('./components/FsImageUpload/index.vue'))['default']
     FsMap: (typeof import('./components/FsMap/index.vue'))['default']
     FsMapPicker: typeof import('./components/FsMapPicker/index.vue')['default']
     FsPrinter: typeof import('./components/FsPrinter/index.vue')['default']
-    FsTableSelect: typeof import('./components/FsTableSelect/index.vue')['default']
+    FsTableSelect: (typeof import('./components/FsTableSelect/index.vue'))['default']
     FsText: (typeof import('./components/FsText/index.vue'))['default']
     FsTour: typeof import('./components/FsTour/index.vue')['default']
     GlobalAside: typeof import('./components/core/GlobalAside.vue')['default']

+ 342 - 0
src/components/FsComplexSelect/FsComplexSelect.vue

@@ -0,0 +1,342 @@
+<script setup lang="ts">
+import avatar from '@/assets/images/avatar.png'
+
+interface SelectProps {
+  //选完后数据
+  modelValue?: any
+  //标题数据
+  tabsList: any
+  //树详情获取接口
+  request: (...args: any) => Promise<any>
+  // label 的属性名
+  labelKey?: string
+  // value 的属性名
+  valueKey?: string
+}
+
+// const props = defineProps<Props>()
+const props = withDefaults(defineProps<SelectProps>(), {
+  tabsList: [
+    {
+      label: '部门',
+      children: []
+    },
+    {
+      label: '角色',
+      children: []
+    },
+    {
+      label: '岗位',
+      children: []
+    },
+    {
+      label: '分组',
+      children: []
+    }
+  ],
+  labelKey: 'label',
+  valueKey: 'id'
+})
+
+const titleList = ref<any>([])
+titleList.value = JSON.parse(JSON.stringify(props.tabsList))
+
+const emits = defineEmits(['update:modelValue', 'clickChild'])
+
+const visibleDialog = ref<boolean>(false)
+
+const selectValue = computed({
+  get: () => props.modelValue,
+  set: value => {
+    emits('update:modelValue', value)
+  }
+})
+
+const customerOption = ref<any>(null)
+const handleSelectCustomer = (res: any) => {
+  customerOption.value.blur()
+  visibleDialog.value = true
+  console.log('selectValue', selectValue.value)
+  titleList.value[0].children = selectValue.value
+
+  setTimeout(() => {
+    const deflist = ref<any>([])
+    titleList.value[0].children.forEach((item: any) => {
+      deflist.value.push(item.id)
+    })
+    treeRef.value.setCheckedKeys(deflist.value)
+  }, 1000)
+
+  //处理数据回显逻辑
+}
+
+const saveDialog = () => {
+  selectValue.value = []
+  setTimeout(() => {
+    titleList.value.forEach((element: any) => {
+      if (element.children.length > 0) {
+        element.children.forEach((elem: any) => {
+          selectValue.value.push(elem)
+        })
+      }
+    })
+    emits('clickChild', titleList.value)
+    visibleDialog.value = false
+  }, 100)
+}
+
+const search = ref<any>('')
+
+const handleClick = (e: any) => {}
+
+watch(search, val => {
+  treeRef.value!.filter(val)
+})
+
+const defaultProps = {
+  children: 'children',
+  label: props.labelKey
+}
+
+const activeName = ref<any>(0)
+
+const treeRef = ref<any>(null)
+
+const data = ref<any>([])
+
+const filterNode = (value: string, data: any) => {
+  if (!value) return true
+  return data.label.includes(value)
+}
+
+const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean) => {
+  initList()
+  let checkValue = treeRef.value.getCheckedNodes()
+  checkValue.forEach((element: any) => {
+    if (!element.children) {
+      titleList.value[activeName.value].children.push(element)
+    }
+  })
+}
+
+const initList = () => {
+  titleList.value = JSON.parse(JSON.stringify(props.tabsList))
+}
+
+const request = (where?: any) => {
+  if (typeof props?.request === 'function') {
+    props
+      .request({
+        ...where
+      })
+      .then((res: any) => {
+        data.value = res.infos || res.list || res.records || res.data || res.info
+        data.value.forEach((item: any) => {
+          const temp =
+            Array.isArray(selectValue.value) && selectValue.value.find(x => x[props.valueKey] === item[props.valueKey])
+
+          console.log('temp', temp)
+          temp && treeRef.value.setCheckedKeys(item.id)
+        })
+      })
+      .catch((e: any) => {
+        console.warn(e)
+      })
+      .finally(() => {})
+  } else if (Array.isArray(props?.request)) {
+    data.value = where
+    console.log('获取树接口', data.value)
+  } else {
+    console.warn('tree组件数据格式错误')
+  }
+}
+request()
+
+const clearAll = () => {
+  initList()
+  treeRef.value.setCheckedKeys([], false)
+}
+
+const delItem = (e: any, i: any) => {
+  e.splice(i, 1)
+  const deflist = ref<any>([])
+  e.forEach((item: any) => {
+    deflist.value.push(item.id)
+  })
+  treeRef.value.setCheckedKeys(deflist.value)
+}
+
+const currentValues = computed(() => {
+  if (selectValue.value == null) {
+    return selectValue.value ?? []
+  }
+  return selectValue.value.slice(0, 5)
+})
+
+const onItemClear = (item: any) => {
+  const list = [...(currentValues.value as Array<any>)]
+  const index = list.findIndex(x => x[props.labelKey] === item[props.labelKey])
+  list.splice(index, 1)
+  selectValue.value = list
+}
+</script>
+
+<template>
+  <div class="table-select-container is-multiple" @click="handleSelectCustomer">
+    <el-input ref="customerOption" :modelValue="''" @focus="handleSelectCustomer">
+      <!-- <template v-if="$slots.prefix" #prefix>
+        <slot name="prefix"></slot>
+      </template>
+      <template #suffix>
+        <div class="select-suffix">
+          <ElIcon class="select-clear" @click.stop="onClear">
+            <slot name="clearIcon">
+              <CircleClose />
+            </slot>
+          </ElIcon>
+        </div>
+      </template> -->
+    </el-input>
+    <div class="table-multiple-tag">
+      <el-tag
+        type="success"
+        size="small"
+        disable-transitions
+        :closable="true"
+        style="margin-right: 5px"
+        v-for="item in currentValues"
+        :key="item[labelKey]"
+        @close="onItemClear(item)"
+      >
+        {{ item[labelKey] }}
+      </el-tag>
+    </div>
+  </div>
+
+  <el-dialog title="选择客户" v-model="visibleDialog" height="800px" append-to-body destroy-on-close>
+    <div class="flex pt-5 pb-5">
+      <div class="w-1/2 mr-5">
+        <el-input v-model="search" placeholder="请输入关键词">
+          <template #append>
+            <el-button>
+              <el-icon size="18" slot="append"><component is="search"></component></el-icon>
+            </el-button>
+          </template>
+        </el-input>
+        <div class="left_border mt-5 h-400px">
+          <el-tabs v-model="activeName" @tab-click="handleClick" stretch>
+            <el-tab-pane v-for="(item, index) in titleList" :label="item.label" :name="index"> </el-tab-pane>
+          </el-tabs>
+          <el-tree
+            ref="treeRef"
+            style="max-width: 600px"
+            :data="data"
+            :props="defaultProps"
+            show-checkbox
+            :check-strictly="false"
+            node-key="id"
+            :filter-node-method="filterNode"
+            @check-change="handleCheckChange"
+          />
+        </div>
+      </div>
+      <div class="w-1/2 ml-5">
+        <div class="flex justify-between items-center h-30px mb-5">
+          <div>已选</div>
+          <el-button text type="danger" @click="clearAll">清空列表</el-button>
+        </div>
+        <div class="left_border h-400px p-3">
+          <div v-for="(itm, idx) in titleList" :key="idx">
+            <div class="text-lg flex items-center" v-if="itm.children.length > 0">
+              <el-icon><Service /></el-icon>
+              {{ itm.label }}
+            </div>
+            <div
+              v-for="(it, id) in itm.children"
+              class="flex justify-between mt-2 items-center right-border pb-2 ml-5"
+              v
+            >
+              <div class="flex items-center">
+                <el-image class="w-40px h-40px mr-2" :src="it.picture ? it.picture : avatar"></el-image>
+                <div>
+                  <div class="text-base">{{ it.label }}</div>
+                  <div>{{ it.childrenName }}</div>
+                </div>
+              </div>
+              <div>
+                <Delete style="width: 1em; height: 1em; margin-right: 8px" @click="delItem(itm.children, id)" />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <el-button @click="visibleDialog = false">取 消</el-button>
+      <el-button type="primary" @click="saveDialog">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style scoped lang="scss">
+.left_border {
+  border: 1px solid #e5e5e5;
+}
+.right-border {
+  border-bottom: 1px solid #e5e5e5;
+}
+.table-multiple-tag {
+  position: relative;
+  top: 0px;
+  width: calc(100% - 24px);
+  height: 100%;
+  min-height: 34px;
+  z-index: 2;
+  padding: 0px 10px 2px 10px;
+  box-sizing: border-box;
+  &.is-disabled {
+    cursor: not-allowed;
+  }
+}
+.table-select-container {
+  width: 100%;
+  position: relative;
+
+  .select-clear {
+    position: absolute;
+    right: 10px;
+    top: 49%;
+    transform: translateY(-50%);
+    cursor: pointer;
+    border-radius: 50%;
+    overflow: hidden;
+    z-index: 2;
+    opacity: 0;
+    &:hover {
+      opacity: 100;
+    }
+  }
+
+  &.is-multiple {
+    :deep(.el-input) {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      left: 0;
+    }
+  }
+  &.is-visible {
+    :deep(.el-input:not(.is-disabled) .el-input__wrapper) {
+      box-shadow: 0 0 0 1px var(--el-color-primary) inset;
+    }
+  }
+
+  &:hover {
+    .select-clear {
+      opacity: 1;
+    }
+  }
+}
+</style>

+ 259 - 0
src/components/cardPoster/cardPoster.vue

@@ -0,0 +1,259 @@
+<script setup lang="ts">
+import html2canvas from 'html2canvas'
+
+const simplicityList = import.meta.glob('./images/Simplicity/*.png')
+
+const affairsList = import.meta.glob('./images/affairs/*.png')
+
+interface Questionnaire {
+  title: string
+  desc: string
+}
+
+interface defaultData {
+  //海报二维码
+  qrCode: string
+  //绑定
+  modelValue: boolean
+  //公司名称
+  cardTitle: string
+  //内容与描述
+  questionnaire?: Questionnaire
+}
+
+const props = withDefaults(defineProps<defaultData>(), {
+  qrCode: '',
+  cardTitle: ''
+})
+
+const questionnaire = ref<Questionnaire>({
+  title: '',
+  desc: ''
+})
+
+function initializeQuestionnaire(qr: Questionnaire | undefined): Questionnaire {
+  if (qr) {
+    const copiedQr = JSON.parse(JSON.stringify(qr))
+    return {
+      title: copiedQr.title,
+      desc: copiedQr.desc
+    }
+  } else {
+    return {
+      title: '',
+      desc: ''
+    }
+  }
+}
+
+questionnaire.value = initializeQuestionnaire(props.questionnaire)
+
+const Simplicity = ref<any>(Object.keys(simplicityList).map(key => new URL(key, import.meta.url).href))
+
+const affairs = ref<any>(Object.keys(affairsList).map(key => new URL(key, import.meta.url).href))
+
+const WHITE_COLOR = '#FFFFFF'
+const BLACK_COLOR = '#000000'
+
+const emits = defineEmits(['update:modelValue'])
+
+const templateType = computed({
+  get: () => props.modelValue,
+  set: value => {
+    emits('update:modelValue', value)
+  }
+})
+
+const posterRef = ref<any>(null)
+const activeName = ref<Number>(1)
+const questionBg = ref<any>(Simplicity.value[0])
+const radio1 = ref<String>('居中')
+const color1 = ref(WHITE_COLOR)
+const color2 = ref(BLACK_COLOR)
+
+const saveimg = (item: string) => {
+  activeName.value = 1
+  questionBg.value = item
+}
+
+const submitForm = () => {
+  html2canvas(posterRef.value).then(canvas => {
+    let baseImg = canvas.toDataURL('image/png')
+    let save = document.createElement('a')
+    save.href = baseImg
+    save.download = '分享海报'
+    save.click()
+  })
+}
+
+const resetForm = () => {
+  templateType.value = false
+}
+
+const radioChange = (e: any) => {
+  color1.value = e === '居中' ? WHITE_COLOR : BLACK_COLOR
+  color2.value = color1.value
+}
+</script>
+
+<template>
+  <el-dialog v-model="templateType" width="800px" title="分享海报">
+    <el-tabs v-model="activeName" class="demo-tabs" style="border-bottom: 1px solid #e5e5e5">
+      <el-tab-pane label="海报模版" :name="0">
+        <div class="text-lg font-bold mb-4">简约</div>
+        <div class="flex justify-between">
+          <div class="relative template_img" v-for="(item, index) in Simplicity" :key="index">
+            <img class="template_img" :src="item" />
+            <div class="absolute add_img">
+              <div class="template_img flex items-center justify-center">
+                <el-button type="primary" @click="saveimg(item)">使用模版</el-button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="text-lg font-bold mb-4 mt-4">商务</div>
+        <div class="flex justify-between">
+          <div class="relative template_img" v-for="(item, index) in affairs" :key="index">
+            <img class="template_img" :src="item" />
+            <div class="absolute add_img">
+              <div class="template_img flex items-center justify-center">
+                <el-button type="primary" @click="saveimg(item)">使用模版</el-button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="生成海报" :name="1">
+        <div class="flex">
+          <div ref="posterRef" class="flex items-center justify-center new_box">
+            <div class="relative">
+              <img :src="questionBg" class="relative_img" />
+              <div v-if="radio1 == '底部'" class="absolute flex justify-center items-center top-3 right-3">
+                <img class="w-20px h-20px mr-5px" src="/logo.png" alt="qrcode" />
+                <div class="font-bold" style="color: white">{{ props.cardTitle }}</div>
+              </div>
+              <div v-if="radio1 == '底部'" class="absolute p-3 flex items-center bottom-0 w-full relative_bg">
+                <img class="mr-3 relative_btm_img" :src="props.qrCode" />
+                <div>
+                  <div class="text-s font-bold" :style="{ color: color1 }">{{ questionnaire.title }}</div>
+                  <div class="text-xs font-bold" :style="{ color: color2 }">{{ questionnaire.desc }}</div>
+                </div>
+              </div>
+              <div v-else class="absolute p-3 top-0 w-full">
+                <div class="text-xl font-bold text-center mb-7" :style="{ color: color1 }">
+                  {{ questionnaire.title }}
+                </div>
+                <div class="text-center mb-6">
+                  <img class="relative_cen_img" :src="props.qrCode" />
+                </div>
+                <div class="mb-3">
+                  <div class="text-s font-bold text-center" :style="{ color: color2 }">
+                    {{ questionnaire.desc }}
+                  </div>
+                </div>
+
+                <div class="flex justify-center items-center">
+                  <img class="w-20px h-20px mr-5px" src="/logo.png" alt="qrcode" />
+                  <div class="font-bold" style="color: white">{{ props.cardTitle }}</div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="separate"></div>
+          <div class="new_box">
+            <div class="flex justify-between">
+              <div class="text-text-base font-bold mb-4">海报标题</div>
+              <div><el-color-picker v-model="color1" />{{ color1 }}</div>
+            </div>
+            <div class="mb-4 w-full">
+              <el-input v-model="questionnaire.title" maxlength="20" show-word-limit class="w-full"></el-input>
+            </div>
+
+            <div class="flex justify-between">
+              <div class="text-text-base font-bold mb-4">海报文本</div>
+              <div><el-color-picker v-model="color2" />{{ color2 }}</div>
+            </div>
+            <div class="mb-4">
+              <el-input
+                v-model="questionnaire.desc"
+                type="textarea"
+                show-word-limit
+                maxlength="80"
+                :rows="4"
+              ></el-input>
+            </div>
+
+            <div>
+              <div class="text-text-base font-bold mb-4">二维码位置</div>
+            </div>
+            <div>
+              <el-radio-group v-model="radio1" size="large" @change="radioChange">
+                <el-radio-button label="居中" />
+                <el-radio-button label="底部" />
+              </el-radio-group>
+            </div>
+          </div>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <div class="flex justify-end w-full">
+        <el-button size="default" @click="resetForm()">取消</el-button>
+        <el-button size="default" type="primary" @click="submitForm()">保存下载</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.template_img {
+  width: 130px;
+  height: 174px;
+  border-radius: 10px;
+}
+
+.template_img:hover .add_img {
+  display: block;
+}
+
+.add_img {
+  background-color: #ffffff99;
+  z-index: 999;
+  top: 0px;
+  display: none;
+}
+
+.separate {
+  background-color: #e5e5e5;
+  width: 1px;
+  height: 454px;
+  margin: 0 30px;
+}
+
+.new_box {
+  width: 370px;
+  height: 454px;
+}
+
+.relative_img {
+  width: 310px;
+  height: 406px;
+  border-radius: 10px;
+}
+
+.relative_bg {
+  background-color: #ffffff99;
+}
+
+.relative_btm_img {
+  width: 80px;
+  height: 80px;
+  border-radius: 5px;
+}
+
+.relative_cen_img {
+  width: 150px;
+  height: 150px;
+  border-radius: 5px;
+}
+</style>

BIN
src/components/cardPoster/images/Simplicity/commercial_1.png


BIN
src/components/cardPoster/images/Simplicity/commercial_2.png


BIN
src/components/cardPoster/images/Simplicity/commercial_3.png


BIN
src/components/cardPoster/images/Simplicity/commercial_4.png


BIN
src/components/cardPoster/images/Simplicity/commercial_5.png


BIN
src/components/cardPoster/images/affairs/concise_1.png


BIN
src/components/cardPoster/images/affairs/concise_2.png


BIN
src/components/cardPoster/images/affairs/concise_3.png


BIN
src/components/cardPoster/images/affairs/concise_4.png


BIN
src/components/cardPoster/images/affairs/concise_5.png


+ 18 - 0
src/router/asyncRouter.ts

@@ -352,6 +352,24 @@ const asyncRouter: RouteRecordRaw[] = [
           title: '区域选择',
           icon: 'MoreFilled'
         }
+      },
+      {
+        path: '/extension/sharePoster',
+        name: 'sharePoster',
+        component: () => import('@/views/extension/sharePoster/index.vue'),
+        meta: {
+          title: '海报分享',
+          icon: 'MoreFilled'
+        }
+      },
+      {
+        path: '/extension/complexSelect',
+        name: 'complexSelect',
+        component: () => import('@/views/extension/complexSelect/index.vue'),
+        meta: {
+          title: '组织选择',
+          icon: 'MoreFilled'
+        }
       }
       // {
       //   path: '/extension/map',

+ 101 - 0
src/views/extension/complexSelect/index.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+const request = function ({ pageNo }: any) {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      const data = {
+        list: [
+          {
+            id: 1,
+            label: '研发1组',
+            children: [
+              {
+                id: 4,
+                label: '张三'
+              }
+            ]
+          },
+          {
+            id: 2,
+            label: '研发2组',
+            children: [
+              {
+                id: 5,
+                label: '李四'
+              },
+              {
+                id: 6,
+                label: '王武'
+              }
+            ]
+          },
+          {
+            id: 3,
+            label: '警务协作平台',
+            children: [
+              {
+                id: 7,
+                label: '监区负责人'
+              },
+              {
+                id: 8,
+                label: '监区1',
+                children: [
+                  {
+                    id: 9,
+                    label: '1区领导'
+                  },
+                  {
+                    id: 10,
+                    label: '测试人员'
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+      resolve(data)
+    }, 1500)
+  })
+}
+
+const data = ref<any>([])
+
+const tabsList = ref<any>([
+  {
+    label: '部门',
+    children: []
+  },
+  {
+    label: '角色',
+    children: []
+  },
+  {
+    label: '岗位',
+    children: []
+  }
+])
+const getlist = (res: any) => {
+  console.log('拿到的数据', res)
+}
+</script>
+
+<template>
+  <el-card header="选择组织" shadow="never">
+    {{ data }}
+    <el-form label-width="80px" style="max-width: 320px" @submit.prevent="">
+      <el-form-item label="选择组织">
+        <fs-complex-select
+          v-model="data"
+          :request="request"
+          :tabsList="tabsList"
+          @clickChild="getlist"
+        ></fs-complex-select>
+      </el-form-item>
+    </el-form>
+  </el-card>
+</template>
+
+<style scoped lang="scss"></style>

+ 22 - 0
src/views/extension/sharePoster/index.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+const cardType = ref<any>(false)
+const onPrint = () => {
+  cardType.value = true
+}
+
+const questionnaire = ref<any>({
+  title: '标题',
+  desc: '描述'
+})
+
+const cardTitle = ref<any>('公司名称')
+</script>
+
+<template>
+  <el-card header="分享" shadow="never">
+    <el-button type="primary" @click="onPrint">分享海报</el-button>
+  </el-card>
+  <card-poster v-model="cardType" :questionnaire="questionnaire" :cardTitle="cardTitle"></card-poster>
+</template>
+
+<style scoped lang="scss"></style>

Some files were not shown because too many files changed in this diff