feat: 新增工具
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
const appDesc = ref(import.meta.env.VITE_APP_DESC || '')
|
||||
const currentYear: number = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full rounded-2xl z-10 p-5 text-center">
|
||||
<el-text>{{ appDesc }} © 2024 <a href="https://github.com/naroat/tools-web" target="_blank" class="text-blue-700">Tools-Web</a></el-text>
|
||||
<el-text>{{ appDesc }} © {{ currentYear }} <a href="https://github.com/naroat/tools-web" target="_blank" class="text-blue-700">Tools-Web</a></el-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "AES加解密"
|
||||
|
||||
// 状态管理
|
||||
const activeTab = ref('encrypt')
|
||||
const inputText = ref('')
|
||||
const outputText = ref('')
|
||||
const key = ref('')
|
||||
const iv = ref('')
|
||||
const mode = ref('CBC')
|
||||
const keySize = ref(128)
|
||||
|
||||
// 加密模式选项
|
||||
const modeOptions = [
|
||||
{ label: 'CBC', value: 'CBC' },
|
||||
{ label: 'ECB', value: 'ECB' }
|
||||
]
|
||||
|
||||
// 密钥长度选项
|
||||
const keySizeOptions = [
|
||||
{ label: '128位', value: 128 },
|
||||
{ label: '192位', value: 192 },
|
||||
{ label: '256位', value: 256 }
|
||||
]
|
||||
|
||||
// 加密处理
|
||||
const encrypt = async () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要加密的文本')
|
||||
return
|
||||
}
|
||||
|
||||
if (!key.value) {
|
||||
ElMessage.warning('请输入密钥')
|
||||
return
|
||||
}
|
||||
|
||||
if (mode.value === 'CBC' && !iv.value) {
|
||||
ElMessage.warning('CBC模式需要输入初始化向量(IV)')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成密钥
|
||||
const cryptoKey = await generateKey(key.value, keySize.value)
|
||||
|
||||
// 加密
|
||||
const encrypted = await aesEncrypt(inputText.value, cryptoKey, iv.value, mode.value)
|
||||
outputText.value = btoa(encrypted)
|
||||
|
||||
ElMessage.success('加密成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('加密失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 解密处理
|
||||
const decrypt = async () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要解密的文本')
|
||||
return
|
||||
}
|
||||
|
||||
if (!key.value) {
|
||||
ElMessage.warning('请输入密钥')
|
||||
return
|
||||
}
|
||||
|
||||
if (mode.value === 'CBC' && !iv.value) {
|
||||
ElMessage.warning('CBC模式需要输入初始化向量(IV)')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成密钥
|
||||
const cryptoKey = await generateKey(key.value, keySize.value)
|
||||
|
||||
// 解密
|
||||
const decrypted = await aesDecrypt(atob(inputText.value), cryptoKey, iv.value, mode.value)
|
||||
outputText.value = decrypted
|
||||
|
||||
ElMessage.success('解密成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('解密失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成密钥
|
||||
const generateKey = async (keyStr: string, size: number) => {
|
||||
// 使用SHA-256哈希密钥,确保长度正确
|
||||
const encoder = new TextEncoder()
|
||||
const keyData = encoder.encode(keyStr)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', keyData)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
|
||||
// 根据密钥长度截取
|
||||
const keyBytes = size / 8
|
||||
const keyBuffer = new Uint8Array(hashArray.slice(0, keyBytes))
|
||||
|
||||
return await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyBuffer,
|
||||
{ name: 'AES-' + mode.value },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
}
|
||||
|
||||
// AES加密
|
||||
const aesEncrypt = async (text: string, key: CryptoKey, ivStr: string, mode: string) => {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(text)
|
||||
|
||||
let ivBuffer: Uint8Array
|
||||
if (mode === 'CBC') {
|
||||
// 使用SHA-256哈希IV,确保长度为16字节
|
||||
const ivData = encoder.encode(ivStr)
|
||||
const ivHashBuffer = await crypto.subtle.digest('SHA-256', ivData)
|
||||
ivBuffer = new Uint8Array(ivHashBuffer.slice(0, 16))
|
||||
}
|
||||
|
||||
const algorithm: AlgorithmIdentifier = mode === 'CBC'
|
||||
? { name: 'AES-CBC', iv: ivBuffer! } as AesCbcParams
|
||||
: { name: 'AES-ECB' }
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(algorithm, key, data)
|
||||
return String.fromCharCode(...new Uint8Array(encrypted))
|
||||
}
|
||||
|
||||
// AES解密
|
||||
const aesDecrypt = async (text: string, key: CryptoKey, ivStr: string, mode: string) => {
|
||||
const data = new Uint8Array(text.split('').map(char => char.charCodeAt(0)))
|
||||
|
||||
let ivBuffer: Uint8Array
|
||||
if (mode === 'CBC') {
|
||||
// 使用SHA-256哈希IV,确保长度为16字节
|
||||
const encoder = new TextEncoder()
|
||||
const ivData = encoder.encode(ivStr)
|
||||
const ivHashBuffer = await crypto.subtle.digest('SHA-256', ivData)
|
||||
ivBuffer = new Uint8Array(ivHashBuffer.slice(0, 16))
|
||||
}
|
||||
|
||||
const algorithm: AlgorithmIdentifier = mode === 'CBC'
|
||||
? { name: 'AES-CBC', iv: ivBuffer! } as AesCbcParams
|
||||
: { name: 'AES-ECB' }
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(algorithm, key, data)
|
||||
const decoder = new TextDecoder()
|
||||
return decoder.decode(decrypted)
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!outputText.value) {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(outputText.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
outputText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="加密" name="encrypt">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入要加密的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-input
|
||||
v-model="key"
|
||||
placeholder="请输入密钥"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-input
|
||||
v-model="iv"
|
||||
placeholder="请输入初始化向量(IV) (CBC模式必填)"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 items-center mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>加密模式:</el-text>
|
||||
<el-select v-model="mode" style="width: 120px;">
|
||||
<el-option
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>密钥长度:</el-text>
|
||||
<el-select v-model="keySize" style="width: 120px;">
|
||||
<el-option
|
||||
v-for="option in keySizeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="encrypt">加密</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
v-model="outputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="加密结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="解密" name="decrypt">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入要解密的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-input
|
||||
v-model="key"
|
||||
placeholder="请输入密钥"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-input
|
||||
v-model="iv"
|
||||
placeholder="请输入初始化向量(IV) (CBC模式必填)"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 items-center mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>加密模式:</el-text>
|
||||
<el-select v-model="mode" class="w-32">
|
||||
<el-option
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>密钥长度:</el-text>
|
||||
<el-select v-model="keySize" class="w-32">
|
||||
<el-option
|
||||
v-for="option in keySizeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="decrypt">解密</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
v-model="outputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="解密结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线AES加解密工具,支持CBC和ECB模式,支持128位、192位、256位密钥长度,可用于数据加密和解密。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
// 状态管理
|
||||
const title = "Base64加解密工具"
|
||||
const activeTab = ref('encode')
|
||||
const encodeInput = ref('')
|
||||
const encodeOutput = ref('')
|
||||
const decodeInput = ref('')
|
||||
const decodeOutput = ref('')
|
||||
|
||||
// 加密处理
|
||||
const encodeText = () => {
|
||||
if (!encodeInput.value) {
|
||||
ElMessage.warning('请输入要加密的文本')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
encodeOutput.value = btoa(unescape(encodeURIComponent(encodeInput.value)))
|
||||
ElMessage.success('加密成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('加密失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 解密处理
|
||||
const decodeText = () => {
|
||||
if (!decodeInput.value) {
|
||||
ElMessage.warning('请输入要解密的Base64字符串')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
decodeOutput.value = decodeURIComponent(escape(atob(decodeInput.value)))
|
||||
ElMessage.success('解密成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('解密失败:请输入有效的Base64字符串')
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const copyEncodeResult = () => {
|
||||
if (!encodeOutput.value) {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(encodeOutput.value)
|
||||
}
|
||||
|
||||
const copyDecodeResult = () => {
|
||||
if (!decodeOutput.value) {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(decodeOutput.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="加密" name="encode">
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="encodeInput"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要加密的文本"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="encodeText">加密</el-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-input
|
||||
v-model="encodeOutput"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="加密结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyEncodeResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="解密" name="decode">
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="decodeInput"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要解密的Base64字符串"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="decodeText">解密</el-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-input
|
||||
v-model="decodeOutput"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="解密结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyDecodeResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线Base64加解密工具,支持文本的Base64编码和解码,可用于URL编码、数据传输等场景。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "进制转换计算器"
|
||||
|
||||
// 状态管理
|
||||
const inputValue = ref('')
|
||||
const fromBase = ref(10)
|
||||
const toBase = ref(2)
|
||||
|
||||
// 进制选项
|
||||
const baseOptions = [
|
||||
{ label: '2 (二进制)', value: 2 },
|
||||
{ label: '8 (八进制)', value: 8 },
|
||||
{ label: '10 (十进制)', value: 10 },
|
||||
{ label: '16 (十六进制)', value: 16 },
|
||||
]
|
||||
|
||||
// 转换结果
|
||||
const convertedValue = computed(() => {
|
||||
if (!inputValue.value) return ''
|
||||
|
||||
try {
|
||||
// 先转换为十进制
|
||||
const decimalValue = parseInt(inputValue.value, fromBase.value)
|
||||
if (isNaN(decimalValue)) return '无效输入'
|
||||
|
||||
// 再转换为目标进制
|
||||
if (toBase.value === 10) {
|
||||
return decimalValue.toString()
|
||||
} else if (toBase.value === 16) {
|
||||
return decimalValue.toString(16).toUpperCase()
|
||||
} else {
|
||||
return decimalValue.toString(toBase.value)
|
||||
}
|
||||
} catch (error) {
|
||||
return '转换失败'
|
||||
}
|
||||
})
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!convertedValue.value || convertedValue.value === '无效输入' || convertedValue.value === '转换失败') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(convertedValue.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputValue.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
placeholder="请输入要转换的数值"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text class="">从进制:</el-text>
|
||||
<el-select v-model="fromBase" style="width: 140px;">
|
||||
<el-option
|
||||
v-for="option in baseOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>到进制:</el-text>
|
||||
<el-select v-model="toBase" style="width: 140px;">
|
||||
<el-option
|
||||
v-for="option in baseOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
:value="convertedValue"
|
||||
placeholder="转换结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线进制转换计算器,支持二进制、八进制、十进制、十六进制等多种进制之间的相互转换。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="calculator">
|
||||
<div class="calculator-display">
|
||||
<div class="display-text">{{ display }}</div>
|
||||
<div class="display-history">{{ history }}</div>
|
||||
</div>
|
||||
|
||||
<div class="calculator-buttons">
|
||||
<div class="button-row">
|
||||
<el-button type="info" @click="clear">C</el-button>
|
||||
<el-button type="info" @click="backspace"><</el-button>
|
||||
<el-button type="info" @click="append('%')">%</el-button>
|
||||
<el-button type="info" @click="append('/')">÷</el-button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<el-button @click="append('7')">7</el-button>
|
||||
<el-button @click="append('8')">8</el-button>
|
||||
<el-button @click="append('9')">9</el-button>
|
||||
<el-button type="info" @click="append('*')">×</el-button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<el-button @click="append('4')">4</el-button>
|
||||
<el-button @click="append('5')">5</el-button>
|
||||
<el-button @click="append('6')">6</el-button>
|
||||
<el-button type="info" @click="append('-')">-</el-button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<el-button @click="append('1')">1</el-button>
|
||||
<el-button @click="append('2')">2</el-button>
|
||||
<el-button @click="append('3')">3</el-button>
|
||||
<el-button type="info" @click="append('+')">+</el-button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<el-button @click="append('0')">0</el-button>
|
||||
<el-button @click="append('.')">.</el-button>
|
||||
<el-button type="success" @click="calculate">=</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线简易计算器,支持基本的加减乘除和取模运算,具有历史记录显示功能,可用于日常简单计算。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "简易计算器"
|
||||
|
||||
const display = ref('0');
|
||||
const history = ref('');
|
||||
const firstOperand = ref<number | null>(null);
|
||||
const operator = ref<string | null>(null);
|
||||
const waitingForSecondOperand = ref(false);
|
||||
|
||||
// 清空
|
||||
const clear = () => {
|
||||
display.value = '0';
|
||||
history.value = '';
|
||||
firstOperand.value = null;
|
||||
operator.value = null;
|
||||
waitingForSecondOperand.value = false;
|
||||
};
|
||||
|
||||
// 退格
|
||||
const backspace = () => {
|
||||
if (waitingForSecondOperand.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (display.value.length > 1) {
|
||||
display.value = display.value.slice(0, -1);
|
||||
} else {
|
||||
display.value = '0';
|
||||
}
|
||||
};
|
||||
|
||||
// 添加数字或操作符
|
||||
const append = (value: string) => {
|
||||
if (['+', '-', '*', '/', '%'].includes(value)) {
|
||||
setOperator(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingForSecondOperand.value) {
|
||||
display.value = value;
|
||||
waitingForSecondOperand.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === '.') {
|
||||
if (!display.value.includes('.')) {
|
||||
display.value += value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (display.value === '0' && value !== '.') {
|
||||
display.value = value;
|
||||
} else {
|
||||
display.value += value;
|
||||
}
|
||||
};
|
||||
|
||||
// 计算
|
||||
const calculate = () => {
|
||||
if (operator.value === null || firstOperand.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondOperand = parseFloat(display.value);
|
||||
let result: number;
|
||||
|
||||
switch (operator.value) {
|
||||
case '+':
|
||||
result = firstOperand.value + secondOperand;
|
||||
break;
|
||||
case '-':
|
||||
result = firstOperand.value - secondOperand;
|
||||
break;
|
||||
case '*':
|
||||
result = firstOperand.value * secondOperand;
|
||||
break;
|
||||
case '/':
|
||||
if (secondOperand === 0) {
|
||||
display.value = 'Error';
|
||||
history.value = '';
|
||||
firstOperand.value = null;
|
||||
operator.value = null;
|
||||
waitingForSecondOperand.value = false;
|
||||
return;
|
||||
}
|
||||
result = firstOperand.value / secondOperand;
|
||||
break;
|
||||
case '%':
|
||||
result = firstOperand.value % secondOperand;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理结果显示
|
||||
if (Number.isInteger(result)) {
|
||||
display.value = result.toString();
|
||||
} else {
|
||||
display.value = result.toFixed(2);
|
||||
}
|
||||
|
||||
history.value = `${firstOperand.value} ${operator.value} ${secondOperand} =`;
|
||||
firstOperand.value = result;
|
||||
operator.value = null;
|
||||
waitingForSecondOperand.value = true;
|
||||
};
|
||||
|
||||
// 监听操作符点击
|
||||
const setOperator = (op: string) => {
|
||||
if (operator.value !== null) {
|
||||
calculate();
|
||||
}
|
||||
|
||||
firstOperand.value = parseFloat(display.value);
|
||||
operator.value = op;
|
||||
waitingForSecondOperand.value = true;
|
||||
history.value = `${firstOperand.value} ${op}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.calculator {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.calculator-display {
|
||||
background-color: #f5f7fa;
|
||||
padding: 20px;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.display-text {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.display-history {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.calculator-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button-row .el-button {
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.button-row .el-button:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.button-row:last-child .el-button {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.button-row .el-button:nth-child(1) {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.button-row .el-button.success {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.calculator {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.button-row .el-button {
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="input-section">
|
||||
<el-tabs v-model="activeCategory">
|
||||
<el-tab-pane label="基础颜色" name="basic">
|
||||
<div class="color-grid">
|
||||
<div
|
||||
v-for="color in basicColors"
|
||||
:key="color.hex"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
@click="copyColor(color)"
|
||||
>
|
||||
<div class="color-info">
|
||||
<span class="color-name">{{ color.name }}</span>
|
||||
<span class="color-hex">{{ color.hex }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="渐变色" name="gradient">
|
||||
<div class="gradient-grid">
|
||||
<div
|
||||
v-for="(gradient, index) in gradients"
|
||||
:key="index"
|
||||
class="gradient-item"
|
||||
:style="{ background: gradient.value }"
|
||||
@click="copyGradient(gradient)"
|
||||
>
|
||||
<div class="gradient-info">
|
||||
<span class="gradient-name">{{ gradient.name }}</span>
|
||||
<span class="gradient-value">{{ gradient.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Material Design" name="material">
|
||||
<div class="color-grid">
|
||||
<div
|
||||
v-for="color in materialColors"
|
||||
:key="color.hex"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
@click="copyColor(color)"
|
||||
>
|
||||
<div class="color-info">
|
||||
<span class="color-name">{{ color.name }}</span>
|
||||
<span class="color-hex">{{ color.hex }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="扁平化" name="flat">
|
||||
<div class="color-grid">
|
||||
<div
|
||||
v-for="color in flatColors"
|
||||
:key="color.hex"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
@click="copyColor(color)"
|
||||
>
|
||||
<div class="color-info">
|
||||
<span class="color-name">{{ color.name }}</span>
|
||||
<span class="color-hex">{{ color.hex }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线色板工具,提供基础颜色、渐变色、Material Design 颜色和扁平化颜色,点击颜色可复制颜色值,方便设计和开发使用。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "色板工具"
|
||||
|
||||
const activeCategory = ref('basic');
|
||||
|
||||
// 基础颜色
|
||||
const basicColors = [
|
||||
{ name: '红色', hex: '#FF0000' },
|
||||
{ name: '绿色', hex: '#00FF00' },
|
||||
{ name: '蓝色', hex: '#0000FF' },
|
||||
{ name: '黄色', hex: '#FFFF00' },
|
||||
{ name: '紫色', hex: '#800080' },
|
||||
{ name: '橙色', hex: '#FFA500' },
|
||||
{ name: '黑色', hex: '#000000' },
|
||||
{ name: '白色', hex: '#FFFFFF' },
|
||||
{ name: '灰色', hex: '#808080' },
|
||||
{ name: '棕色', hex: '#A52A2A' },
|
||||
{ name: '粉色', hex: '#FFC0CB' },
|
||||
{ name: '青色', hex: '#00FFFF' },
|
||||
{ name: '品红', hex: '#FF00FF' },
|
||||
{ name: '橄榄绿', hex: '#808000' },
|
||||
{ name: '海军蓝', hex: '#000080' },
|
||||
{ name: '银色', hex: '#C0C0C0' },
|
||||
];
|
||||
|
||||
// 渐变色
|
||||
const gradients = [
|
||||
{ name: '蓝到紫', value: 'linear-gradient(90deg, #3B82F6, #8B5CF6)' },
|
||||
{ name: '红到黄', value: 'linear-gradient(90deg, #EF4444, #F59E0B)' },
|
||||
{ name: '绿到蓝', value: 'linear-gradient(90deg, #10B981, #3B82F6)' },
|
||||
{ name: '紫到粉', value: 'linear-gradient(90deg, #8B5CF6, #EC4899)' },
|
||||
{ name: '黄到绿', value: 'linear-gradient(90deg, #F59E0B, #10B981)' },
|
||||
{ name: '红到橙', value: 'linear-gradient(90deg, #EF4444, #F97316)' },
|
||||
{ name: '蓝到青', value: 'linear-gradient(90deg, #3B82F6, #06B6D4)' },
|
||||
{ name: '紫到红', value: 'linear-gradient(90deg, #8B5CF6, #EF4444)' },
|
||||
];
|
||||
|
||||
// Material Design颜色
|
||||
const materialColors = [
|
||||
{ name: 'Material Red', hex: '#F44336' },
|
||||
{ name: 'Material Pink', hex: '#E91E63' },
|
||||
{ name: 'Material Purple', hex: '#9C27B0' },
|
||||
{ name: 'Material Deep Purple', hex: '#673AB7' },
|
||||
{ name: 'Material Indigo', hex: '#3F51B5' },
|
||||
{ name: 'Material Blue', hex: '#2196F3' },
|
||||
{ name: 'Material Light Blue', hex: '#03A9F4' },
|
||||
{ name: 'Material Cyan', hex: '#00BCD4' },
|
||||
{ name: 'Material Teal', hex: '#009688' },
|
||||
{ name: 'Material Green', hex: '#4CAF50' },
|
||||
{ name: 'Material Light Green', hex: '#8BC34A' },
|
||||
{ name: 'Material Lime', hex: '#CDDC39' },
|
||||
{ name: 'Material Yellow', hex: '#FFEB3B' },
|
||||
{ name: 'Material Amber', hex: '#FFC107' },
|
||||
{ name: 'Material Orange', hex: '#FF9800' },
|
||||
{ name: 'Material Deep Orange', hex: '#FF5722' },
|
||||
];
|
||||
|
||||
// 扁平化颜色
|
||||
const flatColors = [
|
||||
{ name: 'Flat Turquoise', hex: '#1ABC9C' },
|
||||
{ name: 'Flat Green Sea', hex: '#16A085' },
|
||||
{ name: 'Flat Peter River', hex: '#3498DB' },
|
||||
{ name: 'Flat Belize Hole', hex: '#2980B9' },
|
||||
{ name: 'Flat Amethyst', hex: '#9B59B6' },
|
||||
{ name: 'Flat Wisteria', hex: '#8E44AD' },
|
||||
{ name: 'Flat Wet Asphalt', hex: '#34495E' },
|
||||
{ name: 'Flat Midnight Blue', hex: '#2C3E50' },
|
||||
{ name: 'Flat Sun Flower', hex: '#F1C40F' },
|
||||
{ name: 'Flat Orange', hex: '#F39C12' },
|
||||
{ name: 'Flat Carrot', hex: '#E67E22' },
|
||||
{ name: 'Flat Alizarin', hex: '#E74C3C' },
|
||||
{ name: 'Flat Clouds', hex: '#ECF0F1' },
|
||||
{ name: 'Flat Silver', hex: '#BDC3C7' },
|
||||
{ name: 'Flat Concrete', hex: '#95A5A6' },
|
||||
{ name: 'Flat Asbestos', hex: '#7F8C8D' },
|
||||
];
|
||||
|
||||
// 复制颜色
|
||||
const copyColor = (color: { name: string; hex: string }) => {
|
||||
navigator.clipboard.writeText(color.hex).then(() => {
|
||||
ElMessage.success(`已复制 ${color.name} 的颜色值: ${color.hex}`);
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败');
|
||||
});
|
||||
};
|
||||
|
||||
// 复制渐变色
|
||||
const copyGradient = (gradient: { name: string; value: string }) => {
|
||||
navigator.clipboard.writeText(gradient.value).then(() => {
|
||||
ElMessage.success(`已复制 ${gradient.name} 的渐变值: ${gradient.value}`);
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.color-item:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.color-hex {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.gradient-item {
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-item:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.gradient-info {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.gradient-name {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.gradient-value {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,738 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "Emoji表情大全"
|
||||
|
||||
// 状态管理
|
||||
const searchKeyword = ref('')
|
||||
const selectedCategory = ref('all')
|
||||
|
||||
// Emoji分类
|
||||
const emojiCategories = [
|
||||
{ id: 'all', name: '全部' },
|
||||
{ id: 'smileys', name: '表情符号与情感' },
|
||||
{ id: 'people', name: '人物与身体' },
|
||||
{ id: 'animals', name: '动物与自然' },
|
||||
{ id: 'food', name: '食物与饮料' },
|
||||
{ id: 'travel', name: '旅行与地点' },
|
||||
{ id: 'activities', name: '活动' },
|
||||
{ id: 'objects', name: '物体' },
|
||||
{ id: 'symbols', name: '符号' },
|
||||
{ id: 'flags', name: '旗帜' }
|
||||
]
|
||||
|
||||
// Emoji数据
|
||||
const emojis = [
|
||||
// 表情符号与情感
|
||||
{ emoji: '😀', name: '笑脸', category: 'smileys' },
|
||||
{ emoji: '😂', name: '笑哭', category: 'smileys' },
|
||||
{ emoji: '😍', name: '爱心眼', category: 'smileys' },
|
||||
{ emoji: '😎', name: '墨镜', category: 'smileys' },
|
||||
{ emoji: '🤔', name: '思考', category: 'smileys' },
|
||||
{ emoji: '😢', name: '哭泣', category: 'smileys' },
|
||||
{ emoji: '😡', name: '愤怒', category: 'smileys' },
|
||||
{ emoji: '😱', name: '惊讶', category: 'smileys' },
|
||||
{ emoji: '😴', name: '睡觉', category: 'smileys' },
|
||||
{ emoji: '😵', name: '晕', category: 'smileys' },
|
||||
{ emoji: '🤢', name: '恶心', category: 'smileys' },
|
||||
{ emoji: '🤠', name: '牛仔', category: 'smileys' },
|
||||
{ emoji: '😈', name: '恶魔', category: 'smileys' },
|
||||
{ emoji: '👼', name: '天使', category: 'smileys' },
|
||||
{ emoji: '👻', name: '幽灵', category: 'smileys' },
|
||||
{ emoji: '💩', name: '便便', category: 'smileys' },
|
||||
{ emoji: '😊', name: '微笑', category: 'smileys' },
|
||||
{ emoji: '😃', name: '大笑', category: 'smileys' },
|
||||
{ emoji: '😄', name: '露齿笑', category: 'smileys' },
|
||||
{ emoji: '😁', name: '开心', category: 'smileys' },
|
||||
{ emoji: '😆', name: '眯眼笑', category: 'smileys' },
|
||||
{ emoji: '😅', name: '汗笑', category: 'smileys' },
|
||||
{ emoji: '🤣', name: '大笑', category: 'smileys' },
|
||||
{ emoji: '😇', name: '天使笑', category: 'smileys' },
|
||||
{ emoji: '🥰', name: '可爱', category: 'smileys' },
|
||||
{ emoji: '😘', name: '飞吻', category: 'smileys' },
|
||||
{ emoji: '😗', name: '亲吻', category: 'smileys' },
|
||||
{ emoji: '😙', name: '亲亲', category: 'smileys' },
|
||||
{ emoji: '😚', name: '闭眼亲吻', category: 'smileys' },
|
||||
{ emoji: '😋', name: '美味', category: 'smileys' },
|
||||
{ emoji: '😛', name: '调皮', category: 'smileys' },
|
||||
{ emoji: '😝', name: '吐舌', category: 'smileys' },
|
||||
{ emoji: '😜', name: '眨眼吐舌', category: 'smileys' },
|
||||
{ emoji: '🤪', name: '疯狂', category: 'smileys' },
|
||||
{ emoji: '😏', name: '得意', category: 'smileys' },
|
||||
{ emoji: '😒', name: '无奈', category: 'smileys' },
|
||||
{ emoji: '🙄', name: '白眼', category: 'smileys' },
|
||||
{ emoji: '😬', name: '尴尬', category: 'smileys' },
|
||||
{ emoji: '😌', name: '放松', category: 'smileys' },
|
||||
{ emoji: '😔', name: '难过', category: 'smileys' },
|
||||
{ emoji: '😪', name: '疲惫', category: 'smileys' },
|
||||
{ emoji: '🤤', name: '流口水', category: 'smileys' },
|
||||
{ emoji: '😷', name: '口罩', category: 'smileys' },
|
||||
{ emoji: '🤒', name: '发烧', category: 'smileys' },
|
||||
{ emoji: '🤕', name: '受伤', category: 'smileys' },
|
||||
{ emoji: '🤢', name: '呕吐', category: 'smileys' },
|
||||
{ emoji: '🤮', name: '恶心', category: 'smileys' },
|
||||
{ emoji: '🤧', name: '打喷嚏', category: 'smileys' },
|
||||
{ emoji: '🥵', name: '热', category: 'smileys' },
|
||||
{ emoji: '🥶', name: '冷', category: 'smileys' },
|
||||
{ emoji: '🥴', name: '醉', category: 'smileys' },
|
||||
{ emoji: '😵💫', name: '头晕', category: 'smileys' },
|
||||
{ emoji: '🤯', name: '震惊', category: 'smileys' },
|
||||
{ emoji: '🤠', name: '牛仔', category: 'smileys' },
|
||||
{ emoji: '🥳', name: '庆祝', category: 'smileys' },
|
||||
{ emoji: '😎', name: '酷', category: 'smileys' },
|
||||
{ emoji: '🤓', name: '书呆子', category: 'smileys' },
|
||||
{ emoji: '😕', name: '困惑', category: 'smileys' },
|
||||
{ emoji: '😟', name: '担忧', category: 'smileys' },
|
||||
{ emoji: '🙁', name: '不高兴', category: 'smileys' },
|
||||
{ emoji: '😮', name: '惊讶', category: 'smileys' },
|
||||
{ emoji: '😯', name: '震惊', category: 'smileys' },
|
||||
{ emoji: '😲', name: '惊讶', category: 'smileys' },
|
||||
{ emoji: '😳', name: '害羞', category: 'smileys' },
|
||||
{ emoji: '🥺', name: '恳求', category: 'smileys' },
|
||||
{ emoji: '😦', name: '震惊', category: 'smileys' },
|
||||
{ emoji: '😧', name: '担心', category: 'smileys' },
|
||||
{ emoji: '😨', name: '害怕', category: 'smileys' },
|
||||
{ emoji: '😰', name: '恐慌', category: 'smileys' },
|
||||
{ emoji: '😥', name: '难过', category: 'smileys' },
|
||||
{ emoji: '😢', name: '哭泣', category: 'smileys' },
|
||||
{ emoji: '😭', name: '大哭', category: 'smileys' },
|
||||
{ emoji: '😱', name: '恐惧', category: 'smileys' },
|
||||
{ emoji: '😖', name: '痛苦', category: 'smileys' },
|
||||
{ emoji: '😣', name: '压力', category: 'smileys' },
|
||||
{ emoji: '😞', name: '失望', category: 'smileys' },
|
||||
{ emoji: '😓', name: '汗', category: 'smileys' },
|
||||
{ emoji: '😩', name: '疲惫', category: 'smileys' },
|
||||
{ emoji: '😫', name: '无奈', category: 'smileys' },
|
||||
{ emoji: '🥱', name: '打哈欠', category: 'smileys' },
|
||||
{ emoji: '😤', name: '愤怒', category: 'smileys' },
|
||||
{ emoji: '😡', name: '生气', category: 'smileys' },
|
||||
{ emoji: '😠', name: '愤怒', category: 'smileys' },
|
||||
{ emoji: '🤬', name: '暴怒', category: 'smileys' },
|
||||
{ emoji: '😈', name: '恶魔', category: 'smileys' },
|
||||
{ emoji: '👿', name: '魔鬼', category: 'smileys' },
|
||||
{ emoji: '💀', name: '骷髅', category: 'smileys' },
|
||||
{ emoji: '💩', name: '便便', category: 'smileys' },
|
||||
{ emoji: '🤡', name: '小丑', category: 'smileys' },
|
||||
{ emoji: '👹', name: '妖怪', category: 'smileys' },
|
||||
{ emoji: '👺', name: '小鬼', category: 'smileys' },
|
||||
{ emoji: '👻', name: '幽灵', category: 'smileys' },
|
||||
{ emoji: '💋', name: '吻', category: 'smileys' },
|
||||
{ emoji: '💌', name: '情书', category: 'smileys' },
|
||||
{ emoji: '💘', name: '爱心箭', category: 'smileys' },
|
||||
{ emoji: '💝', name: '礼物', category: 'smileys' },
|
||||
{ emoji: '💖', name: '爱心', category: 'smileys' },
|
||||
{ emoji: '💗', name: '爱心', category: 'smileys' },
|
||||
{ emoji: '💓', name: '心跳', category: 'smileys' },
|
||||
{ emoji: '💞', name: '双心', category: 'smileys' },
|
||||
{ emoji: '💕', name: '双心', category: 'smileys' },
|
||||
{ emoji: '💟', name: '心形', category: 'smileys' },
|
||||
{ emoji: '💔', name: '破碎的心', category: 'smileys' },
|
||||
{ emoji: '❤️', name: '红心', category: 'smileys' },
|
||||
{ emoji: '🧡', name: '橙心', category: 'smileys' },
|
||||
{ emoji: '💛', name: '黄心', category: 'smileys' },
|
||||
{ emoji: '💚', name: '绿心', category: 'smileys' },
|
||||
{ emoji: '💙', name: '蓝心', category: 'smileys' },
|
||||
{ emoji: '💜', name: '紫心', category: 'smileys' },
|
||||
{ emoji: '🖤', name: '黑心', category: 'smileys' },
|
||||
|
||||
// 人物与身体
|
||||
{ emoji: '👍', name: '点赞', category: 'people' },
|
||||
{ emoji: '👎', name: '踩', category: 'people' },
|
||||
{ emoji: '👋', name: '挥手', category: 'people' },
|
||||
{ emoji: '🤝', name: '握手', category: 'people' },
|
||||
{ emoji: '🙏', name: '祈祷', category: 'people' },
|
||||
{ emoji: '👌', name: 'OK', category: 'people' },
|
||||
{ emoji: '✌️', name: '胜利', category: 'people' },
|
||||
{ emoji: '🤘', name: '摇滚', category: 'people' },
|
||||
{ emoji: '🤙', name: '打电话', category: 'people' },
|
||||
{ emoji: '👊', name: '拳头', category: 'people' },
|
||||
{ emoji: '✊', name: '举起拳头', category: 'people' },
|
||||
{ emoji: '👆', name: '向上指', category: 'people' },
|
||||
{ emoji: '👇', name: '向下指', category: 'people' },
|
||||
{ emoji: '👈', name: '向左指', category: 'people' },
|
||||
{ emoji: '👉', name: '向右指', category: 'people' },
|
||||
|
||||
// 动物与自然
|
||||
{ emoji: '🐶', name: '狗', category: 'animals' },
|
||||
{ emoji: '🐱', name: '猫', category: 'animals' },
|
||||
{ emoji: '🐭', name: '老鼠', category: 'animals' },
|
||||
{ emoji: '🐹', name: '仓鼠', category: 'animals' },
|
||||
{ emoji: '🐰', name: '兔子', category: 'animals' },
|
||||
{ emoji: '🦊', name: '狐狸', category: 'animals' },
|
||||
{ emoji: '🐻', name: '熊', category: 'animals' },
|
||||
{ emoji: '🐼', name: '熊猫', category: 'animals' },
|
||||
{ emoji: '🐨', name: '考拉', category: 'animals' },
|
||||
{ emoji: '🐯', name: '老虎', category: 'animals' },
|
||||
{ emoji: '🦁', name: '狮子', category: 'animals' },
|
||||
{ emoji: '🐮', name: '牛', category: 'animals' },
|
||||
{ emoji: '🐷', name: '猪', category: 'animals' },
|
||||
{ emoji: '🐸', name: '青蛙', category: 'animals' },
|
||||
{ emoji: '🐵', name: '猴子', category: 'animals' },
|
||||
{ emoji: '🐔', name: '鸡', category: 'animals' },
|
||||
{ emoji: '🐧', name: '企鹅', category: 'animals' },
|
||||
{ emoji: '🐦', name: '鸟', category: 'animals' },
|
||||
{ emoji: '🐤', name: '小鸡', category: 'animals' },
|
||||
{ emoji: '🐣', name: '破壳小鸡', category: 'animals' },
|
||||
{ emoji: '🐥', name: '黄小鸡', category: 'animals' },
|
||||
{ emoji: '🦆', name: '鸭子', category: 'animals' },
|
||||
{ emoji: '🦅', name: '鹰', category: 'animals' },
|
||||
{ emoji: '🦉', name: '猫头鹰', category: 'animals' },
|
||||
{ emoji: '🦇', name: '蝙蝠', category: 'animals' },
|
||||
{ emoji: '🐺', name: '狼', category: 'animals' },
|
||||
{ emoji: '🐗', name: '野猪', category: 'animals' },
|
||||
{ emoji: '🐴', name: '马', category: 'animals' },
|
||||
{ emoji: '🦄', name: '独角兽', category: 'animals' },
|
||||
{ emoji: '🐝', name: '蜜蜂', category: 'animals' },
|
||||
{ emoji: '🐛', name: '毛毛虫', category: 'animals' },
|
||||
{ emoji: '🦋', name: '蝴蝶', category: 'animals' },
|
||||
{ emoji: '🐌', name: '蜗牛', category: 'animals' },
|
||||
{ emoji: '🐞', name: '瓢虫', category: 'animals' },
|
||||
{ emoji: '🐜', name: '蚂蚁', category: 'animals' },
|
||||
{ emoji: '🐢', name: '乌龟', category: 'animals' },
|
||||
{ emoji: '🐍', name: '蛇', category: 'animals' },
|
||||
{ emoji: '🦎', name: '蜥蜴', category: 'animals' },
|
||||
{ emoji: '🦖', name: '霸王龙', category: 'animals' },
|
||||
{ emoji: '🦕', name: '雷龙', category: 'animals' },
|
||||
{ emoji: '🐙', name: '章鱼', category: 'animals' },
|
||||
{ emoji: '🐚', name: '贝壳', category: 'animals' },
|
||||
{ emoji: '🐌', name: '蜗牛', category: 'animals' },
|
||||
{ emoji: '🌱', name: '发芽', category: 'animals' },
|
||||
{ emoji: '🌿', name: '草', category: 'animals' },
|
||||
{ emoji: '🍃', name: '叶子', category: 'animals' },
|
||||
{ emoji: '🍂', name: '落叶', category: 'animals' },
|
||||
{ emoji: '🍁', name: '枫叶', category: 'animals' },
|
||||
{ emoji: '🌾', name: '稻穗', category: 'animals' },
|
||||
{ emoji: '🌴', name: '棕榈树', category: 'animals' },
|
||||
{ emoji: '🌵', name: '仙人掌', category: 'animals' },
|
||||
{ emoji: '🌷', name: '郁金香', category: 'animals' },
|
||||
{ emoji: '🌺', name: '花朵', category: 'animals' },
|
||||
{ emoji: '🌸', name: '樱花', category: 'animals' },
|
||||
{ emoji: '🌹', name: '玫瑰', category: 'animals' },
|
||||
{ emoji: '🌻', name: '向日葵', category: 'animals' },
|
||||
{ emoji: '🌼', name: '雏菊', category: 'animals' },
|
||||
|
||||
// 食物与饮料
|
||||
{ emoji: '🍎', name: '苹果', category: 'food' },
|
||||
{ emoji: '🍏', name: '青苹果', category: 'food' },
|
||||
{ emoji: '🍐', name: '梨', category: 'food' },
|
||||
{ emoji: '🍊', name: '橘子', category: 'food' },
|
||||
{ emoji: '🍋', name: '柠檬', category: 'food' },
|
||||
{ emoji: '🍌', name: '香蕉', category: 'food' },
|
||||
{ emoji: '🍉', name: '西瓜', category: 'food' },
|
||||
{ emoji: '🍇', name: '葡萄', category: 'food' },
|
||||
{ emoji: '🍓', name: '草莓', category: 'food' },
|
||||
{ emoji: '🍈', name: '瓜', category: 'food' },
|
||||
{ emoji: '🍒', name: '樱桃', category: 'food' },
|
||||
{ emoji: '🍑', name: '桃子', category: 'food' },
|
||||
{ emoji: '🍍', name: '菠萝', category: 'food' },
|
||||
{ emoji: '🥥', name: '椰子', category: 'food' },
|
||||
{ emoji: '🥝', name: '猕猴桃', category: 'food' },
|
||||
{ emoji: '🍅', name: '番茄', category: 'food' },
|
||||
{ emoji: '🥑', name: '牛油果', category: 'food' },
|
||||
{ emoji: '🥦', name: '西兰花', category: 'food' },
|
||||
{ emoji: '🥬', name: '生菜', category: 'food' },
|
||||
{ emoji: '🥒', name: '黄瓜', category: 'food' },
|
||||
{ emoji: '🥜', name: '花生', category: 'food' },
|
||||
{ emoji: '🌰', name: '栗子', category: 'food' },
|
||||
{ emoji: '🍞', name: '面包', category: 'food' },
|
||||
{ emoji: '🥐', name: '羊角面包', category: 'food' },
|
||||
{ emoji: '🥖', name: '法棍', category: 'food' },
|
||||
{ emoji: '🥨', name: '椒盐卷饼', category: 'food' },
|
||||
{ emoji: '🥯', name: '百吉饼', category: 'food' },
|
||||
{ emoji: '🧀', name: '奶酪', category: 'food' },
|
||||
{ emoji: '🍖', name: '肉', category: 'food' },
|
||||
{ emoji: '🍗', name: '鸡肉', category: 'food' },
|
||||
{ emoji: '🍔', name: '汉堡', category: 'food' },
|
||||
{ emoji: '🍟', name: '薯条', category: 'food' },
|
||||
{ emoji: '🍕', name: '披萨', category: 'food' },
|
||||
{ emoji: '🌭', name: '热狗', category: 'food' },
|
||||
{ emoji: '🥪', name: '三明治', category: 'food' },
|
||||
{ emoji: '🌮', name: '墨西哥卷', category: 'food' },
|
||||
{ emoji: '🌯', name: '墨西哥饼', category: 'food' },
|
||||
{ emoji: '🥙', name: '皮塔饼', category: 'food' },
|
||||
{ emoji: '🍱', name: '便当', category: 'food' },
|
||||
{ emoji: '🍘', name: '米饼', category: 'food' },
|
||||
{ emoji: '🍙', name: '饭团', category: 'food' },
|
||||
{ emoji: '🍚', name: '米饭', category: 'food' },
|
||||
{ emoji: '🍛', name: '咖喱饭', category: 'food' },
|
||||
{ emoji: '🍜', name: '拉面', category: 'food' },
|
||||
{ emoji: '🍝', name: '意大利面', category: 'food' },
|
||||
{ emoji: '🍠', name: '红薯', category: 'food' },
|
||||
{ emoji: '🍢', name: '烤串', category: 'food' },
|
||||
{ emoji: '🍣', name: '寿司', category: 'food' },
|
||||
{ emoji: '🍤', name: '虾', category: 'food' },
|
||||
{ emoji: '🍥', name: '鱼板', category: 'food' },
|
||||
{ emoji: '🥮', name: '月饼', category: 'food' },
|
||||
{ emoji: '🍡', name: '丸子', category: 'food' },
|
||||
{ emoji: '🍧', name: '刨冰', category: 'food' },
|
||||
{ emoji: '🍨', name: '冰淇淋', category: 'food' },
|
||||
{ emoji: '🍩', name: '甜甜圈', category: 'food' },
|
||||
{ emoji: '🍪', name: '饼干', category: 'food' },
|
||||
{ emoji: '🎂', name: '蛋糕', category: 'food' },
|
||||
{ emoji: '🍰', name: '甜点', category: 'food' },
|
||||
{ emoji: '🧁', name: '纸杯蛋糕', category: 'food' },
|
||||
{ emoji: '🍫', name: '巧克力', category: 'food' },
|
||||
{ emoji: '🍬', name: '糖果', category: 'food' },
|
||||
{ emoji: '🍭', name: '棒棒糖', category: 'food' },
|
||||
{ emoji: '🍮', name: '布丁', category: 'food' },
|
||||
{ emoji: '🍯', name: '蜂蜜', category: 'food' },
|
||||
{ emoji: '🥛', name: '牛奶', category: 'food' },
|
||||
{ emoji: '🍼', name: '奶瓶', category: 'food' },
|
||||
{ emoji: '☕', name: '咖啡', category: 'food' },
|
||||
{ emoji: '🍵', name: '茶', category: 'food' },
|
||||
{ emoji: '🍶', name: '清酒', category: 'food' },
|
||||
{ emoji: '🍾', name: '香槟', category: 'food' },
|
||||
{ emoji: '🍷', name: '红酒', category: 'food' },
|
||||
{ emoji: '🍸', name: '鸡尾酒', category: 'food' },
|
||||
{ emoji: '🍹', name: '果汁', category: 'food' },
|
||||
{ emoji: '🍺', name: '啤酒', category: 'food' },
|
||||
{ emoji: '🍻', name: '干杯', category: 'food' },
|
||||
{ emoji: '🥂', name: '碰杯', category: 'food' },
|
||||
|
||||
// 旅行与地点
|
||||
{ emoji: '🌍', name: '地球', category: 'travel' },
|
||||
{ emoji: '🌎', name: '西半球', category: 'travel' },
|
||||
{ emoji: '🌏', name: '东半球', category: 'travel' },
|
||||
{ emoji: '🌐', name: '全球', category: 'travel' },
|
||||
{ emoji: '🗺️', name: '地图', category: 'travel' },
|
||||
{ emoji: '🗾', name: '日本地图', category: 'travel' },
|
||||
{ emoji: '🧭', name: '指南针', category: 'travel' },
|
||||
{ emoji: '🏔️', name: '雪山', category: 'travel' },
|
||||
{ emoji: '🏕️', name: '露营', category: 'travel' },
|
||||
{ emoji: '🏖️', name: '沙滩', category: 'travel' },
|
||||
{ emoji: '🏜️', name: '沙漠', category: 'travel' },
|
||||
{ emoji: '🏝️', name: '荒岛', category: 'travel' },
|
||||
{ emoji: '🏞️', name: '风景', category: 'travel' },
|
||||
{ emoji: '🌋', name: '火山', category: 'travel' },
|
||||
{ emoji: '💧', name: '水滴', category: 'travel' },
|
||||
{ emoji: '🌊', name: '海浪', category: 'travel' },
|
||||
{ emoji: '🌅', name: '日出', category: 'travel' },
|
||||
{ emoji: '🌄', name: '日落', category: 'travel' },
|
||||
{ emoji: '🌠', name: '流星', category: 'travel' },
|
||||
{ emoji: '⭐', name: '星星', category: 'travel' },
|
||||
{ emoji: '🌟', name: '闪亮星星', category: 'travel' },
|
||||
{ emoji: '🌙', name: '月亮', category: 'travel' },
|
||||
{ emoji: '🌝', name: '满月', category: 'travel' },
|
||||
{ emoji: '🌚', name: '新月', category: 'travel' },
|
||||
{ emoji: '🌛', name: '弯月', category: 'travel' },
|
||||
{ emoji: '🌜', name: '残月', category: 'travel' },
|
||||
{ emoji: '🌞', name: '太阳', category: 'travel' },
|
||||
{ emoji: '🌡️', name: '温度计', category: 'travel' },
|
||||
{ emoji: '🌤️', name: '晴', category: 'travel' },
|
||||
{ emoji: '⛅', name: '多云', category: 'travel' },
|
||||
{ emoji: '🌥️', name: '阴天', category: 'travel' },
|
||||
{ emoji: '☁️', name: '云', category: 'travel' },
|
||||
{ emoji: '🌧️', name: '下雨', category: 'travel' },
|
||||
{ emoji: '⛈️', name: '雷雨', category: 'travel' },
|
||||
{ emoji: '🌩️', name: '闪电', category: 'travel' },
|
||||
{ emoji: '❄️', name: '雪花', category: 'travel' },
|
||||
{ emoji: '☃️', name: '雪人', category: 'travel' },
|
||||
{ emoji: '⛄', name: '雪⼈', category: 'travel' },
|
||||
{ emoji: '🌬️', name: '风', category: 'travel' },
|
||||
{ emoji: '💨', name: '大风', category: 'travel' },
|
||||
{ emoji: '🕐', name: '1点', category: 'travel' },
|
||||
{ emoji: '🕑', name: '2点', category: 'travel' },
|
||||
{ emoji: '🕒', name: '3点', category: 'travel' },
|
||||
{ emoji: '🕓', name: '4点', category: 'travel' },
|
||||
{ emoji: '🕔', name: '5点', category: 'travel' },
|
||||
{ emoji: '🕕', name: '6点', category: 'travel' },
|
||||
{ emoji: '🕖', name: '7点', category: 'travel' },
|
||||
{ emoji: '🕗', name: '8点', category: 'travel' },
|
||||
{ emoji: '🕘', name: '9点', category: 'travel' },
|
||||
{ emoji: '🕙', name: '10点', category: 'travel' },
|
||||
{ emoji: '🕚', name: '11点', category: 'travel' },
|
||||
{ emoji: '🕛', name: '12点', category: 'travel' },
|
||||
|
||||
// 活动
|
||||
{ emoji: '⚽', name: '足球', category: 'activities' },
|
||||
{ emoji: '🏀', name: '篮球', category: 'activities' },
|
||||
{ emoji: '🏈', name: '橄榄球', category: 'activities' },
|
||||
{ emoji: '⚾', name: '棒球', category: 'activities' },
|
||||
{ emoji: '🥎', name: '垒球', category: 'activities' },
|
||||
{ emoji: '🏐', name: '排球', category: 'activities' },
|
||||
{ emoji: '🏉', name: '橄榄球', category: 'activities' },
|
||||
{ emoji: '🎾', name: '网球', category: 'activities' },
|
||||
{ emoji: '🥏', name: '飞盘', category: 'activities' },
|
||||
{ emoji: '🏐', name: '排球', category: 'activities' },
|
||||
{ emoji: '🏏', name: '板球', category: 'activities' },
|
||||
{ emoji: '🏑', name: '曲棍球', category: 'activities' },
|
||||
{ emoji: '🥍', name: '长曲棍球', category: 'activities' },
|
||||
{ emoji: '🏒', name: '冰球', category: 'activities' },
|
||||
{ emoji: '🥅', name: '球门', category: 'activities' },
|
||||
{ emoji: '⛳', name: '高尔夫', category: 'activities' },
|
||||
{ emoji: '⛸️', name: '滑冰', category: 'activities' },
|
||||
{ emoji: '🎿', name: '滑雪', category: 'activities' },
|
||||
{ emoji: '🛷', name: '雪橇', category: 'activities' },
|
||||
{ emoji: '🏂', name: '单板滑雪', category: 'activities' },
|
||||
{ emoji: '🏋️', name: '举重', category: 'activities' },
|
||||
{ emoji: '🏌️', name: '高尔夫', category: 'activities' },
|
||||
{ emoji: '🚣', name: '划船', category: 'activities' },
|
||||
{ emoji: '🏊', name: '游泳', category: 'activities' },
|
||||
{ emoji: '🏄', name: '冲浪', category: 'activities' },
|
||||
{ emoji: '🏇', name: '赛马', category: 'activities' },
|
||||
{ emoji: '🚴', name: '骑行', category: 'activities' },
|
||||
{ emoji: '🚵', name: '山地自行车', category: 'activities' },
|
||||
{ emoji: '🏹', name: '射箭', category: 'activities' },
|
||||
{ emoji: '🎣', name: '钓鱼', category: 'activities' },
|
||||
{ emoji: '🤿', name: '潜水', category: 'activities' },
|
||||
{ emoji: '🎿', name: '滑雪', category: 'activities' },
|
||||
{ emoji: '🎽', name: '运动衫', category: 'activities' },
|
||||
{ emoji: '🎯', name: '靶心', category: 'activities' },
|
||||
{ emoji: '🎱', name: '台球', category: 'activities' },
|
||||
{ emoji: '🎮', name: '游戏手柄', category: 'activities' },
|
||||
{ emoji: '🎰', name: '老虎机', category: 'activities' },
|
||||
{ emoji: '🎲', name: '骰子', category: 'activities' },
|
||||
{ emoji: '🎯', name: '靶心', category: 'activities' },
|
||||
{ emoji: '🎭', name: '面具', category: 'activities' },
|
||||
{ emoji: '🎪', name: '马戏团', category: 'activities' },
|
||||
{ emoji: '🎫', name: '票', category: 'activities' },
|
||||
{ emoji: '🎟️', name: '门票', category: 'activities' },
|
||||
{ emoji: '🎨', name: '调色板', category: 'activities' },
|
||||
{ emoji: '🎬', name: '电影', category: 'activities' },
|
||||
{ emoji: '🎤', name: '麦克风', category: 'activities' },
|
||||
{ emoji: '🎧', name: '耳机', category: 'activities' },
|
||||
{ emoji: '🎼', name: '乐谱', category: 'activities' },
|
||||
{ emoji: '🎵', name: '音符', category: 'activities' },
|
||||
{ emoji: '🎶', name: '音乐', category: 'activities' },
|
||||
{ emoji: '🎹', name: '钢琴', category: 'activities' },
|
||||
{ emoji: '🥁', name: '鼓', category: 'activities' },
|
||||
{ emoji: '🎷', name: '萨克斯', category: 'activities' },
|
||||
{ emoji: '🎺', name: '小号', category: 'activities' },
|
||||
{ emoji: '🎸', name: '吉他', category: 'activities' },
|
||||
{ emoji: '🎻', name: '小提琴', category: 'activities' },
|
||||
{ emoji: '🎪', name: '马戏团', category: 'activities' },
|
||||
{ emoji: '🎭', name: '戏剧', category: 'activities' },
|
||||
{ emoji: '🎨', name: '艺术', category: 'activities' },
|
||||
{ emoji: '🎯', name: '目标', category: 'activities' },
|
||||
|
||||
// 物体
|
||||
{ emoji: '💻', name: '电脑', category: 'objects' },
|
||||
{ emoji: '📱', name: '手机', category: 'objects' },
|
||||
{ emoji: '📞', name: '电话', category: 'objects' },
|
||||
{ emoji: '📟', name: '寻呼机', category: 'objects' },
|
||||
{ emoji: '📠', name: '传真', category: 'objects' },
|
||||
{ emoji: '💽', name: '光盘', category: 'objects' },
|
||||
{ emoji: '💾', name: '软盘', category: 'objects' },
|
||||
{ emoji: '💿', name: 'CD', category: 'objects' },
|
||||
{ emoji: '📀', name: 'DVD', category: 'objects' },
|
||||
{ emoji: '📼', name: '录像带', category: 'objects' },
|
||||
{ emoji: '📷', name: '相机', category: 'objects' },
|
||||
{ emoji: '📸', name: '拍照', category: 'objects' },
|
||||
{ emoji: '📹', name: '摄像机', category: 'objects' },
|
||||
{ emoji: '🎥', name: '电影摄像机', category: 'objects' },
|
||||
{ emoji: '📺', name: '电视', category: 'objects' },
|
||||
{ emoji: '📻', name: '收音机', category: 'objects' },
|
||||
{ emoji: '📟', name: '寻呼机', category: 'objects' },
|
||||
{ emoji: '🔋', name: '电池', category: 'objects' },
|
||||
{ emoji: '🔌', name: '插头', category: 'objects' },
|
||||
{ emoji: '💡', name: '灯泡', category: 'objects' },
|
||||
{ emoji: '🔦', name: '手电筒', category: 'objects' },
|
||||
{ emoji: '📔', name: '笔记本', category: 'objects' },
|
||||
{ emoji: '📕', name: '书', category: 'objects' },
|
||||
{ emoji: '📖', name: '打开的书', category: 'objects' },
|
||||
{ emoji: '📗', name: '绿书', category: 'objects' },
|
||||
{ emoji: '📘', name: '蓝书', category: 'objects' },
|
||||
{ emoji: '📙', name: '黄书', category: 'objects' },
|
||||
{ emoji: '📚', name: '书架', category: 'objects' },
|
||||
{ emoji: '📓', name: '笔记本', category: 'objects' },
|
||||
{ emoji: '📒', name: '笔记本', category: 'objects' },
|
||||
{ emoji: '📃', name: '文件', category: 'objects' },
|
||||
{ emoji: '📜', name: '卷轴', category: 'objects' },
|
||||
{ emoji: '📄', name: '纸张', category: 'objects' },
|
||||
{ emoji: '📰', name: '报纸', category: 'objects' },
|
||||
{ emoji: '📞', name: '电话', category: 'objects' },
|
||||
{ emoji: '☎️', name: '电话', category: 'objects' },
|
||||
{ emoji: '📟', name: '寻呼机', category: 'objects' },
|
||||
{ emoji: '📠', name: '传真', category: 'objects' },
|
||||
{ emoji: '📡', name: '天线', category: 'objects' },
|
||||
{ emoji: '💾', name: '软盘', category: 'objects' },
|
||||
{ emoji: '💿', name: 'CD', category: 'objects' },
|
||||
{ emoji: '📀', name: 'DVD', category: 'objects' },
|
||||
{ emoji: '📼', name: '录像带', category: 'objects' },
|
||||
{ emoji: '🔍', name: '放大镜', category: 'objects' },
|
||||
{ emoji: '🔎', name: '搜索', category: 'objects' },
|
||||
{ emoji: '🕯️', name: '蜡烛', category: 'objects' },
|
||||
{ emoji: '💣', name: '炸弹', category: 'objects' },
|
||||
{ emoji: '🔫', name: '枪', category: 'objects' },
|
||||
{ emoji: '🔪', name: '刀', category: 'objects' },
|
||||
{ emoji: '💊', name: '药丸', category: 'objects' },
|
||||
{ emoji: '💉', name: '注射器', category: 'objects' },
|
||||
{ emoji: '🔬', name: '显微镜', category: 'objects' },
|
||||
{ emoji: '🔭', name: '望远镜', category: 'objects' },
|
||||
{ emoji: '🧪', name: '试管', category: 'objects' },
|
||||
{ emoji: '🧫', name: '培养皿', category: 'objects' },
|
||||
{ emoji: '🧬', name: 'DNA', category: 'objects' },
|
||||
|
||||
// 符号
|
||||
{ emoji: '❤️', name: '红心', category: 'symbols' },
|
||||
{ emoji: '🧡', name: '橙心', category: 'symbols' },
|
||||
{ emoji: '💛', name: '黄心', category: 'symbols' },
|
||||
{ emoji: '💚', name: '绿心', category: 'symbols' },
|
||||
{ emoji: '💙', name: '蓝心', category: 'symbols' },
|
||||
{ emoji: '💜', name: '紫心', category: 'symbols' },
|
||||
{ emoji: '🖤', name: '黑心', category: 'symbols' },
|
||||
{ emoji: '💔', name: '破碎的心', category: 'symbols' },
|
||||
{ emoji: '💓', name: '心跳', category: 'symbols' },
|
||||
{ emoji: '💗', name: '爱心', category: 'symbols' },
|
||||
{ emoji: '💖', name: '闪亮的心', category: 'symbols' },
|
||||
{ emoji: '💘', name: '丘比特之箭', category: 'symbols' },
|
||||
{ emoji: '💝', name: '礼物', category: 'symbols' },
|
||||
{ emoji: '💞', name: '双心', category: 'symbols' },
|
||||
{ emoji: '💟', name: '心形', category: 'symbols' },
|
||||
{ emoji: '❣️', name: '感叹号', category: 'symbols' },
|
||||
{ emoji: '💕', name: '双心', category: 'symbols' },
|
||||
{ emoji: '💌', name: '情书', category: 'symbols' },
|
||||
{ emoji: '💋', name: '吻', category: 'symbols' },
|
||||
{ emoji: '💯', name: '满分', category: 'symbols' },
|
||||
{ emoji: '💢', name: '生气', category: 'symbols' },
|
||||
{ emoji: '💥', name: '爆炸', category: 'symbols' },
|
||||
{ emoji: '💫', name: '星星', category: 'symbols' },
|
||||
{ emoji: '💦', name: '水滴', category: 'symbols' },
|
||||
{ emoji: '💨', name: '风', category: 'symbols' },
|
||||
{ emoji: '🕳️', name: '洞', category: 'symbols' },
|
||||
{ emoji: '💣', name: '炸弹', category: 'symbols' },
|
||||
{ emoji: '💬', name: '对话', category: 'symbols' },
|
||||
{ emoji: '💭', name: '思考', category: 'symbols' },
|
||||
{ emoji: '💤', name: '打鼾', category: 'symbols' },
|
||||
{ emoji: '💡', name: '灯泡', category: 'symbols' },
|
||||
{ emoji: '💢', name: '生气', category: 'symbols' },
|
||||
{ emoji: '🔔', name: '铃铛', category: 'symbols' },
|
||||
{ emoji: '🔕', name: '静音', category: 'symbols' },
|
||||
{ emoji: '📣', name: '喇叭', category: 'symbols' },
|
||||
{ emoji: '📢', name: '扩音器', category: 'symbols' },
|
||||
{ emoji: '🔊', name: '音量', category: 'symbols' },
|
||||
{ emoji: '🔉', name: '音量', category: 'symbols' },
|
||||
{ emoji: '🔈', name: '音量', category: 'symbols' },
|
||||
{ emoji: '🔇', name: '静音', category: 'symbols' },
|
||||
{ emoji: '✅', name: '对勾', category: 'symbols' },
|
||||
{ emoji: '❌', name: '叉号', category: 'symbols' },
|
||||
{ emoji: '❓', name: '问号', category: 'symbols' },
|
||||
{ emoji: '❗', name: '感叹号', category: 'symbols' },
|
||||
{ emoji: '❔', name: '问号', category: 'symbols' },
|
||||
{ emoji: '❕', name: '感叹号', category: 'symbols' },
|
||||
{ emoji: '💯', name: '100分', category: 'symbols' },
|
||||
{ emoji: '🔟', name: '10', category: 'symbols' },
|
||||
{ emoji: '🔢', name: '数字', category: 'symbols' },
|
||||
{ emoji: '🔣', name: '符号', category: 'symbols' },
|
||||
{ emoji: '🔤', name: '字母', category: 'symbols' },
|
||||
{ emoji: '🔡', name: '小写', category: 'symbols' },
|
||||
{ emoji: '🔠', name: '大写', category: 'symbols' },
|
||||
{ emoji: '🔸', name: '菱形', category: 'symbols' },
|
||||
{ emoji: '🔹', name: '蓝色菱形', category: 'symbols' },
|
||||
{ emoji: '🔺', name: '三角形', category: 'symbols' },
|
||||
{ emoji: '🔻', name: '倒三角形', category: 'symbols' },
|
||||
{ emoji: '💠', name: '钻石', category: 'symbols' },
|
||||
{ emoji: '🔘', name: '圆形', category: 'symbols' },
|
||||
{ emoji: '🔴', name: '红圆', category: 'symbols' },
|
||||
{ emoji: '🔵', name: '蓝圆', category: 'symbols' },
|
||||
{ emoji: '🔺', name: '三角形', category: 'symbols' },
|
||||
{ emoji: '🔻', name: '倒三角形', category: 'symbols' },
|
||||
{ emoji: '🔸', name: '橙色菱形', category: 'symbols' },
|
||||
{ emoji: '🔹', name: '蓝色菱形', category: 'symbols' },
|
||||
{ emoji: '🔶', name: '橙色六边形', category: 'symbols' },
|
||||
{ emoji: '🔷', name: '蓝色六边形', category: 'symbols' },
|
||||
{ emoji: '🔳', name: '白色正方形', category: 'symbols' },
|
||||
{ emoji: '🔲', name: '黑色正方形', category: 'symbols' },
|
||||
|
||||
// 旗帜
|
||||
{ emoji: '🇨🇳', name: '中国国旗', category: 'flags' },
|
||||
{ emoji: '🇺🇸', name: '美国国旗', category: 'flags' },
|
||||
{ emoji: '🇯🇵', name: '日本国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇷', name: '韩国国旗', category: 'flags' },
|
||||
{ emoji: '🇬🇧', name: '英国国旗', category: 'flags' },
|
||||
{ emoji: '🇫🇷', name: '法国国旗', category: 'flags' },
|
||||
{ emoji: '🇩🇪', name: '德国国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇹', name: '意大利国旗', category: 'flags' },
|
||||
{ emoji: '🇷🇺', name: '俄罗斯国旗', category: 'flags' },
|
||||
{ emoji: '🇺🇾', name: '乌拉圭国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇪', name: '比利时国旗', category: 'flags' },
|
||||
{ emoji: '🇳🇱', name: '荷兰国旗', category: 'flags' },
|
||||
{ emoji: '🇪🇸', name: '西班牙国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇹', name: '葡萄牙国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇪', name: '爱尔兰国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇺', name: '澳大利亚国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇦', name: '加拿大国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇱', name: '以色列国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇰', name: '巴基斯坦国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇳', name: '印度国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇩', name: '印度尼西亚国旗', category: 'flags' },
|
||||
{ emoji: '🇹🇭', name: '泰国国旗', category: 'flags' },
|
||||
{ emoji: '🇻🇳', name: '越南国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇦', name: '沙特阿拉伯国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇪', name: '阿联酋国旗', category: 'flags' },
|
||||
{ emoji: '🇹🇷', name: '土耳其国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇾', name: '马来西亚国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇬', name: '新加坡国旗', category: 'flags' },
|
||||
{ emoji: '🇭🇰', name: '香港特别行政区区旗', category: 'flags' },
|
||||
{ emoji: '🇲🇴', name: '澳门特别行政区区旗', category: 'flags' },
|
||||
{ emoji: '🇳🇴', name: '挪威国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇪', name: '瑞典国旗', category: 'flags' },
|
||||
{ emoji: '🇫🇮', name: '芬兰国旗', category: 'flags' },
|
||||
{ emoji: '🇩🇰', name: '丹麦国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇸', name: '冰岛国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇭', name: '瑞士国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇹', name: '奥地利国旗', category: 'flags' },
|
||||
{ emoji: '🇭🇺', name: '匈牙利国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇱', name: '波兰国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇿', name: '捷克国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇰', name: '斯洛伐克国旗', category: 'flags' },
|
||||
{ emoji: '🇭🇷', name: '克罗地亚国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇦', name: '波黑国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇮', name: '斯洛文尼亚国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇰', name: '北马其顿国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇬', name: '保加利亚国旗', category: 'flags' },
|
||||
{ emoji: '🇷🇴', name: '罗马尼亚国旗', category: 'flags' },
|
||||
{ emoji: '🇺🇦', name: '乌克兰国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇾', name: '白俄罗斯国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇿', name: '哈萨克斯坦国旗', category: 'flags' },
|
||||
{ emoji: '🇺🇿', name: '乌兹别克斯坦国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇬', name: '吉尔吉斯斯坦国旗', category: 'flags' },
|
||||
{ emoji: '🇹🇯', name: '塔吉克斯坦国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇲', name: '亚美尼亚国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇿', name: '阿塞拜疆国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇶', name: '伊拉克国旗', category: 'flags' },
|
||||
{ emoji: '🇮🇷', name: '伊朗国旗', category: 'flags' },
|
||||
{ emoji: '🇱🇧', name: '黎巴嫩国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇾', name: '叙利亚国旗', category: 'flags' },
|
||||
{ emoji: '🇯🇴', name: '约旦国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇸', name: '巴勒斯坦国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇼', name: '科威特国旗', category: 'flags' },
|
||||
{ emoji: '🇴🇲', name: '阿曼国旗', category: 'flags' },
|
||||
{ emoji: '🇶🇦', name: '卡塔尔国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇭', name: '巴林国旗', category: 'flags' },
|
||||
{ emoji: '🇾🇪', name: '也门国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇦', name: '摩洛哥国旗', category: 'flags' },
|
||||
{ emoji: '🇩🇿', name: '阿尔及利亚国旗', category: 'flags' },
|
||||
{ emoji: '🇹🇳', name: '突尼斯国旗', category: 'flags' },
|
||||
{ emoji: '🇪🇬', name: '埃及国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇩', name: '苏丹国旗', category: 'flags' },
|
||||
{ emoji: '🇪🇷', name: '厄立特里亚国旗', category: 'flags' },
|
||||
{ emoji: '🇩🇯', name: '吉布提国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇴', name: '索马里国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇪', name: '肯尼亚国旗', category: 'flags' },
|
||||
{ emoji: '🇺🇬', name: '乌干达国旗', category: 'flags' },
|
||||
{ emoji: '🇹🇿', name: '坦桑尼亚国旗', category: 'flags' },
|
||||
{ emoji: '🇷🇼', name: '卢旺达国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇮', name: '布隆迪国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇩', name: '刚果民主共和国国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇬', name: '刚果共和国国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇴', name: '安哥拉国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇿', name: '莫桑比克国旗', category: 'flags' },
|
||||
{ emoji: '🇿🇦', name: '南非国旗', category: 'flags' },
|
||||
{ emoji: '🇳🇦', name: '纳米比亚国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇼', name: '博茨瓦纳国旗', category: 'flags' },
|
||||
{ emoji: '🇿🇲', name: '赞比亚国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇼', name: '马拉维国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇺', name: '毛里求斯国旗', category: 'flags' },
|
||||
{ emoji: '🇸🇨', name: '塞舌尔国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇺', name: '古巴国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇽', name: '墨西哥国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇷', name: '巴西国旗', category: 'flags' },
|
||||
{ emoji: '🇦🇷', name: '阿根廷国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇱', name: '智利国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇪', name: '秘鲁国旗', category: 'flags' },
|
||||
{ emoji: '🇪🇨', name: '厄瓜多尔国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇴', name: '玻利维亚国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇾', name: '巴拉圭国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇺', name: '古巴国旗', category: 'flags' },
|
||||
{ emoji: '🇩🇴', name: '多米尼加共和国国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇭', name: '菲律宾国旗', category: 'flags' },
|
||||
{ emoji: '🇻🇺', name: '瓦努阿图国旗', category: 'flags' },
|
||||
{ emoji: '🇫🇯', name: '斐济国旗', category: 'flags' },
|
||||
{ emoji: '🇵🇳', name: '帕劳国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇵', name: '马绍尔群岛国旗', category: 'flags' },
|
||||
{ emoji: '🇫🇲', name: '密克罗尼西亚联邦国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇮', name: '基里巴斯国旗', category: 'flags' },
|
||||
{ emoji: '🇹🇻', name: '图瓦卢国旗', category: 'flags' },
|
||||
{ emoji: '🇨🇰', name: '库克群岛国旗', category: 'flags' },
|
||||
{ emoji: '🇳🇺', name: '纽埃国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇲', name: '缅甸国旗', category: 'flags' },
|
||||
{ emoji: '🇱🇰', name: '斯里兰卡国旗', category: 'flags' },
|
||||
{ emoji: '🇲🇻', name: '马尔代夫国旗', category: 'flags' },
|
||||
{ emoji: '🇧🇩', name: '孟加拉国国旗', category: 'flags' },
|
||||
{ emoji: '🇳🇵', name: '尼泊尔国旗', category: 'flags' },
|
||||
{ emoji: '🇱🇦', name: '老挝国旗', category: 'flags' },
|
||||
{ emoji: '🇰🇭', name: '柬埔寨国旗', category: 'flags' }
|
||||
]
|
||||
|
||||
// 过滤后的Emoji
|
||||
const filteredEmojis = computed(() => {
|
||||
let result = emojis
|
||||
|
||||
// 按分类过滤
|
||||
if (selectedCategory.value !== 'all') {
|
||||
result = result.filter(emoji => emoji.category === selectedCategory.value)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(emoji => emoji.name.toLowerCase().includes(keyword))
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 复制Emoji
|
||||
const copyEmoji = (emoji: string) => {
|
||||
copy(emoji)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 搜索和分类 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索Emoji"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<el-button
|
||||
v-for="category in emojiCategories"
|
||||
:key="category.id"
|
||||
:type="selectedCategory === category.id ? 'primary' : 'default'"
|
||||
@click="selectedCategory = category.id"
|
||||
size="small"
|
||||
>
|
||||
{{ category.name }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emoji列表 -->
|
||||
<div class="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12 gap-4">
|
||||
<div
|
||||
v-for="emoji in filteredEmojis"
|
||||
:key="emoji.emoji + emoji.name"
|
||||
class="flex flex-col items-center p-2 border rounded hover:bg-gray-50 cursor-pointer"
|
||||
@click="copyEmoji(emoji.emoji)"
|
||||
>
|
||||
<div class="text-3xl mb-1">{{ emoji.emoji }}</div>
|
||||
<div class="text-xs text-center">{{ emoji.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线Emoji表情大全,提供各种分类的Emoji表情,点击即可复制到剪贴板,方便在聊天、文档中使用。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
import { Md5 } from 'ts-md5'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const title = "Hash计算器"
|
||||
|
||||
// 状态管理
|
||||
const inputText = ref('')
|
||||
const algorithm = ref('MD5')
|
||||
const key = ref('')
|
||||
const hashResult = ref('')
|
||||
|
||||
// 计算属性:是否是Hmac算法
|
||||
const isHmacAlgorithm = computed(() => {
|
||||
return algorithm.value.startsWith('HMAC-')
|
||||
})
|
||||
|
||||
// 哈希算法选项
|
||||
const algorithmOptions = [
|
||||
{ label: 'MD5', value: 'MD5' },
|
||||
{ label: 'SHA-1', value: 'SHA-1' },
|
||||
{ label: 'SHA-256', value: 'SHA-256' },
|
||||
{ label: 'SHA-384', value: 'SHA-384' },
|
||||
{ label: 'SHA-512', value: 'SHA-512' },
|
||||
{ label: 'Hmac-MD5', value: 'HMAC-MD5' },
|
||||
{ label: 'Hmac-SHA1', value: 'HMAC-SHA1' },
|
||||
{ label: 'Hmac-SHA256', value: 'HMAC-SHA256' },
|
||||
{ label: 'Hmac-SHA384', value: 'HMAC-SHA384' },
|
||||
{ label: 'Hmac-SHA512', value: 'HMAC-SHA512' }
|
||||
]
|
||||
|
||||
// 监听输入变化,自动计算哈希
|
||||
watch([inputText, algorithm, key], async () => {
|
||||
if (!inputText.value) {
|
||||
hashResult.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (algorithm.value === 'MD5') {
|
||||
// 计算MD5
|
||||
hashResult.value = Md5.hashStr(inputText.value)
|
||||
} else if (algorithm.value.startsWith('HMAC-')) {
|
||||
// 处理Hmac算法
|
||||
if (!key.value) {
|
||||
hashResult.value = '请输入Hmac密钥'
|
||||
return
|
||||
}
|
||||
|
||||
// 提取Hmac算法中的哈希算法部分
|
||||
const hashAlgorithm = algorithm.value.replace('HMAC-', '')
|
||||
|
||||
if (hashAlgorithm === 'MD5') {
|
||||
// 使用crypto-js计算Hmac-MD5
|
||||
try {
|
||||
const hmac = CryptoJS.HmacMD5(inputText.value, key.value)
|
||||
hashResult.value = hmac.toString()
|
||||
} catch (error) {
|
||||
console.error('Hmac-MD5计算失败:', error)
|
||||
hashResult.value = '计算失败'
|
||||
}
|
||||
} else {
|
||||
// 处理其他Hmac算法
|
||||
// 转换为Web Crypto API支持的标准格式
|
||||
let standardHashAlgorithm
|
||||
switch (hashAlgorithm) {
|
||||
case 'SHA1':
|
||||
standardHashAlgorithm = 'SHA-1'
|
||||
break
|
||||
case 'SHA256':
|
||||
standardHashAlgorithm = 'SHA-256'
|
||||
break
|
||||
case 'SHA384':
|
||||
standardHashAlgorithm = 'SHA-384'
|
||||
break
|
||||
case 'SHA512':
|
||||
standardHashAlgorithm = 'SHA-512'
|
||||
break
|
||||
default:
|
||||
standardHashAlgorithm = hashAlgorithm
|
||||
}
|
||||
|
||||
try {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(inputText.value)
|
||||
const keyData = encoder.encode(key.value)
|
||||
|
||||
// 创建密钥
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{ name: 'HMAC', hash: standardHashAlgorithm },
|
||||
false,
|
||||
['sign']
|
||||
)
|
||||
|
||||
// 计算Hmac
|
||||
const hmacBuffer = await crypto.subtle.sign('HMAC', cryptoKey, data)
|
||||
const hmacArray = Array.from(new Uint8Array(hmacBuffer))
|
||||
const hmacHex = hmacArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
hashResult.value = hmacHex
|
||||
} catch (error) {
|
||||
console.error('Hmac计算失败:', error)
|
||||
hashResult.value = '计算失败'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用Web Crypto API计算其他哈希
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(inputText.value)
|
||||
const hashBuffer = await crypto.subtle.digest(algorithm.value, data)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
hashResult.value = hashHex
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('计算失败:', error)
|
||||
hashResult.value = '计算失败'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 计算哈希
|
||||
const calculateHash = async () => {
|
||||
// 由于我们已经在watch中自动计算了哈希,这里只需要显示成功消息
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要计算哈希的文本')
|
||||
return
|
||||
}
|
||||
|
||||
if (algorithm.value.startsWith('HMAC-') && !key.value) {
|
||||
ElMessage.warning('请输入Hmac密钥')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('计算成功')
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!hashResult.value || hashResult.value === '计算失败' || hashResult.value === '请输入Hmac密钥') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(hashResult.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入要计算哈希的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<el-text>哈希算法:</el-text>
|
||||
<el-select v-model="algorithm" style="width: 150px;">
|
||||
<el-option
|
||||
v-for="option in algorithmOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 密钥输入(仅Hmac算法需要) -->
|
||||
<div class="flex items-center gap-2 mb-2" v-if="isHmacAlgorithm">
|
||||
<el-text>密钥(HMAC算法必填):</el-text>
|
||||
<el-input
|
||||
v-model="key"
|
||||
placeholder="请输入Hmac密钥"
|
||||
style="width: 500px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="calculateHash">计算哈希</el-button>
|
||||
<el-button @click="copyResult">复制结果</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
:value="hashResult"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="哈希结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线Hash计算器,支持MD5、SHA-1、SHA-256、SHA-384、SHA-512等多种哈希算法,以及Hmac-MD5、Hmac-SHA1、Hmac-SHA256、Hmac-SHA384、Hmac-SHA512等Hmac算法,可用于数据完整性验证、密码加密等场景。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="upload-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="上传图片">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 JPG、PNG、GIF 等格式的图片
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="image-section">
|
||||
<div v-if="imageUrl" class="image-container">
|
||||
<img
|
||||
ref="image"
|
||||
:src="imageUrl"
|
||||
@load="handleImageLoad"
|
||||
@click="handleImageClick"
|
||||
class="preview-image"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
class="hidden-canvas"
|
||||
></canvas>
|
||||
</div>
|
||||
<div v-else class="no-image">
|
||||
请上传图片
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-section">
|
||||
<h3>颜色信息</h3>
|
||||
<div v-if="selectedColor" class="color-info">
|
||||
<div class="color-preview" :style="{ backgroundColor: selectedColor.hex }"></div>
|
||||
<div class="color-details">
|
||||
<div class="color-item">
|
||||
<span class="color-label">HEX:</span>
|
||||
<span class="color-value">{{ selectedColor.hex }}</span>
|
||||
<el-button type="text" size="small" @click="copyColor(selectedColor.hex)">复制</el-button>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<span class="color-label">RGB:</span>
|
||||
<span class="color-value">{{ selectedColor.rgb }}</span>
|
||||
<el-button type="text" size="small" @click="copyColor(selectedColor.rgb)">复制</el-button>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<span class="color-label">HSL:</span>
|
||||
<span class="color-value">{{ selectedColor.hsl }}</span>
|
||||
<el-button type="text" size="small" @click="copyColor(selectedColor.hsl)">复制</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-color">
|
||||
点击图片获取颜色
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线传图取色工具,上传图片后点击图片任意位置获取颜色值,支持 HEX、RGB、HSL 三种颜色格式,可用于设计和开发中获取颜色参考。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "传图取色"
|
||||
|
||||
const imageUrl = ref('');
|
||||
const image = ref<HTMLImageElement | null>(null);
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const selectedColor = ref<{
|
||||
hex: string;
|
||||
rgb: string;
|
||||
hsl: string;
|
||||
} | null>(null);
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileChange = (file: any) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
imageUrl.value = e.target?.result as string;
|
||||
selectedColor.value = null;
|
||||
};
|
||||
reader.readAsDataURL(file.raw);
|
||||
};
|
||||
|
||||
// 处理图片加载完成
|
||||
const handleImageLoad = () => {
|
||||
if (image.value && canvas.value) {
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (ctx) {
|
||||
// 设置canvas尺寸与图片一致
|
||||
canvas.value.width = image.value.width;
|
||||
canvas.value.height = image.value.height;
|
||||
// 绘制图片到canvas
|
||||
ctx.drawImage(image.value, 0, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图片点击事件
|
||||
const handleImageClick = (e: MouseEvent) => {
|
||||
if (!image.value || !canvas.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rect = image.value.getBoundingClientRect();
|
||||
const x = Math.floor((e.clientX - rect.left) * (image.value.width / rect.width));
|
||||
const y = Math.floor((e.clientY - rect.top) * (image.value.height / rect.height));
|
||||
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (ctx) {
|
||||
const pixel = ctx.getImageData(x, y, 1, 1).data;
|
||||
const [r, g, b, a] = pixel;
|
||||
|
||||
// 转换为各种颜色格式
|
||||
const hex = rgbToHex(r, g, b);
|
||||
const rgb = `rgb(${r}, ${g}, ${b})`;
|
||||
const hsl = rgbToHsl(r, g, b);
|
||||
|
||||
selectedColor.value = {
|
||||
hex,
|
||||
rgb,
|
||||
hsl
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取色失败:', error);
|
||||
ElMessage.error('取色失败,可能是图片跨域问题');
|
||||
}
|
||||
};
|
||||
|
||||
// RGB转HEX
|
||||
const rgbToHex = (r: number, g: number, b: number) => {
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
|
||||
};
|
||||
|
||||
// RGB转HSL
|
||||
const rgbToHsl = (r: number, g: number, b: number) => {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
let l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
h = Math.round(h * 360);
|
||||
s = Math.round(s * 100);
|
||||
l = Math.round(l * 100);
|
||||
|
||||
return `hsl(${h}, ${s}%, ${l}%)`;
|
||||
};
|
||||
|
||||
// 复制颜色值
|
||||
const copyColor = (color: string) => {
|
||||
navigator.clipboard.writeText(color).then(() => {
|
||||
ElMessage.success('已复制到剪贴板');
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.hidden-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.color-section {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.color-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.color-label {
|
||||
width: 60px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.color-value {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
background-color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.no-color {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.color-section {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="input-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="上传图片">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 JPG、PNG、GIF 等格式的图片
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="水印文字">
|
||||
<el-input
|
||||
v-model="watermarkText"
|
||||
placeholder="请输入水印文字"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="水印设置">
|
||||
<div class="settings-grid">
|
||||
<el-form-item label="位置" class="setting-item">
|
||||
<el-select v-model="position" size="small">
|
||||
<el-option label="左上角" value="top-left" />
|
||||
<el-option label="右上角" value="top-right" />
|
||||
<el-option label="左下角" value="bottom-left" />
|
||||
<el-option label="右下角" value="bottom-right" />
|
||||
<el-option label="居中" value="center" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字体大小" class="setting-item">
|
||||
<el-input-number v-model="fontSize" :min="12" :max="100" size="small" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="透明度" class="setting-item">
|
||||
<el-slider v-model="opacity" :min="0" :max="1" :step="0.1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="颜色" class="setting-item">
|
||||
<el-color-picker v-model="textColor" size="small" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="addWatermark">添加水印</el-button>
|
||||
<el-button @click="clearImage">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="preview-section">
|
||||
<h3>预览</h3>
|
||||
<div class="preview-container">
|
||||
<div v-if="imageUrl" class="image-wrapper">
|
||||
<img :src="imageUrl" class="original-image" />
|
||||
<canvas
|
||||
ref="canvas"
|
||||
class="watermark-canvas"
|
||||
:width="imageWidth"
|
||||
:height="imageHeight"
|
||||
></canvas>
|
||||
</div>
|
||||
<div v-else class="no-image">
|
||||
请上传图片
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="watermarkedUrl" class="download-section">
|
||||
<el-button type="success" @click="downloadImage">下载图片</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线图片水印添加工具,上传图片后添加自定义文字水印,支持设置水印位置、字体大小、透明度和颜色,可下载带水印的图片。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "图片水印添加"
|
||||
|
||||
const imageUrl = ref('');
|
||||
const watermarkedUrl = ref('');
|
||||
const watermarkText = ref('水印');
|
||||
const position = ref('bottom-right');
|
||||
const fontSize = ref(24);
|
||||
const opacity = ref(0.5);
|
||||
const textColor = ref('#ffffff');
|
||||
const imageWidth = ref(0);
|
||||
const imageHeight = ref(0);
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileChange = (file: any) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
imageWidth.value = img.width;
|
||||
imageHeight.value = img.height;
|
||||
imageUrl.value = e.target?.result as string;
|
||||
watermarkedUrl.value = '';
|
||||
};
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file.raw);
|
||||
};
|
||||
|
||||
// 添加水印
|
||||
const addWatermark = async () => {
|
||||
if (!imageUrl.value || !watermarkText.value) {
|
||||
ElMessage.warning('请上传图片并输入水印文字');
|
||||
return;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (canvas.value) {
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (ctx) {
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
|
||||
// 绘制原始图片
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 设置水印样式
|
||||
ctx.font = `${fontSize.value}px Arial`;
|
||||
ctx.fillStyle = `${textColor.value}${Math.floor(opacity.value * 255).toString(16).padStart(2, '0')}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// 计算水印位置
|
||||
let x = canvas.value!.width / 2;
|
||||
let y = canvas.value!.height / 2;
|
||||
|
||||
switch (position.value) {
|
||||
case 'top-left':
|
||||
x = fontSize.value * 1.5;
|
||||
y = fontSize.value * 1.5;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
break;
|
||||
case 'top-right':
|
||||
x = canvas.value!.width - fontSize.value * 1.5;
|
||||
y = fontSize.value * 1.5;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'top';
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = fontSize.value * 1.5;
|
||||
y = canvas.value!.height - fontSize.value * 1.5;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'bottom';
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = canvas.value!.width - fontSize.value * 1.5;
|
||||
y = canvas.value!.height - fontSize.value * 1.5;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'bottom';
|
||||
break;
|
||||
}
|
||||
|
||||
// 绘制水印文字
|
||||
ctx.fillText(watermarkText.value, x, y);
|
||||
|
||||
// 生成带水印的图片URL
|
||||
watermarkedUrl.value = canvas.value!.toDataURL('image/png');
|
||||
};
|
||||
img.src = imageUrl.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 下载图片
|
||||
const downloadImage = () => {
|
||||
if (!watermarkedUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = watermarkedUrl.value;
|
||||
link.download = `watermarked_${Date.now()}.png`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const clearImage = () => {
|
||||
imageUrl.value = '';
|
||||
watermarkedUrl.value = '';
|
||||
watermarkText.value = '水印';
|
||||
position.value = 'bottom-right';
|
||||
fontSize.value = 24;
|
||||
opacity.value = 0.5;
|
||||
textColor.value = '#ffffff';
|
||||
imageWidth.value = 0;
|
||||
imageHeight.value = 0;
|
||||
|
||||
if (canvas.value) {
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preview-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.original-image {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.watermark-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { reactive } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { transferred, copy } from '@/utils/string';
|
||||
|
||||
import { Codemirror } from "vue-codemirror";
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import '@codemirror/search';
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="input-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="抽签选项(每行一个)">
|
||||
<el-input
|
||||
v-model="optionsText"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="请输入抽签选项,每行一个"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="抽取设置">
|
||||
<div class="settings-grid">
|
||||
<el-form-item label="抽取数量" class="setting-item">
|
||||
<el-input-number v-model="drawCount" :min="1" :max="10" size="small" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否允许重复" class="setting-item">
|
||||
<el-switch v-model="allowRepeat" size="small" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="drawLottery">开始抽签</el-button>
|
||||
<el-button @click="clearOptions">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<h3>抽签结果</h3>
|
||||
<div v-if="results.length > 0" class="result-list">
|
||||
<div v-for="(result, index) in results" :key="index" class="result-item">
|
||||
<span class="result-number">{{ index + 1 }}</span>
|
||||
<span class="result-text">{{ result }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-result">
|
||||
点击开始抽签按钮开始抽取
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线抽签工具,输入多个选项后随机抽取一个或多个结果,支持设置抽取数量和是否允许重复抽取,可用于随机选择、抽奖等场景。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "抽签工具"
|
||||
|
||||
const optionsText = ref('');
|
||||
const drawCount = ref(1);
|
||||
const allowRepeat = ref(false);
|
||||
const results = ref<string[]>([]);
|
||||
|
||||
// 开始抽签
|
||||
const drawLottery = () => {
|
||||
// 解析输入的选项
|
||||
const options = optionsText.value
|
||||
.split('\n')
|
||||
.map(option => option.trim())
|
||||
.filter(option => option !== '');
|
||||
|
||||
if (options.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算实际抽取数量
|
||||
const actualDrawCount = allowRepeat.value
|
||||
? Math.min(drawCount.value, 10)
|
||||
: Math.min(drawCount.value, options.length);
|
||||
|
||||
// 执行抽签
|
||||
const newResults: string[] = [];
|
||||
const availableOptions = [...options];
|
||||
|
||||
for (let i = 0; i < actualDrawCount; i++) {
|
||||
if (availableOptions.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * availableOptions.length);
|
||||
const selectedOption = availableOptions[randomIndex];
|
||||
newResults.push(selectedOption);
|
||||
|
||||
// 如果不允许重复,从可用选项中移除
|
||||
if (!allowRepeat.value) {
|
||||
availableOptions.splice(randomIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
results.value = newResults;
|
||||
};
|
||||
|
||||
// 清空选项
|
||||
const clearOptions = () => {
|
||||
optionsText.value = '';
|
||||
results.value = [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
|
||||
.result-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #409eff;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.no-result {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="input-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="m3u8地址">
|
||||
<el-input
|
||||
v-model="m3u8Url"
|
||||
placeholder="请输入m3u8格式的视频地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadVideo">加载视频</el-button>
|
||||
<el-button @click="clearUrl">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="player-section">
|
||||
<div v-if="m3u8Url" class="player-container">
|
||||
<video
|
||||
ref="videoPlayer"
|
||||
class="video-js vjs-default-skin vjs-big-play-centered"
|
||||
controls
|
||||
preload="auto"
|
||||
width="100%"
|
||||
height="400"
|
||||
>
|
||||
<source :src="m3u8Url" type="application/x-mpegURL">
|
||||
您的浏览器不支持HTML5视频播放。
|
||||
</video>
|
||||
</div>
|
||||
<div v-else class="no-video">
|
||||
请输入m3u8地址并点击加载视频
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线m3u8播放器,支持播放m3u8格式的视频流,可输入自定义m3u8地址进行播放,适用于直播流和视频点播场景。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "m3u8在线播放"
|
||||
|
||||
const m3u8Url = ref('');
|
||||
const videoPlayer = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
// 加载视频
|
||||
const loadVideo = () => {
|
||||
if (!m3u8Url.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新加载视频元素
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.load();
|
||||
}
|
||||
};
|
||||
|
||||
// 清空地址
|
||||
const clearUrl = () => {
|
||||
m3u8Url.value = '';
|
||||
};
|
||||
|
||||
// 示例地址
|
||||
const exampleUrl = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
|
||||
|
||||
onMounted(() => {
|
||||
// 加载示例地址
|
||||
m3u8Url.value = exampleUrl;
|
||||
loadVideo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.player-section {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.player-container {
|
||||
width: 100%;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.no-video {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 100px 0;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 确保视频播放器占满容器 */
|
||||
:deep(.video-js) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.player-section {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="timer-section">
|
||||
<div class="timer-display">
|
||||
<div class="timer-circle">
|
||||
<svg width="200" height="200" viewBox="0 0 200 200">
|
||||
<!-- 背景圆环 -->
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="90"
|
||||
fill="none"
|
||||
stroke="#e4e7ed"
|
||||
stroke-width="10"
|
||||
/>
|
||||
<!-- 进度圆环 -->
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="90"
|
||||
fill="none"
|
||||
stroke="#409eff"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="circumference"
|
||||
:stroke-dashoffset="progressOffset"
|
||||
transform="rotate(-90 100 100)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="timer-text">
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timer-controls">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="startTimer"
|
||||
:disabled="isRunning"
|
||||
>
|
||||
开始
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="pauseTimer"
|
||||
:disabled="!isRunning"
|
||||
>
|
||||
暂停
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="resetTimer"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>时间设置(分钟)</h3>
|
||||
<el-form label-position="top" class="settings-form">
|
||||
<el-form-item label="工作时间">
|
||||
<el-input-number
|
||||
v-model="workTime"
|
||||
:min="1"
|
||||
:max="60"
|
||||
:step="5"
|
||||
:disabled="isRunning"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="短休息">
|
||||
<el-input-number
|
||||
v-model="shortBreakTime"
|
||||
:min="1"
|
||||
:max="30"
|
||||
:step="5"
|
||||
:disabled="isRunning"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="长休息">
|
||||
<el-input-number
|
||||
v-model="longBreakTime"
|
||||
:min="1"
|
||||
:max="60"
|
||||
:step="5"
|
||||
:disabled="isRunning"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作轮数">
|
||||
<el-input-number
|
||||
v-model="workRounds"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:disabled="isRunning"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<h3>状态</h3>
|
||||
<div class="status-info">
|
||||
<div class="status-item">
|
||||
<span class="status-label">当前模式:</span>
|
||||
<span class="status-value">{{ currentModeText }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">已完成轮数:</span>
|
||||
<span class="status-value">{{ completedRounds }}/{{ workRounds }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">今日完成:</span>
|
||||
<span class="status-value">{{ todayCompleted }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线番茄时钟工具,用于时间管理,支持工作和休息模式切换,可自定义工作时间、休息时间和工作轮数,帮助提高工作效率和专注力。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "番茄时钟"
|
||||
|
||||
// 时间设置
|
||||
const workTime = ref(25);
|
||||
const shortBreakTime = ref(5);
|
||||
const longBreakTime = ref(15);
|
||||
const workRounds = ref(4);
|
||||
|
||||
// 状态
|
||||
const isRunning = ref(false);
|
||||
const currentTime = ref(workTime.value * 60);
|
||||
const currentMode = ref<'work' | 'shortBreak' | 'longBreak'>('work');
|
||||
const completedRounds = ref(0);
|
||||
const todayCompleted = ref(0);
|
||||
const timerInterval = ref<number | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const circumference = computed(() => 2 * Math.PI * 90);
|
||||
|
||||
const progressOffset = computed(() => {
|
||||
const totalTime = currentMode.value === 'work' ? workTime.value * 60 :
|
||||
currentMode.value === 'shortBreak' ? shortBreakTime.value * 60 :
|
||||
longBreakTime.value * 60;
|
||||
const progress = currentTime.value / totalTime;
|
||||
return circumference.value * (1 - progress);
|
||||
});
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
const minutes = Math.floor(currentTime.value / 60);
|
||||
const seconds = currentTime.value % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
});
|
||||
|
||||
const currentModeText = computed(() => {
|
||||
switch (currentMode.value) {
|
||||
case 'work': return '工作';
|
||||
case 'shortBreak': return '短休息';
|
||||
case 'longBreak': return '长休息';
|
||||
default: return '工作';
|
||||
}
|
||||
});
|
||||
|
||||
// 开始计时器
|
||||
const startTimer = () => {
|
||||
if (isRunning.value) return;
|
||||
|
||||
isRunning.value = true;
|
||||
timerInterval.value = window.setInterval(() => {
|
||||
if (currentTime.value > 0) {
|
||||
currentTime.value--;
|
||||
} else {
|
||||
// 时间到
|
||||
clearInterval(timerInterval.value!);
|
||||
isRunning.value = false;
|
||||
|
||||
// 播放提示音
|
||||
playNotification();
|
||||
|
||||
// 切换模式
|
||||
switchMode();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 暂停计时器
|
||||
const pauseTimer = () => {
|
||||
if (!isRunning.value) return;
|
||||
|
||||
clearInterval(timerInterval.value!);
|
||||
isRunning.value = false;
|
||||
};
|
||||
|
||||
// 重置计时器
|
||||
const resetTimer = () => {
|
||||
clearInterval(timerInterval.value!);
|
||||
isRunning.value = false;
|
||||
currentTime.value = workTime.value * 60;
|
||||
currentMode.value = 'work';
|
||||
completedRounds.value = 0;
|
||||
};
|
||||
|
||||
// 切换模式
|
||||
const switchMode = () => {
|
||||
if (currentMode.value === 'work') {
|
||||
completedRounds.value++;
|
||||
todayCompleted.value++;
|
||||
|
||||
if (completedRounds.value % workRounds.value === 0) {
|
||||
// 长休息
|
||||
currentMode.value = 'longBreak';
|
||||
currentTime.value = longBreakTime.value * 60;
|
||||
} else {
|
||||
// 短休息
|
||||
currentMode.value = 'shortBreak';
|
||||
currentTime.value = shortBreakTime.value * 60;
|
||||
}
|
||||
} else {
|
||||
// 回到工作模式
|
||||
currentMode.value = 'work';
|
||||
currentTime.value = workTime.value * 60;
|
||||
}
|
||||
|
||||
ElMessage.success(`切换到${currentModeText.value}模式`);
|
||||
};
|
||||
|
||||
// 播放提示音
|
||||
const playNotification = () => {
|
||||
try {
|
||||
const audio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAA');
|
||||
audio.play();
|
||||
} catch (e) {
|
||||
console.log('无法播放提示音');
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 从本地存储加载今日完成数
|
||||
const today = new Date().toDateString();
|
||||
const storedData = localStorage.getItem('pomodoro');
|
||||
if (storedData) {
|
||||
const data = JSON.parse(storedData);
|
||||
if (data.date === today) {
|
||||
todayCompleted.value = data.completed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 保存今日完成数到本地存储
|
||||
const today = new Date().toDateString();
|
||||
localStorage.setItem('pomodoro', JSON.stringify({
|
||||
date: today,
|
||||
completed: todayCompleted.value
|
||||
}));
|
||||
|
||||
// 清除计时器
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.timer-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.timer-circle {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.timer-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.timer-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.timer-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="test-section">
|
||||
<div
|
||||
ref="testArea"
|
||||
class="test-area"
|
||||
:class="testStatus"
|
||||
@click="handleTestClick"
|
||||
>
|
||||
<div v-if="testStatus === 'ready'" class="test-message">
|
||||
点击开始测试
|
||||
</div>
|
||||
<div v-else-if="testStatus === 'waiting'" class="test-message">
|
||||
准备就绪,等待变色...
|
||||
</div>
|
||||
<div v-else-if="testStatus === 'active'" class="test-message">
|
||||
点击!
|
||||
</div>
|
||||
<div v-else-if="testStatus === 'completed'" class="test-message">
|
||||
反应时间: {{ reactionTime }} ms
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-controls">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="startTest"
|
||||
:disabled="testStatus === 'waiting' || testStatus === 'active'"
|
||||
>
|
||||
开始测试
|
||||
</el-button>
|
||||
<el-button @click="resetTest">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<h3>测试统计</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">最佳反应时间</div>
|
||||
<div class="stat-value">{{ bestTime }} ms</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">平均反应时间</div>
|
||||
<div class="stat-value">{{ averageTime }} ms</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">测试次数</div>
|
||||
<div class="stat-value">{{ testCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-section">
|
||||
<h4>测试历史</h4>
|
||||
<div class="history-list">
|
||||
<div
|
||||
v-for="(time, index) in testHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
>
|
||||
测试 {{ index + 1 }}: {{ time }} ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线反应速度测试工具,通过点击变色的方块来测试你的反应速度,记录测试历史和统计数据,帮助你了解自己的反应能力。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "反应速度测试"
|
||||
|
||||
const testArea = ref<HTMLElement | null>(null);
|
||||
const testStatus = ref<'ready' | 'waiting' | 'active' | 'completed'>('ready');
|
||||
const reactionTime = ref(0);
|
||||
const testHistory = ref<number[]>([]);
|
||||
const startTime = ref(0);
|
||||
const waitingTimer = ref<number | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const testCount = computed(() => testHistory.value.length);
|
||||
|
||||
const bestTime = computed(() => {
|
||||
if (testHistory.value.length === 0) return 0;
|
||||
return Math.min(...testHistory.value);
|
||||
});
|
||||
|
||||
const averageTime = computed(() => {
|
||||
if (testHistory.value.length === 0) return 0;
|
||||
const sum = testHistory.value.reduce((acc, time) => acc + time, 0);
|
||||
return Math.round(sum / testHistory.value.length);
|
||||
});
|
||||
|
||||
// 开始测试
|
||||
const startTest = () => {
|
||||
testStatus.value = 'waiting';
|
||||
|
||||
// 随机等待时间(1-5秒)
|
||||
const waitTime = 1000 + Math.random() * 4000;
|
||||
|
||||
waitingTimer.value = window.setTimeout(() => {
|
||||
testStatus.value = 'active';
|
||||
startTime.value = performance.now();
|
||||
}, waitTime);
|
||||
};
|
||||
|
||||
// 处理测试区域点击
|
||||
const handleTestClick = () => {
|
||||
if (testStatus.value === 'ready') {
|
||||
startTest();
|
||||
} else if (testStatus.value === 'active') {
|
||||
const endTime = performance.now();
|
||||
reactionTime.value = Math.round(endTime - startTime.value);
|
||||
testHistory.value.push(reactionTime.value);
|
||||
testStatus.value = 'completed';
|
||||
}
|
||||
};
|
||||
|
||||
// 重置测试
|
||||
const resetTest = () => {
|
||||
if (waitingTimer.value) {
|
||||
clearTimeout(waitingTimer.value);
|
||||
}
|
||||
testStatus.value = 'ready';
|
||||
reactionTime.value = 0;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.test-area {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.test-area.ready {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.test-area.waiting {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.test-area.active {
|
||||
background-color: #f0f9eb;
|
||||
border-color: #67c23a;
|
||||
box-shadow: 0 0 20px rgba(103, 194, 58, 0.5);
|
||||
}
|
||||
|
||||
.test-area.completed {
|
||||
background-color: #fef0f0;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
.test-message {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.history-section h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
margin-bottom: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="game-section">
|
||||
<div class="player-section">
|
||||
<h3>你的选择</h3>
|
||||
<div class="choices">
|
||||
<div
|
||||
v-for="choice in choices"
|
||||
:key="choice.value"
|
||||
class="choice-item"
|
||||
:class="{ active: selectedChoice === choice.value }"
|
||||
@click="makeChoice(choice.value)"
|
||||
>
|
||||
<div class="choice-icon">{{ choice.icon }}</div>
|
||||
<div class="choice-name">{{ choice.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<h3>对战结果</h3>
|
||||
<div v-if="gameResult" class="result-content">
|
||||
<div class="result-message">{{ gameResult.message }}</div>
|
||||
<div class="result-details">
|
||||
<div class="player-result">你: {{ getChoiceName(selectedChoice) }}</div>
|
||||
<div class="computer-result">电脑: {{ getChoiceName(computerChoice) }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="resetGame">再来一局</el-button>
|
||||
</div>
|
||||
<div v-else class="no-result">
|
||||
请选择你的武器开始游戏
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="computer-section">
|
||||
<h3>电脑选择</h3>
|
||||
<div class="computer-choice">
|
||||
<div v-if="computerChoice" class="choice-item computer">
|
||||
<div class="choice-icon">{{ getChoiceIcon(computerChoice) }}</div>
|
||||
<div class="choice-name">{{ getChoiceName(computerChoice) }}</div>
|
||||
</div>
|
||||
<div v-else class="waiting">
|
||||
等待中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<h3>游戏统计</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总游戏数</div>
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
</div>
|
||||
<div class="stat-item win">
|
||||
<div class="stat-label">胜利</div>
|
||||
<div class="stat-value">{{ stats.wins }}</div>
|
||||
</div>
|
||||
<div class="stat-item lose">
|
||||
<div class="stat-label">失败</div>
|
||||
<div class="stat-value">{{ stats.losses }}</div>
|
||||
</div>
|
||||
<div class="stat-item draw">
|
||||
<div class="stat-label">平局</div>
|
||||
<div class="stat-value">{{ stats.draws }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button @click="resetStats">重置统计</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线剪刀石头布游戏,与电脑对战,支持实时显示对战结果和游戏统计,可用于休闲娱乐和决策参考。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "剪刀石头布"
|
||||
|
||||
const choices = [
|
||||
{ name: '剪刀', value: 'scissors', icon: '✂️' },
|
||||
{ name: '石头', value: 'rock', icon: '🪨' },
|
||||
{ name: '布', value: 'paper', icon: '📄' }
|
||||
];
|
||||
|
||||
const selectedChoice = ref<string>('');
|
||||
const computerChoice = ref<string>('');
|
||||
const gameResult = ref<{ message: string; result: 'win' | 'lose' | 'draw' } | null>(null);
|
||||
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
draws: 0
|
||||
});
|
||||
|
||||
// 生成电脑的选择
|
||||
const generateComputerChoice = () => {
|
||||
const randomIndex = Math.floor(Math.random() * choices.length);
|
||||
return choices[randomIndex].value;
|
||||
};
|
||||
|
||||
// 判断游戏结果
|
||||
const determineWinner = (player: string, computer: string) => {
|
||||
if (player === computer) {
|
||||
return { message: '平局!', result: 'draw' as const };
|
||||
}
|
||||
|
||||
if (
|
||||
(player === 'rock' && computer === 'scissors') ||
|
||||
(player === 'scissors' && computer === 'paper') ||
|
||||
(player === 'paper' && computer === 'rock')
|
||||
) {
|
||||
return { message: '你赢了!', result: 'win' as const };
|
||||
}
|
||||
|
||||
return { message: '你输了!', result: 'lose' as const };
|
||||
};
|
||||
|
||||
// 玩家选择
|
||||
const makeChoice = (choice: string) => {
|
||||
if (gameResult.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedChoice.value = choice;
|
||||
computerChoice.value = generateComputerChoice();
|
||||
gameResult.value = determineWinner(choice, computerChoice.value);
|
||||
|
||||
// 更新统计
|
||||
stats.value.total++;
|
||||
if (gameResult.value.result === 'win') {
|
||||
stats.value.wins++;
|
||||
} else if (gameResult.value.result === 'lose') {
|
||||
stats.value.losses++;
|
||||
} else {
|
||||
stats.value.draws++;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置游戏
|
||||
const resetGame = () => {
|
||||
selectedChoice.value = '';
|
||||
computerChoice.value = '';
|
||||
gameResult.value = null;
|
||||
};
|
||||
|
||||
// 重置统计
|
||||
const resetStats = () => {
|
||||
stats.value = {
|
||||
total: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
draws: 0
|
||||
};
|
||||
resetGame();
|
||||
};
|
||||
|
||||
// 获取选择的名称
|
||||
const getChoiceName = (choice: string) => {
|
||||
const found = choices.find(c => c.value === choice);
|
||||
return found ? found.name : '';
|
||||
};
|
||||
|
||||
// 获取选择的图标
|
||||
const getChoiceIcon = (choice: string) => {
|
||||
const found = choices.find(c => c.value === choice);
|
||||
return found ? found.icon : '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.game-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.player-section, .computer-section {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.choice-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.choice-item:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 10px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.choice-item.active {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
box-shadow: 0 0 10px rgba(64, 158, 255, 0.5);
|
||||
}
|
||||
|
||||
.choice-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.choice-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.computer-choice {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.computer-choice .choice-item {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
padding: 40px;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
border: 2px dashed #e4e7ed;
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
padding: 30px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.result-message {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-details {
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.player-result, .computer-result {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.no-result {
|
||||
padding: 40px;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item.win {
|
||||
background-color: #f0f9eb;
|
||||
border: 1px solid #c2e7b0;
|
||||
}
|
||||
|
||||
.stat-item.lose {
|
||||
background-color: #fef0f0;
|
||||
border: 1px solid #fbc4c4;
|
||||
}
|
||||
|
||||
.stat-item.draw {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.game-section {
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.player-section, .computer-section {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
import { format } from 'sql-formatter'
|
||||
|
||||
const title = "SQL格式化"
|
||||
|
||||
// 状态管理
|
||||
const inputText = ref('')
|
||||
const outputText = ref('')
|
||||
const indentSize = ref(4)
|
||||
|
||||
// 格式化SQL
|
||||
const formatSql = () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要格式化的SQL')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用sql-formatter库格式化SQL
|
||||
const formatted = format(inputText.value)
|
||||
outputText.value = formatted
|
||||
ElMessage.success('格式化成功')
|
||||
} catch (error) {
|
||||
outputText.value = '格式化失败'
|
||||
ElMessage.error('格式化失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩SQL
|
||||
const minifySql = () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要压缩的SQL')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 压缩SQL(去除空格和换行)
|
||||
const minified = inputText.value
|
||||
.replace(/\s+/g, ' ') // 替换多个空白字符为单个空格
|
||||
.trim()
|
||||
|
||||
outputText.value = minified
|
||||
ElMessage.success('压缩成功')
|
||||
} catch (error) {
|
||||
outputText.value = '压缩失败'
|
||||
ElMessage.error('压缩失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!outputText.value || outputText.value === '格式化失败' || outputText.value === '压缩失败') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(outputText.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
outputText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要格式化的SQL"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<el-text>缩进大小:</el-text>
|
||||
<el-input-number v-model="indentSize" :min="1" :max="8" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="formatSql">格式化</el-button>
|
||||
<el-button type="success" @click="minifySql">压缩</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
v-model="outputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="格式化结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线SQL格式化工具,用于美化和压缩SQL代码,支持自定义缩进大小,使SQL代码更易于阅读和维护。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "数据存储单位换算"
|
||||
|
||||
// 状态管理
|
||||
const inputValue = ref('')
|
||||
const fromUnit = ref('B')
|
||||
const toUnit = ref('KB')
|
||||
|
||||
// 存储单位选项
|
||||
const unitOptions = [
|
||||
{ label: 'bit (比特)', value: 'bit' },
|
||||
{ label: 'B (字节)', value: 'B' },
|
||||
{ label: 'KB (千字节)', value: 'KB' },
|
||||
{ label: 'MB (兆字节)', value: 'MB' },
|
||||
{ label: 'GB (吉字节)', value: 'GB' },
|
||||
{ label: 'TB (太字节)', value: 'TB' },
|
||||
{ label: 'PB (拍字节)', value: 'PB' },
|
||||
{ label: 'EB (艾字节)', value: 'EB' }
|
||||
]
|
||||
|
||||
// 单位转换系数
|
||||
const unitFactors = {
|
||||
'bit': 1,
|
||||
'B': 8, // 1字节 = 8比特
|
||||
'KB': 8 * 1024,
|
||||
'MB': 8 * 1024 * 1024,
|
||||
'GB': 8 * 1024 * 1024 * 1024,
|
||||
'TB': 8 * 1024 * 1024 * 1024 * 1024,
|
||||
'PB': 8 * 1024 * 1024 * 1024 * 1024 * 1024,
|
||||
'EB': 8 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
|
||||
}
|
||||
|
||||
// 单个转换结果
|
||||
const convertedValue = computed(() => {
|
||||
if (!inputValue.value) return ''
|
||||
|
||||
const value = parseFloat(inputValue.value)
|
||||
if (isNaN(value)) return '无效输入'
|
||||
|
||||
try {
|
||||
// 先转换为字节
|
||||
const bytes = value * unitFactors[fromUnit.value]
|
||||
// 再转换为目标单位
|
||||
const result = bytes / unitFactors[toUnit.value]
|
||||
|
||||
// 检查小数点后的位数
|
||||
const resultStr = result.toString()
|
||||
const decimalIndex = resultStr.indexOf('.')
|
||||
|
||||
if (decimalIndex !== -1 && resultStr.length - decimalIndex - 1 > 5) {
|
||||
// 小数点超过5位,使用科学计数法
|
||||
return result.toExponential(5)
|
||||
} else {
|
||||
// 否则使用固定小数位
|
||||
return result.toFixed(6)
|
||||
}
|
||||
} catch (error) {
|
||||
return '转换失败'
|
||||
}
|
||||
})
|
||||
|
||||
// 所有单位的转换结果
|
||||
const allConvertedValues = computed(() => {
|
||||
if (!inputValue.value) return []
|
||||
|
||||
const value = parseFloat(inputValue.value)
|
||||
if (isNaN(value)) return []
|
||||
|
||||
try {
|
||||
// 先转换为字节
|
||||
const bytes = value * unitFactors[fromUnit.value]
|
||||
|
||||
// 计算所有单位的转换结果
|
||||
return unitOptions.map(option => {
|
||||
const result = bytes / unitFactors[option.value]
|
||||
|
||||
// 检查小数点后的位数
|
||||
const resultStr = result.toString()
|
||||
const decimalIndex = resultStr.indexOf('.')
|
||||
|
||||
let formattedResult
|
||||
if (decimalIndex !== -1 && resultStr.length - decimalIndex - 1 > 5) {
|
||||
// 小数点超过5位,使用科学计数法
|
||||
formattedResult = result.toExponential(5)
|
||||
} else {
|
||||
// 否则使用固定小数位
|
||||
formattedResult = result.toFixed(6)
|
||||
}
|
||||
|
||||
return {
|
||||
unit: option.label,
|
||||
value: formattedResult
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!convertedValue.value || convertedValue.value === '无效输入' || convertedValue.value === '转换失败') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(convertedValue.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputValue.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
type="number"
|
||||
placeholder="请输入数值"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>从单位:</el-text>
|
||||
<el-select v-model="fromUnit" style="width: 140px;">
|
||||
<el-option
|
||||
v-for="option in unitOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<el-text>到单位:</el-text>
|
||||
<el-select v-model="toUnit" style="width: 140px;">
|
||||
<el-option
|
||||
v-for="option in unitOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
:value="convertedValue"
|
||||
placeholder="转换结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 所有单位转换结果 -->
|
||||
<div v-if="allConvertedValues.length > 0">
|
||||
<el-text class="block mb-2">所有单位转换结果:</el-text>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<div v-for="item in allConvertedValues" :key="item.unit" class="bg-gray-50 p-2 rounded">
|
||||
<div class="text-sm text-gray-500">{{ item.unit }}</div>
|
||||
<div class="font-medium">{{ item.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线数据存储单位换算工具,支持比特(bit)、字节(B)、KB、MB、GB、TB、PB、EB等存储单位之间的相互转换。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "文本替换"
|
||||
|
||||
// 状态管理
|
||||
const inputText = ref('')
|
||||
const findText = ref('')
|
||||
const replaceText = ref('')
|
||||
const useRegex = ref(false)
|
||||
const caseSensitive = ref(true)
|
||||
|
||||
// 替换结果
|
||||
const replaceResult = computed(() => {
|
||||
if (!inputText.value) return ''
|
||||
if (!findText.value) return inputText.value
|
||||
|
||||
try {
|
||||
if (useRegex.value) {
|
||||
// 使用正则表达式替换
|
||||
const regex = new RegExp(findText.value, caseSensitive.value ? 'g' : 'gi')
|
||||
return inputText.value.replace(regex, replaceText.value)
|
||||
} else {
|
||||
// 使用普通文本替换
|
||||
if (caseSensitive.value) {
|
||||
return inputText.value.replace(new RegExp(findText.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replaceText.value)
|
||||
} else {
|
||||
return inputText.value.replace(new RegExp(findText.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replaceText.value)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return '替换失败:正则表达式语法错误'
|
||||
}
|
||||
})
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!replaceResult.value || replaceResult.value === '替换失败:正则表达式语法错误') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(replaceResult.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
findText.value = ''
|
||||
replaceText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要处理的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div>
|
||||
<el-input
|
||||
v-model="findText"
|
||||
placeholder="请输入要查找的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<el-input
|
||||
v-model="replaceText"
|
||||
placeholder="请输入要替换的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<el-checkbox v-model="useRegex">使用正则表达式</el-checkbox>
|
||||
<el-checkbox v-model="caseSensitive">区分大小写</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button @click="copyResult">复制结果</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
:value="replaceResult"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="替换结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线文本替换工具,支持普通文本和正则表达式替换,可用于批量修改文本内容,支持区分大小写选项。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowDown, ArrowUp, Delete } from '@element-plus/icons-vue'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "文本处理工作流"
|
||||
|
||||
// 状态管理
|
||||
const inputText = ref('')
|
||||
const workflowSteps = ref([
|
||||
{ id: 1, type: 'trim', name: '去除首尾空格' },
|
||||
{ id: 2, type: 'lowercase', name: '转为小写' }
|
||||
])
|
||||
const outputText = ref('')
|
||||
|
||||
// 可用的处理步骤类型
|
||||
const availableSteps = [
|
||||
{ type: 'trim', name: '去除首尾空格' },
|
||||
{ type: 'lowercase', name: '转为小写' },
|
||||
{ type: 'uppercase', name: '转为大写' },
|
||||
{ type: 'removeEmptyLines', name: '移除空行' },
|
||||
{ type: 'trimLines', name: '去除每行首尾空格' },
|
||||
{ type: 'sort', name: '按字母排序' }
|
||||
]
|
||||
|
||||
// 执行工作流
|
||||
const executeWorkflow = () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要处理的文本')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let result = inputText.value
|
||||
|
||||
// 按顺序执行每个步骤
|
||||
workflowSteps.value.forEach(step => {
|
||||
result = processText(result, step.type)
|
||||
})
|
||||
|
||||
outputText.value = result
|
||||
ElMessage.success('工作流执行成功')
|
||||
} catch (error) {
|
||||
outputText.value = '执行失败'
|
||||
ElMessage.error('执行失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文本
|
||||
const processText = (text: string, type: string): string => {
|
||||
switch (type) {
|
||||
case 'trim':
|
||||
return text.trim()
|
||||
case 'lowercase':
|
||||
return text.toLowerCase()
|
||||
case 'uppercase':
|
||||
return text.toUpperCase()
|
||||
case 'removeEmptyLines':
|
||||
return text.split('\n').filter(line => line.trim() !== '').join('\n')
|
||||
case 'trimLines':
|
||||
return text.split('\n').map(line => line.trim()).join('\n')
|
||||
case 'sort':
|
||||
return text.split('\n').filter(line => line.trim() !== '').sort().join('\n')
|
||||
default:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
// 添加步骤
|
||||
const addStep = (type: string, name: string) => {
|
||||
const newId = Math.max(...workflowSteps.value.map(step => step.id), 0) + 1
|
||||
workflowSteps.value.push({ id: newId, type, name })
|
||||
}
|
||||
|
||||
// 删除步骤
|
||||
const removeStep = (id: number) => {
|
||||
workflowSteps.value = workflowSteps.value.filter(step => step.id !== id)
|
||||
}
|
||||
|
||||
// 上移步骤
|
||||
const moveStepUp = (index: number) => {
|
||||
if (index > 0) {
|
||||
const temp = workflowSteps.value[index]
|
||||
workflowSteps.value[index] = workflowSteps.value[index - 1]
|
||||
workflowSteps.value[index - 1] = temp
|
||||
}
|
||||
}
|
||||
|
||||
// 下移步骤
|
||||
const moveStepDown = (index: number) => {
|
||||
if (index < workflowSteps.value.length - 1) {
|
||||
const temp = workflowSteps.value[index]
|
||||
workflowSteps.value[index] = workflowSteps.value[index + 1]
|
||||
workflowSteps.value[index + 1] = temp
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!outputText.value || outputText.value === '执行失败') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(outputText.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
outputText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入要处理的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 工作流步骤 -->
|
||||
<div class="mb-4">
|
||||
<el-card class="mb-4">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>工作流步骤</span>
|
||||
<el-dropdown>
|
||||
<el-button type="primary" size="small">
|
||||
添加步骤 <el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="step in availableSteps"
|
||||
:key="step.type"
|
||||
@click="addStep(step.type, step.name)"
|
||||
>
|
||||
{{ step.name }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="workflowSteps.length === 0" class="text-center text-gray-500 py-4">
|
||||
暂无步骤,请添加处理步骤
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(step, index) in workflowSteps"
|
||||
:key="step.id"
|
||||
class="flex items-center gap-2 p-2 border rounded"
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="moveStepUp(index)"
|
||||
:disabled="index === 0"
|
||||
>
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="moveStepDown(index)"
|
||||
:disabled="index === workflowSteps.length - 1"
|
||||
>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<div class="flex-1">{{ step.name }}</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeStep(step.id)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="executeWorkflow">执行工作流</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
v-model="outputText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="处理结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线文本处理工作流工具,允许用户定义一系列文本处理步骤并按顺序执行,可用于复杂的文本处理任务,提高工作效率。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="input-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="转盘选项(每行一个)">
|
||||
<el-input
|
||||
v-model="optionsText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入转盘选项,每行一个"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="updateWheel">更新转盘</el-button>
|
||||
<el-button @click="clearOptions">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="wheel-section">
|
||||
<div class="wheel-wrapper">
|
||||
<div
|
||||
ref="wheel"
|
||||
class="wheel"
|
||||
:style="wheelStyle"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="wheel-sector"
|
||||
:style="getSectorStyle(index)"
|
||||
>
|
||||
<div class="sector-text">{{ option }}</div>
|
||||
</div>
|
||||
<div class="wheel-center"></div>
|
||||
</div>
|
||||
<div class="wheel-pointer"></div>
|
||||
</div>
|
||||
|
||||
<div class="wheel-controls">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="isSpinning || options.length < 2"
|
||||
@click="spinWheel"
|
||||
>
|
||||
{{ isSpinning ? '旋转中...' : '开始旋转' }}
|
||||
</el-button>
|
||||
<div v-if="result" class="result">
|
||||
结果: {{ result }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线转盘工具,自定义选项后旋转转盘随机选择结果,支持多个选项和动画效果,可用于随机选择、抽奖等场景。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "转盘工具"
|
||||
|
||||
const optionsText = ref('');
|
||||
const options = ref<string[]>([]);
|
||||
const isSpinning = ref(false);
|
||||
const result = ref<string>('');
|
||||
const wheel = ref<HTMLElement | null>(null);
|
||||
const rotation = ref(0);
|
||||
|
||||
// 颜色列表
|
||||
const colors = [
|
||||
'#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57',
|
||||
'#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3', '#ff9f43'
|
||||
];
|
||||
|
||||
// 解析选项
|
||||
const parseOptions = () => {
|
||||
const newOptions = optionsText.value
|
||||
.split('\n')
|
||||
.map(option => option.trim())
|
||||
.filter(option => option !== '');
|
||||
options.value = newOptions;
|
||||
};
|
||||
|
||||
// 更新转盘
|
||||
const updateWheel = () => {
|
||||
parseOptions();
|
||||
result.value = '';
|
||||
};
|
||||
|
||||
// 清空选项
|
||||
const clearOptions = () => {
|
||||
optionsText.value = '';
|
||||
options.value = [];
|
||||
result.value = '';
|
||||
};
|
||||
|
||||
// 转盘样式
|
||||
const wheelStyle = computed(() => {
|
||||
return {
|
||||
transform: `rotate(${rotation.value}deg)`
|
||||
};
|
||||
});
|
||||
|
||||
// 获取扇形样式
|
||||
const getSectorStyle = (index: number) => {
|
||||
const count = options.value.length;
|
||||
if (count === 0) return {};
|
||||
|
||||
const angle = 360 / count;
|
||||
const startAngle = index * angle;
|
||||
const endAngle = (index + 1) * angle;
|
||||
|
||||
// 计算扇形路径
|
||||
const radius = 150;
|
||||
const centerX = radius;
|
||||
const centerY = radius;
|
||||
|
||||
const startX = centerX + radius * Math.cos((startAngle - 90) * Math.PI / 180);
|
||||
const startY = centerY + radius * Math.sin((startAngle - 90) * Math.PI / 180);
|
||||
const endX = centerX + radius * Math.cos((endAngle - 90) * Math.PI / 180);
|
||||
const endY = centerY + radius * Math.sin((endAngle - 90) * Math.PI / 180);
|
||||
|
||||
const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
|
||||
|
||||
const path = `M ${centerX} ${centerY} L ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY} Z`;
|
||||
|
||||
return {
|
||||
backgroundColor: colors[index % colors.length],
|
||||
clipPath: `polygon(50% 50%, ${startX}px ${startY}px, ${endX}px ${endY}px)`
|
||||
};
|
||||
};
|
||||
|
||||
// 旋转转盘
|
||||
const spinWheel = () => {
|
||||
if (isSpinning.value || options.value.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSpinning.value = true;
|
||||
result.value = '';
|
||||
|
||||
// 随机旋转角度(3-5圈)
|
||||
const randomRotation = 360 * (3 + Math.random() * 2);
|
||||
const finalRotation = rotation.value + randomRotation;
|
||||
|
||||
// 计算最终指向的选项
|
||||
const anglePerOption = 360 / options.value.length;
|
||||
const normalizedRotation = finalRotation % 360;
|
||||
const selectedIndex = Math.floor((360 - normalizedRotation) / anglePerOption) % options.value.length;
|
||||
|
||||
// 执行旋转动画
|
||||
const duration = 3000 + Math.random() * 2000; // 3-5秒
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeOut = 1 - Math.pow(1 - progress, 3);
|
||||
rotation.value = rotation.value + randomRotation * easeOut;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
// 旋转结束
|
||||
isSpinning.value = false;
|
||||
result.value = options.value[selectedIndex];
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// 初始示例
|
||||
optionsText.value = '选项1\n选项2\n选项3\n选项4\n选项5';
|
||||
parseOptions();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wheel-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wheel-wrapper {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.wheel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.wheel-sector {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sector-text {
|
||||
max-width: 80px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.wheel-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.wheel-pointer {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 15px solid transparent;
|
||||
border-right: 15px solid transparent;
|
||||
border-bottom: 25px solid #ff6b6b;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.wheel-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.result {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 10px;
|
||||
padding: 10px 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.wheel-section {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<div class="tool-content">
|
||||
<div class="input-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="输入文本">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要生成词云的文本"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="词云设置">
|
||||
<div class="settings-grid">
|
||||
<el-form-item label="形状" class="setting-item">
|
||||
<el-select v-model="shape" size="small">
|
||||
<el-option label="圆形" value="circle" />
|
||||
<el-option label="矩形" value="rect" />
|
||||
<el-option label="三角形" value="triangle" />
|
||||
<el-option label="心形" value="heart" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="颜色方案" class="setting-item">
|
||||
<el-select v-model="colorScheme" size="small">
|
||||
<el-option label="默认" value="default" />
|
||||
<el-option label="暖色" value="warm" />
|
||||
<el-option label="冷色" value="cool" />
|
||||
<el-option label="多彩" value="multi" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字体大小范围" class="setting-item">
|
||||
<div class="size-range">
|
||||
<el-input-number v-model="minFontSize" :min="10" :max="50" size="small" />
|
||||
<span class="range-separator">-</span>
|
||||
<el-input-number v-model="maxFontSize" :min="20" :max="100" size="small" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="generateWordCloud">生成词云</el-button>
|
||||
<el-button @click="downloadWordCloud">下载图片</el-button>
|
||||
<el-button @click="clearText">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线词云生成工具,根据输入文本生成词云图,支持自定义词云形状、颜色方案和字体大小范围,可用于文本分析和可视化。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import 'echarts-wordcloud';
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
|
||||
const title = "词云生成"
|
||||
|
||||
const inputText = ref('');
|
||||
const shape = ref('circle');
|
||||
const colorScheme = ref('default');
|
||||
const minFontSize = ref(12);
|
||||
const maxFontSize = ref(60);
|
||||
const chartContainer = ref<HTMLElement | null>(null);
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
// 颜色方案
|
||||
const colorSchemes = {
|
||||
default: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
|
||||
warm: ['#ff7f50', '#87ceeb', '#da70d6', '#32cd32', '#6495ed', '#ff69b4', '#ba55d3', '#cd5c5c', '#ffa500'],
|
||||
cool: ['#00bfff', '#9370db', '#3cb371', '#ff6347', '#4682b4', '#ff69b4', '#32cd32', '#ff4500', '#9400d3'],
|
||||
multi: ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#8b00ff', '#ff00ff', '#1e90ff']
|
||||
};
|
||||
|
||||
// 词频统计函数
|
||||
const getWordFrequency = (text: string) => {
|
||||
// 简单的词频统计,实际应用中可能需要更复杂的分词
|
||||
const words = text
|
||||
.toLowerCase()
|
||||
.replace(/[.,?!;:()\[\]{}]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 1);
|
||||
|
||||
const frequency: Record<string, number> = {};
|
||||
words.forEach(word => {
|
||||
frequency[word] = (frequency[word] || 0) + 1;
|
||||
});
|
||||
|
||||
return Object.entries(frequency)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 100); // 只取前100个高频词
|
||||
};
|
||||
|
||||
// 生成词云
|
||||
const generateWordCloud = () => {
|
||||
if (!inputText.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const words = getWordFrequency(inputText.value);
|
||||
if (words.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (chartContainer.value) {
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
}
|
||||
|
||||
chart = echarts.init(chartContainer.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {},
|
||||
series: [
|
||||
{
|
||||
type: 'wordCloud',
|
||||
shape: shape.value,
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
right: null,
|
||||
bottom: null,
|
||||
sizeRange: [minFontSize.value, maxFontSize.value],
|
||||
rotationRange: [-45, 45],
|
||||
rotationStep: 45,
|
||||
gridSize: 8,
|
||||
drawOutOfBound: false,
|
||||
textStyle: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'bold',
|
||||
color: function () {
|
||||
return colorSchemes[colorScheme.value as keyof typeof colorSchemes][Math.floor(Math.random() * colorSchemes[colorScheme.value as keyof typeof colorSchemes].length)];
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'self',
|
||||
textStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#333'
|
||||
}
|
||||
},
|
||||
data: words
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 清空文本
|
||||
const clearText = () => {
|
||||
inputText.value = '';
|
||||
if (chart) {
|
||||
chart.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// 下载词云图片
|
||||
const downloadWordCloud = () => {
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取图片数据 URL
|
||||
const dataURL = chart.getDataURL({
|
||||
type: 'png',
|
||||
pixelRatio: 2, // 提高图片质量
|
||||
backgroundColor: '#fff'
|
||||
});
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
link.download = `wordcloud-${Date.now()}.png`;
|
||||
link.href = dataURL;
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error('下载词云图片失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 响应式调整
|
||||
const handleResize = () => {
|
||||
if (chart) {
|
||||
chart.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
// 示例文本
|
||||
inputText.value = '词云 生成 工具 在线 文本 分析 可视化 数据 图表 形状 颜色 字体 大小 频率 统计 单词 词组 自定义 圆形 矩形 三角形 心形 暖色 冷色 多彩';
|
||||
generateWordCloud();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.size-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.range-separator {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tool-content {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "词频统计"
|
||||
|
||||
// 状态管理
|
||||
const inputText = ref('')
|
||||
const minWordLength = ref(2)
|
||||
const frequencyResult = ref([] as { word: string; count: number }[])
|
||||
|
||||
// 统计词频
|
||||
const calculateFrequency = () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要统计词频的文本')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const frequency = countWordFrequency(inputText.value, minWordLength.value)
|
||||
frequencyResult.value = frequency
|
||||
ElMessage.success('统计完成')
|
||||
} catch (error) {
|
||||
ElMessage.error('统计失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算词频
|
||||
const countWordFrequency = (text: string, minLength: number) => {
|
||||
// 分词
|
||||
const words = tokenize(text)
|
||||
|
||||
// 过滤长度
|
||||
const filteredWords = words.filter(word => word.length >= minLength)
|
||||
|
||||
// 统计频率
|
||||
const frequencyMap = new Map<string, number>()
|
||||
filteredWords.forEach(word => {
|
||||
frequencyMap.set(word, (frequencyMap.get(word) || 0) + 1)
|
||||
})
|
||||
|
||||
// 转换为数组并排序
|
||||
const frequencyArray = Array.from(frequencyMap.entries())
|
||||
.map(([word, count]) => ({ word, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
|
||||
return frequencyArray
|
||||
}
|
||||
|
||||
// 文本分词
|
||||
const tokenize = (text: string): string[] => {
|
||||
// 简单的分词实现,去除标点符号,转为小写
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[.,?!;:"'()\[\]{}]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 0)
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (frequencyResult.value.length === 0) {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
const resultText = frequencyResult.value
|
||||
.map(item => `${item.word}: ${item.count}`)
|
||||
.join('\n')
|
||||
|
||||
copy(resultText)
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
frequencyResult.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要统计词频的文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<el-text>最小单词长度:</el-text>
|
||||
<el-input-number v-model="minWordLength" :min="1" :max="10" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="calculateFrequency">统计词频</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div v-if="frequencyResult.length > 0" class="mb-4">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-bold">统计结果</div>
|
||||
<el-button size="small" @click="copyResult">复制结果</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="frequencyResult" style="width: 100%">
|
||||
<el-table-column prop="word" label="单词" width="200" />
|
||||
<el-table-column prop="count" label="出现次数" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线词频统计工具,用于统计文本中单词出现的频率,可用于文本分析、关键词提取等场景,支持设置最小单词长度。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue'
|
||||
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copy } from '@/utils/string'
|
||||
|
||||
const title = "XML格式化"
|
||||
|
||||
// 状态管理
|
||||
const inputText = ref('')
|
||||
const outputText = ref('')
|
||||
const indentSize = ref(4)
|
||||
|
||||
// 格式化XML
|
||||
const formatXml = () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要格式化的XML文本')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用DOMParser解析XML
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(inputText.value, 'text/xml')
|
||||
|
||||
// 检查解析是否成功
|
||||
const errorNode = xmlDoc.querySelector('parsererror')
|
||||
if (errorNode) {
|
||||
throw new Error('XML解析错误: ' + errorNode.textContent)
|
||||
}
|
||||
|
||||
// 格式化XML
|
||||
const formatted = formatXmlNode(xmlDoc.documentElement, 0)
|
||||
outputText.value = formatted
|
||||
ElMessage.success('格式化成功')
|
||||
} catch (error) {
|
||||
outputText.value = '格式化失败'
|
||||
ElMessage.error('格式化失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 递归格式化XML节点
|
||||
const formatXmlNode = (node: Node, indent: number): string => {
|
||||
const indentStr = ' '.repeat(indent * indentSize.value)
|
||||
let result = ''
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
result += `${indentStr}<${node.nodeName}`
|
||||
|
||||
// 处理属性
|
||||
if ((node as Element).attributes && (node as Element).attributes.length > 0) {
|
||||
for (let i = 0; i < (node as Element).attributes.length; i++) {
|
||||
const attr = (node as Element).attributes[i]
|
||||
result += ` ${attr.name}="${attr.value}"`
|
||||
}
|
||||
}
|
||||
|
||||
if (node.childNodes.length === 0) {
|
||||
// 空标签
|
||||
result += '/>\n'
|
||||
} else {
|
||||
result += '>\n'
|
||||
|
||||
// 处理子节点
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
const childNode = node.childNodes[i]
|
||||
if (childNode.nodeType === Node.TEXT_NODE && childNode.textContent?.trim() === '') {
|
||||
// 跳过空白文本节点
|
||||
continue
|
||||
}
|
||||
result += formatXmlNode(childNode, indent + 1)
|
||||
}
|
||||
|
||||
result += `${indentStr}</${node.nodeName}>\n`
|
||||
}
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent?.trim()
|
||||
if (text) {
|
||||
result += `${indentStr}${text}\n`
|
||||
}
|
||||
} else if (node.nodeType === Node.CDATA_SECTION_NODE) {
|
||||
result += `${indentStr}<![CDATA[${node.textContent}]]>\n`
|
||||
} else if (node.nodeType === Node.COMMENT_NODE) {
|
||||
result += `${indentStr}<!--${node.textContent}-->\n`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 压缩XML
|
||||
const minifyXml = () => {
|
||||
if (!inputText.value) {
|
||||
ElMessage.warning('请输入要压缩的XML文本')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 压缩XML(去除空格和换行)
|
||||
const minified = inputText.value
|
||||
.replace(/\s+/g, ' ') // 替换多个空白字符为单个空格
|
||||
.replace(/\s*<\s*/g, '<') // 去除标签周围的空格
|
||||
.replace(/\s*>\s*/g, '>') // 去除标签周围的空格
|
||||
.trim()
|
||||
|
||||
outputText.value = minified
|
||||
ElMessage.success('压缩成功')
|
||||
} catch (error) {
|
||||
outputText.value = '压缩失败'
|
||||
ElMessage.error('压缩失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const copyResult = () => {
|
||||
if (!outputText.value || outputText.value === '格式化失败' || outputText.value === '压缩失败') {
|
||||
ElMessage.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
copy(outputText.value)
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
inputText.value = ''
|
||||
outputText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col mt-3 flex-1">
|
||||
<DetailHeader :title="title"></DetailHeader>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-white">
|
||||
<!-- 输入部分 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入要格式化的XML文本"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<el-text>缩进大小:</el-text>
|
||||
<el-input-number v-model="indentSize" :min="1" :max="8" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="formatXml">格式化</el-button>
|
||||
<el-button type="success" @click="minifyXml">压缩</el-button>
|
||||
<el-button @click="clearInput">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 结果部分 -->
|
||||
<div>
|
||||
<el-input
|
||||
v-model="outputText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="格式化结果"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyResult">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- desc -->
|
||||
<ToolDetail title="描述">
|
||||
<el-text>
|
||||
在线XML格式化工具,用于美化和压缩XML代码,支持自定义缩进大小,使XML代码更易于阅读和维护。
|
||||
</el-text>
|
||||
</ToolDetail>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -133,6 +133,213 @@ export function getToolsCate() {
|
||||
url: '/cssformat/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'Base64加解密',
|
||||
logo: '/images/logo/Base64.svg',
|
||||
desc: '在线Base64加解密工具,支持文本的Base64编码和解码',
|
||||
url: '/base64/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '进制转换计算器',
|
||||
logo: '/images/logo/scaletran.png',
|
||||
desc: '在线进制转换计算器,支持二进制、八进制、十进制、十六进制等多种进制之间的相互转换',
|
||||
url: '/baseconverter/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '数据存储单位换算',
|
||||
logo: '/images/logo/storageconverter.svg',
|
||||
desc: '在线数据存储单位换算工具,支持字节、KB、MB、GB、TB、PB、EB等存储单位之间的相互转换',
|
||||
url: '/storageconverter/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
// {
|
||||
// id: 1,
|
||||
// title: 'AES加解密',
|
||||
// logo: '/images/logo/md5.png',
|
||||
// desc: '在线AES加解密工具,支持CBC和ECB模式,支持128位、192位、256位密钥长度',
|
||||
// url: '/aes/',
|
||||
// cateId: 2,
|
||||
// cate: '开发运维',
|
||||
// },
|
||||
{
|
||||
id: 1,
|
||||
title: 'Hash计算器',
|
||||
logo: '/images/logo/hashcalculator.svg',
|
||||
desc: '在线Hash计算器,支持MD5、SHA-1、SHA-256、SHA-384、SHA-512等多种哈希算法',
|
||||
url: '/hashcalculator/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'SHA1加解密',
|
||||
logo: '/images/logo/sha1.svg',
|
||||
desc: '在线SHA1哈希计算器,用于计算文本的SHA1哈希值',
|
||||
url: '/sha1/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'XML格式化',
|
||||
logo: '/images/logo/xmlformat.svg',
|
||||
desc: '在线XML格式化工具,用于美化和压缩XML代码,支持自定义缩进大小',
|
||||
url: '/xmlformat/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'SQL格式化',
|
||||
logo: '/images/logo/sqlformat.svg',
|
||||
desc: '在线SQL格式化工具,用于美化和压缩SQL代码,支持自定义缩进大小',
|
||||
url: '/sqlformat/',
|
||||
cateId: 2,
|
||||
cate: '开发运维',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '文本替换',
|
||||
logo: '/images/logo/textreplace.svg',
|
||||
desc: '在线文本替换工具,支持普通文本和正则表达式替换,可用于批量修改文本内容',
|
||||
url: '/textreplace/',
|
||||
cateId: 3,
|
||||
cate: '文本处理',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '文本处理工作流',
|
||||
logo: '/images/logo/textworkflow.svg',
|
||||
desc: '在线文本处理工作流工具,允许用户定义一系列文本处理步骤并按顺序执行',
|
||||
url: '/textworkflow/',
|
||||
cateId: 3,
|
||||
cate: '文本处理',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'Emoji表情大全',
|
||||
logo: '/images/logo/emoji.svg',
|
||||
desc: '在线Emoji表情大全,提供各种分类的Emoji表情,点击即可复制到剪贴板',
|
||||
url: '/emoji/',
|
||||
cateId: 7,
|
||||
cate: '其他工具',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '词频统计',
|
||||
logo: '/images/logo/wordfrequency.svg',
|
||||
desc: '在线词频统计工具,用于统计文本中单词出现的频率,可用于文本分析、关键词提取等场景',
|
||||
url: '/wordfrequency/',
|
||||
cateId: 3,
|
||||
cate: '文本处理',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '词云生成',
|
||||
logo: '/images/logo/wordcloud.svg',
|
||||
desc: '根据输入文本生成词云图,支持自定义词云形状和颜色',
|
||||
url: '/wordcloud/',
|
||||
cateId: 3,
|
||||
cate: '文本处理',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '抽签工具',
|
||||
logo: '/images/logo/lottery.svg',
|
||||
desc: '输入多个选项,随机抽取一个或多个结果',
|
||||
url: '/lottery/',
|
||||
cateId: 9,
|
||||
cate: '选择随机',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '剪刀石头布',
|
||||
logo: '/images/logo/rockpaperscissors.svg',
|
||||
desc: '与电脑对战的剪刀石头布游戏',
|
||||
url: '/rockpaperscissors/',
|
||||
cateId: 9,
|
||||
cate: '选择随机',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '转盘工具',
|
||||
logo: '/images/logo/wheel.svg',
|
||||
desc: '自定义选项,旋转转盘随机选择结果',
|
||||
url: '/wheel/',
|
||||
cateId: 9,
|
||||
cate: '选择随机',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '传图取色',
|
||||
logo: '/images/logo/imagecolorpicker.svg',
|
||||
desc: '上传图片并点击图片获取颜色值',
|
||||
url: '/imagecolorpicker/',
|
||||
cateId: 5,
|
||||
cate: '图片处理',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '番茄时钟',
|
||||
logo: '/images/logo/pomodoro.svg',
|
||||
desc: '专注工作和休息的时间管理工具',
|
||||
url: '/pomodoro/',
|
||||
cateId: 7,
|
||||
cate: '其他工具',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '简易计算器',
|
||||
logo: '/images/logo/calculator.svg',
|
||||
desc: '基本的加减乘除计算工具',
|
||||
url: '/calculator/',
|
||||
cateId: 7,
|
||||
cate: '其他工具',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '反应速度测试',
|
||||
logo: '/images/logo/JSForamt.png',
|
||||
desc: '测试你的反应速度,点击变色的方块',
|
||||
url: '/reactiontest/',
|
||||
cateId: 7,
|
||||
cate: '其他工具',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'm3u8在线播放',
|
||||
logo: '/images/logo/JSForamt.png',
|
||||
desc: '播放m3u8格式的视频流',
|
||||
url: '/m3u8player/',
|
||||
cateId: 7,
|
||||
cate: '其他工具',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '图片水印添加',
|
||||
logo: '/images/logo/JSForamt.png',
|
||||
desc: '上传图片并添加文字水印',
|
||||
url: '/imagewatermark/',
|
||||
cateId: 5,
|
||||
cate: '图片处理',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '色板工具',
|
||||
logo: '/images/logo/JSForamt.png',
|
||||
desc: '提供各种颜色的色板,点击颜色可复制颜色值',
|
||||
url: '/colorpalette/',
|
||||
cateId: 7,
|
||||
cate: '其他工具',
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -431,6 +431,227 @@ export const constantRoute = [
|
||||
description: '在线富文本编辑, html实时预览,在线编辑文本,文本编辑获取html',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/base64',
|
||||
component: () => import('@/components/Tools/Base64/Base64.vue'),
|
||||
name: 'Base64',
|
||||
meta: {
|
||||
title: "Base64加解密",
|
||||
keywords: 'Base64,base64加密,base64解密,base64编码,base64解码',
|
||||
description: '在线Base64加解密工具,支持文本的Base64编码和解码',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/baseconverter',
|
||||
component: () => import('@/components/Tools/BaseConverter/BaseConverter.vue'),
|
||||
name: 'baseconverter',
|
||||
meta: {
|
||||
title: "进制转换计算器",
|
||||
keywords: '进制转换,二进制,八进制,十进制,十六进制,进制计算器',
|
||||
description: '在线进制转换计算器,支持二进制、八进制、十进制、十六进制等多种进制之间的相互转换',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/storageconverter',
|
||||
component: () => import('@/components/Tools/StorageConverter/StorageConverter.vue'),
|
||||
name: 'storageconverter',
|
||||
meta: {
|
||||
title: "数据存储单位换算",
|
||||
keywords: '存储单位换算,字节,KB,MB,GB,TB,PB,EB',
|
||||
description: '在线数据存储单位换算工具,支持字节、KB、MB、GB、TB、PB、EB等存储单位之间的相互转换',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/aes',
|
||||
component: () => import('@/components/Tools/AES/AES.vue'),
|
||||
name: 'aes',
|
||||
meta: {
|
||||
title: "AES加解密",
|
||||
keywords: 'AES加密,AES解密,加密工具,解密工具',
|
||||
description: '在线AES加解密工具,支持CBC和ECB模式,支持128位、192位、256位密钥长度',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/hashcalculator',
|
||||
component: () => import('@/components/Tools/HashCalculator/HashCalculator.vue'),
|
||||
name: 'hashcalculator',
|
||||
meta: {
|
||||
title: "Hash计算器",
|
||||
keywords: 'Hash计算器,MD5,SHA-1,SHA-256,SHA-384,SHA-512',
|
||||
description: '在线Hash计算器,支持MD5、SHA-1、SHA-256、SHA-384、SHA-512等多种哈希算法',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/xmlformat',
|
||||
component: () => import('@/components/Tools/XmlFormat/XmlFormat.vue'),
|
||||
name: 'xmlformat',
|
||||
meta: {
|
||||
title: "XML格式化",
|
||||
keywords: 'XML格式化,XML压缩,XML美化',
|
||||
description: '在线XML格式化工具,用于美化和压缩XML代码,支持自定义缩进大小',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/sqlformat',
|
||||
component: () => import('@/components/Tools/SqlFormat/SqlFormat.vue'),
|
||||
name: 'sqlformat',
|
||||
meta: {
|
||||
title: "SQL格式化",
|
||||
keywords: 'SQL格式化,SQL压缩,SQL美化',
|
||||
description: '在线SQL格式化工具,用于美化和压缩SQL代码,支持自定义缩进大小',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/textreplace',
|
||||
component: () => import('@/components/Tools/TextReplace/TextReplace.vue'),
|
||||
name: 'textreplace',
|
||||
meta: {
|
||||
title: "文本替换",
|
||||
keywords: '文本替换,正则表达式替换,批量替换',
|
||||
description: '在线文本替换工具,支持普通文本和正则表达式替换,可用于批量修改文本内容',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/textworkflow',
|
||||
component: () => import('@/components/Tools/TextWorkflow/TextWorkflow.vue'),
|
||||
name: 'textworkflow',
|
||||
meta: {
|
||||
title: "文本处理工作流",
|
||||
keywords: '文本处理,工作流,批量处理',
|
||||
description: '在线文本处理工作流工具,允许用户定义一系列文本处理步骤并按顺序执行',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/emoji',
|
||||
component: () => import('@/components/Tools/Emoji/Emoji.vue'),
|
||||
name: 'emoji',
|
||||
meta: {
|
||||
title: "Emoji表情大全",
|
||||
keywords: 'Emoji,表情符号,表情大全',
|
||||
description: '在线Emoji表情大全,提供各种分类的Emoji表情,点击即可复制到剪贴板',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/wordfrequency',
|
||||
component: () => import('@/components/Tools/WordFrequency/WordFrequency.vue'),
|
||||
name: 'wordfrequency',
|
||||
meta: {
|
||||
title: "词频统计",
|
||||
keywords: '词频统计,文本分析,关键词提取',
|
||||
description: '在线词频统计工具,用于统计文本中单词出现的频率,可用于文本分析、关键词提取等场景',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/wordcloud',
|
||||
component: () => import('@/components/Tools/WordCloud/WordCloud.vue'),
|
||||
name: 'wordcloud',
|
||||
meta: {
|
||||
title: "词云生成",
|
||||
keywords: '词云,文本可视化,词频分析',
|
||||
description: '根据输入文本生成词云图,支持自定义词云形状和颜色',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/lottery',
|
||||
component: () => import('@/components/Tools/Lottery/Lottery.vue'),
|
||||
name: 'lottery',
|
||||
meta: {
|
||||
title: "抽签工具",
|
||||
keywords: '抽签,随机选择,抽奖',
|
||||
description: '输入多个选项,随机抽取一个或多个结果',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/rockpaperscissors',
|
||||
component: () => import('@/components/Tools/RockPaperScissors/RockPaperScissors.vue'),
|
||||
name: 'rockpaperscissors',
|
||||
meta: {
|
||||
title: "剪刀石头布",
|
||||
keywords: '剪刀石头布,游戏,对战',
|
||||
description: '与电脑对战的剪刀石头布游戏',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/wheel',
|
||||
component: () => import('@/components/Tools/Wheel/Wheel.vue'),
|
||||
name: 'wheel',
|
||||
meta: {
|
||||
title: "转盘工具",
|
||||
keywords: '转盘,随机选择,抽奖',
|
||||
description: '自定义选项,旋转转盘随机选择结果',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/imagecolorpicker',
|
||||
component: () => import('@/components/Tools/ImageColorPicker/ImageColorPicker.vue'),
|
||||
name: 'imagecolorpicker',
|
||||
meta: {
|
||||
title: "传图取色",
|
||||
keywords: '图片取色,颜色提取,色彩分析',
|
||||
description: '上传图片并点击图片获取颜色值',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/pomodoro',
|
||||
component: () => import('@/components/Tools/Pomodoro/Pomodoro.vue'),
|
||||
name: 'pomodoro',
|
||||
meta: {
|
||||
title: "番茄时钟",
|
||||
keywords: '番茄工作法,时间管理,专注',
|
||||
description: '专注工作和休息的时间管理工具',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/calculator',
|
||||
component: () => import('@/components/Tools/Calculator/Calculator.vue'),
|
||||
name: 'calculator',
|
||||
meta: {
|
||||
title: "简易计算器",
|
||||
keywords: '计算器,加减乘除,数学计算',
|
||||
description: '基本的加减乘除计算工具',
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/reactiontest',
|
||||
component: () => import('@/components/Tools/ReactionTest/ReactionTest.vue'),
|
||||
name: 'reactiontest',
|
||||
meta: {
|
||||
title: "反应速度测试",
|
||||
keywords: '反应速度,测试,游戏',
|
||||
description: '测试你的反应速度,点击变色的方块',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/m3u8player',
|
||||
component: () => import('@/components/Tools/M3U8Player/M3U8Player.vue'),
|
||||
name: 'm3u8player',
|
||||
meta: {
|
||||
title: "m3u8在线播放",
|
||||
keywords: 'm3u8,视频播放,流媒体',
|
||||
description: '播放m3u8格式的视频流',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/imagewatermark',
|
||||
component: () => import('@/components/Tools/ImageWatermark/ImageWatermark.vue'),
|
||||
name: 'imagewatermark',
|
||||
meta: {
|
||||
title: "图片水印添加",
|
||||
keywords: '图片水印,水印添加,图片处理',
|
||||
description: '上传图片并添加文字水印',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/colorpalette',
|
||||
component: () => import('@/components/Tools/ColorPalette/ColorPalette.vue'),
|
||||
name: 'colorpalette',
|
||||
meta: {
|
||||
title: "色板工具",
|
||||
keywords: '色板,颜色,渐变色,Material Design',
|
||||
description: '提供各种颜色的色板,点击颜色可复制颜色值',
|
||||
}
|
||||
},
|
||||
// 关于
|
||||
{
|
||||
path: '/about',
|
||||
|
||||
Reference in New Issue
Block a user