feat: 用户列表代理商查询、商品续费、VNC节点access_mode、IDC信息展示、新购订单table修复
- 用户列表新增通过IDC代理商uid查询用户信息 - 用户商品详情新增续费功能(admin/good/user_goods/renew) - VNC节点管理新增access_mode和use_ssl参数 - 用户详情左栏新增IDC平台信息展示 - 新购订单选择商品后所属表不再被覆盖,固定为good Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -105,6 +105,12 @@ export const getUserOperationRecord = (data) => {
|
|||||||
export const mockUserLogin = (data) => {
|
export const mockUserLogin = (data) => {
|
||||||
return http2.get('/api/v1/admin/user/user/simulation_login?user_id='+data.user_id)
|
return http2.get('/api/v1/admin/user/user/simulation_login?user_id='+data.user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**通过IDC代理商uid获取用户信息 */
|
||||||
|
export const getUserByIdcAgentUid = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/user/idc_agent/user', { params })
|
||||||
|
}
|
||||||
|
|
||||||
/**新建任务 */
|
/**新建任务 */
|
||||||
export const createTask = (data) => {
|
export const createTask = (data) => {
|
||||||
return http2.post('/api/v1/admin/user/user/create', data,{
|
return http2.post('/api/v1/admin/user/user/create', data,{
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export const getUserGoodsDetail = (params) => http2.get(`${GOODS_BASE}/detail`,
|
|||||||
export const createUserGoods = (data) => http2.post(`${GOODS_BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
export const createUserGoods = (data) => http2.post(`${GOODS_BASE}/create`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
export const updateUserGoods = (data) => http2.post(`${GOODS_BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
export const updateUserGoods = (data) => http2.post(`${GOODS_BASE}/update`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, { params })
|
export const deleteUserGoods = (params) => http2.delete(`${GOODS_BASE}/delete`, { params })
|
||||||
|
export const renewUserGoods = (data) => http2.post(`${GOODS_BASE}/renew`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
|
|
||||||
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
|
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
|
||||||
export const getUserVmTrafficHourly = (params) => http2.get(`${BASE}/traffic_hourly`, { params })
|
export const getUserVmTrafficHourly = (params) => http2.get(`${BASE}/traffic_hourly`, { params })
|
||||||
|
|||||||
@@ -1285,7 +1285,7 @@ const clearUser = () => {
|
|||||||
const handleProductSelect = (product) => {
|
const handleProductSelect = (product) => {
|
||||||
orderForm.commodity_id = product.id
|
orderForm.commodity_id = product.id
|
||||||
selectedProductInfo.value = product
|
selectedProductInfo.value = product
|
||||||
if (product.table) orderForm.table = product.table
|
if (orderForm.type !== 'create' && product.table) orderForm.table = product.table
|
||||||
orderForm.plan_id = null
|
orderForm.plan_id = null
|
||||||
planFixedArgs.value = []
|
planFixedArgs.value = []
|
||||||
// 自动生成订单名称:商品名-时间戳
|
// 自动生成订单名称:商品名-时间戳
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<el-button plain @click="loadDetail" :loading="loading">
|
<el-button plain @click="loadDetail" :loading="loading">
|
||||||
<el-icon><Refresh /></el-icon> 刷新
|
<el-icon><Refresh /></el-icon> 刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="success" plain @click="openRenew" :disabled="!detail || isDeleted">
|
||||||
|
续费
|
||||||
|
</el-button>
|
||||||
<el-button type="primary" plain @click="openEdit" :disabled="!detail">
|
<el-button type="primary" plain @click="openEdit" :disabled="!detail">
|
||||||
<el-icon><Edit /></el-icon> 编辑
|
<el-icon><Edit /></el-icon> 编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -233,6 +236,35 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 续费弹窗 -->
|
||||||
|
<el-dialog v-model="renewVisible" title="用户商品续费" width="480px" destroy-on-close>
|
||||||
|
<el-form :model="renewForm" label-width="110px">
|
||||||
|
<el-form-item label="续费月数">
|
||||||
|
<el-input-number v-model="renewForm.purchase_duration" :min="1" :max="120" controls-position="right" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="续费单价">
|
||||||
|
<div class="unit-input-row">
|
||||||
|
<el-input-number v-model="renewForm.renew_price" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||||||
|
<span class="unit-text">元</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px">不填则使用商品默认续费价格</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="总计费用">
|
||||||
|
<span style="font-size: 16px; font-weight: 600; color: #409eff">¥{{ renewTotalPrice }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="直接完成支付">
|
||||||
|
<el-switch v-model="renewForm.complete_pay" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="renewForm.complete_pay" label="扣除用户余额">
|
||||||
|
<el-switch v-model="renewForm.deduct_balance" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="renewVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="renewLoading" @click="handleRenew">确认续费</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
<!-- 编辑弹窗 -->
|
||||||
<el-dialog v-model="editVisible" title="编辑用户商品" width="520px" destroy-on-close>
|
<el-dialog v-model="editVisible" title="编辑用户商品" width="520px" destroy-on-close>
|
||||||
<el-form :model="editForm" label-width="110px">
|
<el-form :model="editForm" label-width="110px">
|
||||||
@@ -289,7 +321,7 @@ import { ref, reactive, computed, onMounted, onActivated, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ArrowLeft, ArrowDown, Refresh, Edit, Delete } from '@element-plus/icons-vue'
|
import { ArrowLeft, ArrowDown, Refresh, Edit, Delete } from '@element-plus/icons-vue'
|
||||||
import { getUserGoodsDetail, updateUserGoods, deleteUserGoods } from '@/api/admin/userVm'
|
import { getUserGoodsDetail, updateUserGoods, deleteUserGoods, renewUserGoods } from '@/api/admin/userVm'
|
||||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||||
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
|
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
|
||||||
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
|
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
|
||||||
@@ -417,6 +449,41 @@ const goToOrder = () => {
|
|||||||
router.push({ path: '/order/list', query: { order_id: oid } })
|
router.push({ path: '/order/list', query: { order_id: oid } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 续费 ----
|
||||||
|
const renewVisible = ref(false)
|
||||||
|
const renewLoading = ref(false)
|
||||||
|
const renewForm = reactive({ purchase_duration: 1, renew_price: 0, complete_pay: false, deduct_balance: false })
|
||||||
|
const renewTotalPrice = computed(() => {
|
||||||
|
const price = renewForm.renew_price || (detail.value?.renewPrice || detail.value?.renew_price || 0) / 100
|
||||||
|
return (price * renewForm.purchase_duration).toFixed(2)
|
||||||
|
})
|
||||||
|
const openRenew = () => {
|
||||||
|
const rawRenew = detail.value?.renewPrice || detail.value?.renew_price || 0
|
||||||
|
Object.assign(renewForm, { purchase_duration: 1, renew_price: rawRenew / 100, complete_pay: false, deduct_balance: false })
|
||||||
|
renewVisible.value = true
|
||||||
|
}
|
||||||
|
const handleRenew = async () => {
|
||||||
|
renewLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = { id: goodsId.value, purchase_duration: renewForm.purchase_duration }
|
||||||
|
if (renewForm.renew_price) data.renew_price = Math.round(renewForm.renew_price * 100)
|
||||||
|
if (renewForm.complete_pay) data.complete_pay = true
|
||||||
|
if (renewForm.complete_pay && renewForm.deduct_balance) data.deduct_balance = true
|
||||||
|
const res = await renewUserGoods(data)
|
||||||
|
if (res?.data?.code === 200) {
|
||||||
|
ElMessage.success('续费成功')
|
||||||
|
renewVisible.value = false
|
||||||
|
loadDetail()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(extractApiError(res?.data, '续费失败'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(extractApiError(e?.response?.data, '续费失败'))
|
||||||
|
} finally {
|
||||||
|
renewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const editVisible = ref(false)
|
const editVisible = ref(false)
|
||||||
const editForm = reactive({ note: '', renew_price: 0, base_price: 0, expire_time: '', item_id: 0, _serviceId: 0, _serviceName: '', _itemName: '' })
|
const editForm = reactive({ note: '', renew_price: 0, base_price: 0, expire_time: '', item_id: 0, _serviceId: 0, _serviceName: '', _itemName: '' })
|
||||||
const showVmSelector = ref(false)
|
const showVmSelector = ref(false)
|
||||||
|
|||||||
@@ -116,6 +116,20 @@
|
|||||||
<el-descriptions-item label="认证类型">{{ getRealNameTypeText(userInfo.RealName?.Type) }}</el-descriptions-item>
|
<el-descriptions-item label="认证类型">{{ getRealNameTypeText(userInfo.RealName?.Type) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="userInfo.idc_agent" class="info-section-card" shadow="never" style="margin-top: 20px;">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="title">IDC平台信息</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="1" border size="small">
|
||||||
|
<el-descriptions-item label="代理商UID">{{ userInfo.idc_agent.Uid || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="代理等级">{{ userInfo.idc_agent.AgentLevelName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="等级编号">{{ userInfo.idc_agent.AgentLevent ?? '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="绑定时间">{{ formatDate(userInfo.idc_agent.CreatedAt) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧记录栏 -->
|
<!-- 右侧记录栏 -->
|
||||||
|
|||||||
@@ -32,10 +32,23 @@
|
|||||||
@keyup.enter="handleJumpToUser"
|
@keyup.enter="handleJumpToUser"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="search-group">
|
||||||
|
<span class="search-label">代理商ID</span>
|
||||||
|
<el-input
|
||||||
|
v-model="agentUid"
|
||||||
|
placeholder="007idc代理商uid"
|
||||||
|
clearable
|
||||||
|
class="search-input-small"
|
||||||
|
@keyup.enter="handleSearchByAgentUid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="search-buttons">
|
<div class="search-buttons">
|
||||||
<el-button type="warning" @click="handleJumpToUser">
|
<el-button type="warning" @click="handleJumpToUser">
|
||||||
<el-icon><Position /></el-icon><span class="btn-text">跳转</span>
|
<el-icon><Position /></el-icon><span class="btn-text">跳转</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="info" @click="handleSearchByAgentUid" :loading="agentSearchLoading">
|
||||||
|
<el-icon><Search /></el-icon><span class="btn-text">代理商查询</span>
|
||||||
|
</el-button>
|
||||||
<el-button type="primary" @click="handleQuery">
|
<el-button type="primary" @click="handleQuery">
|
||||||
<el-icon><Search /></el-icon><span class="btn-text">查询</span>
|
<el-icon><Search /></el-icon><span class="btn-text">查询</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -653,7 +666,8 @@ import {
|
|||||||
updateUserGroup,
|
updateUserGroup,
|
||||||
updateUserRealName,
|
updateUserRealName,
|
||||||
getUserLoginRecord,
|
getUserLoginRecord,
|
||||||
getUserOperationRecord
|
getUserOperationRecord,
|
||||||
|
getUserByIdcAgentUid
|
||||||
} from '@/api/admin/user'
|
} from '@/api/admin/user'
|
||||||
import { getFileDetail } from '@/api/admin/file'
|
import { getFileDetail } from '@/api/admin/file'
|
||||||
import { closeAllMessage } from '../../utils/message'
|
import { closeAllMessage } from '../../utils/message'
|
||||||
@@ -663,6 +677,10 @@ const router = useRouter()
|
|||||||
// 跳转用户ID
|
// 跳转用户ID
|
||||||
const jumpUserId = ref('')
|
const jumpUserId = ref('')
|
||||||
|
|
||||||
|
// 代理商ID查询
|
||||||
|
const agentUid = ref('')
|
||||||
|
const agentSearchLoading = ref(false)
|
||||||
|
|
||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
key: '',
|
key: '',
|
||||||
@@ -887,6 +905,33 @@ const handleJumpToUser = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通过代理商ID查询用户
|
||||||
|
const handleSearchByAgentUid = async () => {
|
||||||
|
if (!agentUid.value || !agentUid.value.trim()) {
|
||||||
|
ElMessage.warning('请输入代理商ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agentSearchLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getUserByIdcAgentUid({ uid: agentUid.value.trim() })
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const userId = res.data.data.UserId
|
||||||
|
if (userId) {
|
||||||
|
router.push({ path: '/user/detail', query: { user_id: userId } })
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('未找到绑定该代理商ID的用户')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(res?.data?.message || '未找到绑定该代理商ID的用户')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.response?.data?.message || '查询失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
} finally {
|
||||||
|
agentSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 选择项变化
|
// 选择项变化
|
||||||
const handleSelectionChange = (selection) => {
|
const handleSelectionChange = (selection) => {
|
||||||
selectedRows.value = selection
|
selectedRows.value = selection
|
||||||
|
|||||||
@@ -39,6 +39,18 @@
|
|||||||
<span class="host-addr">{{ row.ip || '-' }}:{{ row.port || '-' }}</span>
|
<span class="host-addr">{{ row.ip || '-' }}:{{ row.port || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="访问方案" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.access_mode === 'token'" type="warning" size="small">Token传参</el-tag>
|
||||||
|
<el-tag v-else size="small">域名解析</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="SSL" width="70">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.use_ssl" type="success" size="small">是</el-tag>
|
||||||
|
<span v-else class="text-muted">否</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="Token" min-width="140" show-overflow-tooltip>
|
<el-table-column label="Token" min-width="140" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.token" class="token-mask">{{ maskToken(row.token) }}</span>
|
<span v-if="row.token" class="token-mask">{{ maskToken(row.token) }}</span>
|
||||||
@@ -81,7 +93,16 @@
|
|||||||
<el-input v-model="formData.port" placeholder="如 6080" />
|
<el-input v-model="formData.port" placeholder="如 6080" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Token">
|
<el-form-item label="Token">
|
||||||
<el-input v-model="formData.token" placeholder="认证Token(可选)" show-password />
|
<el-input v-model="formData.token" placeholder="节点鉴权Token(可选)" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="访问方案">
|
||||||
|
<el-select v-model="formData.access_mode" style="width: 100%">
|
||||||
|
<el-option label="域名解析 (domain)" value="domain" />
|
||||||
|
<el-option label="Token传参 (token)" value="token" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用SSL">
|
||||||
|
<el-switch v-model="formData.use_ssl" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -224,7 +245,7 @@ const handleSearch = () => { queryParams.page = 1; loadList() }
|
|||||||
const formDialogVisible = ref(false)
|
const formDialogVisible = ref(false)
|
||||||
const formType = ref('add')
|
const formType = ref('add')
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const formData = reactive({ id: undefined, name: '', ip: '', port: '', token: '' })
|
const formData = reactive({ id: undefined, name: '', ip: '', port: '', token: '', access_mode: 'domain', use_ssl: false })
|
||||||
const formRules = {
|
const formRules = {
|
||||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
ip: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
|
ip: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
|
||||||
@@ -233,14 +254,15 @@ const formRules = {
|
|||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
formType.value = 'add'
|
formType.value = 'add'
|
||||||
Object.assign(formData, { id: undefined, name: '', ip: '', port: '', token: '' })
|
Object.assign(formData, { id: undefined, name: '', ip: '', port: '', token: '', access_mode: 'domain', use_ssl: false })
|
||||||
formDialogVisible.value = true
|
formDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
formType.value = 'edit'
|
formType.value = 'edit'
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
id: row.id, name: row.name || '', ip: row.ip || '', port: row.port || '', token: row.token || ''
|
id: row.id, name: row.name || '', ip: row.ip || '', port: row.port || '', token: row.token || '',
|
||||||
|
access_mode: row.access_mode || 'domain', use_ssl: !!row.use_ssl
|
||||||
})
|
})
|
||||||
formDialogVisible.value = true
|
formDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -251,10 +273,12 @@ const handleSubmit = () => {
|
|||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
let res
|
let res
|
||||||
|
const payload = { service_id: serviceId.value, name: formData.name, ip: formData.ip, port: formData.port, token: formData.token, access_mode: formData.access_mode, use_ssl: formData.use_ssl }
|
||||||
if (formType.value === 'add') {
|
if (formType.value === 'add') {
|
||||||
res = await addVncNode({ service_id: serviceId.value, name: formData.name, ip: formData.ip, port: formData.port, token: formData.token })
|
res = await addVncNode(payload)
|
||||||
} else {
|
} else {
|
||||||
res = await updateVncNode({ service_id: serviceId.value, id: formData.id, name: formData.name, ip: formData.ip, port: formData.port, token: formData.token })
|
payload.id = formData.id
|
||||||
|
res = await updateVncNode(payload)
|
||||||
}
|
}
|
||||||
if (res?.data?.code === 200) {
|
if (res?.data?.code === 200) {
|
||||||
ElMessage.success(formType.value === 'add' ? '创建成功' : '修改成功')
|
ElMessage.success(formType.value === 'add' ? '创建成功' : '修改成功')
|
||||||
|
|||||||
Reference in New Issue
Block a user