和后台联调度承诺页面。

This commit is contained in:
zhonghua.li
2026-04-21 09:06:46 +08:00
parent 303315e840
commit 810dd2036b

View File

@@ -9,14 +9,19 @@
</view>
</view>
<scroll-view class="content-scroll" scroll-y="true">
<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">{{ overCommitmentRate }}%</text>
<text class="stat-value">{{ overCommitmentRateDisplay }}</text>
<text class="stat-label">过度承诺率</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ totalIncidents }}</text>
<text class="stat-value">{{ listTotal }}</text>
<text class="stat-label">违规事件数</text>
</view>
</view>
@@ -29,40 +34,249 @@
</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">
<text class="section-title">违规记录</text>
<view class="list-item" v-for="(item, index) in dataList" :key="index">
<view class="item-left">
<text class="item-date">{{ item.employee }}</text>
<text class="item-desc">{{ item.description }}</text>
<view class="section-title-row">
<text class="section-title">触及红线记录</text>
<text class="section-meta"> {{ listTotal }} </text>
</view>
<view class="item-right">
<text class="item-count">{{ item.count }} </text>
<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 {
overCommitmentRate: '3.2',
totalIncidents: '42',
dataList: [
{ employee: '张三', description: '承诺无法兑现的服务', count: '8' },
{ employee: '李四', description: '超出权限的优惠承诺', count: '6' },
{ employee: '王五', description: '虚假的产品性能承诺', count: '5' },
{ employee: '赵六', description: '不切实际的交货时间承诺', count: '4' },
{ employee: '孙七', description: '其他违规承诺', count: '19' }
]
/** 趋势/占比等需单独统计接口;列表页先占位 */
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)
}
}
}
}
@@ -144,7 +358,8 @@ export default {
color: #666666;
}
.chart-section, .data-list {
.chart-section,
.data-list {
margin: 20rpx 30rpx;
background: #ffffff;
border-radius: 16rpx;
@@ -152,13 +367,62 @@ export default {
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 {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333333;
padding: 30rpx;
border-bottom: 1rpx solid #e9ecef;
}
.section-meta {
font-size: 24rpx;
color: #888888;
}
.chart-placeholder {
@@ -179,10 +443,7 @@ export default {
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
@@ -190,32 +451,89 @@ export default {
border-bottom: none;
}
.item-left {
flex: 1;
.item-main {
width: 100%;
}
.item-date {
display: block;
.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-desc {
display: block;
font-size: 24rpx;
color: #666666;
.item-reason-text {
font-size: 26rpx;
color: #333333;
line-height: 1.5;
word-break: break-all;
}
.item-right {
text-align: right;
}
.item-count {
display: block;
.list-hint {
padding: 48rpx 30rpx;
text-align: center;
font-size: 28rpx;
font-weight: bold;
color: #999999;
}
.list-hint--error {
color: #ff6b6b;
}
.list-footer {
padding: 24rpx;
text-align: center;
font-size: 24rpx;
color: #aaaaaa;
}
</style>