优化路由逻辑,根据用户类型动态重定向到不同的默认页面,增强登录后的用户体验;更新退单管理界面,改进搜索功能和响应式设计,提升移动端用户体验。

This commit is contained in:
zyh
2025-08-28 12:23:05 +08:00
parent 602d88a5a2
commit 2065b062e3
4 changed files with 307 additions and 180 deletions

View File

@@ -177,3 +177,4 @@ src/
---
*最后更新时间: 2024年1月*

View File

@@ -20,7 +20,20 @@ export const routes = [
path: '/',
component: AdminLayout,
children: [
{ path: '', redirect: '/users' },
{
path: '',
redirect: (to) => {
// 根据用户类型重定向到不同的默认页面
const { getCurrentUserType } = require('@/utils/permission')
const userType = getCurrentUserType()
if (userType?.toLowerCase() === 'agent') {
return '/links' // 代理商跳转到链接管理
} else {
return '/users' // 管理员跳转到用户管理
}
}
},
{ path: 'users', name: 'Users', component: UserList, meta: { title: '用户管理' } },
{ path: 'settings', name: 'Settings', component: Settings, meta: { title: '系统设置' } },
{ path: 'links', name: 'Links', component: LinkGenerate, meta: { title: '链接管理' } },
@@ -44,7 +57,17 @@ router.beforeEach((to, from, next) => {
// 检查路由权限
if (to.name && !canAccessRoute(to.name)) {
return next({ name: 'Users' }) // 无权限时跳转到用户管理
// 根据用户类型跳转到有权限的默认页面
const { getCurrentUserType } = require('@/utils/permission')
const userType = getCurrentUserType()
if (userType?.toLowerCase() === 'agent') {
// 代理商跳转到链接管理页面
return next({ name: 'Links' })
} else {
// 管理员或其他用户跳转到用户管理
return next({ name: 'Users' })
}
}
next()

View File

@@ -127,8 +127,19 @@ async function onSubmit() {
showSuccessMessage('登录成功')
persistRemember()
console.debug('login response:', res.data)
const redirect = route.query.redirect || '/'
router.replace(String(redirect))
// 如果有 redirect 参数,直接跳转
if (route.query.redirect) {
router.replace(String(route.query.redirect))
} else {
// 根据用户类型跳转到合适的默认页面
const userType = data?.userType?.toLowerCase()
if (userType === 'agent') {
router.replace('/links') // 代理商跳转到链接管理
} else {
router.replace('/users') // 管理员跳转到用户管理
}
}
} catch (e) {
showErrorMessage(e, '登录失败')
} finally {

View File

@@ -5,23 +5,41 @@
<p class="page-description">管理用户的退单申请支持按链接编号查询和执行退单操作</p>
</div>
<!-- 搜索区域 -->
<!-- 搜索区域响应式 -->
<div class="search-section">
<el-card>
<el-row :gutter="20">
<el-col :span="8">
<el-form :model="searchForm" @submit.prevent>
<el-row :gutter="12">
<!-- 链接编号手机端独占一行桌面端占较宽比例 -->
<el-col :xs="24" :sm="12" :md="10" :lg="10">
<div class="code-input-wrap">
<el-input
v-model="searchForm.codeNo"
placeholder="请输入链接编号"
size="large"
clearable
placeholder="请输入链接编号(支持粘贴后回车)"
@keyup.enter="handleSearch"
>
<template #prepend>链接编号</template>
<template #prefix>
<el-icon><Link /></el-icon>
</template>
</el-input>
<el-tooltip content="从剪贴板粘贴" placement="top">
<el-button
class="paste-btn"
:icon="DocumentCopy"
size="large"
@click="pasteFromClipboard"
/>
</el-tooltip>
</div>
</el-col>
<el-col :span="8">
<!-- 状态筛选可选主要用于查看信息时做个提示筛选不影响后端查询 -->
<el-col :xs="24" :sm="8" :md="8" :lg="6">
<el-select
v-model="searchForm.status"
size="large"
placeholder="选择状态"
clearable
style="width: 100%"
@@ -35,17 +53,26 @@
<el-option label="已过期" value="EXPIRED" />
</el-select>
</el-col>
<el-col :span="8">
<el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon>
查询
<!-- 按钮区手机端纵向铺满 -->
<el-col :xs="24" :sm="4" :md="6" :lg="8">
<div class="btn-group">
<el-button
type="primary"
size="large"
@click="handleSearch"
:loading="loading"
:disabled="!searchForm.codeNo.trim()"
>
<el-icon class="mr-6"><Search /></el-icon> 查询
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
<el-button size="large" @click="handleReset">
<el-icon class="mr-6"><Refresh /></el-icon> 重置
</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
@@ -54,7 +81,20 @@
<el-card>
<template #header>
<div class="card-header">
<div class="code-and-status">
<span>链接信息</span>
<span class="code-chip">
<span class="mono">{{ linkInfo.codeNo }}</span>
<el-tooltip content="复制链接编号" placement="top">
<el-button
:icon="DocumentCopy"
circle
size="small"
@click="copyCodeNo(linkInfo.codeNo)"
/>
</el-tooltip>
</span>
</div>
<el-tag :type="getStatusTagType(linkInfo.status)">
{{ getStatusText(linkInfo.status) }}
</el-tag>
@@ -62,7 +102,9 @@
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="链接编号">{{ linkInfo.codeNo }}</el-descriptions-item>
<el-descriptions-item label="链接编号">
<span class="mono">{{ linkInfo.codeNo }}</span>
</el-descriptions-item>
<el-descriptions-item label="当前状态">
<el-tag :type="getStatusTagType(linkInfo.status)">
{{ getStatusText(linkInfo.status) }}
@@ -70,10 +112,14 @@
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDateTime(linkInfo.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDateTime(linkInfo.updatedAt) }}</el-descriptions-item>
<el-descriptions-item label="代理ID">{{ linkInfo.agentId || '-' }}</el-descriptions-item>
<el-descriptions-item label="关联设备">{{ linkInfo.machineId || '-' }}</el-descriptions-item>
<el-descriptions-item label="总点数">{{ linkInfo.totalPoints || '-' }}</el-descriptions-item>
<el-descriptions-item label="当前点数">{{ linkInfo.currentPoints || '-' }}</el-descriptions-item>
<el-descriptions-item label="代理ID">
<span class="mono">{{ linkInfo.agentId || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="关联设备">
<span class="mono">{{ linkInfo.machineId || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="总点数">{{ linkInfo.totalPoints ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="当前点数">{{ linkInfo.currentPoints ?? '-' }}</el-descriptions-item>
<el-descriptions-item v-if="linkInfo.refundAt" label="退单时间">
{{ formatDateTime(linkInfo.refundAt) }}
</el-descriptions-item>
@@ -87,24 +133,13 @@
@click="handleRefund"
:loading="refunding"
>
<el-icon><RefreshLeft /></el-icon>
执行退单
<el-icon class="mr-6"><RefreshLeft /></el-icon> 执行退单
</el-button>
<el-button
v-else-if="linkInfo.status === 'REFUNDED'"
disabled
type="info"
>
<el-icon><Check /></el-icon>
已退单
<el-button v-else-if="linkInfo.status === 'REFUNDED'" disabled type="info">
<el-icon class="mr-6"><Check /></el-icon> 已退单
</el-button>
<el-button
v-else
disabled
type="info"
>
<el-icon><Warning /></el-icon>
当前状态不允许退单
<el-button v-else disabled type="info">
<el-icon class="mr-6"><Warning /></el-icon> 当前状态不允许退单
</el-button>
</div>
</el-card>
@@ -162,18 +197,22 @@
</el-card>
</div>
<!-- 退单确认对话框 -->
<!-- 退单确认对话框移动端全屏 -->
<el-dialog
v-model="refundDialogVisible"
title="确认退单"
width="500px"
:width="isMobile ? '96vw' : '500px'"
:fullscreen="isMobile"
:append-to-body="true"
:close-on-click-modal="false"
:before-close="handleRefundDialogClose"
top="8vh"
>
<div class="refund-confirm">
<el-icon class="warning-icon"><WarningFilled /></el-icon>
<div class="confirm-content">
<h3>确认要对以下链接执行退单操作吗</h3>
<p class="link-code">链接编号<strong>{{ linkInfo?.codeNo }}</strong></p>
<p class="link-code">链接编号<strong class="mono">{{ linkInfo?.codeNo }}</strong></p>
<p class="warning-text">
<el-icon><Warning /></el-icon>
退单操作不可逆执行后链接将无法继续使用
@@ -183,22 +222,25 @@
<template #footer>
<el-button @click="refundDialogVisible = false">取消</el-button>
<el-button
type="danger"
@click="confirmRefund"
:loading="refunding"
>
确认退单
</el-button>
<el-button type="danger" @click="confirmRefund" :loading="refunding">确认退单</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, RefreshLeft, Check, Warning, WarningFilled } from '@element-plus/icons-vue'
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
Search,
Refresh,
RefreshLeft,
Check,
Warning,
WarningFilled,
Link,
DocumentCopy
} from '@element-plus/icons-vue'
import { getLinkStatus, refundLink } from '@/api/links'
export default {
@@ -209,92 +251,135 @@ export default {
RefreshLeft,
Check,
Warning,
WarningFilled
WarningFilled,
Link,
DocumentCopy
},
setup() {
// 响应式数据
// 视口与状态
const isMobile = ref(false)
const loading = ref(false)
const refunding = ref(false)
const refundDialogVisible = ref(false)
const linkInfo = ref(null)
// 表单
const searchForm = reactive({
codeNo: '',
status: ''
})
// 查询链接信息
// 自适应
const updateIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
onMounted(() => {
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', updateIsMobile)
})
// 粘贴剪贴板
const pasteFromClipboard = async () => {
try {
if (!navigator.clipboard) {
ElMessage.warning('当前环境不支持剪贴板读取')
return
}
const text = await navigator.clipboard.readText()
if (!text) {
ElMessage.info('剪贴板为空')
return
}
searchForm.codeNo = text.trim()
// 自动触发查询(可按需注释)
if (searchForm.codeNo) handleSearch()
} catch (err) {
ElMessage.error('读取剪贴板失败,请手动粘贴')
}
}
// 复制编号
const copyCodeNo = async (code) => {
try {
await navigator.clipboard.writeText(code || '')
ElMessage.success('已复制链接编号')
} catch {
ElMessage.error('复制失败,请手动选择复制')
}
}
// 查询
const handleSearch = async () => {
if (!searchForm.codeNo.trim()) {
ElMessage.warning('请输入链接编号')
return
}
loading.value = true
try {
const response = await getLinkStatus(searchForm.codeNo)
const response = await getLinkStatus(searchForm.codeNo.trim())
linkInfo.value = response.data
// 如果选择了状态筛选,仅做前端提示
if (linkInfo.value && searchForm.status && linkInfo.value.status !== searchForm.status) {
ElMessage.info(`已找到链接,但状态为「${getStatusText(linkInfo.value.status)}`)
}
if (!linkInfo.value) {
ElMessage.warning('未找到相关链接信息')
}
} catch (error) {
console.error('查询链接失败:', error)
linkInfo.value = null
// 根据错误状态显示不同消息
if (error.response?.status === 404) {
if (error?.response?.status === 404) {
ElMessage.error('链接不存在')
} else if (error.response?.status === 403) {
} else if (error?.response?.status === 403) {
ElMessage.error('无权限查看此链接')
} else {
ElMessage.error('查询失败,请稍后重试')
ElMessage.error(error?.response?.data?.message || '查询失败,请稍后重试')
}
} finally {
loading.value = false
}
}
// 重置搜索
// 重置
const handleReset = () => {
searchForm.codeNo = ''
searchForm.status = ''
linkInfo.value = null
}
// 判断是否可以退单
const canRefund = (status) => {
return ['NEW', 'USING', 'LOGGED_IN'].includes(status)
}
// 资格判断
const canRefund = (status) => ['NEW', 'USING', 'LOGGED_IN'].includes(status)
// 处理退单按钮点击
// 打开确认弹窗
const handleRefund = () => {
if (!linkInfo.value) return
refundDialogVisible.value = true
}
// 确认退单
const confirmRefund = async () => {
if (!linkInfo.value) return
refunding.value = true
try {
await refundLink(linkInfo.value.codeNo)
// 更新本地状态
linkInfo.value.status = 'REFUNDED'
linkInfo.value.refundAt = new Date().toISOString()
linkInfo.value.updatedAt = new Date().toISOString()
const nowISO = new Date().toISOString()
linkInfo.value = {
...linkInfo.value,
status: 'REFUNDED',
refundAt: nowISO,
updatedAt: nowISO
}
refundDialogVisible.value = false
ElMessage.success('退单操作成功')
} catch (error) {
console.error('退单失败:', error)
// 根据错误状态显示不同消息
if (error.response?.status === 400) {
if (error?.response?.status === 400) {
const errorCode = error.response.data?.code
switch (errorCode) {
case 'LINK_003':
@@ -309,53 +394,50 @@ export default {
default:
ElMessage.error(error.response.data?.message || '退单失败')
}
} else if (error.response?.status === 403) {
} else if (error?.response?.status === 403) {
ElMessage.error('无权限操作此链接')
} else {
ElMessage.error('退单失败,请稍后重试')
ElMessage.error(error?.response?.data?.message || '退单失败,请稍后重试')
}
} finally {
refunding.value = false
}
}
// 关闭退单对话框
// 关闭弹窗
const handleRefundDialogClose = () => {
if (!refunding.value) {
refundDialogVisible.value = false
}
}
// 获取状态标签类型
// 状态展示
const getStatusTagType = (status) => {
const statusTypes = {
'NEW': 'info',
'USING': 'warning',
'LOGGED_IN': 'primary',
'COMPLETED': 'success',
'REFUNDED': 'info',
'EXPIRED': 'danger'
const map = {
NEW: 'info',
USING: 'warning',
LOGGED_IN: 'primary',
COMPLETED: 'success',
REFUNDED: 'info',
EXPIRED: 'danger'
}
return statusTypes[status] || 'info'
return map[status] || 'info'
}
// 获取状态文本
const getStatusText = (status) => {
const statusTexts = {
'NEW': '新建',
'USING': '使用中',
'LOGGED_IN': '已登录',
'COMPLETED': '已完成',
'REFUNDED': '已退单',
'EXPIRED': '已过期'
const map = {
NEW: '新建',
USING: '使用中',
LOGGED_IN: '已登录',
COMPLETED: '已完成',
REFUNDED: '已退单',
EXPIRED: '已过期'
}
return statusTexts[status] || status
return map[status] || status
}
// 格式化时间
// 时间格式化
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
try {
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
@@ -366,23 +448,29 @@ export default {
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
} catch {
return dateTime
}
}
return {
// state
isMobile,
loading,
refunding,
refundDialogVisible,
linkInfo,
searchForm,
// actions
pasteFromClipboard,
copyCodeNo,
handleSearch,
handleReset,
canRefund,
handleRefund,
confirmRefund,
handleRefundDialogClose,
// helpers
getStatusTagType,
getStatusText,
formatDateTime
@@ -399,35 +487,60 @@ export default {
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0 0 8px 0;
color: #303133;
font-size: 24px;
font-weight: 600;
}
.page-description {
margin: 0;
color: #909399;
font-size: 14px;
}
/* 搜索区样式 */
.search-section {
margin-bottom: 20px;
}
.code-input-wrap {
display: flex;
gap: 8px;
align-items: center;
}
.code-input-wrap .paste-btn {
padding: 0 12px;
}
/* 结果卡片 */
.link-info-section {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
gap: 8px;
}
.code-and-status {
display: flex;
align-items: center;
gap: 12px;
}
.code-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
border-radius: 6px;
background: #f5f7fa;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace;
}
/* 操作按钮 */
.action-buttons {
margin-top: 20px;
padding-top: 20px;
@@ -435,24 +548,23 @@ export default {
text-align: right;
}
/* 空状态 */
.empty-state {
margin-bottom: 20px;
}
/* 帮助卡片 */
.help-section {
margin-bottom: 20px;
}
.help-list {
margin: 0;
padding-left: 20px;
}
.help-list li {
margin-bottom: 8px;
line-height: 1.6;
}
.help-list .el-tag {
margin-right: 8px;
}
@@ -463,87 +575,67 @@ export default {
align-items: flex-start;
gap: 16px;
}
.warning-icon {
font-size: 24px;
color: #e6a23c;
flex-shrink: 0;
margin-top: 4px;
}
.confirm-content {
flex: 1;
}
.confirm-content h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #303133;
}
.link-code {
margin: 8px 0;
font-size: 14px;
color: #606266;
}
.link-code strong {
color: #409eff;
font-family: 'Courier New', monospace;
}
.warning-text {
margin: 12px 0 0 0;
color: #e6a23c;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
/* 按钮组(响应式) */
.btn-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.refund-management {
padding: 12px;
}
.page-header h2 {
font-size: 20px;
}
.action-buttons {
text-align: center;
}
.el-descriptions {
font-size: 12px;
}
.refund-confirm {
flex-direction: column;
text-align: center;
}
.warning-icon {
align-self: center;
}
/* 手机端按钮纵向排列更易点按 */
.btn-group {
grid-template-columns: 1fr;
}
}
/* 深色主题适配 */
.dark .page-header h2 {
color: #e5eaf3;
}
.dark .page-header h2 { color: #e5eaf3; }
.dark .page-description { color: #a3a6ad; }
.dark .confirm-content h3 { color: #e5eaf3; }
.dark .link-code { color: #a3a6ad; }
.dark .page-description {
color: #a3a6ad;
}
.dark .confirm-content h3 {
color: #e5eaf3;
}
.dark .link-code {
color: #a3a6ad;
}
/* 小图标间隔 */
.mr-6 { margin-right: 6px; }
</style>