ai-code.vue 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505
  1. <template>
  2. <div class="ai-code">
  3. <div class="bg">
  4. <div class="a"></div>
  5. <div class="b"></div>
  6. </div>
  7. <div class="back" @click="toBack">
  8. <el-icon>
  9. <back />
  10. </el-icon>
  11. 返回
  12. </div>
  13. <div class="panel" :class="[`is-${step.value}`]">
  14. <div class="head">
  15. <p class="title">Cool Ai 极速编码</p>
  16. <p class="tag">让软件开发<span>再</span>快一点</p>
  17. <p class="desc">
  18. {{ desc.text }}
  19. </p>
  20. </div>
  21. <div class="start" v-if="step.value == 'start'">
  22. <el-button class="go btn-primary" @click="step.next">
  23. 快速开始
  24. <el-icon>
  25. <arrow-right-bold />
  26. </el-icon>
  27. </el-button>
  28. <el-button class="doc" @click="toDoc"> 文档 </el-button>
  29. </div>
  30. <div class="enter" v-if="step.value == 'enter'">
  31. <el-input
  32. :ref="setRefs('inputEntity')"
  33. v-model="form.entity"
  34. placeholder="如:学生信息、商品信息"
  35. @keydown.enter="step.next"
  36. />
  37. <el-icon class="icon is-loading" v-if="step.loading">
  38. <loading />
  39. </el-icon>
  40. <cl-svg class="icon" name="enter" v-else @click="step.next" />
  41. </div>
  42. <div
  43. class="form"
  44. :class="{
  45. show: ['form', 'coding'].includes(step.value)
  46. }"
  47. >
  48. <div class="editor">
  49. <div class="topbar">
  50. <div class="dots">
  51. <el-tooltip content="返回">
  52. <span @click="step.prev()"></span>
  53. </el-tooltip>
  54. <span></span>
  55. <span></span>
  56. </div>
  57. </div>
  58. <div class="content">
  59. <div class="row">
  60. <div class="label">
  61. 实体名称
  62. <el-tooltip
  63. placement="top"
  64. content="指某类事物的集合名称,如:学生信息、商品信息"
  65. >
  66. <el-icon>
  67. <question-filled />
  68. </el-icon>
  69. </el-tooltip>
  70. </div>
  71. <el-input v-model="form.entity" maxlength="20" placeholder="请输入" />
  72. </div>
  73. <div class="row module">
  74. <div class="label">
  75. 模块
  76. <el-tooltip
  77. placement="top"
  78. content="前、后端模块的标识,如:user、goods、demo"
  79. >
  80. <el-icon>
  81. <question-filled />
  82. </el-icon>
  83. </el-tooltip>
  84. </div>
  85. <el-input v-model="form.module" maxlength="20" placeholder="请输入" />
  86. <el-popover
  87. :ref="setRefs('modulePopover')"
  88. :teleported="false"
  89. :popper-style="{
  90. padding: '5px',
  91. borderRadius: '6px',
  92. zIndex: 99
  93. }"
  94. placement="left"
  95. >
  96. <template #reference>
  97. <cl-svg class="add" name="arrow-down" />
  98. </template>
  99. <div class="module-list">
  100. <div
  101. class="item"
  102. v-for="(item, index) in module.dirs"
  103. :key="index"
  104. @click="
  105. () => {
  106. form.module = item;
  107. refs.modulePopover?.hide();
  108. }
  109. "
  110. >
  111. {{ item }}
  112. </div>
  113. </div>
  114. </el-popover>
  115. </div>
  116. <div class="row">
  117. <div class="label">
  118. 字段
  119. <el-tooltip
  120. placement="top"
  121. content="实体数据的字段名称,如:ID、姓名、手机号、状态"
  122. >
  123. <el-icon>
  124. <question-filled />
  125. </el-icon>
  126. </el-tooltip>
  127. </div>
  128. <el-input v-model="form.column" maxlength="200" placeholder="请输入" />
  129. </div>
  130. <div class="row">
  131. <div class="label">
  132. 其他你想做的事
  133. <el-tooltip
  134. placement="top"
  135. content="功能的扩展,如:分页查询时姓名、手机号字段设置成可模糊搜索"
  136. >
  137. <el-icon>
  138. <question-filled />
  139. </el-icon>
  140. </el-tooltip>
  141. </div>
  142. <el-input v-model="form.other" maxlength="200" placeholder="请输入" />
  143. </div>
  144. </div>
  145. </div>
  146. <div class="btns">
  147. <el-button class="btn-primary" @click="code.create()">
  148. 生成代码
  149. <cl-svg name="code" />
  150. </el-button>
  151. </div>
  152. <div class="tips">如遇见 “代码缺失”、“请求超时”,请尝试「刷新」吧</div>
  153. </div>
  154. <div class="coding" v-if="step.value == 'coding'">
  155. <div class="editor">
  156. <div class="topbar">
  157. <div class="dots">
  158. <el-tooltip content="返回">
  159. <span @click="step.prev()"></span>
  160. </el-tooltip>
  161. <span></span>
  162. <span></span>
  163. </div>
  164. </div>
  165. <div class="content">
  166. <div class="tabs">
  167. <div
  168. class="item"
  169. v-for="(item, index) in code.list"
  170. :key="index"
  171. :class="{
  172. active: code.active == item.value
  173. }"
  174. @click="
  175. () => {
  176. code.active = item.value;
  177. }
  178. "
  179. >
  180. {{ item.label }}
  181. </div>
  182. <div class="op" v-if="!isEmpty(code.list) && !code.loading">
  183. <el-tooltip content="重新生成" v-if="code.active == 'vue'">
  184. <el-icon @click="code.refresh()">
  185. <refresh />
  186. </el-icon>
  187. </el-tooltip>
  188. <el-tooltip content="复制代码">
  189. <el-icon @click="code.copy()">
  190. <document-copy />
  191. </el-icon>
  192. </el-tooltip>
  193. <el-tooltip content="创建文件">
  194. <el-icon @click="createFile">
  195. <download />
  196. </el-icon>
  197. </el-tooltip>
  198. </div>
  199. </div>
  200. <div class="code">
  201. <cl-editor-monaco
  202. :ref="setRefs('editor')"
  203. v-model="activeCode.content"
  204. height="100%"
  205. :border="false"
  206. :options="{
  207. theme: 'ai-code--dark'
  208. }"
  209. :key="activeCode.value"
  210. :language="activeCode.value == 'vue' ? 'html' : 'typescript'"
  211. v-if="activeCode"
  212. />
  213. </div>
  214. <div class="console">
  215. <el-scrollbar :ref="setRefs('console.scrollbar')">
  216. <div class="item" v-for="(item, index) in code.logs" :key="index">
  217. <span class="date"> {{ item.date }} </span>
  218. <span class="text">
  219. {{ item.text }}
  220. </span>
  221. <el-icon
  222. class="is-loading"
  223. v-if="code.loading ? index == code.logs.length - 1 : false"
  224. >
  225. <loading />
  226. </el-icon>
  227. </div>
  228. </el-scrollbar>
  229. </div>
  230. </div>
  231. </div>
  232. </div>
  233. </div>
  234. <!-- 创建菜单 -->
  235. <cl-form ref="Form" />
  236. </div>
  237. </template>
  238. <script lang="tsx" setup name="helper-ai-code">
  239. import { onMounted, reactive, computed, nextTick } from "vue";
  240. import { useCool, module } from "/@/cool";
  241. import {
  242. Download,
  243. Back,
  244. ArrowRightBold,
  245. Loading,
  246. DocumentCopy,
  247. QuestionFilled,
  248. Refresh
  249. } from "@element-plus/icons-vue";
  250. import { ElMessage, ElMessageBox } from "element-plus";
  251. import { assign, isEmpty } from "lodash-es";
  252. import { useMenu, useAi } from "../hooks";
  253. import { config, isDev } from "/@/config";
  254. import { useForm } from "@cool-vue/crud";
  255. import * as monaco from "monaco-editor";
  256. import { sleep, storage } from "/@/cool/utils";
  257. import dayjs from "dayjs";
  258. import type { CodeItem, EpsColumn } from "../types";
  259. import { useClipboard } from "@vueuse/core";
  260. const { service, refs, setRefs, router } = useCool();
  261. const menu = useMenu();
  262. const ai = useAi();
  263. const Form = useForm();
  264. const { copy } = useClipboard();
  265. // 编辑器样式
  266. monaco.editor.defineTheme("ai-code--dark", {
  267. base: "vs-dark",
  268. inherit: true,
  269. rules: [],
  270. colors: {
  271. "editor.background": "#0f151e",
  272. "editor.inactiveSelectionBackground": "#0f151e"
  273. }
  274. });
  275. // 表单
  276. const form = reactive({
  277. entity: "",
  278. module: "",
  279. other: "",
  280. column: ""
  281. });
  282. // 执行步骤
  283. const step = reactive({
  284. loading: false,
  285. value: "start",
  286. list: ["start", "enter", "form", "coding"],
  287. async next() {
  288. if (step.loading) {
  289. return false;
  290. }
  291. step.loading = true;
  292. let active = step.value;
  293. const i = step.list.indexOf(active);
  294. if (i < step.list.length - 1) {
  295. active = step.list[i + 1];
  296. }
  297. switch (active) {
  298. case "enter":
  299. setTimeout(() => {
  300. refs.inputEntity.focus();
  301. }, 300);
  302. break;
  303. case "form":
  304. if (!form.entity) {
  305. step.loading = false;
  306. return false;
  307. }
  308. desc.set(["正在做初步分析,请稍等..."]);
  309. await code.getColumns();
  310. break;
  311. }
  312. step.loading = false;
  313. step.value = active;
  314. // 切换文案
  315. desc.change();
  316. },
  317. prev() {
  318. const i = step.list.indexOf(step.value);
  319. if (i > 0) {
  320. step.value = step.list[i - 1];
  321. }
  322. }
  323. });
  324. // 代码
  325. const code = reactive({
  326. active: "node-entity",
  327. // 代码列表
  328. list: [] as CodeItem[],
  329. // 其他数据
  330. data: {
  331. router: "",
  332. prefix: "",
  333. fileName: "",
  334. columns: [] as EpsColumn[],
  335. fieldEq: [],
  336. keyWordLikeFields: [],
  337. api: [
  338. {
  339. path: "/add",
  340. summary: "新增"
  341. },
  342. {
  343. path: "/info",
  344. summary: "单个信息"
  345. },
  346. {
  347. path: "/update",
  348. summary: "修改"
  349. },
  350. {
  351. path: "/delete",
  352. summary: "删除"
  353. },
  354. {
  355. path: "/page",
  356. summary: "分页查询"
  357. },
  358. {
  359. path: "/list",
  360. summary: "列表查询"
  361. }
  362. ]
  363. },
  364. // 日志
  365. logs: [] as any[],
  366. // 生成中
  367. loading: false,
  368. // 获取字段
  369. async getColumns() {
  370. return ai
  371. .invokeFlow("comm-column", {
  372. name: form.entity,
  373. modules: module.dirs.join("、")
  374. })
  375. .then((res) => {
  376. form.column = res.columns;
  377. form.module = res.module;
  378. });
  379. },
  380. // 清空
  381. clear() {
  382. code.list = [];
  383. code.logs = [];
  384. },
  385. // 提示
  386. tips(val?: string) {
  387. code.logs.push({
  388. date: dayjs().format("HH:mm:ss"),
  389. text: val
  390. });
  391. // 日志滚动
  392. nextTick(() => {
  393. refs["console.scrollbar"]?.wrapRef?.scrollTo({
  394. top: Math.random() + 10000,
  395. behavior: "smooth"
  396. });
  397. });
  398. },
  399. // 生成代码
  400. async create() {
  401. if (!form.entity) {
  402. return ElMessage.warning("请填写实体名称");
  403. }
  404. if (!form.module) {
  405. return ElMessage.warning("请填写模块");
  406. }
  407. if (!form.column) {
  408. return ElMessage.warning("请填写字段");
  409. }
  410. code.loading = true;
  411. // 清空
  412. code.clear();
  413. // 下一步
  414. step.next();
  415. code.tips("AI 开始编码");
  416. await sleep(300);
  417. code.tips("Entity 代码生成中");
  418. // entity 代码
  419. const entity = await code.setContent("Entity 实体", "node-entity");
  420. code.tips("Entity 生成成功,开始解析");
  421. // entity 关键数据
  422. const entityData = await ai.invokeFlow("comm-parse-entity", {
  423. entity,
  424. module: form.module
  425. });
  426. code.tips(`Entity 解析成功,${JSON.stringify(entityData)}`);
  427. code.data.router = entityData.path.replace("/admin", "");
  428. code.data.prefix = entityData.path;
  429. code.data.fileName = entityData.fileName;
  430. code.parseColumn();
  431. code.tips("Service 代码生成中");
  432. // service 代码
  433. const service = await code.setContent("Service 服务", "node-service", {
  434. ...entityData,
  435. entity
  436. });
  437. code.tips("Service 生成成功,开始解析");
  438. // service 关键数据
  439. const serviceData = await ai.invokeFlow("comm-parse-service", {
  440. service
  441. });
  442. code.tips(`Service 解析成功,${JSON.stringify(serviceData)}`);
  443. code.tips("Controller 代码生成中");
  444. // controller 代码
  445. const controller = await code.setContent("Controller 控制器", "node-controller", {
  446. ...serviceData,
  447. ...entityData,
  448. service,
  449. entity
  450. });
  451. code.tips("Controller 生成成功,开始解析");
  452. // controller 关键数据
  453. const controllerData = await ai.invokeFlow("comm-parse-controller", {
  454. controller
  455. });
  456. code.tips(`Controller 解析成功,${JSON.stringify(controllerData)}`);
  457. code.data.fieldEq = controllerData.fieldEq;
  458. code.data.keyWordLikeFields = controllerData.keyWordLikeFields;
  459. await code.createVue(false);
  460. code.tips("编码完成");
  461. code.loading = false;
  462. if (isDev) {
  463. ElMessageBox.confirm("编码完成,是否创建文件?", "提示", {
  464. type: "success",
  465. confirmButtonText: "开始创建",
  466. cancelButtonText: "稍后再看"
  467. })
  468. .then(() => {
  469. createFile();
  470. })
  471. .catch(() => null);
  472. }
  473. },
  474. // 解析字段
  475. async parseColumn() {
  476. const a = ai.invokeFlow("comm-parse-entity-column", {
  477. entity: code.getContent("node-entity")
  478. });
  479. const b = ai.invokeFlow("comm-parse-column", {
  480. entity: code.getContent("node-entity")
  481. });
  482. await Promise.all([a, b]).then((res) => {
  483. if (res[0]?.columns) {
  484. code.data.columns = res[0].columns.map((e: EpsColumn) => {
  485. if (res[1]?.columns) {
  486. e.component = res[1].columns[e.propertyName] || "input";
  487. }
  488. return e;
  489. });
  490. code.data.columns.push({
  491. comment: "更新时间",
  492. length: 0,
  493. component: "datetime",
  494. nullable: false,
  495. propertyName: "updateTime",
  496. type: "datetime"
  497. });
  498. code.data.columns.push({
  499. comment: "创建时间",
  500. length: 0,
  501. component: "datetime",
  502. nullable: false,
  503. propertyName: "createTime",
  504. type: "datetime"
  505. });
  506. }
  507. });
  508. },
  509. // 创建vue
  510. async createVue(isParse: boolean = true) {
  511. const item = code.add("Vue 页面", "vue");
  512. item.content = "";
  513. assign(code.data, form);
  514. code.tips("Vue 代码开始生成");
  515. if (isParse) {
  516. code.tips("AI 分析中");
  517. await code.parseColumn();
  518. }
  519. // 生成内容
  520. item.content = menu.createVue({
  521. ...code.data,
  522. module: form.module
  523. });
  524. await sleep(300);
  525. // 格式化
  526. refs.editor.formatCode();
  527. code.tips("Vue 生成成功");
  528. },
  529. // 添加 tab
  530. add(label: string, flow: string) {
  531. let item = code.list.find((e) => e.value == flow);
  532. if (!item) {
  533. item = reactive<CodeItem>({
  534. label,
  535. value: flow,
  536. content: "",
  537. _content: ""
  538. });
  539. code.list.push(item);
  540. }
  541. code.active = flow;
  542. return item;
  543. },
  544. // 获取数据
  545. get(value: string) {
  546. return code.list.find((e) => e.value == value)!;
  547. },
  548. // 获取内容
  549. getContent(value: string) {
  550. return code.list.find((e) => e.value == value)?.content;
  551. },
  552. // 设置内容
  553. async setContent(label: string, flow: string, data?: any) {
  554. return new Promise((resolve) => {
  555. const item = code.add(label, flow);
  556. // 是否结束
  557. let isEnd = false;
  558. // 所有内容
  559. let content = "";
  560. ai.invokeFlow(flow, { ...form, ...data }, (res) => {
  561. isEnd = res.isEnd;
  562. if (!res.isEnd) {
  563. content += res.content;
  564. if (content.indexOf("```typescript\n") == 0) {
  565. item._content = content
  566. .replace(/^```typescript\n/g, "")
  567. .replace(/```$/, "");
  568. }
  569. }
  570. });
  571. const timer = setInterval(() => {
  572. if (step.value != "coding") {
  573. clearInterval(timer);
  574. return;
  575. }
  576. const v = item._content[item.content.length] || "";
  577. if (isEnd) {
  578. if (!v) {
  579. clearInterval(timer);
  580. resolve(item.content);
  581. return false;
  582. }
  583. }
  584. item.content += v;
  585. // 滚动到底
  586. if (flow == code.active) {
  587. refs.editor?.revealLine(99999);
  588. }
  589. }, 5);
  590. });
  591. },
  592. // 复制
  593. copy() {
  594. copy(code.getContent(code.active)!);
  595. ElMessage.success("复制成功");
  596. // 存本地,方便调试
  597. storage.set("ai-code.list", code.list);
  598. storage.set("ai-code.data", code.data);
  599. storage.set("ai-code.form", form);
  600. },
  601. // 重新生成
  602. async refresh() {
  603. code.loading = true;
  604. await code.createVue();
  605. code.loading = false;
  606. }
  607. });
  608. const activeCode = computed(() => {
  609. return code.list.find((e) => e.value == code.active);
  610. });
  611. // 滚动文案
  612. const desc = reactive({
  613. list: [] as string[],
  614. text: "",
  615. change() {
  616. switch (step.value) {
  617. case "enter":
  618. desc.list = ["请简要描述您的功能,AI帮你写代码"];
  619. break;
  620. case "form":
  621. desc.list = ["准备就绪,配置预设参数"];
  622. break;
  623. default:
  624. desc.list = [
  625. "COOL为开发者而生",
  626. "只需少量的口语提示就能完成特定的功能,大大节省开发时间"
  627. ];
  628. }
  629. desc.start();
  630. },
  631. set(arr: string[]) {
  632. desc.list = arr;
  633. desc.start();
  634. },
  635. t1: null as any,
  636. t2: null as any,
  637. start() {
  638. desc.stop();
  639. function next(n: number) {
  640. const val = desc.list[n];
  641. if (val) {
  642. function next2(n2: number) {
  643. const v = val[n2];
  644. if (v) {
  645. desc.t2 = setTimeout(() => {
  646. desc.text += v;
  647. next2(n2 + 1);
  648. }, 60);
  649. } else {
  650. desc.t2 = setTimeout(() => {
  651. if (desc.list.length > 1) {
  652. desc.t1 = setInterval(() => {
  653. desc.text = desc.text.slice(0, -1);
  654. if (!desc.text) {
  655. clearInterval(desc.t1);
  656. next(n + 1);
  657. }
  658. }, 50);
  659. }
  660. }, 1500);
  661. }
  662. }
  663. next2(0);
  664. } else {
  665. next(0);
  666. }
  667. }
  668. if (!isEmpty(desc.list)) {
  669. next(0);
  670. }
  671. },
  672. stop() {
  673. if (desc.t1) {
  674. clearInterval(desc.t1);
  675. }
  676. if (desc.t2) {
  677. clearTimeout(desc.t2);
  678. }
  679. desc.text = "";
  680. },
  681. init() {
  682. desc.change();
  683. }
  684. });
  685. // 创建文件
  686. function createFile() {
  687. if (!isDev) {
  688. return ElMessage.error("只有在开发环境下才能创建文件");
  689. }
  690. Form.value?.open({
  691. title: "配置菜单",
  692. width: "800px",
  693. items: [
  694. {
  695. prop: "parentId",
  696. label: "上级节点",
  697. component: {
  698. name: "cl-menu-select",
  699. props: {
  700. type: 1
  701. }
  702. }
  703. },
  704. {
  705. prop: "keepAlive",
  706. value: true,
  707. label: "路由缓存",
  708. component: {
  709. name: "el-radio-group",
  710. options: [
  711. {
  712. label: "开启",
  713. value: true
  714. },
  715. {
  716. label: "关闭",
  717. value: false
  718. }
  719. ]
  720. }
  721. },
  722. {
  723. prop: "icon",
  724. label: "菜单图标",
  725. component: {
  726. name: "cl-menu-icon"
  727. }
  728. },
  729. {
  730. prop: "orderNum",
  731. label: "排序号",
  732. component: {
  733. name: "el-input-number",
  734. props: {
  735. placeholder: "请填写排序号",
  736. min: 0,
  737. max: 99,
  738. "controls-position": "right"
  739. }
  740. }
  741. }
  742. ],
  743. op: {
  744. saveButtonText: "开始创建"
  745. },
  746. on: {
  747. submit(data, { close }) {
  748. code.tips("创建 Vue 文件中,过程可能会发生页面及服务重启");
  749. // 添加菜单、权限
  750. menu.create({
  751. code: code.getContent("vue"),
  752. ...code.data,
  753. ...data,
  754. name: form.entity
  755. })
  756. .then((create) => {
  757. // 创建后端文件
  758. service.base.sys.menu.create({
  759. ...form,
  760. ...code.data,
  761. controller: code.getContent("node-controller"),
  762. entity: code.getContent("node-entity"),
  763. service: code.getContent("node-service")
  764. });
  765. // 每3s检测服务状态
  766. const timer = setInterval(() => {
  767. code.tips("检测后端服务是否启动");
  768. service
  769. .request({
  770. url: "/"
  771. })
  772. .then(() => {
  773. code.tips("文件创建成功");
  774. ElMessage.success("文件创建成功");
  775. clearInterval(timer);
  776. create();
  777. });
  778. }, 3000);
  779. })
  780. .catch(() => null);
  781. close();
  782. }
  783. }
  784. });
  785. }
  786. // 文档
  787. function toDoc() {
  788. window.open("https://cool-js.com/");
  789. }
  790. // 返回
  791. function toBack() {
  792. ElMessageBox.confirm(`确定要返回 ${config.app.name} 吗?`, "提示", {
  793. type: "warning"
  794. })
  795. .then(() => {
  796. router.back();
  797. })
  798. .catch(() => {});
  799. }
  800. onMounted(() => {
  801. desc.init();
  802. // 测试
  803. if (step.value == "coding") {
  804. code.list = storage.get("ai-code.list") || [];
  805. if (storage.get("ai-code.data")) {
  806. code.data = storage.get("ai-code.data");
  807. }
  808. if (storage.get("ai-code.form")) {
  809. assign(form, storage.get("ai-code.form"));
  810. }
  811. }
  812. });
  813. </script>
  814. <style lang="scss" scoped>
  815. $color: #41d1ff;
  816. .ai-code {
  817. display: flex;
  818. flex-direction: column;
  819. justify-content: center;
  820. align-items: center;
  821. position: relative;
  822. height: 100vh;
  823. overflow: hidden;
  824. .bg {
  825. position: absolute;
  826. left: 0;
  827. top: 0;
  828. height: 100%;
  829. width: 100%;
  830. background-color: #090c13;
  831. display: flex;
  832. justify-content: center;
  833. .a {
  834. background-color: $color;
  835. transform: rotate(20deg);
  836. right: -10px;
  837. }
  838. .b {
  839. background-color: #4165d7;
  840. transform: rotate(-20deg);
  841. right: 10px;
  842. }
  843. .a,
  844. .b {
  845. height: 300px;
  846. width: 420px;
  847. position: relative;
  848. opacity: 0.4;
  849. border-radius: 100%;
  850. filter: blur(60px);
  851. top: 30vh;
  852. animation: fb 5s ease-in-out infinite;
  853. }
  854. @keyframes fb {
  855. 0% {
  856. filter: blur(60px);
  857. }
  858. 40% {
  859. filter: blur(150px);
  860. }
  861. 80% {
  862. filter: blur(60px);
  863. }
  864. 100% {
  865. filter: blur(60px);
  866. }
  867. }
  868. }
  869. .back {
  870. display: flex;
  871. align-items: center;
  872. justify-content: center;
  873. position: fixed;
  874. left: 20px;
  875. top: 20px;
  876. color: #fff;
  877. border: 1px solid rgba(255, 255, 255, 0.8);
  878. border-radius: 30px;
  879. padding: 6px 13px 6px 10px;
  880. cursor: pointer;
  881. transition: all 0.2s;
  882. font-size: 12px;
  883. z-index: 9;
  884. .el-icon {
  885. font-size: 16px;
  886. margin-right: 8px;
  887. }
  888. &:hover {
  889. background-color: rgba(255, 255, 255, 0.1);
  890. }
  891. }
  892. .panel {
  893. display: flex;
  894. flex-direction: column;
  895. justify-content: center;
  896. align-items: center;
  897. position: relative;
  898. height: 100%;
  899. width: 1040px;
  900. max-width: 100%;
  901. .editor {
  902. background-color: #080e14;
  903. width: 100%;
  904. .topbar {
  905. display: flex;
  906. align-items: center;
  907. height: 36px;
  908. padding: 0 12px;
  909. .dots {
  910. display: flex;
  911. span {
  912. display: inline-block;
  913. height: 12px;
  914. width: 12px;
  915. border-radius: 12px;
  916. background-color: #2f3447;
  917. margin-right: 8px;
  918. &:first-child {
  919. cursor: pointer;
  920. &:hover {
  921. background-color: var(--el-color-danger);
  922. }
  923. }
  924. }
  925. }
  926. }
  927. .content {
  928. background-color: #0f151e;
  929. }
  930. }
  931. .btn-primary {
  932. border: 0;
  933. background-size: 300% 100%;
  934. background-image: linear-gradient(-60deg, $color, rgba($color, 0.5), $color);
  935. background-position: 100% 0px;
  936. box-shadow: 0 0 10px 1px rgba(255, 255, 255, 0.2);
  937. border-radius: 8px;
  938. letter-spacing: 1px;
  939. color: #111;
  940. transition: all 0.3s ease;
  941. .el-icon {
  942. transition: transform 0.1s;
  943. }
  944. &:hover {
  945. background-position: 0% 0px;
  946. .el-icon {
  947. transform: translateX(5px);
  948. }
  949. }
  950. }
  951. .head {
  952. padding: 50px 0;
  953. text-align: center;
  954. color: #fff;
  955. line-height: 1;
  956. letter-spacing: 2px;
  957. user-select: none;
  958. transition: all 0.2s ease 0.1s;
  959. .title {
  960. display: inline-block;
  961. font-size: 40px;
  962. background-clip: text;
  963. font-weight: bold;
  964. text-shadow: 0 5px 10px #333;
  965. transition: all 0.3s;
  966. transition-delay: 0.2s;
  967. }
  968. .tag {
  969. margin-top: 30px;
  970. font-size: 18px;
  971. color: #eee;
  972. span {
  973. color: $color;
  974. padding: 0 2px;
  975. }
  976. }
  977. .desc {
  978. display: flex;
  979. align-items: center;
  980. justify-content: center;
  981. height: 35px;
  982. padding: 0 1px;
  983. color: #fff;
  984. font-size: 22px;
  985. margin-top: 50px;
  986. &::after {
  987. content: "";
  988. display: inline-block;
  989. margin-left: 4px;
  990. height: 22px;
  991. width: 3px;
  992. background-color: #fff;
  993. border-radius: 3px;
  994. animation: shan 1s ease infinite;
  995. }
  996. @keyframes shan {
  997. 0% {
  998. opacity: 0;
  999. }
  1000. 50% {
  1001. opacity: 1;
  1002. }
  1003. 100% {
  1004. opacity: 0;
  1005. }
  1006. }
  1007. }
  1008. }
  1009. .start {
  1010. height: 50px;
  1011. text-align: center;
  1012. margin: 0 auto;
  1013. flex-shrink: 0;
  1014. .el-button {
  1015. height: 40px;
  1016. background-color: #fff;
  1017. border-radius: 8px;
  1018. }
  1019. .go {
  1020. width: 140px;
  1021. .el-icon {
  1022. margin-left: 5px;
  1023. color: #444;
  1024. }
  1025. }
  1026. .doc {
  1027. background-color: transparent;
  1028. width: 100px;
  1029. color: #fff;
  1030. border-width: 2px;
  1031. border-color: rgba(255, 255, 255, 0.7);
  1032. }
  1033. }
  1034. .enter {
  1035. display: flex;
  1036. align-items: center;
  1037. margin: 0 auto;
  1038. position: relative;
  1039. animation: enter 0.3s forwards;
  1040. width: 10px;
  1041. :deep(.el-input__wrapper) {
  1042. background-color: rgba(0, 0, 0, 0.3);
  1043. padding: 10px 20px;
  1044. border-radius: 12px;
  1045. box-shadow: 0 0 10px 1px #4165d719;
  1046. .el-input__inner {
  1047. color: #fff;
  1048. font-size: 16px;
  1049. text-align: center;
  1050. letter-spacing: 2px;
  1051. }
  1052. }
  1053. .icon {
  1054. position: absolute;
  1055. right: 18px;
  1056. color: #ccc;
  1057. font-size: 18px;
  1058. cursor: pointer;
  1059. &:hover {
  1060. color: #fff;
  1061. }
  1062. }
  1063. }
  1064. @keyframes enter {
  1065. from {
  1066. width: 10px;
  1067. }
  1068. to {
  1069. width: 320px;
  1070. }
  1071. }
  1072. .form {
  1073. transform: translateY(50vh);
  1074. width: calc(100% - 40px);
  1075. transition: all 0.3s ease;
  1076. margin: 0 auto;
  1077. .editor {
  1078. border-radius: 8px;
  1079. .content {
  1080. color: #fff;
  1081. box-sizing: border-box;
  1082. border-radius: 0 0 8px 8px;
  1083. padding: 5px 0 10px 0;
  1084. .row {
  1085. font-size: 12px;
  1086. margin-bottom: 10px;
  1087. &:last-child {
  1088. margin-bottom: 0;
  1089. }
  1090. .label {
  1091. display: flex;
  1092. align-items: center;
  1093. padding: 5px 15px;
  1094. font-size: 12px;
  1095. margin-bottom: 5px;
  1096. color: #999;
  1097. .el-icon {
  1098. margin-left: 4px;
  1099. cursor: pointer;
  1100. }
  1101. }
  1102. :deep(.el-input__wrapper) {
  1103. background-color: #2f344722;
  1104. box-shadow: none;
  1105. padding: 0 15px;
  1106. .el-input__inner {
  1107. color: #fff;
  1108. }
  1109. .el-icon {
  1110. margin-left: 2px;
  1111. }
  1112. }
  1113. &.module {
  1114. position: relative;
  1115. :deep(.el-input__wrapper) {
  1116. padding-left: 35px;
  1117. }
  1118. .add {
  1119. cursor: pointer;
  1120. margin-right: 8px;
  1121. position: absolute;
  1122. left: 15px;
  1123. top: 40px;
  1124. }
  1125. }
  1126. }
  1127. }
  1128. }
  1129. .btns {
  1130. display: flex;
  1131. justify-content: center;
  1132. .el-button {
  1133. height: 50px;
  1134. width: 200px;
  1135. font-size: 16px;
  1136. }
  1137. .cl-svg {
  1138. font-size: 18px;
  1139. margin-left: 5px;
  1140. color: #333;
  1141. }
  1142. }
  1143. .tips {
  1144. color: #eee;
  1145. text-align: center;
  1146. font-size: 14px;
  1147. user-select: none;
  1148. margin-top: 30px;
  1149. }
  1150. .module-list {
  1151. .item {
  1152. border-radius: 6px;
  1153. display: flex;
  1154. align-items: center;
  1155. height: 30px;
  1156. padding: 0 10px;
  1157. cursor: pointer;
  1158. border-radius: 6px;
  1159. font-size: 12px;
  1160. &:hover {
  1161. background-color: var(--el-fill-color-light);
  1162. }
  1163. }
  1164. }
  1165. &.show {
  1166. transform: translateY(0);
  1167. .btns {
  1168. margin-top: 60px;
  1169. }
  1170. }
  1171. }
  1172. .coding {
  1173. position: fixed;
  1174. bottom: 0;
  1175. left: 0;
  1176. height: 100vh;
  1177. width: 100%;
  1178. animation: coding 0.3s forwards;
  1179. box-sizing: border-box;
  1180. opacity: 0;
  1181. z-index: 10;
  1182. .editor {
  1183. height: 100%;
  1184. border-radius: 10px 10px 0 0;
  1185. }
  1186. .content {
  1187. height: calc(100% - 36px);
  1188. background-color: #080e14;
  1189. .tabs {
  1190. display: flex;
  1191. height: 40px;
  1192. .item {
  1193. display: flex;
  1194. align-items: center;
  1195. justify-content: center;
  1196. padding: 0 15px;
  1197. font-size: 12px;
  1198. cursor: pointer;
  1199. color: var(--el-color-info);
  1200. &.active {
  1201. background-color: #0f151e;
  1202. color: #fff;
  1203. }
  1204. &:hover {
  1205. color: #eee;
  1206. }
  1207. }
  1208. .op {
  1209. display: flex;
  1210. align-items: center;
  1211. margin-left: auto;
  1212. margin-right: 5px;
  1213. .el-icon {
  1214. height: 30px;
  1215. width: 30px;
  1216. color: #fff;
  1217. font-size: 15px;
  1218. cursor: pointer;
  1219. border-radius: 5px;
  1220. &:hover {
  1221. background-color: #0f151e;
  1222. }
  1223. }
  1224. }
  1225. }
  1226. .code {
  1227. height: calc(100% - 190px);
  1228. }
  1229. .console {
  1230. height: 150px;
  1231. padding: 5px 0;
  1232. box-sizing: border-box;
  1233. border-top: 1px solid #2f3447;
  1234. .item {
  1235. font-size: 12px;
  1236. padding: 5px 10px;
  1237. color: #fff;
  1238. .date {
  1239. margin-right: 5px;
  1240. color: #ccc;
  1241. }
  1242. .el-icon {
  1243. margin: 0 5px;
  1244. font-size: 14px;
  1245. position: relative;
  1246. top: 3px;
  1247. }
  1248. }
  1249. }
  1250. }
  1251. }
  1252. @keyframes coding {
  1253. from {
  1254. opacity: 0;
  1255. transform: translateY(10vh);
  1256. }
  1257. to {
  1258. opacity: 1;
  1259. transform: translateY(0);
  1260. }
  1261. }
  1262. }
  1263. }
  1264. </style>