Explorar el Código

refactor(queryReport): 优化查询报表筛选维度与订单详情交互

通过扩展查询条件和增强信息展示能力,提升报表统计的精确度与订单查看的便捷性。

主要变更:
- api: 新增 `getTicketTypeById` 接口用于获取票种详细配置。
- components: 优化 `OrderDetail` 组件,将票种名称改为可点击链接,支持通过弹窗查看完整的票种属性(如分类、价格、团散类型、核销凭证等)。
- queryReport:
  - 核销查询 (checkQuery) 与 退款查询 (refundQuery): 引入使用时间范围、单价区间、景点多选、团队多选及客户类型筛选;将导出功能重构为下拉菜单,支持“核销明细”与“核销流水”两种模式。
  - 销售查询 (salesQuery): 新增支付状态(待支付、已支付、已关闭)的筛选维度。
LaveyD hace 6 días
padre
commit
5e86929dfb

+ 5 - 0
src/api/ticketType.js

@@ -15,6 +15,11 @@ export function getTicketTypeInfo (params) {
   return http.post('/admin/ticketType/detail', { data: params })
 }
 
+// 根据id获取票种详情
+export function getTicketTypeById (params) {
+  return http.post('/admin/ticketType/getById', { data: params })
+}
+
 // 根据游玩日期获取门票信息
 export function getTicketTypeByDate (params) {
   return http.post('/admin/ticketType/get', { data: params })

+ 170 - 1
src/components/GlobalComponents/OrderDetail.vue

@@ -133,7 +133,18 @@
               v-if="isColumnVisible('ticketTypeName')"
               width="100"
               prop="ticketTypeName"
-              label="票种"> </el-table-column>
+              label="票种">
+              <template slot-scope="scope">
+                <el-link
+                  v-if="scope.row.ticketTypeId"
+                  type="primary"
+                  :underline="false"
+                  @click="showTicketTypeDetail(scope.row)">
+                  {{ scope.row.ticketTypeName }}
+                </el-link>
+                <span v-else>{{ scope.row.ticketTypeName }}</span>
+              </template>
+            </el-table-column>
             <el-table-column
               v-if="isColumnVisible('unitPrice')"
               width="80"
@@ -566,6 +577,126 @@
           </el-table>
         </div>
       </el-dialog>
+
+      <el-dialog
+        title="票种信息"
+        :visible.sync="ticketTypeVisible"
+        width="75%"
+        append-to-body
+      >
+        <div v-loading="ticketTypeLoading">
+          <el-form
+            inline
+            label-width="120px"
+            class="dialog-info"
+            v-if="ticketTypeData"
+          >
+            <el-form-item label="票种分类">
+              {{ ticketTypeData.category | formatCategory }}
+            </el-form-item>
+            <el-form-item label="票种名称">
+              {{ ticketTypeData.name }}
+            </el-form-item>
+            <el-form-item label="在售状态">
+              {{ ticketTypeData.isSale === 1 ? '在售' : '禁售' }}
+            </el-form-item>
+            <el-form-item label="是否支持开票">
+              {{ ticketTypeData.isSupportInvoice === 1 ? '支持开票' : '不支持开票' }}
+            </el-form-item>
+            <el-form-item label="销售价格">
+              {{ ticketTypeData.price }} 元
+            </el-form-item>
+            <el-form-item label="团散">
+              {{ ttValue(ticketTypeData.teamIndividual, { 0: '不限', 1: '散客', 2: '团队' }) }}
+            </el-form-item>
+            <el-form-item label="多日票类型">
+              {{ ttValue(ticketTypeData.useDaysDelayType, { 1: '按照游玩时间延长', 2: '按照首次核销时间延长' }) }}
+            </el-form-item>
+            <el-form-item label="日期类型">
+              {{ ttValue(ticketTypeData.useDateType, { 1: '日历票', 2: '期票,时间期限内有效' }) }}
+            </el-form-item>
+            <el-form-item
+              v-if="ticketTypeData.category === 'member'"
+              label="会员时长类型"
+            >
+              {{ ttValue(ticketTypeData.memberUseDateType, { 1: '【按天】', 2: '【按照使用日期】' }) }}
+            </el-form-item>
+            <el-form-item
+              v-if="!(ticketTypeData.category === 'member' && ticketTypeData.memberUseDateType === 1)"
+              label="使用天数"
+            >
+              {{ ticketTypeData.useDays }}
+            </el-form-item>
+            <el-form-item
+              v-if="!(ticketTypeData.category === 'member' && ticketTypeData.memberUseDateType === 2)"
+              label="使用日期"
+            >
+              {{ ticketTypeData.useDateStart }} - {{ ticketTypeData.useDateEnd }}
+            </el-form-item>
+            <el-form-item
+              v-if="ticketTypeData.category === 'member'"
+              label="激活方式"
+            >
+              {{ ttValue(ticketTypeData.memberActiveType, { 1: '【购买即激活】', 2: '【首次使用激活】' }) }}
+            </el-form-item>
+            <el-form-item
+              v-if="ticketTypeData.isSaleDateLimit === 1"
+              label="售卖时间"
+            >
+              {{ ticketTypeData.saleDateStart }} - {{ ticketTypeData.saleDateEnd }}
+            </el-form-item>
+            <el-form-item label="核销凭证">
+              {{ ttLabels(ticketTypeData.checkType, checkTypeDic) }}
+            </el-form-item>
+            <el-form-item label="核销规则">
+              {{ ttLabels(ticketTypeData.checkRule, checkRuleDic) }}
+            </el-form-item>
+            <el-form-item label="批量核销">
+              {{ ticketTypeData.batchCheckRule === 'batch' ? '当票可以使用多次时,一次核销完成' : '单次核销' }}
+            </el-form-item>
+            <el-form-item label="核销语音">
+              {{ ticketTypeData.checkVoice || '(默认播报票种名称)' }}
+            </el-form-item>
+            <el-form-item label="星期检票规则">
+              {{ ttWeekday(ticketTypeData.useDayOfWeek) }}
+            </el-form-item>
+            <el-form-item label="销售渠道">
+              {{ ttLabels(ticketTypeData.source, [{ value: 'window', label: '窗口' }, { value: 'term', label: '自助机' }]) }}
+            </el-form-item>
+            <el-form-item label="是否可以退订">
+              {{ ticketTypeData.isAllowCancel === 1 ? '可以' : '不可以' }}
+            </el-form-item>
+            <el-form-item label="是否必须换票使用">
+              {{ ticketTypeData.isNeedPrint === 1 ? '需要' : '不限' }}
+            </el-form-item>
+            <el-form-item label="过期操作">
+              {{ ttValue(ticketTypeData.outOfDateDeal, { 1: '人工处理', 2: '自动核销' }) }}
+            </el-form-item>
+          </el-form>
+          <div class="block-title" style="margin:10px 0" v-if="ticketTypeData && ticketTypeData.ticketTypeScenicList && ticketTypeData.ticketTypeScenicList.length">
+            适用景点及检票次数
+          </div>
+          <el-table
+            border
+            stripe
+            v-if="ticketTypeData && ticketTypeData.ticketTypeScenicList && ticketTypeData.ticketTypeScenicList.length"
+            :data="ticketTypeData.ticketTypeScenicList"
+            :max-height="200"
+          >
+            <el-table-column prop="scenicName" label="景点"></el-table-column>
+            <el-table-column label="可检票次数">
+              <template slot-scope="scope">
+                {{ scope.row.checkLimitTimes === -1 ? '无限制' : scope.row.checkLimitTimes }}
+              </template>
+            </el-table-column>
+            <el-table-column label="单日可检票次数">
+              <template slot-scope="scope">
+                {{ scope.row.singleDayLimitTimes === -1 ? '无限制' : scope.row.singleDayLimitTimes }}
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-dialog>
     </el-form>
   </el-dialog>
 </template>
@@ -574,6 +705,8 @@
 import { channelList, IDENTIFY_TYPES } from '@/assets/staticData'
 import { getPayStatus } from '@/utils/index'
 import { addUserConfig, getUserConfig } from '@/api/configSetting'
+import { getTicketTypeById } from '@/api/ticketType'
+import { checkTypeDic, checkRuleDic } from '@/const'
 import QRCode from 'qrcode'
 
 const fixedColumns = [
@@ -631,11 +764,16 @@ export default {
     return {
       channelList,
       papersType: IDENTIFY_TYPES,
+      checkTypeDic,
+      checkRuleDic,
       posType: '',
       orderData: null,
       visible: false,
       detailVisible: false,
       record: {},
+      ticketTypeVisible: false,
+      ticketTypeLoading: false,
+      ticketTypeData: null,
       activeTab: 'ticketCheckInfo',
       dialogVisible: false,
       fixedColumns,
@@ -849,6 +987,37 @@ export default {
       this.record = record
       this.detailVisible = true
     },
+    async showTicketTypeDetail (ticket) {
+      if (!ticket.ticketTypeId) return
+      this.ticketTypeVisible = true
+      this.ticketTypeLoading = true
+      this.ticketTypeData = null
+      try {
+        const res = await getTicketTypeById({ id: ticket.ticketTypeId })
+        this.ticketTypeData = res.data || null
+      } finally {
+        this.ticketTypeLoading = false
+      }
+    },
+    ttValue (val, map) {
+      if (val === undefined || val === null || val === '') return ''
+      return map[val] || val
+    },
+    ttLabels (val, dic) {
+      if (!val) return ''
+      const arr = Array.isArray(val) ? val : String(val).split(',')
+      return arr.map(item => {
+        const found = dic.find(i => `${i.value}` === `${item}`)
+        return found ? found.label : item
+      }).join('、')
+    },
+    ttWeekday (val) {
+      if (!val) return ''
+      const arr = String(val).split(',')
+      const weekdayMap = { 1: '星期日', 2: '星期一', 3: '星期二', 4: '星期三', 5: '星期四', 6: '星期五', 7: '星期六' }
+      if (arr.length === 7) return '每天'
+      return arr.map(i => weekdayMap[i]).filter(Boolean).join('、')
+    },
     isValidUrl (url) {
       try {
         const raw = String(url || '').trim()

+ 121 - 7
src/pages/queryReport/checkQuery.vue

@@ -206,6 +206,83 @@
           </el-option>
         </el-select>
       </el-form-item>
+      <el-form-item
+        label="使用时间"
+        prop="useTime">
+        <el-date-picker
+          v-model="form.useTime"
+          style="width:240px"
+          type="daterange"
+          format="yyyy-MM-dd"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item
+        label="单价区间"
+        prop="unitPriceBegin"
+      >
+        <el-input-number
+          v-model="form.unitPriceBegin"
+          :min="0"
+          :step="0.01"
+          :precision="2"></el-input-number>&nbsp;~&nbsp;
+        <el-input-number
+          v-model="form.unitPriceEnd"
+          :min="0"
+          :step="0.01"
+          :precision="2"></el-input-number>
+      </el-form-item>
+      <el-form-item
+        label="景点"
+        prop="scenicIdList"
+      >
+        <el-select
+          v-model="form.scenicIdList"
+          placeholder="请选择"
+          multiple
+          clearable
+          filterable>
+          <el-option
+            v-for="(item, idx) in scenicList"
+            :key="`scene-${idx}`"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="团队"
+        prop="teamIdList"
+      >
+        <el-select
+          v-model="form.teamIdList"
+          multiple
+          placeholder="请选择"
+          clearable
+          filterable>
+          <el-option
+            v-for="item in teamList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="客户类型"
+        prop="teamIndividual"
+      >
+        <el-select
+          v-model="form.teamIndividual"
+          placeholder="请选择"
+          clearable>
+          <el-option label="全部" value=""></el-option>
+          <el-option label="团队" value="2"></el-option>
+          <el-option label="散客" value="1"></el-option>
+        </el-select>
+      </el-form-item>
       <div class="btn-wrap">
         <el-button
           @click="reset"
@@ -219,11 +296,15 @@
         >
           搜索
         </el-button>
-        <!-- <el-button
-          type="primary"
-          @click="reportExport">
-          导出
-        </el-button> -->
+        <el-dropdown @command="reportExport" style="margin-left: 10px;">
+          <el-button type="primary">
+            导出<i class="el-icon-arrow-down el-icon--right"></i>
+          </el-button>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item command="ticket">核销明细</el-dropdown-item>
+            <el-dropdown-item command="checkLog">核销流水</el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
       </div>
     </el-form>
 
@@ -328,7 +409,7 @@
 <script>
 import { getCheckList } from '@/api/queryReport'
 import { getSaleChannelList, getSingleOrder } from '@/api/order'
-import { getDeviceList } from '@/api/checker'
+import { getDeviceList, getTeamList } from '@/api/checker'
 import ReaderInput from '@/components/ReaderInput'
 import TicketTypeSelect from '@/components/TicketTypeSelect'
 import moment from 'moment'
@@ -347,6 +428,7 @@ export default {
       orderCategories,
       ticketStatusDic,
       otaSourceList: [],
+      teamList: [],
       form: {
         timeBegin: new Date(moment().startOf('day').valueOf()),
         timeEnd: new Date(moment().endOf('day').valueOf()),
@@ -362,6 +444,14 @@ export default {
         saleTermIdList: [],
         adminIdList: [],
         otaSourceNameList: [],
+        useTime: [],
+        useTimeBegin: '',
+        useTimeEnd: '',
+        unitPriceBegin: undefined,
+        unitPriceEnd: undefined,
+        scenicIdList: [],
+        teamIdList: [],
+        teamIndividual: '',
         pageNum: 1,
         pageSize: 10,
         ticketNo: '',
@@ -414,6 +504,9 @@ export default {
       const { orderCategory } = this.form
       return orderCategory ? ticketTypeList.filter(item => item.category === orderCategory) : ticketTypeList
     },
+    scenicList () {
+      return this.$store.state.app.scenicList
+    },
     payChannelOptions () {
       return this.$store.getters.payChannelOptions
     }
@@ -425,6 +518,7 @@ export default {
     }
     this.getSaleChannelList()
     this.getCheckerList()
+    this.getTeams()
     this.getList()
   },
   methods: {
@@ -449,8 +543,15 @@ export default {
         console.error('获取检票设备列表失败', e)
       }
     },
+    getTeams () {
+      getTeamList({ pageSize: -1, pageNum: 1 }).then(res => {
+        this.teamList = res.data.records || []
+      })
+    },
     reset () {
       this.$refs.form.resetFields()
+      this.form.unitPriceBegin = undefined
+      this.form.unitPriceEnd = undefined
     },
     setSearchKeywords (val) {
       this.form.searchKeywords = val
@@ -470,9 +571,10 @@ export default {
       })
       this.reset()
     },
-    reportExport () {
+    reportExport (exportType) {
       this.handleExport({
         ...this.form,
+        checkExportType: exportType || 'ticket',
         export: true
       })
     },
@@ -499,6 +601,18 @@ export default {
         this.form.timeEnd = ''
       }
 
+      if (!this.form.useTime || this.form.useTime.length !== 2) {
+        this.form.useTimeBegin = ''
+        this.form.useTimeEnd = ''
+      } else {
+        this.form.useTimeBegin = moment(this.form.useTime[0]).format('YYYY-MM-DD')
+        this.form.useTimeEnd = moment(this.form.useTime[1]).format('YYYY-MM-DD')
+      }
+
+      if (this.form.unitPriceBegin && this.form.unitPriceEnd && this.form.unitPriceBegin > this.form.unitPriceEnd) {
+        this.$message.warning('单价区间起始金额不能大于终止金额')
+      }
+
       if (!this.form.ticketTypeIdList) {
         this.form.ticketTypeIdList = []
       }

+ 113 - 0
src/pages/queryReport/refundQuery/RefundQuery.vue

@@ -189,6 +189,83 @@
           </el-option>
         </el-select>
       </el-form-item>
+      <el-form-item
+        label="使用时间"
+        prop="useTime">
+        <el-date-picker
+          v-model="form.useTime"
+          style="width:240px"
+          type="daterange"
+          format="yyyy-MM-dd"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item
+        label="单价区间"
+        prop="unitPriceBegin"
+      >
+        <el-input-number
+          v-model="form.unitPriceBegin"
+          :min="0"
+          :step="0.01"
+          :precision="2"></el-input-number>&nbsp;~&nbsp;
+        <el-input-number
+          v-model="form.unitPriceEnd"
+          :min="0"
+          :step="0.01"
+          :precision="2"></el-input-number>
+      </el-form-item>
+      <el-form-item
+        label="景点"
+        prop="scenicIdList"
+      >
+        <el-select
+          v-model="form.scenicIdList"
+          placeholder="请选择"
+          multiple
+          clearable
+          filterable>
+          <el-option
+            v-for="(item, idx) in scenicList"
+            :key="`scene-${idx}`"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="团队"
+        prop="teamIdList"
+      >
+        <el-select
+          v-model="form.teamIdList"
+          multiple
+          placeholder="请选择"
+          clearable
+          filterable>
+          <el-option
+            v-for="item in teamList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="客户类型"
+        prop="teamIndividual"
+      >
+        <el-select
+          v-model="form.teamIndividual"
+          placeholder="请选择"
+          clearable>
+          <el-option label="全部" value=""></el-option>
+          <el-option label="团队" value="2"></el-option>
+          <el-option label="散客" value="1"></el-option>
+        </el-select>
+      </el-form-item>
       <div class="btn-wrap">
         <el-button
           @click="reset"
@@ -317,6 +394,7 @@
 <script>
 import { getCancelOrderList } from '@/api/queryReport'
 import { getSaleChannelList, getSingleOrder } from '@/api/order'
+import { getTeamList } from '@/api/checker'
 import ReaderInput from '@/components/ReaderInput'
 import TicketTypeSelect from '@/components/TicketTypeSelect'
 import moment from 'moment'
@@ -336,6 +414,7 @@ export default {
       orderCategories,
       ticketStatusDic,
       otaSourceList: [],
+      teamList: [],
       form: {
         timeBegin: new Date(moment().startOf('day').valueOf()),
         timeEnd: new Date(moment().endOf('day').valueOf()),
@@ -348,6 +427,17 @@ export default {
         payChannelList: [],
         orderStatus: '',
         ticketTypeIdList: [],
+        adminIdList: [],
+        saleTermIdList: [],
+        otaSourceNameList: [],
+        useTime: [],
+        useTimeBegin: '',
+        useTimeEnd: '',
+        unitPriceBegin: undefined,
+        unitPriceEnd: undefined,
+        scenicIdList: [],
+        teamIdList: [],
+        teamIndividual: '',
         pageNum: 1,
         pageSize: 10,
         ticketNo: '',
@@ -371,6 +461,9 @@ export default {
       const { orderCategory } = this.form
       return orderCategory ? ticketTypeList.filter(item => item.category === orderCategory) : ticketTypeList
     },
+    scenicList () {
+      return this.$store.state.app.scenicList
+    },
     payChannelOptions () {
       return this.$store.getters.payChannelOptions
     }
@@ -381,6 +474,7 @@ export default {
       this.form = searchParams // 记住上次搜索条件
     }
     this.getSaleChannelList()
+    this.getTeams()
     this.getList()
   },
   methods: {
@@ -397,8 +491,15 @@ export default {
         this.otaSourceList = res.data
       })
     },
+    getTeams () {
+      getTeamList({ pageSize: -1, pageNum: 1 }).then(res => {
+        this.teamList = res.data.records || []
+      })
+    },
     reset () {
       this.$refs.form.resetFields()
+      this.form.unitPriceBegin = undefined
+      this.form.unitPriceEnd = undefined
     },
     setSearchKeywords (val) {
       this.form.searchKeywords = val
@@ -433,6 +534,18 @@ export default {
         this.form.timeEnd = ''
       }
 
+      if (!this.form.useTime || this.form.useTime.length !== 2) {
+        this.form.useTimeBegin = ''
+        this.form.useTimeEnd = ''
+      } else {
+        this.form.useTimeBegin = moment(this.form.useTime[0]).format('YYYY-MM-DD')
+        this.form.useTimeEnd = moment(this.form.useTime[1]).format('YYYY-MM-DD')
+      }
+
+      if (this.form.unitPriceBegin && this.form.unitPriceEnd && this.form.unitPriceBegin > this.form.unitPriceEnd) {
+        this.$message.warning('单价区间起始金额不能大于终止金额')
+      }
+
       if (!this.form.ticketTypeIdList) {
         this.form.ticketTypeIdList = []
       }

+ 29 - 0
src/pages/queryReport/salesQuery.vue

@@ -139,6 +139,27 @@
           </el-option>
         </el-select>
       </el-form-item>
+      <el-form-item
+        label="支付状态"
+        prop="payStatus"
+      >
+        <el-select
+          v-model="form.payStatus"
+          clearable
+          placeholder="请选择"
+        >
+          <el-option
+            label="全部"
+            value=""></el-option>
+          <el-option
+            v-for="item in payStatusList"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
       <el-form-item
         label="订单类型"
         prop="orderCategory"
@@ -1831,6 +1852,12 @@ const COLUMN_SCHEMA = [
   { key: 'cancelCheckNum', label: '取消票人数', group: '退款', column: 'extra', apiKey: 'cancelCheckNum' }
 ]
 
+const PAY_STATUS_LIST = [
+  { value: 'WAIT_PAY', label: '待支付' },
+  { value: 'PAYED', label: '已支付' },
+  { value: 'CLOSED', label: '已关闭' }
+]
+
 const API_KEY_TO_FIELD = COLUMN_SCHEMA.reduce((acc, item) => {
   acc[item.apiKey] = item.key
   return acc
@@ -1910,6 +1937,7 @@ export default {
       papersType,
       isDev: process.env.NODE_ENV === 'development',
       orderCategories,
+      payStatusList: PAY_STATUS_LIST,
       form: {
         searchKeywords: '', // 搜索关键字
         partnerOrderNo: '', // OTA订单号
@@ -1928,6 +1956,7 @@ export default {
         adminIdList: [], // 售票员
         saleTermIdList: [], // 售卖自助机
         orderStatus: '', // 订单状态
+        payStatus: '', // 支付状态
         ticketStatus: '', // 门票状态
         visitorName: '', // 游客姓名
         visitorPhone: '', // 游客手机