Ver código fonte

交易记录、持仓查询

zhb 2 meses atrás
pai
commit
f56c221388

+ 240 - 0
components/cwg-detail-popup.vue

@@ -0,0 +1,240 @@
+<template>
+    <uni-popup ref="popupRef" type="center" @change="handlePopupChange">
+        <view class="cwg-dialog">
+            <!-- 弹窗头部 -->
+            <view class="dialog-header">
+                <text class="dialog-title">{{ title || t('Tips.DeleteAccount') }}</text>
+                <uni-icons type="closeempty" size="20" color="#999" @click="closeDialog" />
+            </view>
+
+            <!-- 弹窗内容 -->
+            <view class="dialog-content">
+                <view class="lines">
+                    <cwg-label-line-value v-for="(item, index) in items" :key="index" :label="item.label"
+                        :value="item.value" />
+                </view>
+
+            </view>
+            <view class="dialog-footer">
+                <button class="single-btn" :class="singleBtnType" @click="closeDialog" v-t="'Ib.Custom.Close'" />
+            </view>
+        </view>
+    </uni-popup>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
+const props = defineProps({
+    // 是否显示弹窗
+    visible: {
+        type: Boolean,
+        default: false
+    },
+    title: { type: String, default: '详情' },
+    items: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['update:visible', 'confirm', 'close', 'single-click'])
+
+// 弹窗引用
+const popupRef = ref(null)
+
+// 监听 visible 变化
+watch(() => props.visible, (val) => {
+    if (val) {
+        popupRef.value?.open()
+    } else {
+        popupRef.value?.close()
+    }
+}, { immediate: true })
+
+// 弹窗状态变化
+const handlePopupChange = (e) => {
+    if (!e.show) {
+        emit('update:visible', false)
+        emit('close')
+    }
+}
+
+// 关闭弹窗
+const closeDialog = () => {
+    popupRef.value?.close()
+}
+
+// 暴露方法给父组件
+defineExpose({
+    close: closeDialog,
+    open: () => popupRef.value?.open()
+})
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+:deep(.uni-popup) {
+    z-index: 101;
+}
+
+.cwg-dialog {
+    background-color: var(--color-white);
+    border-radius: px2rpx(8);
+    overflow: hidden;
+    width: px2rpx(600);
+    max-width: 90vw;
+}
+
+@media (min-width: 768px) {
+    :deep(.cwg-dialog) {
+        width: px2rpx(600) !important;
+    }
+}
+
+
+.dialog-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: px2rpx(30) px2rpx(30) px2rpx(20);
+    border-bottom: 1px solid #f0f0f0;
+
+    .dialog-title {
+        font-size: px2rpx(20);
+        font-weight: 600;
+        color: #333;
+    }
+}
+
+.dialog-content {
+    padding: px2rpx(20) px2rpx(30);
+    max-height: 60vh;
+    overflow-y: auto;
+
+    // 自定义滚动条样式
+    &::-webkit-scrollbar {
+        width: px2rpx(6);
+    }
+
+    &::-webkit-scrollbar-thumb {
+        background-color: #ddd;
+        border-radius: px2rpx(3);
+    }
+}
+
+.lines {
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(10);
+}
+
+.actions {
+    margin-top: px2rpx(16);
+    display: flex;
+    justify-content: center;
+}
+
+.actions button {
+    min-width: px2rpx(160);
+    height: px2rpx(40);
+    border-radius: px2rpx(8);
+    font-size: px2rpx(14);
+}
+
+.dialog-footer {
+    padding: px2rpx(20) px2rpx(30) px2rpx(30);
+    border-top: 1px solid #f0f0f0;
+
+    // 双按钮模式
+    &:not(:has(button:only-child)):has(button) {
+        display: flex;
+        justify-content: flex-end;
+        gap: px2rpx(20);
+    }
+
+    // 单按钮模式
+    &:has(button:only-child) {
+        display: flex;
+        justify-content: center;
+    }
+
+    .footer-line {
+        height: 1px;
+        background-color: #f0f0f0;
+        margin: px2rpx(-20) 0 0;
+    }
+
+    button {
+        min-width: px2rpx(120);
+        height: px2rpx(40);
+        border-radius: px2rpx(4);
+        font-size: px2rpx(16);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border: none;
+        cursor: pointer;
+        transition: all 0.3s ease;
+
+        &:active {
+            opacity: 0.8;
+            transform: scale(0.98);
+        }
+
+        &:disabled {
+            opacity: 0.5;
+            cursor: not-allowed;
+        }
+    }
+
+    .cancel-btn {
+        background-color: #f5f5f5;
+        color: #666;
+
+        &:active {
+            background-color: #e8e8e8;
+        }
+    }
+
+    .confirm-btn {
+        &.primary {
+            background-color: #007aff;
+            color: #fff;
+
+            &:active {
+                background-color: #0056b3;
+            }
+        }
+
+        &.danger {
+            background-color: #ff6b6b;
+            color: #fff;
+
+            &:active {
+                background-color: #ff5252;
+            }
+        }
+    }
+
+    .single-btn {
+        min-width: px2rpx(200);
+
+        &.primary {
+            background-color: #007aff;
+            color: #fff;
+        }
+
+        &.danger {
+            background-color: #ff6b6b;
+            color: #fff;
+        }
+
+        &.default {
+            background-color: #f5f5f5;
+            color: #666;
+        }
+    }
+}
+</style>

+ 325 - 434
components/cwg-tabel.vue

@@ -1,83 +1,64 @@
 <template>
-    <view class="table-container">
-        <!-- 表格 -->
-        <uni-table :type="selectionType" :border="false" @selection-change="handleSelectionChange">
-            <uni-tr class="tabel-header">
-                <uni-th v-for="column in columns" :key="column.prop" :align="column.align || 'center'"
-                    :width="column.width || '50'" :class="[
-                        headerClass
-                    ]" :style="getHeaderStyle(column)">
-                    <view class="header-content">
-                        {{ column.label }}
-                        <view v-if="column.sortable" class="sort-icon">
-                            <uni-icons :type="getSortIcon(column)" :size="14" color="#999" />
-                        </view>
-                    </view>
-                </uni-th>
-                <uni-th v-if="showOperation" align="center" :width="operationWidth" :style="getOperationHeaderStyle()">
-                    {{ t('common.operation') }}
-                </uni-th>
-            </uni-tr>
-            <template v-for="(row, rowIndex) in tableData" :key="rowIndex">
-
-                <uni-tr>
-                    <uni-td v-for="column in columns" :key="column.prop" :align="column.align || 'center'"
-                        :class="getCellClass(column, row)" :style="getCellStyle(column, row)">
-                        <!-- 自定义渲染插槽 -->
-                        <slot v-if="column.slot" :name="column.slot" :row="row" :column="column" :index="rowIndex">
-                            {{ row[column.prop] }}
-                        </slot>
-
-                        <!-- 标签类型渲染 -->
-                        <uni-tag v-else-if="column.type === 'tag'" :text="formatTagText(row[column.prop], column)"
-                            :type="formatTagType(row[column.prop], column)" size="small" />
-
-                        <!-- 文件类型渲染 -->
-                        <view v-else-if="column.type === 'file'">
-                            <cwg-file :path="row[column.prop]" />
+    <view>
+        <!-- 统一表格容器,根据屏幕宽度自适应 -->
+        <view class="table-container" :class="{ 'mobile-table': isMobile }">
+            <uni-table :type="selectionType" :border="false" @selection-change="handleSelectionChange" emptyText="">
+                <!-- 表头:根据设备类型显示不同的列 -->
+                <uni-tr class="table-header">
+                    <uni-th v-for="column in displayColumns" :key="column.prop" :align="column.align || 'center'"
+                        :width="column.width || '200'" :class="[headerClass, { sortable: column.sortable }]"
+                        :style="getHeaderStyle(column)" @click="column.sortable && handleSort(column)">
+                        <view class="header-content">
+                            {{ column.label }}
+                            <view v-if="column.sortable" class="sort-icon">
+                                <uni-icons :type="getSortIcon(column)" :size="14" color="#999" />
+                            </view>
                         </view>
