li.shaoyi 2 năm trước cách đây
mục cha
commit
b06da7c262
100 tập tin đã thay đổi với 2266 bổ sung81 xóa
  1. 4 0
      .env.qxst
  2. 6 1
      android/fxgl.txt
  3. 3 1
      package.json
  4. BIN
      src/packages/gstj/assets/images/boot-1080p.png
  5. BIN
      src/packages/gstj/assets/images/boot-480p.png
  6. BIN
      src/packages/gstj/assets/images/boot-720p.png
  7. BIN
      src/packages/gstj/assets/images/hongbao-open.png
  8. BIN
      src/packages/gstj/assets/images/hongbao.png
  9. BIN
      src/packages/gstj/assets/images/level-bg.png
  10. BIN
      src/packages/gstj/assets/images/lottery/lottery-bg.png
  11. BIN
      src/packages/gstj/assets/images/lottery/lottery-hongbao.png
  12. BIN
      src/packages/gstj/assets/images/lottery/lottery-money.png
  13. BIN
      src/packages/gstj/assets/images/lottery/lottery-red-envelope.png
  14. BIN
      src/packages/gstj/assets/images/lottery/lottery-title.png
  15. 2 2
      src/packages/gstj/views/account/certification/Index.vue
  16. 15 19
      src/packages/gstj/views/goods/trade/index.vue
  17. 0 25
      src/packages/gstj/views/home/main/Index.vue
  18. 3 3
      src/packages/gstj/views/mine/Index.vue
  19. 1 1
      src/packages/gstj/views/order/delivery/components/offline/detail/Index.vue
  20. 2 2
      src/packages/mobile/views/account/certification/Index.vue
  21. 15 19
      src/packages/mobile/views/goods/trade/index.vue
  22. 3 4
      src/packages/mobile/views/mine/Index.vue
  23. 1 1
      src/packages/pc/assets/themes/default/default.less
  24. 1 1
      src/packages/pc/views/auth/components/layout/index.vue
  25. 3 2
      src/packages/pc/views/footer/goods/position/components/transfer/index.vue
  26. 32 0
      src/packages/qxst/App.vue
  27. BIN
      src/packages/qxst/assets/app_logo/1024x1024.png
  28. 1 0
      src/packages/qxst/assets/iconfont/iconfont.js
  29. BIN
      src/packages/qxst/assets/icons/cart.png
  30. 0 0
      src/packages/qxst/assets/icons/ccwl.svg
  31. 0 0
      src/packages/qxst/assets/icons/cpjg.svg
  32. 1 0
      src/packages/qxst/assets/icons/cpjs.svg
  33. BIN
      src/packages/qxst/assets/icons/fire.png
  34. BIN
      src/packages/qxst/assets/icons/friend.png
  35. BIN
      src/packages/qxst/assets/icons/futures.png
  36. 0 0
      src/packages/qxst/assets/icons/generalize.svg
  37. BIN
      src/packages/qxst/assets/icons/gold.png
  38. BIN
      src/packages/qxst/assets/icons/goods.png
  39. 1 0
      src/packages/qxst/assets/icons/htzr.svg
  40. 0 0
      src/packages/qxst/assets/icons/order.svg
  41. 0 0
      src/packages/qxst/assets/icons/ptgz.svg
  42. 0 0
      src/packages/qxst/assets/icons/red-envelope.svg
  43. 0 0
      src/packages/qxst/assets/icons/schedule.svg
  44. BIN
      src/packages/qxst/assets/icons/signin.png
  45. BIN
      src/packages/qxst/assets/icons/spot.png
  46. 0 0
      src/packages/qxst/assets/icons/statement.svg
  47. BIN
      src/packages/qxst/assets/icons/useradd.png
  48. 0 0
      src/packages/qxst/assets/icons/wareorder.svg
  49. 0 0
      src/packages/qxst/assets/icons/wddj.svg
  50. 0 0
      src/packages/qxst/assets/icons/wdrw.svg
  51. BIN
      src/packages/qxst/assets/images/avatar.jpg
  52. BIN
      src/packages/qxst/assets/images/avatar.png
  53. BIN
      src/packages/qxst/assets/images/block-bg.png
  54. BIN
      src/packages/qxst/assets/images/boot-1080p.png
  55. BIN
      src/packages/qxst/assets/images/boot-480p.png
  56. BIN
      src/packages/qxst/assets/images/boot-720p.png
  57. BIN
      src/packages/qxst/assets/images/certification.png
  58. BIN
      src/packages/qxst/assets/images/guide-1.png
  59. BIN
      src/packages/qxst/assets/images/guide-2.png
  60. BIN
      src/packages/qxst/assets/images/login-logo.png
  61. BIN
      src/packages/qxst/assets/logo.png
  62. 0 0
      src/packages/qxst/assets/logo.svg
  63. 14 0
      src/packages/qxst/assets/themes/base/animation.less
  64. 77 0
      src/packages/qxst/assets/themes/base/mixin-resize.less
  65. 149 0
      src/packages/qxst/assets/themes/base/mixin.less
  66. 116 0
      src/packages/qxst/assets/themes/base/reset.less
  67. 10 0
      src/packages/qxst/assets/themes/dark/dark.less
  68. 70 0
      src/packages/qxst/assets/themes/default/default.less
  69. 408 0
      src/packages/qxst/assets/themes/global/global.less
  70. 6 0
      src/packages/qxst/assets/themes/light/light.less
  71. 5 0
      src/packages/qxst/assets/themes/style.less
  72. 14 0
      src/packages/qxst/components/base/banner/index.less
  73. 46 0
      src/packages/qxst/components/base/banner/index.vue
  74. 62 0
      src/packages/qxst/components/base/html-container/index.vue
  75. 12 0
      src/packages/qxst/components/base/html-panel/index.less
  76. 42 0
      src/packages/qxst/components/base/html-panel/index.vue
  77. 17 0
      src/packages/qxst/components/base/iconfont/index.less
  78. 64 0
      src/packages/qxst/components/base/iconfont/index.vue
  79. 71 0
      src/packages/qxst/components/base/list/index.less
  80. 59 0
      src/packages/qxst/components/base/list/index.vue
  81. 40 0
      src/packages/qxst/components/base/popup/index.less
  82. 45 0
      src/packages/qxst/components/base/popup/index.vue
  83. 4 0
      src/packages/qxst/components/base/pull-refresh/index.less
  84. 113 0
      src/packages/qxst/components/base/pull-refresh/index.vue
  85. 10 0
      src/packages/qxst/components/base/qrcode-scan/index.less
  86. 79 0
      src/packages/qxst/components/base/qrcode-scan/index.vue
  87. 13 0
      src/packages/qxst/components/base/region/index.less
  88. 103 0
      src/packages/qxst/components/base/region/index.vue
  89. 39 0
      src/packages/qxst/components/base/router-transition/index.backup.less
  90. 83 0
      src/packages/qxst/components/base/router-transition/index.less
  91. 18 0
      src/packages/qxst/components/base/router-transition/index.vue
  92. 13 0
      src/packages/qxst/components/base/select/index.less
  93. 96 0
      src/packages/qxst/components/base/select/index.vue
  94. 47 0
      src/packages/qxst/components/base/tabbar/index.less
  95. 69 0
      src/packages/qxst/components/base/tabbar/index.vue
  96. 7 0
      src/packages/qxst/components/base/tabbar/types.ts
  97. 28 0
      src/packages/qxst/components/base/table/index.less
  98. 72 0
      src/packages/qxst/components/base/table/index.vue
  99. 8 0
      src/packages/qxst/components/base/table/types.ts
  100. 43 0
      src/packages/qxst/components/base/uploader/index.vue

+ 4 - 0
.env.qxst

@@ -0,0 +1,4 @@
+VUE_APP_ENV = 'qxst'
+VUE_APP_TITLE = 黔鑫生态
+VUE_APP_ROOT = src/packages/qxst/
+VUE_APP_HOST = localhost

+ 6 - 1
android/fxgl.txt

@@ -17,4 +17,9 @@ http://103.40.249.126:18280/cfg?key=mtp_20
 
 甘肃碳交
 cn.muchinfo.cgeex_trial_v1.0.0.apk
-http://8.130.36.162:8280/cfg?key=mtp_20 
+http://8.130.36.162:8280/cfg?key=mtp_20 
+
+
+黔鑫生态
+cn.muchinfo.qxst_demo_v1.0.0.apk
+http://8.130.72.213:8280/cfg?key=mtp_20

+ 3 - 1
package.json

@@ -7,10 +7,12 @@
     "dev:mobile": "vue-cli-service serve --mode mobile",
     "dev:gstj": "vue-cli-service serve --mode gstj",
     "dev:sbyj": "vue-cli-service serve --mode sbyj",
+    "dev:qxst": "vue-cli-service serve --mode qxst",
     "build:pc": "vue-cli-service build --mode pc",
     "build:mobile": "vue-cli-service build --mode mobile",
     "build:gstj": "vue-cli-service build --mode gstj",
     "build:sbyj": "vue-cli-service build --mode sbyj",
+    "build:qxst": "vue-cli-service build --mode qxst",
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
@@ -62,4 +64,4 @@
     "vconsole": "^3.14.6",
     "worker-loader": "^3.0.8"
   }
-}
+}

BIN
src/packages/gstj/assets/images/boot-1080p.png


BIN
src/packages/gstj/assets/images/boot-480p.png


BIN
src/packages/gstj/assets/images/boot-720p.png


BIN
src/packages/gstj/assets/images/hongbao-open.png


BIN
src/packages/gstj/assets/images/hongbao.png


BIN
src/packages/gstj/assets/images/level-bg.png


BIN
src/packages/gstj/assets/images/lottery/lottery-bg.png


BIN
src/packages/gstj/assets/images/lottery/lottery-hongbao.png


BIN
src/packages/gstj/assets/images/lottery/lottery-money.png


BIN
src/packages/gstj/assets/images/lottery/lottery-red-envelope.png


BIN
src/packages/gstj/assets/images/lottery/lottery-title.png


+ 2 - 2
src/packages/gstj/views/account/certification/Index.vue

@@ -28,8 +28,8 @@
         </Form>
         <img src="../../../assets/images/certification.png" />
         <template #footer>
-            <div class="g-form__footer">
-                <Button type="primary" @click="formRef?.submit" round block>提交实名认证</Button>
+            <div class="g-form__footer inset">
+                <Button type="danger" @click="formRef?.submit" round block>提交实名认证</Button>
             </div>
         </template>
     </app-view>

