feat: 新增工具

This commit is contained in:
陶然
2026-03-02 15:07:32 +08:00
parent 071d7fb5e1
commit 0701b6a286
46 changed files with 13053 additions and 5327 deletions
+2 -1
View File
@@ -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>
+339
View File
@@ -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位192256位密钥长度可用于数据加密和解密
</el-text>
</ToolDetail>
</div>
</template>
<style scoped>
</style>
+144
View File
@@ -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>
+738
View File
@@ -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计算器支持MD5SHA-1SHA-256SHA-384SHA-512等多种哈希算法以及Hmac-MD5Hmac-SHA1Hmac-SHA256Hmac-SHA384Hmac-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">
支持 JPGPNGGIF 等格式的图片
</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>
在线传图取色工具上传图片后点击图片任意位置获取颜色值支持 HEXRGBHSL 三种颜色格式可用于设计和开发中获取颜色参考
</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">
支持 JPGPNGGIF 等格式的图片
</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';
+206
View File
@@ -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>
+391
View File
@@ -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)KBMBGBTBPBEB等存储单位之间的相互转换
</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>
+307
View File
@@ -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>
+207
View File
@@ -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: '其他工具',
}
]
},