- Fix ExternalIntegral order list (no double restPage); default 普通订单; UI columns for useIntegral and buyer uid/nickname/phone; enrich StoreOrderDetailResponse and admin query select. - External user list: UserResponse.selfBonus and fillWaSelfBonus from wa_users.id=uid. - Integral log: AdminIntegralSearchRequest nickName/phone; findAdminList filters and ordering; integralExternal API sends page/limit as query params. - Integral detail page: linkType Chinese mapping including selfbonus; update docs/newpage.md. - Dashboard grid menu entries for integral-external routes. Made-with: Cursor
369 lines
11 KiB
Vue
369 lines
11 KiB
Vue
<template>
|
||
<div class="divBox">
|
||
<!-- 返回按钮 -->
|
||
<div class="back-bar">
|
||
<el-button size="small" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
|
||
</div>
|
||
|
||
<!-- 用户概览卡片(从用户列表进入时展示;无 uid 时为全部明细模式) -->
|
||
<el-card v-if="uid" class="box-card overview-card">
|
||
<div class="overview-header">
|
||
<span class="user-title">
|
||
{{ userInfo.nickname || ('UID: ' + uid) }}
|
||
<span class="uid-badge">UID: {{ uid }}</span>
|
||
</span>
|
||
</div>
|
||
<el-row :gutter="20" class="stats-row">
|
||
<el-col :xs="12" :sm="8" :md="6">
|
||
<div class="stat-item">
|
||
<div class="stat-label">积分</div>
|
||
<div class="stat-value integral-color">{{ userInfo.integral != null ? userInfo.integral : '-' }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="6">
|
||
<div class="stat-item">
|
||
<div class="stat-label">个人奖金</div>
|
||
<div class="stat-value bonus-color">
|
||
{{ userInfo.selfBonus != null ? ('¥' + userInfo.selfBonus) : '-' }}
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</el-card>
|
||
<el-card v-else class="box-card overview-card overview-card--all">
|
||
<div class="overview-header">
|
||
<span class="user-title">全部积分明细</span>
|
||
<span class="hint-text">未指定用户时将展示全部记录,支持下方条件筛选</span>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 积分明细列表 -->
|
||
<el-card class="box-card mt10">
|
||
<div slot="header" class="clearfix">
|
||
<span>积分明细</span>
|
||
</div>
|
||
<div class="container mb10">
|
||
<el-form inline size="small" :model="searchForm" label-width="96px">
|
||
<el-row>
|
||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||
<el-form-item label="用户ID:">
|
||
<el-input
|
||
v-model="searchForm.uidStr"
|
||
placeholder="可选,留空查全部"
|
||
clearable
|
||
class="filter-input"
|
||
@keyup.enter.native="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||
<el-form-item label="用户名称:">
|
||
<el-input
|
||
v-model="searchForm.nickName"
|
||
placeholder="昵称模糊匹配"
|
||
clearable
|
||
class="filter-input"
|
||
@keyup.enter.native="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||
<el-form-item label="手机号:">
|
||
<el-input
|
||
v-model="searchForm.phone"
|
||
placeholder="手机号模糊匹配"
|
||
clearable
|
||
class="filter-input"
|
||
@keyup.enter.native="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||
<el-form-item label="时间选择:">
|
||
<el-date-picker
|
||
v-model="timeVal"
|
||
type="daterange"
|
||
align="right"
|
||
unlink-panels
|
||
value-format="yyyy-MM-dd"
|
||
format="yyyy-MM-dd"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
@change="onchangeTime"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||
<el-button @click="handleReset">重置</el-button>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="listLoading"
|
||
:data="tableData.data"
|
||
style="width: 100%"
|
||
size="mini"
|
||
highlight-current-row
|
||
:header-cell-style="{ fontWeight: 'bold' }"
|
||
>
|
||
<el-table-column prop="id" label="ID" min-width="80" />
|
||
<el-table-column prop="uid" label="用户ID" min-width="80" />
|
||
<el-table-column prop="nickName" label="用户昵称" min-width="120" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.nickName || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="积分变动" min-width="100">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.type === 1 ? 'success' : 'danger'" size="small">
|
||
{{ scope.row.type === 1 ? '+' : '-' }}{{ scope.row.integral }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="balance" label="剩余积分" min-width="100" />
|
||
<el-table-column label="类型" min-width="80">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.type === 1 ? 'success' : 'danger'" size="small">
|
||
{{ scope.row.type === 1 ? '增加' : '扣减' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="关联类型" min-width="100">
|
||
<template slot-scope="scope">
|
||
<el-tag size="small" effect="plain">{{ linkTypeFilter(scope.row.linkType) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="100">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="statusTypeFilter(scope.row.status)" size="small">
|
||
{{ statusFilter(scope.row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="mark" label="备注" min-width="150" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.mark || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createTime" label="创建时间" min-width="150" />
|
||
</el-table>
|
||
|
||
<div class="block mt20">
|
||
<el-pagination
|
||
:page-sizes="[15, 30, 45, 60]"
|
||
:page-size="searchForm.limit"
|
||
:current-page="searchForm.page"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="tableData.total"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getExternalIntegralLog } from '@/api/integralExternal';
|
||
|
||
export default {
|
||
name: 'IntegralExternalUserDetail',
|
||
data() {
|
||
return {
|
||
uid: null,
|
||
userInfo: {
|
||
nickname: '',
|
||
integral: null,
|
||
selfBonus: null,
|
||
},
|
||
listLoading: false,
|
||
tableData: {
|
||
data: [],
|
||
total: 0,
|
||
},
|
||
searchForm: {
|
||
uidStr: '',
|
||
nickName: '',
|
||
phone: '',
|
||
dateLimit: '',
|
||
page: 1,
|
||
limit: 15,
|
||
},
|
||
timeVal: [],
|
||
};
|
||
},
|
||
created() {
|
||
const { uid, nickname, integral, selfBonus } = this.$route.query;
|
||
this.uid = uid ? Number(uid) : null;
|
||
this.userInfo.nickname = nickname || '';
|
||
this.userInfo.integral = integral !== '' && integral != null ? Number(integral) : null;
|
||
this.userInfo.selfBonus = selfBonus !== '' && selfBonus != null ? Number(selfBonus) : null;
|
||
if (this.uid) {
|
||
this.searchForm.uidStr = String(this.uid);
|
||
}
|
||
},
|
||
mounted() {
|
||
this.getList();
|
||
},
|
||
methods: {
|
||
getList() {
|
||
this.listLoading = true;
|
||
const uidParsed = this.searchForm.uidStr === '' || this.searchForm.uidStr == null
|
||
? null
|
||
: parseInt(String(this.searchForm.uidStr).trim(), 10);
|
||
const uid = Number.isNaN(uidParsed) ? null : uidParsed;
|
||
const params = {
|
||
page: this.searchForm.page,
|
||
limit: this.searchForm.limit,
|
||
uid,
|
||
nickName: this.searchForm.nickName ? this.searchForm.nickName.trim() : undefined,
|
||
phone: this.searchForm.phone ? this.searchForm.phone.trim() : undefined,
|
||
dateLimit: this.searchForm.dateLimit || undefined,
|
||
};
|
||
Object.keys(params).forEach((k) => {
|
||
if (params[k] === undefined || params[k] === null || params[k] === '') {
|
||
delete params[k];
|
||
}
|
||
});
|
||
|
||
getExternalIntegralLog(params)
|
||
.then((res) => {
|
||
this.tableData.data = res.list || [];
|
||
this.tableData.total = res.total || 0;
|
||
})
|
||
.catch(() => {})
|
||
.finally(() => {
|
||
this.listLoading = false;
|
||
});
|
||
},
|
||
handleSearch() {
|
||
this.searchForm.page = 1;
|
||
this.getList();
|
||
},
|
||
handleReset() {
|
||
this.searchForm.uidStr = '';
|
||
this.searchForm.nickName = '';
|
||
this.searchForm.phone = '';
|
||
this.searchForm.dateLimit = '';
|
||
this.searchForm.page = 1;
|
||
this.timeVal = [];
|
||
this.uid = null;
|
||
this.userInfo = { nickname: '', integral: null, selfBonus: null };
|
||
this.getList();
|
||
},
|
||
onchangeTime(e) {
|
||
this.timeVal = e;
|
||
this.searchForm.dateLimit = e ? e.join(',') : '';
|
||
this.handleSearch();
|
||
},
|
||
handleSizeChange(val) {
|
||
this.searchForm.limit = val;
|
||
this.getList();
|
||
},
|
||
handleCurrentChange(val) {
|
||
this.searchForm.page = val;
|
||
this.getList();
|
||
},
|
||
goBack() {
|
||
this.$router.push('/integral-external/user');
|
||
},
|
||
linkTypeFilter(type) {
|
||
if (type == null || type === '') return '-';
|
||
const raw = String(type).trim();
|
||
if (!raw) return '-';
|
||
const key = raw.toLowerCase();
|
||
const typeMap = {
|
||
order: '订单',
|
||
sign: '签到',
|
||
system: '系统',
|
||
selfbonus: '个人奖金',
|
||
};
|
||
return typeMap[key] || `其他(${raw})`;
|
||
},
|
||
statusFilter(status) {
|
||
const statusMap = { 1: '订单创建', 2: '冻结期', 3: '完成', 4: '失效' };
|
||
return statusMap[status] || '未知';
|
||
},
|
||
statusTypeFilter(status) {
|
||
const typeMap = { 1: 'info', 2: 'warning', 3: 'success', 4: 'danger' };
|
||
return typeMap[status] || 'info';
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.back-bar {
|
||
margin-bottom: 12px;
|
||
}
|
||
.mt10 {
|
||
margin-top: 10px;
|
||
}
|
||
.mt20 {
|
||
margin-top: 20px;
|
||
}
|
||
.mb10 {
|
||
margin-bottom: 10px;
|
||
}
|
||
.overview-card {
|
||
.overview-header {
|
||
margin-bottom: 16px;
|
||
.user-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #303133;
|
||
.uid-badge {
|
||
margin-left: 10px;
|
||
font-size: 12px;
|
||
font-weight: normal;
|
||
color: #909399;
|
||
background: #f4f4f5;
|
||
border-radius: 4px;
|
||
padding: 2px 8px;
|
||
}
|
||
}
|
||
}
|
||
.stats-row {
|
||
.stat-item {
|
||
text-align: center;
|
||
padding: 12px 0;
|
||
.stat-label {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
margin-bottom: 6px;
|
||
}
|
||
.stat-value {
|
||
font-size: 22px;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.integral-color {
|
||
color: #e6a23c;
|
||
}
|
||
.bonus-color {
|
||
color: #67c23a;
|
||
}
|
||
.block {
|
||
text-align: right;
|
||
}
|
||
.filter-input {
|
||
width: 180px;
|
||
}
|
||
.overview-card--all .hint-text {
|
||
display: block;
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
font-weight: normal;
|
||
}
|
||
</style>
|