li.shaoyi 3 년 전
부모
커밋
bf06ea01d7

+ 7 - 5
src/components/base/echarts/index.ts

@@ -52,13 +52,15 @@ export function useEcharts() {
             }, 50);
 
             // 监听元素变化
-            new ResizeObserver(resize).observe(el);
+            const resizeObserver = new ResizeObserver(resize);
+            resizeObserver.observe(el);
             context?.emit('ready', chart);
-        }
-    })
 
-    onUnmounted(() => {
-        chart?.dispose();
+            onUnmounted(() => {
+                resizeObserver.unobserve(el);
+                chart.dispose();
+            })
+        }
     })
 
     return {

+ 33 - 0
src/components/base/table/drag.ts

@@ -0,0 +1,33 @@
+export function useDrag() {
+    const scroll = {
+        x: 0,
+        y: 0
+    }
+
+    // 开始拖动
+    const dragStart = (e: DragEvent) => {
+        scroll.x = e.clientX;
+        scroll.y = e.clientY;
+    }
+
+    // 正在拖动
+    const drag = (e: DragEvent) => {
+        const { style } = e.target as HTMLDivElement;
+        const x = e.clientX - scroll.x; // 计算偏移量
+
+        style.setProperty('transform', 'translateX(' + x + 'px)');
+    }
+
+    const dragEnd = (e: DragEvent) => {
+        const { style } = e.target as HTMLDivElement;
+        const x = e.clientX - scroll.x; // 计算偏移量
+
+        style.setProperty('transform', 'translateX(' + x + 'px)');
+    }
+
+    return {
+        dragStart,
+        drag,
+        dragEnd,
+    }
+}

+ 35 - 0
src/components/base/table/index.less

@@ -0,0 +1,35 @@
+.cat-table {
+    display       : flex;
+    flex-direction: column;
+
+    &__wrapper {
+        >table {
+            min-width: 100%;
+
+            td,
+            th {
+                border : 1px solid #666;
+                padding: 10px;
+            }
+
+            .cell {
+                text-overflow: ellipsis;
+                white-space  : normal;
+                word-break   : break-all;
+                text-align   : center;
+            }
+        }
+    }
+}
+
+.cat-scrollbar {
+    font-size  : 0;
+    line-height: 0;
+
+    &__thumb {
+        display         : inline-block;
+        height          : 8px;
+        border-radius   : 4px;
+        background-color: #666;
+    }
+}

+ 161 - 0
src/components/base/table/index.ts

@@ -0,0 +1,161 @@
+import { defineComponent, h, ref, onMounted, onUnmounted, PropType, watch } from 'vue'
+import ResizeObserver from 'resize-observer-polyfill'
+import { useDrag } from './drag'
+import './index.less'
+
+interface CatTableColumn {
+    key: string;
+    label: string;
+    width: number;
+}
+
+/**
+ * 通用表格组件(开发中)
+ */
+export default defineComponent({
+    props: {
+        columns: {
+            type: Array as PropType<CatTableColumn[]>,
+            default: () => ([])
+        },
+        rows: {
+            default: () => ([])
+        }
+    },
+    setup(props) {
+        const { dragStart, drag, dragEnd } = useDrag();
+        const tableElement = ref<HTMLDivElement>();
+        const headerElement = ref<HTMLTableElement>();
+        const bodyElement = ref<HTMLTableElement>();
+        const headerColumns = ref<CatTableColumn[]>([]);
+        const scrollbarWidth = ref(0); // 滚动条宽度
+
+        const tableStyle = () => ({
+            width: headerColumns.value.reduce((pre, cur) => pre += cur.width, 0) + 'px'
+        })
+
+        const renderColGroup = () => h('colgroup', headerColumns.value.map((column) => h('col', { width: column.width })));
+
+        const renderHeader = () => h('div', {
+            class: 'cat-table__wrapper'
+        }, [
+            h('table', {
+                ref: headerElement,
+                class: 'cat-table__header',
+                cellspacing: 0,
+                cellpadding: 0,
+                style: tableStyle(),
+            }, [
+                renderColGroup(),
+                h('thead', h('tr', headerColumns.value.map((column) => h('th', h('div', { class: 'cell' }, column.label)))))
+            ])
+        ])
+
+        const renderBody = () => h('div', {
+            class: 'cat-table__wrapper'
+        }, [
+            h('table', {
+                ref: bodyElement,
+                class: 'cat-table__body',
+                cellspacing: 0,
+                cellpadding: 0,
+                style: tableStyle(),
+            }, [
+                renderColGroup(),
+                h('tbody', props.rows.map((row) => {
+                    return h('tr', headerColumns.value.map((column) => h('td', h('div', { class: 'cell' }, row[column.key]))))
+                }))
+            ]),
+            ...scrollbarWidth.value > 0 ? [renderScrollbar()] : [],
+        ])
+
+        const renderScrollbar = () => h('div', {
+            class: 'cat-scrollbar',
+        }, h('div', {
+            class: 'cat-scrollbar__thumb',
+            draggable: true,
+            ondragstart: dragStart,
+            ondrag: drag,
+            ondragend: dragEnd,
+            style: {
+                width: scrollbarWidth.value + 'px'
+            },
+        }))
+
+        const columnResize = (() => {
+            let timer = 0;
+            let prevWidth = 0; // 记录上次宽度
+
+            return () => {
+                const table = tableElement.value;
+                if (table) {
+                    const miniWidth = 48; // 限制列宽最小宽度
+                    headerColumns.value = [];
+
+                    // 计算出列宽的组合数据
+                    const colgroup = props.columns.reduce((pre, cur) => {
+                        if (cur.width > miniWidth) {
+                            pre.width -= cur.width;
+                            pre.length -= 1;
+                        }
+                        return pre;
+                    }, {
+                        width: table.clientWidth,
+                        length: props.columns.length,
+                    })
+
+                    // 列宽平均分配
+                    props.columns.forEach((column) => {
+                        const item = { ...column };
+                        if (!item.width) {
+                            const width = Math.floor(colgroup.width / colgroup.length);
+                            item.width = width > miniWidth ? width : miniWidth;
+                        }
+                        headerColumns.value.push(item)
+                    })
+
+                    // 判断是否显示滚动条
+                    if (colgroup.width !== prevWidth) {
+                        clearTimeout(timer);
+                        scrollbarWidth.value = 0;
+
+                        timer = window.setTimeout(() => {
+                            prevWidth = colgroup.width;
+                            const body = bodyElement.value;
+
+                            if (body) {
+                                const ratio = table.clientWidth / body.clientWidth;
+                                if (ratio < 1) {
+                                    scrollbarWidth.value = table.clientWidth * ratio;
+                                }
+                            }
+                        }, 200)
+                    }
+                }
+            }
+        })()
+
+        watch(() => props.columns, () => columnResize());
+
+        onMounted(() => {
+            const el = tableElement.value;
+            if (el) {
+                // 监听元素变化
+                const resizeObserver = new ResizeObserver(columnResize);
+                resizeObserver.observe(el);
+
+                onUnmounted(() => {
+                    resizeObserver.unobserve(el);
+                })
+            }
+        })
+
+        return () => h('div', {
+            ref: tableElement,
+            class: 'cat-table'
+        }, [
+            ...props.columns.length ? [renderHeader()] : [],
+            ...props.rows.length ? [renderBody()] : [],
+        ])
+    }
+})

+ 2 - 2
src/packages/mobile/components/base/tabbar/index.vue

@@ -50,11 +50,11 @@ const props = defineProps({
 })
 
 const selectedIndex = ref(props.dataIndex);
-const { clientWidth } = client.getState();
+const { state } = client;
 
 const styles = computed(() => ({
   position: props.fixed ? 'fixed' : 'static',
-  width: clientWidth.value + 'px',
+  width: state.clientWidth + 'px',
 }))
 
 const onChange = (index: number) => {

+ 1 - 1
src/packages/mobile/components/base/table/index.vue

@@ -26,7 +26,7 @@
                     <!-- 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="expand" :row="row"></slot>
+                            <slot name="expandRow" :row="row"></slot>
                         </td>
                     </tr>
                 </template>

+ 2 - 2
src/packages/mobile/components/layouts/navbar/index.vue

@@ -54,12 +54,12 @@ const props = defineProps({
 
 const router = useRouter();
 const attrs = useAttrs();
-const { clientWidth } = client.getState();
+const { state } = client;
 
 const styles = computed(() => ({
   position: props.fixed ? 'fixed' : 'static',
   zIndex: props.fixed ? '1' : 'auto',
-  width: clientWidth.value + 'px',
+  width: state.clientWidth + 'px',
 }))
 
 // 返回按钮事件

+ 4 - 4
src/packages/mobile/components/layouts/page/index.vue

@@ -1,7 +1,7 @@
 <template>
   <router-view class="app-page" v-slot="{ Component, route }">
-    <transition :name="transitionName">
-      <keep-alive :exclude="excludeName">
+    <transition :name="state.transitionName">
+      <keep-alive :exclude="state.excludeName">
         <component :is="handleComponent(Component, route)" :key="$route.fullPath" />
       </keep-alive>
     </transition>
@@ -12,7 +12,7 @@
 import { RouteRecordNormalized, RouteRecordName } from 'vue-router';
 import animateRouter from '@mobile/router/animateRouter'
 
-const { transitionName, excludeName } = animateRouter.getState();
+const { state } = animateRouter;
 
 // 手动给组件添加 name 属性,处理缓存 exclude 无效的问题
 const handleComponent = (component: Record<'type', { name: RouteRecordName | undefined }>, route: RouteRecordNormalized) => {
@@ -23,6 +23,6 @@ const handleComponent = (component: Record<'type', { name: RouteRecordName | und
 }
 </script>
 
-<style lang="less" scoped>
+<style lang="less">
 @import './index.less';
 </style>

+ 76 - 64
src/packages/mobile/router/animateRouter.ts

@@ -1,71 +1,30 @@
-import { reactive, toRefs, readonly } from 'vue'
+import { ref, toRefs, readonly } from 'vue'
 import { createRouter, RouterOptions, RouteRecordRaw, RouteLocationNormalized } from 'vue-router'
 
-interface RouterState {
-    historyStack: RouteLocationNormalized[]; // 已访问的路由列表
+interface HistoryState {
+    historyStacks: RouteLocationNormalized[]; // 已访问的路由列表
     excludeName: string[]; // 不缓存的组件名称
     actionName: '' | 'push' | 'replace' | 'forward' | 'back'; // 当前路由动作
     transitionName: '' | 'slide-right' | 'slide-left'; // 前进后退动画
 }
 
 export default new (class {
-    private state = reactive<RouterState>({
-        historyStack: [],
+    private _state = ref<HistoryState>({
+        historyStacks: [],
         excludeName: [],
         actionName: '',
         transitionName: '',
     })
 
-    /**
-     * 处理路由历史状态
-     * @param route 
-     */
-    private handleState(route: RouteLocationNormalized) {
-        this.state.excludeName = [];
-        const { historyStack, actionName } = this.state;
-
-        // 如果是替换动作,必定是前进
-        if (actionName === 'replace') {
-            const lastIndex = historyStack.length - 1;
-            const lastPage = historyStack[lastIndex];
-
-            this.state.excludeName.push(lastPage.name as string);
-            this.state.historyStack.fill(route, lastIndex); // 替换最后一个位置
-            this.state.transitionName = 'slide-left'; // 前进动画
-        } else {
-            // 倒序查找路由所在的位置
-            const index = (() => {
-                for (let i = historyStack.length - 1; i >= 0; i--) {
-                    if (historyStack[i].fullPath == route.fullPath) {
-                        return i;
-                    }
-                }
-                return -1;
-            })();
+    /** 只读状态 */
+    state;
 
-            if (index > -1) {
-                if (actionName === 'push') {
-                    this.state.historyStack.push(route);
-                    this.state.transitionName = 'slide-left'; //前进动画
-                } else {
-                    if (historyStack.length > 1) {
-                        const i = index + 1;
-                        const n = historyStack.length - i;
-
-                        this.state.excludeName = historyStack.map((e) => e.name).slice(-n) as string[]; // 返回数组最后位置开始的n个元素
-                        this.state.historyStack.splice(i, n); // 从i位置开始删除后面所有元素(包括i)
-                    }
-                    this.state.transitionName = 'slide-right'; //后退动画
-                }
-            } else {
-                historyStack.push(route);
-                if (historyStack.length > 1) {
-                    this.state.transitionName = 'slide-left'; // 前进动画
-                }
-            }
+    constructor() {
+        const state = sessionStorage.getItem('historyState');
+        if (state) {
+            this._state.value = JSON.parse(state);
         }
-
-        this.state.actionName = '';
+        this.state = readonly(toRefs(this._state.value));
     }
 
     /**
@@ -76,54 +35,107 @@ export default new (class {
     create(options: RouterOptions) {
         const router = createRouter(options);
         const { push, replace, go, forward, back } = router;
+        const { actionName } = toRefs(this._state.value);
 
         // 添加
         router.push = (to: RouteRecordRaw) => {
-            this.state.actionName = 'push';
+            actionName.value = 'push';
             return push(to);
         }
 
         // 替换
         router.replace = (to: RouteRecordRaw) => {
-            this.state.actionName = 'replace';
+            actionName.value = 'replace';
             return replace(to);
         }
 
         // 前进后退
         router.go = (delta: number) => {
             if (delta > 0) {
-                this.state.actionName = 'forward';
+                actionName.value = 'forward';
             }
             if (delta < 0) {
-                this.state.actionName = 'back';
+                actionName.value = 'back';
             }
             go(delta);
         }
 
         // 前进
         router.forward = () => {
-            this.state.actionName = 'forward';
+            actionName.value = 'forward';
             forward();
         }
 
         // 后退
         router.back = () => {
-            this.state.actionName = 'back';
+            actionName.value = 'back';
             back();
         }
 
         router.beforeResolve((to) => {
-            this.handleState(to);
+            this.add(to);
         })
 
         return router;
     }
 
     /**
-     * 获取路由历史状态(只读)
-     * @returns 
+     * 添加历史记录
+     * @param route 
      */
-    getState() {
-        return toRefs(readonly(this.state));
+    private add(route: RouteLocationNormalized) {
+        const { historyStacks, excludeName, actionName, transitionName } = toRefs(this._state.value);
+        excludeName.value = [];
+
+        // 如果是替换动作,必定是前进
+        if (actionName.value === 'replace') {
+            const lastIndex = historyStacks.value.length - 1;
+            const lastPage = historyStacks.value[lastIndex];
+
+            if (lastPage) {
+                excludeName.value.push(lastPage.name as string);
+                historyStacks.value[lastIndex] = route; // 更新最后一条记录
+            } else {
+                historyStacks.value.push(route);
+            }
+            transitionName.value = 'slide-left'; // 前进动画
+        } else {
+            // 倒序查找路由所在的位置
+            const index = (() => {
+                for (let i = historyStacks.value.length - 1; i >= 0; i--) {
+                    if (historyStacks.value[i].fullPath == route.fullPath) {
+                        return i;
+                    }
+                }
+                return -1;
+            })();
+
+            if (index > -1) {
+                if (actionName.value === 'push') {
+                    historyStacks.value.push(route);
+                    transitionName.value = 'slide-left'; //前进动画
+                } else {
+                    if (historyStacks.value.length > 1) {
+                        const i = index + 1;
+                        const n = historyStacks.value.length - i;
+
+                        excludeName.value = historyStacks.value.map((e) => e.name).slice(-n) as string[]; // 返回数组最后位置开始的n个元素
+                        historyStacks.value.splice(i, n); // 从i位置开始删除后面所有元素(包括i)
+                    }
+                    transitionName.value = 'slide-right'; //后退动画
+                }
+            } else {
+                // 忽略重定向的页面
+                if (!route.redirectedFrom) {
+                    historyStacks.value.push(route);
+                    if (historyStacks.value.length > 1) {
+                        transitionName.value = 'slide-left'; // 前进动画
+                    }
+                }
+            }
+        }
+
+        actionName.value = '';
+        sessionStorage.setItem('historyState', JSON.stringify(this._state.value));
     }
 })

+ 2 - 2
src/packages/mobile/views/account/login/index.vue

@@ -26,8 +26,8 @@ import { useAccount } from '@/business/account'
 import animateRouter from '@mobile/router/animateRouter'
 
 const { loading, account, userLogin } = useAccount();
-const { historyStack } = animateRouter.getState();
-const showBackButton = computed(() => historyStack.value.length > 1);
+const { state } = animateRouter;
+const showBackButton = computed(() => state.historyStacks.length > 1);
 
 const loginAction = () => {
   userLogin().then(() => notify('登录成功', 'success')).catch((err) => notify(err, 'danger'));

+ 1 - 1
src/packages/mobile/views/home/components/market/index.vue

@@ -6,7 +6,7 @@
         <GridItem icon="photo-o" text="商品" :to="{ name: 'order' }" v-for="index in 6" :key="index" />
       </Grid>
       <app-table :data-list="quoteList" :columns="columns" @row-click="rowClick">
-        <template #expand>
+        <template #expandRow>
           扩展
         </template>
         <template #last="{ row }">

+ 1 - 1
src/packages/pc/components/layouts/page/index.vue

@@ -26,6 +26,6 @@ import AppSidebar from "../sidebar/index.vue";
 const sidebarCollapse = ref(false);
 </script>
 
-<style lang="less" scoped>
+<style lang="less">
 @import './index.less';
 </style>

+ 3 - 0
src/packages/pc/views/market/quote/index.vue

@@ -11,6 +11,7 @@
     </app-table>
     <el-button @click="socket.connectTrade()">连接交易服务</el-button>
     <el-button @click="socket.closeTradeServer()">断开交易服务</el-button>
+    <CatTable :columns="columns" :rows="tableList"></CatTable>
     <!-- 底部动态组件 -->
     <template #footer>
       <app-tab-component :options="{ selectedRow, code: 'quote' }" />
@@ -35,6 +36,7 @@ import AppContextmenu from '@pc/components/base/contextmenu/index.vue'
 import AppButtonGroup from '@pc/components/modules/button-group/index.vue'
 import AppFilter from './components/filter/index.vue'
 import AppTabComponent from '@/components/base/tab-component/index.vue'
+import CatTable from '@/components/base/table/index'
 
 const components = {
   trade: defineAsyncComponent(() => import('@pc/components/modules/trade/index.vue')),
@@ -57,6 +59,7 @@ columns.value = [
   {
     key: 'goodsName',
     label: '商品',
+    width: 1200
   },
   {
     key: 'lastPrice',

+ 6 - 11
src/utils/client/index.ts

@@ -1,11 +1,14 @@
-import { reactive, toRefs, readonly } from 'vue'
+import { reactive, readonly } from 'vue'
 import { timerInterceptor } from '@/utils/timer'
 
 export default new (class {
-    private state = reactive({
+    private _state = reactive({
         clientWidth: 0
     })
 
+    /** 只读状态 */
+    state = readonly(this._state);
+
     constructor() {
         // 等待 html 加载完成
         document.addEventListener('DOMContentLoaded', () => {
@@ -38,7 +41,7 @@ export default new (class {
             el.style.setProperty('font-size', fontSize);
         }
 
-        this.state.clientWidth = body.clientWidth;
+        this._state.clientWidth = body.clientWidth;
     }
 
     /**
@@ -64,12 +67,4 @@ export default new (class {
             isPc,
         }
     }
-
-    /**
-     * 获取属性状态(只读)
-     * @returns 
-     */
-    getState() {
-        return toRefs(readonly(this.state));
-    }
 })