540 lines
13 KiB
Vue
540 lines
13 KiB
Vue
<template>
|
||
<view class="feature-page">
|
||
<view class="page-header">
|
||
<view class="nav-bar">
|
||
<view class="nav-left" @click="goBack">
|
||
<text class="back-icon">←</text>
|
||
</view>
|
||
<text class="nav-title">过度承诺统计</text>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view
|
||
class="content-scroll"
|
||
scroll-y="true"
|
||
lower-threshold="100"
|
||
@scrolltolower="onScrollToLower"
|
||
>
|
||
<view class="stats-card">
|
||
<view class="stat-item">
|
||
<text class="stat-value">{{ overCommitmentRateDisplay }}</text>
|
||
<text class="stat-label">过度承诺率</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-value">{{ listTotal }}</text>
|
||
<text class="stat-label">违规事件数</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="chart-section">
|
||
<text class="section-title">过度承诺趋势图</text>
|
||
<view class="chart-placeholder">
|
||
<text class="placeholder-text">⚠️ 图表区域</text>
|
||
<text class="placeholder-desc">此处将显示过度承诺统计趋势图表</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="search-toolbar">
|
||
<input
|
||
class="search-input"
|
||
v-model="listQuery.reasonKeyword"
|
||
placeholder="搜索触发原因关键词"
|
||
placeholder-style="color: #b0b0b0"
|
||
confirm-type="search"
|
||
@confirm="onSearch"
|
||
/>
|
||
<view class="toolbar-actions">
|
||
<view class="toolbar-btn" @click="onSearch">
|
||
<text>搜索</text>
|
||
</view>
|
||
<view class="toolbar-btn toolbar-btn--ghost" @click="onRefresh">
|
||
<text>刷新</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="data-list">
|
||
<view class="section-title-row">
|
||
<text class="section-title">触及红线记录</text>
|
||
<text class="section-meta">共 {{ listTotal }} 条</text>
|
||
</view>
|
||
|
||
<view v-if="listLoading && !dataList.length" class="list-hint">加载中…</view>
|
||
<view v-else-if="listError" class="list-hint list-hint--error">{{ listError }}</view>
|
||
<view v-else-if="!listLoading && !dataList.length" class="list-hint">暂无记录</view>
|
||
|
||
<view
|
||
class="list-item"
|
||
v-for="item in dataList"
|
||
:key="item.id || item.Id || item.audioManagementSegmentsId"
|
||
>
|
||
<view class="item-main">
|
||
<view class="item-row item-row--title">
|
||
<text class="item-name">{{ displaySalesName(item) }}</text>
|
||
<text class="item-time">{{ formatDateTime(item.createTime || item.create_time) }}</text>
|
||
</view>
|
||
<view class="item-row" v-if="displaySalesPhone(item)">
|
||
<text class="item-label">电话</text>
|
||
<text class="item-value">{{ displaySalesPhone(item) }}</text>
|
||
</view>
|
||
<view class="item-row" v-if="item.redLineType || item.red_line_type">
|
||
<text class="item-label">类型</text>
|
||
<text class="item-value">{{ item.redLineType || item.red_line_type }}</text>
|
||
</view>
|
||
<view class="item-row" v-if="item.redLineSubtype || item.red_line_subtype">
|
||
<text class="item-label">子类型</text>
|
||
<text class="item-value">{{ item.redLineSubtype || item.red_line_subtype }}</text>
|
||
</view>
|
||
<view class="item-row" v-if="item.checkThreshold || item.check_threshold">
|
||
<text class="item-label">阈值</text>
|
||
<text class="item-value">{{ item.checkThreshold || item.check_threshold }}</text>
|
||
</view>
|
||
<view class="item-reason" v-if="item.reasonText || item.reason_text">
|
||
<text class="item-reason-label">触发文本</text>
|
||
<text class="item-reason-text">{{ item.reasonText || item.reason_text }}</text>
|
||
</view>
|
||
<view class="item-row" v-if="item.remark">
|
||
<text class="item-label">备注</text>
|
||
<text class="item-value item-value--muted">{{ item.remark }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="listLoading && dataList.length" class="list-footer">加载中…</view>
|
||
<view v-else-if="!listHasMore && dataList.length" class="list-footer">没有更多了</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { get } from '@/common/request.js'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
/** 趋势/占比等需单独统计接口;列表页先占位 */
|
||
overCommitmentRateDisplay: '—',
|
||
dataList: [],
|
||
listTotal: 0,
|
||
listPages: 0,
|
||
listPage: { current: 1, size: 10 },
|
||
listQuery: {
|
||
audioManagementId: '',
|
||
audioManagementSegmentsId: '',
|
||
redLineType: '',
|
||
redLineSubtype: '',
|
||
salesId: '',
|
||
reasonKeyword: '',
|
||
createStartTime: '',
|
||
createEndTime: ''
|
||
},
|
||
listLoading: false,
|
||
listError: '',
|
||
listHasMore: true
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.applyRouteQuery(options || {})
|
||
this.refreshList()
|
||
},
|
||
methods: {
|
||
goBack() {
|
||
uni.navigateBack()
|
||
},
|
||
/** 支持从路由传入筛选条件(与后端 @RequestParam 一致,camelCase) */
|
||
applyRouteQuery(options) {
|
||
const keys = [
|
||
'audioManagementId',
|
||
'audioManagementSegmentsId',
|
||
'redLineType',
|
||
'redLineSubtype',
|
||
'salesId',
|
||
'reasonKeyword',
|
||
'createStartTime',
|
||
'createEndTime'
|
||
]
|
||
keys.forEach((k) => {
|
||
const v = options[k]
|
||
if (v != null && String(v).trim() !== '') {
|
||
try {
|
||
this.listQuery[k] = decodeURIComponent(String(v).trim())
|
||
} catch {
|
||
this.listQuery[k] = String(v).trim()
|
||
}
|
||
}
|
||
})
|
||
},
|
||
buildListParams() {
|
||
const p = {
|
||
current: this.listPage.current,
|
||
size: this.listPage.size
|
||
}
|
||
const q = this.listQuery
|
||
if (q.audioManagementId) p.audioManagementId = q.audioManagementId
|
||
if (q.audioManagementSegmentsId) p.audioManagementSegmentsId = q.audioManagementSegmentsId
|
||
if (q.redLineType) p.redLineType = q.redLineType
|
||
if (q.redLineSubtype) p.redLineSubtype = q.redLineSubtype
|
||
if (q.salesId) p.salesId = q.salesId
|
||
if (q.reasonKeyword && q.reasonKeyword.trim()) p.reasonKeyword = q.reasonKeyword.trim()
|
||
if (q.createStartTime) p.createStartTime = q.createStartTime
|
||
if (q.createEndTime) p.createEndTime = q.createEndTime
|
||
return p
|
||
},
|
||
async fetchListPage(append) {
|
||
if (this.listLoading) return
|
||
this.listLoading = true
|
||
this.listError = ''
|
||
try {
|
||
const res = await get('/api/redLineRecord/list', this.buildListParams())
|
||
if (res.statusCode !== 200 || !res.data) {
|
||
throw new Error(res.data?.message || `请求失败(${res.statusCode})`)
|
||
}
|
||
const body = res.data
|
||
if (!body.success) {
|
||
throw new Error(body.message || '查询失败')
|
||
}
|
||
const records = Array.isArray(body.data) ? body.data : []
|
||
const total = Number(body.total) || 0
|
||
const pages = Number(body.pages) || 0
|
||
const current = Number(body.current) || this.listPage.current
|
||
|
||
this.listTotal = total
|
||
this.listPages = pages
|
||
this.listPage.current = current
|
||
|
||
if (append) {
|
||
this.dataList = this.dataList.concat(records)
|
||
} else {
|
||
this.dataList = records
|
||
}
|
||
|
||
const loaded = this.dataList.length
|
||
this.listHasMore = pages > 0 ? current < pages : loaded < total
|
||
} catch (e) {
|
||
console.error('[over_commitment] list error', e)
|
||
this.listError = e.message || '加载失败'
|
||
if (!append) {
|
||
this.dataList = []
|
||
this.listTotal = 0
|
||
}
|
||
uni.showToast({ title: this.listError, icon: 'none' })
|
||
} finally {
|
||
this.listLoading = false
|
||
}
|
||
},
|
||
refreshList() {
|
||
this.listPage.current = 1
|
||
this.listHasMore = true
|
||
return this.fetchListPage(false)
|
||
},
|
||
onSearch() {
|
||
this.refreshList()
|
||
},
|
||
onRefresh() {
|
||
this.refreshList()
|
||
},
|
||
onScrollToLower() {
|
||
if (this.listLoading || !this.listHasMore) return
|
||
const next = this.listPage.current + 1
|
||
if (this.listPages > 0 && next > this.listPages) return
|
||
this.listPage.current = next
|
||
this.fetchListPage(true)
|
||
},
|
||
displaySalesName(item) {
|
||
const n = item.salesName || item.sales_name
|
||
if (n) return n
|
||
const id = item.salesId || item.sales_id
|
||
return id ? `销售ID ${id}` : '未知销售'
|
||
},
|
||
displaySalesPhone(item) {
|
||
return item.salesPhone || item.sales_phone || ''
|
||
},
|
||
formatDateTime(val) {
|
||
if (val == null || val === '') return ''
|
||
if (Array.isArray(val) && val.length >= 6) {
|
||
const y = val[0]
|
||
const m = String(val[1]).padStart(2, '0')
|
||
const d = String(val[2]).padStart(2, '0')
|
||
const h = String(val[3] ?? 0).padStart(2, '0')
|
||
const min = String(val[4] ?? 0).padStart(2, '0')
|
||
const s = String(val[5] ?? 0).padStart(2, '0')
|
||
return `${y}-${m}-${d} ${h}:${min}:${s}`
|
||
}
|
||
if (typeof val === 'string') {
|
||
return val.replace('T', ' ').slice(0, 19)
|
||
}
|
||
try {
|
||
const d = new Date(val)
|
||
if (Number.isNaN(d.getTime())) return String(val)
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const day = String(d.getDate()).padStart(2, '0')
|
||
const h = String(d.getHours()).padStart(2, '0')
|
||
const min = String(d.getMinutes()).padStart(2, '0')
|
||
const s = String(d.getSeconds()).padStart(2, '0')
|
||
return `${y}-${m}-${day} ${h}:${min}:${s}`
|
||
} catch {
|
||
return String(val)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.feature-page {
|
||
min-height: 100vh;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.page-header {
|
||
background: #ffffff;
|
||
border-bottom: 1rpx solid #e9ecef;
|
||
}
|
||
|
||
.nav-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 88rpx;
|
||
padding: 0 30rpx;
|
||
}
|
||
|
||
.nav-left {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 36rpx;
|
||
color: #333333;
|
||
}
|
||
|
||
.nav-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.content-scroll {
|
||
height: calc(100vh - 88rpx);
|
||
}
|
||
|
||
.stats-card {
|
||
display: flex;
|
||
margin: 20rpx 30rpx;
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 40rpx 30rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.stat-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-item:not(:last-child) {
|
||
border-right: 1rpx solid #e9ecef;
|
||
}
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #ff6b6b;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.chart-section,
|
||
.data-list {
|
||
margin: 20rpx 30rpx;
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.search-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 0 30rpx 20rpx;
|
||
padding: 16rpx 20rpx;
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 26rpx;
|
||
background: #f5f6f8;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.toolbar-actions {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
padding: 12rpx 24rpx;
|
||
font-size: 26rpx;
|
||
color: #2a68ff;
|
||
background: #eef3ff;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.toolbar-btn--ghost {
|
||
color: #666666;
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.section-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx;
|
||
border-bottom: 1rpx solid #e9ecef;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.section-meta {
|
||
font-size: 24rpx;
|
||
color: #888888;
|
||
}
|
||
|
||
.chart-placeholder {
|
||
padding: 80rpx 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.placeholder-text {
|
||
display: block;
|
||
font-size: 48rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.placeholder-desc {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.list-item {
|
||
padding: 28rpx 30rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.list-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.item-main {
|
||
width: 100%;
|
||
}
|
||
|
||
.item-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
color: #555555;
|
||
}
|
||
|
||
.item-row--title {
|
||
margin-top: 0;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.item-name {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
flex: 1;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.item-time {
|
||
font-size: 22rpx;
|
||
color: #999999;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.item-label {
|
||
width: 120rpx;
|
||
flex-shrink: 0;
|
||
color: #888888;
|
||
}
|
||
|
||
.item-value {
|
||
flex: 1;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.item-value--muted {
|
||
color: #777777;
|
||
}
|
||
|
||
.item-reason {
|
||
margin-top: 16rpx;
|
||
padding: 16rpx;
|
||
background: #f8f9fb;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.item-reason-label {
|
||
display: block;
|
||
font-size: 22rpx;
|
||
color: #888888;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.item-reason-text {
|
||
font-size: 26rpx;
|
||
color: #333333;
|
||
line-height: 1.5;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.list-hint {
|
||
padding: 48rpx 30rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.list-hint--error {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.list-footer {
|
||
padding: 24rpx;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #aaaaaa;
|
||
}
|
||
</style>
|