Files
frost-navigation/src/components/Tools/MinecraftUUIDConverter.vue
2022-03-17 12:49:34 +08:00

658 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="tool-page">
<div class="actions">
<div class="title">操作</div>
<div class="content">
<el-button
type="primary"
size="small"
plain
@click="generateRandomUUID()"
>随机生成</el-button>
</div>
</div>
<div class="form">
<div class="title">表单</div>
<div class="content" @input="handleInputEv($event)">
<!-- 十六进制 -->
<div class="content-row content-hex">
<div class="inputs">
<div class="input-label">Hexadecimal: </div>
<div class="input-content">
<span>"</span>
<input
ref="inputHex"
v-model="form.hex"
type="text"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="hex"
placeholder="00000000-0000-0000-0000-000000000000"
spellcheck="false"
:pattern="pattern.hex"
required
/>
<span>"</span>
</div>
</div>
<div class="btns">
<el-button
type="success"
size="mini"
plain
@click="copyToClipboard('hex')"
>复制</el-button>
</div>
</div>
<!-- 高低位 -->
<div class="content-row content-halves">
<div class="inputs">
<div class="input-label">UUID Most: </div>
<div class="input-content">
<input
ref="inputMost"
v-model="form.most"
type="number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="halves"
inputmode="numeric"
spellcheck="false"
:pattern="pattern.number"
required
/>
<span> L</span>
</div>
<div class="input-label">UUID Least: </div>
<div class="input-content">
<input
ref="inputLeast"
v-model="form.least"
type="number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="halves"
inputmode="numeric"
spellcheck="false"
:pattern="pattern.number"
required
/>
<span> L</span>
</div>
</div>
<div class="btns">
<el-button
type="success"
size="mini"
plain
@click="copyToClipboard('halves')"
>复制</el-button>
</div>
</div>
<!-- 整型数组 -->
<div class="content-row content-array">
<div class="inputs" @paste="handlePasteArray($event)">
<div class="input-label">UUID: </div>
<div class="input-content">
<span>[I; </span>
<input
ref="inputArray1"
v-model="form.array['1']"
type="number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="array"
inputmode="numeric"
spellcheck="false"
:pattern="pattern.number"
required
/>
<span>, </span>
<input
ref="inputArray2"
v-model="form.array['2']"
type="number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="array"
inputmode="numeric"
spellcheck="false"
:pattern="pattern.number"
required
/>
<span>, </span>
<input
ref="inputArray3"
v-model="form.array['3']"
type="number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="array"
inputmode="numeric"
spellcheck="false"
:pattern="pattern.number"
required
/>
<span>, </span>
<input
ref="inputArray4"
v-model="form.array['4']"
type="number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-input-type="array"
inputmode="numeric"
spellcheck="false"
:pattern="pattern.number"
required
/>
<span>]</span>
</div>
</div>
<div class="btns">
<el-button
type="success"
size="mini"
plain
@click="copyToClipboard('array')"
>复制</el-button>
</div>
</div>
</div>
</div>
<div class="info">
<div class="title">说明</div>
<div class="content">
<p>可以在UUID的输入框中直接粘贴多组数值</p>
<p>例如<code>1629527812, 47203901, -2056695293, 349258036</code></p>
<p>
<span>资料参考</span>
<el-link
type="primary"
href="https://minecraft.fandom.com/zh/wiki/通用唯一识别码"
target="_blank"
>通用唯一识别码</el-link>
</p>
<p>
<span>算法参考</span>
<el-link
type="primary"
href="https://github.com/AjaxGb/mc-uuid-converter"
target="_blank"
>AjaxGb / mc-uuid-converter</el-link>
</p>
</div>
</div>
<!-- 用于复制文本 -->
<textarea
ref="copyArea"
v-model="copyText"
class="text-copy"
tabindex="-1"
readonly
></textarea>
</div>
</template>
<script>
export default {
name: 'UUIDConverter',
data() {
return {
// 兼容性
isSupported: false,
// 输入框内容
form: {
hex: '00000000-0000-0000-0000-000000000000',
most: 0,
least: 0,
array: { '1': 0, '2': 0, '3': 0, '4': 0 },
},
// 输入框正则
pattern: {
hex: '',
number: '',
},
// 被复制的文本
copyText: '',
/** @type {Uint8Array} */
uuidBytes: null,
/** @type {DataView} */
uuid: null,
}
},
created() {
this.detectFn();
this.initDatas();
},
methods: {
/** 检测功能兼容性 */
detectFn() {
const fnTypes = [
typeof window.BigInt,
typeof window.crypto,
typeof DataView,
typeof Uint8Array,
];
if (fnTypes.indexOf('undefined') > -1) {
this.$message({
duration: 5000,
message: 'The browser does not support the required function.',
type: 'error',
});
return;
}
if (!('setBigUint64' in DataView.prototype)) {
this.$message({
duration: 5000,
message: 'The browser does not support writing and reading 64-bit integers.',
type: 'error',
});
return;
}
this.isSupported = true;
},
/** 初始化数据 */
initDatas() {
if (!this.isSupported) {
return;
}
const uuidBytes = new Uint8Array(16);
const uuid = new DataView(uuidBytes.buffer);
this.uuidBytes = uuidBytes;
this.uuid = uuid;
this.pattern.hex = '([0-9a-fA-F]{1,8}-[0-9a-fA-F]{1,4}-[0-9a-fA-F]{1,4}-[0-9a-fA-F]{1,4}-[0-9a-fA-F]{1,12}|[0-9a-fA-F]{1,32})';
this.pattern.number = '[-+]?\\d+';
},
/**
* 复制结果到剪贴板
*
* @param {string} type 类型
*/
copyToClipboard(type) {
/** @type {HTMLInputElement} */
const el = this.$refs['copyArea'];
const form = this.form;
if (!el) {
return;
}
switch (type) {
case 'array':
this.copyText = `[I; ${Object.values(form.array).join(', ')}]`;
break;
case 'halves':
this.copyText = `UUID Most: ${form.most}L, UUID Least: ${form.least}L`;
break;
case 'hex':
this.copyText = form.hex;
break;
default:
return;
}
this.$nextTick(() => {
el.focus();
el.select();
document.execCommand('copy');
});
},
/** 随机生成 UUID */
generateRandomUUID() {
window.crypto.getRandomValues(this.uuidBytes);
// Set version to 4 (random)
this.uuidBytes[6] = (this.uuidBytes[6] & 0x0f) | (4 << 4);
// Set variant to 1 (LeachSalz)
this.uuidBytes[8] = (this.uuidBytes[8] & 0x3f) | 0x80;
// 更新
this.updateFormDatas();
},
/**
* 处理输入事件
*
* @param {InputEvent} ev 输入事件对象
*/
handleInputEv(ev) {
/** @type {HTMLInputElement} */
const el = ev.target;
const type = el.dataset.inputType;
type && this.updateFormDatas(type);
},
/**
* 处理粘贴数组事件
*
* @param {ClipboardEvent} ev 剪贴板事件对象
*/
handlePasteArray(ev) {
/** @type {HTMLInputElement} */
const el = ev.target;
const type = el.dataset.inputType;
if (type !== 'array') {
return;
}
const data = ev.clipboardData;
const values = this.form.array;
if (!data) {
return;
}
for (const item of data.items) {
// 纯文本
if (item.kind === 'string' && item.type.startsWith('text/plain')) {
// 读取
item.getAsString((text) => {
const length = 4;
const numbers = text.match(/[+-]?\d+/g);
if (numbers && numbers.length === length) {
for (let i = 0; i < length; i++) {
values[String(i + 1)] = parseInt(numbers[i]);
}
this.$nextTick(() => {
this.updateFormDatas(type);
});
}
});
break;
}
}
},
/**
* 处理整型数组 UUID
*
* @param {string} mode 模式parse、unparse
*/
handleArray(mode) {
/** @type {HTMLInputElement[]} */
const inputs = [];
// 获取元素
for (let i = 1; i <= 4; i++) {
const el = this.$refs[`inputArray${i}`];
inputs.push(el);
}
// 元素不存在
if (inputs.indexOf(undefined) > -1) {
return;
}
const { form, uuid } = this;
if (mode === 'parse') {
for (let i = 0; i < inputs.length; i++) {
const num = Number(form.array[String(i + 1)]);
const offset = i * 4;
if (inputs[i].validity.valid) {
uuid.setInt32(offset, num, false);
}
}
} else if (mode === 'unparse') {
for (let i = 0; i < inputs.length; i++) {
const prop = String(i + 1);
const result = uuid.getInt32(i * 4, false);
form.array[prop] = result;
}
}
},
/**
* 处理高低位 UUID
*
* @param {string} mode 模式parse、unparse
*/
handleHalves(mode) {
/** @type {HTMLInputElement} */
const elMost = this.$refs['inputMost'];
/** @type {HTMLInputElement} */
const elLeast = this.$refs['inputLeast'];
// 元素不存在
if (!(elMost && elLeast)) {
return;
}
const { form, uuid } = this;
if (mode === 'parse') {
if (elMost.validity.valid) {
uuid.setBigInt64(0, window.BigInt(form.most), false);
}
if (elLeast.validity.valid) {
uuid.setBigInt64(8, window.BigInt(form.least), false);
}
} else if (mode === 'unparse') {
form.most = uuid.getBigInt64(0, false);
form.least = uuid.getBigInt64(8, false);
}
},
/**
* 处理十六进制 UUID
*
* @param {string} mode 模式parse、unparse
*/
handleHex(mode) {
/** @type {HTMLInputElement} */
const el = this.$refs['inputHex'];
// 元素不存在
if (!el) {
return;
}
const { form, uuid } = this;
const groupSizes = [8, 4, 4, 4, 12];
if (mode === 'parse') {
if (!el.validity.valid) {
return;
}
const hex = form.hex;
const hexText = (
hex.includes('-') ?
hex.trim()
.split('-')
.map((g, i) => g.padStart(groupSizes[i], '0'))
.join('') :
hex.trim().padStart(32, '0')
);
uuid.setBigUint64(0, window.BigInt('0x' + hexText.substring(0, 16)), false);
uuid.setBigUint64(8, window.BigInt('0x' + hexText.substring(16)), false);
} else if (mode === 'unparse') {
const hexText = (
uuid.getBigUint64(0, false).toString(16).padStart(16, '0') +
uuid.getBigUint64(8, false).toString(16).padStart(16, '0')
);
const groups = [];
let groupStart = 0;
for (const groupSize of groupSizes) {
groups.push(hexText.substring(groupStart, groupStart + groupSize));
groupStart += groupSize;
}
form.hex = groups.join('-');
}
},
/**
* 更新表单数据
*
* @param {string} type 类型
*/
updateFormDatas(type) {
const handlers = {
array: this.handleArray,
halves: this.handleHalves,
hex: this.handleHex,
};
const handler = handlers[type];
// 调用对应的处理函数
handler && handler('parse');
for (let key in handlers) {
if (key !== type) {
handlers[key]('unparse');
}
}
},
},
}
</script>
<style lang="less" scoped>
input[type="number"],
input[type="text"] {
padding: 0.25rem 0.5rem;
border: 0.0625rem solid #CCC;
border-radius: 0.25rem;
outline: none;
font-family: monospace;
font-size: 0.8rem;
line-height: 1.5em;
text-align: right;
&:invalid {
background-color: #9E9E9E;
color: #FFF;
}
}
input[type="number"] {
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
}
}
.form {
font-family: monospace;
.content {
overflow-x: auto;
}
.content-row {
min-width: 37.5rem;
}
}
.content-row {
display: flex;
align-items: center;
padding: 1em 0;
font-size: 0.8rem;
&:not(:last-child) {
border-bottom: 0.0625rem solid #EEE;
}
.inputs {
display: flex;
align-items: center;
flex-grow: 1;
padding: 0.25em 0;
}
.input-label {
flex-shrink: 0;
padding-right: 0.5em;
width: 7em;
font-weight: bold;
text-align: right;
}
.input-content {
flex-shrink: 0;
}
.btns {
flex-shrink: 0;
}
}
.content-hex {
input[type="text"] {
width: 32.5em;
}
}
.content-halves {
input[type="number"] {
width: 12.5em;
}
}
.content-array {
input[type="number"] {
width: 7em;
}
}
.text-copy {
width: 2rem;
height: 1rem;
overflow: hidden;
opacity: 0;
cursor: default;
resize: none;
}
</style>