+                    </uni-th>
+                </uni-tr>
 
-                        <!-- 默认文本渲染 -->
-                        <template v-else>{{ formatCellValue(row[column.prop], column, row) }}</template>
-                    </uni-td>
-
-                    <!-- 操作按钮 -->
-                    <uni-td v-if="showOperation" align="center">
-                        <view class="operation-btns">
-                            <template v-for="(action, index) in operationActions" :key="index">
-                                <button v-if="showAction(action, row)" class="btn" size="mini"
-                                    :type="action.type || 'default'" @click="handleAction(action, row)">
-                                    <cwg-icon :name="action.icon" :size="16" color="#fff" v-if="action.icon" />
-                                    {{ action.text }}
-                                </button>
+                <!-- 表格主体 -->
+                <template v-for="(row, rowIndex) in tableData" :key="rowIndex">
+                    <uni-tr>
+                        <!-- 数据列:根据设备类型动态渲染 -->
+                        <uni-td v-for="column in displayColumns" :key="column.prop" :align="column.align || 'center'"
+                            :class="getCellClass(column, row)" :style="getCellStyle(column, row)"
+                            @click="openRowDetail(row)">
+                            <template v-if="column.slot">
+                                <slot :name="column.slot" :row="row" :column="column" :index="rowIndex">
+                                    {{ row[column.prop] }}
+                                </slot>
                             </template>
-                        </view>
-                    </uni-td>
-                </uni-tr>
-                <!-- 展开行 - 这里是您要放置 expand 插槽的地方 -->
-                <uni-tr v-if="expandedRows[rowIndex]" class="expand-row">
-                    <uni-td :colspan="columnSpan" class="expand-cell">
-                        <slot name="expand" :row="row" :rowIndex="rowIndex">
-                            <view class="default-expand-content">
-                                <text class="no-content">暂无展开内容</text>
+                            <uni-tag v-else-if="column.type === 'tag'" :text="formatTagText(row[column.prop], column)"
+                                :type="formatTagType(row[column.prop], column)" size="small" />
+                            <view v-else-if="column.type === 'file'">
+                                <cwg-file :path="row[column.prop]" />
                             </view>
-                        </slot>
-                    </uni-td>
-                </uni-tr>
-            </template>
-
-            <!-- 空数据提示 -->
-            <uni-tr v-if="tableData.length === 0">
-                <uni-td :colspan="columnSpan" align="center">
-                    <view class="empty-data">
-                        <uni-icons type="info" size="30" color="#999"></uni-icons>
-                        <text class="empty-text">暂无数据</text>
-                    </view>
-                </uni-td>
-            </uni-tr>
-        </uni-table>
-
-        <!-- 分页组件 -->
+                            <cwg-icon v-else-if="column.type === 'more'" name="crm-chevron-down"
+                                class="crm-chevron-down" :size="16" color="#007" @click="toggleRowExpand(rowIndex)" />
+                            <template v-else>
+                                {{ formatCellValue(row[column.prop], column, row) }}
+                            </template>
+                        </uni-td>
+                    </uni-tr>
+                    <!-- 桌面端展开行(仅当非移动端且行展开时显示) -->
+                    <uni-tr v-if="!isMobile && expandedRows[rowIndex]" class="expand-row">
+                        <uni-td :colspan="columnSpan" class="expand-cell">
+                            <slot name="expand" :row="row" :rowIndex="rowIndex">
+                                <view class="default-expand-content">
+                                    <text class="no-content">暂无展开内容</text>
+                                </view>
+                            </slot>
+                        </uni-td>
+                    </uni-tr>
+                </template>
+            </uni-table>
+        </view>
+        <!-- 空状态 -->
+        <view v-if="tableData.length === 0">
+            <cwg-empty-state />
+        </view>
+        <!-- 分页 -->
         <view class="pagination-container" v-if="showPagination && tableData.length > 0">
             <view class="pagination-info">
                 <text>共 {{ pagination.total }} 条记录</text>
@@ -91,7 +72,7 @@
             <view class="pagination">
                 <view class="page-item prev" :class="{ disabled: pagination.current === 1 }"
                     @click="handlePageChange('prev')">
-                    <uni-icons type="arrowleft" size="16" color="#666"></uni-icons>
+                    <uni-icons type="arrowleft" size="16" color="#666" />
                     <text>上一页</text>
                 </view>
 
@@ -101,189 +82,145 @@
                         {{ page }}
                     </view>
                 </view>
+
                 <view class="page-item next" :class="{ disabled: pagination.current === pagination.pages }"
                     @click="handlePageChange('next')">
                     <text>下一页</text>
-                    <uni-icons type="arrowright" size="16" color="#666"></uni-icons>
+                    <uni-icons type="arrowright" size="16" color="#666" />
                 </view>
             </view>
         </view>
+        <!-- 移动端详情弹窗 -->
+        <cwg-detail-popup v-model:visible="detailVisible" title="详情" :items="detailItems" />
     </view>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted } from 'vue'
