🎉 first commit!

This commit is contained in:
evenwan 2023-06-29 17:42:54 +08:00
commit 9098329eb5
16 changed files with 3064 additions and 0 deletions

25
.gitignore vendored Normal file
View 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?

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# minecraft-color-gradient-generator
Yolo!

13
index.html Normal file
View 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
View 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
View 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="仅限服务器不支持&amp;&num;前缀发送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>

View 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;
}
}

View 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;
}
}

View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

21
tsconfig.json Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})

2435
yarn.lock Normal file

File diff suppressed because it is too large Load Diff