和后台联调度承诺页面。
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user