zhb пре 2 месеци
родитељ
комит
e274fc9d8a

+ 455 - 0
components/cwg-complex-search.vue

@@ -0,0 +1,455 @@
+<template>
+    <view class="complex-search">
+        <!-- PC/平板端:直接显示表单 -->
+        <cwg-match-media :min-width="991">
+            <view v-if="!isMobile" class="search-bar search-form">
+                <view v-for="(row, rowIndex) in formRows" :key="rowIndex" class="form-row">
+                    <view v-for="field in row" :key="field.key" class="form-item">
+                        <view class="search-bar">
+                            <!-- 根据字段类型渲染不同组件 -->
+                            <template v-if="field.type === 'input'">
+                                <uni-easyinput v-model="formData[field.key]" :placeholder="field.placeholder || '请输入'"
+                                    clearable />
+                            </template>
+                            <template v-else-if="field.type === 'select'">
+                                <cwg-combox v-model:value="formData[field.key]" :options="field.options"
+                                    :placeholder="field.placeholder || '请选择'" clearable @change="handleSearch" />
+                            </template>
+                            <template v-else-if="field.type === 'date'">
+                                <uni-datetime-picker v-model="formData[field.key]" type="date"
+                                    :placeholder="field.placeholder || '请选择日期'" @change="handleDateChange" />
+                            </template>
+                            <template v-else-if="field.type === 'daterange'">
+                                <uni-datetime-picker v-model="formData[field.key]" type="daterange"
+                                    :placeholder="field.placeholder || '请选择日期范围'" @change="handleDateChange" />
+                            </template>
+                            <template v-else-if="field.type === 'number'">
+                                <uni-easyinput v-model="formData[field.key]" type="number"
+                                    :placeholder="field.placeholder || '请输入数字'" />
+                            </template>
+                        </view>
+                    </view>
+                </view>
+                <view class="form-actions">
+                    <button class="reset-btn" @click="resetForm" v-t="'Documentary.tradingCenter.item4'" />
+                    <button class="search-btn" type="primary" @click="handleSearch" v-t="'Btn.Search'" />
+                </view>
+            </view>
+        </cwg-match-media>
+        <cwg-match-media :max-width="991">
+            <!-- 移动端:只显示筛选按钮,点击弹出底部抽屉 -->
+            <view class="mobile-filter">
+                <view v-if="dateField" class="mobile-date-wrapper">
+                    <uni-datetime-picker v-model="formData[dateField.key]"
+                        :type="dateField.type === 'daterange' ? 'daterange' : 'date'"
+                        :placeholder="dateField.placeholder || (dateField.type === 'daterange' ? '选择日期范围' : '选择日期')"
+                        @change="handleDateChange" />
+                </view>
+                <button class="filter-chip" @click="openFilterPopup">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
+                        stroke="#141d22" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                        class="filter-icon">
+                        <path
+                            d="M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z">
+                        </path>
+                    </svg>
+                    <text class="filter-label" v-t="'Documentary.tradingCenter.item3'" />
+                </button>
+            </view>
+        </cwg-match-media>
+
+        <cwg-popup v-model:visible="visible" type="center" :mask-click="false" :showFooters="true"
+            :title="t('Documentary.tradingCenter.item3')">
+            <scroll-view scroll-y class="drawer-content">
+                <view v-for="field in nonDateField" :key="field.key" class="filter-item">
+                    <view class="label">{{ field.label }}</view>
+                    <view class="control">
+                        <template v-if="field.type === 'input'">
+                            <uni-easyinput v-model="tempFormData[field.key]" :placeholder="field.placeholder || '请输入'"
+                                clearable />
+                        </template>
+                        <template v-else-if="field.type === 'select'">
+                            <uni-data-select v-model:value="tempFormData[field.key]" :localdata="field.options"
+                                :placeholder="field.placeholder || '请选择'" clearable v-if="shouldUseSelect(field)" />
+                            <view class="chip-group" v-else>
+                                <view class="chip-list">
+                                    <view v-for="opt in field.options" :key="opt.value" class="chip" :class="{
+                                        'chip-filled': tempFormData[field.key] === opt.value,
+                                        'chip-outlined': tempFormData[field.key] !== opt.value
+                                    }" @click="selectChip(field.key, opt.value)">
+                                        {{ opt.text }}
+                                    </view>
+                                </view>
+                            </view>
+                        </template>
+                        <template v-else-if="field.type === 'number'">
+                            <uni-easyinput v-model="tempFormData[field.key]" type="number"
+                                :placeholder="field.placeholder || '请输入数字'" />
+                        </template>
+                    </view>
+                </view>
+            </scroll-view>
+            <template #footer>
+                <button class="reset-btn" @click="resetTempForm" v-t="'Documentary.tradingCenter.item4'" />
+                <button class="search-btn" type="primary" @click="applyFilter" v-t="'Btn.Search'" />
+            </template>
+        </cwg-popup>
+
+    </view>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
+const props = defineProps({
+    // 字段配置列表,每个字段包含: key, label, type, options(可选), placeholder(可选)
+    fields: {
+        type: Array,
+        required: true
+    },
+    // 每行显示的字段数(PC端),默认3个
+    columns: {
+        type: Number,
+        default: 1
+    },
+    // 表单初始值 / v-model 绑定值
+    modelValue: {
+        type: Object,
+        default: () => ({})
+    }
+})
+
+const emit = defineEmits(['update:modelValue', 'search', 'reset'])
+
+// 表单数据(PC端)
+const formData = ref({})
+// 移动端临时数据
+const tempFormData = ref({})
+// 移动端弹窗是否可见
+const visible = ref(false)
+
+// 将字段按列数分组(仅用于PC布局)
+const formRows = computed(() => {
+    const rows = []
+    for (let i = 0; i < props.fields.length; i += props.columns) {
+        rows.push(props.fields.slice(i, i + props.columns))
+    }
+    return rows
+})
+
+const selectChip = (key, value) => {
+    // 如果点击的是当前已选中的项,可以保持不变,也可以置空(根据需求)
+    // 此处保持选中该项,不提供取消功能(符合常见筛选行为)
+    tempFormData.value[key] = value
+}
+// 初始化表单数据
+const initFormData = () => {
+    const initial = {}
+    props.fields.forEach(field => {
+        // 1. 优先使用外部传入的 modelValue
+        if (props.modelValue && props.modelValue[field.key] !== undefined) {
+            initial[field.key] = props.modelValue[field.key]
+        }
+        // 2. 其次使用字段配置的 defaultValue
+        else if (field.defaultValue !== undefined) {
+            initial[field.key] = field.defaultValue
+        }
+        // 3. 日期字段特殊处理:默认当前月
+        else if (field.type === 'date' || field.type === 'daterange') {
+            initial[field.key] = getDefaultDateValue(field)
+        }
+        // 4. 其他字段默认为空字符串
+        else {
+            initial[field.key] = ''
+        }
+    })
+    formData.value = initial
+    tempFormData.value = JSON.parse(JSON.stringify(initial))
+}
+// 获取第一个日期或日期范围字段(用于移动端顶部快捷显示)
+const dateField = computed(() => {
+    return props.fields.find(field => field.type === 'date' || field.type === 'daterange')
+})
+// 判断字段是否应该使用下拉选择器(用于排序和渲染)
+const shouldUseSelect = (field) => {
+    if (!field.options) return false
+    return field.options.length > 10 || field.isSelect === true
+}
+const handleDateChange = (value) => {
+    // 日期变化时自动触发搜索
+    handleSearch()
+}
+// 排序:输入框 + 下拉选择器 → 前面;Chip 选择器 → 后面
+const nonDateField = computed(() => {
+    const filtered = props.fields.filter(field => field.type !== 'date' && field.type !== 'daterange')
+    const priorityFields = []
+    const otherFields = []
+
+    filtered.forEach(field => {
+        if (field.type === 'input' || field.type === 'number') {
+            priorityFields.push(field)
+        }
+        else if (field.type === 'select') {
+            if (shouldUseSelect(field)) {
+                priorityFields.push(field)
+            } else {
+                otherFields.push(field)
+            }
+        }
+        else {
+            otherFields.push(field)
+        }
+    })
+    return [...priorityFields, ...otherFields]
+})
+
+// 监听外部 modelValue 变化
+watch(() => props.modelValue, (newVal) => {
+    if (newVal) {
+        props.fields.forEach(field => {
+            if (newVal[field.key] !== undefined) {
+                formData.value[field.key] = newVal[field.key]
+            }
+        })
+    }
+}, { deep: true, immediate: true })
+
+// 监听内部 formData 变化,同步到外部
+watch(formData, (newVal) => {
+    emit('update:modelValue', newVal)
+}, { deep: true })
+
+// 重置表单(清空所有字段)
+const resetForm = () => {
+    const empty = {}
+    props.fields.forEach(field => {
+        if (field.defaultValue !== undefined) {
+            empty[field.key] = field.defaultValue
+        } else if (field.type === 'date' || field.type === 'daterange') {
+            empty[field.key] = getDefaultDateValue(field)
+        } else {
+            empty[field.key] = ''
+        }
+    })
+    formData.value = empty
+    emit('reset', empty)
+}
+// 触发查询
+const handleSearch = () => {
+    emit('search', { ...formData.value })
+}
+
+// 移动端:打开弹窗
+const openFilterPopup = () => {
+    // 复制当前表单数据到临时对象
+    tempFormData.value = JSON.parse(JSON.stringify(formData.value))
+    visible.value = true
+}
+
+const closePopup = () => {
+    visible.value = false
+}
+
+const resetTempForm = () => {
+    const empty = {}
+    props.fields.forEach(field => {
+        if (field.defaultValue !== undefined) {
+            empty[field.key] = field.defaultValue
+        } else if (field.type === 'date' || field.type === 'daterange') {
+            empty[field.key] = getDefaultDateValue(field)
+        } else {
+            empty[field.key] = ''
+        }
+    })
+    tempFormData.value = empty
+}
+// 获取日期字段的默认值(当前月范围或当天)
+const getDefaultDateValue = (field) => {
+    const now = new Date()
+    const year = now.getFullYear()
+    const month = now.getMonth()
+    const firstDay = new Date(year, month, 1)
+    const lastDay = new Date(year, month + 1, 0)
+
+    const formatDate = (date) => {
+        const y = date.getFullYear()
+        const m = String(date.getMonth() + 1).padStart(2, '0')
+        const d = String(date.getDate()).padStart(2, '0')
+        return `${y}-${m}-${d}`
+    }
+
+    if (field.type === 'daterange') {
+        return [formatDate(firstDay), formatDate(lastDay)]
+    } else if (field.type === 'date') {
+        // 单日期默认设为今天(也可设为第一天,根据需求调整)
+        return formatDate(now)
+    }
+    return ''
+}
+const applyFilter = () => {
+    // 将临时数据同步到正式表单
+    formData.value = JSON.parse(JSON.stringify(tempFormData.value))
+    closePopup()
+    handleSearch()
+}
+
+// 监听fields变化,重新初始化
+watch(() => props.fields, () => {
+    initFormData()
+}, { deep: true, immediate: false })
+
+onMounted(() => {
+    // 初始化表单数据
+    initFormData()
+})
+
+</script>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.complex-search {
+    width: 100%;
+}
+
+.search-bar {
+
+    .cwg-combox,
+    .uni-easyinput,
+    .uni-date {
+        width: px2rpx(240) !important;
+        flex: none;
+    }
+
+    .form-actions {
+        display: flex;
+        justify-content: flex-end;
+        gap: px2rpx(10);
+
+        .reset-btn,
+        .search-btn {
+            line-height: px2rpx(35);
+            font-size: px2rpx(14);
+        }
+    }
+}
+
+.mobile-filter {
+    margin-bottom: px2rpx(10);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: px2rpx(12);
+
+    .mobile-date-wrapper {
+        flex: 1;
+    }
+
+    .filter-chip {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+        background-color: #ffffff;
+        border: 1px solid #e5e5e5;
+        border-radius: px2rpx(4);
+        padding: px2rpx(0) px2rpx(16);
+        font-size: px2rpx(16);
+        font-weight: 500;
+        color: #141d22;
+        line-height: px2rpx(35);
+        transition: all 0.2s ease;
+        box-shadow: none;
+
+        .iconfont,
+        .uni-icons {
+            font-size: 32rpx;
+            color: #141d22;
+        }
+
+        /* 按下反馈效果 */
+        &:active {
+            transform: scale(0.96);
+            background-color: #f5f7fa;
+            /* 轻量点击背景色 */
+            border-color: #b9bfc7;
+        }
+
+        &::after {
+            border: none;
+        }
+    }
+}
+
+/* 弹窗整体优化 */
+.drawer-content {
+    padding: px2rpx(20);
+    border-radius: px2rpx(16);
+    box-sizing: border-box;
+
+    .label {
+        font-size: px2rpx(18);
+        font-weight: 600;
+        color: #2c3e50;
+        margin-bottom: px2rpx(20);
+        letter-spacing: px2rpx(1);
+    }
+
+    :deep(.uni-scroll-view-content) {
+        display: flex;
+        flex-direction: column;
+        gap: px2rpx(20);
+    }
+
+    :deep(uni-button) {
+        line-height: px2rpx(35);
+    }
+
+    /* ========== Chip 胶囊样式 (移动端筛选弹窗) ========== */
+    .chip-group {
+        margin-bottom: px2rpx(20);
+
+        .chip-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: px2rpx(16);
+        }
+
+        .chip {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            padding: px2rpx(4) px2rpx(12);
+            border-radius: px2rpx(64);
+            font-size: px2rpx(14);
+            font-weight: 500;
+            transition: all 0.2s ease;
+            background-color: #ffffff;
+            border: px2rpx(1) solid #d1d5db;
+            color: #141d22;
+            cursor: pointer;
+
+            &:active {
+                transform: scale(0.96);
+            }
+
+            /* 填充样式(选中) */
+            &.chip-filled {
+                background-color: #007aff;
+                border-color: #007aff;
+                color: #ffffff;
+            }
+
+            /* 轮廓样式(未选中) */
+            &.chip-outlined {
+                background-color: #ffffff;
+                border-color: #d1d5db;
+                color: #141d22;
+            }
+        }
+    }
+}
+</style>

