Files
smartDriveEEUniApp/pages-subpackage/ai_features/over_commitment/over_commitment.vue
2026-04-21 09:06:46 +08:00

540 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>