mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-01 00:26:38 +08:00
1328 lines
37 KiB
Vue
1328 lines
37 KiB
Vue
<template>
|
|
<div class="user-container">
|
|
<!-- 页面头部 -->
|
|
<div class="page-header">
|
|
<h1 class="page-title">用户管理</h1>
|
|
<div class="header-actions">
|
|
<!-- 搜索框 -->
|
|
<el-input
|
|
v-model="searchKeyword"
|
|
placeholder="搜索用户名或姓名"
|
|
prefix-icon="el-icon-search"
|
|
@keyup.enter.native="handleSearch"
|
|
clearable
|
|
style="width: 250px; margin-right: 15px;"
|
|
>
|
|
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
|
|
</el-input>
|
|
|
|
<el-button
|
|
type="primary"
|
|
icon="el-icon-plus"
|
|
@click="handleAdd"
|
|
class="add-btn"
|
|
>
|
|
新增用户
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 用户列表 -->
|
|
<div class="table-container">
|
|
<el-table
|
|
:data="userList"
|
|
v-loading="loading"
|
|
stripe
|
|
border
|
|
style="width: 100%"
|
|
>
|
|
<el-table-column prop="id" label="ID"></el-table-column>
|
|
<el-table-column prop="name" label="姓名"></el-table-column>
|
|
<el-table-column prop="user" label="用户名"></el-table-column>
|
|
<el-table-column label="流量统计" width="220">
|
|
<template slot-scope="scope">
|
|
<div class="flow-stats">
|
|
<div class="flow-item">
|
|
<span class="flow-label">总流量:</span>
|
|
<span class="flow-value">{{ formatFlow(scope.row.flow, 'gb') }}</span>
|
|
</div>
|
|
<div class="flow-item">
|
|
<span class="flow-label">已用:</span>
|
|
<span class="flow-value used">{{ formatFlow(calculateUserTotalUsedFlow(scope.row)) }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="num" label="转发数量" width="100"></el-table-column>
|
|
<el-table-column prop="status" label="状态">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
|
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="flowResetTime" label="流量重置日期" width="130">
|
|
<template slot-scope="scope">
|
|
<span v-if="scope.row.flowResetTime">
|
|
每月{{ scope.row.flowResetTime }}号
|
|
</span>
|
|
<span v-else class="no-reset-time">未设置</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="expTime" label="过期时间" width="180">
|
|
<template slot-scope="scope">
|
|
<span v-if="scope.row.expTime" :class="getExpTimeClass(scope.row.expTime)">
|
|
{{ scope.row.expTime | dateFormat }}
|
|
</span>
|
|
<span v-else class="no-exptime">永久</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="createdTime" label="创建时间">
|
|
<template slot-scope="scope">
|
|
{{ scope.row.createdTime | dateFormat }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="updatedTime" label="更新时间">
|
|
<template slot-scope="scope">
|
|
{{ scope.row.updatedTime | dateFormat }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="280">
|
|
<template slot-scope="scope">
|
|
<el-button
|
|
size="mini"
|
|
type="primary"
|
|
icon="el-icon-edit"
|
|
@click="handleEdit(scope.row)"
|
|
>
|
|
编辑
|
|
</el-button>
|
|
<el-button
|
|
size="mini"
|
|
type="success"
|
|
icon="el-icon-s-tools"
|
|
@click="handleAssignTunnel(scope.row)"
|
|
>
|
|
分配
|
|
</el-button>
|
|
<el-button
|
|
size="mini"
|
|
type="danger"
|
|
icon="el-icon-delete"
|
|
@click="handleDelete(scope.row)"
|
|
>
|
|
删除
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 分页组件 -->
|
|
<div class="pagination-container">
|
|
<el-pagination
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange"
|
|
:current-page="pagination.current"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:page-size="pagination.size"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="pagination.total"
|
|
>
|
|
</el-pagination>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 新增/编辑用户对话框 -->
|
|
<el-dialog
|
|
:title="dialogTitle"
|
|
:visible.sync="dialogVisible"
|
|
width="500px"
|
|
@close="resetForm"
|
|
>
|
|
<el-form
|
|
:model="userForm"
|
|
:rules="rules"
|
|
ref="userForm"
|
|
label-width="80px"
|
|
>
|
|
<el-form-item label="姓名" prop="name">
|
|
<el-input
|
|
v-model="userForm.name"
|
|
placeholder="请输入姓名"
|
|
clearable
|
|
></el-input>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="用户名" prop="user">
|
|
<el-input
|
|
v-model="userForm.user"
|
|
placeholder="请输入用户名"
|
|
clearable
|
|
></el-input>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="密码" prop="pwd">
|
|
<el-input
|
|
v-model="userForm.pwd"
|
|
type="password"
|
|
:placeholder="isEdit ? '留空则不修改密码' : '请输入密码'"
|
|
clearable
|
|
show-password
|
|
></el-input>
|
|
</el-form-item>
|
|
|
|
<el-row :gutter="20">
|
|
<el-col :span="12">
|
|
<el-form-item label="流量(GB)" prop="flow">
|
|
<el-input-number
|
|
v-model="userForm.flow"
|
|
:min="0"
|
|
:max="99999"
|
|
placeholder="请输入流量"
|
|
style="width: 100%;"
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="转发数量" prop="num">
|
|
<el-input-number
|
|
v-model="userForm.num"
|
|
:min="0"
|
|
:max="9999"
|
|
placeholder="请输入转发数量"
|
|
style="width: 100%;"
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-form-item label="过期时间" prop="expTime">
|
|
<el-date-picker
|
|
v-model="userForm.expTime"
|
|
type="datetime"
|
|
placeholder="选择过期时间"
|
|
style="width: 100%;"
|
|
:picker-options="pickerOptions"
|
|
>
|
|
</el-date-picker>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="流量重置日期" prop="flowResetTime">
|
|
<el-select
|
|
v-model="userForm.flowResetTime"
|
|
placeholder="选择每月重置日期"
|
|
style="width: 100%;"
|
|
>
|
|
<el-option
|
|
v-for="day in 31"
|
|
:key="day"
|
|
:label="`每月${day}号(0点重置)`"
|
|
:value="day"
|
|
></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
<el-radio-group v-model="userForm.status">
|
|
<el-radio :label="1">正常</el-radio>
|
|
<el-radio :label="0">禁用</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
|
<el-button
|
|
type="primary"
|
|
@click="handleSubmit"
|
|
:loading="submitLoading"
|
|
>
|
|
{{ submitLoading ? '提交中...' : '确 定' }}
|
|
</el-button>
|
|
</span>
|
|
</el-dialog>
|
|
|
|
<!-- 用户隧道权限分配对话框 -->
|
|
<el-dialog
|
|
:title="`用户 ${currentUser.name} 的隧道权限管理`"
|
|
:visible.sync="tunnelDialogVisible"
|
|
width="50%"
|
|
@close="resetTunnelDialog"
|
|
>
|
|
<!-- 分配新隧道权限 -->
|
|
<div class="assign-section">
|
|
<h4>分配新隧道权限</h4>
|
|
<el-form :model="tunnelForm" :rules="tunnelRules" ref="tunnelForm" label-width="120px">
|
|
<el-row :gutter="20">
|
|
<el-col :span="12">
|
|
<el-form-item label="选择隧道" prop="tunnelId">
|
|
<el-select
|
|
v-model="tunnelForm.tunnelId"
|
|
placeholder="请选择隧道"
|
|
style="width: 100%;"
|
|
>
|
|
<el-option
|
|
v-for="tunnel in availableTunnels"
|
|
:key="tunnel.id"
|
|
:label="`${tunnel.name}`"
|
|
:value="tunnel.id"
|
|
></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="限速规则" prop="speedId">
|
|
<el-select
|
|
v-model="tunnelForm.speedId"
|
|
placeholder="请选择限速规则"
|
|
style="width: 100%;"
|
|
clearable
|
|
>
|
|
<el-option
|
|
label="不限速"
|
|
:value="null"
|
|
></el-option>
|
|
<el-option
|
|
v-for="speedLimit in speedLimitList"
|
|
:key="speedLimit.id"
|
|
:label="`${speedLimit.name}`"
|
|
:value="speedLimit.id"
|
|
></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="20">
|
|
<el-col :span="12">
|
|
<el-form-item label="流量限制(GB)" prop="flow">
|
|
<el-input-number
|
|
v-model="tunnelForm.flow"
|
|
:min="0"
|
|
:max="99999"
|
|
placeholder="流量限制"
|
|
style="width: 100%;"
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="转发数量" prop="num">
|
|
<el-input-number
|
|
v-model="tunnelForm.num"
|
|
:min="0"
|
|
:max="9999"
|
|
placeholder="转发数量"
|
|
style="width: 100%;"
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="20">
|
|
<el-col :span="12">
|
|
<el-form-item label="流量重置日期" prop="flowResetTime">
|
|
<el-select
|
|
v-model="tunnelForm.flowResetTime"
|
|
placeholder="选择每月重置日期"
|
|
style="width: 100%;"
|
|
>
|
|
<el-option
|
|
v-for="day in 31"
|
|
:key="day"
|
|
:label="`每月${day}号(0点重置)`"
|
|
:value="day"
|
|
></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="到期时间" prop="expTime">
|
|
<el-date-picker
|
|
v-model="tunnelForm.expTime"
|
|
type="datetime"
|
|
placeholder="选择到期时间"
|
|
style="width: 100%;"
|
|
:picker-options="pickerOptions"
|
|
>
|
|
</el-date-picker>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-form item>
|
|
<el-button
|
|
type="primary"
|
|
@click="handleAssignTunnelPermission"
|
|
:loading="assignLoading"
|
|
>
|
|
分配权限
|
|
</el-button>
|
|
</el-form>
|
|
</el-form>
|
|
</div>
|
|
|
|
<el-divider></el-divider>
|
|
|
|
<!-- 已有隧道权限列表 -->
|
|
<div class="permission-list-section">
|
|
<h4>已有隧道权限</h4>
|
|
<el-table
|
|
:data="userTunnelList"
|
|
v-loading="tunnelListLoading"
|
|
border
|
|
style="width: 100%"
|
|
>
|
|
<el-table-column prop="tunnelName" label="隧道名称"></el-table-column>
|
|
<el-table-column label="状态" width="80">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="small">
|
|
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="流量统计" width="180">
|
|
<template slot-scope="scope">
|
|
<div class="tunnel-flow-stats">
|
|
<div class="tunnel-flow-item">
|
|
<span class="tunnel-flow-label">限制:</span>
|
|
<span class="tunnel-flow-value">{{ formatFlow(scope.row.flow, 'gb') }}</span>
|
|
</div>
|
|
<div class="tunnel-flow-item">
|
|
<span class="tunnel-flow-label">已用:</span>
|
|
<span class="tunnel-flow-value used">{{ formatFlow(calculateTunnelUsedFlow(scope.row)) }}</span>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="num" label="转发数量" width="100"></el-table-column>
|
|
<el-table-column prop="speedLimitName" label="限速规则" width="150">
|
|
<template slot-scope="scope">
|
|
<el-tag v-if="scope.row.speedLimitName" type="warning">
|
|
{{ scope.row.speedLimitName }}
|
|
</el-tag>
|
|
<el-tag v-else type="success">不限速</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="flowResetTime" label="流量重置日期" width="130">
|
|
<template slot-scope="scope">
|
|
每月{{ scope.row.flowResetTime }}号
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="expTime" label="到期时间" width="180">
|
|
<template slot-scope="scope">
|
|
{{ scope.row.expTime | dateFormat }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150">
|
|
<template slot-scope="scope">
|
|
<el-button
|
|
size="mini"
|
|
type="primary"
|
|
icon="el-icon-edit"
|
|
@click="handleEditTunnelPermission(scope.row)"
|
|
>
|
|
</el-button>
|
|
<el-button
|
|
size="mini"
|
|
type="danger"
|
|
icon="el-icon-delete"
|
|
@click="handleRemoveTunnelPermission(scope.row)"
|
|
>
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
<el-button @click="tunnelDialogVisible = false">关 闭</el-button>
|
|
</span>
|
|
</el-dialog>
|
|
|
|
<!-- 编辑隧道权限对话框 -->
|
|
<el-dialog
|
|
:title="`编辑隧道权限 - ${editTunnelForm.tunnelName}`"
|
|
:visible.sync="editTunnelDialogVisible"
|
|
width="50%"
|
|
@close="resetEditTunnelForm"
|
|
>
|
|
<el-form :model="editTunnelForm" :rules="editTunnelRules" ref="editTunnelForm" label-width="120px">
|
|
<el-row :gutter="20">
|
|
<el-col :span="12">
|
|
<el-form-item label="流量限制(GB)" prop="flow">
|
|
<el-input-number
|
|
v-model="editTunnelForm.flow"
|
|
:min="0"
|
|
:max="99999"
|
|
placeholder="流量限制"
|
|
style="width: 100%;"
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="转发数量" prop="num">
|
|
<el-input-number
|
|
v-model="editTunnelForm.num"
|
|
:min="0"
|
|
:max="9999"
|
|
placeholder="转发数量"
|
|
style="width: 100%;"
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-form-item label="限速规则" prop="speedId">
|
|
<el-select
|
|
v-model="editTunnelForm.speedId"
|
|
placeholder="请选择限速规则"
|
|
style="width: 100%;"
|
|
clearable
|
|
>
|
|
<el-option
|
|
label="不限速"
|
|
:value="null"
|
|
></el-option>
|
|
<el-option
|
|
v-for="speedLimit in speedLimitList"
|
|
:key="speedLimit.id"
|
|
:label="`${speedLimit.name}`"
|
|
:value="speedLimit.id"
|
|
></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="流量重置日期" prop="flowResetTime">
|
|
<el-select
|
|
v-model="editTunnelForm.flowResetTime"
|
|
placeholder="选择每月重置日期"
|
|
style="width: 100%;"
|
|
>
|
|
<el-option
|
|
v-for="day in 31"
|
|
:key="day"
|
|
:label="`每月${day}号(0点重置)`"
|
|
:value="day"
|
|
></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="到期时间" prop="expTime">
|
|
<el-date-picker
|
|
v-model="editTunnelForm.expTime"
|
|
type="datetime"
|
|
placeholder="选择到期时间"
|
|
style="width: 100%;"
|
|
:picker-options="pickerOptions"
|
|
>
|
|
</el-date-picker>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
<el-button @click="editTunnelDialogVisible = false">取 消</el-button>
|
|
<el-button
|
|
type="primary"
|
|
@click="handleUpdateTunnelPermission"
|
|
:loading="updateLoading"
|
|
>
|
|
确 定
|
|
</el-button>
|
|
</span>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { getAllUsers, createUser, updateUser, deleteUser, getTunnelList, assignUserTunnel, getUserTunnelList, removeUserTunnel, updateUserTunnelFlow, updateUserTunnel, getSpeedLimitList } from "@/api";
|
|
|
|
export default {
|
|
name: "User",
|
|
data() {
|
|
return {
|
|
userList: [],
|
|
loading: false,
|
|
dialogVisible: false,
|
|
dialogTitle: '',
|
|
isEdit: false,
|
|
submitLoading: false,
|
|
searchKeyword: '',
|
|
pagination: {
|
|
current: 1,
|
|
size: 10,
|
|
total: 0
|
|
},
|
|
userForm: {
|
|
id: null,
|
|
name: '',
|
|
user: '',
|
|
pwd: '',
|
|
status: 1,
|
|
flow: 0,
|
|
num: 0,
|
|
expTime: null,
|
|
flowResetTime: 1
|
|
},
|
|
rules: {
|
|
name: [
|
|
{ required: true, message: '请输入姓名', trigger: 'blur' }
|
|
],
|
|
user: [
|
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
|
{ min: 3, message: '用户名长度至少3位', trigger: 'blur' }
|
|
],
|
|
pwd: [
|
|
{
|
|
validator: (rule, value, callback) => {
|
|
if (!this.isEdit && (!value || value.trim() === '')) {
|
|
callback(new Error('请输入密码'));
|
|
} else if (value && value.length > 0 && value.length < 6) {
|
|
callback(new Error('密码长度至少6位'));
|
|
} else {
|
|
callback();
|
|
}
|
|
},
|
|
trigger: 'blur'
|
|
}
|
|
],
|
|
status: [
|
|
{ required: true, message: '请选择状态', trigger: 'change' }
|
|
],
|
|
flow: [
|
|
{ required: true, message: '请输入流量', trigger: 'blur' }
|
|
],
|
|
num: [
|
|
{ required: true, message: '请输入转发数量', trigger: 'blur' }
|
|
],
|
|
expTime: [
|
|
{ required: true, message: '请选择过期时间', trigger: 'change' }
|
|
],
|
|
flowResetTime: [
|
|
{ required: true, message: '请选择流量重置日期', trigger: 'change' }
|
|
]
|
|
},
|
|
// 隧道权限管理相关数据
|
|
tunnelDialogVisible: false,
|
|
currentUser: {},
|
|
userTunnelList: [],
|
|
tunnelListLoading: false,
|
|
assignLoading: false,
|
|
allTunnels: [],
|
|
tunnelForm: {
|
|
tunnelId: null,
|
|
flow: 100,
|
|
flowResetTime: 1,
|
|
expTime: null,
|
|
speedId: null,
|
|
num: 0
|
|
},
|
|
tunnelRules: {
|
|
tunnelId: [
|
|
{ required: true, message: '请选择隧道', trigger: 'change' }
|
|
],
|
|
flow: [
|
|
{ required: true, message: '请输入流量限制', trigger: 'blur' }
|
|
],
|
|
num: [
|
|
{ required: true, message: '请输入转发数量', trigger: 'blur' }
|
|
],
|
|
flowResetTime: [
|
|
{ required: true, message: '请选择流量重置日期', trigger: 'change' }
|
|
],
|
|
expTime: [
|
|
{ required: true, message: '请选择到期时间', trigger: 'change' }
|
|
]
|
|
},
|
|
pickerOptions: {
|
|
disabledDate(date) {
|
|
return date.getTime() < Date.now() - 86400000; // 禁用今天之前的日期
|
|
}
|
|
},
|
|
editTunnelDialogVisible: false,
|
|
editTunnelForm: {
|
|
id: null,
|
|
tunnelId: null,
|
|
tunnelName: '',
|
|
flow: 100,
|
|
flowResetTime: 1,
|
|
expTime: null,
|
|
speedId: null,
|
|
num: 0
|
|
},
|
|
editTunnelRules: {
|
|
flow: [
|
|
{ required: true, message: '请输入流量限制', trigger: 'blur' }
|
|
],
|
|
num: [
|
|
{ required: true, message: '请输入转发数量', trigger: 'blur' }
|
|
],
|
|
flowResetTime: [
|
|
{ required: true, message: '请选择流量重置日期', trigger: 'change' }
|
|
],
|
|
expTime: [
|
|
{ required: true, message: '请选择到期时间', trigger: 'change' }
|
|
]
|
|
},
|
|
updateLoading: false,
|
|
speedLimitList: []
|
|
};
|
|
},
|
|
computed: {
|
|
// 可分配的隧道列表(排除已分配的)
|
|
availableTunnels() {
|
|
const assignedTunnelIds = this.userTunnelList.map(item => item.tunnelId);
|
|
return this.allTunnels.filter(tunnel => !assignedTunnelIds.includes(tunnel.id));
|
|
}
|
|
},
|
|
mounted() {
|
|
this.getUserList();
|
|
this.getTunnelList();
|
|
this.getSpeedLimitList();
|
|
},
|
|
methods: {
|
|
// 获取用户列表
|
|
getUserList() {
|
|
this.loading = true;
|
|
const params = {
|
|
current: this.pagination.current,
|
|
size: this.pagination.size,
|
|
keyword: this.searchKeyword
|
|
};
|
|
|
|
getAllUsers(params).then(res => {
|
|
this.loading = false;
|
|
if (res.code !== 0) {
|
|
return this.$message.error(res.msg);
|
|
}
|
|
|
|
const data = res.data || {};
|
|
this.userList = data.records || [];
|
|
this.pagination.total = data.total || 0;
|
|
this.pagination.current = data.current || 1;
|
|
this.pagination.size = data.size || 10;
|
|
}).catch(() => {
|
|
this.loading = false;
|
|
this.$message.error('获取用户列表失败');
|
|
});
|
|
},
|
|
|
|
// 获取隧道列表
|
|
getTunnelList() {
|
|
getTunnelList().then(res => {
|
|
if (res.code === 0) {
|
|
this.allTunnels = res.data || [];
|
|
}
|
|
}).catch(() => {
|
|
console.error('获取隧道列表失败');
|
|
});
|
|
},
|
|
|
|
// 搜索
|
|
handleSearch() {
|
|
this.pagination.current = 1; // 重置到第一页
|
|
this.getUserList();
|
|
},
|
|
|
|
// 每页条数改变
|
|
handleSizeChange(size) {
|
|
this.pagination.size = size;
|
|
this.pagination.current = 1; // 重置到第一页
|
|
this.getUserList();
|
|
},
|
|
|
|
// 当前页改变
|
|
handleCurrentChange(current) {
|
|
this.pagination.current = current;
|
|
this.getUserList();
|
|
},
|
|
|
|
// 新增用户
|
|
handleAdd() {
|
|
this.dialogTitle = '新增用户';
|
|
this.isEdit = false;
|
|
this.dialogVisible = true;
|
|
},
|
|
|
|
// 编辑用户
|
|
handleEdit(row) {
|
|
this.dialogTitle = '编辑用户';
|
|
this.isEdit = true;
|
|
this.userForm = {
|
|
id: row.id,
|
|
name: row.name,
|
|
user: row.user,
|
|
pwd: '',
|
|
status: row.status,
|
|
flow: row.flow,
|
|
num: row.num,
|
|
expTime: row.expTime ? new Date(row.expTime) : null,
|
|
flowResetTime: row.flowResetTime || 1
|
|
};
|
|
this.dialogVisible = true;
|
|
},
|
|
|
|
// 分配隧道权限
|
|
handleAssignTunnel(row) {
|
|
this.currentUser = row;
|
|
this.tunnelDialogVisible = true;
|
|
this.getUserTunnelList();
|
|
// 设置默认时间为下个月的今天
|
|
this.setDefaultTimes();
|
|
},
|
|
|
|
// 设置默认时间为下个月的今天
|
|
setDefaultTimes() {
|
|
const now = new Date();
|
|
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate(), now.getHours(), now.getMinutes());
|
|
// 流量重置时间现在是数字选择,不需要设置
|
|
this.tunnelForm.expTime = nextMonth;
|
|
},
|
|
|
|
// 删除用户
|
|
handleDelete(row) {
|
|
this.$confirm(`确定要删除用户 "${row.name}" 吗?`, '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
deleteUser(row.id).then(res => {
|
|
if (res.code !== 0) {
|
|
return this.$message.error(res.msg);
|
|
}
|
|
this.$message.success('删除成功');
|
|
this.getUserList();
|
|
}).catch(() => {
|
|
this.$message.error('删除失败');
|
|
});
|
|
});
|
|
},
|
|
|
|
// 提交表单
|
|
handleSubmit() {
|
|
this.$refs.userForm.validate(valid => {
|
|
if (valid) {
|
|
this.submitLoading = true;
|
|
|
|
const apiCall = this.isEdit ? updateUser : createUser;
|
|
const submitData = { ...this.userForm };
|
|
|
|
// 如果是编辑且密码为空,则不提交密码字段
|
|
if (this.isEdit && !submitData.pwd) {
|
|
delete submitData.pwd;
|
|
}
|
|
|
|
// 转换过期时间为时间戳
|
|
if (submitData.expTime) {
|
|
submitData.expTime = submitData.expTime.getTime();
|
|
}
|
|
|
|
// 流量重置时间现在是数字(几号),直接使用
|
|
|
|
apiCall(submitData).then(res => {
|
|
this.submitLoading = false;
|
|
if (res.code !== 0) {
|
|
return this.$message.error(res.msg);
|
|
}
|
|
|
|
this.$message.success(this.isEdit ? '更新成功' : '创建成功');
|
|
this.dialogVisible = false;
|
|
this.getUserList();
|
|
}).catch(() => {
|
|
this.submitLoading = false;
|
|
this.$message.error(this.isEdit ? '更新失败' : '创建失败');
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
// ============ 隧道权限管理相关方法 ============
|
|
|
|
// 获取用户隧道权限列表
|
|
getUserTunnelList() {
|
|
this.tunnelListLoading = true;
|
|
getUserTunnelList({ userId: this.currentUser.id }).then(res => {
|
|
this.tunnelListLoading = false;
|
|
if (res.code === 0) {
|
|
this.userTunnelList = res.data || [];
|
|
} else {
|
|
this.$message.error(res.msg || '获取隧道权限列表失败');
|
|
}
|
|
}).catch(() => {
|
|
this.tunnelListLoading = false;
|
|
this.$message.error('获取隧道权限列表失败');
|
|
});
|
|
},
|
|
|
|
// 分配隧道权限
|
|
handleAssignTunnelPermission() {
|
|
this.$refs.tunnelForm.validate(valid => {
|
|
if (valid) {
|
|
this.assignLoading = true;
|
|
const data = {
|
|
userId: this.currentUser.id,
|
|
tunnelId: this.tunnelForm.tunnelId,
|
|
flow: this.tunnelForm.flow,
|
|
flowResetTime: this.tunnelForm.flowResetTime,
|
|
expTime: this.tunnelForm.expTime.getTime(),
|
|
speedId: this.tunnelForm.speedId,
|
|
num: this.tunnelForm.num
|
|
};
|
|
|
|
assignUserTunnel(data).then(res => {
|
|
this.assignLoading = false;
|
|
if (res.code === 0) {
|
|
this.$message.success('分配成功');
|
|
this.resetTunnelForm();
|
|
this.getUserTunnelList(); // 刷新列表
|
|
} else {
|
|
this.$message.error(res.msg || '分配失败');
|
|
}
|
|
}).catch(() => {
|
|
this.assignLoading = false;
|
|
this.$message.error('分配失败');
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
// 删除隧道权限
|
|
handleRemoveTunnelPermission(row) {
|
|
this.$confirm(`确定要删除该隧道权限吗?`, '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
removeUserTunnel({ id: row.id }).then(res => {
|
|
if (res.code === 0) {
|
|
this.$message.success('删除成功');
|
|
this.getUserTunnelList(); // 刷新列表
|
|
} else {
|
|
this.$message.error(res.msg || '删除失败');
|
|
}
|
|
}).catch(() => {
|
|
this.$message.error('删除失败');
|
|
});
|
|
});
|
|
},
|
|
|
|
// 重置表单
|
|
resetForm() {
|
|
this.userForm = {
|
|
id: null,
|
|
name: '',
|
|
user: '',
|
|
pwd: '',
|
|
status: 1,
|
|
flow: 0,
|
|
num: 0,
|
|
expTime: null,
|
|
flowResetTime: 1
|
|
};
|
|
if (this.$refs.userForm) {
|
|
this.$refs.userForm.resetFields();
|
|
}
|
|
},
|
|
|
|
// 重置隧道权限对话框
|
|
resetTunnelDialog() {
|
|
this.currentUser = {};
|
|
this.userTunnelList = [];
|
|
this.resetTunnelForm();
|
|
},
|
|
|
|
// 重置隧道分配表单
|
|
resetTunnelForm() {
|
|
this.tunnelForm = {
|
|
tunnelId: null,
|
|
flow: 100,
|
|
flowResetTime: 1,
|
|
expTime: null,
|
|
speedId: null,
|
|
num: 0
|
|
};
|
|
if (this.$refs.tunnelForm) {
|
|
this.$refs.tunnelForm.resetFields();
|
|
}
|
|
// 重置后设置默认时间
|
|
this.setDefaultTimes();
|
|
},
|
|
|
|
// 编辑隧道权限
|
|
handleEditTunnelPermission(row) {
|
|
this.editTunnelForm = {
|
|
id: row.id,
|
|
tunnelId: row.tunnelId,
|
|
tunnelName: row.tunnelName,
|
|
flow: row.flow,
|
|
flowResetTime: row.flowResetTime,
|
|
expTime: new Date(row.expTime),
|
|
speedId: row.speedId,
|
|
num: row.num
|
|
};
|
|
this.editTunnelDialogVisible = true;
|
|
},
|
|
|
|
// 更新隧道权限
|
|
handleUpdateTunnelPermission() {
|
|
this.$refs.editTunnelForm.validate(valid => {
|
|
if (valid) {
|
|
this.updateLoading = true;
|
|
const data = {
|
|
id: this.editTunnelForm.id,
|
|
flow: this.editTunnelForm.flow,
|
|
flowResetTime: this.editTunnelForm.flowResetTime,
|
|
expTime: this.editTunnelForm.expTime.getTime(),
|
|
speedId: this.editTunnelForm.speedId,
|
|
num: this.editTunnelForm.num
|
|
};
|
|
|
|
updateUserTunnel(data).then(res => {
|
|
this.updateLoading = false;
|
|
if (res.code === 0) {
|
|
this.$message.success('更新成功');
|
|
this.editTunnelDialogVisible = false;
|
|
this.getUserTunnelList();
|
|
} else {
|
|
this.$message.error(res.msg || '更新失败');
|
|
}
|
|
}).catch(() => {
|
|
this.updateLoading = false;
|
|
this.$message.error('更新失败');
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
// 重置编辑隧道权限表单
|
|
resetEditTunnelForm() {
|
|
this.editTunnelForm = {
|
|
id: null,
|
|
tunnelId: null,
|
|
tunnelName: '',
|
|
flow: 100,
|
|
flowResetTime: 1,
|
|
expTime: null,
|
|
speedId: null,
|
|
num: 0
|
|
};
|
|
if (this.$refs.editTunnelForm) {
|
|
this.$refs.editTunnelForm.resetFields();
|
|
}
|
|
},
|
|
|
|
// 获取限速规则列表
|
|
getSpeedLimitList() {
|
|
getSpeedLimitList().then(res => {
|
|
if (res.code === 0) {
|
|
this.speedLimitList = res.data || [];
|
|
}
|
|
}).catch(() => {
|
|
console.error('获取限速规则列表失败');
|
|
});
|
|
},
|
|
|
|
formatFlow(value, unit = 'bytes') {
|
|
if (unit === 'gb') {
|
|
// flow字段,单位是GB
|
|
return value + ' GB';
|
|
} else {
|
|
// inFlow、outFlow字段,单位是字节
|
|
if (value === 0) return '0 B';
|
|
if (value < 1024) return value + ' B';
|
|
if (value < 1024 * 1024) return (value / 1024).toFixed(2) + ' KB';
|
|
if (value < 1024 * 1024 * 1024) return (value / (1024 * 1024)).toFixed(2) + ' MB';
|
|
return (value / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
}
|
|
},
|
|
|
|
calculateFlowPercentage(row) {
|
|
const totalUsedBytes = this.calculateTunnelUsedFlow(row);
|
|
if (row.flow === 0) return 0;
|
|
const flowLimitBytes = row.flow * 1024 * 1024 * 1024; // 将GB转换为字节
|
|
const percentage = (totalUsedBytes / flowLimitBytes) * 100;
|
|
return Math.min(percentage, 100); // 限制最大值为100%
|
|
},
|
|
|
|
getFlowStatus(row) {
|
|
const percentage = this.calculateFlowPercentage(row);
|
|
if (percentage >= 90) return 'exception';
|
|
if (percentage >= 70) return 'warning';
|
|
return 'success';
|
|
},
|
|
|
|
getExpTimeClass(expTime) {
|
|
const now = new Date();
|
|
const expDate = new Date(expTime);
|
|
|
|
if (expDate < now) {
|
|
// 已过期
|
|
return 'exp-time-expired';
|
|
}
|
|
|
|
const diffTime = expDate - now;
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffDays <= 7) {
|
|
return 'exp-time-soon';
|
|
} else if (diffDays <= 30) {
|
|
return 'exp-time-mid';
|
|
} else {
|
|
return 'exp-time-long';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 计算隧道已用流量
|
|
* 根据隧道的流量计算类型(tunnelFlow)决定单向还是双向计算
|
|
*/
|
|
calculateTunnelUsedFlow(tunnel) {
|
|
if (!tunnel) return 0;
|
|
|
|
const inFlow = tunnel.inFlow || 0; // 下载流量
|
|
const outFlow = tunnel.outFlow || 0; // 上传流量
|
|
|
|
// tunnelFlow: 1-单向计算(仅上传),2-双向计算(上传+下载)
|
|
if (tunnel.tunnelFlow === 1) {
|
|
// 单向计算:只计算上传流量(outFlow)
|
|
return outFlow;
|
|
} else {
|
|
// 双向计算或默认:计算上传和下载的总流量
|
|
return inFlow + outFlow;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 计算用户总已用流量
|
|
* 直接使用后端计算好的用户流量统计
|
|
*/
|
|
calculateUserTotalUsedFlow(user) {
|
|
// 后端已经根据隧道类型计算了实际流量,直接使用
|
|
return (user.inFlow || 0) + (user.outFlow || 0);
|
|
}
|
|
},
|
|
|
|
filters: {
|
|
dateFormat(value) {
|
|
if (!value) return '';
|
|
const date = new Date(value);
|
|
return date.toLocaleString();
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-container {
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding: 20px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin: 0;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.add-btn {
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.table-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
padding: 20px;
|
|
}
|
|
|
|
.pagination-container {
|
|
margin-top: 20px;
|
|
text-align: right;
|
|
}
|
|
|
|
/* 对话框样式调整 */
|
|
.dialog-footer {
|
|
text-align: right;
|
|
}
|
|
|
|
/* 隧道权限管理样式 */
|
|
.assign-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.assign-section h4,
|
|
.permission-list-section h4 {
|
|
margin-bottom: 15px;
|
|
color: #333;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.permission-list-section {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 768px) {
|
|
.user-container {
|
|
padding: 10px;
|
|
}
|
|
|
|
.page-header {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.header-actions {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.page-title {
|
|
text-align: center;
|
|
}
|
|
|
|
.pagination-container {
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
/* 表格样式优化 */
|
|
.table-container >>> .el-table th {
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
.table-container >>> .el-table--border td,
|
|
.table-container >>> .el-table--border th {
|
|
border-right: 1px solid #ebeef5;
|
|
}
|
|
|
|
/* 按钮样式调整 */
|
|
.table-container >>> .el-button--mini {
|
|
padding: 5px 10px;
|
|
margin: 0 2px;
|
|
}
|
|
|
|
/* 分页样式 */
|
|
.pagination-container >>> .el-pagination {
|
|
padding: 0;
|
|
}
|
|
|
|
/* 流量统计样式 */
|
|
.flow-stats {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.flow-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.flow-label {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.flow-value {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.flow-value.used {
|
|
color: #F56C6C;
|
|
}
|
|
|
|
.flow-value.in {
|
|
color: #67C23A;
|
|
}
|
|
|
|
.flow-value.out {
|
|
color: #E6A23C;
|
|
}
|
|
|
|
/* 隧道流量统计样式 */
|
|
.tunnel-flow-stats {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.tunnel-flow-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.tunnel-flow-label {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.tunnel-flow-value {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.tunnel-flow-value.used {
|
|
color: #F56C6C;
|
|
}
|
|
|
|
.tunnel-flow-usage {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* 过期时间样式 */
|
|
.exp-time-expired {
|
|
color: #F56C6C;
|
|
font-weight: 700;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.exp-time-soon {
|
|
color: #F56C6C;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.exp-time-mid {
|
|
color: #E6A23C;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.exp-time-long {
|
|
color: #67C23A;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.no-exptime {
|
|
color: #909399;
|
|
font-style: italic;
|
|
}
|
|
|
|
.no-reset-time {
|
|
color: #909399;
|
|
font-style: italic;
|
|
}
|
|
</style> |