+ 28 - 26
pages/customer/payment-history.vue

@@ -2,14 +2,8 @@
     <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
         <cwg-header :title="t('Home.page_customer.item4')" />
         <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-complex-search :fields="filterFields" v-model="searchParams" @search="handleSearch"
+                @reset="handleReset" />
             <cwg-tabel ref="tableRef" :columns="columns" :mobilePrimaryFields="mobilePrimaryFields"
                 :queryParams="search" :api="listApi" :show-operation="false" :showPagination="false">
                 <template #avatar="{ row }">
@@ -122,7 +116,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from 'vue';
+import { computed, ref, nextTick } from 'vue';
 import { useI18n } from 'vue-i18n';
 const { t, locale } = useI18n();
 import { financialApi } from '@/service/financial';
@@ -225,7 +219,7 @@ const columns = ref([
     {
         prop: 'note',
         label: t('Custom.Recording.Note'),
-        formatter: ({ row }) => row.note || '--',
+        type: 'note',
         align: 'right'
     },
     {
@@ -235,7 +229,31 @@ const columns = ref([
         align: 'right'
     },
 ])
+// 动态传入筛选字段配置
+const filterFields = [
+    { key: 'type', type: 'select', label: t('Custom.PaymentHistory.payType'), placeholder: t('placeholder.choose'), options: typeMap.value, defaultValue: null },
+    {
+        key: 'orderStatus', type: 'select', label: t('Custom.PaymentHistory.Status'), placeholder: t('placeholder.choose'), options: orderStatusMap.value, defaultValue: null
+    },
+    { key: 'login', type: 'input', label: t('Custom.PaymentHistory.TradingAccount'), placeholder: t('placeholder.login'), defaultValue: '' },
+    { key: 'date', label: t('placeholder.Start') + ' - ' + t('placeholder.End'), type: 'daterange' }
+]
+
+const searchParams = ref({})
 
+const handleSearch = (params) => {
+    search.value = params
+    nextTick(() => {
+        tableRef.value.refreshTable()
+    })
+}
+
+const handleReset = (emptyParams) => {
+    search.value = {}
+    nextTick(() => {
+        tableRef.value.refreshTable()
+    })
+}
 const mobilePrimaryFields = ref([
     {
         prop: 'serial',
@@ -366,20 +384,4 @@ const cancle = async (id, type) => {
     cursor: not-allowed;
     opacity: 0.5;
 }
-
-.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>

+ 23 - 7
pages/customer/recording-history.vue

@@ -2,12 +2,8 @@
     <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
         <cwg-header :title="t('Home.page_customer.item7')" />
         <view class="info-card">
-            <view class="search-bar">
-                <cwg-combox v-model:value="search.type" :clearable="false" :options="typeMap"
-                    :placeholder="t('Custom.Recording.AccountType')" />
-                <uni-datetime-picker type="daterange" v-model="search.date"
-                    :placeholder="t('placeholder.Start') + ' - ' + t('placeholder.End')" />
-            </view>
+            <cwg-complex-search :fields="filterFields" v-model="searchParams" @search="handleSearch"
+                @reset="handleReset" />
             <cwg-tabel ref="tableRef" :columns="currentColumns" :queryParams="search" :api="listApi"
                 :show-operation="false" :showPagination="false">
                 <!-- 状态列自定义渲染 -->
@@ -35,7 +31,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, onMounted } from 'vue';
+import { computed, ref, nextTick } from 'vue';
 import { useI18n } from 'vue-i18n';
 const { t, locale } = useI18n();
 import { customApi } from '@/service/custom';
@@ -243,7 +239,27 @@ const getColumnsByType = (type: number) => {
             ]
     }
 }
+// 动态传入筛选字段配置
+const filterFields = [
+    { key: 'type', type: 'select', label: t('Custom.PaymentHistory.payType'), placeholder: t('placeholder.choose'), options: typeMap.value, defaultValue: 1 },
+    { key: 'login', type: 'input', label: t('Custom.PaymentHistory.TradingAccount'), placeholder: t('placeholder.login'), defaultValue: '' },
+    { key: 'date', label: t('placeholder.Start') + ' - ' + t('placeholder.End'), type: 'daterange' }
+]
+const searchParams = ref({})
+const tableRef = ref(null)
+const handleSearch = (params) => {
+    search.value = params
+    nextTick(() => {
+        tableRef.value.refreshTable()
+    })
+}
 
+const handleReset = (emptyParams) => {
+    search.value = {}
+    nextTick(() => {
+        tableRef.value.refreshTable()
+    })
+}
 // 当前列配置
 const currentColumns = computed(() => getColumnsByType(search.value.type))
 // 获取状态文本

+ 1 - 1
static/scss/global/global.scss

@@ -951,7 +951,7 @@ uni-content {
     justify-content: flex-start;
     flex-wrap: wrap;
     gap: px2rpx(16);
-    margin: px2rpx(16) 0;
+    // margin: px2rpx(16) 0;
 
     .cwg-combox,
     .uni-easyinput,

+ 2 - 2
uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue

@@ -397,7 +397,7 @@ $uni-text-color-grey: #999;
 	transition-duration: 0.3s;
 	opacity: 0;
 	/* #ifndef APP-NVUE */
-	z-index: 99;
+	z-index: 11199;
 	/* #endif */
 }
 
@@ -417,7 +417,7 @@ $uni-text-color-grey: #999;
 	transform: translateY(460px);
 	/* #ifndef APP-NVUE */
 	bottom: calc(var(--window-bottom));
-	z-index: 99;
+	z-index: 11200;
 	/* #endif */
 }
 

+ 2 - 2
uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue

@@ -658,7 +658,7 @@ $uni-primary: #007aff !default;
 	transition-duration: 0.3s;
 	opacity: 0;
 	/* #ifndef APP-NVUE */
-	z-index: 99;
+	z-index: 11199;
 	/* #endif */
 }
 
@@ -675,7 +675,7 @@ $uni-primary: #007aff !default;
 	transition-duration: 0.3s;
 	transform: translateY(460px);
 	/* #ifndef APP-NVUE */
-	z-index: 99;
+	z-index: 11200;
 	/* #endif */
 }
 

+ 1 - 4
utils/noteHelper.ts

@@ -2,14 +2,11 @@
 /**
  * 获取备注文本(同步纯函数)
  */
-export function getNoteText(row: any, locale: string ,userStore: any): string {
-    console.log('row', row);
+export function getNoteText(row: any, locale: string, userStore: any): string {
     if (!row) return '--';
     if (row.remark) return row.remark;
     if (row.note) return row.note;
-
     if (row.approveDesc) {
-
         const option = userStore.reasonsOptions?.[row.approveDesc];
         if (option) {
             return locale === 'cn' || locale === 'zhHant'