forked from mirror/minecraft-color-gradient-generator
🎉 first commit!
This commit is contained in:
commit
9098329eb5
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cmn-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="google" content="notranslate" />
|
||||
<title>Minecraft 渐变颜色生成器</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "minecraft-color-gradient-generator",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --force",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"color-stepper": "^2.0.0-beta2",
|
||||
"reset.css": "^2.0.2",
|
||||
"view-ui-plus": "^1.3.14",
|
||||
"vue": "^3.2.47",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"less": "^4.1.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.9",
|
||||
"vue-tsc": "^1.4.2"
|
||||
}
|
||||
}
|
253
src/App.vue
Normal file
253
src/App.vue
Normal file
@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="cg-container">
|
||||
<header class="cg-header">
|
||||
<p>Minecraft 渐变颜色生成器</p>
|
||||
</header>
|
||||
<main class="cg-main">
|
||||
<color-list v-model="gradientColorList" />
|
||||
<div class="cg-content">
|
||||
<section class="cg-content--form">
|
||||
<Input v-model="inputVal" type="textarea" :rows="2" size="large" placeholder="输入欲加渐变的文字..." show-word-limit maxlength="100" />
|
||||
</section>
|
||||
<div class="cg-configration">
|
||||
<Form ref="formInline" :model="formData" :label-width="100" inline>
|
||||
<FormItem>
|
||||
<template #label>
|
||||
<div>
|
||||
字符模式
|
||||
<Tooltip content="客户端发送使用&,服务端发送使用§" placement="top-start">
|
||||
<Icon type="md-alert" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<Switch v-model="formData.charMode">
|
||||
<template #open><span>&</span></template>
|
||||
<template #close><span>§</span></template>
|
||||
</Switch>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<template #label>
|
||||
<div>
|
||||
兼容模式
|
||||
<Tooltip content="仅限服务器不支持&#前缀发送Hex颜色" placement="top-start" max-width="200">
|
||||
<Icon type="md-alert" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<Switch v-model="formData.compatibleMode" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<template #label>
|
||||
<div>
|
||||
移除空格
|
||||
<Tooltip content="移除空格/换行/制表符" placement="top-start">
|
||||
<Icon type="md-alert" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<Switch v-model="formData.clearSpaceChar" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<Card title="预览" class="preview-card">
|
||||
<template #extra>
|
||||
<span style="cursor: pointer" @click="onCopyClickHandler">复制</span>
|
||||
</template>
|
||||
<div class="gradient-bar" :style="`background: linear-gradient(to right, ${gradientColorList.join(', ')});`"></div>
|
||||
<div class="cg-result cg-result--preview">
|
||||
<VNodeComp :content="resultVNodes" />
|
||||
</div>
|
||||
<Divider />
|
||||
<div class="cg-result cg-result--raw" contenteditable>
|
||||
{{ resultVal }}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="cg-footer">
|
||||
"Minecraft" 是 Mojang Synergies AB 的商标
|
||||
<Divider type="vertical" />
|
||||
<a href="https://github.com/tuanzisama" target="_blank">
|
||||
<Icon type="logo-github" />
|
||||
@tuanzisama
|
||||
</a>
|
||||
<Divider type="vertical" />
|
||||
<a
|
||||
href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=_z8lAFy5g4oHy5j68j1Yx-FK6spjSDKd&authKey=oYJwq0XWxSZ9uWnN1VW1om1HD2bwnCD%2BXXrVEk5ciPy9V%2Byp8ywAre89%2F6z52ksC&noverify=0&group_code=202653244"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon type="md-color-wand" />
|
||||
元界MC - 社交×互动×游戏
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, reactive, ref, VNode } from "vue";
|
||||
import ColorList from "./components/ColorList.vue";
|
||||
import { VNode as VNodeComp } from "./utils/vnode";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { genColorGradients } from "./utils/util";
|
||||
|
||||
const inputVal = ref("");
|
||||
const gradientColorList = ref<string[]>([]);
|
||||
const formData = reactive({
|
||||
charMode: true,
|
||||
compatibleMode: false,
|
||||
clearSpaceChar: true,
|
||||
});
|
||||
|
||||
const inputFormatVal = computed<string>(() => {
|
||||
if (formData.clearSpaceChar) return inputVal.value.replace(/\t|\s|\r|\n/g, "");
|
||||
else return inputVal.value;
|
||||
});
|
||||
|
||||
const gradientBarCss = computed<string>(() => {
|
||||
return `linear-gradient(${gradientColorList.value.join(", ")});`;
|
||||
});
|
||||
|
||||
const resultVNodes = computed<VNode | VNode[]>(() => {
|
||||
if (gradientColorList.value.length < 2) {
|
||||
return inputFormatVal.value.split("").map((el) => h("span", el));
|
||||
} else {
|
||||
const gradientArray = genColorGradients(gradientColorList.value, inputFormatVal.value.length);
|
||||
|
||||
return inputFormatVal.value.split("").map((el, index) => {
|
||||
return h("span", { style: { color: gradientArray[index] } }, el);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const resultVal = computed<string>(() => {
|
||||
if (gradientColorList.value.length < 2) {
|
||||
return inputFormatVal.value;
|
||||
} else {
|
||||
const gradientArray = genColorGradients(gradientColorList.value, inputFormatVal.value.length);
|
||||
return inputFormatVal.value.split("").reduce((acc, cur, index) => {
|
||||
const colorChar = formData.charMode ? "&" : "§";
|
||||
if (formData.compatibleMode) {
|
||||
const colorSplit = gradientArray[index]
|
||||
.slice(1)
|
||||
.split("")
|
||||
.map((c) => `${colorChar}${c}`)
|
||||
.join("");
|
||||
return `${acc}${colorChar}x${colorSplit}${cur}`;
|
||||
} else {
|
||||
return `${acc}${colorChar}${gradientArray[index]}${cur}`;
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
});
|
||||
|
||||
const onCopyClickHandler = () => {
|
||||
if (resultVal.value.length === 0) {
|
||||
Message.warning("请先输入文本哦");
|
||||
return;
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(resultVal.value)
|
||||
.then(() => Message.success("复制成功啦"))
|
||||
.catch((err) => {
|
||||
Message.error("复制失败,请尝试更新您的浏览器");
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import url("./assets/styles/mixins.less");
|
||||
.cg-container {
|
||||
width: 96%;
|
||||
height: 100%;
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
.cg-header {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cg-main {
|
||||
display: flex;
|
||||
.cg-content {
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
&--form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.cg-configration {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
border: 1px dashed #dcdee2;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
&:deep(.ivu-form-item) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.preview-card {
|
||||
:deep(.ivu-card-body) {
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.gradient-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
.cg-result {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: 20px;
|
||||
line-height: 25px;
|
||||
letter-spacing: 1px;
|
||||
.custom-scrollbar();
|
||||
outline: none;
|
||||
border: none;
|
||||
word-break: break-all;
|
||||
&--preview {
|
||||
height: 80px;
|
||||
}
|
||||
&--raw {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cg-footer {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.cg-header {
|
||||
height: 100px !important;
|
||||
}
|
||||
.cg-main {
|
||||
flex-direction: column !important;
|
||||
:deep(.color-list) {
|
||||
flex-direction: row !important;
|
||||
.color-item {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.cg-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
23
src/assets/styles/main.less
Normal file
23
src/assets/styles/main.less
Normal file
@ -0,0 +1,23 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.color-container__color-picker-popover {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.ivu-color-picker {
|
||||
.ivu-icon.ivu-icon-ios-arrow-down {
|
||||
display: none;
|
||||
}
|
||||
.ivu-input-icon-normal + .ivu-input {
|
||||
height: 38px !important;
|
||||
padding: 7px !important;
|
||||
}
|
||||
.ivu-color-picker-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
19
src/assets/styles/mixins.less
Normal file
19
src/assets/styles/mixins.less
Normal file
@ -0,0 +1,19 @@
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&::-webkit-scrollbar-track {
|
||||
border: 0;
|
||||
}
|
||||
}
|
199
src/components/ColorList.vue
Normal file
199
src/components/ColorList.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="color-container">
|
||||
<Poptip confirm title="是否删除所有颜色配置?" @on-ok="onDeleteAllHandler" placement="right" transfer>
|
||||
<div class="color-item is-delete">
|
||||
<div class="color-item--cube delete">
|
||||
<Icon type="md-trash" />
|
||||
<span>全部删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</Poptip>
|
||||
<div class="color-item is-add">
|
||||
<div class="color-item--cube add">
|
||||
<Icon type="md-add" />
|
||||
<span>新增</span>
|
||||
<div class="color-picker">
|
||||
<ColorPicker size="large" recommend @on-change="onColorPickerChangeHandler" transfer-class-name="color-container__color-picker-popover" transfer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<draggable v-model="privateValue" tag="ul" class="color-list">
|
||||
<template #item="{ element, index }">
|
||||
<Dropdown class="color-item" trigger="contextMenu" placement="right" transfer>
|
||||
<li class="color-item--cube" :style="`--cube-color: ${element}`">
|
||||
<span class="hex">{{ element }}</span>
|
||||
<section class="edit">
|
||||
<ColorPicker v-model="privateValue[index]" transfer-class-name="color-container__color-picker-popover" transfer />
|
||||
</section>
|
||||
</li>
|
||||
<template #list>
|
||||
<DropdownMenu>
|
||||
<DropdownItem style="color: #ed4014" @click="onDeleteColorHandler(index)">删除</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Message } from "view-ui-plus";
|
||||
import { computed } from "vue";
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
const props = withDefaults(defineProps<ColorListProps>(), {
|
||||
modelValue: () => [],
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const privateValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
const onDeleteColorHandler = (index: number) => {
|
||||
props.modelValue.splice(index, 1);
|
||||
};
|
||||
|
||||
const onDeleteAllHandler = () => {
|
||||
Message.success("已删除所有颜色配置");
|
||||
emit("update:modelValue", []);
|
||||
};
|
||||
|
||||
const onColorPickerChangeHandler = (val: string) => {
|
||||
if (props.modelValue.includes(val)) {
|
||||
Message.warning("已存在相同颜色");
|
||||
return;
|
||||
}
|
||||
props.modelValue.push(val.toUpperCase());
|
||||
};
|
||||
</script>
|
||||
<script lang="ts">
|
||||
interface ColorListProps {
|
||||
modelValue: string[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import url("../assets/styles/mixins.less");
|
||||
|
||||
.color-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 800px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
.custom-scrollbar();
|
||||
.color-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ivu-poptip {
|
||||
width: 100%;
|
||||
&:deep(.ivu-poptip-rel) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.color-item {
|
||||
width: 200px;
|
||||
margin: 0 10px 16px 10px;
|
||||
&--cube {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
border: 1px dashed #dcdee2;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
background-color: #fff;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
box-shadow: rgba(62, 57, 107, 0.3) 0px 6px 16px 0px;
|
||||
.edit {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: "";
|
||||
background-color: var(--cube-color);
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
.hex {
|
||||
color: #17233d;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.edit {
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
transition: all 0.3s;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
&:deep(.ivu-color-picker) {
|
||||
.ivu-icon.ivu-icon-ios-arrow-down {
|
||||
display: none;
|
||||
}
|
||||
.ivu-input-icon-normal + .ivu-input {
|
||||
padding-right: 7px;
|
||||
}
|
||||
.ivu-color-picker-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-delete {
|
||||
user-select: none;
|
||||
.color-item--cube {
|
||||
height: 40px !important;
|
||||
background-color: #f56c6c;
|
||||
border: none;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
.ivu-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-add {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
user-select: none;
|
||||
.color-item--cube {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 25px;
|
||||
color: #17233d;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
.color-picker {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
8
src/main.ts
Normal file
8
src/main.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createApp } from "vue";
|
||||
import ViewUIPlus from "view-ui-plus";
|
||||
import App from "./App.vue";
|
||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||
import "reset.css";
|
||||
import "./assets/styles/main.less";
|
||||
|
||||
createApp(App).use(ViewUIPlus).mount("#app");
|
13
src/utils/util.ts
Normal file
13
src/utils/util.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as cs from "color-stepper";
|
||||
|
||||
export const strHexTo16Bit = (hexStr: string): number => {
|
||||
return (parseInt(hexStr.substr(1), 16) << 8) / 256;
|
||||
};
|
||||
|
||||
export const genColorGradients = (gradients: string[], size: number): string[] => {
|
||||
if (gradients.length === 0 || size === 0) return [];
|
||||
return cs.generateSteps(
|
||||
gradients.map((c) => c.slice(1)),
|
||||
size
|
||||
);
|
||||
};
|
10
src/utils/vnode.ts
Normal file
10
src/utils/vnode.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export const VNode = defineComponent({
|
||||
props: {
|
||||
content: { type: Object },
|
||||
},
|
||||
render(): any {
|
||||
return this.content
|
||||
},
|
||||
})
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"removeComments": false,
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"isolatedModules": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"/@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
Loading…
Reference in New Issue
Block a user