神仙都没用 a5bd14020f 1
2024-06-28 15:54:35 +08:00

1448 lines
26 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="ai-code">
<div class="bg">
<div class="a"></div>
<div class="b"></div>
</div>
<div class="back" @click="toBack">
<el-icon>
<back />
</el-icon>
返回
</div>
<div class="panel" :class="[`is-${step.value}`]">
<div class="head">
<p class="title">Cool Ai 极速编码</p>
<p class="tag">让软件开发<span></span>快一点</p>
<p class="desc">
{{ desc.text }}
</p>
</div>
<div class="start" v-if="step.value == 'start'">
<el-button class="go btn-primary" @click="step.next">
快速开始
<el-icon>
<arrow-right-bold />
</el-icon>
</el-button>
<el-button class="doc" @click="toDoc"> 文档 </el-button>
</div>
<div class="enter" v-if="step.value == 'enter'">
<el-input
:ref="setRefs('inputEntity')"
v-model="form.entity"
placeholder="如:学生信息、商品信息"
@keydown.enter="step.next"
/>
<el-icon class="icon is-loading" v-if="step.loading">
<loading />
</el-icon>
<cl-svg class="icon" name="enter" v-else @click="step.next" />
</div>
<div
class="form"
:class="{
show: ['form', 'coding'].includes(step.value)
}"
>
<div class="editor">
<div class="topbar">
<div class="dots">
<el-tooltip content="返回">
<span @click="step.prev()"></span>
</el-tooltip>
<span></span>
<span></span>
</div>
</div>
<div class="content">
<div class="row">
<div class="label">
实体名称
<el-tooltip
placement="top"
content="指某类事物的集合名称,如:学生信息、商品信息"
>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</div>
<el-input v-model="form.entity" maxlength="20" placeholder="请输入" />
</div>
<div class="row module">
<div class="label">
模块
<el-tooltip
placement="top"
content="前、后端模块的标识user、goods、demo"
>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</div>
<el-input v-model="form.module" maxlength="20" placeholder="请输入" />
<el-popover
:ref="setRefs('modulePopover')"
:teleported="false"
:popper-style="{
padding: '5px',
borderRadius: '6px',
zIndex: 99
}"
placement="left"
>
<template #reference>
<cl-svg class="add" name="arrow-down" />
</template>
<div class="module-list">
<div
class="item"
v-for="(item, index) in module.dirs"
:key="index"
@click="
() => {
form.module = item;
refs.modulePopover?.hide();
}
"
>
{{ item }}
</div>
</div>
</el-popover>
</div>
<div class="row">
<div class="label">
字段
<el-tooltip
placement="top"
content="实体数据的字段名称ID、姓名、手机号、状态"
>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</div>
<el-input v-model="form.column" maxlength="200" placeholder="请输入" />
</div>
<div class="row">
<div class="label">
其他你想做的事
<el-tooltip
placement="top"
content="功能的扩展,如:分页查询时姓名、手机号字段设置成可模糊搜索"
>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</div>
<el-input v-model="form.other" maxlength="200" placeholder="请输入" />
</div>
</div>
</div>
<div class="btns">
<el-button class="btn-primary" @click="code.create()">
生成代码
<cl-svg name="code" />
</el-button>
</div>
<div class="tips">如遇见 “代码缺失”、“请求超时”,请尝试「刷新」吧</div>
</div>
<div class="coding" v-if="step.value == 'coding'">
<div class="editor">
<div class="topbar">
<div class="dots">
<el-tooltip content="返回">
<span @click="step.prev()"></span>
</el-tooltip>
<span></span>
<span></span>
</div>
</div>
<div class="content">
<div class="tabs">
<div
class="item"
v-for="(item, index) in code.list"
:key="index"
:class="{
active: code.active == item.value
}"
@click="
() => {
code.active = item.value;
}
"
>
{{ item.label }}
</div>
<div class="op" v-if="!isEmpty(code.list) && !code.loading">
<el-tooltip content="重新生成" v-if="code.active == 'vue'">
<el-icon @click="code.refresh()">
<refresh />
</el-icon>
</el-tooltip>
<el-tooltip content="复制代码">
<el-icon @click="code.copy()">
<document-copy />
</el-icon>
</el-tooltip>
<el-tooltip content="创建文件">
<el-icon @click="createFile">
<download />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="code">
<cl-editor-monaco
:ref="setRefs('editor')"
v-model="activeCode.content"
height="100%"
:border="false"
:options="{
theme: 'ai-code--dark'
}"
:key="activeCode.value"
:language="activeCode.value == 'vue' ? 'html' : 'typescript'"
v-if="activeCode"
/>
</div>
<div class="console">
<el-scrollbar :ref="setRefs('console.scrollbar')">
<div class="item" v-for="(item, index) in code.logs" :key="index">
<span class="date"> {{ item.date }} </span>
<span class="text">
{{ item.text }}
</span>
<el-icon
class="is-loading"
v-if="code.loading ? index == code.logs.length - 1 : false"
>
<loading />
</el-icon>
</div>
</el-scrollbar>
</div>
</div>
</div>
</div>
</div>
<!-- 创建菜单 -->
<cl-form ref="Form" />
</div>
</template>
<script lang="tsx" setup name="helper-ai-code">
import { onMounted, reactive, computed } from "vue";
import { useCool, module } from "/@/cool";
import {
Download,
Back,
ArrowRightBold,
Loading,
DocumentCopy,
QuestionFilled,
Refresh
} from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { assign, isEmpty } from "lodash-es";
import { useMenu, useAi } from "../hooks";
import { config, isDev } from "/@/config";
import { useForm } from "@cool-vue/crud";
import * as monaco from "monaco-editor";
import { sleep, storage } from "/@/cool/utils";
import dayjs from "dayjs";
import { nextTick } from "vue";
import type { CodeItem } from "../types";
import { useClipboard } from "@vueuse/core";
const { service, refs, setRefs, router } = useCool();
const menu = useMenu();
const ai = useAi();
const Form = useForm();
const { copy } = useClipboard();
// 编辑器样式
monaco.editor.defineTheme("ai-code--dark", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": "#0f151e",
"editor.inactiveSelectionBackground": "#0f151e"
}
});
// 表单
const form = reactive({
entity: "",
module: "",
other: "",
column: ""
});
// 执行步骤
const step = reactive({
loading: false,
value: "start",
list: ["start", "enter", "form", "coding"],
async next() {
if (step.loading) {
return false;
}
step.loading = true;
let active = step.value;
const i = step.list.indexOf(active);
if (i < step.list.length - 1) {
active = step.list[i + 1];
}
switch (active) {
case "enter":
setTimeout(() => {
refs.inputEntity.focus();
}, 300);
break;
case "form":
if (!form.entity) {
step.loading = false;
return false;
}
desc.set(["正在做初步分析,请稍等..."]);
await code.getColumns();
break;
}
step.loading = false;
step.value = active;
// 切换文案
desc.change();
},
prev() {
const i = step.list.indexOf(step.value);
if (i > 0) {
step.value = step.list[i - 1];
}
}
});
// 代码
const code = reactive({
active: "node-entity",
// 代码列表
list: (storage.get("ai-code.list") || []) as CodeItem[],
// 其他数据
data: (storage.get("ai-code.data") || {}) as any,
// 日志
logs: [] as any[],
// 生成中
loading: false,
// 获取字段
async getColumns() {
return ai
.invokeFlow("comm-column", {
name: form.entity,
modules: module.dirs.join("、")
})
.then((res) => {
form.column = res.columns;
form.module = res.module;
});
},
// 清空
clear() {
code.list = [];
code.logs = [];
},
// 提示
tips(val?: string) {
code.logs.push({
date: dayjs().format("HH:mm:ss"),
text: val
});
// 日志滚动
nextTick(() => {
refs["console.scrollbar"]?.wrapRef?.scrollTo({
top: Math.random() + 10000,
behavior: "smooth"
});
});
},
// 生成代码
async create() {
if (!form.entity) {
return ElMessage.warning("请填写实体名称");
}
if (!form.module) {
return ElMessage.warning("请填写模块");
}
if (!form.column) {
return ElMessage.warning("请填写字段");
}
code.loading = true;
// 清空
code.clear();
// 下一步
step.next();
code.tips("AI 开始编码");
await sleep(300);
code.tips("Entity 代码生成中");
// entity 代码
const entity = await code.setContent("Entity 实体", "node-entity");
code.tips("Entity 生成成功,开始解析");
// entity 关键数据
const entityData = await ai.invokeFlow("comm-parse-entity", {
entity
});
code.tips(`Entity 解析成功,${JSON.stringify(entityData)}`);
code.tips("Service 代码生成中");
// service 代码
const service = await code.setContent("Service 服务层", "node-service", {
...entityData,
entity
});
code.tips("Service 生成成功,开始解析");
// service 关键数据
const serviceData = await ai.invokeFlow("comm-parse-service", {
service
});
code.tips(`Service 解析成功,${JSON.stringify(serviceData)}`);
code.tips("Controller 代码生成中");
// controller 代码
await code.setContent("Controller 控制器", "node-controller", {
...serviceData,
...entityData,
service,
entity
});
code.tips("Controller 生成成功");
await code.createVue();
code.tips("编码完成");
code.loading = false;
ElMessageBox.confirm("编码完成,是否创建文件?", "提示", {
type: "success",
confirmButtonText: "开始创建",
cancelButtonText: "稍后再看"
})
.then(() => {
createFile();
})
.catch(() => null);
},
// 创建vue
async createVue() {
const item = code.add("Vue 页面", "vue");
item.content = "";
code.data = {
router: "",
prefix: "",
path: "",
fileName: "",
className: "",
columns: [],
api: [
{
path: "/add",
summary: "新增"
},
{
path: "/info",
summary: "单个信息"
},
{
path: "/update",
summary: "修改"
},
{
path: "/delete",
summary: "删除"
},
{
path: "/page",
summary: "分页查询"
},
{
path: "/list",
summary: "列表查询"
}
],
...form,
name: form.entity
};
code.tips("Vue 代码生成中");
// 解析
await service.base.sys.menu
.parse({
module: form.module,
entity: code.getContent("node-entity")
})
.then((res) => {
res.router = res.path.replace("/admin", "");
res.prefix = res.path;
assign(code.data, res);
});
code.tips("AI 分析字段中");
// ai分析
await ai.matchType({ columns: code.data.columns, name: form.entity });
// 生成内容
item.content = menu.createVue(code.data);
await sleep(300);
// 格式化
refs.editor.formatCode();
code.tips("Vue 生成成功");
},
// 添加 tab
add(label: string, flow: string) {
let item = code.list.find((e) => e.value == flow);
if (!item) {
item = reactive<CodeItem>({
label,
value: flow,
content: "",
_content: ""
});
code.list.push(item);
}
code.active = flow;
return item;
},
// 获取数据
get(value: string) {
return code.list.find((e) => e.value == value)!;
},
// 获取内容
getContent(value: string) {
return code.list.find((e) => e.value == value)?.content;
},
// 设置内容
async setContent(label: string, flow: string, data?: any) {
return new Promise((resolve) => {
const item = code.add(label, flow);
// 是否结束
let isEnd = false;
// 所有内容
let content = "";
ai.invokeFlow(flow, { ...form, ...data }, (res) => {
isEnd = res.isEnd;
if (!res.isEnd) {
content += res.content;
if (content.indexOf("```typescript\n") == 0) {
item._content = content
.replace(/^```typescript\n/g, "")
.replace(/```$/, "");
}
}
});
const timer = setInterval(() => {
if (step.value != "coding") {
clearInterval(timer);
return;
}
const v = item._content[item.content.length] || "";
if (isEnd) {
if (!v) {
clearInterval(timer);
resolve(item.content);
return false;
}
}
item.content += v;
// 滚动到底
if (flow == code.active) {
refs.editor?.revealLine(99999);
}
}, 10);
});
},
// 复制
copy() {
copy(code.getContent(code.active)!);
code.save();
ElMessage.success("复制成功");
},
// 重新生成
async refresh() {
code.loading = true;
await code.createVue();
code.loading = false;
},
// 保存
save() {
storage.set("ai-code.list", code.list);
storage.set("ai-code.data", code.data);
}
});
const activeCode = computed(() => {
return code.list.find((e) => e.value == code.active);
});
// 滚动文案
const desc = reactive({
list: [] as string[],
text: "",
change() {
switch (step.value) {
case "enter":
desc.list = ["请简要描述您的功能AI帮你写代码"];
break;
case "form":
desc.list = ["准备就绪,配置预设参数"];
break;
default:
desc.list = [
"COOL为开发者而生",
"只需少量的口语提示就能完成特定的功能,大大节省开发时间"
];
}
desc.start();
},
set(arr: string[]) {
desc.list = arr;
desc.start();
},
t1: null as any,
t2: null as any,
start() {
desc.stop();
function next(n: number) {
const val = desc.list[n];
if (val) {
function next2(n2: number) {
const v = val[n2];
if (v) {
desc.t2 = setTimeout(() => {
desc.text += v;
next2(n2 + 1);
}, 60);
} else {
desc.t2 = setTimeout(() => {
if (desc.list.length > 1) {
desc.t1 = setInterval(() => {
desc.text = desc.text.slice(0, -1);
if (!desc.text) {
clearInterval(desc.t1);
next(n + 1);
}
}, 50);
}
}, 1500);
}
}
next2(0);
} else {
next(0);
}
}
if (!isEmpty(desc.list)) {
next(0);
}
},
stop() {
if (desc.t1) {
clearInterval(desc.t1);
}
if (desc.t2) {
clearTimeout(desc.t2);
}
desc.text = "";
},
init() {
desc.change();
}
});
// 创建文件
function createFile() {
if (!isDev) {
return ElMessage.error("只有在开发环境下才有效");
}
Form.value?.open({
title: "配置菜单",
width: "800px",
items: [
{
prop: "parentId",
label: "上级节点",
component: {
name: "cl-menu-select",
props: {
type: 1
}
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "icon",
label: "菜单图标",
component: {
name: "cl-menu-icon"
}
},
{
prop: "orderNum",
label: "排序号",
component: {
name: "el-input-number",
props: {
placeholder: "请填写排序号",
min: 0,
max: 99,
"controls-position": "right"
}
}
}
],
op: {
saveButtonText: "开始创建"
},
on: {
submit(data, { close }) {
code.tips("创建 Vue 文件中,过程可能会发生页面及服务重启");
close();
// 添加菜单、权限
menu.create({
code: code.getContent("vue"),
...code.data,
...data
})
.then((create) => {
// 创建后端文件
service.base.sys.menu.create({
...form,
...code.data,
controller: code.getContent("node-controller"),
entity: code.getContent("node-entity"),
service: code.getContent("node-service")
});
// 每3s检测服务状态
const timer = setInterval(() => {
code.tips("检测后端服务是否启动");
service
.request({
url: "/"
})
.then(() => {
code.tips("文件创建成功");
ElMessage.success("文件创建成功");
clearInterval(timer);
create();
});
}, 3000);
})
.catch(() => null);
}
}
});
}
// 文档
function toDoc() {
window.open("https://cool-js.com/");
}
// 返回
function toBack() {
ElMessageBox.confirm(`确定要返回 ${config.app.name} 吗?`, "提示", {
type: "warning"
})
.then(() => {
router.back();
})
.catch(() => {});
}
onMounted(() => {
desc.init();
});
</script>
<style lang="scss" scoped>
$color: #41d1ff;
.ai-code {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
height: 100vh;
overflow: hidden;
.bg {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-color: #090c13;
display: flex;
justify-content: center;
.a {
background-color: $color;
transform: rotate(20deg);
right: -10px;
}
.b {
background-color: #4165d7;
transform: rotate(-20deg);
right: 10px;
}
.a,
.b {
height: 300px;
width: 420px;
position: relative;
opacity: 0.4;
border-radius: 100%;
filter: blur(60px);
top: 30vh;
animation: fb 5s ease-in-out infinite;
}
@keyframes fb {
0% {
filter: blur(60px);
}
40% {
filter: blur(150px);
}
80% {
filter: blur(60px);
}
100% {
filter: blur(60px);
}
}
}
.back {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 20px;
top: 20px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 30px;
padding: 6px 13px 6px 10px;
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
z-index: 9;
.el-icon {
font-size: 16px;
margin-right: 8px;
}
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
.panel {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
height: 100%;
width: 1040px;
max-width: 100%;
.editor {
background-color: #080e14;
width: 100%;
.topbar {
display: flex;
align-items: center;
height: 36px;
padding: 0 12px;
.dots {
display: flex;
span {
display: inline-block;
height: 12px;
width: 12px;
border-radius: 12px;
background-color: #2f3447;
margin-right: 8px;
&:first-child {
cursor: pointer;
&:hover {
background-color: var(--el-color-danger);
}
}
}
}
}
.content {
background-color: #0f151e;
}
}
.btn-primary {
border: 0;
background-size: 300% 100%;
background-image: linear-gradient(-60deg, $color, rgba($color, 0.5), $color);
background-position: 100% 0px;
box-shadow: 0 0 10px 1px rgba(255, 255, 255, 0.2);
border-radius: 8px;
letter-spacing: 1px;
color: #111;
transition: all 0.3s ease;
.el-icon {
transition: transform 0.1s;
}
&:hover {
background-position: 0% 0px;
.el-icon {
transform: translateX(5px);
}
}
}
.head {
padding: 50px 0;
text-align: center;
color: #fff;
line-height: 1;
letter-spacing: 2px;
user-select: none;
transition: all 0.2s ease 0.1s;
.title {
display: inline-block;
font-size: 40px;
background-clip: text;
font-weight: bold;
text-shadow: 0 5px 10px #333;
transition: all 0.3s;
transition-delay: 0.2s;
}
.tag {
margin-top: 30px;
font-size: 18px;
color: #eee;
span {
color: $color;
padding: 0 2px;
}
}
.desc {
display: flex;
align-items: center;
justify-content: center;
height: 35px;
padding: 0 1px;
color: #fff;
font-size: 22px;
margin-top: 50px;
&::after {
content: "";
display: inline-block;
margin-left: 4px;
height: 22px;
width: 3px;
background-color: #fff;
border-radius: 3px;
animation: shan 1s ease infinite;
}
@keyframes shan {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
}
.start {
height: 50px;
text-align: center;
margin: 0 auto;
flex-shrink: 0;
.el-button {
height: 40px;
background-color: #fff;
border-radius: 8px;
}
.go {
width: 140px;
.el-icon {
margin-left: 5px;
color: #444;
}
}
.doc {
background-color: transparent;
width: 100px;
color: #fff;
border-width: 2px;
border-color: rgba(255, 255, 255, 0.7);
}
}
.enter {
display: flex;
align-items: center;
margin: 0 auto;
position: relative;
animation: enter 0.3s forwards;
width: 10px;
:deep(.el-input__wrapper) {
background-color: rgba(0, 0, 0, 0.3);
padding: 10px 20px;
border-radius: 12px;
box-shadow: 0 0 10px 1px #4165d719;
.el-input__inner {
color: #fff;
font-size: 16px;
text-align: center;
letter-spacing: 2px;
}
}
.icon {
position: absolute;
right: 18px;
color: #ccc;
font-size: 18px;
cursor: pointer;
&:hover {
color: #fff;
}
}
}
@keyframes enter {
from {
width: 10px;
}
to {
width: 320px;
}
}
.form {
transform: translateY(50vh);
width: calc(100% - 40px);
transition: all 0.3s ease;
margin: 0 auto;
.editor {
border-radius: 8px;
.content {
color: #fff;
box-sizing: border-box;
border-radius: 0 0 8px 8px;
padding: 5px 0 10px 0;
.row {
font-size: 12px;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
.label {
display: flex;
align-items: center;
padding: 5px 15px;
font-size: 12px;
margin-bottom: 5px;
color: #999;
.el-icon {
margin-left: 4px;
cursor: pointer;
}
}
:deep(.el-input__wrapper) {
background-color: #2f344722;
box-shadow: none;
padding: 0 15px;
.el-input__inner {
color: #fff;
}
.el-icon {
margin-left: 2px;
}
}
&.module {
position: relative;
:deep(.el-input__wrapper) {
padding-left: 35px;
}
.add {
cursor: pointer;
margin-right: 8px;
position: absolute;
left: 15px;
top: 40px;
}
}
}
}
}
.btns {
display: flex;
justify-content: center;
.el-button {
height: 50px;
width: 200px;
font-size: 16px;
}
.cl-svg {
font-size: 18px;
margin-left: 5px;
color: #333;
}
}
.tips {
color: #eee;
text-align: center;
font-size: 14px;
user-select: none;
margin-top: 30px;
}
.module-list {
.item {
border-radius: 6px;
display: flex;
align-items: center;
height: 30px;
padding: 0 10px;
cursor: pointer;
border-radius: 6px;
font-size: 12px;
&:hover {
background-color: var(--el-fill-color-light);
}
}
}
&.show {
transform: translateY(0);
.btns {
margin-top: 60px;
}
}
}
.coding {
position: fixed;
bottom: 0;
left: 0;
height: 100vh;
width: 100%;
animation: coding 0.3s forwards;
box-sizing: border-box;
opacity: 0;
z-index: 10;
.editor {
height: 100%;
border-radius: 10px 10px 0 0;
}
.content {
height: calc(100% - 36px);
background-color: #080e14;
.tabs {
display: flex;
height: 40px;
.item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
font-size: 12px;
cursor: pointer;
color: var(--el-color-info);
&.active {
background-color: #0f151e;
color: #fff;
}
&:hover {
color: #eee;
}
}
.op {
display: flex;
align-items: center;
margin-left: auto;
margin-right: 5px;
.el-icon {
height: 30px;
width: 30px;
color: #fff;
font-size: 15px;
cursor: pointer;
border-radius: 5px;
&:hover {
background-color: #0f151e;
}
}
}
}
.code {
height: calc(100% - 190px);
}
.console {
height: 150px;
padding: 5px 0;
box-sizing: border-box;
border-top: 1px solid #2f3447;
.item {
font-size: 12px;
padding: 5px 10px;
color: #fff;
.date {
margin-right: 5px;
color: #ccc;
}
.el-icon {
margin: 0 5px;
font-size: 14px;
position: relative;
top: 3px;
}
}
}
}
}
@keyframes coding {
from {
opacity: 0;
transform: translateY(10vh);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
}
</style>