整理页面

This commit is contained in:
dzl
2025-09-23 15:12:07 +08:00
parent 972c7d4cc6
commit e5098b4379
7 changed files with 4 additions and 2582 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理系统</title>
<title>项目后台管理系统</title>
<style>
* {
margin: 0;

View File

@@ -4,7 +4,7 @@
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
<div class="logo">
<img v-if="!isCollapse" src="/logo.svg" alt="Logo" class="logo-img" />
<span v-if="!isCollapse" class="logo-text">后台管理</span>
<span v-if="!isCollapse" class="logo-text">项目后台管理</span>
<el-icon v-else class="logo-icon"><Setting /></el-icon>
</div>
@@ -47,42 +47,11 @@
<el-icon><Promotion /></el-icon>
<template #title>融豆管理</template>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/daily-transfer-stats">
<el-icon><DataAnalysis /></el-icon>
<template #title>昨日转账统计</template>
</el-menu-item>
<!-- <el-menu-item v-if="userStore.isAdmin" index="/matching-management">
<el-icon><Connection /></el-icon>
<template #title>匹配管理</template>
</el-menu-item> -->
<el-menu-item v-if="userStore.isAdmin" index="/agents">
<el-icon><Avatar /></el-icon>
<template #title>代理管理</template>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/announcements">
<el-icon><Bell /></el-icon>
<template #title>通知公告</template>
</el-menu-item>
<!-- <el-menu-item v-if="userStore.isAdmin" index="/database-monitor">
<el-icon><Monitor /></el-icon>
<template #title>数据库监控</template>
</el-menu-item> -->
<el-menu-item index="/profile">
<el-icon><UserFilled /></el-icon>
<template #title>个人资料</template>
</el-menu-item>
<!-- <el-menu-item v-if="userStore.isAdmin" index="/settings">
<el-icon><Setting /></el-icon>
<template #title>系统设置</template>
</el-menu-item> -->
</el-menu>
</el-aside>
@@ -302,7 +271,7 @@ const breadcrumbs = computed(() => {
matched.forEach(item => {
if (item.path !== '/') {
breadcrumbList.push({
title: item.meta.title.replace(' - 后台管理系统', ''),
title: item.meta.title.replace(' - 项目后台管理系统', ''),
path: item.path
})
}

View File

@@ -81,46 +81,6 @@ const routes = [
requiresAdmin: true
}
},
{
path: 'daily-transfer-stats',
name: 'DailyTransferStats',
component: () => import('@/views/DailyTransferStats.vue'),
meta: {
title: '昨日转账统计 - 炬融圈',
icon: 'DataAnalysis',
requiresAdmin: true
}
},
{
path: 'agents',
name: 'Agents',
component: () => import('@/views/Agents.vue'),
meta: {
title: '代理管理 - 炬融圈',
icon: 'Avatar',
requiresAdmin: true
}
},
{
path: 'announcements',
name: 'Announcements',
component: () => import('@/views/Announcements.vue'),
meta: {
title: '通知公告 - 炬融圈',
icon: 'Bell',
requiresAdmin: true
}
},
// {
// path: 'database-monitor',
// name: 'DatabaseMonitor',
// component: () => import('@/views/DatabaseMonitor.vue'),
// meta: {
// title: '数据库监控 - 炬融圈',
// icon: 'Monitor',
// requiresAdmin: true
// }
// },
{
path: 'profile',
name: 'Profile',
@@ -130,16 +90,6 @@ const routes = [
icon: 'UserFilled'
}
}
// {
// path: 'settings',
// name: 'Settings',
// component: () => import('@/views/Settings.vue'),
// meta: {
// title: '系统设置 - 炬融圈',
// icon: 'Setting',
// requiresAdmin: true
// }
// }
]
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -1,684 +0,0 @@
<template>
<div class="announcements-page">
<div class="page-header">
<h1 class="page-title">通知公告管理</h1>
<p class="page-subtitle">管理系统中的所有通知公告</p>
</div>
<!-- 搜索和操作栏 -->
<el-card class="search-card" shadow="never">
<el-row :gutter="20" class="search-row">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-input
v-model="searchForm.keyword"
placeholder="搜索公告标题或内容"
:prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
/>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-select
v-model="searchForm.status"
placeholder="选择状态"
clearable
style="width: 100%"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已归档" value="archived" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-select
v-model="searchForm.type"
placeholder="选择类型"
clearable
style="width: 100%"
>
<el-option label="全部" value="" />
<el-option label="系统通知" value="system" />
<el-option label="维护公告" value="maintenance" />
<el-option label="活动推广" value="promotion" />
<el-option label="重要警告" value="warning" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" class="button-group">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
</el-row>
</el-card>
<!-- 公告列表 -->
<el-card class="table-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">公告列表 ({{ pagination.total }})</span>
<el-button type="primary" @click="showCreateDialog">
<el-icon><Plus /></el-icon>
新建公告
</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="announcements"
stripe
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
<el-table-column prop="title" label="标题" min-width="200">
<template #default="{ row }">
<div class="title-cell">
<el-tag v-if="row.is_pinned" type="danger" size="small" class="pin-tag">
置顶
</el-tag>
<span class="title-text">{{ row.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="120">
<template #default="{ row }">
<el-tag :type="getTypeTagType(row.type)" size="small">
{{ getTypeLabel(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag :type="getPriorityTagType(row.priority)" size="small">
{{ getPriorityLabel(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建者" width="120" />
<el-table-column prop="publish_time" label="发布时间" width="180">
<template #default="{ row }">
{{ row.publish_time ? formatDateTime(row.publish_time) : '-' }}
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" sortable="custom">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="showEditDialog(row)">
编辑
</el-button>
<el-button
v-if="row.status === 'draft'"
type="success"
size="small"
@click="publishAnnouncement(row)"
>
发布
</el-button>
<el-button type="danger" size="small" @click="deleteAnnouncement(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 创建/编辑公告对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑公告' : '新建公告'"
width="800px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="100px"
>
<el-form-item label="公告标题" prop="title">
<el-input v-model="form.title" placeholder="请输入公告标题" maxlength="255" show-word-limit />
</el-form-item>
<el-form-item label="公告内容" prop="content">
<el-input
v-model="form.content"
type="textarea"
:rows="6"
placeholder="请输入公告内容"
maxlength="2000"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="公告类型" prop="type">
<el-select v-model="form.type" placeholder="选择公告类型" style="width: 100%">
<el-option label="系统通知" value="system" />
<el-option label="维护公告" value="maintenance" />
<el-option label="活动推广" value="promotion" />
<el-option label="重要警告" value="warning" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="选择优先级" style="width: 100%">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
<el-option label="紧急" value="urgent" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="选择状态" style="width: 100%">
<el-option label="草稿" value="draft" />
<el-option label="发布" value="published" />
<el-option label="归档" value="archived" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否置顶">
<el-switch v-model="form.is_pinned" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布时间">
<el-date-picker
v-model="form.publish_time"
type="datetime"
placeholder="选择发布时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="过期时间">
<el-date-picker
v-model="form.expire_time"
type="datetime"
placeholder="选择过期时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import api from '@/utils/api'
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const submitting = ref(false)
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 公告列表
const announcements = ref([])
// 搜索表单
const searchForm = reactive({
keyword: '',
status: '',
type: ''
})
// 分页
const pagination = reactive({
page: 1,
limit: 10,
total: 0
})
// 表单数据
const form = reactive({
id: null,
title: '',
content: '',
type: 'system',
priority: 'medium',
status: 'draft',
is_pinned: false,
publish_time: '',
expire_time: ''
})
// 表单验证规则
const formRules = {
title: [
{ required: true, message: '请输入公告标题', trigger: 'blur' },
{ min: 1, max: 255, message: '标题长度在 1 到 255 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入公告内容', trigger: 'blur' },
{ min: 1, max: 2000, message: '内容长度在 1 到 2000 个字符', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择公告类型', trigger: 'change' }
],
priority: [
{ required: true, message: '请选择优先级', trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
// 计算属性
const totalPages = computed(() => Math.ceil(pagination.total / pagination.limit))
// 方法
const fetchAnnouncements = async () => {
try {
loading.value = true
const params = {
page: pagination.page,
limit: pagination.limit,
...searchForm
}
const response = await api.get('/announcements', { params })
if (response.data.success) {
announcements.value = response.data.data.announcements
pagination.total = response.data.data.total
}
} catch (error) {
console.error('获取公告列表失败:', error)
ElMessage.error('获取公告列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
fetchAnnouncements()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
status: '',
type: ''
})
pagination.page = 1
fetchAnnouncements()
}
const handleSizeChange = (size) => {
pagination.limit = size
pagination.page = 1
fetchAnnouncements()
}
const handleCurrentChange = (page) => {
pagination.page = page
fetchAnnouncements()
}
const handleSortChange = ({ prop, order }) => {
// 实现排序逻辑
fetchAnnouncements()
}
const showCreateDialog = () => {
isEdit.value = false
resetForm()
dialogVisible.value = true
}
const showEditDialog = (row) => {
isEdit.value = true
Object.assign(form, {
id: row.id,
title: row.title,
content: row.content,
type: row.type,
priority: row.priority,
status: row.status,
is_pinned: row.is_pinned,
publish_time: row.publish_time,
expire_time: row.expire_time
})
dialogVisible.value = true
}
const resetForm = () => {
Object.assign(form, {
id: null,
title: '',
content: '',
type: 'system',
priority: 'medium',
status: 'draft',
is_pinned: false,
publish_time: '',
expire_time: ''
})
if (formRef.value) {
formRef.value.resetFields()
}
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitting.value = true
const data = { ...form }
delete data.id
let response
if (isEdit.value) {
response = await api.put(`/announcements/${form.id}`, data)
} else {
response = await api.post('/announcements', data)
}
if (response.data.success) {
ElMessage.success(isEdit.value ? '公告更新成功' : '公告创建成功')
dialogVisible.value = false
fetchAnnouncements()
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error(isEdit.value ? '公告更新失败' : '公告创建失败')
} finally {
submitting.value = false
}
}
const publishAnnouncement = async (row) => {
try {
await ElMessageBox.confirm('确定要发布这个公告吗?', '确认发布', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const response = await api.put(`/announcements/${row.id}`, {
status: 'published',
publish_time: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
if (response.data.success) {
ElMessage.success('公告发布成功')
fetchAnnouncements()
}
} catch (error) {
if (error !== 'cancel') {
console.error('发布公告失败:', error)
ElMessage.error('发布公告失败')
}
}
}
const deleteAnnouncement = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这个公告吗?删除后无法恢复。', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const response = await api.delete(`/announcements/${row.id}`)
if (response.data.success) {
ElMessage.success('公告删除成功')
fetchAnnouncements()
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除公告失败:', error)
ElMessage.error('删除公告失败')
}
}
}
// 辅助方法
const getTypeLabel = (type) => {
const labels = {
system: '系统通知',
maintenance: '维护公告',
promotion: '活动推广',
warning: '重要警告'
}
return labels[type] || type
}
const getTypeTagType = (type) => {
const types = {
system: 'info',
maintenance: 'warning',
promotion: 'success',
warning: 'danger'
}
return types[type] || 'info'
}
const getPriorityLabel = (priority) => {
const labels = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
}
return labels[priority] || priority
}
const getPriorityTagType = (priority) => {
const types = {
low: 'info',
medium: '',
high: 'warning',
urgent: 'danger'
}
return types[priority] || ''
}
const getStatusLabel = (status) => {
const labels = {
draft: '草稿',
published: '已发布',
archived: '已归档'
}
return labels[status] || status
}
const getStatusTagType = (status) => {
const types = {
draft: 'info',
published: 'success',
archived: 'warning'
}
return types[status] || 'info'
}
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
return new Date(dateTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 生命周期
onMounted(() => {
fetchAnnouncements()
})
</script>
<style scoped>
.announcements-page {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: #909399;
margin: 0;
}
.search-card {
margin-bottom: 20px;
}
.search-row {
align-items: flex-end;
}
.button-group {
display: flex;
gap: 10px;
}
.table-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.title-cell {
display: flex;
align-items: center;
gap: 8px;
}
.pin-tag {
flex-shrink: 0;
}
.title-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.announcements-page {
padding: 10px;
}
.search-row .el-col {
margin-bottom: 10px;
}
.button-group {
width: 100%;
}
.button-group .el-button {
flex: 1;
}
}
</style>

View File

@@ -1,525 +0,0 @@
<template>
<div class="daily-transfer-stats-container">
<el-card class="page-header">
<h2>昨日转账统计</h2>
<p>统计用户昨日转出金额今日入账金额及平账差额</p>
</el-card>
<!-- 统计概览 -->
<el-card class="stats-overview">
<el-row :gutter="20">
<el-col :span="6">
<div class="stat-item">
<div class="stat-value">{{ statsData.totalUsers }}</div>
<div class="stat-label">涉及用户数</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item">
<div class="stat-value">¥{{ statsData.totalYesterdayOut.toLocaleString() }}</div>
<div class="stat-label">昨日总转出</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item">
<div class="stat-value">¥{{ statsData.totalTodayIn.toLocaleString() }}</div>
<div class="stat-label">今日总入账</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item">
<div class="stat-value" :class="{ 'negative': statsData.totalBalanceNeeded > 0, 'positive': statsData.totalBalanceNeeded < 0 }">
¥{{ Math.abs(statsData.totalBalanceNeeded).toLocaleString() }}
</div>
<div class="stat-label">{{ statsData.totalBalanceNeeded > 0 ? '总缺口' : '总盈余' }}</div>
</div>
</el-col>
</el-row>
</el-card>
<!-- 用户列表 -->
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>用户转账统计详情</span>
<div class="date-info">
<el-tag type="info" size="small">昨日: {{ dateInfo.yesterday }}</el-tag>
<el-tag type="success" size="small">今日: {{ dateInfo.today }}</el-tag>
</div>
</div>
</template>
<el-table
:data="userList"
v-loading="loading"
stripe
style="width: 100%"
:default-sort="{ prop: 'balance_needed', order: 'descending' }"
>
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="real_name" label="真实姓名" width="120" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="用户余额" width="120" sortable prop="balance">
<template #default="{ row }">
<span
class="amount-text"
:class="{
'negative': parseFloat(row.balance) < 0,
'positive': parseFloat(row.balance) > 0,
'zero': parseFloat(row.balance) === 0
}"
>
¥{{ parseFloat(row.balance).toLocaleString() }}
</span>
</template>
</el-table-column>
<el-table-column label="昨日转出金额" width="140" sortable prop="yesterday_out_amount">
<template #default="{ row }">
<span class="amount-text">¥{{ parseFloat(row.yesterday_out_amount).toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="今日转账金额" width="140" sortable prop="today_in_amount">
<template #default="{ row }">
<span class="amount-text positive">¥{{ parseFloat(row.confirmed_from_amount).toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="今日入账金额" width="140" sortable prop="today_in_amount">
<template #default="{ row }">
<span class="amount-text positive">¥{{ parseFloat(row.today_in_amount).toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="平账差额" width="140" sortable prop="balance_needed">
<template #default="{ row }">
<span
class="amount-text"
:class="{
'negative': parseFloat(row.balance_needed) > 0,
'positive': parseFloat(row.balance_needed) < 0,
'zero': parseFloat(row.balance_needed) === 0
}"
>
{{ parseFloat(row.balance_needed) > 0 ? '还需' : parseFloat(row.balance_needed) < 0 ? '盈余' : '已平账' }}
{{ parseFloat(row.balance_needed) !== 0 ? '¥' + Math.abs(parseFloat(row.balance_needed)).toLocaleString() : '' }}
</span>
</template>
</el-table-column>
<el-table-column label="平账状态" width="100">
<template #default="{ row }">
<el-tag
:type="getBalanceStatusType(parseFloat(row.balance_needed))"
size="small"
>
{{ getBalanceStatusText(parseFloat(row.balance_needed)) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="viewUserTransfers(row.user_id)"
>
查看详情
</el-button>
<el-button
type="success"
size="small"
@click="showTransferDialog(row)"
:disabled="parseFloat(row.balance_needed) <= 0"
>
转账平账
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 转账对话框 -->
<el-dialog v-model="transferDialog.visible" title="转账平账" width="500px">
<el-form :model="transferForm" :rules="transferRules" ref="transferFormRef" label-width="100px">
<el-form-item label="目标用户">
<el-input :value="transferDialog.targetUser?.username + ' (' + (transferDialog.targetUser?.real_name || '未设置') + ')'" disabled />
</el-form-item>
<el-form-item label="当前缺口">
<el-input :value="'¥' + Math.abs(parseFloat(transferDialog.targetUser?.balance_needed || 0)).toLocaleString()" disabled />
</el-form-item>
<el-form-item label="转出用户" prop="from_user_id">
<el-select v-model="transferForm.from_user_id" placeholder="选择转出用户" @change="onFromUserChange">
<el-option
v-for="user in users"
:key="user.id"
:label="`${user.username} (${user.real_name || '未设置'}) - 余额: ¥${user.balance || 0}`"
:value="user.id"
/>
</el-select>
<div v-if="selectedFromUser" class="user-balance-info">
<el-text type="info" size="small">
当前余额: ¥{{ selectedFromUser.balance || 0 }}
</el-text>
</div>
</el-form-item>
<el-form-item label="转账金额" prop="amount">
<el-input-number
v-model="transferForm.amount"
:min="0.01"
:precision="2"
placeholder="请输入转账金额"
style="width: 100%"
/>
<div class="amount-suggestion">
<el-button
type="text"
size="small"
@click="setFullAmount"
>
设为全额平账 (¥{{ Math.abs(parseFloat(transferDialog.targetUser?.balance_needed || 0)).toLocaleString() }})
</el-button>
</div>
</el-form-item>
<el-form-item label="备注" prop="description">
<el-input
v-model="transferForm.description"
type="textarea"
:rows="3"
placeholder="请输入转账备注"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="transferDialog.visible = false">取消</el-button>
<el-button type="primary" @click="createTransfer" :loading="transferDialog.loading">
确认转账
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/utils/api'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(false)
const userList = ref([])
const users = ref([])
const dateInfo = reactive({
yesterday: '',
today: ''
})
// 转账对话框相关数据
const transferDialog = ref({
visible: false,
loading: false,
targetUser: null
})
const transferForm = ref({
from_user_id: '',
amount: null,
description: ''
})
const selectedFromUser = ref(null)
const transferFormRef = ref(null)
// 转账表单验证规则
const transferRules = {
from_user_id: [
{ required: true, message: '请选择转出用户', trigger: 'change' }
],
amount: [
{ required: true, message: '请输入转账金额', trigger: 'blur' },
{ type: 'number', min: 0.01, message: '金额必须大于0.01', trigger: 'blur' }
]
}
// 统计数据
const statsData = computed(() => {
const totalUsers = userList.value.length
const totalYesterdayOut = userList.value.reduce((sum, user) => sum + parseFloat(user.yesterday_out_amount), 0)
const totalTodayIn = userList.value.reduce((sum, user) => sum + parseFloat(user.today_in_amount), 0)
const totalBalanceNeeded = userList.value.reduce((sum, user) => sum + parseFloat(user.balance_needed), 0)
return {
totalUsers,
totalYesterdayOut,
totalTodayIn,
totalBalanceNeeded
}
})
// 获取平账状态类型
const getBalanceStatusType = (balanceNeeded) => {
if (balanceNeeded > 0) return 'danger'
if (balanceNeeded < 0) return 'success'
return 'info'
}
// 获取平账状态文本
const getBalanceStatusText = (balanceNeeded) => {
if (balanceNeeded > 0) return '未平账'
if (balanceNeeded < 0) return '有盈余'
return '已平账'
}
// 查看用户转账详情
const viewUserTransfers = (userId) => {
router.push(`/transfers?userId=${userId}`)
}
// 显示转账对话框
const showTransferDialog = (user) => {
transferDialog.value.targetUser = user
transferForm.value = {
from_user_id: '',
amount: null,
description: `为用户 ${user.username} 平账转账`
}
selectedFromUser.value = null
transferDialog.value.visible = true
fetchUsers()
}
// 转出用户变更时的处理
const onFromUserChange = (userId) => {
selectedFromUser.value = users.value.find(user => user.id === userId) || null
}
// 设置全额平账金额
const setFullAmount = () => {
if (transferDialog.value.targetUser) {
const balanceNeeded = Math.abs(parseFloat(transferDialog.value.targetUser.balance_needed))
transferForm.value.amount = balanceNeeded
}
}
// 创建转账
const createTransfer = async () => {
if (!transferFormRef.value) return
try {
await transferFormRef.value.validate()
transferDialog.value.loading = true
const transferData = {
to_user_id: transferDialog.value.targetUser.user_id,
amount: transferForm.value.amount,
description: transferForm.value.description,
transfer_type: 'user_to_user'
}
await api.transfers.createAdminTransfer(transferForm.value.from_user_id, transferData)
ElMessage.success('转账成功')
transferDialog.value.visible = false
fetchDailyStats() // 刷新统计数据
} catch (error) {
const errorMessage = error.response?.data?.error?.message || error.response?.data?.message || error.message
ElMessage.error('转账失败: ' + errorMessage)
} finally {
transferDialog.value.loading = false
}
}
// 获取用户列表
const fetchUsers = async () => {
try {
const response = await api.users.getUsers({ limit: 1000 })
users.value = response.data.users || []
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
// 获取昨日转账统计数据
const fetchDailyStats = async () => {
loading.value = true
try {
const response = await api.transfers.getDailyStats()
if (response.data.success) {
userList.value = response.data.data.users
dateInfo.yesterday = response.data.data.date.yesterday
dateInfo.today = response.data.data.date.today
} else {
ElMessage.error(response.data.message || '获取数据失败')
}
} catch (error) {
console.error('获取昨日转账统计失败:', error)
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDailyStats()
})
</script>
<style scoped>
.daily-transfer-stats-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0 0 8px 0;
color: #303133;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #606266;
font-size: 14px;
}
.stats-overview {
margin-bottom: 20px;
}
.stat-item {
text-align: center;
padding: 20px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #409eff;
margin-bottom: 8px;
}
.stat-value.negative {
color: #f56c6c;
}
.stat-value.positive {
color: #67c23a;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.table-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.date-info {
display: flex;
gap: 10px;
}
.amount-text {
font-weight: 600;
}
.amount-text.positive {
color: #67c23a;
}
.amount-text.negative {
color: #f56c6c;
}
.amount-text.zero {
color: #909399;
}
/* 转账对话框样式 */
.user-balance-info {
margin-top: 5px;
}
.amount-suggestion {
margin-top: 5px;
}
.amount-suggestion .el-button {
padding: 0;
font-size: 12px;
color: #409eff;
}
.amount-suggestion .el-button:hover {
color: #66b1ff;
}
/* 改进表格样式 */
.el-table {
border-radius: 8px;
overflow: hidden;
}
.el-table .el-table__header {
background-color: #f8f9fa;
}
.el-table .el-table__header th {
background-color: #f8f9fa;
color: #606266;
font-weight: 600;
}
/* 改进按钮间距 */
.el-table .el-button + .el-button {
margin-left: 8px;
}
/* 改进统计卡片样式 */
.stats-overview .el-card__body {
padding: 15px;
}
.stat-item {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
transition: transform 0.2s ease;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 改进页面头部 */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
}
.page-header h2 {
color: white;
}
.page-header p {
color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@@ -10,7 +10,7 @@
<div class="login-header">
<div class="logo">
<el-icon class="logo-icon"><Setting /></el-icon>
<h1 class="title">后台管理系统</h1>
<h1 class="title">项目后台管理系统</h1>
</div>
<p class="subtitle">欢迎回来请登录您的账户</p>
</div>