+ 15 - 19
src/packages/gstj/views/goods/trade/index.vue

@@ -1,14 +1,17 @@
 <template>
     <app-view class="g-form">
         <template #header>
-            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '买卖大厅'" />
+            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '买卖大厅'">
+                <template #footer>
+                    <Tabs v-model:active="tabIndex" @click="onRefresh">
+                        <Tab title="卖大厅" :name="BuyOrSell.Sell" />
+                        <Tab title="买大厅" :name="BuyOrSell.Buy" />
+                    </Tabs>
+                </template>
+            </app-navbar>
         </template>
         <app-pull-refresh ref="pullRefreshRef" v-model:loading="loading" v-model:error="error" v-model:pageIndex="pageIndex"
             :page-count="pageCount" @refresh="onRefresh">
-            <Tabs v-model:active="tabIndex" @click="onTabChange">
-                <Tab title="卖大厅" :name="BuyOrSell.Sell" />
-                <Tab title="买大厅" :name="BuyOrSell.Buy" />
-            </Tabs>
             <div class="trade-section sell" v-if="dataList.length">
                 <app-list :columns="columns" :data-list="dataList">
                     <template #username="{ row }">
@@ -29,7 +32,7 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, defineAsyncComponent } from 'vue'
+import { shallowRef, defineAsyncComponent, watch } from 'vue'
 import { showToast, Tabs, Tab, Button } from 'vant'
 import { useNavigation } from '../../../router/navigation'
 import { useComponent } from '@/hooks/component'
@@ -63,20 +66,13 @@ const columns: Model.TableColumn[] = [
     { prop: 'operate', label: '摘牌' },
 ]
 
-const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => onTabChange())
+const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => onRefresh())
 
 const { pageIndex, loading, run, pageCount } = useRequest(queryWrTradeOrderDetail, {
     params: {
-        pagesize: 20,
         goodsid,
         buyorsell: tabIndex.value
     },
-    onSuccess: (res) => {
-        if (pageIndex.value === 1) {
-            dataList.value = []
-        }
-        dataList.value.push(...res.data)
-    },
     onError: () => {
         error.value = true
     }
@@ -88,11 +84,6 @@ const onRefresh = () => {
     })
 }
 
-const onTabChange = () => {
-    pageIndex.value = 1
-    onRefresh()
-}
-
 const onDelisting = (row: Model.WrTradeOrderDetailRsp) => {
     selectedRow.value = row
     /// 不能与自己成交
@@ -102,4 +93,9 @@ const onDelisting = (row: Model.WrTradeOrderDetailRsp) => {
     }
     openComponent('delisting')
 }
+
+// 监听行情变动
+watch(() => quote.value, () => {
+    onRefresh()
+})
 </script>

+ 0 - 25
src/packages/gstj/views/home/main/Index.vue

@@ -30,39 +30,14 @@
 import { shallowRef } from "vue";
 import { Cell, CellGroup, PullRefresh } from "vant";
 import { formatDate } from "@/filters";
-import { useNavigation } from '../../../router/navigation';
 import { queryImageConfigs } from "@/services/api/common";
 import { queryNewTitles } from "@/services/api/news";
-import { useLoginStore } from '@/stores'
 import Banner from '../../../components/base/banner/index.vue'
-import Iconfont from '../../../components/base/iconfont/index.vue'
 
-const loginStore = useLoginStore();
-const { routerTo, setGlobalUrlParams } = useNavigation();
 const refreshing = shallowRef(false); // 是否处于加载中状态
 const topBanners = shallowRef<string[]>([]); // 轮播图列表
 const newsList = shallowRef<Model.NewTitlesRsp[]>([]); // 资讯列表
 
-// 跳转导航页面
-const switchTab = (tabIndex: number) => {
-  if (loginStore.token) {
-    setGlobalUrlParams({ tabIndex })
-    switch (tabIndex) {
-      case 1:
-        routerTo('home-presale', true)
-        break
-      case 2:
-        routerTo('home-transfer', true)
-        break
-      case 3:
-        routerTo('home-goods', true)
-        break
-    }
-  } else {
-    routerTo('user-login')
-  }
-}
-
 // 下拉刷新
 const onRefresh = () => {
   if (!topBanners.value.length) {

+ 3 - 3
src/packages/gstj/views/mine/Index.vue

@@ -13,7 +13,8 @@
                         <div class="profile-user__info">
                             <div class="top">
                                 <span>{{ userStore.accountName }}</span>
-                                <Icon name="checked" color="var(--van-tag-success-color)" v-if="authStatus" />
+                                <Icon name="checked" color="var(--van-tag-success-color)"
+                                    v-if="authStatus === AuthStatus.Certified" />
                                 <Icon name="warning" color="var(--van-tag-warning-color)" v-else />
                             </div>
                             <div class="bottom">{{ loginStore.loginId }}</div>
@@ -68,8 +69,7 @@
         </app-block>
         <app-block class="g-navmenu">
             <CellGroup>
-                <Cell is-link :to="{ name: 'account-certification' }"
-                    v-if="[AuthStatus.Uncertified, AuthStatus.Rejected].includes(authStatus)">
+                <Cell is-link :to="{ name: 'account-certification' }" v-if="authStatus !== AuthStatus.Certified">
                     <template #title>
                         <Iconfont icon="icon-shimingrenzheng">实名认证</Iconfont>
                     </template>

+ 1 - 1
src/packages/gstj/views/order/delivery/components/offline/detail/Index.vue

@@ -7,7 +7,7 @@
             </template>
             <div class="g-form__container">
                 <CellGroup title="线下交收单信息">
-                    <Cell title="商品代码/名称" :value="selectedRow.goodscode+'/'+selectedRow.goodsnamedisplay" />
+                    <Cell title="商品代码/名称" :value="selectedRow.goodsnamedisplay" />
                     <Cell title="交收方向" :value="selectedRow.buyorselldisplay" />
                     <Cell title="交收数量" :value="selectedRow.deliveryqty" />
                     <Cell title="交收价格" :value="selectedRow.deliveryprice" />

+ 2 - 2
src/packages/mobile/views/account/certification/Index.vue

@@ -28,8 +28,8 @@
         </Form>
         <img src="../../../assets/images/certification.png" />
         <template #footer>
-            <div class="g-form__footer">
-                <Button type="primary" @click="formRef?.submit" round block>提交实名认证</Button>
+            <div class="g-form__footer inset">
+                <Button type="danger" @click="formRef?.submit" round block>提交实名认证</Button>
             </div>
         </template>
     </app-view>

+ 15 - 19
src/packages/mobile/views/goods/trade/index.vue

@@ -1,14 +1,17 @@
 <template>
     <app-view class="g-form">
         <template #header>
-            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '买卖大厅'" />
+            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '买卖大厅'">
+                <template #footer>
+                    <Tabs v-model:active="tabIndex" @click="onRefresh">
+                        <Tab title="卖大厅" :name="BuyOrSell.Sell" />
+                        <Tab title="买大厅" :name="BuyOrSell.Buy" />
+                    </Tabs>
+                </template>
+            </app-navbar>
         </template>
         <app-pull-refresh ref="pullRefreshRef" v-model:loading="loading" v-model:error="error" v-model:pageIndex="pageIndex"
             :page-count="pageCount" @refresh="onRefresh">
-            <Tabs v-model:active="tabIndex" @click="onTabChange">
-                <Tab title="卖大厅" :name="BuyOrSell.Sell" />
-                <Tab title="买大厅" :name="BuyOrSell.Buy" />
-            </Tabs>
             <div class="trade-section sell" v-if="dataList.length">
                 <app-list :columns="columns" :data-list="dataList">
                     <template #username="{ row }">
@@ -29,7 +32,7 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, defineAsyncComponent } from 'vue'
+import { shallowRef, defineAsyncComponent, watch } from 'vue'
 import { showToast, Tabs, Tab, Button } from 'vant'
 import { useNavigation } from '../../../router/navigation'
 import { useComponent } from '@/hooks/component'
@@ -63,20 +66,13 @@ const columns: Model.TableColumn[] = [
     { prop: 'operate', label: '摘牌' },
 ]
 
-const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => onTabChange())
+const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => onRefresh())
 
 const { pageIndex, loading, run, pageCount } = useRequest(queryWrTradeOrderDetail, {
     params: {
-        pagesize: 20,
         goodsid,
         buyorsell: tabIndex.value
     },
-    onSuccess: (res) => {
-        if (pageIndex.value === 1) {
-            dataList.value = []
-        }
-        dataList.value.push(...res.data)
-    },
     onError: () => {
         error.value = true
     }
@@ -88,11 +84,6 @@ const onRefresh = () => {
     })
 }
 
-const onTabChange = () => {
-    pageIndex.value = 1
-    onRefresh()
-}
-
 const onDelisting = (row: Model.WrTradeOrderDetailRsp) => {
     selectedRow.value = row
     /// 不能与自己成交
@@ -102,4 +93,9 @@ const onDelisting = (row: Model.WrTradeOrderDetailRsp) => {
     }
     openComponent('delisting')
 }
+
+// 监听行情变动
+watch(() => quote.value, () => {
+    onRefresh()
+})
 </script>

+ 3 - 4
src/packages/mobile/views/mine/Index.vue

@@ -13,7 +13,8 @@
                         <div class="profile-user__info">
                             <div class="top">
                                 <span>{{ userStore.accountName }}</span>
-                                <Icon name="checked" color="var(--van-tag-success-color)" v-if="authStatus" />
+                                <Icon name="checked" color="var(--van-tag-success-color)"
+                                    v-if="authStatus === AuthStatus.Certified" />
                                 <Icon name="warning" color="var(--van-tag-warning-color)" v-else />
                             </div>
                             <div class="bottom">{{ loginStore.loginId }}</div>
@@ -68,8 +69,7 @@
         </app-block>
         <app-block class="g-navmenu">
             <CellGroup>
-                <Cell is-link :to="{ name: 'account-certification' }"
-                    v-if="[AuthStatus.Uncertified, AuthStatus.Rejected].includes(authStatus)">
+                <Cell is-link :to="{ name: 'account-certification' }" v-if="authStatus !== AuthStatus.Certified">
                     <template #title>
                         <Iconfont icon="icon-shimingrenzheng">实名认证</Iconfont>
                     </template>
