feat: 新增7个工具,修复部分问题
新增工具: 数据图表:折线图(#) 数据图表:饼图(#) 数据图表:散点图(#) 选择随机: 投骰子(#) 选择随机: 抛硬币(#) 文本处理:删除重复行(#) 图片工具:图片切割(#) 修复: 数据图表上传数据文件只有第一次有效问题
This commit is contained in:
@@ -296,7 +296,7 @@ const updateDataFile = async (params) => {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
fileReader.readAsBinaryString(_file)
|
||||
fileReader.readAsArrayBuffer(_file)
|
||||
}
|
||||
|
||||
//copy
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, onMounted } from 'vue'
|
||||
import Spreadsheet from 'x-data-spreadsheet'
|
||||
import 'x-data-spreadsheet/dist/locale/zh-cn';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
// import { copy } from '@/utils/string'
|
||||
import { toEchartsData, toSpreadsheetData } from '@/utils/echarts'
|
||||
import * as echarts from 'echarts'
|
||||
import * as XLSX from 'xlsx'
|
||||
const info = reactive({
|
||||
title: "折线图",
|
||||
})
|
||||
|
||||
const chartDom = ref<HTMLElement|null>()
|
||||
const myChart = ref<echarts.ECharts>()
|
||||
|
||||
const setOptionName = ref(1)
|
||||
//缩放比例
|
||||
const sacleSize = ref(100)
|
||||
//画布宽高
|
||||
const widthCanvas = ref(720)
|
||||
const heightCanvas = ref(400)
|
||||
//下载文件类型
|
||||
const downType = ref('1')
|
||||
//图形属性颜色
|
||||
const attrColor = ref('#5470c6')
|
||||
//标题位置
|
||||
const titlePos = ref('center')
|
||||
//标题
|
||||
const title = ref('然部落在线工具')
|
||||
//副标题
|
||||
const subTitle = ref('在线图表制作工具')
|
||||
//显示标题 - 开关
|
||||
const titleSwitch = ref(true)
|
||||
//显示副标题 - 开关
|
||||
const subTitleSwitch = ref(true)
|
||||
|
||||
/** 水印 */
|
||||
const watermarkSwitch = ref(false) //开关
|
||||
const waterMarkText = ref('然部落工具');
|
||||
|
||||
//创建水印
|
||||
const createWatermark = () => {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
canvas.width = canvas.height = 100;
|
||||
ctx!.textAlign = 'center';
|
||||
ctx!.textBaseline = 'middle';
|
||||
ctx!.globalAlpha = 0.08;
|
||||
ctx!.font = '20px Microsoft Yahei';
|
||||
ctx!.translate(50, 50);
|
||||
ctx!.rotate(-Math.PI / 4);
|
||||
ctx!.fillText(waterMarkText.value, 0, 0);
|
||||
return canvas
|
||||
}
|
||||
|
||||
//操作图表
|
||||
const canvasHandle = (type) => {
|
||||
// let element = document.getElementById('main')
|
||||
let element = chartDom.value
|
||||
switch(type) {
|
||||
case "scale":
|
||||
//缩放画布
|
||||
let scale = sacleSize.value / 100
|
||||
element!.style.transform = `scale(${scale})`
|
||||
break
|
||||
case "size":
|
||||
//图表尺寸
|
||||
element!.style.width = widthCanvas.value + 'px';
|
||||
element!.style.height = heightCanvas.value + 'px';
|
||||
reloadCanvas()
|
||||
break
|
||||
case "title":
|
||||
let fakerTitle = ''
|
||||
let fakerSubTitle = ''
|
||||
//修改标题相关配置
|
||||
if (titleSwitch.value === true) {
|
||||
fakerTitle = title.value
|
||||
}
|
||||
if (subTitleSwitch.value === true) {
|
||||
fakerSubTitle = subTitle.value
|
||||
}
|
||||
myChart.value?.setOption({
|
||||
title: {
|
||||
text: fakerTitle,
|
||||
subtext: fakerSubTitle,
|
||||
left: titlePos.value
|
||||
}
|
||||
})
|
||||
break
|
||||
case "color":
|
||||
//图表属性颜色
|
||||
myChart.value?.setOption({
|
||||
series: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: attrColor.value
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
case "watermark":
|
||||
//水印
|
||||
if (watermarkSwitch.value === true) {
|
||||
myChart.value?.setOption({
|
||||
backgroundColor: {
|
||||
image: createWatermark(),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
myChart.value?.setOption({
|
||||
backgroundColor: '#fff'
|
||||
})
|
||||
}
|
||||
break;
|
||||
case "data":
|
||||
//更新数据
|
||||
myChart.value?.setOption({
|
||||
xAxis: {
|
||||
data: colunmData.value,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: valueData.value,
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const handleChange = () => {
|
||||
|
||||
}
|
||||
|
||||
//数据
|
||||
const colunmData = ref(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
|
||||
const valueData = ref(['23', '24', '18', '25', '27', '28', '25']);
|
||||
//选项
|
||||
const option = {
|
||||
backgroundColor: '#fff',
|
||||
// backgroundColor: {
|
||||
// // type: 'pattern',
|
||||
// // image: canvas,
|
||||
// // repeat: 'repeat'
|
||||
// },
|
||||
title: {
|
||||
text: title.value,
|
||||
subtext: subTitle.value,
|
||||
left: titlePos.value
|
||||
},
|
||||
xAxis: {
|
||||
data: colunmData.value,
|
||||
},
|
||||
yAxis: {},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
data: valueData.value,
|
||||
itemStyle: {
|
||||
color: attrColor.value
|
||||
}
|
||||
}
|
||||
],
|
||||
tooltip: {}
|
||||
};
|
||||
|
||||
//重新加载画布
|
||||
const reloadCanvas = () => {
|
||||
myChart.value = echarts.init(chartDom.value)
|
||||
myChart.value.clear();
|
||||
myChart.value.resize({
|
||||
width: widthCanvas.value,
|
||||
height: heightCanvas.value
|
||||
})
|
||||
myChart.value.setOption(option)
|
||||
}
|
||||
|
||||
//下载echarts图表图片
|
||||
const downEchartsImg = () => {
|
||||
let ext = downType.value == '1' ? 'png' : 'jpeg'
|
||||
let imgUrl = myChart.value?.getDataURL({
|
||||
type: ext as "png" | 'jpeg',
|
||||
pixelRatio: 2,
|
||||
})
|
||||
|
||||
if (imgUrl != undefined) {
|
||||
var downloadLink = document.createElement('a');
|
||||
downloadLink.href = imgUrl;
|
||||
downloadLink.download = 'echart.' + ext;
|
||||
downloadLink.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 上传、编辑数据 */
|
||||
//抽屉开关
|
||||
const drawer = ref(false)
|
||||
const rowsData = ref({})
|
||||
|
||||
const editData = () => {
|
||||
if (drawer.value == true) {
|
||||
drawer.value = false
|
||||
} else {
|
||||
drawer.value = true
|
||||
/** 在线excel */
|
||||
Spreadsheet.locale('zh-cn', (window.x_spreadsheet as any).$messages['zh-cn']);
|
||||
//const sheet = new Spreadsheet("#x-spreadsheet", {
|
||||
new Spreadsheet("#x-spreadsheet", {
|
||||
showToolbar: false, //隐藏顶部工具栏
|
||||
showBottomBar: false,//隐藏底部工具栏,
|
||||
view: {
|
||||
height: () => document.documentElement.clientHeight / 2,
|
||||
width: () => document.documentElement.clientWidth,
|
||||
}
|
||||
})
|
||||
.loadData({
|
||||
//样式
|
||||
styles: [
|
||||
{
|
||||
bgcolor: '#f4f5f8',
|
||||
textwrap: true,
|
||||
color: '#900b09',
|
||||
border: {
|
||||
top: ['thin', '#0366d6'],
|
||||
bottom: ['thin', '#0366d6'],
|
||||
right: ['thin', '#0366d6'],
|
||||
left: ['thin', '#0366d6'],
|
||||
},
|
||||
},
|
||||
],
|
||||
//数据
|
||||
rows: rowsData.value
|
||||
}) // load data
|
||||
.change(data => {
|
||||
//表格数据改变后触发
|
||||
//规则: 获取第一列和第二列的数据
|
||||
let tmp = toEchartsData(data)
|
||||
colunmData.value = tmp[0]
|
||||
valueData.value = tmp[1]
|
||||
canvasHandle('data')
|
||||
});
|
||||
// sheet.validate()
|
||||
}
|
||||
}
|
||||
|
||||
//上传数据文件
|
||||
const fileList = ref()
|
||||
const updateDataFile = async (params) => {
|
||||
const _file = params.file;
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (ev) => {
|
||||
try {
|
||||
if (!ev.target) {
|
||||
return
|
||||
}
|
||||
const data = ev.target.result;
|
||||
const workbook = XLSX.read(data, {
|
||||
type: 'binary'
|
||||
});
|
||||
let useCount = 0;
|
||||
let tmpColumnData:any[] = [];
|
||||
let tmpValueData:any[] = [];
|
||||
for (let sheet in workbook.Sheets) {
|
||||
//这里只需要第一个sheet
|
||||
if (useCount > 0) {
|
||||
continue;
|
||||
}
|
||||
//循环读取每个文件
|
||||
const sheetArray = XLSX.utils.sheet_to_json(workbook.Sheets[sheet], {header: ['0', '1']});
|
||||
//若当前sheet没有数据,则continue
|
||||
if(sheetArray.length == 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
for(let item in sheetArray){
|
||||
if (sheetArray[item] != undefined) {
|
||||
let tmp1 = sheetArray as []
|
||||
tmpColumnData.push(tmp1[item][0])
|
||||
tmpValueData.push(tmp1[item][1])
|
||||
}
|
||||
}
|
||||
useCount++
|
||||
}
|
||||
//更新数据
|
||||
colunmData.value = tmpColumnData
|
||||
valueData.value = tmpValueData
|
||||
//更新图表
|
||||
canvasHandle('data')
|
||||
//更新表格
|
||||
rowsData.value = toSpreadsheetData([
|
||||
colunmData, valueData
|
||||
])
|
||||
} catch (e) {
|
||||
console.log('error')
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
fileReader.readAsArrayBuffer(_file)
|
||||
}
|
||||
|
||||
//copy
|
||||
// const copyRes = async (resStr: string) => {
|
||||
// copy(resStr)
|
||||
// }
|
||||
|
||||
onMounted(() => {
|
||||
//init echart dom
|
||||
chartDom.value = document.getElementById('main')
|
||||
//设置画布宽高
|
||||
canvasHandle('size')
|
||||
//reload canvas
|
||||
reloadCanvas()
|
||||
//数据格式转换成
|
||||
rowsData.value = toSpreadsheetData([
|
||||
colunmData, valueData
|
||||
])
|
||||
// console.log(rowsData.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl flex">
|
||||
<div class="w-4/6">
|
||||
<div class="flex flex-row-reverse mb-4">
|
||||
<el-input-number v-model="sacleSize" :min="1" :max="100" :step="5" @change="canvasHandle('scale')" step-strictly/>
|
||||
<el-text>缩放:</el-text>
|
||||
</div>
|
||||
<!-- <div class="flex justify-center items-center"> -->
|
||||
<div class="flex justify-center items-center max-h-[500px] max-w-[1000px] overflow-auto">
|
||||
<div id="main" class="bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/6 bg-white ml-3 rounded-lg p-3">
|
||||
<div class="mb-3">
|
||||
<el-button class="w-full mb-3" type="primary" size="large" @click="downEchartsImg">下载图表</el-button>
|
||||
<div class="flex items-center ml-1">
|
||||
<el-text>文件类型</el-text>
|
||||
<el-radio-group v-model="downType" class="ml-4">
|
||||
<el-radio size="large" label="1">PNG</el-radio>
|
||||
<el-radio size="large" label="2">JPEG</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="支持上传:xls, xlsx, csv文件"
|
||||
placement="top-start"
|
||||
>
|
||||
<el-button class="w-1/2">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
class="dataFileRef flex"
|
||||
ref="dataFileRef"
|
||||
accept=".xls,.xlsx,.csv"
|
||||
:http-request="updateDataFile"
|
||||
:limit="1"
|
||||
>
|
||||
上传数据
|
||||
</el-upload>
|
||||
</el-button>
|
||||
|
||||
</el-tooltip>
|
||||
<el-button class="w-1/2" @click="editData">编辑数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse v-model="setOptionName" @change="handleChange" accordion>
|
||||
<el-collapse-item title="画布设置" name="1">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">宽度</el-text>
|
||||
<el-input-number
|
||||
v-model="widthCanvas"
|
||||
:min="1"
|
||||
:max="4000"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
class="h-8 w-60 max-w-[60%] ml-3"
|
||||
@change="canvasHandle('size')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3">
|
||||
<el-text class="w-16 text-right">高度</el-text>
|
||||
<el-input-number
|
||||
v-model="heightCanvas"
|
||||
:min="1"
|
||||
:max="4000"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
class="h-8 w-60 max-w-[60%] ml-3"
|
||||
@change="canvasHandle('size')"
|
||||
/>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="标题设置" name="2">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">位置</el-text>
|
||||
<el-radio-group v-model="titlePos" class="ml-3" @change="canvasHandle('title')">
|
||||
<el-radio-button label="left" value="left">左</el-radio-button>
|
||||
<el-radio-button label="center" value="center">中</el-radio-button>
|
||||
<el-radio-button label="right" value="right">右</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-divider content-position="left" class="pl-10">标题</el-divider>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="titleSwitch" class="ml-3" @change="canvasHandle('title')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">标题</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="title" @blur="canvasHandle('title')"></el-input>
|
||||
</div>
|
||||
<el-divider content-position="left">副标题</el-divider>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="subTitleSwitch" class="ml-3" @change="canvasHandle('title')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">副标题</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="subTitle" @blur="canvasHandle('title')"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="图形属性" name="3">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">颜色</el-text>
|
||||
<el-color-picker v-model="attrColor" class="ml-3" @change="canvasHandle('color')"/>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="水印设置" name="4">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="watermarkSwitch" class="ml-3" @change="canvasHandle('watermark')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">水印内容</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="waterMarkText" @change="canvasHandle('watermark')"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer id="x-spreadsheet" v-model="drawer" direction="btt" class="sheet" style="">
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-drawer__body){
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.el-drawer__header){
|
||||
display: none !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
:deep(.el-drawer){
|
||||
height: 50% !important;
|
||||
}
|
||||
:deep(.el-upload-list){
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,474 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, onMounted } from 'vue'
|
||||
import Spreadsheet from 'x-data-spreadsheet'
|
||||
import 'x-data-spreadsheet/dist/locale/zh-cn';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
// import { copy } from '@/utils/string'
|
||||
import { toEchartsPieData, toSpreadsheetData, tranObjAndColumn } from '@/utils/echarts'
|
||||
import * as echarts from 'echarts'
|
||||
import * as XLSX from 'xlsx'
|
||||
const info = reactive({
|
||||
title: "饼图",
|
||||
})
|
||||
|
||||
const chartDom = ref<HTMLElement|null>()
|
||||
const myChart = ref<echarts.ECharts>()
|
||||
|
||||
const setOptionName = ref(1)
|
||||
//缩放比例
|
||||
const sacleSize = ref(100)
|
||||
//画布宽高
|
||||
const widthCanvas = ref(720)
|
||||
const heightCanvas = ref(400)
|
||||
//下载文件类型
|
||||
const downType = ref('1')
|
||||
//图形属性颜色
|
||||
const attrColor = ref('#5470c6')
|
||||
//标题位置
|
||||
const titlePos = ref('center')
|
||||
//标题
|
||||
const title = ref('然部落在线工具')
|
||||
//副标题
|
||||
const subTitle = ref('在线图表制作工具')
|
||||
//显示标题 - 开关
|
||||
const titleSwitch = ref(true)
|
||||
//显示副标题 - 开关
|
||||
const subTitleSwitch = ref(true)
|
||||
|
||||
/** 水印 */
|
||||
const watermarkSwitch = ref(false) //开关
|
||||
const waterMarkText = ref('然部落工具');
|
||||
|
||||
//创建水印
|
||||
const createWatermark = () => {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
canvas.width = canvas.height = 100;
|
||||
ctx!.textAlign = 'center';
|
||||
ctx!.textBaseline = 'middle';
|
||||
ctx!.globalAlpha = 0.08;
|
||||
ctx!.font = '20px Microsoft Yahei';
|
||||
ctx!.translate(50, 50);
|
||||
ctx!.rotate(-Math.PI / 4);
|
||||
ctx!.fillText(waterMarkText.value, 0, 0);
|
||||
return canvas
|
||||
}
|
||||
|
||||
//操作图表
|
||||
const canvasHandle = (type) => {
|
||||
// let element = document.getElementById('main')
|
||||
let element = chartDom.value
|
||||
switch(type) {
|
||||
case "scale":
|
||||
//缩放画布
|
||||
let scale = sacleSize.value / 100
|
||||
element!.style.transform = `scale(${scale})`
|
||||
break
|
||||
case "size":
|
||||
//图表尺寸
|
||||
element!.style.width = widthCanvas.value + 'px';
|
||||
element!.style.height = heightCanvas.value + 'px';
|
||||
reloadCanvas()
|
||||
break
|
||||
case "title":
|
||||
let fakerTitle = ''
|
||||
let fakerSubTitle = ''
|
||||
//修改标题相关配置
|
||||
if (titleSwitch.value === true) {
|
||||
fakerTitle = title.value
|
||||
}
|
||||
if (subTitleSwitch.value === true) {
|
||||
fakerSubTitle = subTitle.value
|
||||
}
|
||||
myChart.value?.setOption({
|
||||
title: {
|
||||
text: fakerTitle,
|
||||
subtext: fakerSubTitle,
|
||||
left: titlePos.value
|
||||
}
|
||||
})
|
||||
break
|
||||
case "color":
|
||||
//图表属性颜色
|
||||
myChart.value?.setOption({
|
||||
series: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: attrColor.value
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
case "watermark":
|
||||
//水印
|
||||
if (watermarkSwitch.value === true) {
|
||||
myChart.value?.setOption({
|
||||
backgroundColor: {
|
||||
image: createWatermark(),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
myChart.value?.setOption({
|
||||
backgroundColor: '#fff'
|
||||
})
|
||||
}
|
||||
break;
|
||||
case "data":
|
||||
//更新数据
|
||||
myChart.value?.setOption({
|
||||
series: [
|
||||
{
|
||||
data: seriesData.value,
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const handleChange = () => {
|
||||
|
||||
}
|
||||
|
||||
//数据
|
||||
const colunmData = ref(['Search Engine', 'Direct', 'Email', 'Union Ads', 'Video Ads'] as string[]);
|
||||
const valueData = ref([1048, 735, 580, 484, 300] as number[]);
|
||||
const seriesData = ref([] as Object[])
|
||||
//选项
|
||||
const option = {
|
||||
backgroundColor: '#fff',
|
||||
// backgroundColor: {
|
||||
// // type: 'pattern',
|
||||
// // image: canvas,
|
||||
// // repeat: 'repeat'
|
||||
// },
|
||||
title: {
|
||||
text: title.value,
|
||||
subtext: subTitle.value,
|
||||
left: titlePos.value
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: seriesData.value,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
// trigger: 'item'
|
||||
},
|
||||
};
|
||||
|
||||
//重新加载画布
|
||||
const reloadCanvas = () => {
|
||||
myChart.value = echarts.init(chartDom.value)
|
||||
myChart.value.clear();
|
||||
myChart.value.resize({
|
||||
width: widthCanvas.value,
|
||||
height: heightCanvas.value
|
||||
})
|
||||
myChart.value.setOption(option)
|
||||
//加载数据
|
||||
canvasHandle('data')
|
||||
}
|
||||
|
||||
//下载echarts图表图片
|
||||
const downEchartsImg = () => {
|
||||
let ext = downType.value == '1' ? 'png' : 'jpeg'
|
||||
let imgUrl = myChart.value?.getDataURL({
|
||||
type: ext as "png" | 'jpeg',
|
||||
pixelRatio: 2,
|
||||
})
|
||||
|
||||
if (imgUrl != undefined) {
|
||||
var downloadLink = document.createElement('a');
|
||||
downloadLink.href = imgUrl;
|
||||
downloadLink.download = 'echart.' + ext;
|
||||
downloadLink.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 上传、编辑数据 */
|
||||
//抽屉开关
|
||||
const drawer = ref(false)
|
||||
const rowsData = ref({})
|
||||
|
||||
const editData = () => {
|
||||
if (drawer.value == true) {
|
||||
drawer.value = false
|
||||
} else {
|
||||
drawer.value = true
|
||||
/** 在线excel */
|
||||
Spreadsheet.locale('zh-cn', (window.x_spreadsheet as any).$messages['zh-cn']);
|
||||
//const sheet = new Spreadsheet("#x-spreadsheet", {
|
||||
new Spreadsheet("#x-spreadsheet", {
|
||||
showToolbar: false, //隐藏顶部工具栏
|
||||
showBottomBar: false,//隐藏底部工具栏,
|
||||
view: {
|
||||
height: () => document.documentElement.clientHeight / 2,
|
||||
width: () => document.documentElement.clientWidth,
|
||||
}
|
||||
})
|
||||
.loadData({
|
||||
//样式
|
||||
styles: [
|
||||
{
|
||||
bgcolor: '#f4f5f8',
|
||||
textwrap: true,
|
||||
color: '#900b09',
|
||||
border: {
|
||||
top: ['thin', '#0366d6'],
|
||||
bottom: ['thin', '#0366d6'],
|
||||
right: ['thin', '#0366d6'],
|
||||
left: ['thin', '#0366d6'],
|
||||
},
|
||||
},
|
||||
],
|
||||
//数据
|
||||
rows: rowsData.value
|
||||
}) // load data
|
||||
.change(data => {
|
||||
//表格数据改变后触发
|
||||
//规则: 获取第一列和第二列的数据
|
||||
seriesData.value = toEchartsPieData(data)
|
||||
canvasHandle('data')
|
||||
});
|
||||
// sheet.validate()
|
||||
}
|
||||
}
|
||||
|
||||
//上传数据文件
|
||||
const fileList = ref()
|
||||
const updateDataFile = async (params) => {
|
||||
const _file = params.file;
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (ev) => {
|
||||
try {
|
||||
if (!ev.target) {
|
||||
return
|
||||
}
|
||||
const data = ev.target.result;
|
||||
const workbook = XLSX.read(data, {
|
||||
type: 'binary'
|
||||
});
|
||||
let useCount = 0;
|
||||
let tmpColumnData:any[] = [];
|
||||
let tmpValueData:any[] = [];
|
||||
for (let sheet in workbook.Sheets) {
|
||||
//这里只需要第一个sheet
|
||||
if (useCount > 0) {
|
||||
continue;
|
||||
}
|
||||
//循环读取每个文件
|
||||
const sheetArray = XLSX.utils.sheet_to_json(workbook.Sheets[sheet], {header: ['0', '1']});
|
||||
//若当前sheet没有数据,则continue
|
||||
if(sheetArray.length == 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
for(let item in sheetArray){
|
||||
if (sheetArray[item] != undefined) {
|
||||
let tmp1 = sheetArray as []
|
||||
tmpColumnData.push(tmp1[item][0])
|
||||
tmpValueData.push(tmp1[item][1])
|
||||
}
|
||||
}
|
||||
useCount++
|
||||
}
|
||||
//更新数据
|
||||
colunmData.value = tmpColumnData
|
||||
valueData.value = tmpValueData
|
||||
seriesData.value = tranObjAndColumn([
|
||||
colunmData.value, valueData.value
|
||||
])
|
||||
//更新图表
|
||||
canvasHandle('data')
|
||||
//更新表格
|
||||
rowsData.value = toSpreadsheetData([
|
||||
colunmData, valueData
|
||||
])
|
||||
} catch (e) {
|
||||
console.log('error')
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
fileReader.readAsArrayBuffer(_file)
|
||||
}
|
||||
|
||||
//copy
|
||||
// const copyRes = async (resStr: string) => {
|
||||
// copy(resStr)
|
||||
// }
|
||||
|
||||
onMounted(() => {
|
||||
//初始化数据
|
||||
seriesData.value = tranObjAndColumn([
|
||||
colunmData.value, valueData.value
|
||||
])
|
||||
//init echart dom
|
||||
chartDom.value = document.getElementById('main')
|
||||
//设置画布宽高
|
||||
canvasHandle('size')
|
||||
//reload canvas
|
||||
reloadCanvas()
|
||||
//数据格式转换成
|
||||
rowsData.value = toSpreadsheetData([
|
||||
colunmData, valueData
|
||||
])
|
||||
// console.log(rowsData.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl flex">
|
||||
<div class="w-4/6">
|
||||
<div class="flex flex-row-reverse mb-4">
|
||||
<el-input-number v-model="sacleSize" :min="1" :max="100" :step="5" @change="canvasHandle('scale')" step-strictly/>
|
||||
<el-text>缩放:</el-text>
|
||||
</div>
|
||||
<!-- <div class="flex justify-center items-center"> -->
|
||||
<div class="flex justify-center items-center max-h-[500px] max-w-[1000px] overflow-auto">
|
||||
<div id="main" class="bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/6 bg-white ml-3 rounded-lg p-3">
|
||||
<div class="mb-3">
|
||||
<el-button class="w-full mb-3" type="primary" size="large" @click="downEchartsImg">下载图表</el-button>
|
||||
<div class="flex items-center ml-1">
|
||||
<el-text>文件类型</el-text>
|
||||
<el-radio-group v-model="downType" class="ml-4">
|
||||
<el-radio size="large" label="1">PNG</el-radio>
|
||||
<el-radio size="large" label="2">JPEG</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="支持上传:xls, xlsx, csv文件"
|
||||
placement="top-start"
|
||||
>
|
||||
<el-button class="w-1/2">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
class="dataFileRef flex"
|
||||
ref="dataFileRef"
|
||||
accept=".xls,.xlsx,.csv"
|
||||
:http-request="updateDataFile"
|
||||
:limit="1"
|
||||
>
|
||||
上传数据
|
||||
</el-upload>
|
||||
</el-button>
|
||||
|
||||
</el-tooltip>
|
||||
<el-button class="w-1/2" @click="editData">编辑数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse v-model="setOptionName" @change="handleChange" accordion>
|
||||
<el-collapse-item title="画布设置" name="1">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">宽度</el-text>
|
||||
<el-input-number
|
||||
v-model="widthCanvas"
|
||||
:min="1"
|
||||
:max="4000"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
class="h-8 w-60 max-w-[60%] ml-3"
|
||||
@change="canvasHandle('size')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3">
|
||||
<el-text class="w-16 text-right">高度</el-text>
|
||||
<el-input-number
|
||||
v-model="heightCanvas"
|
||||
:min="1"
|
||||
:max="4000"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
class="h-8 w-60 max-w-[60%] ml-3"
|
||||
@change="canvasHandle('size')"
|
||||
/>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="标题设置" name="2">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">位置</el-text>
|
||||
<el-radio-group v-model="titlePos" class="ml-3" @change="canvasHandle('title')">
|
||||
<el-radio-button label="left" value="left">左</el-radio-button>
|
||||
<el-radio-button label="center" value="center">中</el-radio-button>
|
||||
<el-radio-button label="right" value="right">右</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-divider content-position="left" class="pl-10">标题</el-divider>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="titleSwitch" class="ml-3" @change="canvasHandle('title')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">标题</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="title" @blur="canvasHandle('title')"></el-input>
|
||||
</div>
|
||||
<el-divider content-position="left">副标题</el-divider>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="subTitleSwitch" class="ml-3" @change="canvasHandle('title')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">副标题</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="subTitle" @blur="canvasHandle('title')"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<!-- <el-collapse-item title="图形属性" name="3">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">颜色</el-text>
|
||||
<el-color-picker v-model="attrColor" class="ml-3" @change="canvasHandle('color')"/>
|
||||
</div>
|
||||
</el-collapse-item> -->
|
||||
<el-collapse-item title="水印设置" name="4">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="watermarkSwitch" class="ml-3" @change="canvasHandle('watermark')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">水印内容</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="waterMarkText" @change="canvasHandle('watermark')"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer id="x-spreadsheet" v-model="drawer" direction="btt" custom-class="sheet" style="">
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-drawer__body){
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.el-drawer__header){
|
||||
display: none !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
:deep(.el-drawer){
|
||||
height: 50% !important;
|
||||
}
|
||||
:deep(.el-upload-list){
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,469 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, onMounted } from 'vue'
|
||||
import Spreadsheet from 'x-data-spreadsheet'
|
||||
import 'x-data-spreadsheet/dist/locale/zh-cn';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
// import { copy } from '@/utils/string'
|
||||
import { toEchartsData, toSpreadsheetData, tranObjAndColumn } from '@/utils/echarts'
|
||||
import * as echarts from 'echarts'
|
||||
import * as XLSX from 'xlsx'
|
||||
const info = reactive({
|
||||
title: "散点图",
|
||||
})
|
||||
|
||||
const chartDom = ref<HTMLElement|null>()
|
||||
const myChart = ref<echarts.ECharts>()
|
||||
|
||||
const setOptionName = ref(1)
|
||||
//缩放比例
|
||||
const sacleSize = ref(100)
|
||||
//画布宽高
|
||||
const widthCanvas = ref(720)
|
||||
const heightCanvas = ref(400)
|
||||
//下载文件类型
|
||||
const downType = ref('1')
|
||||
//图形属性颜色
|
||||
const attrColor = ref('#5470c6')
|
||||
//标题位置
|
||||
const titlePos = ref('center')
|
||||
//标题
|
||||
const title = ref('然部落在线工具')
|
||||
//副标题
|
||||
const subTitle = ref('在线图表制作工具')
|
||||
//显示标题 - 开关
|
||||
const titleSwitch = ref(true)
|
||||
//显示副标题 - 开关
|
||||
const subTitleSwitch = ref(true)
|
||||
|
||||
/** 水印 */
|
||||
const watermarkSwitch = ref(false) //开关
|
||||
const waterMarkText = ref('然部落工具');
|
||||
|
||||
//创建水印
|
||||
const createWatermark = () => {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
canvas.width = canvas.height = 100;
|
||||
ctx!.textAlign = 'center';
|
||||
ctx!.textBaseline = 'middle';
|
||||
ctx!.globalAlpha = 0.08;
|
||||
ctx!.font = '20px Microsoft Yahei';
|
||||
ctx!.translate(50, 50);
|
||||
ctx!.rotate(-Math.PI / 4);
|
||||
ctx!.fillText(waterMarkText.value, 0, 0);
|
||||
return canvas
|
||||
}
|
||||
|
||||
//操作图表
|
||||
const canvasHandle = (type) => {
|
||||
// let element = document.getElementById('main')
|
||||
let element = chartDom.value
|
||||
switch(type) {
|
||||
case "scale":
|
||||
//缩放画布
|
||||
let scale = sacleSize.value / 100
|
||||
element!.style.transform = `scale(${scale})`
|
||||
break
|
||||
case "size":
|
||||
//图表尺寸
|
||||
element!.style.width = widthCanvas.value + 'px';
|
||||
element!.style.height = heightCanvas.value + 'px';
|
||||
reloadCanvas()
|
||||
break
|
||||
case "title":
|
||||
let fakerTitle = ''
|
||||
let fakerSubTitle = ''
|
||||
//修改标题相关配置
|
||||
if (titleSwitch.value === true) {
|
||||
fakerTitle = title.value
|
||||
}
|
||||
if (subTitleSwitch.value === true) {
|
||||
fakerSubTitle = subTitle.value
|
||||
}
|
||||
myChart.value?.setOption({
|
||||
title: {
|
||||
text: fakerTitle,
|
||||
subtext: fakerSubTitle,
|
||||
left: titlePos.value
|
||||
}
|
||||
})
|
||||
break
|
||||
case "color":
|
||||
//图表属性颜色
|
||||
myChart.value?.setOption({
|
||||
series: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: attrColor.value
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
case "watermark":
|
||||
//水印
|
||||
if (watermarkSwitch.value === true) {
|
||||
myChart.value?.setOption({
|
||||
backgroundColor: {
|
||||
image: createWatermark(),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
myChart.value?.setOption({
|
||||
backgroundColor: '#fff'
|
||||
})
|
||||
}
|
||||
break;
|
||||
case "data":
|
||||
//更新数据
|
||||
myChart.value?.setOption({
|
||||
series: [
|
||||
{
|
||||
data: seriesData.value,
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const handleChange = () => {
|
||||
|
||||
}
|
||||
|
||||
//数据
|
||||
const colunmData = ref([10.0, 8.07, 13.0, 9.05, 11.0, 14.0, 13.4, 10.0, 14.0, 12.5, 9.15, 11.5, 3.03, 12.2, 2.02, 1.05, 4.05, 6.03, 12.0, 7.08, 5.02, 6.03]);
|
||||
const valueData = ref([8.04, 6.95, 7.58, 8.81, 8.33, 7.66, 6.81, 6.33, 8.96, 6.82, 7.2, 7.2, 4.23, 7.83, 4.47, 3.33, 4.96, 7.24, 6.26, 8.84, 5.82, 5.68, 7.05]);
|
||||
const seriesData = ref([] as Array<any>[])
|
||||
//选项
|
||||
const option = {
|
||||
backgroundColor: '#fff',
|
||||
// backgroundColor: {
|
||||
// // type: 'pattern',
|
||||
// // image: canvas,
|
||||
// // repeat: 'repeat'
|
||||
// },
|
||||
title: {
|
||||
text: title.value,
|
||||
subtext: subTitle.value,
|
||||
left: titlePos.value
|
||||
},
|
||||
xAxis: {},
|
||||
yAxis: {},
|
||||
series: [
|
||||
{
|
||||
type: 'scatter',
|
||||
symbolSize: 30,
|
||||
data: seriesData.value,
|
||||
}
|
||||
],
|
||||
tooltip: {}
|
||||
};
|
||||
|
||||
//重新加载画布
|
||||
const reloadCanvas = () => {
|
||||
myChart.value = echarts.init(chartDom.value)
|
||||
myChart.value.clear();
|
||||
myChart.value.resize({
|
||||
width: widthCanvas.value,
|
||||
height: heightCanvas.value
|
||||
})
|
||||
myChart.value.setOption(option)
|
||||
//加载数据
|
||||
canvasHandle('data')
|
||||
}
|
||||
|
||||
//下载echarts图表图片
|
||||
const downEchartsImg = () => {
|
||||
let ext = downType.value == '1' ? 'png' : 'jpeg'
|
||||
let imgUrl = myChart.value?.getDataURL({
|
||||
type: ext as "png" | 'jpeg',
|
||||
pixelRatio: 2,
|
||||
})
|
||||
|
||||
if (imgUrl != undefined) {
|
||||
var downloadLink = document.createElement('a');
|
||||
downloadLink.href = imgUrl;
|
||||
downloadLink.download = 'echart.' + ext;
|
||||
downloadLink.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 上传、编辑数据 */
|
||||
//抽屉开关
|
||||
const drawer = ref(false)
|
||||
const rowsData = ref({})
|
||||
|
||||
const editData = () => {
|
||||
if (drawer.value == true) {
|
||||
drawer.value = false
|
||||
} else {
|
||||
drawer.value = true
|
||||
/** 在线excel */
|
||||
Spreadsheet.locale('zh-cn', (window.x_spreadsheet as any).$messages['zh-cn']);
|
||||
//const sheet = new Spreadsheet("#x-spreadsheet", {
|
||||
new Spreadsheet("#x-spreadsheet", {
|
||||
showToolbar: false, //隐藏顶部工具栏
|
||||
showBottomBar: false,//隐藏底部工具栏,
|
||||
view: {
|
||||
height: () => document.documentElement.clientHeight / 2,
|
||||
width: () => document.documentElement.clientWidth,
|
||||
}
|
||||
})
|
||||
.loadData({
|
||||
//样式
|
||||
styles: [
|
||||
{
|
||||
bgcolor: '#f4f5f8',
|
||||
textwrap: true,
|
||||
color: '#900b09',
|
||||
border: {
|
||||
top: ['thin', '#0366d6'],
|
||||
bottom: ['thin', '#0366d6'],
|
||||
right: ['thin', '#0366d6'],
|
||||
left: ['thin', '#0366d6'],
|
||||
},
|
||||
},
|
||||
],
|
||||
//数据
|
||||
rows: rowsData.value
|
||||
}) // load data
|
||||
.change(data => {
|
||||
//表格数据改变后触发
|
||||
//规则: 获取第一列和第二列的数据
|
||||
let tmp = toEchartsData(data)
|
||||
colunmData.value = tmp[0]
|
||||
valueData.value = tmp[1]
|
||||
canvasHandle('data')
|
||||
});
|
||||
// sheet.validate()
|
||||
}
|
||||
}
|
||||
|
||||
//上传数据文件
|
||||
const fileList = ref()
|
||||
const updateDataFile = async (params) => {
|
||||
const _file = params.file;
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (ev) => {
|
||||
try {
|
||||
if (!ev.target) {
|
||||
return
|
||||
}
|
||||
const data = ev.target.result;
|
||||
const workbook = XLSX.read(data, {
|
||||
type: 'binary'
|
||||
});
|
||||
let useCount = 0;
|
||||
let tmpColumnData:any[] = [];
|
||||
let tmpValueData:any[] = [];
|
||||
for (let sheet in workbook.Sheets) {
|
||||
//这里只需要第一个sheet
|
||||
if (useCount > 0) {
|
||||
continue;
|
||||
}
|
||||
//循环读取每个文件
|
||||
const sheetArray = XLSX.utils.sheet_to_json(workbook.Sheets[sheet], {header: ['0', '1']});
|
||||
//若当前sheet没有数据,则continue
|
||||
if(sheetArray.length == 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
for(let item in sheetArray){
|
||||
if (sheetArray[item] != undefined) {
|
||||
let tmp1 = sheetArray as []
|
||||
tmpColumnData.push(tmp1[item][0])
|
||||
tmpValueData.push(tmp1[item][1])
|
||||
}
|
||||
}
|
||||
useCount++
|
||||
}
|
||||
//更新数据
|
||||
colunmData.value = tmpColumnData
|
||||
valueData.value = tmpValueData
|
||||
seriesData.value = tranObjAndColumn([
|
||||
colunmData.value, valueData.value
|
||||
])
|
||||
//更新图表
|
||||
canvasHandle('data')
|
||||
//更新表格
|
||||
rowsData.value = toSpreadsheetData([
|
||||
colunmData, valueData
|
||||
])
|
||||
} catch (e) {
|
||||
console.log('error')
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
fileReader.readAsArrayBuffer(_file)
|
||||
}
|
||||
|
||||
//copy
|
||||
// const copyRes = async (resStr: string) => {
|
||||
// copy(resStr)
|
||||
// }
|
||||
|
||||
onMounted(() => {
|
||||
//初始化数据
|
||||
seriesData.value = tranObjAndColumn([
|
||||
colunmData.value, valueData.value
|
||||
], 'toCoord')
|
||||
//init echart dom
|
||||
chartDom.value = document.getElementById('main')
|
||||
//设置画布宽高
|
||||
canvasHandle('size')
|
||||
//reload canvas
|
||||
reloadCanvas()
|
||||
//数据格式转换成
|
||||
rowsData.value = toSpreadsheetData([
|
||||
colunmData, valueData
|
||||
])
|
||||
// console.log(rowsData.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl flex">
|
||||
<div class="w-4/6">
|
||||
<div class="flex flex-row-reverse mb-4">
|
||||
<el-input-number v-model="sacleSize" :min="1" :max="100" :step="5" @change="canvasHandle('scale')" step-strictly/>
|
||||
<el-text>缩放:</el-text>
|
||||
</div>
|
||||
<!-- <div class="flex justify-center items-center"> -->
|
||||
<div class="flex justify-center items-center max-h-[500px] max-w-[1000px] overflow-auto">
|
||||
<div id="main" class="bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/6 bg-white ml-3 rounded-lg p-3">
|
||||
<div class="mb-3">
|
||||
<el-button class="w-full mb-3" type="primary" size="large" @click="downEchartsImg">下载图表</el-button>
|
||||
<div class="flex items-center ml-1">
|
||||
<el-text>文件类型</el-text>
|
||||
<el-radio-group v-model="downType" class="ml-4">
|
||||
<el-radio size="large" label="1">PNG</el-radio>
|
||||
<el-radio size="large" label="2">JPEG</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="支持上传:xls, xlsx, csv文件"
|
||||
placement="top-start"
|
||||
>
|
||||
<el-button class="w-1/2">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
class="dataFileRef flex"
|
||||
ref="dataFileRef"
|
||||
accept=".xls,.xlsx,.csv"
|
||||
:http-request="updateDataFile"
|
||||
:limit="1"
|
||||
>
|
||||
上传数据
|
||||
</el-upload>
|
||||
</el-button>
|
||||
|
||||
</el-tooltip>
|
||||
<el-button class="w-1/2" @click="editData">编辑数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse v-model="setOptionName" @change="handleChange" accordion>
|
||||
<el-collapse-item title="画布设置" name="1">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">宽度</el-text>
|
||||
<el-input-number
|
||||
v-model="widthCanvas"
|
||||
:min="1"
|
||||
:max="4000"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
class="h-8 w-60 max-w-[60%] ml-3"
|
||||
@change="canvasHandle('size')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3">
|
||||
<el-text class="w-16 text-right">高度</el-text>
|
||||
<el-input-number
|
||||
v-model="heightCanvas"
|
||||
:min="1"
|
||||
:max="4000"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
class="h-8 w-60 max-w-[60%] ml-3"
|
||||
@change="canvasHandle('size')"
|
||||
/>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="标题设置" name="2">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">位置</el-text>
|
||||
<el-radio-group v-model="titlePos" class="ml-3" @change="canvasHandle('title')">
|
||||
<el-radio-button label="left" value="left">左</el-radio-button>
|
||||
<el-radio-button label="center" value="center">中</el-radio-button>
|
||||
<el-radio-button label="right" value="right">右</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-divider content-position="left" class="pl-10">标题</el-divider>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="titleSwitch" class="ml-3" @change="canvasHandle('title')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">标题</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="title" @blur="canvasHandle('title')"></el-input>
|
||||
</div>
|
||||
<el-divider content-position="left">副标题</el-divider>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="subTitleSwitch" class="ml-3" @change="canvasHandle('title')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">副标题</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="subTitle" @blur="canvasHandle('title')"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="图形属性" name="3">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">颜色</el-text>
|
||||
<el-color-picker v-model="attrColor" class="ml-3" @change="canvasHandle('color')"/>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="水印设置" name="4">
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">显示</el-text>
|
||||
<el-switch v-model="watermarkSwitch" class="ml-3" @change="canvasHandle('watermark')"/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-text class="w-16 text-right">水印内容</el-text>
|
||||
<el-input class="h-8 w-60 max-w-[60%] ml-3" v-model="waterMarkText" @change="canvasHandle('watermark')"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer id="x-spreadsheet" v-model="drawer" direction="btt" custom-class="sheet" style="">
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-drawer__body){
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.el-drawer__header){
|
||||
display: none !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
:deep(.el-drawer){
|
||||
height: 50% !important;
|
||||
}
|
||||
:deep(.el-upload-list){
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { ref,reactive } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
// import { copy } from '@/utils/string'
|
||||
const info = reactive({
|
||||
title: "抛硬币",
|
||||
})
|
||||
|
||||
const genStatus = ref(false)
|
||||
const isHeads = ref(true)
|
||||
|
||||
const throwCoin = () => {
|
||||
genStatus.value = true
|
||||
isHeads.value = Math.random() < 0.5;
|
||||
setTimeout(() => {
|
||||
genStatus.value = false;
|
||||
}, 2500); // 休眠5秒,5000毫秒
|
||||
}
|
||||
//copy
|
||||
// const copyRes = async (resStr: string) => {
|
||||
// copy(resStr)
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div style="flex-grow: 3; display: flex; justify-content: center;">
|
||||
<!-- coin -->
|
||||
<div class="rotate-wrap" :class="[genStatus == true ? (isHeads == true ? 'rotate-wrap-ani-z' : 'rotate-wrap-ani-f') : '']">
|
||||
<div class="front circle" :class="[isHeads == true ? 'is-head' : '']"></div>
|
||||
<div class="reverse circle" :class="[isHeads == false ? 'is-head' : '']"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<el-button type="primary" class="w-48" size="large" @click="throwCoin" v-if="!genStatus">抛硬币</el-button>
|
||||
<el-button type="primary" class="w-48" size="large" disabled v-else>抛硬币...</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.rotate-wrap {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 100px;
|
||||
transform-style: preserve-3d;
|
||||
position: relative;
|
||||
}
|
||||
.rotate-wrap-ani-z{
|
||||
/* animation: rotate 5s; */
|
||||
animation: rotateZ 2.5s linear infinite;
|
||||
}
|
||||
.rotate-wrap-ani-f{
|
||||
/* animation: rotate 5s; */
|
||||
animation: rotateF 2.5s linear infinite;
|
||||
}
|
||||
.rotate-wrap:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.rotate-wrap .front, .rotate-wrap .reverse{
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-size: cover;
|
||||
}
|
||||
.rotate-wrap .front{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-image: url('/public/images/coin/dollar.png');
|
||||
}
|
||||
.rotate-wrap .reverse{
|
||||
background-image: url('/public/images/coin/xingxing.png');
|
||||
}
|
||||
|
||||
.circle{
|
||||
border-radius: 50%;
|
||||
}
|
||||
.line{
|
||||
height: 200px;
|
||||
width: 1px;
|
||||
position:absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
background: red;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
.is-head{
|
||||
transform: translateZ(1px);
|
||||
}
|
||||
|
||||
@keyframes rotateZ {
|
||||
0%{
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
20%{
|
||||
transform: rotateX(720deg) scale(1.2);
|
||||
}
|
||||
50%{
|
||||
transform: rotateX(1080deg) scale(1.5);
|
||||
}
|
||||
80%{
|
||||
transform: rotateX(1440deg) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(2160deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotateF {
|
||||
0%{
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
20%{
|
||||
transform: rotateX(720deg) scale(1.2);
|
||||
}
|
||||
50%{
|
||||
transform: rotateX(1080deg) scale(1.5);
|
||||
}
|
||||
80%{
|
||||
transform: rotateX(1440deg) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(1980deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { ref,reactive } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import DiceCore from "@/components/Dice/DiceCore.vue"
|
||||
// import { copy } from '@/utils/string'
|
||||
const info = reactive({
|
||||
title: "投骰子",
|
||||
})
|
||||
//骰子数量
|
||||
const diceNum = ref(1)
|
||||
const genStatus = ref(false)
|
||||
const diceRef = ref()
|
||||
const resetDiceKey = ref(0)
|
||||
|
||||
const shake = async () => {
|
||||
genStatus.value = true
|
||||
|
||||
let res = await new Promise((resolve) => {
|
||||
for (let i = 0; i < diceNum.value; i++) {
|
||||
diceRef.value[i].throwDice()
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
//等待执行完成
|
||||
setTimeout(() => {
|
||||
console.log(res)
|
||||
genStatus.value = false
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
//重置骰子点数
|
||||
const resetDice = () => {
|
||||
resetDiceKey.value++
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<view class="grow-[1.5]"></view>
|
||||
<div class="flex justify-center grow-[3]">
|
||||
<div class="flex flex-wrap h-96 items-center">
|
||||
<!-- 骰子组件 -->
|
||||
<DiceCore ref="diceRef" v-for="i in diceNum" :key="resetDiceKey" :class="[diceNum == 1 ? 'dice_full' : '', diceNum == 3 && i == 1 ? 'dice_full' : 'dice_default', diceNum == 5 && i == 3 ? 'dice_full' : 'dice_default']"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="ml-5 flex justify-center">
|
||||
<el-text style="">骰子数量:</el-text>
|
||||
<div class="w-4/6">
|
||||
<el-slider class="w-72 ml-3" v-model="diceNum" :min="1" :max="6" :step="1" blockSize="18" :showValue="true"></el-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-5 flex justify-center">
|
||||
<el-button type="primary" class="w-48" size="large" @click="shake" v-if="!genStatus">投骰子</el-button>
|
||||
<el-button type="primary" class="w-48" size="large" disabled v-else>投骰子...</el-button>
|
||||
<el-button type="primary" class="w-48" size="large" @click="resetDice">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dice_default{
|
||||
flex: 0 0 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dice_full{
|
||||
flex: 0 0 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
|
||||
//定义emit
|
||||
const emit = defineEmits(['throwEnd'])
|
||||
//是否正在掷筛子
|
||||
const isDicing = ref(false)
|
||||
//当前显示的动画图片索引
|
||||
const aniIndex = ref(0)
|
||||
//掷筛子动画所用到的图片(4张骰子旋转过程中的图)
|
||||
const diceAnimationImages = ref([
|
||||
'/public/images/dice/dice_run1.png',
|
||||
'/public/images/dice/dice_run2.png',
|
||||
'/public/images/dice/dice_run3.png',
|
||||
'/public/images/dice/dice_run4.png'
|
||||
])
|
||||
//骰子每一个点的对应的图片
|
||||
const diceImages = ref({
|
||||
//1点的图片
|
||||
1: '/public/images/dice/dice1.png',
|
||||
//2点的图片
|
||||
2: '/public/images/dice/dice2.png',
|
||||
//3点的图片
|
||||
3: '/public/images/dice/dice3.png',
|
||||
//4点的图片
|
||||
4: '/public/images/dice/dice4.png',
|
||||
//5点的图片
|
||||
5: '/public/images/dice/dice5.png',
|
||||
//6点的图片
|
||||
6: '/public/images/dice/dice6.png'
|
||||
})
|
||||
//当前掷筛子掷到的点数(默认1点)
|
||||
const currentPoint = ref(1)
|
||||
//定时器
|
||||
const timer = ref(0)
|
||||
|
||||
const getDicePoint = async () => {
|
||||
let point = 1;
|
||||
//从1~6随机一个数
|
||||
point = Math.floor(Math.random() * 6 + 1);
|
||||
return point;
|
||||
}
|
||||
//掷骰子
|
||||
const throwDice = async () => {
|
||||
//如果当前骰子正在滚动则不能掷骰子
|
||||
if (isDicing.value) {
|
||||
return
|
||||
}
|
||||
//从接口获取点数
|
||||
currentPoint.value = await getDicePoint();
|
||||
//开启骰子动画
|
||||
await startAnimation();
|
||||
//动画完毕之后可以通知父组件当前掷到的点数
|
||||
emit('throwEnd', currentPoint.value);
|
||||
}
|
||||
//开启动画效果
|
||||
const startAnimation = async () => {
|
||||
return new Promise((resolve) => {
|
||||
//设置筛子开始运动
|
||||
isDicing.value = true;
|
||||
//记录动画次数
|
||||
let num = 0;
|
||||
//每隔100毫秒来回切换一张“动”图形成掷骰子的动画
|
||||
timer.value = setInterval(() => {
|
||||
let index = aniIndex.value;
|
||||
index++;
|
||||
if (index >= diceAnimationImages.value.length) {
|
||||
index = 0;
|
||||
}
|
||||
aniIndex.value = index;
|
||||
num++;
|
||||
//差不多执行1.2秒钟的时候可以停止了
|
||||
if (num > 12) {
|
||||
//关闭定时器
|
||||
clearInterval(timer.value);
|
||||
//设置骰子停止
|
||||
isDicing.value = false;
|
||||
//返回结果
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
//组件销毁之前清除定时器
|
||||
clearInterval(timer.value);
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
throwDice
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 骰子组件 -->
|
||||
<div class="dice-wrap" @tap="throwDice">
|
||||
<!-- 筛子运动时候的展示的图片 -->
|
||||
<el-image v-if="isDicing" :src="diceAnimationImages[aniIndex]" class="dice-icon" loading="lazy"></el-image>
|
||||
<!-- 筛子静止时候的显示的对应点数的图片 -->
|
||||
<el-image v-else :src="diceImages[currentPoint]" class="dice-icon" loading="lazy"></el-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dice-wrap {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.dice-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -15,9 +15,9 @@ const feedBackUrl = import.meta.env.VITE_FEEDBACK_URL
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<div class="bg-white rounded-md p-5">
|
||||
<br>
|
||||
<p>本项目是一个开源工具站, 求星星</p>
|
||||
<p>本项目基于 <el-link class="" href="https://github.com/taoran1401/tools-web" target="_blank" type="primary">tools-web</el-link> 构建</p>
|
||||
<br>
|
||||
<p>本项目开源地址:<el-link href="https://github.com/taoran1401/tools-web">https://github.com/taoran1401/tools-web</el-link></p>
|
||||
<p class=""><span class="bg-gray-200 p-1">ctrl + D</span>收藏本站,以便以后使用</p>
|
||||
<br>
|
||||
<p class="font-bold">联系作者/问题反馈</p>
|
||||
<br>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref,computed, watch } from 'vue'
|
||||
import { UploadProps,UploadRawFile,genFileId } from 'element-plus'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
|
||||
const info = reactive({
|
||||
title: "图片切割",
|
||||
})
|
||||
|
||||
const fileList = ref()
|
||||
const lineNum = ref(3)
|
||||
const image = ref({} as any)
|
||||
const cutImg = ref([] as string[])
|
||||
const dataFileRef = ref()
|
||||
|
||||
//上传
|
||||
const updateDataFile = async (params) => {
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(params.file);
|
||||
reader.addEventListener(
|
||||
'load',
|
||||
async () => {
|
||||
const imageTmp = new Image();
|
||||
imageTmp.onload = () => {
|
||||
image.value = imageTmp;
|
||||
cut();
|
||||
};
|
||||
imageTmp.src = reader.result as string;
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
//当超出限制时,执行的钩子函数
|
||||
//这里覆盖前一个文件
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
dataFileRef.value!.clearFiles()
|
||||
const file = files[0] as UploadRawFile
|
||||
file.uid = genFileId()
|
||||
dataFileRef.value!.handleStart(file)
|
||||
dataFileRef.value!.submit()
|
||||
}
|
||||
|
||||
//切割
|
||||
const cut = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const img = image.value;
|
||||
|
||||
const wpiece = Math.floor(img.naturalWidth / lineNum.value);
|
||||
const hpiece = Math.floor(img.naturalHeight / lineNum.value);
|
||||
const results = [] as string[];
|
||||
|
||||
canvas.width = wpiece;
|
||||
canvas.height = hpiece;
|
||||
|
||||
for (let r = 0; r < lineNum.value; r++) {
|
||||
for (let c = 0; c < lineNum.value; c++) {
|
||||
ctx?.drawImage(
|
||||
img,
|
||||
c * wpiece,
|
||||
r * hpiece,
|
||||
wpiece,
|
||||
hpiece,
|
||||
0,
|
||||
0,
|
||||
wpiece,
|
||||
hpiece
|
||||
);
|
||||
results.push(canvas.toDataURL());
|
||||
}
|
||||
}
|
||||
cutImg.value = results;
|
||||
}
|
||||
|
||||
//计算切割样式
|
||||
const cutImgStyle = computed(() => {
|
||||
const width = `width: ${image.value.width || 800}px;`;
|
||||
const grid = `grid: repeat(${lineNum.value}, 1fr) / repeat(${
|
||||
lineNum.value
|
||||
}, 1fr);`;
|
||||
return width + grid;
|
||||
})
|
||||
|
||||
watch(cutImgStyle, () => {
|
||||
if (fileList.value) cut();
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
class="dataFileRef flex"
|
||||
ref="dataFileRef"
|
||||
accept="image/*"
|
||||
:http-request="updateDataFile"
|
||||
:on-exceed="handleExceed"
|
||||
:limit="1"
|
||||
>
|
||||
<el-button type="primary">请上传需要切割的图片</el-button>
|
||||
</el-upload>
|
||||
<div class="flex mt-3">
|
||||
<div class="flex">
|
||||
<el-text>行数和列数</el-text>
|
||||
<el-input-number v-model="lineNum" :min="1" :max="10" class="ml-3"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-3 min-h-md bg-gray-100 p-3 mb-3 flex flex-col" v-if="image.src">
|
||||
<!-- 预览 -->
|
||||
<div class="mb-3">
|
||||
<el-text>预览: </el-text>
|
||||
<div :style="cutImgStyle" class="grid gap-1 max-w-full">
|
||||
<img v-for="(src,index) in cutImg" :key="index" :src="src" alt="结果" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原图 -->
|
||||
<div>
|
||||
<el-text>原图: </el-text>
|
||||
<div>
|
||||
<img :src="image.src" alt="原图" v-if="image.src"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty :image-size="200" description="无预览"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const info = reactive({
|
||||
title: "文本去重",
|
||||
content: '',
|
||||
tranRes: '',
|
||||
})
|
||||
|
||||
|
||||
//clear
|
||||
const clear = () => {
|
||||
info.content = ''
|
||||
}
|
||||
|
||||
|
||||
const removeDuplicate = computed(() => {
|
||||
let tmpArr = info.content.split('\n')
|
||||
let resultSet = new Set(tmpArr)
|
||||
let fromResArr = Array.from(resultSet)
|
||||
info.tranRes = fromResArr.join("\n")
|
||||
return info.tranRes
|
||||
})
|
||||
|
||||
//copy
|
||||
const copyRes = async () => {
|
||||
copy(info.tranRes)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="info.title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div>
|
||||
<el-input type="textarea" :rows="8" v-model="info.content"></el-input>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<el-button type="primary" @click="copyRes">复制结果</el-button>
|
||||
<el-button type="danger" @click="clear">清空内容</el-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 min-h-md bg-gray-100 p-3 mb-3">
|
||||
<el-input type="textarea" :rows="8" v-model="removeDuplicate"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user