Merge branch 'feature/marketing-integral-log' into czc231

Restore backend-adminend/src/api/integral.js (integralListApi) after modify/delete conflict with czc231.

Made-with: Cursor
This commit is contained in:
apple
2026-04-09 16:01:43 +08:00
41 changed files with 7190 additions and 182 deletions

View File

@@ -0,0 +1,23 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2025 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import request from '@/utils/request';
/**
* 积分记录分页列表
* @param data
*/
export function integralListApi(data) {
return request({
url: '/admin/user/integral/list',
method: 'post',
data,
});
}

View File

@@ -0,0 +1,41 @@
/**
* 积分外部页面 API免认证
* 使用 requestNoAuth 实例,不注入 token不拦截 401。
* 对应后端ExternalIntegralController → api/external/integral/*
*/
import requestNoAuth from '@/utils/requestNoAuth';
/**
* 积分订单列表
*/
export function getExternalOrderList(params) {
return requestNoAuth({
url: 'external/integral/order/list',
method: 'get',
params,
});
}
/**
* 用户积分列表(含 eb_user 积分字段)
*/
export function getExternalUserList(params) {
return requestNoAuth({
url: 'external/integral/user/list',
method: 'get',
params,
});
}
/**
* 用户积分明细分页列表page/limit 走 query与 /admin/user/integral/list 一致)
*/
export function getExternalIntegralLog(data) {
const { page, limit, ...body } = data;
return requestNoAuth({
url: 'external/integral/log/list',
method: 'post',
params: { page, limit },
data: body,
});
}

View File

@@ -48,3 +48,17 @@ export function filterIsPromoter(status) {
};
return statusMap[status];
}
/**
* 手机号脱敏(中间 4 位替换为 ****
* 适用于外部免登录页面展示,防止敏感信息泄露。
* 例13812345678 → 138****5678
* @param {string|number} phone
* @return {string}
*/
export function phoneDesensitize(phone) {
if (!phone) return '-';
const str = String(phone);
if (str.length < 7) return str; // 过短则不处理
return str.replace(/(\d{3})\d{4}(\d+)/, '$1****$2');
}

View File

@@ -0,0 +1,18 @@
<template>
<div class="integral-external-layout">
<router-view />
</div>
</template>
<script>
export default {
name: 'EmptyLayout',
};
</script>
<style scoped>
.integral-external-layout {
min-height: 100vh;
background: #f0f2f5;
}
</style>

View File