+import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
 
 const props = defineProps({
-    // 表格列配置
-    columns: {
-        type: Array,
-        required: true,
-        default: () => []
-    },
+    /**
+     * 表格列配置(columns 数组中每一项是一个列配置对象)
+     * - prop: 字段名,对应 row[prop],用于取值/排序/详情展示
+     * - label: 表头显示文本(也会作为详情弹窗的 label)
+     * - align: 对齐方式('left' | 'center' | 'right')
+     * - width: 列宽(设置后该列固定宽度;未设置则自适应
+     * - slot: 自定义渲染插槽名(<template #xxx="{ row, column, index }">),优先级最高
+     * - formatter: ({ value, row }) => string | number,自定义格式化显示值(无 slot 时生效)
+     * - sortable: 是否可排序(点击表头切换 asc/desc)
+     * - headerStyle: 表头样式(对象),会合并到表头 style
+     * - cellStyle: 单元格样式(对象或函数({ row, column }) => style)
+     * - cellClass: 单元格 class(string 或函数({ row, column }) => string | string[])
+     * - isTabel: false 时不在表格中显示该列,但仍会在“详情弹窗”中显示
+     * - type: 内置渲染类型
+     * - 'tag': 使用 uni-tag 渲染,搭配 tagMap/tagTypeMap
+     * - 'file': 使用 cwg-file 渲染
+     * - 'more': 展示“更多”图标(用于移动端点击查看详情等)
+     * - 'date': 日期格式化(搭配 dateFormat)
+     * - tagMap: 标签映射对象
+     * - tagTypeMap: 标签类型映射对象
+     * - dateFormat: 日期格式
+    *  */
+    columns: { type: Array, required: true, default: () => [] },
+    // 移动端配置
+    mobilePrimaryCount: { type: Number, default: 3 },
+    // 参数如columns
+    mobilePrimaryFields: { type: Array, default: () => [] },
     // API 请求函数
-    api: {
-        type: Function,
-        required: true
-    },
+    api: { type: Function, required: true },
     // 查询参数
-    queryParams: {
-        type: Object,
-        default: () => ({})
-    },
+    queryParams: { type: Object, default: () => ({}) },
     // 是否立即加载
-    immediate: {
-        type: Boolean,
-        default: true
-    },
+    immediate: { type: Boolean, default: true },
     // 选择类型:'selection' | null
-    selectionType: {
-        type: String,
-        default: null
-    },
-    // 是否显示操作列
-    showOperation: {
-        type: Boolean,
-        default: false
-    },
-    // 操作按钮配置
-    operationActions: {
-        type: Array,
-        default: () => [
-            { text: '编辑', type: 'primary', action: 'edit', icon: 'crm-puls' },
-            { text: '删除', type: 'warn', action: 'delete', icon: 'crm-delete' },
-            { text: '查看', type: 'default', action: 'view', icon: 'view' }
-        ]
-    },
-    // 是否显示底部操作栏
-    showBottomActions: {
-        type: Boolean,
-        default: false
-    },
-    // 底部操作按钮
-    bottomActions: {
-        type: Array,
-        default: () => []
-    },
+    selectionType: { type: String, default: null },
     // 是否显示每页条数选择
-    showPageSize: {
-        type: Boolean,
-        default: true
-    },
+    showPageSize: { type: Boolean, default: true },
     // 是否显示分页
-    showPagination: {
-        type: Boolean,
-        default: true
-    },
+    showPagination: { type: Boolean, default: true },
+    isViewDetail: { type: Boolean, default: true },
     // 每页条数选项
-    pageSizes: {
-        type: Array,
-        default: () => [10, 20, 30, 50, 100]
-    },
+    pageSizes: { type: Array, default: () => [10, 20, 30, 50, 100] },
     // 默认每页条数
-    defaultPageSize: {
-        type: Number,
-        default: 10
-    },
-    // ========== 表头样式自定义 ==========
-    // 表头背景色
-    headerBackground: {
-        type: String,
-        default: 'var(--color-slate-200)'
-    },
-    // 表头文字颜色
-    headerColor: {
-        type: String,
-        default: 'var(--color-slate-800)'
-    },
-    // 表头字体大小
-    headerFontSize: {
-        type: [String, Number],
-        default: '14rpx'
-    },
-    // 表头字体粗细
-    headerFontWeight: {
-        type: [String, Number],
-        default: 600
-    },
-    // 表头高度
-    headerHeight: {
-        type: [String, Number],
-        default: '40rpx'
-    },
-    // 表头自定义类名
-    headerClass: {
-        type: [String, Array],
-        default: ''
-    },
-    // 表头自定义样式对象
-    headerStyle: {
-        type: Object,
-        default: () => ({})
-    },
-    // 是否固定表头
-    stickyHeader: {
-        type: Boolean,
-        default: true
-    },
-    // 表头固定时的偏移量
-    stickyOffset: {
-        type: [String, Number],
-        default: '0'
-    }
+    defaultPageSize: { type: Number, default: 10 },
+    // 表头样式自定义
+    headerBackground: { type: String, default: '#fff' },
+    headerColor: { type: String, default: 'var(--color-slate-800)' },
+    headerFontSize: { type: [String, Number], default: '14rpx' },
+    headerFontWeight: { type: [String, Number], default: 600 },
+    headerHeight: { type: [String, Number], default: '40rpx' },
+    headerClass: { type: [String, Array], default: '' },
+    headerStyle: { type: Object, default: () => ({}) },
+    stickyHeader: { type: Boolean, default: true },
+    stickyOffset: { type: [String, Number], default: '0' },
 })
 
 const emit = defineEmits([
     'selection-change',
     'action-click',
-    'bottom-action-click',
     'page-change',
     'load-success',
-    'load-error'
+    'load-error',
+    'sort-change'
 ])
 
-// 展开行状态
-const expandedRows = ref({})
-// 切换行展开状态
-const toggleRowExpand = (rowIndex) => {
-    const key = rowIndex
-    if (expandedRows.value[key]) {
-        expandedRows.value[key] = false
-    } else {
-        expandedRows.value = {}
-        expandedRows.value[key] = true
-    }
-    emit('expand-change', { rowIndex, expanded: expandedRows.value[key] })
-}
-
-// 表格数据
+// ========== 响应式状态 ==========
 const tableData = ref([])
 const selectedItems = ref([])
-
-// 分页参数
+const detailVisible = ref(false)
+const detailItems = ref([])
 const pagination = ref({
     current: 1,
     pageSize: props.defaultPageSize,
     total: 0,
     pages: 0
 })
-
-// 加载状态
 const loading = ref(false)
+const expandedRows = ref({})
+
+// 移动端检测
+const isMobile = ref(false)
+// 排序状态
+const sortState = ref({ prop: '', order: '' }) // order: 'asc' | 'desc' | ''
+
+// ========== 计算属性 ==========
+// 显示的列(根据设备类型)
+const displayColumns = computed(() => {
+    const filterForTable = (cols) => (cols || []).filter((c) => c && c.isTabel !== false)
+
+    const normalizeColumns = (cols) => {
+        if (!Array.isArray(cols)) return []
+        if (cols.length === 0) return []
+        const first = cols[0]
+        if (typeof first === 'string') {
+            return cols
+                .map((prop) => props.columns.find((c) => c && c.prop === prop))
+                .filter(Boolean)
+        }
+        return cols
+    }
+
+    if (!isMobile.value) return filterForTable(normalizeColumns(props.columns))
+
+    if (props.mobilePrimaryFields && props.mobilePrimaryFields.length) {
+        return filterForTable(normalizeColumns(props.mobilePrimaryFields))
+    }
+    return filterForTable(normalizeColumns(props.columns.slice(0, props.mobilePrimaryCount)))
+})
 
-// 计算列跨度
+// 列跨度(用于展开行)
 const columnSpan = computed(() => {
-    let span = props.columns.length
+    let span = displayColumns.value.length
     if (props.showOperation) span += 1
     return span
 })
 
-// 计算每页条数索引
+// 每页条数索引
 const pageSizeIndex = computed(() => {
     return props.pageSizes.indexOf(pagination.value.pageSize)
 })
 
