Alsmile 2 лет назад
Родитель
Сommit
2fd7d2c94a

+ 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;

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

@@ -0,0 +1,178 @@
+<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 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: '微信',
+  });
+  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>

+ 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>

+ 110 - 24
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,17 @@
         </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 @success="onSuccess" />
+    </t-dialog>
   </div>
 </template>
 
@@ -552,6 +581,7 @@ 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';
@@ -573,7 +603,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 +634,8 @@ const qrcode = reactive({
 
 const publishDialog = reactive<any>({});
 
+const publishChargeDialog = reactive<any>({});
+
 onMounted(() => {
   meta2d = new Meta2d('meta2d', meta2dOptions);
   registerBasicDiagram();
@@ -1176,7 +1208,7 @@ const onLeaveQrcode = () => {
   }
 };
 
-const onPublish = async () => {
+const onShowPublish = async () => {
   // @ts-ignore
   publishDialog.name = meta2d.store.data.name;
   publishDialog.status = '';
@@ -1199,18 +1231,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 +1255,58 @@ 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;
 };
 </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>