@@ -18,7 +18,9 @@ import getPageTitle from '@/utils/get-page-title';
NProgress.configure({ showSpinner: false }); // NProgress Configuration
const whiteList = ['/login', '/auth-redirect']; // no redirect whitelist
// no redirect whitelist — exact match for /login, prefix match for /integral-external
const whiteList = ['/login', '/auth-redirect'];
const whiteListPrefixes = ['/integral-external'];
router.beforeEach(async (to, from, next) => {
// start progress bar
@@ -56,7 +58,7 @@ router.beforeEach(async (to, from, next) => {
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
if (whiteList.indexOf(to.path) !== -1 || whiteListPrefixes.some(prefix => to.path.startsWith(prefix))) {
// in the free login whitelist, go directly
next();
} else {

View File

@@ -32,6 +32,7 @@ import maintainRouter from './modules/maintain';
import mobileRouter from './modules/mobile';
import statistic from './modules/statistic';
import designRouter from './modules/design';
import integralExternalRouter from './modules/integralExternal';
/**
* Note: sub-menu only appear when route children.length >= 1
@@ -90,6 +91,8 @@ export const constantRoutes = [
statistic,
//装修
designRouter,
// 积分外部页面(免认证)
integralExternalRouter,
{
path: '/404',
component: () => import('@/views/error-page/404'),

View File

@@ -0,0 +1,30 @@
const EmptyLayout = () => import('@/layout/EmptyLayout');
const integralExternalRouter = {
path: '/integral-external',
component: EmptyLayout,
redirect: '/integral-external/order',
hidden: true,
children: [
{
path: 'order',
component: () => import('@/views/integral-external/order/index'),
name: 'IntegralExternalOrder',
meta: { title: '积分订单' },
},
{
path: 'user',
component: () => import('@/views/integral-external/user/index'),
name: 'IntegralExternalUser',
meta: { title: '用户积分' },
},
{
path: 'user/integral-detail',
component: () => import('@/views/integral-external/user-integral-detail/index'),
name: 'IntegralExternalUserDetail',
meta: { title: '用户积分明细' },
},
],
};
export default integralExternalRouter;

View File

@@ -44,6 +44,12 @@ const userRouter = {
name: 'Group',
meta: { title: '用户分组', icon: '' },
},
{
path: 'integral',
component: () => import('@/views/user/integral/index'),
name: 'IntegralLog',
meta: { title: '积分日志', icon: '', noCache: true },
},
],
};

View File

@@ -0,0 +1,51 @@
/**
* 免认证 Axios 实例
* 供积分外部页面(/integral-external/*)使用。
* 不注入 Authori-zation token不拦截 401 自动跳转登录页。
*/
import axios from 'axios';
import { Message } from 'element-ui';
import SettingMer from '@/utils/settingMer';
const service = axios.create({
baseURL: SettingMer.apiBaseURL,
timeout: 60000,
});
// 请求拦截器 — 不注入 token
service.interceptors.request.use(
(config) => {
// GET 请求防缓存
if (/get/i.test(config.method)) {
config.params = config.params || {};
config.params.temp = Date.parse(new Date()) / 1000;
}
return config;
},
(error) => Promise.reject(error),
);
// 响应拦截器 — 不拦截 401 跳转
service.interceptors.response.use(
(response) => {
const res = response.data;
if (res.code !== 0 && res.code !== 200) {
Message({
message: res.msg || res.message || '请求失败',
type: 'error',
duration: 5 * 1000,
});
return Promise.reject(new Error(res.msg || '请求失败'));
}
return res.data;
},
(error) => {
const msg = error.response
? `网络请求失败 (${error.response.status})`
: '网络连接失败,请检查服务器是否启动';
Message({ message: msg, type: 'error', duration: 5 * 1000 });
return Promise.reject(error);
},
);
export default service;

View File

@@ -79,13 +79,6 @@ export default {
url: '/order/index',
perms: ['admin:order:list'],
},
{
bgColor: '#A277FF',
icon: 'iconduanxinpeizhi',
title: '一号通',
url: '/operation/onePass',
perms: ['admin:pass:login'],
},
{
bgColor: '#E8B600',
icon: 'iconwenzhangguanli',
@@ -107,6 +100,27 @@ export default {
url: '/marketing/coupon/list',
perms: ['admin:coupon:list'],
},
{
bgColor: '#13c2c2',
icon: 'icondingdanguanli',
title: '积分订单',
url: '/integral-external/order',
alwaysShow: true,
},
{
bgColor: '#722ed1',
icon: 'iconhuiyuanguanli',
title: '用户积分',
url: '/integral-external/user',
alwaysShow: true,
},
{
bgColor: '#eb2f96',
icon: 'iconfenxiaoguanli',
title: '用户积分明细',
url: '/integral-external/user/integral-detail',
alwaysShow: true,
},
],
statisticData: [
{ title: '待发货订单', num: 0, path: '/order/index', perms: ['admin:order:list'] },
@@ -128,7 +142,7 @@ export default {
permList: function () {
let arr = [];
this.nav_list.forEach((item) => {
if (this.checkPermi(item.perms)) {
if (item.alwaysShow || this.checkPermi(item.perms)) {
arr.push(item);
}
});

View File

@@ -0,0 +1,253 @@
<template>
<div class="divBox relative">
<el-card class="box-card">
<div class="clearfix">
<div class="container">
<el-form size="small" label-width="100px">
<el-form-item label="订单状态:">
<el-radio-group v-model="tableFrom.status" type="button" @change="seachList">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="unPaid">未支付</el-radio-button>
<el-radio-button label="notShipped">未发货</el-radio-button>
<el-radio-button label="spike">待收货</el-radio-button>
<el-radio-button label="bargain">待评价</el-radio-button>
<el-radio-button label="complete">交易完成</el-radio-button>
<el-radio-button label="refunding">退款中</el-radio-button>
<el-radio-button label="refunded">已退款</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="时间选择:" class="width100">
<el-radio-group
v-model="tableFrom.dateLimit"
type="button"
class="mr20"
size="small"
@change="selectChange(tableFrom.dateLimit)"
>
<el-radio-button v-for="(item, i) in fromList.fromTxt" :key="i" :label="item.val">
{{ item.text }}
</el-radio-button>
</el-radio-group>
<el-date-picker
v-model="timeVal"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
size="small"
type="daterange"
placement="bottom-end"
placeholder="自定义时间"
style="width: 220px"
@change="onchangeTime"
/>
</el-form-item>
<el-form-item label="订单号:" class="width100">
<el-input
v-model="tableFrom.orderNo"
placeholder="请输入订单号"
class="selWidth"
size="small"
clearable
>
<el-button slot="append" icon="el-icon-search" size="small" @click="seachList" />
</el-input>
</el-form-item>
</el-form>
</div>
</div>
</el-card>
<el-card class="box-card mt10">
<el-table
v-loading="listLoading"
:data="tableData.data"
size="mini"
class="table"
highlight-current-row
:header-cell-style="{ fontWeight: 'bold' }"
>
<el-table-column label="订单号" min-width="210">
<template slot-scope="scope">
<span style="display: block">{{ scope.row.orderId }}</span>
<span v-if="scope.row.isDel" style="color: #ed4014; display: block">用户已删除</span>
</template>
</el-table-column>
<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 label="用户手机号" min-width="120">
<template slot-scope="scope">
<span>{{ scope.row.userPhone | phoneDesensitize }}</span>
</template>
</el-table-column>
<el-table-column prop="realName" label="收货人" min-width="100" />
<el-table-column label="商品信息" min-width="280">
<template slot-scope="scope">
<div v-if="scope.row.productList && scope.row.productList.length">
<div
v-for="(val, i) in scope.row.productList"
:key="i"
class="tabBox acea-row row-middle"
style="flex-wrap: inherit; margin-bottom: 4px"
>
<div class="demo-image__preview mr10" style="width: 40px; height: 40px; flex-shrink: 0">
<el-image :src="val.info.image" :preview-src-list="[val.info.image]" style="width: 40px; height: 40px" />
</div>
<div class="text_overflow">
<span class="tabBox_tit mr10">{{ val.info.productName }}</span>
<span class="tabBox_pice">{{ val.info.price }} × {{ val.info.payNum }}</span>
</div>
</div>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="使用积分" min-width="90">
<template slot-scope="scope">
<span>{{ scope.row.useIntegral != null ? scope.row.useIntegral : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="payPrice" label="实际支付" min-width="90">
<template slot-scope="scope">
<span>{{ scope.row.payPrice }}</span>
</template>
</el-table-column>
<el-table-column label="支付方式" min-width="90">
<template slot-scope="scope">
<span>{{ scope.row.payTypeStr || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="订单状态" min-width="100">
<template slot-scope="scope">
<span :class="scope.row.refundStatus === 1 || scope.row.refundStatus === 2 ? 'refund-tag' : ''">
{{ scope.row.statusStr ? scope.row.statusStr.value : '-' }}
</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="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getExternalOrderList } from '@/api/integralExternal';
export default {
name: 'IntegralExternalOrder',
data() {
return {
listLoading: false,
tableData: {
data: [],
total: 0,
},
tableFrom: {
type: 0,
status: 'all',
dateLimit: '',
orderNo: '',
page: 1,
limit: 15,
},
timeVal: [],
fromList: {
fromTxt: [
{ text: '全部', val: '' },
{ text: '今天', val: 'today' },
{ text: '昨天', val: 'yesterday' },
{ text: '最近7天', val: 'lately7' },
{ text: '最近30天', val: 'lately30' },
{ text: '本月', val: 'month' },
{ text: '本年', val: 'year' },
],
},
};
},
mounted() {
this.getList();
},
methods: {
getList() {
this.listLoading = true;
const params = { ...this.tableFrom };
if (!params.dateLimit) delete params.dateLimit;
if (!params.orderNo) delete params.orderNo;
getExternalOrderList(params)
.then((res) => {
this.tableData.data = res.list || [];
this.tableData.total = res.total || 0;
})
.catch(() => {})
.finally(() => {
this.listLoading = false;
});
},
seachList() {
this.tableFrom.page = 1;
this.getList();
},
selectChange(val) {
if (val) this.timeVal = [];
this.tableFrom.dateLimit = val;
this.seachList();
},
onchangeTime(e) {
this.timeVal = e;
this.tableFrom.dateLimit = e ? e.join(',') : '';
this.seachList();
},
handleSizeChange(val) {
this.tableFrom.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.tableFrom.page = val;
this.getList();
},
},
};
</script>
<style scoped lang="scss">
.mt10 {
margin-top: 10px;
}
.mt20 {
margin-top: 20px;
}
.refund-tag {
color: #f124c7;
font-weight: bold;
}
.tabBox {
display: flex;
align-items: center;
}
.tabBox_tit {
font-size: 12px;
color: #333;
}
.tabBox_pice {
font-size: 12px;
color: #999;
}
.block {
text-align: right;
}
</style>

View File

@@ -0,0 +1,368 @@
<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>

View File

@@ -0,0 +1,206 @@
<template>
<div class="divBox relative">
<el-card class="box-card">
<div class="clearfix">
<div class="container">
<el-form inline size="small" :model="userFrom" label-width="90px">
<el-row>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="用户搜索:">
<el-input
v-model="userFrom.keywords"
placeholder="请输入昵称或手机号"
clearable
class="selWidth"
@keyup.enter.native="seachList"
/>
</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"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
size="small"
type="daterange"
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" size="small" @click="seachList">搜索</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
<el-table
v-loading="listLoading"
:data="tableData.data"
size="mini"
highlight-current-row
:header-cell-style="{ fontWeight: 'bold' }"
>
<el-table-column label="用户信息" min-width="200">
<template slot-scope="scope">
<div class="user-info-cell">
<el-avatar :size="36" :src="scope.row.avatar" icon="el-icon-user" class="mr10" />
<div>
<div>{{ scope.row.nickname || '-' }}</div>
<div class="uid-text">UID: {{ scope.row.uid }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="手机号" min-width="130">
<template slot-scope="scope">
<span>{{ scope.row.phone | phoneDesensitize }}</span>
</template>
</el-table-column>
<el-table-column label="积分" min-width="100">
<template slot-scope="scope">
<span class="integral-val">{{ scope.row.integral != null ? scope.row.integral : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="个人奖金" min-width="110">
<template slot-scope="scope">
<span>{{ scope.row.selfBonus != null ? ('¥' + scope.row.selfBonus) : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="注册时间" min-width="150" />
<el-table-column label="操作" min-width="120" fixed="right" align="center">
<template slot-scope="scope">
<el-button type="text" size="small" @click="viewIntegralDetail(scope.row)">
查看积分明细
</el-button>
</template>
</el-table-column>
</el-table>
<div class="block mt20">
<el-pagination
:page-sizes="[15, 30, 45, 60]"
:page-size="userFrom.limit"
:current-page="userFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getExternalUserList } from '@/api/integralExternal';
export default {
name: 'IntegralExternalUser',
data() {
return {
listLoading: false,
tableData: {
data: [],
total: 0,
},
userFrom: {
keywords: '',
dateLimit: '',
page: 1,
limit: 15,
},
timeVal: [],
};
},
mounted() {
this.getList();
},
methods: {
getList() {
this.listLoading = true;
const params = { ...this.userFrom };
if (!params.keywords) delete params.keywords;
if (!params.dateLimit) delete params.dateLimit;
getExternalUserList(params)
.then((res) => {
this.tableData.data = res.list || [];
this.tableData.total = res.total || 0;
})
.catch(() => {})
.finally(() => {
this.listLoading = false;
});
},
seachList() {
this.userFrom.page = 1;
this.getList();
},
handleReset() {
this.userFrom = { keywords: '', dateLimit: '', page: 1, limit: 15 };
this.timeVal = [];
this.getList();
},
onchangeTime(e) {
this.timeVal = e;
this.userFrom.dateLimit = e ? e.join(',') : '';
this.seachList();
},
handleSizeChange(val) {
this.userFrom.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.userFrom.page = val;
this.getList();
},
viewIntegralDetail(row) {
this.$router.push({
path: '/integral-external/user/integral-detail',
query: {
uid: row.uid,
nickname: row.nickname || '',
integral: row.integral != null ? row.integral : '',
selfBonus: row.selfBonus != null ? row.selfBonus : '',
},
});
},
},
};
</script>
<style scoped lang="scss">
.user-info-cell {
display: flex;
align-items: center;
}
.mr10 {
margin-right: 10px;
}
.uid-text {
font-size: 11px;
color: #999;
}
.integral-val {
font-weight: bold;
color: #e6a23c;
}
.mt20 {
margin-top: 20px;
}
.block {
text-align: right;
}
.selWidth {
width: 200px;
}
</style>

View File

@@ -3,85 +3,102 @@
<el-card class="box-card">
<div slot="header" class="clearfix">
<div class="container">
<el-form size="small" label-width="120px">
<el-form-item label="时间选择:" class="width100">
<el-radio-group
v-model="tableFrom.dateLimit"
type="button"
class="mr20"
size="small"
@change="selectChange(tableFrom.dateLimit)"
>
<el-radio-button v-for="(item, i) in fromList.fromTxt" :key="i" :label="item.val"
>{{ item.text }}
</el-radio-button>
</el-radio-group>
<el-date-picker
v-model="timeVal"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
size="small"
type="daterange"
placement="bottom-end"
placeholder="自定义时间"
style="width: 250px"
@change="onchangeTime"
/>
</el-form-item>
<el-form-item label="用户微信昵称:">
<el-input v-model="tableFrom.keywords" placeholder="请输入用户昵称" class="selWidth" size="small">
<el-button slot="append" icon="el-icon-search" size="small" @click="getList(1)" />
</el-input>
</el-form-item>
<el-form inline size="small" :model="searchForm" ref="searchForm" label-width="80px">
<el-row>
<el-col :xs="24" :sm="24" :md="18" :lg="18" :xl="18">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="用户搜索:">
<el-input v-model="searchForm.keywords" placeholder="请输入用户昵称" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="用户ID">
<el-input-number v-model="searchForm.uid" placeholder="请输入用户ID" :min="1" :controls="false" style="width: 100%" />
</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="结束日期"
:picker-options="pickerOptions"
@change="onchangeTime"
/>
</el-form-item>
</el-col>
</el-col>
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6" class="text-right">
<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>
<!--<cards-data :cardLists="cardLists"></cards-data>-->
</div>
<el-table
v-loading="listLoading"
:data="tableData.data"
size="small"
class="table"
style="width: 100%"
size="mini"
highlight-current-row
:header-cell-style="{ fontWeight: 'bold' }"
>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="title" label="标题" min-width="130" />
<el-table-column
sortable
prop="balance"
label="积分余量"
min-width="120"
:sort-method="
(a, b) => {
return a.balance - b.balance;
}
"
/>
<el-table-column
sortable
label="明细数字"
min-width="120"
prop="integral"
:sort-method="
(a, b) => {
return a.integral - b.integral;
}
"
/>
<el-table-column label="备注" min-width="120" prop="mark" />
<el-table-column label="用户昵称" min-width="120" prop="nickName" />
<el-table-column prop="updateTime" label=" 添加时间" min-width="150" />
<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 prop="integral" 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 prop="linkType" 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 prop="status" 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">
<el-pagination
:page-sizes="[20, 40, 60, 80]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
: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="pageChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
@@ -89,78 +106,129 @@
</template>
<script>
import { integralListApi } from '@/api/marketing';
import cardsData from '@/components/cards/index';
import { integralListApi } from '@/api/integral';
export default {
components: { cardsData },
name: 'IntegralLog',
data() {
return {
loading: false,
options: [],
fromList: this.$constants.fromList,
listLoading: false,
listLoading: true,
tableData: {
data: [],
total: 0,
},
tableFrom: {
page: 1,
limit: 20,
dateLimit: '',
searchForm: {
keywords: '',
uid: null,
dateLimit: '',
page: 1,
limit: 15,
},
userIdList: [],
userList: [],
timeVal: [],
values: [],
pickerOptions: this.$timeOptions,
};
},
mounted() {
this.getList();
// this.getUserList()
},
methods: {
seachList() {
this.tableFrom.page = 1;
this.getList();
},
// 选择时间
selectChange(tab) {
this.tableFrom.dateLimit = tab;
this.tableFrom.page = 1;
this.timeVal = [];
this.getList();
},
// 具体日期
onchangeTime(e) {
this.timeVal = e;
this.tableFrom.dateLimit = e ? this.timeVal.join(',') : '';
this.tableFrom.page = 1;
this.getList();
},
// 列表
// 获取列表
getList() {
this.listLoading = true;
integralListApi({ limit: this.tableFrom.limit, page: this.tableFrom.page }, this.tableFrom)
const params = {
...this.searchForm,
};
// 移除空值
if (!params.keywords) delete params.keywords;
if (!params.uid) delete params.uid;
if (!params.dateLimit) delete params.dateLimit;
integralListApi(params)
.then((res) => {
this.tableData.data = res.list;
this.tableData.total = res.total;
this.tableData.data = res.list || [];
this.tableData.total = res.total || 0;
this.listLoading = false;
})
.catch((res) => {
.catch(() => {
this.listLoading = false;
});
},
pageChange(page) {
this.tableFrom.page = page;
// 搜索
handleSearch() {
this.searchForm.page = 1;
this.getList();
},
handleSizeChange(val) {
this.tableFrom.limit = val;
// 重置
handleReset() {
this.searchForm = {
keywords: '',
uid: null,
dateLimit: '',
page: 1,
limit: 15,
};
this.timeVal = [];
this.getList();
},
// 时间选择
onchangeTime(e) {
this.timeVal = e;
this.searchForm.dateLimit = e ? e.join(',') : '';
},
// 分页大小变化
handleSizeChange(val) {
this.searchForm.limit = val;
this.getList();
},
// 页码变化
handleCurrentChange(val) {
this.searchForm.page = val;
this.getList();
},
// 关联类型过滤器
linkTypeFilter(type) {
const typeMap = {
order: '订单',
sign: '签到',
system: '系统',
};
return typeMap[type] || type || '-';
},
// 状态过滤器
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 lang="sass" scoped></style>
<style scoped lang="scss">
.text-right {
text-align: right;
}
.block {
margin-top: 20px;
}
.el-input,
.el-date-picker {
width: 100%;
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div slot="header" class="clearfix">
<div class="container">
<el-form inline size="small" :model="searchForm" ref="searchForm" label-width="80px">
<el-row>
<el-col :xs="24" :sm="24" :md="18" :lg="18" :xl="18">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="用户搜索:">
<el-input v-model="searchForm.keywords" placeholder="请输入用户昵称或ID" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="用户ID">
<el-input-number v-model="searchForm.uid" placeholder="请输入用户ID" :min="1" :controls="false" style="width: 100%" />
</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="结束日期"
:picker-options="pickerOptions"
@change="onchangeTime"
/>
</el-form-item>
</el-col>
</el-col>
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6" class="text-right">
<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>
</div>
<el-table
v-loading="listLoading"
:data="tableData.data"
style="width: 100%"
size="mini"
highlight-current-row
>
<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 prop="integral" 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 prop="type" 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 prop="linkType" 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 prop="status" 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">
<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 { integralListApi } from '@/api/integral';
export default {
name: 'IntegralLog',
data() {
return {
listLoading: true,
tableData: {
data: [],
total: 0,
},
searchForm: {
keywords: '',
uid: null,
dateLimit: '',
page: 1,
limit: 15,
},
timeVal: [],
pickerOptions: this.$timeOptions,
};
},
mounted() {
this.getList();
},
methods: {
// 获取列表
getList() {
this.listLoading = true;
const params = {
...this.searchForm,
};
// 移除空值
if (!params.keywords) delete params.keywords;
if (!params.uid) delete params.uid;
if (!params.dateLimit) delete params.dateLimit;
integralListApi(params)
.then((res) => {
this.tableData.data = res.list || [];
this.tableData.total = res.total || 0;
this.listLoading = false;
})
.catch(() => {
this.listLoading = false;
});
},
// 搜索
handleSearch() {
this.searchForm.page = 1;
this.getList();
},
// 重置
handleReset() {
this.searchForm = {
keywords: '',
uid: null,
dateLimit: '',
page: 1,
limit: 15,
};
this.timeVal = [];
this.getList();
},
// 时间选择
onchangeTime(e) {
this.timeVal = e;
this.searchForm.dateLimit = e ? e.join(',') : '';
},
// 分页大小变化
handleSizeChange(val) {
this.searchForm.limit = val;
this.getList();
},
// 页码变化
handleCurrentChange(val) {
this.searchForm.page = val;
this.getList();
},
// 关联类型过滤器
linkTypeFilter(type) {
const typeMap = {
order: '订单',
sign: '签到',
system: '系统',
};
return typeMap[type] || type || '-';
},
// 状态过滤器
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">
.text-right {
text-align: right;
}
.block {
margin-top: 20px;
}
.el-input,
.el-date-picker {
width: 100%;
}
</style>