-// 计算显示的页码
+// 可见页码
 const visiblePages = computed(() => {
     const maxVisible = 5
     const half = Math.floor(maxVisible / 2)
@@ -295,13 +232,7 @@ const visiblePages = computed(() => {
     }
     return Array.from({ length: end - start + 1 }, (_, i) => start + i)
 })
-
-// 监听查询参数变化
-watch(() => props.queryParams, () => {
-    refreshTable()
-}, { deep: true })
-
-// ========== 表头样式计算 ==========
+// ========== 工具函数 ==========
 // 获取表头样式
 const getHeaderStyle = (column) => {
     const baseStyle = {
@@ -313,40 +244,35 @@ const getHeaderStyle = (column) => {
         lineHeight: typeof props.headerHeight === 'number' ? props.headerHeight + 'rpx' : props.headerHeight,
         ...props.headerStyle
     }
-
-    // 固定表头
     if (props.stickyHeader) {
         baseStyle.position = 'sticky'
         baseStyle.top = props.stickyOffset
         baseStyle.zIndex = 100
     }
-    // 列自定义样式
+    if (column.width) {
+        const w = typeof column.width === 'number' ? column.width + 'px' : column.width
+        baseStyle.width = w
+        baseStyle.minWidth = w
+        baseStyle.maxWidth = w
+    }
     if (column.headerStyle) {
         Object.assign(baseStyle, column.headerStyle)
     }
-
     return baseStyle
 }
 
-// 获取操作列表头样式
-const getOperationHeaderStyle = () => {
-    return {
-        backgroundColor: props.headerBackground,
-        color: props.headerColor,
-        fontSize: typeof props.headerFontSize === 'number' ? props.headerFontSize + 'px' : props.headerFontSize,
-        fontWeight: props.headerFontWeight,
-        height: typeof props.headerHeight === 'number' ? props.headerHeight + 'px' : props.headerHeight,
-        lineHeight: typeof props.headerHeight === 'number' ? props.headerHeight + 'px' : props.headerHeight,
-        position: props.stickyHeader ? 'sticky' : 'static',
-        top: props.stickyHeader ? props.stickyOffset : 'auto',
-        zIndex: props.stickyHeader ? 100 : 'auto'
-    }
-}
-
 // 获取单元格样式
 const getCellStyle = (column, row) => {
     const style = {}
-
+    if (column.width) {
+        const w = typeof column.width === 'number' ? column.width + 'px' : column.width
+        style.width = w
+        style.minWidth = w
+        style.maxWidth = w
+        style.whiteSpace = 'nowrap'
+        style.overflow = 'hidden'
+        style.textOverflow = 'ellipsis'
+    }
     if (column.cellStyle) {
         if (typeof column.cellStyle === 'function') {
             Object.assign(style, column.cellStyle({ row, column }))
@@ -354,7 +280,6 @@ const getCellStyle = (column, row) => {
             Object.assign(style, column.cellStyle)
         }
     }
-
     return style
 }
 
@@ -377,10 +302,103 @@ const getCellClass = (column, row) => {
     }
     return classes.join(' ')
 }
-// 加载数据
+
+// 格式化单元格值
+const formatCellValue = (value, column, row) => {
+    if (column.formatter) {
+        return column.formatter({ value, row })
+    }
+    if (column.type === 'date' && value) {
+        return formatDate(value, column.dateFormat || 'YYYY-MM-DD HH:mm:ss')
+    }
+    if (value === null || value === undefined) {
+        return '-'
+    }
+    return value
+}
+
+// 格式化标签文本
+const formatTagText = (value, column) => {
+    if (column.tagMap && column.tagMap[value]) {
+        return column.tagMap[value]
+    }
+    return value || '-'
+}
+
+// 格式化标签类型
+const formatTagType = (value, column) => {
+    if (column.tagTypeMap && column.tagTypeMap[value]) {
+        return column.tagTypeMap[value]
+    }
+    return 'default'
+}
+
+// 格式化日期
+const formatDate = (date, format) => {
+    if (!date) return '-'
+    const d = new Date(date)
+    const year = d.getFullYear()
+    const month = String(d.getMonth() + 1).padStart(2, '0')
+    const day = String(d.getDate()).padStart(2, '0')
+    const hour = String(d.getHours()).padStart(2, '0')
+    const minute = String(d.getMinutes()).padStart(2, '0')
+    const second = String(d.getSeconds()).padStart(2, '0')
+
+    return format
+        .replace('YYYY', year)
+        .replace('MM', month)
+        .replace('DD', day)
+        .replace('HH', hour)
+        .replace('mm', minute)
+        .replace('ss', second)
+}
+
+// 排序图标
+const getSortIcon = (column) => {
+    if (sortState.value.prop !== column.prop) return 'arrow-up'
+    return sortState.value.order === 'asc' ? 'arrow-up' : 'arrow-down'
+}
+
+// 处理排序
+const handleSort = (column) => {
+    if (!column.sortable) return
+    let newOrder = ''
+    if (sortState.value.prop === column.prop) {
+        if (sortState.value.order === '') newOrder = 'asc'
+        else if (sortState.value.order === 'asc') newOrder = 'desc'
+        else newOrder = ''
+    } else {
+        newOrder = 'asc'
+    }
+    sortState.value = { prop: newOrder ? column.prop : '', order: newOrder }
+    emit('sort-change', { prop: sortState.value.prop, order: sortState.value.order })
+    refreshTable()
+}
+// 展开行切换
+const toggleRowExpand = (rowIndex) => {
+    const key = rowIndex
+    if (expandedRows.value[key]) {
+        expandedRows.value[key] = false
+    } else {
+        expandedRows.value = {}
+        expandedRows.value[key] = true
+    }
+    emit('expand-change', { rowIndex, expanded: expandedRows.value[key] })
+}
+// 打开详情弹窗(移动端使用)
+const openRowDetail = (row) => {
+    detailItems.value = props.columns
+        .filter((column) => column && column.prop && column.label)
+        .map((column) => ({
+            label: column.label,
+            value: String(formatCellValue(row[column.prop], column, row))
+        }))
+    detailVisible.value = true
+}
+
+// ========== 数据加载 ==========
 const loadData = async () => {
     if (loading.value) return
-
     loading.value = true
     try {
         const params = {
@@ -390,14 +408,13 @@ const loadData = async () => {
             },
             ...props.queryParams
         }
-
+        // 添加排序参数
+        if (sortState.value.prop && sortState.value.order) {
+            params.sort = { field: sortState.value.prop, order: sortState.value.order }
+        }
         const res = await props.api(params)
-
-        // 处理不同的API返回格式
         if (res.code === 200 || res.code === 0) {
             const data = res.data || res
-
-            // 支持不同的分页数据结构
             if (Array.isArray(data)) {
                 tableData.value = data
                 pagination.value.total = data.length
@@ -409,7 +426,6 @@ const loadData = async () => {
             } else {
                 tableData.value = []
             }
-
             emit('load-success', res)
         } else {
             throw new Error(res.message || '加载失败')
@@ -425,40 +441,28 @@ const loadData = async () => {
         loading.value = false
     }
 }
-
 // 刷新表格
 const refreshTable = () => {
     pagination.value.current = 1
     loadData()
 }
-
-// 重新加载
 const reload = () => {
     loadData()
 }
-
 // 分页变化
 const handlePageChange = (page) => {
     if (page === 'prev') {
-        if (pagination.value.current > 1) {
-            pagination.value.current--
-        } else {
-            return
-        }
+        if (pagination.value.current > 1) pagination.value.current--
+        else return
     } else if (page === 'next') {
-        if (pagination.value.current < pagination.value.pages) {
-            pagination.value.current++
-        } else {
-            return
-        }
+        if (pagination.value.current < pagination.value.pages) pagination.value.current++
+        else return
     } else {
         pagination.value.current = page
     }
-
     loadData()
     emit('page-change', pagination.value)
 }
-
 // 每页条数变化
 const handlePageSizeChange = (e) => {
     const size = props.pageSizes[e.detail.value]
@@ -466,102 +470,52 @@ const handlePageSizeChange = (e) => {
     pagination.value.current = 1
     loadData()
 }
-
 // 选择变化
 const handleSelectionChange = (e) => {
     selectedItems.value = e.detail.value
     emit('selection-change', selectedItems.value)
 }
-
-// 操作按钮点击
-const handleAction = (action, row) => {
-    emit('action-click', { action: action.action, row, original: action })
-}
-
-// 底部操作点击
-const handleBottomAction = (action) => {
-    emit('bottom-action-click', action)
-}
-
-// 判断是否显示操作按钮
-const showAction = (action, row) => {
-    if (action.show) {
-        return action.show(row)
-    }
-    return true
-}
-
-// 格式化单元格值
-const formatCellValue = (value, column, row) => {
-    if (column.formatter) {
-        return column.formatter({ value, row })
-    }
-
-    if (column.type === 'date' && value) {
-        return formatDate(value, column.dateFormat || 'YYYY-MM-DD HH:mm:ss')
-    }
-
-    if (value === null || value === undefined) {
-        return '-'
-    }
-
-    return value
-}
-
-// 格式化标签文本
-const formatTagText = (value, column) => {
-    if (column.tagMap && column.tagMap[value]) {
-        return column.tagMap[value]
-    }
-    return value || '-'
-}
-
-// 格式化标签类型
-const formatTagType = (value, column) => {
-    if (column.tagTypeMap && column.tagTypeMap[value]) {
-        return column.tagTypeMap[value]
-    }
-    return 'default'
-}
-
-// 格式化日期
-const formatDate = (date, format) => {
-    if (!date) return '-'
-    const d = new Date(date)
-    const year = d.getFullYear()
-    const month = String(d.getMonth() + 1).padStart(2, '0')
-    const day = String(d.getDate()).padStart(2, '0')
-    const hour = String(d.getHours()).padStart(2, '0')
-    const minute = String(d.getMinutes()).padStart(2, '0')
-    const second = String(d.getSeconds()).padStart(2, '0')
-
-    return format
-        .replace('YYYY', year)
-        .replace('MM', month)
-        .replace('DD', day)
-        .replace('HH', hour)
-        .replace('mm', minute)
-        .replace('ss', second)
-}
-
-// 预览图片
-const handlePreviewImage = (url) => {
-    uni.previewImage({
-        urls: [url]
-    })
-}
-
 // 获取选中项
-const getSelectedItems = () => {
-    return selectedItems.value
+const getSelectedItems = () => selectedItems.value
+const clearSelection = () => { selectedItems.value = [] }
+// ========== 移动端检测 ==========
+const checkIsMobile = () => {
+    // 适配 uni-app 环境
+    // #ifdef H5
+    const width = window.innerWidth
+    isMobile.value = width < 991
+    // #endif
+    // #ifndef H5
+    const systemInfo = uni.getSystemInfoSync()
+    isMobile.value = systemInfo.windowWidth < 991
+    // #endif
 }
-
-// 清空选中
-const clearSelection = () => {
-    selectedItems.value = []
+// 监听窗口大小变化(仅 H5)
+// #ifdef H5
+const handleResize = () => {
+    checkIsMobile()
 }
-
-// 暴露方法给父组件
+// #endif
+// ========== 监听参数变化 ==========
+watch(() => props.queryParams, () => {
+    refreshTable()
+}, { deep: true })
+// ========== 生命周期 ==========
+onMounted(() => {
+    checkIsMobile()
+    // #ifdef H5
+    window.addEventListener('resize', handleResize)
+    // #endif
+    if (props.immediate) {
+        loadData()
+    }
+})
+onUnmounted(() => {
+    // #ifdef H5
+    window.removeEventListener('resize', handleResize)
+    // #endif
+})
+// 暴露方法
 defineExpose({
     refreshTable,
     reload,
@@ -572,13 +526,6 @@ defineExpose({
     toggleRowExpand,
     pagination
 })
-
-// 初始化加载
-onMounted(() => {
-    if (props.immediate) {
-        loadData()
-    }
-})
 </script>
 
 <style scoped lang="scss">
@@ -586,18 +533,15 @@ onMounted(() => {
 
 .table-container {
     width: 100%;
-    overflow-x: auto;
-    /* 关键:横向滚动 */
-    -webkit-overflow-scrolling: touch;
-    /* 流畅滚动 */
+    // overflow-x: auto;
+    // -webkit-overflow-scrolling: touch;
     max-height: calc(100vh - 209px);
-    overflow: hidden;
     color: var(--color-slate-800);
 
     :deep(.uni-table-scroll) {
         width: 100%;
         max-height: calc(100vh - 375px);
-        overflow: auto;
+        // overflow: auto;
     }
 
     :deep(.uni-table) {
@@ -618,20 +562,14 @@ onMounted(() => {
                 .sort-icon {
                     display: inline-flex;
                     cursor: pointer;
-
-                    .uni-icons {
-                        transition: all 0.3s;
-                    }
                 }
             }
 
             &.sortable {
                 cursor: pointer;
 
-                &:hover {
-                    .sort-icon .uni-icons {
-                        color: var(--color-primary) !important;
-                    }
+                &:hover .sort-icon .uni-icons {
+                    color: var(--color-primary) !important;
                 }
             }
 
@@ -641,89 +579,42 @@ onMounted(() => {
         }
 
         .uni-table-tr {
-            border-bottom: 5px solid var(--color-slate-200) !important;
-
             &:last-child {
                 border-bottom: none !important;
             }
+
+            .uni-table-th {
+                border-bottom: 1px solid #141d22 !important;
+            }
         }
 
         .uni-table-th,
         .uni-table-td {
-            padding: px2rpx(12) px2rpx(20);
+            padding: px2rpx(16) px2rpx(5);
             color: var(--color-slate-800);
             box-sizing: border-box;
         }
 
-        .uni-table-td {
-            word-break: break-word;
-        }
-
         .expand-cell {
             padding: 0 !important;
         }
     }
 }
 
-.table-image {
-    width: px2rpx(30);
-    height: px2rpx(30);
-    border-radius: px2rpx(8);
-}
-
-.operation-btns {
-    display: flex;
-    gap: px2rpx(10);
-    justify-content: center;
-    flex-wrap: wrap;
-}
-
-.btn {
-    margin: 0 px2rpx(4);
-    font-size: px2rpx(16);
-    padding: 0 px2rpx(20);
-    height: px2rpx(36);
-    border-radius: none;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+.crm-chevron-down {
+    transform: rotate(-90deg);
     cursor: pointer;
-    transition: all 0.3s;
-
-    &:hover {
-        opacity: 0.8;
-    }
-
-    &:active {
-        transform: scale(0.95);
-    }
-}
-
-.empty-data {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    padding: px2rpx(30) 0;
 }
 
-.empty-text {
-    margin-top: px2rpx(20);
-    font-size: px2rpx(16);
-    color: #999;
-}
-
-.bottom-actions {
-    margin: px2rpx(30) px2rpx(20);
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
+.mobile-table {
+    :deep(.uni-table) {
+        min-width: 0 !important;
+    }
 }
 
-.left-actions,
-.right-actions {
-    display: flex;
-    gap: px2rpx(20);
+.detail-btn-mobile {
+    background-color: #f0f0f0;
+    color: #333;
 }
 
 /* 分页样式 */
@@ -846,4 +737,4 @@ onMounted(() => {
         gap: px2rpx(10);
     }
 }
-</style>
+</style>

+ 14 - 0
pages.json

@@ -197,6 +197,20 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/customer/trade-history",
+      "style": {
+        "navigationBarTitleText": "",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/customer/trade-position",
+      "style": {
+        "navigationBarTitleText": "",
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/ib/index",
       "style": {

+ 37 - 9
pages/customer/payment-history.vue

@@ -10,15 +10,14 @@
                 <uni-datetime-picker type="daterange" v-model="search.date"
                     :placeholder="t('placeholder.Start') + ' - ' + t('placeholder.End')" />
             </view>
-            <cwg-tabel ref="tableRef" :columns="columns" :queryParams="search" :api="listApi" :show-operation="false"
-                :showPagination="false">
+            <cwg-tabel ref="tableRef" :columns="columns" :mobilePrimaryFields="mobilePrimaryFields"
+                :queryParams="search" :api="listApi" :show-operation="false" :showPagination="false">
                 <template #avatar="{ row }">
                     <image :src="row.avatar" class="avatar" mode="widthFix" />
                     <cwg-file :path="row.path" />
                 </template>
                 <template #type="{ row }">
-                    <view class="status-badge">{{typeMap.find(item => item.value === row.type)?.text}}
-                    </view>
+                    {{typeMap.find(item => item.value === row.type)?.text}}
                 </template>
                 <template #status="{ row }">
                     <OrderStatusMachineCell :row="row" @cancel="handleOrderCancel" @action="handleOrderAction" />
@@ -35,7 +34,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, onMounted } from 'vue';
+import { computed, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 const { t, locale } = useI18n();
 import { financialApi } from '@/service/financial';
@@ -58,7 +57,6 @@ const orderStatusMap = computed(() => ([
 
 const handleOrderCancel = (row) => {
     console.log('取消订单:', row)
-    // 处理取消逻辑
 }
 
 const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
@@ -109,11 +107,41 @@ const columns = ref([
         prop: 'note',
         label: t('Custom.Recording.Note'),
         formatter: ({ row }) => row.note || '--',
-        align: 'left'
-    }
+        align: 'right'
+    },
+    {
+        prop: 'more',
+        type: 'more',
+        width: 20,
+        align: 'right'
+    },
 ])
 
-const addFileDialog = ref(null);
+const mobilePrimaryFields = ref([
+    {
+        prop: 'serial',
+        label: t('Custom.PaymentHistory.Serial'),
+        align: 'left'
+    },
+    {
+        prop: 'status',
+        label: t('Custom.PaymentHistory.Status'),
+        slot: 'status',
+        align: 'left'
+    },
+    {
+        prop: 'amount',
+        label: t('Custom.PaymentHistory.Amount'),
+        formatter: ({ row }) => row.amount + ' ' + row.currency,
+        align: 'right'
+    },
+    {
+        prop: 'more',
+        type: 'more',
+        width: 20,
+        align: 'right'
+    },
+])
 const listApi = ref(null)
 listApi.value = financialApi.BalanceList
 </script>

+ 306 - 0
pages/customer/trade-history.vue

@@ -0,0 +1,306 @@
+<template>
+    <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+        <cwg-header :title="t('Ib.Report.Tit1')" />
+        <view class="info-card">
+            <view class="search-bar">
+                <cwg-combox v-model:value="search.type" :options="typeMap" :placeholder="t('placeholder.choose')" />
+                <cwg-combox v-model:value="search.orderStatus" :options="orderStatusMap"
+                    :placeholder="t('Custom.PaymentHistory.StatusPlaceholder')" />
+                <uni-easyinput v-model="search.login" :placeholder="t('placeholder.login')" />
+                <uni-datetime-picker type="daterange" v-model="search.date"
+                    :placeholder="t('placeholder.Start') + ' - ' + t('placeholder.End')" />
+            </view>
+            <cwg-tabel ref="tableRef" :columns="columns" :mobilePrimaryFields="mobilePrimaryFields"
+                :queryParams="search" :api="listApi" :show-operation="false" :showPagination="false">
+                <template #symbol="{ row }">
+                    <view class="symbol-cell">
+                        <view class="pair">{{ getSymbolParts(row.symbol)[0] }}/{{ getSymbolParts(row.symbol)[1]
+                            }}</view>
+                        <view class="desc">{{ row.openPrice }}
+                            <text :class="getCmdColorClass(row.cmdName)">{{ formatCmdName(row.cmdName) }}{{ row.volume
+                                }}{{ t('Label.Lot') }}</text>
+                        </view>
+                    </view>
+                </template>
+                <template #profit="{ row }">
+                    <view class="symbol-cell">
+                        <text :class="getProfitColorClass(row.profit)">{{ row.profit || 0 }}</text>
+                    </view>
+                </template>
+            </cwg-tabel>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+const { t, locale } = useI18n();
+import { customApi } from '@/service/custom';
+const search = ref({
+    "reportType": 3,
+    "agentId": "",
+    "groupType": null,
+    "login": "",
+    "cId": "700101",
+    "customName": "",
+    "ibNo": "",
+    "symbol": "",
+    "cmdName": null,
+    "platform": "MT4",
+    "salesNo": "",
+    "salesName": "",
+    "dwType": null,
+    "customType": 0,
+    "rankingType": 1,
+    "loginTypes": [],
+    "isShort": 0,
+    "detail_type": 1,
+    "date": [
+        "1970-01-01",
+        "2026-03-27"
+    ],
+    "startDate": "1970-01-01",
+    "endDate": "2026-03-27",
+    "orderColumn": null,
+    "orderType": null,
+})
+const typeMap = computed(() => ([
+    { value: null, text: t('Custom.PaymentHistory.All') },
+    { value: 1, text: t('Custom.PaymentHistory.Deposit') },
+    { value: 2, text: t('Custom.PaymentHistory.Withdrawals') }
+]));
+const orderStatusMap = computed(() => ([
+    { value: null, text: t('Custom.PaymentHistory.All') },
+    { value: 1, text: t('State.ToBeProcessed') },
+    { value: 2, text: t('State.Completed') },
+    { value: 3, text: t('State.InTheProcessing') },
+    { value: 4, text: t('State.Refused') },
+    { value: 5, text: t('State.expireTime') },
+    { value: 6, text: t('State.Cancelled') },
+]));
+
+// 表格列配置
+const columns = ref([
+    {
+        prop: 'symbol',
+        label: t('Label.Varieties'),      // 交易品种
+        align: 'left'
+    },
+    {
+        prop: 'cmdName',
+        label: t('Label.Type'),           // 类型
+        align: 'left'
+    },
+    {
+        prop: 'openTime',
+        label: t('Label.OpenTime'),       // 开仓时间(协调世界时)
+        align: 'left'
+    },
+    {
+        prop: 'volume',
+        label: t('Label.Volume'),         // 手
+        formatter: ({ row }) => `${row.volume || 0} ${t('Label.Lot')}`,
+        align: 'right'
+    },
+    {
+        prop: 'openPrice',
+        label: t('Label.OpenPrice'),      // 开仓价
+        align: 'right'
+    },
+    {
+        prop: 'closePrice',
+        label: t('Label.ClosePrice'),     // 平仓价
+        align: 'right'
+    },
+    {
+        prop: 'tp',
+        label: t('Label.EP'), // 止盈
+        align: 'right'
+    },
+    {
+        prop: 'sl',
+        label: t('Label.EL'), // 止损
+        align: 'right'
+    },
+    {
+        prop: 'profit',
+        label: t('Label.ProfitLoss') + '(USD)',     // 利润, USD
+        slot: 'profit',
+        align: 'right'
+    },
+    {
+        prop: 'comment',
+        label: t('Label.Note'),
+        isTabel: false,
+        align: 'right'
+    },
+    {
+        prop: 'more',
+        type: 'more',
+        width: 20,
+        align: 'right'
+    },
+])
+
+const mobilePrimaryFields = ref([
+    {
+        prop: 'symbol',
+        label: t('Label.Varieties'),      // 交易品种
+        align: 'left',
+        slot: 'symbol'
+    },
+    {
+        prop: 'profit',
+        label: t('Label.ProfitLoss') + '(USD)',     // 利润, USD
+        slot: 'profit',
+        align: 'right'
+    },
+    {
+        prop: 'more',
+        type: 'more',
+        width: 20,
+        align: 'right'
+    },
+])
+
+const listApi = ref(null)
+listApi.value = customApi.tradeShardingHistory
+
+const getSymbolParts = (sym: string) => {
+    if (!sym) return ['', '']
+    const s = String(sym).toUpperCase()
+    if (s.includes('/')) {
+        const [base, quote] = s.split('/')
+        return [base, quote]
+    }
+    const base = s.slice(0, 3)
+    const quote = s.slice(3)
+    return [base, quote]
+}
+const formatCmdName = (cmd: string) => {
+    const v = String(cmd || '').toLowerCase()
+    if (v.includes('sell')) return '卖出'
+    if (v.includes('buy')) return '买入'
+    return cmd || ''
+}
+const getCmdColorClass = (cmd: string) => {
+    const v = String(cmd || '').toLowerCase()
+    if (v.includes('sell')) return 'is-sell'
+    if (v.includes('buy')) return 'is-buy'
+    return ''
+}
+const getProfitColorClass = (profit: any) => {
+    const n = Number(profit)
+    if (!Number.isFinite(n) || n === 0) return ''
+    return n > 0 ? 'is-profit' : 'is-loss'
+}
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.avatar {
+    width: px2rpx(60);
+    height: px2rpx(60);
+    border-radius: 4px;
+}
+
+.content-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: px2rpx(20);
+    font-weight: 500;
+
+    .content-title-btns {
+        margin: px2rpx(8) 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-error);
+            color: white;
+            padding: 0 px2rpx(12);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
+}
+
+.operation-btn {
+    :deep(span) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(4);
+        cursor: pointer;
+        background-color: var(--color-slate-150);
+        padding: px2rpx(8) 0;
+    }
+}
+
+.operation-btn.disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+}
+
+.symbol-cell {
+    display: inline-flex;
+    align-items: flex-start;
+    gap: 0.25rem;
+    flex-direction: column;
+
+    .pair {
+        font-weight: 600;
+        color: var(--color-slate-900);
+    }
+
+    .desc {
+        color: var(--color-slate-600);
+    }
+}
+
+.is-sell,
+.is-loss {
+    color: #eb483f;
+}
+
+.is-buy,
+.is-profit {
+    color: #46cd7c;
+}
+
+
+
+
+.search-bar {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    gap: px2rpx(16);
+    margin: px2rpx(16) 0;
+
+    .cwg-combox,
+    .uni-easyinput,
+    .uni-date {
+        width: px2rpx(240) !important;
+        flex: none;
+    }
+}
+</style>

+ 307 - 0
pages/customer/trade-position.vue

@@ -0,0 +1,307 @@
+<template>
+    <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+        <cwg-header :title="t('Ib.Report.Tit4')" />
+        <view class="info-card">
+            <view class="search-bar">
+                <cwg-combox v-model:value="search.type" :options="typeMap" :placeholder="t('placeholder.choose')" />
+                <cwg-combox v-model:value="search.orderStatus" :options="orderStatusMap"
+                    :placeholder="t('Custom.PaymentHistory.StatusPlaceholder')" />
+                <uni-easyinput v-model="search.login" :placeholder="t('placeholder.login')" />
+                <uni-datetime-picker type="daterange" v-model="search.date"
+                    :placeholder="t('placeholder.Start') + ' - ' + t('placeholder.End')" />
+            </view>
+            <cwg-tabel ref="tableRef" :columns="columns" :mobilePrimaryFields="mobilePrimaryFields"
+                :queryParams="search" :api="listApi" :show-operation="false" :showPagination="false">
+                <template #symbol="{ row }">
+                    <view class="symbol-cell">
+                        <view class="pair">{{ getSymbolParts(row.symbol)[0] }}/{{ getSymbolParts(row.symbol)[1]
+                            }}</view>
+                        <view class="desc">{{ row.openPrice }}
+                            <text :class="getCmdColorClass(row.cmdName)">{{ formatCmdName(row.cmdName) }}{{ row.volume
+                                }}{{ t('Label.Lot') }}</text>
+                        </view>
+                    </view>
+                </template>
+                <template #profit="{ row }">
+                    <view class="symbol-cell">
+                        <text :class="getProfitColorClass(row.profit)">{{ row.profit || 0 }}</text>
+                    </view>
+                </template>
+            </cwg-tabel>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+const { t, locale } = useI18n();
+import { customApi } from '@/service/custom';
+const search = ref(
+    {
+        "reportType": 3,
+        "agentId": null,
+        "groupType": null,
+        "login": "",
+        "cId": "",
+        "customName": "",
+        "ibNo": "",
+        "symbol": "",
+        "cmdName": "",
+        "platform": "MT4",
+        "salesNo": "",
+        "salesName": "",
+        "dwType": null,
+        "customType": 0,
+        "rankingType": 1,
+        "loginTypes": [],
+        "isShort": 0,
+        "detail_type": 4,
+        "date": [
+            "1970-01-01",
+            "2026-03-27"
+        ],
+        "startDate": "1970-01-01",
+        "endDate": "2026-03-27",
+        "orderColumn": null,
+        "orderType": null,
+        "page": {
+            "current": 1,
+            "row": 10
+        }
+    }
+)
+const typeMap = computed(() => ([
+    { value: null, text: t('Custom.PaymentHistory.All') },
+    { value: 1, text: t('Custom.PaymentHistory.Deposit') },
+    { value: 2, text: t('Custom.PaymentHistory.Withdrawals') }
+]));
+const orderStatusMap = computed(() => ([
+    { value: null, text: t('Custom.PaymentHistory.All') },
+    { value: 1, text: t('State.ToBeProcessed') },
+    { value: 2, text: t('State.Completed') },
+    { value: 3, text: t('State.InTheProcessing') },
+    { value: 4, text: t('State.Refused') },
+    { value: 5, text: t('State.expireTime') },
+    { value: 6, text: t('State.Cancelled') },
+]));
+
+// 表格列配置
+const columns = ref([
+    {
+        prop: 'symbol',
+        label: t('Label.Varieties'),      // 交易品种
+        align: 'left'
+    },
+    {
+        prop: 'cmdName',
+        label: t('Label.Type'),           // 类型
+        align: 'left'
+    },
+    {
+        prop: 'openTime',
+        label: t('Label.OpenTime'),       // 开仓时间(协调世界时)
+        align: 'left'
+    },
+    {
+        prop: 'volume',
+        label: t('Label.Volume'),         // 手
+        formatter: ({ row }) => `${row.volume || 0} ${t('Label.Lot')}`,
+        align: 'right'
+    },
+    {
+        prop: 'openPrice',
+        label: t('Label.OpenPrice'),      // 开仓价
+        align: 'right'
+    },
+    {
+        prop: 'tp',
+        label: t('Label.EP'), // 止盈
+        align: 'right'
+    },
+    {
+        prop: 'sl',
+        label: t('Label.EL'), // 止损
+        align: 'right'
+    },
+    {
+        prop: 'profit',
+        label: t('Label.ProfitLoss') + '(USD)',     // 利润, USD
+        slot: 'profit',
+        align: 'right'
+    },
+    {
+        prop: 'comment',
+        label: t('Label.Note'),
+        align: 'right',
+        isTabel: false
+    },
+    {
+        prop: 'more',
+        type: 'more',
+        width: 20,
+        align: 'right'
+    },
+])
+
+const mobilePrimaryFields = ref([
+    {
+        prop: 'symbol',
+        label: t('Label.Varieties'),      // 交易品种
+        align: 'left',
+        slot: 'symbol'
+    },
+    {
+        prop: 'profit',
+        label: t('Label.ProfitLoss') + '(USD)',     // 利润, USD
+        slot: 'profit',
+        align: 'right'
+    },
+    {
+        prop: 'more',
+        type: 'more',
+        width: 20,
+        align: 'right'
+    },
+])
+
+const listApi = ref(null)
+listApi.value = customApi.tradePosition
+
+const getSymbolParts = (sym: string) => {
+    if (!sym) return ['', '']
+    const s = String(sym).toUpperCase()
+    if (s.includes('/')) {
+        const [base, quote] = s.split('/')
+        return [base, quote]
+    }
+    const base = s.slice(0, 3)
+    const quote = s.slice(3)
+    return [base, quote]
+}
+const formatCmdName = (cmd: string) => {
+    const v = String(cmd || '').toLowerCase()
+    if (v.includes('sell')) return '卖出'
+    if (v.includes('buy')) return '买入'
+    return cmd || ''
+}
+const getCmdColorClass = (cmd: string) => {
+    const v = String(cmd || '').toLowerCase()
+    if (v.includes('sell')) return 'is-sell'
+    if (v.includes('buy')) return 'is-buy'
+    return ''
+}
+const getProfitColorClass = (profit: any) => {
+    const n = Number(profit)
+    if (!Number.isFinite(n) || n === 0) return ''
+    return n > 0 ? 'is-profit' : 'is-loss'
+}
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.avatar {
+    width: px2rpx(60);
+    height: px2rpx(60);
+    border-radius: 4px;
+}
+
+.content-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: px2rpx(20);
+    font-weight: 500;
+
+    .content-title-btns {
+        margin: px2rpx(8) 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-error);
+            color: white;
+            padding: 0 px2rpx(12);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
+}
+
+.operation-btn {
+    :deep(span) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(4);
+        cursor: pointer;
+        background-color: var(--color-slate-150);
+        padding: px2rpx(8) 0;
+    }
+}
+
+.operation-btn.disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+}
+
+.symbol-cell {
+    display: inline-flex;
+    align-items: flex-start;
+    gap: 0.25rem;
+    flex-direction: column;
+
+    .pair {
+        font-weight: 600;
+        color: var(--color-slate-900);
+    }
+
+    .desc {
+        color: var(--color-slate-600);
+    }
+}
+
+.is-sell,
+.is-loss {
+    color: #eb483f;
+}
+
+.is-buy,
+.is-profit {
+    color: #46cd7c;
+}
+
+
+
+
+.search-bar {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    gap: px2rpx(16);
+    margin: px2rpx(16) 0;
+
+    .cwg-combox,
+    .uni-easyinput,
+    .uni-date {
+        width: px2rpx(240) !important;
+        flex: none;
+    }
+}
+</style>

+ 8 - 0
service/custom.ts

@@ -102,4 +102,12 @@ export const customApi = {
   demoList: (params = {}) => post('/account/demo/all/list', params, 'Host80'),
   // 修改昵称
   updateNick: (params = {}) => post('/custom/login/update/nick/name', params, 'Host80'),
+  // 持仓查询
+  tradePosition: (params = {}) => post('/trade/position', params, 'Host80'),
+  // 持仓查询导出
+  tradePositionExport: (params = {}) => post('/trade/position/export', params, 'Host80'),
+  // 交易历史
+  tradeShardingHistory: (params = {}) => post('/trade/sharding/history', params, 'Host80'),
+  // 交易历史导出
+  tradeShardingHistoryExport: (params = {}) => post('/trade/sharding/history/export', params, 'Host80'),
 };

+ 1 - 1
uni_modules/uni-popup/components/uni-popup/uni-popup.vue

@@ -468,7 +468,7 @@
 	.uni-popup {
 		position: fixed;
 		/* #ifndef APP-NVUE */
-		z-index: 99;
+		z-index: 101;
 
 		/* #endif */
 		&.top,

+ 10 - 1
windows/left-window.vue

@@ -47,6 +47,8 @@ const menu = ref<MenuItem[]>(
                 { path: '/pages/customer/withdrawal', label: 'Home.page_customer.item3', icon: 'icon-withdrawal' },
                 { path: '/pages/customer/payment-history', label: 'Home.page_customer.item4', icon: 'icon-payment' },
                 { path: '/pages/customer/transfer', label: 'Home.page_customer.item5', icon: 'icon-transfer' },
+                { path: '/pages/customer/trade-history', label: 'Ib.Report.Tit1', icon: 'icon-transfer' },
+                { path: '/pages/customer/trade-position', label: 'Ib.Report.Tit4', icon: 'icon-transfer' },
                 { path: '/pages/customer/recording-history', label: 'Home.page_customer.item7', icon: 'icon-application' }
             ]
         },
@@ -63,7 +65,14 @@ const menu = ref<MenuItem[]>(
                 { path: '/pages/ib/recording', label: 'Home.page_ib.item7', icon: 'icon-application' }
             ]
         },
-
+        {
+            path: '/pages/analytics/analystViews', isOpenMenu: false, label: 'News.News', icon: 'crm-chart-area',
+            children: [
+                { path: '/pages/analytics/analystViews', label: 'News.Announcement', icon: 'icon-application' },
+                { path: '/pages/analytics/news', label: 'News.NewsInformation', icon: 'icon-application' },
+                { path: `https://www.${Config.host}.com/${locale.value}/economic-calendar`, label: 'News.FinancialCalendar', icon: 'icon-application', isExternal: true },
+            ]
+        },
         {
             path: '/pages/customer/withdrawal', isOpenMenu: false, label: 'Downloadpage.item1', icon: 'crm-download',
             children: []