@@ -188,7 +188,6 @@ const userLogout = () => {
 }
 
 onActivated(() => {
-    console.log(authStatus.value)
     if (authStatus.value !== AuthStatus.Certified) {
         // 获取用户账号信息
         queryUserAccount().then((res) => {

+ 1 - 1
src/packages/pc/assets/themes/default/default.less

@@ -187,7 +187,7 @@
 
             &__content {
                 align-items: flex-start;
-                padding-right: 80px;
+                padding-right: 48px;
             }
 
             &--row {

+ 1 - 1
src/packages/pc/views/auth/components/layout/index.vue

@@ -29,7 +29,7 @@ defineProps({
   },
 })
 
-//const year = new Date().getFullYear()
+const year = new Date().getFullYear()
 </script>
 
 <style lang="less">

+ 3 - 2
src/packages/pc/views/footer/goods/position/components/transfer/index.vue

@@ -32,7 +32,8 @@
                     <el-input-number placeholder="请输入数量" v-model="formData.OrderQty" :precision="0"
                         :max="selectedRow.enableqty" :min="0" />
                     <el-radio-group size="small" v-model="qtyStep" @change="onRadioChange">
-                        <el-radio v-for="(value, index) in qtyStepList" :key="index" :label="value" border>
+                        <el-radio v-for="(value, index) in qtyStepList" :key="index" :label="value" border
+                            style="width: 25%;">
                             {{ parsePercent(value, 0) }}
                         </el-radio>
                     </el-radio-group>
@@ -80,7 +81,7 @@ const { formSubmit, formData, loading } = useOrder()
 const show = ref(true)
 const refresh = ref(false)
 const formRef = ref<FormInstance>()
-const qtyStepList = [0.25, 0.75, 1] // 数量步长列表
+const qtyStepList = [0.25, 0.5, 0.75, 1] // 数量步长列表
 const qtyStep = ref<number>() // 数量步长
 
 const formRules: FormRules = {

+ 32 - 0
src/packages/qxst/App.vue

@@ -0,0 +1,32 @@
+<template>
+  <router-view />
+</template>
+
+<script lang="ts" setup>
+import { useNavigation } from './router/navigation'
+import { dialog } from '@/utils/vant'
+import { useLogin } from '@/business/login'
+import eventBus from '@/services/bus'
+
+const { userLogout } = useLogin()
+const { backHome } = useNavigation()
+
+// 接收用户登出通知
+eventBus.$on('LogoutNotify', (msg) => {
+  userLogout(() => {
+    if (msg) {
+      dialog({
+        message: msg as string,
+        confirmButtonText: '确定'
+      }).then(() => {
+        // ---待处理---
+        // 登出后应该回退到首页,如果回退后非首页,会导致路由拦截而跳转到登录页面,此时因为 tabIndex = 0 的问题,登录页被 replace 成首页,导致路由还能继续后退
+        // 临时解决方案是先退回首页后再进行登出操作
+        backHome()
+      })
+    } else {
+      backHome()
+    }
+  })
+})
+</script>

BIN
src/packages/qxst/assets/app_logo/1024x1024.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
src/packages/qxst/assets/iconfont/iconfont.js


BIN
src/packages/qxst/assets/icons/cart.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/ccwl.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/cpjg.svg


+ 1 - 0
src/packages/qxst/assets/icons/cpjs.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 204.49 205.39"><defs><style>.cls-1,.cls-2,.cls-3{fill-rule:evenodd;}.cls-1{fill:url(#未命名的渐变_2);}.cls-2{opacity:0.5;fill:url(#未命名的渐变_5);}.cls-2,.cls-3{isolation:isolate;}.cls-3,.cls-4{fill:#fff;}.cls-3{opacity:0.2;}.cls-5{fill:#c30d23;}</style><linearGradient id="未命名的渐变_2" x1="-948.92" y1="1698.32" x2="-948.31" y2="1699.33" gradientTransform="matrix(174.47, 0, 0, -174.47, 165599.73, 296506.15)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c30d23"/><stop offset="1" stop-color="#dd364a"/></linearGradient><linearGradient id="未命名的渐变_5" x1="131.37" y1="115.54" x2="177.6" y2="174.65" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffc6cd"/><stop offset="1" stop-color="#e83a4f"/></linearGradient></defs><title>1-产品介绍</title><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path class="cls-1" d="M102.24,0c137.31,6.2,135.34,200.13,0,205.39C-35.07,199.19-33.09,5.26,102.24,0Z"/><path class="cls-2" d="M110,204.86C34.91,173.61,47.38,51.24,143.18,47.6c24,1.1,42.66,9.34,56.06,21.59C217.65,125.73,187.49,197.43,110,204.86Z"/><path class="cls-3" d="M.15,108.59C-2.09,65.72,20.91,21.49,68.66,5.83,79.64,49,56.45,100.76.15,108.59Z"/><path class="cls-4" d="M145,43.73A66.72,66.72,0,0,0,37.76,109.6c2.59,12.15,1.3,30.1-2,45.2a7,7,0,0,0,6.82,8.65c.08,0,1.43-.22,1.43-.22,15.63-3.11,35.1-5.24,44.87-2.39A50.49,50.49,0,0,0,103,162.35,66.72,66.72,0,0,0,145,43.73Zm-35.29,73H74.54a5.27,5.27,0,1,1,0-10.53h35.12a5.27,5.27,0,1,1,0,10.53ZM130.73,88.6H74.54a5.27,5.27,0,0,1,0-10.54h56.19a5.27,5.27,0,1,1,0,10.54Z"/><path class="cls-5" d="M114.93,111.43a5.26,5.26,0,0,1-5.27,5.26H74.54a5.27,5.27,0,1,1,0-10.53h35.12A5.27,5.27,0,0,1,114.93,111.43Z"/><path class="cls-5" d="M136,83.33a5.27,5.27,0,0,1-5.27,5.27H74.54a5.27,5.27,0,0,1,0-10.54h56.19A5.27,5.27,0,0,1,136,83.33Z"/></g></g></svg>

BIN
src/packages/qxst/assets/icons/fire.png


BIN
src/packages/qxst/assets/icons/friend.png


BIN
src/packages/qxst/assets/icons/futures.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/generalize.svg


BIN
src/packages/qxst/assets/icons/gold.png


BIN
src/packages/qxst/assets/icons/goods.png


+ 1 - 0
src/packages/qxst/assets/icons/htzr.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 204.49 205.39"><defs><style>.cls-1,.cls-2,.cls-3{fill-rule:evenodd;}.cls-1{fill:url(#未命名的渐变_2);}.cls-2{opacity:0.5;fill:url(#未命名的渐变_5);}.cls-2,.cls-3{isolation:isolate;}.cls-3,.cls-4{fill:#fff;}.cls-3{opacity:0.2;}</style><linearGradient id="未命名的渐变_2" x1="-1233.09" y1="1692.07" x2="-1232.49" y2="1693.08" gradientTransform="matrix(174.47, 0, 0, -174.47, 215179.21, 295416.49)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c30d23"/><stop offset="1" stop-color="#dd364a"/></linearGradient><linearGradient id="未命名的渐变_5" x1="131.37" y1="115.54" x2="177.6" y2="174.65" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffc6cd"/><stop offset="1" stop-color="#e83a4f"/></linearGradient></defs><title>1-合同转让</title><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path class="cls-1" d="M102.24,0c137.31,6.2,135.34,200.13,0,205.39C-35.07,199.19-33.09,5.26,102.24,0Z"/><path class="cls-2" d="M110,204.86C34.91,173.61,47.38,51.24,143.18,47.6c24,1.1,42.66,9.34,56.06,21.59C217.65,125.73,187.49,197.43,110,204.86Z"/><path class="cls-3" d="M.15,108.59C-2.09,65.72,20.91,21.49,68.66,5.83,79.64,49,56.45,100.76.15,108.59Z"/><rect class="cls-4" x="66.42" y="42.99" width="71.65" height="119.41" rx="13.32"/><path class="cls-4" d="M160.11,62.28h0A10.41,10.41,0,0,0,149.7,72.69V135a10.41,10.41,0,0,0,10.41,10.41h0A10.41,10.41,0,0,0,170.52,135V72.69A10.41,10.41,0,0,0,160.11,62.28Z"/><path class="cls-4" d="M44.37,62.28A10.41,10.41,0,0,0,34,72.69V135a10.41,10.41,0,0,0,20.82,0V72.69A10.41,10.41,0,0,0,44.37,62.28Z"/></g></g></svg>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/order.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/ptgz.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/red-envelope.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/schedule.svg


BIN
src/packages/qxst/assets/icons/signin.png


BIN
src/packages/qxst/assets/icons/spot.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/statement.svg


BIN
src/packages/qxst/assets/icons/useradd.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/wareorder.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/wddj.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/icons/wdrw.svg


BIN
src/packages/qxst/assets/images/avatar.jpg


BIN
src/packages/qxst/assets/images/avatar.png


BIN
src/packages/qxst/assets/images/block-bg.png


BIN
src/packages/qxst/assets/images/boot-1080p.png


BIN
src/packages/qxst/assets/images/boot-480p.png


BIN
src/packages/qxst/assets/images/boot-720p.png


BIN
src/packages/qxst/assets/images/certification.png


BIN
src/packages/qxst/assets/images/guide-1.png


BIN
src/packages/qxst/assets/images/guide-2.png


BIN
src/packages/qxst/assets/images/login-logo.png


BIN
src/packages/qxst/assets/logo.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/packages/qxst/assets/logo.svg


+ 14 - 0
src/packages/qxst/assets/themes/base/animation.less

@@ -0,0 +1,14 @@
+//导航图标动画Demo
+@keyframes icon-scale {
+    0% {
+        transform: scale3d(1.3, 1.3, 1.3);
+    }
+
+    50% {
+        transform: scale3d(.5, .5, .5);
+    }
+
+    100% {
+        transform: scale3d(1, 1, 1);
+    }
+}

+ 77 - 0
src/packages/qxst/assets/themes/base/mixin-resize.less

@@ -0,0 +1,77 @@
+/*!
+ * ©teamwei.com
+ * author: teamwei
+ * date: 2021-09-07
+ */
+
+
+/* 移动设备 */
+@phone: ~"screen and (max-width: 575px)";
+
+/* 移动设备 (横屏) */
+@phone-h: ~"screen and (min-width: 576px) and (max-width: 767px)";
+
+/* 平板设备 */
+@tablet: ~"screen and (min-width: 768px) and (max-width: 991px)";
+
+/* 电脑设备 */
+@pc: ~"screen and (min-width: 992px)";
+
+/* 
+    根据不同设备适配属性单位
+    @device 设备代码
+*/
+.mixin-property-rules(@property, @value, @device) {
+    @n: length(@value);
+
+    .each(@i, @parent: "") when (@i < @n + 1) {
+        @arg: extract(@value, @i);
+
+        /* 手机 */
+        .ifNumber() when (isnumber(@arg) = true) and (@device = 001) {
+            @child: unit(@arg * 2 / 100, rem);
+        }
+        /* 平板、电脑 */
+        .ifNumber() when (isnumber(@arg) = true) and (@device = 002) {
+            @child: unit(@arg, px);
+        }
+        /* 非数值 */
+        .ifNumber() when (isnumber(@arg) = false) {
+            @child: @arg;
+        }
+
+        .ifNumber();
+        @newValue: ~"@{parent} @{child}";
+
+        .ifLast() when (@i = @n) {
+            @{property}: @newValue;
+        }
+
+        .ifLast();
+        .each(@i + 1, @newValue);
+    }
+
+    .each(1);
+}
+
+.mixin-property(@property, @value) {
+    @media @phone,@phone-h {
+        .mixin-property-rules(@property, @value, 001);
+    }
+
+    @media @pc,@tablet {
+        .mixin-property-rules(@property, @value, 002);
+    }
+}
+
+html {
+    font-size: 14px;
+
+    @media @phone {
+        font-size: ~"calc(100vw / 750 * 100)";
+    }
+
+    @media @phone-h {
+        font-size: ~"calc(100vw / 1344 * 100)";
+    }
+}

+ 149 - 0
src/packages/qxst/assets/themes/base/mixin.less

@@ -0,0 +1,149 @@
+/*!
+ * ©teamwei.com
+ * author: teamwei
+ * date: 2021-08-28
+ */
+
+@border-color: #eee;
+@arrow-color : #aaa;
+
+/* 移动端1px边框修复 */
+.mixin-border() {
+    position   : relative;
+    line-height: normal;
+
+    &:before {
+        content         : '';
+        position        : absolute;
+        transform-origin: 0 0;
+        box-sizing      : border-box;
+    }
+}
+
+/* 左边框 */
+.mixin-border-left(@width: 1px, @rgb: @border-color, @type: solid) {
+    .mixin-border();
+
+    &:before {
+        top        : 0;
+        bottom     : 0;
+        left       : 0;
+        width      : @width;
+        border-left: @width @type @rgb;
+        transform  : scaleX(0.5);
+    }
+}
+
+/* 右边框 */
+.mixin-border-right(@width: 1px, @rgb: @border-color, @type: solid) {
+    .mixin-border();
+
+    &:before {
+        top         : 0;
+        bottom      : 0;
+        right       : 0;
+        width       : @width;
+        border-right: @width @type @rgb;
+        transform   : scaleX(0.5);
+    }
+}
+
+/* 上边框 */
+.mixin-border-top(@width: 1px, @rgb: @border-color, @type: solid) {
+    .mixin-border();
+
+    &:before {
+        top       : 0;
+        left      : 0;
+        right     : 0;
+        height    : @width;
+        border-top: @width @type @rgb;
+        transform : scaleY(0.5);
+    }
+}
+
+/* 下边框 */
+.mixin-border-bottom(@width: 1px, @rgb: @border-color, @type: solid) {
+    .mixin-border();
+
+    &:before {
+        bottom       : 0;
+        left         : 0;
+        right        : 0;
+        height       : @width;
+        border-bottom: @width @type @rgb;
+        transform    : scaleY(0.5);
+    }
+}
+
+.mixin-arrow() {
+    position    : relative;
+    font-size   : 0;
+    border-style: solid;
+
+    &:after {
+        content     : '';
+        position    : absolute;
+        border-style: solid;
+    }
+}
+
+.mixin-arrow-left(@size: 9px, @color: @arrow-color, @maskcolor: #fff) {
+    .mixin-arrow();
+    border-width: @size @size @size 0;
+    border-color: transparent @color transparent;
+
+    &:after {
+        left        : 2px;
+        top         : -@size;
+        border-width: @size @size @size 0;
+        border-color: transparent @maskcolor transparent;
+    }
+}
+
+.mixin-arrow-right(@size: 9px, @color: @arrow-color, @maskcolor: #fff) {
+    .mixin-arrow();
+    border-width: @size 0 @size @size;
+    border-color: transparent transparent transparent @color;
+
+    &:after {
+        right       : 2px;
+        top         : -@size;
+        border-width: @size 0 @size @size;
+        border-color: transparent transparent transparent @maskcolor;
+    }
+}
+
+.mixin-arrow-up(@size: 9px, @color: @arrow-color, @maskcolor: #fff) {
+    .mixin-arrow();
+    border-width: 0 @size @size;
+    border-color: transparent transparent @color;
+
+    &:after {
+        left        : -@size;
+        top         : 2px;
+        border-width: 0 @size @size;
+        border-color: transparent transparent @maskcolor;
+    }
+}
+
+.mixin-arrow-down(@size: 9px, @color: @arrow-color, @maskcolor: #fff) {
+    .mixin-arrow();
+    border-width: @size @size 0;
+    border-color: @color transparent transparent;
+
+    &:after {
+        left        : -@size;
+        bottom      : 2px;
+        border-width: @size @size 0;
+        border-color: @maskcolor transparent transparent;
+    }
+}
+
+.mixin-text-overflow(@row: 1) {
+    overflow          : hidden;
+    text-overflow     : ellipsis;
+    display           : -webkit-box;
+    -webkit-line-clamp: @row;
+    -webkit-box-orient: vertical;
+}

+ 116 - 0
src/packages/qxst/assets/themes/base/reset.less

@@ -0,0 +1,116 @@
+@import './animation.less';
+
+* {
+    font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", STHeiti, "Microsoft Yahei", Tahoma, Simsun, sans-serif;
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+    background-repeat: no-repeat;
+    background-position: center center;
+    background-size: contain;
+    outline: none;
+    -webkit-tap-highlight-color: transparent;
+
+    //-webkit-appearance: none;
+    &::-webkit-input-placeholder {
+        color: #ccc;
+    }
+}
+
+html {
+    height: 100%;
+}
+
+body {
+    position: relative;
+    height: inherit;
+    font-size: .28rem;
+    color: #333;
+    background-color: #666;
+    margin: auto !important;
+    overflow: hidden;
+    -webkit-overflow-scrolling: touch;
+
+    a {
+        text-decoration: none;
+        color: #333;
+
+        &:hover {
+            color: inherit;
+            text-decoration: underline;
+        }
+
+        &:focus {
+            outline: none;
+        }
+    }
+
+    p,
+    label,
+    h1,
+    h2,
+    h3,
+    h4,
+    h5,
+    h6 {
+        margin: 0;
+        font-weight: normal;
+    }
+
+    img {
+        border: 0;
+        max-width: 100%;
+    }
+
+    button,
+    input,
+    textarea,
+    select {
+        line-height: initial;
+        border: 0;
+        background-color: transparent;
+        outline: none;
+
+        &:focus {
+            outline: none;
+        }
+    }
+
+    button {
+        display: inline-flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+
+        span {
+            display: inline-block;
+        }
+    }
+
+    input::-webkit-outer-spin-button,
+    input::-webkit-inner-spin-button {
+        -webkit-appearance: none;
+    }
+
+    input[type="number"] {
+        -moz-appearance: textfield;
+    }
+
+    ul,
+    ol,
+    dl {
+        list-style-type: none;
+    }
+}
+
+.app {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    background-color: #fff;
+    overflow-x: hidden;
+    padding-bottom: constant(safe-area-inset-bottom);
+    /* 兼容 iOS<11.2 */
+    padding-bottom: env(safe-area-inset-bottom);
+    /* 兼容iOS>= 11.2 */
+}

+ 10 - 0
src/packages/qxst/assets/themes/dark/dark.less

@@ -0,0 +1,10 @@
+[theme='dark'] {
+    /* 导航栏 */
+    --navbar-color     : #fff;
+    --navbar-background: #333;
+
+    /* 标签栏 */
+    --tabbar-background : #333;
+    --tabbar-icon       : #fff;
+    --tabbar-icon-active: #dc143c;
+}

+ 70 - 0
src/packages/qxst/assets/themes/default/default.less

@@ -0,0 +1,70 @@
+:root {
+    /* 字体大小规范 */
+    --font-x-large: 18px;
+    --font-large: 16px;
+    --font-medium: 14px;
+    --font-small: 12px;
+    --font-x-small: 10px;
+
+    /* 颜色规范 */
+    --color-default: #384048;
+    --color-primary: #409EFF;
+    --color-secondary: #04c786;
+    --color-info: #999;
+    --color-border: #eee;
+    --color-up: #ff2b2b;
+    --color-down: #04c786;
+
+    /* 导航栏 */
+    --navbar-height: .88rem;
+    --navbar-color: #fff;
+    --navbar-background: #C30D23;
+    --navbar-backbutton-color: #fff;
+
+    /* 标签栏 */
+    --tabbar-background: #fff;
+    --tabbar-icon: #999;
+    --tabbar-icon-active: #c30d23;
+
+    /* 内容边距 */
+    --content-inset: .24rem;
+
+    /* Vant-Button */
+    --van-button-border-width: 0;
+    --van-button-primary-background: #1e78b9;
+    --van-button-danger-background: #d82d42;
+
+    /* Vant-Checkbox */
+    --van-checkbox-checked-icon-color: #DD364A !important;
+
+    /* Vant-Tabs */
+    --van-tabs-bottom-bar-color: #DD364A;
+
+    --van-dialog-confirm-button-text-color: #DD364A;
+
+    .app-tabs {
+        .tabs {
+            flex-wrap: wrap;
+
+            &-item {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                color: #626675;
+                cursor: pointer;
+                border-radius: .04rem;
+                background-color: #f0f0f1;
+                padding: .08rem .16rem;
+
+                &:not(:first-child) {
+                    margin-left: .1rem;
+                }
+
+                &.is-active {
+                    color: #222;
+                    font-weight: bold;
+                }
+            }
+        }
+    }
+}

+ 408 - 0
src/packages/qxst/assets/themes/global/global.less

@@ -0,0 +1,408 @@
+[class*='g-image'] {
+    position: relative;
+    object-fit: cover;
+    overflow: hidden;
+
+    &:before {
+        content: '';
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        background: #fff url("../../images/avatar.png") no-repeat center;
+        background-size: cover;
+    }
+}
+
+.g-price-up {
+    color: #ff3333;
+}
+
+.g-price-normal {
+    color: #333333;
+}
+
+.g-price-down {
+    color: #0baf1f;
+}
+
+.g-form {
+    &__container {
+        display: flex;
+        flex-direction: column;
+        padding-bottom: .32rem;
+
+        /* 父元素的第一个子元素 */
+        .van-cell-group--inset:first-of-type {
+            margin-top: .32rem;
+        }
+
+        /* 相邻兄弟元素 */
+        .van-cell-group--inset+.van-cell-group--inset {
+            margin-top: .24rem;
+        }
+
+        .van-field {
+            .van-stepper {
+                display: flex;
+                align-items: center;
+                width: 100%;
+
+                &__input {
+                    flex: 1;
+                }
+            }
+        }
+    }
+
+    &__footer {
+        display: flex;
+        align-items: center;
+
+        &:empty {
+            display: none;
+        }
+
+        &.inset {
+            gap: .2rem;
+            padding: .2rem .32rem;
+        }
+    }
+}
+
+.g-flex {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+
+    &--row {
+        flex-direction: row;
+    }
+
+    &__body {
+        flex: 1;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+    }
+
+    &__footer {
+        margin-top: auto;
+    }
+}
+
+.g-color {
+    &--up {
+        color: var(--color-up);
+    }
+
+    &--down {
+        color: var(--color-down);
+    }
+}
+
+.g-block {
+    &--bg {
+        background: #fff url('../../images/block-bg.png') no-repeat center bottom;
+        background-size: 100%;
+    }
+}
+
+/* 导航列表 */
+.g-navmenu {
+    .app-iconfont {
+        height: 100%;
+
+        &__icon {
+            font-size: .32rem;
+            margin-right: .24rem;
+        }
+    }
+}
+
+/* 商品列表 */
+.g-goods-list {
+    padding: .2rem;
+
+    .goods {
+        background-color: #fff;
+        border-radius: .12rem;
+        overflow: hidden;
+
+        &-image {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 2.4rem;
+            font-size: 0;
+        }
+
+        &-info {
+            padding: .2rem;
+
+            &__title {
+                font-size: .26rem;
+                margin-bottom: .1rem;
+            }
+
+            &__desc {
+                font-size: .24rem;
+                color: #999;
+                margin-bottom: .1rem;
+            }
+
+            &__price {
+                color: #f2270c;
+
+                .unit {
+                    font-size: .24rem;
+                }
+
+                .integer {
+                    font-size: .3rem;
+                }
+            }
+        }
+    }
+}
+
+/* 订单列表 */
+.g-order-list {
+    padding: .2rem;
+    padding-bottom: 0;
+
+    &__box {
+        &:not(:first-child) {
+            margin-top: .2rem;
+        }
+
+        background-color: #fff;
+        border-radius: .16rem;
+        padding: .24rem;
+
+    }
+
+    &__titlebar {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: .2rem;
+
+        .left {
+            h4 {
+                font-weight: bold;
+            }
+
+            span {
+                font-size: .24rem;
+                color: #999;
+            }
+        }
+
+        .right {
+            font-size: .24rem;
+            color: #999;
+        }
+    }
+
+    &__content {
+        font-size: .24rem;
+
+        ul {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: space-between;
+
+            li {
+                display: flex;
+                justify-content: space-between;
+                line-height: .4rem;
+                width: calc(~"50% - .24rem");
+
+                span {
+                    &:first-child {
+                        color: #999;
+                        white-space: nowrap;
+                        padding-right: .24rem;
+                    }
+                }
+            }
+        }
+    }
+
+    &__btnbar {
+        display: flex;
+        justify-content: flex-end;
+        gap: .16rem;
+        margin-top: .2rem;
+
+        .van-button {
+            width: 1.6rem;
+            border-width: 1px;
+        }
+    }
+}
+
+.g-detail {
+    &__buy {
+        background-color: #fff;
+
+        .topic {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            color: #fff;
+            background-image: linear-gradient(to right, #ee0a24, #ff6034);
+            padding: .2rem .24rem;
+
+            &-left {
+                .price-text {
+                    font-size: .24rem;
+                }
+
+                .price-integer {
+                    font-size: .44rem;
+                }
+            }
+
+            &-right {
+                display: flex;
+                flex-direction: column;
+                font-size: .24rem;
+            }
+        }
+
+        .title {
+            font-size: .3rem;
+            font-weight: bold;
+            line-height: .48rem;
+            padding: .24rem;
+            padding-bottom: 0;
+
+            .van-tag {
+                font-weight: normal;
+            }
+
+            span {
+                margin-right: .1rem;
+            }
+        }
+
+        .desc {
+            padding: 0 .24rem;
+        }
+
+        .qty {
+            font-size: .24rem;
+            color: #999;
+            padding: .1rem .24rem 0 .24rem;
+        }
+
+        .info {
+            background-color: #fff;
+            padding: .2rem;
+
+            ul {
+                display: flex;
+                flex-wrap: wrap;
+                font-size: .26rem;
+
+                li {
+                    display: flex;
+                    justify-content: space-between;
+                    width: 50%;
+                    padding: .08rem .24rem;
+
+                    span {
+                        &:first-child {
+                            color: #999;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    &__desc {
+        background-color: #fff;
+        margin-top: .24rem;
+    }
+
+    &__footer {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        height: .88rem;
+        background-color: #fff;
+
+        .price {
+            padding-left: .32rem;
+
+            &-text,
+            &-unit {
+                font-size: .24rem;
+            }
+
+            &-unit {
+                color: #f2270c;
+            }
+
+            &-integer {
+                font-size: .32rem;
+                color: #f2270c;
+            }
+        }
+
+        .submit {
+            align-self: stretch;
+            display: flex;
+            margin-left: auto;
+
+            &-button {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                height: 100%;
+                min-width: 2rem;
+                font-weight: bold;
+                color: #fff;
+                padding: 0 .48rem;
+
+                &.warning {
+                    background-image: linear-gradient(to right, #ffd01e, #ff8917);
+                    background-color: #ff8a17;
+                }
+
+                &.danger {
+                    background-image: linear-gradient(to right, #ff6034, #ee0a24);
+                    background-color: #ee270a;
+                }
+            }
+        }
+    }
+}
+
+.van {
+    &-dialog {
+        &__message {
+            font-size: .28rem;
+            line-height: .44rem;
+        }
+    }
+
+    &-tabs {
+        &--list {
+            display: flex;
+            flex-direction: column;
+            height: 100%;
+        }
+
+        &--list &__content {
+            flex: 1;
+            overflow-y: auto;
+
+            .van-tab__panel {
+                height: 100%;
+            }
+        }
+    }
+}

+ 6 - 0
src/packages/qxst/assets/themes/light/light.less

@@ -0,0 +1,6 @@
+[theme='light'] {
+    /* 导航栏 */
+    --navbar-color           : #333;
+    --navbar-background      : #fff;
+    --navbar-backbutton-color: #666;
+}

+ 5 - 0
src/packages/qxst/assets/themes/style.less

@@ -0,0 +1,5 @@
+@import './base/reset.less';
+@import './global/global.less';
+@import './default/default.less';
+@import './light/light.less';
+@import './dark/dark.less';

+ 14 - 0
src/packages/qxst/components/base/banner/index.less

@@ -0,0 +1,14 @@
+.app-banner {
+    background-color: #ddd;
+
+    .van-swipe {
+        &-item {
+            font-size: 0;
+
+            img {
+                width: 100%;
+                height: 100%;
+            }
+        }
+    }
+}

+ 46 - 0
src/packages/qxst/components/base/banner/index.vue

@@ -0,0 +1,46 @@
+<template>
+    <div class="app-banner" :style="`min-height: ${swipeHeight};`">
+        <Swipe :autoplay="5000" indicator-color="white" lazy-render>
+            <SwipeItem v-for="(src, index) in dataList" :key="index" :style="`height: ${swipeHeight};`">
+                <slot :src="src">
+                    <img :src="getFileUrl(src)" @click="onClick(index)" />
+                </slot>
+            </SwipeItem>
+        </Swipe>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { PropType, computed } from 'vue'
+import { Swipe, SwipeItem } from 'vant'
+import { getFileUrl } from '@/filters'
+
+const props = defineProps({
+    //数据列表
+    dataList: {
+        type: Array as PropType<string[]>,
+        default: () => ([])
+    },
+    height: {
+        type: [Number, String],
+        default: 180,
+    }
+})
+
+const emit = defineEmits(['click'])
+
+const swipeHeight = computed(() => {
+    if (typeof props.height === 'number') {
+        return props.height + 'px'
+    }
+    return props.height
+})
+
+const onClick = (index: number) => {
+    emit('click', index)
+}
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 62 - 0
src/packages/qxst/components/base/html-container/index.vue

@@ -0,0 +1,62 @@
+<template>
+    <div ref="htmlRef" v-html="context"></div>
+</template>
+  
+<script lang="ts" setup>
+import { shallowRef, onMounted, onUnmounted } from 'vue'
+import plus from '@/utils/h5plus'
+import PhotoSwipeLightbox, { SlideData } from 'photoswipe/lightbox'
+import 'photoswipe/style.css'
+
+defineProps({
+    context: {
+        type: String,
+        default: ''
+    }
+})
+
+let lightbox: PhotoSwipeLightbox = null
+const htmlRef = shallowRef<HTMLElement>()
+
+onMounted(() => {
+    const el = htmlRef.value
+    if (el) {
+        el.querySelectorAll('a').forEach((e) => {
+            const href = e.href
+            e.onclick = () => plus.openURL(href)
+            e.removeAttribute('href')
+        })
+
+        if (!lightbox) {
+            lightbox = new PhotoSwipeLightbox({
+                gallery: el,
+                children: 'img',
+                pswpModule: () => import('photoswipe'),
+            })
+
+            // https://photoswipe.com/data-sources/#custom-html-markup
+            lightbox.addFilter('domItemData', (itemData: SlideData, el: HTMLImageElement) => {
+                if (el) {
+                    itemData.src = el.src
+                    itemData.w = el.naturalWidth
+                    itemData.h = el.naturalHeight
+                    itemData.msrc = el.src
+                }
+                return itemData
+            })
+
+            lightbox.on('beforeOpen', () => plus.hideStatusBar())
+            lightbox.on('close', () => plus.showStatusBar())
+            lightbox.init()
+        }
+    }
+})
+
+onUnmounted(() => {
+    if (lightbox) {
+        lightbox.destroy()
+        lightbox = null
+    }
+})
+</script>
+  

+ 12 - 0
src/packages/qxst/components/base/html-panel/index.less

@@ -0,0 +1,12 @@
+.app-html {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    padding: .2rem;
+
+    iframe {
+        flex: 1;
+        background-color: #fff;
+        border: 0;
+    }
+}

+ 42 - 0
src/packages/qxst/components/base/html-panel/index.vue

@@ -0,0 +1,42 @@
+<template>
+    <div class="app-html">
+        <iframe ref="iframeRef" :src="url" v-if="show" />
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef } from 'vue'
+//import http from '@/services/http'
+
+defineProps({
+    url: {
+        type: String,
+        required: true
+    },
+})
+
+const iframeRef = shallowRef<HTMLIFrameElement>()
+const show = shallowRef(false)
+
+setTimeout(() => {
+    show.value = true
+}, 200)
+
+// ios 不支持请求本地文件
+// http.request<string>({
+//     url: props.url,
+//     method: 'get',
+//     responseEncoding: 'utf-8'
+// }).then((res) => {
+//     const iframe = iframeRef.value?.contentWindow
+//     if (iframe) {
+//         iframe.document.open()
+//         iframe.document.write(res)
+//         iframe.document.close()
+//     }
+// })
+</script>
+
+<style lang="less" scoped>
+@import './index.less';
+</style>

+ 17 - 0
src/packages/qxst/components/base/iconfont/index.less

@@ -0,0 +1,17 @@
+.app-iconfont {
+    display        : inline-flex;
+    justify-content: center;
+    align-items    : center;
+
+    &__icon {
+        width         : 1em;
+        height        : 1em;
+        vertical-align: -0.15em;
+        fill          : currentColor;
+        overflow      : hidden;
+    }
+
+    &__label {
+        line-height: 1;
+    }
+}

+ 64 - 0
src/packages/qxst/components/base/iconfont/index.vue

@@ -0,0 +1,64 @@
+<template>
+    <div ref="iconfontRef" class="app-iconfont">
+        <svg :class="['app-iconfont__icon', active && 'is-active']" aria-hidden="true" :style="iconStyles">
+            <use :xlink:href="activeIconName" :fill="activeColor" v-if="active" />
+            <use :xlink:href="iconName" :fill="color" v-else />
+        </svg>
+        <span class="app-iconfont__label">
+            <slot></slot>
+        </span>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, computed, PropType, onMounted } from 'vue'
+
+const props = defineProps({
+    icon: {
+        type: String,
+        required: true
+    },
+    color: String,
+    size: String,
+    active: Boolean,
+    activeIcon: String,
+    activeColor: String,
+    // 标签方向
+    labelDirection: {
+        type: String as PropType<'left' | 'right' | 'top' | 'bottom'>,
+        default: 'right',
+    },
+})
+
+const iconfontRef = shallowRef<HTMLDivElement>()
+const iconName = computed(() => '#' + props.icon)
+const activeIconName = computed(() => '#' + (props.activeIcon ?? props.icon))
+
+const iconStyles = computed(() => ({
+    fontSize: props.size
+}))
+
+onMounted(() => {
+    const el = iconfontRef.value
+    if (el) {
+        switch (props.labelDirection) {
+            case 'left': {
+                el.style.setProperty('flex-direction', 'row-reverse')
+                break
+            }
+            case 'top': {
+                el.style.setProperty('flex-direction', 'column-reverse')
+                break
+            }
+            case 'bottom': {
+                el.style.setProperty('flex-direction', 'column')
+                break
+            }
+        }
+    }
+})
+</script>
+
+<style lang="less" scoped>
+@import './index.less';
+</style>

+ 71 - 0
src/packages/qxst/components/base/list/index.less

@@ -0,0 +1,71 @@
+.app-list {
+    min-width: 100%;
+    background-color: #fff;
+    overflow-x: auto;
+    padding: 0 .32rem;
+
+    &__table {
+        width: 100%;
+        text-align: center;
+
+        th {
+            font-size: .24rem;
+            font-weight: normal;
+            color: #999;
+            white-space: nowrap;
+        }
+
+        td {
+            white-space: nowrap;
+        }
+    }
+
+    &__row {
+        position: relative;
+
+        &::after {
+            content: '';
+            position: absolute;
+            bottom: 0;
+            right: 0;
+            left: 0;
+            pointer-events: none;
+            border-bottom: 1px solid #eee;
+            transform: scaleY(.5);
+        }
+    }
+
+    &__column {
+        padding: .12rem .16rem;
+    }
+
+    &__header &__column {
+        padding: 0 .16rem;
+    }
+
+    &__column:first-child &__cell {
+        align-items: flex-start;
+        padding-left: 0;
+    }
+
+    &__column:last-child &__cell {
+        align-items: flex-end;
+        padding-right: 0;
+    }
+
+    &__cell {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        min-height: .72rem;
+
+        .text-small {
+            font-size: .24rem;
+            color: #999;
+        }
+    }
+
+    &__body &__cell {
+        font-size: .28rem;
+    }
+}

+ 59 - 0
src/packages/qxst/components/base/list/index.vue

@@ -0,0 +1,59 @@
+<template>
+    <div class="app-list">
+        <table class="app-list__table" cellspacing="0" cellpadding="0">
+            <!-- <colgroup>
+                <col :width="colWidth" v-for="i in columns.length" :key="i" />
+            </colgroup> -->
+            <thead class="app-list__header" v-if="showHeader">
+                <tr class="app-list__row">
+                    <th class="app-list__column" v-for="(column, i) in columns" :key="i">
+                        <div class="app-list__cell">{{ column.label }}</div>
+                    </th>
+                </tr>
+            </thead>
+            <tbody class="app-list__body">
+                <template v-for="(row, i) in dataList" :key="i">
+                    <tr class="app-list__row" @click="rowClick(row, i)">
+                        <td class="app-list__column" :class="column.className" v-for="(column, n) in columns"
+                            :key="i + n.toString()">
+                            <div class="app-list__cell">
+                                <slot :name="column.prop" :value="row[column.prop]" :row="row">{{ row[column.prop] }}
+                                </slot>
+                            </div>
+                        </td>
+                    </tr>
+                </template>
+            </tbody>
+        </table>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { PropType } from 'vue'
+
+defineProps({
+    dataList: {
+        type: Array,
+        required: true,
+    },
+    columns: {
+        type: Array as PropType<Model.TableColumn[]>,
+        required: true,
+    },
+    showHeader: {
+        type: Boolean,
+        default: true
+    }
+})
+
+const emit = defineEmits(['rowClick'])
+//const colWidth = computed(() => (100 / props.columns.length) + '%')
+
+const rowClick = (row: unknown, index: number) => {
+    emit('rowClick', row, index)
+}
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 40 - 0
src/packages/qxst/components/base/popup/index.less

@@ -0,0 +1,40 @@
+.app-popup {
+    .app-modal__container {
+        min-height: 50%;
+        border-radius: var(--van-popup-round-border-radius, .28rem) var(--van-popup-round-border-radius, .28rem) 0 0;
+        overflow: hidden;
+    }
+
+    .app-view {
+        background-color: #fff;
+    }
+
+    &__header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        background-color: #fff;
+        padding: .32rem;
+        padding-bottom: 0;
+
+        .title {
+            font-size: .32rem;
+            font-weight: bold;
+        }
+    }
+
+    &__container {
+        background-color: #fff;
+
+        padding: .2rem;
+    }
+
+    &__footer {
+        background-color: #fff;
+        padding-top: .48rem;
+
+        &:empty {
+            display: none;
+        }
+    }
+}

+ 45 - 0
src/packages/qxst/components/base/popup/index.vue

@@ -0,0 +1,45 @@
+<template>
+    <app-modal class="app-popup" :show="show" direction="bottom" width="100%" @mask="closed">
+        <app-view class="g-form">
+            <template #header>
+                <slot name="header">
+                    <div class="app-popup__header">
+                        <div class="title">{{ title }}</div>
+                        <Icon name="close" size=".36rem" @click="closed" />
+                    </div>
+                </slot>
+            </template>
+            <div class="app-popup__container">
+                <slot></slot>
+            </div>
+            <template #footer>
+                <div class="g-form__footer app-popup__footer inset">
+                    <slot name="footer"></slot>
+                </div>
+            </template>
+        </app-view>
+    </app-modal>
+</template>
+
+<script lang="ts" setup>
+import { Icon } from 'vant'
+import AppModal from '@/components/base/modal/index.vue'
+
+defineProps({
+    show: {
+        type: Boolean,
+        default: false
+    },
+    title: String
+})
+
+const emit = defineEmits(['update:show'])
+
+const closed = () => {
+    emit('update:show', false)
+}
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 4 - 0
src/packages/qxst/components/base/pull-refresh/index.less

@@ -0,0 +1,4 @@
+.app-pull-refresh {
+    height: 100%;
+    overflow-y: auto;
+}

+ 113 - 0
src/packages/qxst/components/base/pull-refresh/index.vue

@@ -0,0 +1,113 @@
+<template>
+    <PullRefresh class="app-pull-refresh" v-model="refreshing" @refresh="onRefresh">
+        <slot name="header"></slot>
+        <List ref="listRef" v-model:loading="showLoading" v-model:error="showError" :finished="finished" @load="onLoad">
+            <slot></slot>
+            <template #finished>
+                <span>{{ finishedText }}</span>
+            </template>
+            <template #error>
+                <span>{{ errorText }}</span>
+            </template>
+        </List>
+        <slot name="footer"></slot>
+    </PullRefresh>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, computed, nextTick, watch } from 'vue'
+import { List, PullRefresh, ListInstance } from 'vant'
+
+const props = defineProps({
+    loading: {
+        type: Boolean,
+        default: false,
+    },
+    pageIndex: {
+        type: Number,
+        default: 1,
+    },
+    pageCount: {
+        type: Number,
+        default: 1,
+    },
+    error: {
+        type: Boolean,
+        default: false,
+    },
+    finishedText: {
+        type: String,
+        default: '没有更多了',
+    },
+    errorText: {
+        type: String,
+        default: '请求失败,点击重新加载',
+    }
+})
+
+const emit = defineEmits(['update:loading', 'update:pageIndex', 'update:error', 'refresh'])
+const listRef = shallowRef<ListInstance>()
+const refreshing = shallowRef(false) // 是否处于下拉加载状态
+const finished = shallowRef(false) // 是否已加载完成所有数据
+
+const showLoading = computed({
+    get: () => props.loading,
+    set: (val) => emit('update:loading', val)
+})
+
+const showError = computed({
+    get: () => props.error,
+    set: (val) => emit('update:error', val)
+})
+
+const currentPage = computed({
+    get: () => props.pageIndex,
+    set: (val) => emit('update:pageIndex', val)
+})
+
+// 上拉加载
+const onLoad = () => {
+    if (refreshing.value) {
+        currentPage.value = 1
+    }
+    nextTick(() => {
+        if (currentPage.value <= props.pageCount) {
+            emit('refresh')
+        } else {
+            refreshing.value = false
+            finished.value = true
+        }
+    })
+}
+
+// 下拉刷新
+const onRefresh = () => {
+    // 下拉刷新过程中,可能会触发上拉加载,导致发送两次请求,所以先将 finished 设为 true,完成后再设为 false
+    finished.value = true
+    onLoad()
+}
+
+watch(showLoading, (status) => {
+    if (!status) {
+        finished.value = false
+        if (refreshing.value) {
+            refreshing.value = false
+        }
+        if (!showError.value) {
+            currentPage.value++
+        }
+    }
+})
+
+// 暴露组件属性给父组件调用
+defineExpose({
+    refresh: () => {
+        currentPage.value = 1
+        onRefresh()
+    },
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 10 - 0
src/packages/qxst/components/base/qrcode-scan/index.less

@@ -0,0 +1,10 @@
+.app-qrcode-scan {
+    &__camera {
+        position: fixed;
+        bottom  : 0;
+        left    : 0;
+        z-index : 1000;
+        width   : 100vw;
+        height  : 30vh;
+    }
+}

+ 79 - 0
src/packages/qxst/components/base/qrcode-scan/index.vue

@@ -0,0 +1,79 @@
+<template>
+    <div class="app-qrcode-scan">
+        <div @click="scan">
+            <slot>
+                <Button type="primary">扫一扫</Button>
+            </slot>
+        </div>
+        <div id="camera" class="app-qrcode-scan__camera"></div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, shallowRef } from 'vue'
+import { Button, showFailToast } from 'vant'
+import { Html5Qrcode } from 'html5-qrcode'
+import plus from '@/utils/h5plus'
+
+const emit = defineEmits(['success'])
+const html5QrCode = shallowRef<Html5Qrcode>()
+const showCamera = shallowRef(false)
+
+const scan = () => {
+    plus.onPlusReady((plus) => {
+        const barcode = plus.barcode.create('barcode', [plus.barcode.QR], {
+            background: '#fff',
+            frameColor: '#07c160',
+            scanbarColor: '#07c160',
+            top: '100px',
+            left: '0px',
+            width: '100%',
+            height: '580px',
+            position: 'static'
+        });
+        barcode.onmarked = (type: any, result: any) => {
+            console.log('扫码成功', type, result)
+            emit('success', result)
+            barcode.close()
+        }
+        barcode.onerror = (err: any) => {
+            console.log('扫码失败', err)
+            showFailToast('扫码失败')
+        }
+        plus.webview.currentWebview().append(barcode);//必要的
+    })
+}
+
+const onScan = () => {
+    Html5Qrcode.getCameras().then(devices => {
+        if (devices && devices.length) {
+            html5QrCode.value?.start(
+                {
+                    facingMode: 'environment'
+                },
+                {
+                    fps: 10,
+                    qrbox: { width: 250, height: 250 }
+                },
+                (decodedText) => {
+                    emit('success', decodedText)
+                },
+                (err) => {
+                    console.log('html5QrCode', err)
+                }).catch(() => {
+                    showFailToast('获取设备信息失败')
+                })
+        }
+    }).catch(() => {
+        showFailToast('您需要授予相机访问权限')
+    })
+}
+
+onMounted(() => {
+    html5QrCode.value = new Html5Qrcode('camera')
+})
+</script>
+
+<style lang="less" scoped>
+@import './index.less';
+</style>

+ 13 - 0
src/packages/qxst/components/base/region/index.less

@@ -0,0 +1,13 @@
+.app-region {
+    flex       : 1;
+    display    : flex;
+    align-items: center;
+
+    &__input {
+        flex: 1;
+    }
+
+    input {
+        width: 100%;
+    }
+}

+ 103 - 0
src/packages/qxst/components/base/region/index.vue

@@ -0,0 +1,103 @@
+<template>
+    <div class="app-region" @click="onClick">
+        <slot :label="inputValue">
+            <input class="app-region__input" v-model="inputValue" :placeholder="placeholder" readonly />
+        </slot>
+        <Popup v-model:show="show" position="bottom" teleport="body" round>
+            <Cascader :title="title" v-model="selectedValue" :options="options" @close="show = false" @change="onChange"
+                @finish="onFinish" />
+        </Popup>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, computed, watch } from 'vue'
+import { Popup, Cascader, CascaderOption } from 'vant'
+import axios from 'axios'
+
+const props = defineProps({
+    modelValue: {
+        type: [String, Number],
+        default: 0
+    },
+    label: {
+        type: String,
+    },
+    title: {
+        type: String,
+        default: '请选择地区'
+    },
+    readonly: {
+        type: Boolean,
+        default: false
+    },
+    placeholder: {
+        type: String,
+        default: '请选择'
+    },
+})
+
+const emit = defineEmits(['update:modelValue', 'update:label', 'change', 'finish'])
+const show = shallowRef(false) // 是否弹出选择器
+const inputValue = shallowRef(props.label)
+const options = shallowRef<CascaderOption[]>([])
+
+// 选中的值
+const selectedValue = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+})
+
+const onClick = () => {
+    if (!props.readonly) {
+        show.value = true
+    }
+}
+
+// 选中项变化时触发
+const onChange = ({ value }: { value: string | number }) => {
+    emit('change', value)
+}
+
+// 全部选项选择完毕后触发
+const onFinish = ({ selectedOptions }: { selectedOptions: CascaderOption[] }) => {
+    const selection = selectedOptions.map((e) => e.value)
+    show.value = false
+    inputValue.value = selectedOptions.map((option) => option.text).join(' ')
+
+    emit('update:modelValue', [...selection].pop())
+    emit('update:label', inputValue.value)
+    emit('finish', selection)
+}
+
+// 扁平列表树形化
+const arrayToTree = (list: Model.Region[]) => {
+    const getChildren = (parent?: string) => {
+        const result: CascaderOption[] = []
+        list.forEach((e) => {
+            if (e.parentcode === parent) {
+                const children = getChildren(e.divisioncode)
+                result.push({
+                    text: e.divisionname,
+                    value: e.autoid,
+                    children: children.length ? children : undefined,
+                })
+            }
+        })
+        return result
+    }
+    return getChildren('0086')
+}
+
+watch(() => props.label, (val) => {
+    inputValue.value = val
+})
+
+axios('./config/address.json').then((res) => {
+    options.value = arrayToTree(res.data)
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 39 - 0
src/packages/qxst/components/base/router-transition/index.backup.less

@@ -0,0 +1,39 @@
+/* 动画时长 */
+@transition-duration: 220ms;
+
+/* 无过渡效果 */
+.delay-enter-active,
+.delay-leave-active {
+    transition-duration: @transition-duration;
+}
+
+.route-in-enter-active,
+.route-in-leave-active,
+.route-out-enter-active,
+.route-out-leave-active {
+    pointer-events: none;
+    position: absolute;
+    transition-property: transform;
+    transition-duration: @transition-duration;
+    background-color: #fff;
+}
+
+.route-in-enter-from {
+    z-index: 1;
+    transform: translate3d(100%, 0, 0);
+}
+
+.route-out-enter-from {
+    z-index: 1;
+    transform: translate3d(-100%, 0, 0);
+}
+
+.route-out-leave-active {
+    transform: translate3d(100%, 0, 0);
+    transition-timing-function: ease-in;
+}
+
+.route-in-leave-active {
+    transform: translate3d(-100%, 0, 0);
+    transition-timing-function: ease-in;
+}

+ 83 - 0
src/packages/qxst/components/base/router-transition/index.less

@@ -0,0 +1,83 @@
+/* 动画时长 */
+@transition-duration: 220ms;
+
+/* 无过渡效果 */
+.delay-enter-active,
+.delay-leave-active {
+    transition-duration: @transition-duration;
+}
+
+.route-in-leave-from,
+.route-in-enter-from,
+.route-out-enter-from,
+.route-out-enter-from {
+    will-change: transform, opacity;
+}
+
+.route-in-leave-to,
+.route-in-enter-active,
+.route-out-leave-active,
+.route-out-enter-to {
+    pointer-events: none;
+    transition: transform @transition-duration;
+}
+
+.route-in-leave-from {
+    &::after {
+        content: '';
+        opacity: 0;
+    }
+}
+
+.route-in-leave-to {
+    &::after {
+        content: '';
+        position: absolute;
+        z-index: 100;
+        top: 0;
+        left: 0;
+        width: 100vw;
+        height: 100vh;
+        background-color: rgba(0, 0, 0, .5);
+        opacity: 1;
+        transition: opacity @transition-duration;
+    }
+}
+
+.route-in-enter-from {
+    transform: translate3d(100%, 0, 0);
+}
+
+.route-in-enter-active {
+    position: absolute;
+    z-index: 1000;
+    top: 0;
+}
+
+.route-out-leave-active {
+    position: absolute;
+    z-index: 1000;
+    transform: translate3d(100%, 0, 0);
+}
+
+.route-out-enter-from {
+    &::after {
+        content: '';
+        opacity: 1;
+    }
+}
+
+.route-out-enter-to {
+    &::after {
+        content: '';
+        position: absolute;
+        z-index: 100;
+        top: 0;
+        left: 0;
+        width: 100vw;
+        height: 100vh;
+        background-color: rgba(0, 0, 0, .5);
+        opacity: 0;
+        transition: opacity @transition-duration;
+    }
+}

+ 18 - 0
src/packages/qxst/components/base/router-transition/index.vue

@@ -0,0 +1,18 @@
+<template>
+    <transition :name="transitionName">
+        <slot></slot>
+    </transition>
+</template>
+  
+<script lang="ts" setup>
+defineProps({
+    transitionName: {
+        type: String,
+        default: 'delay'
+    }
+})
+</script>
+  
+<style lang="less">
+@import './index.less';
+</style>

+ 13 - 0
src/packages/qxst/components/base/select/index.less

@@ -0,0 +1,13 @@
+.app-select {
+    flex       : 1;
+    display    : flex;
+    align-items: center;
+
+    &__input {
+        flex: 1;
+    }
+
+    input {
+        width: 100%;
+    }
+}

+ 96 - 0
src/packages/qxst/components/base/select/index.vue

@@ -0,0 +1,96 @@
+<template>
+    <div class="app-select" @click="onClick">
+        <slot :label="inputValue">
+            <input class="'app-select__input'" v-model="inputValue" :placeholder="placeholder" readonly />
+        </slot>
+        <Popup v-model:show="show" position="bottom" teleport="body" round>
+            <Picker :columns="columns" @cancel="onCancel" @confirm="onConfirm" />
+        </Popup>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, computed, PropType, watch } from 'vue'
+import { Popup, Picker, PickerConfirmEventParams, FieldInstance } from 'vant'
+
+const props = defineProps({
+    modelValue: {
+        type: [Number, String]
+    },
+    options: {
+        type: Array,
+        default: () => ([])
+    },
+    optionProps: {
+        type: Object as PropType<{ label?: string; value?: string; }>,
+        default: () => ({
+            label: 'label',
+            value: 'value'
+        })
+    },
+    readonly: {
+        type: Boolean,
+        default: false
+    },
+    placeholder: {
+        type: String,
+        default: '请选择'
+    },
+})
+
+const emit = defineEmits(['update:show', 'update:modelValue', 'confirm'])
+const fieldRef = shallowRef<FieldInstance>()
+const show = shallowRef(false) // 是否弹出选择器
+const selectedIndex = shallowRef(-1)
+
+const columns = computed(() => {
+    if (props.options) {
+        return props.options.map((e) => ({
+            text: e[props.optionProps.label],
+            value: e[props.optionProps.value],
+        }))
+    }
+    return []
+})
+
+// 当前输入框的值
+const inputValue = computed(() => {
+    const item = props.options[selectedIndex.value]
+    if (item) {
+        return item[props.optionProps.label] ?? ''
+    }
+    return ''
+})
+
+const onClick = () => {
+    if (!props.readonly) {
+        show.value = true
+    }
+}
+
+const onCancel = () => {
+    show.value = false
+}
+
+const onConfirm = ({ selectedIndexes: [index], selectedValues: [value] }: PickerConfirmEventParams) => {
+    show.value = false
+    if (selectedIndex.value !== index) {
+        // 更新当前选中的值
+        selectedIndex.value = index
+        fieldRef.value?.validate()
+
+        emit('update:modelValue', value)
+        emit('confirm', value, index)
+    }
+}
+
+watch(() => [props.modelValue, props.options], ([value, items]) => {
+    selectedIndex.value = items.findIndex((e) => e[props.optionProps.value]?.toString() === value?.toString())
+}, {
+    immediate: true
+})
+</script>
+
+<style lang="less" scoped>
+@import './index.less';
+</style>

+ 47 - 0
src/packages/qxst/components/base/tabbar/index.less

@@ -0,0 +1,47 @@
+.app-tabbar {
+    height          : 1rem;
+    background-color: #fff;
+
+    &__wrapper {
+        bottom    : 0;
+        display   : flex;
+        width     : 100%;
+        height    : ~"calc(1rem - 1px)";
+        border-top: 1px solid #eee;
+        box-sizing: content-box;
+    }
+
+    &__item {
+        flex           : 1;
+        display        : flex;
+        flex-direction : column;
+        justify-content: center;
+        align-items    : center;
+        height         : inherit;
+        cursor         : pointer;
+
+        [class^='g-icon'] {
+            display        : flex;
+            flex-direction : column;
+            justify-content: center;
+            align-items    : center;
+
+            img {
+                width     : .44rem;
+                height    : .44rem;
+                object-fit: contain;
+            }
+
+            span {
+                font-size : .24rem;
+                margin-top: .05rem;
+            }
+
+            &:before {
+                width    : .44rem;
+                height   : .44rem;
+                font-size: .44rem;
+            }
+        }
+    }
+}

+ 69 - 0
src/packages/qxst/components/base/tabbar/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="app-tabbar">
+    <div class="app-tabbar__wrapper" :style="styles">
+      <template v-for="(item, index) in dataList" :key="index">
+        <div :class="['app-tabbar__item', 'app-tabbar__item--' + item.name, dataIndex === index && 'is-active']"
+          @click="onClick(index)">
+          <slot :item="item" :index="index">
+            <!--判断是否图片图标-->
+            <template v-if="item.iconType === 'image'">
+              <div :class="['g-icon', dataIndex === index && 'active']">
+                <img :src="item.activeIcon" v-if="dataIndex === index" />
+                <img :src="item.icon" v-else />
+                <span>{{ item.label }}</span>
+              </div>
+            </template>
+            <template v-else>
+              <app-iconfont label-direction="bottom" :icon="item.icon" :active-icon="item.activeIcon"
+                :active="dataIndex === index">{{ item.label }}</app-iconfont>
+            </template>
+          </slot>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { PropType, computed } from 'vue'
+import { Tabbar } from './types'
+import { useGlobalStore } from '@/stores'
+import AppIconfont from '../../base/iconfont/index.vue'
+
+const emit = defineEmits(['click'])
+
+const props = defineProps({
+  // 数据列表
+  dataList: {
+    type: Array as PropType<Tabbar[]>,
+    default: () => ([]),
+  },
+  // 当前标签索引
+  dataIndex: {
+    type: Number,
+    default: 0,
+  },
+  // 是否固定在底部
+  fixed: {
+    type: Boolean,
+    default: false,
+  }
+})
+
+const globalStore = useGlobalStore()
+
+const styles = computed(() => ({
+  position: props.fixed ? 'fixed' : 'static',
+  width: globalStore.clientWidth + 'px',
+}))
+
+const onClick = (index: number) => {
+  if (props.dataIndex !== index) {
+    emit('click', index)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+@import './index.less';
+</style>

+ 7 - 0
src/packages/qxst/components/base/tabbar/types.ts

@@ -0,0 +1,7 @@
+export interface Tabbar {
+    name: string;
+    label: string;
+    iconType?: 'iconfont' | 'image';
+    icon: string;
+    activeIcon?: string;
+}

+ 28 - 0
src/packages/qxst/components/base/table/index.less

@@ -0,0 +1,28 @@
+.app-table {
+    overflow: auto;
+
+    &__body {
+        width           : 100%;
+        text-align      : center;
+        background-color: #fff;
+        margin-bottom   : .2rem;
+        border          : 0;
+    }
+
+    & thead &__row {
+        background-color: #d9e2ed;
+    }
+
+    & thead &__cell {
+        padding: .12rem 0;
+    }
+
+    & tbody &__row {
+        background-color: #fff
+    }
+
+    & tbody &__cell {
+        color  : #666;
+        padding: .12rem 0;
+    }
+}

+ 72 - 0
src/packages/qxst/components/base/table/index.vue

@@ -0,0 +1,72 @@
+<template>
+    <div class="app-table">
+        <table class="app-table__body" cellspacing="0" cellpadding="0">
+            <thead>
+                <Draggable class="app-table__row" :list="columns" tag="tr" item-key="key">
+                    <template #header v-if="$slots.expand">
+                        <th class="app-table__cell expand"></th>
+                    </template>
+                    <template #item="{ element }">
+                        <th class="app-table__cell">{{ element.label }}</th>
+                    </template>
+                </Draggable>
+            </thead>
+            <tbody>
+                <template v-for="(row, i) in dataList" :key="i">
+                    <tr class="app-table__row" @click="rowClick(i)">
+                        <td class="app-table__cell expand" v-if="$slots.expand">
+                            <Icon name="arrow-down" v-if="selectedIndex === i" />
+                            <Icon name="arrow" v-else />
+                        </td>
+                        <td class="app-table__cell" :class="column.className" v-for="(column, n) in columns"
+                            :key="i + n.toString()">
+                            <slot :name="column.prop" :value="row[column.prop]" :row="row">{{ row[column.prop] }}</slot>
+                        </td>
+                    </tr>
+                    <!-- expand -->
+                    <tr class="app-table__row expand" v-show="selectedIndex === i" v-if="$slots.expand">
+                        <td class="app-table__cell" :colspan="columns.length + 1">
+                            <slot name="expandRow" :row="row"></slot>
+                        </td>
+                    </tr>
+                </template>
+            </tbody>
+        </table>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { PropType, ref } from 'vue'
+import { Icon } from 'vant'
+import { TableColumn } from './types'
+import Draggable from 'vuedraggable'
+
+const emit = defineEmits(['rowClick'])
+
+const props = defineProps({
+    // 数据列表
+    dataList: {
+        type: Array,
+        default: () => ([]),
+    },
+    columns: {
+        type: Array as PropType<TableColumn[]>,
+        default: () => ([]),
+    },
+})
+
+const selectedIndex = ref(-1);
+
+const rowClick = (index: number) => {
+    if (selectedIndex.value === index) {
+        selectedIndex.value = -1;
+    } else {
+        selectedIndex.value = index;
+    }
+    emit('rowClick', index, props.dataList[index]);
+}
+</script>
+
+<style lang="less" scoped>
+@import './index.less';
+</style>

+ 8 - 0
src/packages/qxst/components/base/table/types.ts

@@ -0,0 +1,8 @@
+export interface TableColumn {
+    prop: string;
+    label: string;
+    className?: string;
+    align?: string;
+    width?: number;
+    sort?: number;
+}

+ 43 - 0
src/packages/qxst/components/base/uploader/index.vue

@@ -0,0 +1,43 @@
+<template>
+    <Uploader v-model="fileList" :max-count="1" :max-size="5 * 1024 * 1024" @oversize="onOversize" :after-read="afterRead"
+        @delete="onDelete" />
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { showFailToast, Uploader, UploaderFileListItem } from 'vant'
+import service from '@/services'
+import axios from 'axios'
+
+const emit = defineEmits(['success'])
+const fileList = ref<UploaderFileListItem[]>([])
+
+const onOversize = () => {
+    showFailToast('图片大小不能超过 5Mb')
+}
+
+// eslint-disable-next-line
+const afterRead = (file: any) => {
+    const data = new FormData()
+    data.append('file', file.file)
+
+    file.status = 'uploading'
+    file.message = '上传中...'
+    axios.post(service.getConfig('uploadUrl'), data).then(res => {
+        if (res.status == 200) {
+            file.status = 'success'
+            file.message = '上传成功'
+            if (res.data.length) {
+                emit('success', res.data[0].filePath)
+            }
+        } else {
+            file.status = 'failed'
+            file.message = '上传失败'
+        }
+    })
+}
+
+const onDelete = () => {
+    emit('success', '')
+}
+</script>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác