Pārlūkot izejas kodu

Merge branch 'main' of https://github.com/le5le-com/visualization-design

ananzhusen 2 gadi atpakaļ
vecāks
revīzija
92556ffdfd

+ 2 - 1
.env.base

@@ -1,3 +1,4 @@
 BASE_URL=/v/
 VITE_ROUTER_BASE=/v/
-VITE_MARKET=1
+VITE_BASEURL=1
+VITE_TRIAL=0

+ 4 - 0
.env.trial

@@ -0,0 +1,4 @@
+BASE_URL=/v/
+VITE_ROUTER_BASE=/v/
+VITE_BASEURL=1
+VITE_TRIAL=1

+ 1 - 1
.github/workflows/main.yml

@@ -39,7 +39,7 @@ jobs:
       - name: Build
         run:  |
           cd webs
-          pnpm i && pnpm build    
+          pnpm i && pnpm run prod    
 
       - name: 安装COS
         run: |

+ 3 - 2
package.json

@@ -4,8 +4,9 @@
   "version": "0.0.1",
   "scripts": {
     "start": "vite --open --port 7000",
-    "build": "vue-tsc --noEmit && vite build --base=https://assets.le5lecdn.com/v/",
-    "app": "vue-tsc --noEmit && vite build --mode base --base=/v/",
+    "prod": "vue-tsc --noEmit && vite build --base=https://assets.le5lecdn.com/v/",
+    "build": "vue-tsc --noEmit && vite build --mode base --base=/v/",
+    "trial": "vue-tsc --noEmit && vite build --mode trial --base=/v/",
     "preview": "vite preview"
   },
   "dependencies": {

+ 4 - 4
src/services/api.ts

@@ -46,18 +46,18 @@ export async function updateCollection(collection: string, data: any) {
   return await axios.post(`/api/data/${collection}/update`, data);
 }
 
-export async function getLe5le2d(id: string) {
-  return await axios.post('/api/data/le5le2d/get', {
+export async function getLe5leV(id: string) {
+  return await axios.post('/api/data/le5leV/get', {
     id,
   });
 }
 
 export async function getComponents(id: string) {
-  return await axios.post(`/api/data/le5le2d-components/get`, {
+  return await axios.post(`/api/data/le5leV-components/get`, {
     id,
   });
 }
 
 export async function getComponentsList(data: any, config: any) {
-  return await axios.post('/api/data/le5le2d-components/list', data, config);
+  return await axios.post('/api/data/le5leV-components/list', data, config);
 }

+ 1 - 61
src/services/common.ts

@@ -103,7 +103,7 @@ export const save = async (
   } else {
     data.component = false; // 必要值
   }
-  let collection = data.component ? 'le5le2d-components' : 'le5le2d';
+  let collection = data.component ? 'le5leV-components' : 'le5leV';
   let ret: any;
   if (!data.name) {
     // 文件名称
@@ -111,29 +111,6 @@ export const save = async (
     (meta2d.store.data as Meta2dBackData).name = data.name;
   }
   !data.version && (data.version = baseVer);
-  /* //不需要文件夹
-  let list = undefined;
-  let folder: any = undefined;
-  let folderId = undefined;
-  if (
-    !data.component &&
-    data.folder &&
-    !(data.teams && data.owner?.id !== user.id)
-  ) {
-    //自己的图纸才允许去请求
-    folder = getFolders({
-      type: collection,
-      name: data.folder,
-    });
-    if (folder) {
-      list = folder.list; //团队图纸文件夹
-      folderId = folder._id;
-    }
-  }
-  if (!list) {
-    list = [];
-  }
-*/
   if (!data.folder) {
     data.folder = '大屏';
     data.tags = ['大屏'];
@@ -158,15 +135,6 @@ export const save = async (
       delete (data as any)[k];
     }
     ret = await addCollection(collection, data);
-    /*
-    if (!data.component) {
-      list.push({
-        id: ret._id,
-        image: data.image,
-        name: data.name,
-        component: data.component,
-      });
-    }*/
   } else {
     if (data._id && data.teams && data.owner?.id !== user.id) {
       // 团队图纸 不允许修改文件夹信息
@@ -174,35 +142,8 @@ export const save = async (
       ret = await updateCollection(collection, data);
     } else if (data._id) {
       ret = await updateCollection(collection, data);
-      /*
-      if (!data.component) {
-        list.forEach((i: any) => {
-          if (i.id === data._id) {
-            i.image = data.image;
-          }
-        });
-      }
-      //TODO 处理老接口图纸情况
-      let one = list.find((item: any) => item.id === data._id);
-      if (!data.component && !one) {
-        list.push({
-          id: ret._id,
-          image: data.image,
-          name: data.name,
-          component: data.component,
-        });
-      }*/
     } else {
       ret = await addCollection(collection, data); // 新增
-      /*
-      if (!data.component) {
-        list.push({
-          id: ret._id,
-          image: data.image,
-          name: data.name,
-          component: data.component,
-        });
-      }*/
     }
   }
 
@@ -235,7 +176,6 @@ export const save = async (
   // 保存成功,重新请求文件夹
   meta2d.emit('t-save-success', true);
   // 已保存,不再是新的,无需提示保存
-  // setDot(false);
   dot.value = false;
   localforage.removeItem(localStorageName);
   return true;

+ 10 - 10
src/services/icons.ts

@@ -1,10 +1,10 @@
-import axios from "@/http";
-import { parseSvg } from "@meta2d/svg";
-import { cdn } from "./api";
-import { getFolders } from "./png";
-const market = import.meta.env.VITE_MARKET;
+import axios from '@/http';
+import { parseSvg } from '@meta2d/svg';
+import { cdn } from './api';
+import { getFolders } from './png';
+const market = import.meta.env.VITE_BASEURL;
 
-const normalFolder = market ? "/2d/svg/" : "/svg/";
+const normalFolder = market ? '/2d/svg/' : '/svg/';
 /**
  * 请求 svg 的目录
  * @returns
@@ -18,12 +18,12 @@ export async function getIconFolders() {
  * @returns
  */
 export async function getIcons(name: string) {
-  const files = (await axios.get(normalFolder + name + "/")) as any[];
+  const files = (await axios.get(normalFolder + name + '/')) as any[];
   return await Promise.all(files.map((f) => svgToPens(f, name)));
 }
 
 export function filename(str: string) {
-  const i = str.lastIndexOf(".");
+  const i = str.lastIndexOf('.');
   return str.substring(0, i);
 }
 
@@ -34,9 +34,9 @@ async function svgToPens(f: any, diretoryName: string) {
       ? globalThis.fileJson[_name]
       : _name
     : _name;
-  const image = cdn + normalFolder + diretoryName + "/" + f.name;
+  const image = cdn + normalFolder + diretoryName + '/' + f.name;
   const svgDom: string = await axios.get(image);
-  let _svgDom = svgDom.replace("stroke:#333;", "stroke:#bdc7db;");
+  let _svgDom = svgDom.replace('stroke:#333;', 'stroke:#bdc7db;');
   const data = parseSvg(_svgDom);
   return {
     name,

+ 8 - 8
src/services/png.ts

@@ -1,9 +1,9 @@
-import axios from "@/http";
-import { cdn } from "./api";
+import axios from '@/http';
+import { cdn } from './api';
 
-const market = import.meta.env.VITE_MARKET;
+const market = import.meta.env.VITE_BASEURL;
 
-const normalFolder = market ? "/2d/png/" : "/png/";
+const normalFolder = market ? '/2d/png/' : '/png/';
 /**
  * 请求 png 的目录
  * @returns
@@ -18,7 +18,7 @@ export async function getPngFolders() {
  * @returns
  */
 export async function getPngs(name: string) {
-  const files = (await axios.get(normalFolder + name + "/")) as any[];
+  const files = (await axios.get(normalFolder + name + '/')) as any[];
   if (!files || !files.map) {
     return [];
   }
@@ -31,7 +31,7 @@ export async function getPngs(name: string) {
           : fname
         : fname,
       pinyin: globalThis.fileJson ? fname : null,
-      image: cdn + normalFolder + name + "/" + f.name,
+      image: cdn + normalFolder + name + '/' + f.name,
     };
   });
 }
@@ -44,7 +44,7 @@ export async function getFolders(folderName: string) {
   }
   return await Promise.all(
     ret.map(async (c: any) => {
-      const files = (await axios.get(folderName + c.name + "/")) as any[];
+      const files = (await axios.get(folderName + c.name + '/')) as any[];
       return {
         name: globalThis.folderJson
           ? globalThis.folderJson[c.name]
@@ -63,6 +63,6 @@ export async function getFolders(folderName: string) {
 }
 
 export function filename(str: string) {
-  const i = str.lastIndexOf(".");
+  const i = str.lastIndexOf('.');
   return str.substring(0, i);
 }

+ 34 - 38
src/services/user.ts

@@ -1,11 +1,11 @@
-import { reactive } from "vue";
+import { reactive } from 'vue';
 
-import axios from "axios";
-import dayjs from "dayjs";
+import axios from 'axios';
+import dayjs from 'dayjs';
 
-import { updateObject } from "@/services/object";
-import router from "@/router";
-import { deleteCookie, setCookie } from "@/services/cookie";
+import { updateObject } from '@/services/object';
+import router from '@/router';
+import { deleteCookie, setCookie } from '@/services/cookie';
 
 export interface IUser {
   id?: string;
@@ -23,7 +23,7 @@ export interface IUser {
   remember?: boolean;
   captcha?: string;
   vip?: string;
-  vipExpired?: boolean;
+  isVip?: boolean;
   roles?: string[];
   isOperation?: boolean;
   company?: any;
@@ -38,7 +38,7 @@ export interface IUser {
 }
 
 const user = reactive<IUser>({
-  id: "",
+  id: '',
 });
 
 const message = reactive<{ unread: number }>({
@@ -49,13 +49,13 @@ export const useUser = () => {
   const getUser = async (token?: boolean) => {
     const params: any = {};
     if (token) {
-      if (localStorage.getItem("remember")) {
-        params.token = "r";
+      if (localStorage.getItem('remember')) {
+        params.token = 'r';
       } else {
-        params.token = "1";
+        params.token = '1';
       }
     }
-    const ret: IUser = await axios.get("/api/account/profile", { params });
+    const ret: IUser = await axios.get('/api/account/profile', { params });
     if (!ret) {
       return;
     }
@@ -66,7 +66,7 @@ export const useUser = () => {
 
   const getMessage = async () => {
     const ret: { unread: number } = await axios.post(
-      "/api/message/unread/count"
+      '/api/message/unread/count'
     );
     ret && (message.unread = ret.unread);
   };
@@ -79,36 +79,32 @@ export const useUser = () => {
     if (data.vip) {
       const vip = new Date(data.vip);
       if (vip > new Date()) {
-        data.vipExpired = false;
-      } else if (vip > new Date("2023-01-17T08:00:00+08:00")) {
-        data.vipExpired = true;
-      } else {
-        data.vip = undefined;
+        data.isVip = true;
       }
       if (data.vip) {
-        data.vip = dayjs(data.vip).format("YYYY-MM-DD HH:mm:ss");
+        data.vip = dayjs(data.vip).format('YYYY-MM-DD HH:mm:ss');
       }
     }
     if (data.roles) {
       for (const item of data.roles) {
-        if (item.indexOf("运营") > -1) {
+        if (item.indexOf('运营') > -1) {
           data.isOperation = true;
           break;
         }
       }
     }
-    data.created = dayjs(data.createdAt).format("YYYY-MM-DD HH:mm:ss");
+    data.created = dayjs(data.createdAt).format('YYYY-MM-DD HH:mm:ss');
 
     if (data.token) {
       let baseUrl = import.meta.env.BASE_URL;
-      if (baseUrl && baseUrl !== "/") {
-        localStorage.setItem("token", data.token);
+      if (baseUrl && baseUrl !== '/') {
+        localStorage.setItem('token', data.token);
       } else {
-        setCookie("token", data.token, {
-          path: "/",
+        setCookie('token', data.token, {
+          path: '/',
           domain: getRootDomain(),
         });
-        localStorage.removeItem("token");
+        localStorage.removeItem('token');
       }
       delete data.token;
     }
@@ -117,21 +113,21 @@ export const useUser = () => {
 
   const signout = () => {
     updateObject(user, {});
-    localStorage.removeItem("token");
+    localStorage.removeItem('token');
     const domain = getRootDomain();
     if (domain) {
-      deleteCookie("token", {
-        path: "/",
-        domain: "le5le.com",
+      deleteCookie('token', {
+        path: '/',
+        domain: 'le5le.com',
       });
     }
 
-    router.replace({ path: "/login", query: router.currentRoute.value.query });
+    router.replace({ path: '/login', query: router.currentRoute.value.query });
   };
 
   const getRootDomain = () => {
-    let domain = "";
-    const domainItems = document.domain.split(".");
+    let domain = '';
+    const domainItems = document.domain.split('.');
     if (
       domainItems.length < 3 ||
       (domainItems.length === 4 &&
@@ -140,14 +136,14 @@ export const useUser = () => {
         +domainItems[2] > 0 &&
         +domainItems[3] > 0)
     ) {
-      domain = "";
+      domain = '';
     } else if (
-      document.domain.endsWith(".com.cn") ||
-      document.domain.endsWith(".org.cn")
+      document.domain.endsWith('.com.cn') ||
+      document.domain.endsWith('.org.cn')
     ) {
-      domain = domainItems.slice(-3).join(".");
+      domain = domainItems.slice(-3).join('.');
     } else {
-      domain = domainItems.slice(-2).join(".");
+      domain = domainItems.slice(-2).join('.');
     }
 
     return domain;

+ 2 - 2
src/services/utils.ts

@@ -1,10 +1,10 @@
 import { Pen, Meta2dData } from '@meta2d/core';
 import { MessagePlugin, NotifyPlugin, Button } from 'tdesign-vue-next';
 import { h, ref } from 'vue';
-const market = import.meta.env.VITE_MARKET;
+const market = import.meta.env.VITE_BASEURL;
 
 export const noLoginTip = '请先登录,否则无法保存!';
-export const localStorageName = 'le5le2d';
+export const localStorageName = 'le5leV';
 
 export interface Meta2dBackData extends Meta2dData {
   id?: string;

+ 12 - 0
src/styles/app.css

@@ -83,6 +83,10 @@ h5 {
   color: var(--color-gray);
 }
 
+.warning {
+  color: var(--color-warning);
+}
+
 .success {
   color: var(--color-success);
 }
@@ -314,6 +318,14 @@ a.hover:hover {
   margin-bottom: 16px;
 }
 
+.mt-20 {
+  margin-top: 20px;
+}
+
+.mt-24 {
+  margin-top: 24px;
+}
+
 .px-4 {
   padding-left: 4px;
   padding-right: 4px;

+ 2 - 2
src/views/Preview.vue

@@ -12,7 +12,7 @@ import { defaultFormat } from '@/services/defaults';
 import { useRouter, useRoute } from 'vue-router';
 import { Meta2d, Options, Pen } from '@meta2d/core';
 import { registerBasicDiagram } from '@/services/register';
-import { cdn, getLe5le2d } from '@/services/api';
+import { cdn, getLe5leV } from '@/services/api';
 const route = useRoute();
 
 const meta2dDom = ref('');
@@ -43,7 +43,7 @@ const watcher = watch(
 
 const open = async () => {
   if (route.query.id) {
-    const ret: any = getLe5le2d(route.query.id + '');
+    const ret: any = getLe5leV(route.query.id + '');
     ret && meta2d.open(ret);
   } else {
     let data: any = await localforage.getItem(localStorageName);

+ 183 - 0
src/views/components/ChargeCloudPublish.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="cloud-charge">
+    <div class="body">
+      <li
+        v-for="(item, index) in data.vips"
+        :key="index"
+        class="cloud-item"
+        :class="{ active: data.selected === index }"
+        @click="data.selected = index"
+      >
+        <h4 class="mt-16">{{ item.name }}</h4>
+        <div class="detail mt-8">快速发布,提升工作效率</div>
+        <div class="detail">减少运维工作,降低成本</div>
+        <div class="detail">专注业务,创新无负担</div>
+
+        <div class="price">
+          <span>¥</span>
+          {{ item.price }}
+        </div>
+        <div v-if="item.originalPrice !== item.price" class="original-price">
+          <span>原价:¥</span>
+          {{ item.originalPrice }}
+        </div>
+      </li>
+    </div>
+    <div class="btns">
+      <t-button @click="onSubmitOrder">提交订单</t-button>
+    </div>
+  </div>
+
+  <t-dialog
+    v-if="wechatPayDialog.show"
+    v-model:visible="wechatPayDialog.show"
+    class="pay-dialog"
+    header="乐吾乐收银台"
+    :close-on-overlay-click="false"
+    :top="95"
+    :width="700"
+    confirm-btn="支付完成"
+    :cancel-btn="null"
+    @close="getPayResult"
+  >
+    <WechatPay
+      :order="data.order"
+      :code-url="data.order.codeUrl"
+      @success="onSuccess"
+    />
+  </t-dialog>
+</template>
+
+<script lang="ts" setup>
+import { onBeforeMount, reactive } from 'vue';
+import axios from 'axios';
+import WechatPay from './WechatPay.vue';
+
+const { projectId } = defineProps<{
+  projectId: string;
+}>();
+
+const emit = defineEmits(['success']);
+
+const data = reactive<any>({
+  vips: [],
+  selected: 0,
+  order: { id: '', _id: '', codeUrl: '' },
+});
+
+const wechatPayDialog = reactive({
+  show: false,
+});
+
+onBeforeMount(async () => {
+  const ret: { list: any } = await axios.post('/api/goods/list', {
+    type: '云发布',
+  });
+  data.vips = ret.list;
+});
+
+const onSubmitOrder = async () => {
+  const result: any = await axios.post('/api/order/submit', {
+    goods: data.vips[data.selected],
+    paymentMethod: '微信',
+    data: { id: projectId },
+  });
+  if (result) {
+    data.order = result;
+    wechatPayDialog.show = true;
+  }
+};
+
+const onSuccess = (success: boolean) => {
+  emit('success', success);
+};
+
+const getPayResult = async () => {
+  const result: { state: number } = await axios.post('/order/pay/state', {
+    id: data.order.id || data.order._id,
+  });
+  if (result && result.state) {
+    getUser(true);
+    return true;
+  }
+};
+</script>
+
+<style lang="postcss" scoped>
+.cloud-charge {
+  height: 300px;
+
+  display: flex;
+  flex-direction: column;
+
+  & > .body {
+    flex-grow: 1;
+    display: flex;
+    margin-left: -10px;
+    margin-right: -10px;
+
+    .cloud-item {
+      width: 200px;
+      border-radius: 6px;
+      box-shadow: 0px 1px 20px 0px rgba(0, 10, 38, 0.1);
+      margin: 0 10px;
+      text-align: center;
+      border: 1px solid var(--color-sub-border);
+
+      h1 {
+        font-weight: 700;
+        line-height: 40px;
+        border-radius: 6px 6px 0px 0px;
+        box-shadow: 0px 1px 20px 0px rgba(0, 10, 38, 0.1);
+        margin-bottom: 16px;
+      }
+
+      .detail {
+        font-size: 12px;
+        line-height: 30px;
+        display: flex;
+        justify-content: center;
+      }
+
+      .price {
+        margin-left: -10px;
+        margin-top: 20px;
+        color: var(--color-bland);
+        font-size: 24px;
+        text-align: center;
+
+        span {
+          font-size: 12px;
+          position: relative;
+          top: -4px;
+        }
+      }
+
+      .original-price {
+        font-size: 12px;
+        color: var(--color-desc);
+      }
+
+      &:hover {
+        border-color: var(--color-primary);
+        cursor: pointer;
+      }
+      &.active {
+        border-color: var(--color-primary);
+        cursor: pointer;
+
+        &.disable {
+          border-color: transparent;
+          cursor: default;
+        }
+      }
+    }
+  }
+
+  & > .btns {
+    flex-shrink: 0;
+    text-align: right;
+    margin-top: 20px;
+  }
+}
+</style>

+ 74 - 64
src/views/components/Header.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-header">
-    <a class="logo" href="https://le5le.com" target="_blank">
+    <a class="logo" :href="assets.home" target="_blank">
       <img src="/favicon.ico" />
       <span>乐吾乐</span>
     </a>
@@ -215,8 +215,8 @@
     >
       <a> 帮助 </a>
       <t-dropdown-menu>
-        <t-dropdown-item v-for="item in helpList" :divider="item.divider">
-          <a :href="item.url" :target="item.target">{{ item.name }}</a>
+        <t-dropdown-item v-for="item in assets.helps" :divider="item.divider">
+          <a :href="item.url" target="_blank">{{ item.name }}</a>
         </t-dropdown-item>
       </t-dropdown-menu>
     </t-dropdown>
@@ -230,29 +230,36 @@
       :delay2="[10, 150]"
       overlayClassName="custom-dropdown header"
     >
-      <a style="margin-left: 32px; margin-right: 0">
+      <a style="margin-left: 32px; margin-right: 12px">
         <t-avatar
           size="small"
           :image="user.avatarUrl ? user.avatarUrl : baseUrl + 'img/avatar.png'"
         />
       </a>
       <t-dropdown-menu>
-        <t-dropdown-item>
-          <router-link to="/account/info">
+        <t-dropdown-item divider="true">
+          <a :href="assets.account">
             {{ user.username }}
-          </router-link>
+            <label class="ml-16 vip-label">VIP</label>
+          </a>
         </t-dropdown-item>
-        <t-dropdown-item>
-          <router-link to="/account/info">我的图纸</router-link>
+        <t-dropdown-item divider="true">
+          <a :href="`${assets.account}/v`" target="_blank"> 我的大屏 </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <router-link to="/account/teams">我的团队</router-link>
+          <a :href="`${assets.account}/account/teams`" target="_blank">
+            我的团队
+          </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <router-link to="/account/info">账号信息</router-link>
+          <a :href="`${assets.account}/account/info`" target="_blank">
+            账号信息
+          </a>
         </t-dropdown-item>
-        <t-dropdown-item :divider="true">
-          <router-link to="/account/security"> 安全设置 </router-link>
+        <t-dropdown-item divider="true">
+          <a :href="`${assets.account}/account/security`" target="_blank">
+            安全设置
+          </a>
         </t-dropdown-item>
         <t-dropdown-item>
           <a @click="signout">退出</a>
@@ -260,15 +267,15 @@
       </t-dropdown-menu>
     </t-dropdown>
     <div class="flex middle" v-else>
-      <a class="button primary solid" style="width: 80px" :href="login()"
-        >登录</a
-      >
+      <a class="button primary solid" style="width: 80px" :href="login()">
+        登录
+      </a>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { reactive, ref, onUnmounted, nextTick } from 'vue';
+import { reactive, ref, onBeforeMount, onUnmounted, nextTick } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
 import { useUser } from '@/services/user';
 import { MessagePlugin } from 'tdesign-vue-next';
@@ -308,12 +315,61 @@ const route = useRoute();
 
 const baseUrl = import.meta.env.BASE_URL || '/';
 
+const assets = reactive({
+  home: 'https://le5le.com',
+  account: 'https://account.le5le.com',
+  helps: [
+    {
+      name: '产品介绍',
+      url: 'https://doc.le5le.com/document/118756411',
+    },
+    {
+      name: '快速上手',
+      url: 'https://doc.le5le.com/document/119363000',
+    },
+    {
+      name: '使用手册',
+      url: 'https://doc.le5le.com/document/118764244',
+    },
+    {
+      name: '快捷键',
+      url: 'https://doc.le5le.com/document/119620214',
+      divider: true,
+    },
+    {
+      name: '企业服务与支持',
+      url: 'https://doc.le5le.com/document/119296274',
+      divider: true,
+    },
+    {
+      name: '关于我们',
+      url: 'https://le5le.com/about.html',
+    },
+  ],
+});
+
 const { user, signout } = useUser();
 const { setDot } = useDot();
 const data = reactive({
   name: '空白文件',
 });
 
+onBeforeMount(async () => {
+  // 官网或安装包版本
+  if (
+    import.meta.env.VITE_TRIAL == undefined ||
+    import.meta.env.VITE_TRIAL == 1
+  ) {
+    return;
+  }
+
+  // 企业版
+  const ret = await axios.get('/api/assets');
+  if (ret) {
+    Object.assign(assets, ret);
+  }
+});
+
 const onInputName = () => {
   (meta2d.store.data as Meta2dBackData).name = data.name;
   setDot(true);
@@ -332,18 +388,7 @@ onUnmounted(() => {
 });
 
 function login() {
-  //TODO 临时地址
-  return `https://account.le5le.com/?cb=${encodeURIComponent(location.href)}`;
-  // if (market) {
-  //       return `/account/login?cb=${encodeURIComponent(location.href)}`;
-  //     } else {
-  //       let arr = location.host.split('.');
-  //       arr[0] = 'http://account';
-  //       let accountUrl = arr.join('.');
-  //       return `${
-  //         loginUrl ? loginUrl : accountUrl
-  //       }?cb=${encodeURIComponent(location.href)}`;
-  //     }
+  return `${assets.account}?cb=${encodeURIComponent(location.href)}`;
 }
 
 function load(newT: boolean = false) {
@@ -1000,41 +1045,6 @@ const changeDisableAnchor = () => {
     onAutoAnchor();
   }
 };
-
-const helpList = [
-  {
-    name: '产品介绍',
-    url: 'https://doc.le5le.com/document/118756411',
-    target: '_blank',
-  },
-  {
-    name: '快速上手',
-    url: 'https://doc.le5le.com/document/119363000',
-    target: '_blank',
-  },
-  {
-    name: '使用手册',
-    url: 'https://doc.le5le.com/document/118764244',
-    target: '_blank',
-  },
-  {
-    name: '快捷键',
-    url: 'https://doc.le5le.com/document/119620214',
-    target: '_blank',
-    divider: true,
-  },
-  {
-    name: '企业服务与支持',
-    url: 'https://doc.le5le.com/document/119296274',
-    target: '_blank',
-    divider: true,
-  },
-  {
-    name: '关于我们',
-    url: 'https://le5le.com/about.html',
-    target: '_blank',
-  },
-];
 </script>
 <style lang="postcss" scoped>
 .app-header {

+ 1 - 1
src/views/components/Network.vue

@@ -189,7 +189,7 @@ const getNetworks = async () => {
 
 const onSelect = (item: any) => {
   Object.assign(modelValue, item);
-  popupVisible = false;
+  popupVisible.value = false;
 };
 </script>
 <style lang="postcss" scoped>

+ 125 - 28
src/views/components/View.vue

@@ -235,8 +235,8 @@
         </template>
       </t-popup>
 
-      <t-tooltip content="发布" placement="bottom">
-        <a @click="onPublish"><t-icon name="cloud" /></a>
+      <t-tooltip content="发布" placement="bottom">
+        <a @click="onShowPublish"><t-icon name="cloud" /></a>
       </t-tooltip>
     </div>
     <div id="meta2d"></div>
@@ -421,14 +421,14 @@
 
     <t-dialog
       v-if="publishDialog.show"
-      width="800px"
-      header="发布"
+      width="700px"
+      header="发布"
       :visible="true"
       :cancel-btn="null"
       :confirm-btn="'完成'"
       @close="publishDialog.show = false"
     >
-      <div class="body">
+      <div class="body" style="height: 350px">
         <div class="form-item mt-8">
           <label>项目名称</label>
           <div class="flex middle">
@@ -439,16 +439,27 @@
           <div class="form-item mt-8">
             <label>发布状态</label>
             <div v-if="publishDialog.data.id" class="flex middle">
-              <div v-if="publishDialog.data.isExpired"></div>
-              <div v-else></div>
-              {{ publishDialog.data.expired }}
+              <div v-if="publishDialog.data.isExpired" class="warning">
+                <t-icon name="stop-circle-1" class="mr-4" />
+                已过期
+              </div>
+              <div v-else class="primary">
+                <t-icon name="play-circle" class="mr-4" />
+                正在运行
+              </div>
+              <div class="gray" style="margin-left: 32px; margin-top: -2px">
+                有效期至:{{ publishDialog.data.expired }}
+              </div>
+              <a class="bland ml-12" @click="publishChargeDialog.show = true">
+                续费
+              </a>
             </div>
             <div v-else class="flex middle">
-              <t-icon name="play-circle" class="mr-8" />
+              <t-icon name="play-circle-stroke" class="mr-4" />
               未发布
             </div>
           </div>
-          <div class="form-item mt-12">
+          <div class="form-item mt-16">
             <label>乐吾乐域名</label>
             <div class="flex w-full">
               <t-input
@@ -457,20 +468,21 @@
                 :status="publishDialog.status"
                 :tips="publishDialog.tips"
                 class="mr-4"
-                style="width: fit-content"
+                style="width: 350px"
                 placeholder="子域名"
+                @change="onChangeSubdomain"
               />
               <t-tooltip content="随机生成">
                 <t-icon
                   name="refresh"
-                  class="mr-8 hover"
+                  class="hover"
                   @click="refreshSubdomain"
                 />
               </t-tooltip>
-              <div class="mt-4">v.le5le.com</div>
+              <div class="mt-4 ml-16">v.le5le.com</div>
             </div>
           </div>
-          <div class="form-item mt-16">
+          <div class="form-item mt-24">
             <label>访问域名</label>
             <div class="w-full">
               <div class="flex middle">
@@ -504,7 +516,7 @@
                     </a>
                     <template #content>
                       <div style="padding: 12px 12px 6px 12px">
-                        <img :src="qrcode.url" />
+                        <img :src="publishDialog.url" />
                       </div>
                     </template>
                   </t-popup>
@@ -518,6 +530,12 @@
                 <div class="title desc">- 自定义域名</div>
                 <div>
                   在域名服务商的DNS控制台设置好对应的CNAME,值为乐吾乐域名。
+                  <a
+                    href="https://doc.le5le.com/document/126505408"
+                    target="_blank"
+                  >
+                    更多帮助
+                  </a>
                 </div>
               </div>
             </div>
@@ -525,7 +543,7 @@
           <div class="form-item mt-16">
             <label></label>
             <div class="w-full">
-              <t-button>发布</t-button>
+              <t-button @click="onPublish">发布</t-button>
             </div>
           </div>
         </template>
@@ -534,6 +552,20 @@
         </div>
       </div>
     </t-dialog>
+
+    <t-dialog
+      v-if="publishChargeDialog.show"
+      v-model:visible="publishChargeDialog.show"
+      header="云发布续费"
+      :close-on-overlay-click="false"
+      :width="700"
+      :footer="false"
+    >
+      <ChargeCloudPublish
+        :projectId="publishDialog.id"
+        @success="onSuccessChargeCloud"
+      />
+    </t-dialog>
   </div>
 </template>
 
@@ -552,10 +584,11 @@ import localforage from 'localforage';
 import dayjs from 'dayjs';
 import QRCode from 'qrcode';
 import axios from 'axios';
+import { MessagePlugin } from 'tdesign-vue-next';
 
 import { registerBasicDiagram } from '@/services/register';
 import { useUser } from '@/services/user';
-import { cdn, getLe5le2d, updateCollection } from '@/services/api';
+import { cdn, getLe5leV, updateCollection } from '@/services/api';
 import {
   save,
   newFile,
@@ -573,7 +606,7 @@ import { s8 } from '@/services/random';
 
 import ContextMenu from './ContextMenu.vue';
 import Network from './Network.vue';
-import { MessagePlugin } from 'tdesign-vue-next';
+import ChargeCloudPublish from './ChargeCloudPublish.vue';
 
 const router = useRouter();
 const route = useRoute();
@@ -604,6 +637,8 @@ const qrcode = reactive({
 
 const publishDialog = reactive<any>({});
 
+const publishChargeDialog = reactive<any>({});
+
 onMounted(() => {
   meta2d = new Meta2d('meta2d', meta2dOptions);
   registerBasicDiagram();
@@ -665,7 +700,7 @@ const watcher = watch(
 
 const open = async () => {
   if (route.query.id) {
-    const ret: any = await getLe5le2d(route.query.id + '');
+    const ret: any = await getLe5leV(route.query.id + '');
     if (ret) {
       meta2d.open(ret);
       shared.value = ret.shared;
@@ -1150,7 +1185,7 @@ const share = async () => {
     return;
   }
 
-  const ret: any = await updateCollection('le5le2d', {
+  const ret: any = await updateCollection('le5leV', {
     _id: route.query.id,
     shared: !shared.value,
   });
@@ -1176,14 +1211,17 @@ const onLeaveQrcode = () => {
   }
 };
 
-const onPublish = async () => {
+const onShowPublish = async () => {
   // @ts-ignore
   publishDialog.name = meta2d.store.data.name;
   publishDialog.status = '';
   publishDialog.tips = '';
+  publishDialog.id = route.query.id + '';
   publishDialog.show = true;
 
-  const ret: any = await axios.post(`/api/domain/get`, { id: route.query.id });
+  const ret: any = await axios.post(`/api/domain/get`, {
+    id: publishDialog.id,
+  });
   if (ret) {
     if (ret.expired) {
       const expired = new Date(ret.expired);
@@ -1199,18 +1237,20 @@ const onPublish = async () => {
       }
     }
     publishDialog.data = ret;
+    makePublishQrcode();
   }
 };
 
 const refreshSubdomain = async () => {
   publishDialog.data.subDomain = s8();
-  if (
-    !publishDialog.data.domain ||
-    publishDialog.data.domain.indexOf('.v.le5le.com') > 0
-  ) {
-    publishDialog.data.domain = publishDialog.data.subDomain + '.v.le5le.com';
-  }
+  onChangeSubdomain();
+};
 
+const onChangeSubdomain = async () => {
+  if (!publishDialog.data.subDomain) {
+    publishDialog.status = 'error';
+    publishDialog.tips = '请填写子域名';
+  }
   const ret: any = await axios.post(`/api/domain/exists`, {
     subDomain: publishDialog.data.subDomain,
   });
@@ -1221,6 +1261,63 @@ const refreshSubdomain = async () => {
     publishDialog.status = '';
     publishDialog.tips = '';
   }
+
+  if (
+    !publishDialog.data.domain ||
+    publishDialog.data.domain.indexOf('.v.le5le.com') >= 0
+  ) {
+    publishDialog.data.domain = publishDialog.data.subDomain + '.v.le5le.com';
+  }
+
+  makePublishQrcode();
+};
+
+const onPublish = async () => {
+  if (publishDialog.status) {
+    return;
+  }
+  if (!publishDialog.data.subDomain) {
+    publishDialog.status = 'error';
+    publishDialog.tips = '请填写子域名';
+  }
+
+  if (!publishDialog.data.domain) {
+    publishDialog.data.domain = publishDialog.data.subDomain + '.v.le5le.com';
+  }
+
+  const ret: any = await axios.post(`/api/domain/set`, {
+    id: route.query.id,
+    subDomain: publishDialog.data.subDomain,
+    domain: publishDialog.data.domain,
+  });
+  if (ret) {
+    if (ret.expired) {
+      const expired = new Date(ret.expired);
+      if (expired > new Date()) {
+        ret.isExpired = false;
+      } else if (expired > new Date('2023-01-17T08:00:00+08:00')) {
+        ret.isExpired = true;
+      } else {
+        ret.expired = undefined;
+      }
+      if (ret.expired) {
+        ret.expired = dayjs(ret.expired).format('YYYY-MM-DD HH:mm:ss');
+      }
+    }
+
+    publishDialog.data = ret;
+    makePublishQrcode();
+  }
+};
+
+const makePublishQrcode = async () => {
+  const qr: any = await QRCode.toDataURL(`http://${publishDialog.data.domain}`);
+  publishDialog.url = qr;
+};
+
+const onSuccessChargeCloud = () => {
+  publishChargeDialog.show = false;
+  onShowPublish();
 };
 </script>
 <style lang="postcss" scoped>

+ 104 - 0
src/views/components/WechatPay.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="wechat-pay">
+    <div class="order">
+      <div>
+        <div>订单编号:{{ props.order.id }}</div>
+        <div>订单类型:{{ props.order.goods.type }}</div>
+      </div>
+      <div class="flex items-center">
+        应付金额:
+        <span style="font-size: 20px; color: #f5222d">
+          ¥{{ props.order.amount }}</span
+        >
+      </div>
+    </div>
+    <div class="wepay">
+      <h5>微信支付</h5>
+      <div class="flex center middle">
+        <div class="mr-20">
+          <img class="wepay-qrcode" :src="payQRCode" />
+          <div class="wepay-text">
+            <div>请使用微信扫描</div>
+            <div>二维码完成支付</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onBeforeMount, onUnmounted, ref } from 'vue';
+import axios from 'axios';
+import QRCode from 'qrcode';
+
+const props = defineProps<{
+  order: any;
+  codeUrl: string;
+}>();
+
+const emit = defineEmits(['success']);
+
+const payType = ref('wechat');
+
+const payQRCode = ref('');
+
+let timer: any;
+
+onBeforeMount(async () => {
+  payQRCode.value = await QRCode.toDataURL(props.codeUrl);
+
+  timer = setInterval(async () => {
+    const success = await getPayResult();
+    if (success) {
+      clearInterval(timer);
+      emit('success', success);
+    }
+  }, 5000);
+});
+
+onUnmounted(() => {
+  clearInterval(timer);
+});
+
+const getPayResult = async () => {
+  const result: { state: number } = await axios.post('/api/order/pay/state', {
+    id: props.order.id || props.order._id,
+  });
+  if (result && result.state) {
+    return true;
+  }
+};
+</script>
+
+<style lang="postcss" scoped>
+.wechat-pay {
+  .order {
+    padding: 10px 0;
+    line-height: 30px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .pay-type {
+    background-color: #f7f8fa;
+  }
+
+  .wepay {
+    color: var(--color-title);
+    margin-top: 16px;
+  }
+
+  .wepay-qrcode {
+    width: 150px;
+    margin-top: 24px;
+  }
+  .wepay-text {
+    padding: 10px 40px;
+    line-height: 20px;
+    color: #ffffff;
+    font-size: 13px;
+  }
+}
+</style>