feat: 新增积分外部页面(免认证三页 + 配套基础设施)
前端: - 新增 EmptyLayout 空壳布局(无侧边栏/导航) - 新增 requestNoAuth Axios 实例(不注入 token) - 新增 integralExternal 路由模块(/integral-external/*) - permission.js 加入 whiteListPrefixes 前缀白名单跳过登录 - 新增 phoneDesensitize 手机号脱敏过滤器 - 新增三个免认证页面: · 积分订单页(/integral-external/order) · 用户积分页(/integral-external/user,手机号脱敏) · 用户积分明细子页(/integral-external/user/integral-detail) 后端: - 新增 ExternalIntegralController(无 @PreAuthorize) · GET /api/external/integral/order/list · GET /api/external/integral/user/list · POST /api/external/integral/log/list - WebSecurityConfig 加入 /api/external/integral/** permitAll 文档与工具: - 新增 coding plan、schedule、测试报告 - 新增 start-backend.sh / start-frontend.sh 本地启动脚本 - 新增 .mvn/wrapper/maven-wrapper.properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
39
backend-adminend/src/api/integralExternal.js
Normal file
39
backend-adminend/src/api/integralExternal.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 积分外部页面 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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户积分明细分页列表
|
||||
*/
|
||||
export function getExternalIntegralLog(data) {
|
||||
return requestNoAuth({
|
||||
url: 'external/integral/log/list',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
18
backend-adminend/src/layout/EmptyLayout.vue
Normal file
18
backend-adminend/src/layout/EmptyLayout.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
30
backend-adminend/src/router/modules/integralExternal.js
Normal file
30
backend-adminend/src/router/modules/integralExternal.js
Normal 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;
|
||||
51
backend-adminend/src/utils/requestNoAuth.js
Normal file
51
backend-adminend/src/utils/requestNoAuth.js
Normal 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;
|
||||
254
backend-adminend/src/views/integral-external/order/index.vue
Normal file
254
backend-adminend/src/views/integral-external/order/index.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<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.type" type="button" class="mr20" size="small" @change="seachList">
|
||||
<el-radio-button v-for="(item, i) in typeOptions" :key="i" :label="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<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="orderType" label="订单类型" min-width="110" />
|
||||
<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 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: '',
|
||||
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' },
|
||||
],
|
||||
},
|
||||
typeOptions: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '普通订单', value: '0' },
|
||||
{ label: '秒杀订单', value: '1' },
|
||||
{ label: '砍价订单', value: '2' },
|
||||
{ label: '拼团订单', value: '3' },
|
||||
{ label: '视频号订单', value: '4' },
|
||||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.listLoading = true;
|
||||
const params = { ...this.tableFrom };
|
||||
if (!params.type) delete params.type;
|
||||
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>
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="divBox">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="back-bar">
|
||||
<el-button size="small" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户概览卡片 -->
|
||||
<el-card 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 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="80px">
|
||||
<el-row>
|
||||
<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: {
|
||||
uid: null,
|
||||
dateLimit: '',
|
||||
page: 1,
|
||||
limit: 15,
|
||||
},
|
||||
timeVal: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 从路由 query 中注入 uid 及概览信息
|
||||
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;
|
||||
this.searchForm.uid = this.uid;
|
||||
},
|
||||
mounted() {
|
||||
if (this.uid) {
|
||||
this.getList();
|
||||
} else {
|
||||
this.$message.error('缺少用户ID,无法加载积分明细');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.listLoading = true;
|
||||
const params = { ...this.searchForm };
|
||||
if (!params.dateLimit) delete params.dateLimit;
|
||||
|
||||
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.dateLimit = '';
|
||||
this.searchForm.page = 1;
|
||||
this.timeVal = [];
|
||||
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) {
|
||||
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">
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
206
backend-adminend/src/views/integral-external/user/index.vue
Normal file
206
backend-adminend/src/views/integral-external/user/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user