fix: 修改新增用户商品的配置项逻辑
This commit is contained in:
@@ -450,6 +450,18 @@ export const migrateVm = (data) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 发起虚拟机数据迁移 */
|
||||||
|
export const dataMigrateVm = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/service/host_service/point/vm/data_migrate', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取虚拟机数据迁移进度 */
|
||||||
|
export const getDataMigrateProgress = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/service/host_service/point/vm/data_migrate/progress', { params })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ================================
|
* ================================
|
||||||
* 主控服务接口 - 安全组管理
|
* 主控服务接口 - 安全组管理
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const searchParams = reactive({
|
|||||||
key: '',
|
key: '',
|
||||||
method: '',
|
method: '',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 20
|
count: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
<el-dialog v-model="visible" title="选择套餐" width="700px" append-to-body @close="handleClose">
|
<el-dialog v-model="visible" title="选择套餐" width="700px" append-to-body @close="handleClose">
|
||||||
<div class="selector-toolbar">
|
<div class="selector-toolbar">
|
||||||
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建套餐</el-button>
|
||||||
<span style="color:#909399;font-size:13px" v-if="goodId">商品 ID: {{ goodId }}</span>
|
<span style="color:#909399;font-size:13px" v-if="goodId">商品 ID: {{ goodId }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="list" v-loading="loading" highlight-current-row @current-change="row => selected = row" :height="360" stripe size="small">
|
<el-table :data="list" v-loading="loading" highlight-current-row @current-change="row => selected = row" :height="300" stripe size="small">
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="name" label="套餐名称" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="name" label="套餐名称" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column prop="note" label="说明" min-width="160" show-overflow-tooltip>
|
<el-table-column prop="note" label="说明" min-width="160" show-overflow-tooltip>
|
||||||
@@ -28,12 +29,59 @@
|
|||||||
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
<el-button type="primary" :disabled="!selected" @click="handleConfirm">确定选择</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 新建套餐弹窗 -->
|
||||||
|
<el-dialog v-model="showCreate" title="新建套餐" width="680px" append-to-body destroy-on-close class="scrollable-dialog">
|
||||||
|
<el-form :model="createForm" label-width="90px">
|
||||||
|
<el-form-item label="套餐名称" required><el-input v-model="createForm.name" placeholder="请输入套餐名称" /></el-form-item>
|
||||||
|
<el-form-item label="说明"><el-input v-model="createForm.note" type="textarea" :rows="2" placeholder="请输入套餐说明" /></el-form-item>
|
||||||
|
<el-form-item label="参数配置">
|
||||||
|
<div style="width:100%">
|
||||||
|
<div v-if="!goodId" style="color:#c0c4cc;font-size:13px">请先选择商品</div>
|
||||||
|
<div v-else-if="createSpecLoading" style="color:#909399;font-size:13px">加载参数中...</div>
|
||||||
|
<div v-else-if="createSpecList.length === 0" style="color:#909399;font-size:13px">该商品暂无参数</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="spec in createSpecList" :key="spec.id" style="margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #f5f5f5">
|
||||||
|
<div style="font-size:13px;font-weight:500;color:#303133;margin-bottom:6px">
|
||||||
|
{{ spec.name }}
|
||||||
|
<el-tag v-if="spec.must" size="small" type="danger" style="margin-left:4px">必填</el-tag>
|
||||||
|
</div>
|
||||||
|
<template v-if="spec.type === 'select' && spec.attrs && spec.attrs.length > 0">
|
||||||
|
<el-radio-group v-model="createSpecValues[spec.id]" size="small" @change="buildCreateArgsJson">
|
||||||
|
<el-radio-button v-for="attr in spec.attrs" :key="attr.id" :value="attr.id">{{ attr.name }}</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="spec.type === 'number'">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
|
<el-input-number v-model="createSpecValues[spec.id]" :min="spec.min || 0" :max="spec.max || 9999" :step="spec.step || 1" :step-strictly="true" size="small" @change="buildCreateArgsJson" style="width:180px" />
|
||||||
|
<span style="font-size:12px;color:#909399">范围: {{ spec.min || 0 }} ~ {{ spec.max || 9999 }},步长: {{ spec.step || 1 }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-input v-model="createSpecValues[spec.id]" placeholder="请输入值" size="small" style="width:200px" @input="buildCreateArgsJson" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="createForm.args" style="margin-top:8px">
|
||||||
|
<div style="font-size:12px;color:#909399;margin-bottom:4px">参数 JSON:</div>
|
||||||
|
<el-input v-model="createForm.args" type="textarea" :rows="3" readonly style="font-family:monospace;font-size:12px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序"><el-input-number v-model="createForm.index" :min="0" controls-position="right" style="width:120px" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreate = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, reactive, watch } from 'vue'
|
||||||
import { Refresh } from '@element-plus/icons-vue'
|
import { Refresh, Plus } from '@element-plus/icons-vue'
|
||||||
import { getProductPlanList } from '@/api/admin/product'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getProductPlanList, createProductPlan, getProductParameterList } from '@/api/admin/product'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
@@ -48,6 +96,46 @@ const total = ref(0)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const selected = ref(null)
|
const selected = ref(null)
|
||||||
|
const showCreate = ref(false)
|
||||||
|
const createLoading = ref(false)
|
||||||
|
const createForm = reactive({ name: '', note: '', index: 0, args: '' })
|
||||||
|
const createSpecList = ref([])
|
||||||
|
const createSpecLoading = ref(false)
|
||||||
|
const createSpecValues = reactive({})
|
||||||
|
|
||||||
|
watch(showCreate, (v) => {
|
||||||
|
if (v && props.goodId) loadCreateSpec()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadCreateSpec = async () => {
|
||||||
|
createSpecLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductParameterList({ good_id: props.goodId })
|
||||||
|
if (res?.data?.code === 200) {
|
||||||
|
createSpecList.value = res.data.data || []
|
||||||
|
for (const spec of createSpecList.value) {
|
||||||
|
if (spec.type === 'number' && createSpecValues[spec.id] === undefined) createSpecValues[spec.id] = spec.min || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { createSpecList.value = [] } finally { createSpecLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildCreateArgsJson = () => {
|
||||||
|
const result = []
|
||||||
|
for (const spec of createSpecList.value) {
|
||||||
|
const val = createSpecValues[spec.id]
|
||||||
|
if (val === undefined || val === null || val === '') continue
|
||||||
|
if (spec.type === 'select') {
|
||||||
|
const attr = spec.attrs?.find(a => a.id === val)
|
||||||
|
if (attr) result.push({ arg_id: spec.id, name: spec.name, attr_id: attr.id, value: attr.value, number: 0 })
|
||||||
|
} else if (spec.type === 'number') {
|
||||||
|
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: '', number: val })
|
||||||
|
} else {
|
||||||
|
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: String(val), number: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createForm.args = result.length > 0 ? JSON.stringify(result) : ''
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||||
watch(visible, (v) => emit('update:modelValue', v))
|
watch(visible, (v) => emit('update:modelValue', v))
|
||||||
@@ -66,6 +154,28 @@ const loadList = async () => {
|
|||||||
} catch { /* */ } finally { loading.value = false }
|
} catch { /* */ } finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitCreate = async () => {
|
||||||
|
if (!createForm.name) { ElMessage.warning('请输入套餐名称'); return }
|
||||||
|
if (!props.goodId) { ElMessage.warning('请先选择商品'); return }
|
||||||
|
createLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('good_id', props.goodId)
|
||||||
|
fd.append('name', createForm.name)
|
||||||
|
if (createForm.note) fd.append('note', createForm.note)
|
||||||
|
fd.append('index', createForm.index)
|
||||||
|
if (createForm.args) fd.append('args', createForm.args)
|
||||||
|
const res = await createProductPlan(fd)
|
||||||
|
if (res?.data?.code === 200) {
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
showCreate.value = false
|
||||||
|
Object.assign(createForm, { name: '', note: '', index: 0, args: '' })
|
||||||
|
for (const k in createSpecValues) delete createSpecValues[k]
|
||||||
|
loadList()
|
||||||
|
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||||
|
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
const handleClose = () => { visible.value = false }
|
const handleClose = () => { visible.value = false }
|
||||||
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
const handleConfirm = () => { if (selected.value) { emit('confirm', selected.value); handleClose() } }
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="选择数据卷进行挂载" width="680px" append-to-body @close="handleClose">
|
||||||
|
<div class="selector-toolbar">
|
||||||
|
<el-input v-model="keyword" placeholder="搜索数据卷名称" clearable style="width:200px"
|
||||||
|
@keyup.enter="loadList" @clear="loadList">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button :icon="Refresh" @click="loadList" :loading="loading">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="showCreate = true">新建数据卷</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="list" v-loading="loading" highlight-current-row
|
||||||
|
@current-change="row => selected = row" :height="280" stripe size="small">
|
||||||
|
<el-table-column prop="id" label="ID" width="70" />
|
||||||
|
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="大小" width="80">
|
||||||
|
<template #default="{ row }">{{ row.size }} GB</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类型" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 'ready' ? 'success' : 'info'" size="small">{{ row.status || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂载" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_mount ? 'success' : 'info'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="!list.length && !loading" :image-size="60" description="暂无数据卷" />
|
||||||
|
<div class="selector-footer-bar">
|
||||||
|
<span v-if="selected" style="color:#606266;font-size:13px">已选:{{ selected.name }} (ID: {{ selected.id }})</span>
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10,20]" :total="total"
|
||||||
|
layout="total,sizes,prev,pager,next" small background
|
||||||
|
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||||
|
@current-change="p => { page = p; loadList() }" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!selected || !!selected.is_mount" @click="handleConfirm">
|
||||||
|
{{ selected?.is_mount ? '已挂载' : '确定挂载' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 新建数据卷弹窗 -->
|
||||||
|
<el-dialog v-model="showCreate" title="新建数据卷" width="440px" append-to-body destroy-on-close>
|
||||||
|
<el-form :model="createForm" label-width="100px">
|
||||||
|
<el-form-item label="名称" required><el-input v-model="createForm.name" placeholder="数据卷名称" /></el-form-item>
|
||||||
|
<el-form-item label="大小(GB)"><el-input-number v-model="createForm.size" :min="1" controls-position="right" style="width:100%" /></el-form-item>
|
||||||
|
<el-form-item label="目标设备名"><el-input v-model="createForm.target_device" placeholder="不填自动生成" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreate = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getUserVmVolumeList, createUserVmVolume } from '@/api/admin/userVm'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
userGoodsId: { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
const selected = ref(null)
|
||||||
|
|
||||||
|
const showCreate = ref(false)
|
||||||
|
const createLoading = ref(false)
|
||||||
|
const createForm = reactive({ name: '', size: 10, target_device: '' })
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (v) => { visible.value = v; if (v) { selected.value = null; loadList() } })
|
||||||
|
watch(visible, (v) => emit('update:modelValue', v))
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
if (!props.userGoodsId) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getUserVmVolumeList({ user_goods_id: props.userGoodsId, page: page.value, count: pageSize.value })
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const d = res.data.data
|
||||||
|
list.value = d.data || (Array.isArray(d) ? d : [])
|
||||||
|
total.value = d.all_count ?? d.total ?? list.value.length
|
||||||
|
}
|
||||||
|
} catch { /* */ } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCreate = async () => {
|
||||||
|
if (!createForm.name) { ElMessage.warning('请输入名称'); return }
|
||||||
|
createLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await createUserVmVolume({ user_goods_id: props.userGoodsId, ...createForm })
|
||||||
|
if (res?.data?.code === 200) {
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
showCreate.value = false
|
||||||
|
Object.assign(createForm, { name: '', size: 10, target_device: '' })
|
||||||
|
loadList()
|
||||||
|
} else ElMessage.error(res?.data?.message || '创建失败')
|
||||||
|
} catch { ElMessage.error('创建失败') } finally { createLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => { visible.value = false }
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (selected.value && !selected.value.is_mount) {
|
||||||
|
emit('confirm', selected.value)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||||
|
.selector-footer-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
||||||
|
</style>
|
||||||
@@ -71,7 +71,7 @@ const loadList = async () => {
|
|||||||
if (!hostIdFilter.value) return
|
if (!hostIdFilter.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getVmList({ service_id: props.serviceId, host_id: hostIdFilter.value, page: 1, count: 100 })
|
const res = await getVmList({ service_id: props.serviceId, host_id: hostIdFilter.value, page: 1, count: 10 })
|
||||||
const body = res?.data
|
const body = res?.data
|
||||||
if (body?.code === 200 && body?.data) {
|
if (body?.code === 200 && body?.data) {
|
||||||
const inner = body.data
|
const inner = body.data
|
||||||
|
|||||||
@@ -122,3 +122,55 @@ export function formatToApiTime(time) {
|
|||||||
const pad = (n) => String(n).padStart(2, '0')
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 虚拟机状态映射 ==========
|
||||||
|
const VM_STATUS_MAP = {
|
||||||
|
pending: { label: '等待中', type: 'info' },
|
||||||
|
creating: { label: '创建中', type: 'warning' },
|
||||||
|
ready: { label: '就绪', type: 'success' },
|
||||||
|
running: { label: '运行中', type: 'success' },
|
||||||
|
stopped: { label: '已停止', type: 'danger' },
|
||||||
|
stop: { label: '已停止', type: 'danger' },
|
||||||
|
shutoff: { label: '已关闭', type: 'danger' },
|
||||||
|
error: { label: '错误', type: 'danger' },
|
||||||
|
paused: { label: '已暂停', type: 'warning' },
|
||||||
|
reboot: { label: '重启中', type: 'warning' },
|
||||||
|
poweroff: { label: '已关机', type: 'info' },
|
||||||
|
unknown: { label: '未知', type: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取虚拟机状态标签文字
|
||||||
|
*/
|
||||||
|
export function vmStatusLabel(status) {
|
||||||
|
return VM_STATUS_MAP[status]?.label || status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取虚拟机状态 Tag 类型
|
||||||
|
*/
|
||||||
|
export function vmStatusType(status) {
|
||||||
|
return VM_STATUS_MAP[status]?.type || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 磁盘状态映射 ==========
|
||||||
|
const VOLUME_STATUS_MAP = {
|
||||||
|
pending: { label: '等待中', type: 'info' },
|
||||||
|
ready: { label: '就绪', type: 'success' },
|
||||||
|
error: { label: '错误', type: 'danger' },
|
||||||
|
unknown: { label: '未知', type: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取磁盘状态标签文字
|
||||||
|
*/
|
||||||
|
export function volumeStatusLabel(status) {
|
||||||
|
return VOLUME_STATUS_MAP[status]?.label || status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取磁盘状态 Tag 类型
|
||||||
|
*/
|
||||||
|
export function volumeStatusType(status) {
|
||||||
|
return VOLUME_STATUS_MAP[status]?.type || 'info'
|
||||||
|
}
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ const toLoad = async (data) => {
|
|||||||
})
|
})
|
||||||
form.server_id = data
|
form.server_id = data
|
||||||
nowserver_id.value = data
|
nowserver_id.value = data
|
||||||
let res = await getServerPlan({server_id:data,count:100})
|
let res = await getServerPlan({server_id:data,count:10})
|
||||||
planlist.value = res.data.data.map(item => {
|
planlist.value = res.data.data.map(item => {
|
||||||
return {
|
return {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -748,7 +748,7 @@ const fetchCategoryList = async (serverId) => {
|
|||||||
// 编辑镜像
|
// 编辑镜像
|
||||||
const handleEdit = async (data) => {
|
const handleEdit = async (data) => {
|
||||||
try {
|
try {
|
||||||
let res = await getServerPlan({server_id: data.server_id,count: 100})
|
let res = await getServerPlan({server_id: data.server_id,count: 10})
|
||||||
if (res.data && res.data.data) {
|
if (res.data && res.data.data) {
|
||||||
planlist.value = res.data.data.map(item => {
|
planlist.value = res.data.data.map(item => {
|
||||||
return {
|
return {
|
||||||
@@ -874,7 +874,7 @@ const getit = async () => {
|
|||||||
|
|
||||||
// 选择图片
|
// 选择图片
|
||||||
const picPagin = reactive({
|
const picPagin = reactive({
|
||||||
count: 50,
|
count: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
key: '',
|
key: '',
|
||||||
user_type: 1
|
user_type: 1
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ const categoryRules = {
|
|||||||
// 素材库相关
|
// 素材库相关
|
||||||
const picSwitch = ref(false)
|
const picSwitch = ref(false)
|
||||||
const picPagin = reactive({
|
const picPagin = reactive({
|
||||||
count: 50,
|
count: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
key: '',
|
key: '',
|
||||||
user_type: 1
|
user_type: 1
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ const showNewCategoryInput = ref(false)
|
|||||||
const picSwitch = ref(false)
|
const picSwitch = ref(false)
|
||||||
const picLoading = ref(false)
|
const picLoading = ref(false)
|
||||||
const picPagin = reactive({
|
const picPagin = reactive({
|
||||||
count: 20,
|
count: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
key: '',
|
key: '',
|
||||||
user_type: 1
|
user_type: 1
|
||||||
@@ -314,7 +314,7 @@ const initData = async () => {
|
|||||||
// Fallback: fetch list and find item
|
// Fallback: fetch list and find item
|
||||||
const listRes = await getUserMirrorList({
|
const listRes = await getUserMirrorList({
|
||||||
server_id: serverId.value,
|
server_id: serverId.value,
|
||||||
count: 100,
|
count: 10,
|
||||||
page: 1
|
page: 1
|
||||||
})
|
})
|
||||||
if (listRes.data.code === 200) {
|
if (listRes.data.code === 200) {
|
||||||
|
|||||||
@@ -1901,7 +1901,7 @@ const GetSpecs = async () => {
|
|||||||
try {
|
try {
|
||||||
let plans = await getServerPlan({
|
let plans = await getServerPlan({
|
||||||
server_id: route.query.server_id,
|
server_id: route.query.server_id,
|
||||||
count: 30
|
count: 10
|
||||||
});
|
});
|
||||||
spec_list.value = plans.data.data;
|
spec_list.value = plans.data.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2430,7 +2430,7 @@ const fetchContainerMirrorList = async () => {
|
|||||||
|
|
||||||
containerMirrorLoading.value = true;
|
containerMirrorLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await getMirrorList({server_id: route.query.server_id, page: 1, count: 999,key: '',class_id: ''});
|
const response = await getMirrorList({server_id: route.query.server_id, page: 1, count: 10,key: '',class_id: ''});
|
||||||
console.log("获取镜像列表1111:",response);
|
console.log("获取镜像列表1111:",response);
|
||||||
|
|
||||||
if (response && response.data && response.data.code === 200) {
|
if (response && response.data && response.data.code === 200) {
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ const fetchTags = async () => {
|
|||||||
// 根据 tag 获取拼团类型列表
|
// 根据 tag 获取拼团类型列表
|
||||||
const fetchTypeListByTag = async (tag) => {
|
const fetchTypeListByTag = async (tag) => {
|
||||||
try {
|
try {
|
||||||
const res = await getGroupBuyTypeList({ page: 1, count: 100, tag })
|
const res = await getGroupBuyTypeList({ page: 1, count: 10, tag })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
typeList.value = res.data?.data || []
|
typeList.value = res.data?.data || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ const fetchTags = async () => {
|
|||||||
// 根据 tag 获取拼团类型列表(用于创建对话框)
|
// 根据 tag 获取拼团类型列表(用于创建对话框)
|
||||||
const fetchCreateTypeListByTag = async (tag) => {
|
const fetchCreateTypeListByTag = async (tag) => {
|
||||||
try {
|
try {
|
||||||
const res = await getGroupBuyTypeList({ page: 1, count: 100, tag })
|
const res = await getGroupBuyTypeList({ page: 1, count: 10, tag })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
createTypeList.value = res.data?.data || []
|
createTypeList.value = res.data?.data || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ const getFullStatsData = async () => {
|
|||||||
// 获取第一页大量数据来进行统计,或者调用专门的统计接口
|
// 获取第一页大量数据来进行统计,或者调用专门的统计接口
|
||||||
const statsParams = {
|
const statsParams = {
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000, // 获取大量数据进行统计
|
count: 10, // 获取大量数据进行统计
|
||||||
server_id: '',
|
server_id: '',
|
||||||
user_id: '',
|
user_id: '',
|
||||||
key: queryParams.domain || ''
|
key: queryParams.domain || ''
|
||||||
|
|||||||
@@ -412,14 +412,14 @@ const statisticsCards = computed(() => [
|
|||||||
const fetchStatistics = async () => {
|
const fetchStatistics = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取用户数量
|
// 获取用户数量
|
||||||
const userRes = await getUserList({ page: 1, count: 1, key: '' })
|
const userRes = await getUserList({ page: 1, count: 10, key: '' })
|
||||||
console.log("用户数量,",userRes)
|
console.log("用户数量,",userRes)
|
||||||
if (userRes.data?.code === 200) {
|
if (userRes.data?.code === 200) {
|
||||||
userCount.value = userRes.data.data.all_count || 0
|
userCount.value = userRes.data.data.all_count || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取订单数量
|
// 获取订单数量
|
||||||
const orderRes = await getOrderList({ page: 1, count: 1 })
|
const orderRes = await getOrderList({ page: 1, count: 10 })
|
||||||
console.log("订单数量,",orderRes)
|
console.log("订单数量,",orderRes)
|
||||||
if (orderRes.data?.code === 200) {
|
if (orderRes.data?.code === 200) {
|
||||||
orderCount.value = orderRes.data.data.all_count || 0
|
orderCount.value = orderRes.data.data.all_count || 0
|
||||||
@@ -441,7 +441,7 @@ const fetchRecentLists = async () => {
|
|||||||
listLoading.value = true
|
listLoading.value = true
|
||||||
try {
|
try {
|
||||||
// 获取最近用户
|
// 获取最近用户
|
||||||
const userRes = await getUserList({ page: 1, count: 5, key: '' })
|
const userRes = await getUserList({ page: 1, count: 10, key: '' })
|
||||||
if (userRes.data?.code === 200) {
|
if (userRes.data?.code === 200) {
|
||||||
recentUsers.value = (userRes.data.data.data || []).map(user => ({
|
recentUsers.value = (userRes.data.data.data || []).map(user => ({
|
||||||
id: user.user_id,
|
id: user.user_id,
|
||||||
@@ -453,7 +453,7 @@ const fetchRecentLists = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取最近订单
|
// 获取最近订单
|
||||||
const orderRes = await getOrderList({ page: 1, count: 5 })
|
const orderRes = await getOrderList({ page: 1, count: 10 })
|
||||||
if (orderRes.data?.code === 200) {
|
if (orderRes.data?.code === 200) {
|
||||||
recentOrders.value = (orderRes.data.data.list || []).map(order => ({
|
recentOrders.value = (orderRes.data.data.list || []).map(order => ({
|
||||||
id: order.id,
|
id: order.id,
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ const fetchVoucherListOptions = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000,
|
count: 10,
|
||||||
discount_type: 'coupon'
|
discount_type: 'coupon'
|
||||||
})
|
})
|
||||||
console.log('获取代金券列表:', res.data)
|
console.log('获取代金券列表:', res.data)
|
||||||
@@ -407,7 +407,7 @@ const fetchProductList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getProductList({
|
const res = await getProductList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000
|
count: 10
|
||||||
})
|
})
|
||||||
console.log('获取商品列表:', res.data)
|
console.log('获取商品列表:', res.data)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ const fetchVoucherListOptions = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000,
|
count: 10,
|
||||||
discount_type: 'coupon'
|
discount_type: 'coupon'
|
||||||
})
|
})
|
||||||
console.log('获取代金券列表:', res.data)
|
console.log('获取代金券列表:', res.data)
|
||||||
@@ -397,7 +397,7 @@ const fetchUserGroupList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getUserGroupList({
|
const res = await getUserGroupList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10000,
|
count: 10,
|
||||||
key: ''
|
key: ''
|
||||||
})
|
})
|
||||||
console.log('获取用户组列表:', res.data)
|
console.log('获取用户组列表:', res.data)
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const currentGroupBuy = ref(null)
|
|||||||
// 加载拼团列表
|
// 加载拼团列表
|
||||||
const loadGroupBuyList = async () => {
|
const loadGroupBuyList = async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await getGroupBuyList({ page: 1, pageSize: 20 })
|
const resp = await getGroupBuyList({ page: 1, pageSize: 10 })
|
||||||
if (resp && resp.code === 200) {
|
if (resp && resp.code === 200) {
|
||||||
groupBuyList.value = resp.data || []
|
groupBuyList.value = resp.data || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ const fetchVoucherListOptions = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000,
|
count: 10,
|
||||||
discount_type: 'coupon'
|
discount_type: 'coupon'
|
||||||
})
|
})
|
||||||
console.log('获取代金券列表:', res.data)
|
console.log('获取代金券列表:', res.data)
|
||||||
@@ -502,7 +502,7 @@ const fetchDiscountList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100,
|
count: 10,
|
||||||
discount_type: 'coupon'
|
discount_type: 'coupon'
|
||||||
})
|
})
|
||||||
console.log('获取代金券列表:', res.data)
|
console.log('获取代金券列表:', res.data)
|
||||||
@@ -513,7 +513,7 @@ const fetchDiscountList = async () => {
|
|||||||
}
|
}
|
||||||
const res2 = await getDiscountCodeList({
|
const res2 = await getDiscountCodeList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100,
|
count: 10,
|
||||||
discount_type: 'code'
|
discount_type: 'code'
|
||||||
})
|
})
|
||||||
console.log('获取优惠码列表:', res2.data)
|
console.log('获取优惠码列表:', res2.data)
|
||||||
@@ -533,7 +533,7 @@ const fetchVoucherOptions = async () => {
|
|||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
discount_type: 'coupon',
|
discount_type: 'coupon',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
voucherOptions.value = res.data.data?.data || []
|
voucherOptions.value = res.data.data?.data || []
|
||||||
@@ -549,7 +549,7 @@ const fetchCodeOptions = async () => {
|
|||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
discount_type: 'code',
|
discount_type: 'code',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
codeOptions.value = res.data.data?.data || []
|
codeOptions.value = res.data.data?.data || []
|
||||||
@@ -566,7 +566,7 @@ const fetchUserList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getUserList({
|
const res = await getUserList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100,
|
count: 10,
|
||||||
key: ''
|
key: ''
|
||||||
})
|
})
|
||||||
console.log('获取用户列表:', res.data)
|
console.log('获取用户列表:', res.data)
|
||||||
@@ -588,7 +588,7 @@ const fetchGroupOptions = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getUserGroupList({
|
const res = await getUserGroupList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
groupOptions.value = res.data.data?.data || []
|
groupOptions.value = res.data.data?.data || []
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ const fetchUserList = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await getUserList({
|
const res = await getUserList({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10000,
|
count: 10,
|
||||||
key: ''
|
key: ''
|
||||||
})
|
})
|
||||||
UserOptions.value = res.data.data?.data || []
|
UserOptions.value = res.data.data?.data || []
|
||||||
@@ -412,7 +412,7 @@ const fetchDiscountList = async () => {
|
|||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
discount_type: 'coupon',
|
discount_type: 'coupon',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
discountOptions.value = res.data.data?.data || []
|
discountOptions.value = res.data.data?.data || []
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ const fetchDiscountList = async () => {
|
|||||||
const res = await getDiscountCodeList({
|
const res = await getDiscountCodeList({
|
||||||
discount_type: 'coupon',
|
discount_type: 'coupon',
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 1000
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
discountOptions.value = res.data.data?.data || []
|
discountOptions.value = res.data.data?.data || []
|
||||||
|
|||||||
@@ -1113,7 +1113,7 @@ const viewMode = ref('tree')
|
|||||||
// 查询参数
|
// 查询参数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100,
|
count: 10,
|
||||||
tag: undefined,
|
tag: undefined,
|
||||||
level: undefined,
|
level: undefined,
|
||||||
disable: undefined,
|
disable: undefined,
|
||||||
@@ -1328,7 +1328,7 @@ const loadProductsForGroup = async (groupId) => {
|
|||||||
const res = await getProductList({
|
const res = await getProductList({
|
||||||
good_group_id: groupId,
|
good_group_id: groupId,
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100,
|
count: 10,
|
||||||
delete: false
|
delete: false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1410,7 +1410,7 @@ const toggleExpand = async (row) => {
|
|||||||
const res = await getProductGroupList({
|
const res = await getProductGroupList({
|
||||||
parent_id: group.id,
|
parent_id: group.id,
|
||||||
level: childLevel,
|
level: childLevel,
|
||||||
count: 100
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
const children = res.data.data.data || []
|
const children = res.data.data.data || []
|
||||||
@@ -1540,7 +1540,7 @@ const selectedTagName = computed(() => {
|
|||||||
const fetchTagOptionsForSelector = async () => {
|
const fetchTagOptionsForSelector = async () => {
|
||||||
tagSelectorLoading.value = true
|
tagSelectorLoading.value = true
|
||||||
try {
|
try {
|
||||||
const params = { page: 1, count: 100 }
|
const params = { page: 1, count: 10 }
|
||||||
if (tagSelectorSearch.value) {
|
if (tagSelectorSearch.value) {
|
||||||
params.key = tagSelectorSearch.value
|
params.key = tagSelectorSearch.value
|
||||||
}
|
}
|
||||||
@@ -1571,7 +1571,7 @@ const fetchTagOptionsForSelector = async () => {
|
|||||||
// 初始化获取所有标签
|
// 初始化获取所有标签
|
||||||
const fetchAllTagOptions = async () => {
|
const fetchAllTagOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getProductGroupTagList({ page: 1, count: 100 })
|
const res = await getProductGroupTagList({ page: 1, count: 10 })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
const data = res.data.data
|
const data = res.data.data
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
@@ -1920,10 +1920,10 @@ const handleEditProduct = (product, parentGroupId) => {
|
|||||||
|
|
||||||
Object.assign(productForm, {
|
Object.assign(productForm, {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
name: product.name,
|
name: product.name || '',
|
||||||
table: product.table,
|
table: product.table || '',
|
||||||
tag: product.tag,
|
tag: typeof product.tag === 'string' ? product.tag : '',
|
||||||
content: product.content,
|
content: product.content || '',
|
||||||
cover_id: product.coverId,
|
cover_id: product.coverId,
|
||||||
good_group_id: groupId,
|
good_group_id: groupId,
|
||||||
inventory_control: product.inventoryControl,
|
inventory_control: product.inventoryControl,
|
||||||
@@ -1945,10 +1945,10 @@ const submitProductForm = () => {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
name: productForm.name.trim(),
|
name: (productForm.name || '').trim(),
|
||||||
table: productForm.table.trim(),
|
table: (productForm.table || '').trim(),
|
||||||
tag: productForm.tag.trim(),
|
tag: (typeof productForm.tag === 'string' ? productForm.tag : '').trim(),
|
||||||
content: productForm.content.trim(),
|
content: (productForm.content || '').trim(),
|
||||||
cover_id: productForm.cover_id,
|
cover_id: productForm.cover_id,
|
||||||
good_group_id: productForm.good_group_id,
|
good_group_id: productForm.good_group_id,
|
||||||
inventory_control: productForm.inventory_control,
|
inventory_control: productForm.inventory_control,
|
||||||
@@ -2153,7 +2153,7 @@ const submitForm = () => {
|
|||||||
// ==================== 分组标签管理 ====================
|
// ==================== 分组标签管理 ====================
|
||||||
const tagQueryParams = reactive({
|
const tagQueryParams = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 100,
|
count: 10,
|
||||||
key: ''
|
key: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -981,7 +981,7 @@ const toggleGroupExpand = async (row) => {
|
|||||||
const res = await getProductGroupList({
|
const res = await getProductGroupList({
|
||||||
parent_id: row.id,
|
parent_id: row.id,
|
||||||
level: childLevel,
|
level: childLevel,
|
||||||
count: 100
|
count: 10
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
const children = res.data.data.data || []
|
const children = res.data.data.data || []
|
||||||
@@ -1060,7 +1060,7 @@ const fetchProductList = async () => {
|
|||||||
const fetchGroupList = async () => {
|
const fetchGroupList = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取全部分组用于下拉列表
|
// 获取全部分组用于下拉列表
|
||||||
const res = await getProductGroupList({ page: 1, count: 100 })
|
const res = await getProductGroupList({ page: 1, count: 10 })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
groupOptions.value = res.data.data.data || []
|
groupOptions.value = res.data.data.data || []
|
||||||
if (groupOptions.value.length === 0) {
|
if (groupOptions.value.length === 0) {
|
||||||
@@ -1069,7 +1069,7 @@ const fetchGroupList = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取一级分组用于树形选择器
|
// 获取一级分组用于树形选择器
|
||||||
const treeRes = await getProductGroupList({ level: 1, count: 100 })
|
const treeRes = await getProductGroupList({ level: 1, count: 10 })
|
||||||
if (treeRes.data.code === 200) {
|
if (treeRes.data.code === 200) {
|
||||||
const rootItems = treeRes.data.data.data || []
|
const rootItems = treeRes.data.data.data || []
|
||||||
groupTreeData.value = rootItems.map(item => ({
|
groupTreeData.value = rootItems.map(item => ({
|
||||||
|
|||||||
@@ -57,14 +57,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增弹窗 -->
|
<!-- 新增弹窗 -->
|
||||||
<el-dialog v-model="createVisible" title="新增用户商品" width="560px" destroy-on-close>
|
<el-dialog v-model="createVisible" title="新增用户商品" width="600px" destroy-on-close class="scrollable-dialog">
|
||||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px">
|
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px">
|
||||||
<el-form-item label="商品" prop="good_id">
|
<el-form-item label="商品" prop="good_id">
|
||||||
<div class="selector-row">
|
<div class="selector-row">
|
||||||
<el-input :model-value="createForm._goodName || (createForm.good_id ? `商品 #${createForm.good_id}` : '')"
|
<el-input :model-value="createForm._goodName || (createForm.good_id ? `商品 #${createForm.good_id}` : '')"
|
||||||
readonly placeholder="请选择商品" style="flex:1" />
|
readonly placeholder="请选择商品" style="flex:1" />
|
||||||
<el-button type="primary" @click="showProductSelector = true" style="margin-left:8px">选择</el-button>
|
<el-button type="primary" @click="showProductSelector = true" style="margin-left:8px">选择</el-button>
|
||||||
<el-button v-if="createForm.good_id" @click="createForm.good_id = 0; createForm._goodName = ''" style="margin-left:4px">清除</el-button>
|
<el-button v-if="createForm.good_id" @click="createForm.good_id = 0; createForm._goodName = ''; createForm._goodTag = ''; createForm.item_id = 0; createForm._itemName = ''; clearArgsConfig()" style="margin-left:4px">清除</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="用户" prop="user_id">
|
<el-form-item label="用户" prop="user_id">
|
||||||
@@ -86,11 +86,37 @@
|
|||||||
<el-form-item label="套餐">
|
<el-form-item label="套餐">
|
||||||
<div class="selector-row">
|
<div class="selector-row">
|
||||||
<el-input :model-value="createForm._planName || (createForm.good_plan_id ? `套餐 #${createForm.good_plan_id}` : '')"
|
<el-input :model-value="createForm._planName || (createForm.good_plan_id ? `套餐 #${createForm.good_plan_id}` : '')"
|
||||||
readonly placeholder="可选" style="flex:1" />
|
readonly placeholder="可选,选择后自动填入参数" style="flex:1" />
|
||||||
<el-button type="primary" @click="showPlanSelector = true" style="margin-left:8px">选择</el-button>
|
<el-button type="primary" @click="showPlanSelector = true" style="margin-left:8px">选择</el-button>
|
||||||
<el-button v-if="createForm.good_plan_id" @click="createForm.good_plan_id = 0; createForm._planName = ''" style="margin-left:4px">清除</el-button>
|
<el-button v-if="createForm.good_plan_id" @click="createForm.good_plan_id = 0; createForm._planName = ''" style="margin-left:4px">清除</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 订单参数配置 -->
|
||||||
|
<el-form-item label="订单参数">
|
||||||
|
<div class="selector-row">
|
||||||
|
<el-input :model-value="createForm.order_args ? `已配置 ${argsCount} 个参数` : ''"
|
||||||
|
readonly placeholder="可选,点击配置参数" style="flex:1" />
|
||||||
|
<el-button type="primary" @click="openArgsDialog" :disabled="!createForm.good_id" style="margin-left:8px">配置</el-button>
|
||||||
|
<el-button v-if="createForm.order_args" @click="createForm.order_args = ''; clearArgsConfig()" style="margin-left:4px">清除</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!createForm.good_id" style="font-size:12px;color:#c0c4cc;margin-top:4px">请先选择商品</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="归属项">
|
||||||
|
<div class="selector-row">
|
||||||
|
<el-input :model-value="createForm._itemName || (createForm.item_id ? `#${createForm.item_id}` : '')"
|
||||||
|
readonly placeholder="可选" style="flex:1" />
|
||||||
|
<el-button type="primary" @click="handleItemSelect" :disabled="!createForm.good_id" style="margin-left:8px">
|
||||||
|
{{ createForm._goodTag === '云服务器' ? '选择虚拟机' : '使用商品ID' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="createForm.item_id" @click="createForm.item_id = 0; createForm._itemName = ''" style="margin-left:4px">清除</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!createForm.good_id" style="font-size:12px;color:#c0c4cc;margin-top:4px">请先选择商品</div>
|
||||||
|
<div v-else-if="createForm._goodTag === '云服务器'" style="font-size:12px;color:#909399;margin-top:4px">云服务器商品,点击选择用户虚拟机作为归属项</div>
|
||||||
|
<div v-else style="font-size:12px;color:#909399;margin-top:4px">普通商品,点击将商品ID赋值为归属项</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="续费价格(元)">
|
<el-form-item label="续费价格(元)">
|
||||||
<el-input-number v-model="createForm._renewYuan" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
<el-input-number v-model="createForm._renewYuan" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -134,21 +160,99 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<ProductSelector v-model="showProductSelector" @confirm="p => { createForm.good_id = p.id; createForm._goodName = p.name }" />
|
<ProductSelector v-model="showProductSelector" @confirm="handleProductSelected" />
|
||||||
<UserSelector v-model:visible="showUserSelector" @select="u => { createForm.user_id = u.user_id; createForm._userName = u.user_name }" />
|
<UserSelector v-model:visible="showUserSelector" @select="u => { createForm.user_id = u.user_id; createForm._userName = u.user_name }" />
|
||||||
<OrderSelector v-model="showOrderSelector" @confirm="o => { createForm.order_id = o.id; createForm._orderName = o.name }" />
|
<OrderSelector v-model="showOrderSelector" @confirm="o => { createForm.order_id = o.id; createForm._orderName = o.name }" />
|
||||||
<PlanSelector v-model="showPlanSelector" :good-id="createForm.good_id" @confirm="p => { createForm.good_plan_id = p.id; createForm._planName = p.name }" />
|
<PlanSelector v-model="showPlanSelector" :good-id="createForm.good_id" @confirm="handlePlanSelectedForCreate" />
|
||||||
|
|
||||||
|
<!-- 用户虚拟机选择弹窗(item_id 为云服务器时使用) -->
|
||||||
|
<el-dialog v-model="showVmListDialog" title="选择用户虚拟机" width="800px" append-to-body destroy-on-close>
|
||||||
|
<div class="filter-section" style="margin-bottom:12px">
|
||||||
|
<el-form :inline="true" size="default">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input v-model="vmListQuery.key" placeholder="搜索" clearable style="width:180px"
|
||||||
|
@keyup.enter="loadVmListForItem" @clear="loadVmListForItem" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="loadVmListForItem">搜索</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<el-table :data="vmListForItem" v-loading="vmListLoading" highlight-current-row
|
||||||
|
@current-change="r => vmListSelected = r" :height="350" style="width:100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="用户" min-width="120">
|
||||||
|
<template #default="{ row }">{{ row.user?.UserName || row.user?.username || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.good?.name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="归属项ID" width="100">
|
||||||
|
<template #default="{ row }">{{ row.itemId || row.item_id || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="到期时间" width="170">
|
||||||
|
<template #default="{ row }">{{ formatExpireTime(row.expireTime || row.expire_time) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-top:12px">
|
||||||
|
<el-pagination v-model:current-page="vmListQuery.page" v-model:page-size="vmListQuery.count"
|
||||||
|
:total="vmListTotal" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next"
|
||||||
|
@size-change="s => { vmListQuery.count = s; vmListQuery.page = 1; loadVmListForItem() }"
|
||||||
|
@current-change="p => { vmListQuery.page = p; loadVmListForItem() }" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showVmListDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!vmListSelected" @click="confirmVmForItem">确定选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订单参数配置弹窗 -->
|
||||||
|
<el-dialog v-model="showArgsDialog" title="配置订单参数" width="600px" append-to-body destroy-on-close class="scrollable-dialog">
|
||||||
|
<div v-if="argsSpecLoading" style="text-align:center;padding:20px;color:#909399">加载参数中...</div>
|
||||||
|
<div v-else-if="argsSpecList.length === 0" style="text-align:center;padding:20px;color:#909399">该商品暂无参数配置</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="spec in argsSpecList" :key="spec.id" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid #f0f0f0">
|
||||||
|
<div style="font-size:14px;font-weight:500;color:#303133;margin-bottom:8px">
|
||||||
|
{{ spec.name }}
|
||||||
|
<el-tag v-if="spec.must" size="small" type="danger" style="margin-left:6px">必填</el-tag>
|
||||||
|
</div>
|
||||||
|
<template v-if="spec.type === 'select' && spec.attrs && spec.attrs.length > 0">
|
||||||
|
<el-radio-group v-model="argsValues[spec.id]" size="small" @change="buildArgsJson">
|
||||||
|
<el-radio-button v-for="attr in spec.attrs" :key="attr.id" :value="attr.id">{{ attr.name }}</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="spec.type === 'number'">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px">
|
||||||
|
<el-input-number v-model="argsValues[spec.id]" :min="spec.min || 0" :max="spec.max || 9999" :step="spec.step || 1" :step-strictly="true" @change="buildArgsJson" style="width:200px" />
|
||||||
|
<span style="font-size:12px;color:#909399">范围: {{ spec.min || 0 }} ~ {{ spec.max || 9999 }},步长: {{ spec.step || 1 }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-input v-model="argsValues[spec.id]" placeholder="请输入值" style="width:200px" @input="buildArgsJson" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="createForm.order_args" style="margin-top:8px">
|
||||||
|
<div style="font-size:12px;color:#909399;margin-bottom:6px">生成的参数 JSON:</div>
|
||||||
|
<el-input v-model="createForm.order_args" type="textarea" :rows="4" readonly style="font-family:monospace;font-size:12px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showArgsDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="showArgsDialog = false">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
import { getUserGoodsList, createUserGoods, updateUserGoods, deleteUserGoods } from '@/api/admin/userVm'
|
import { getUserGoodsList, createUserGoods, updateUserGoods, deleteUserGoods, getUserVmList } from '@/api/admin/userVm'
|
||||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||||
import { formatToApiTime } from '@/utils/tool'
|
import { formatToApiTime } from '@/utils/tool'
|
||||||
|
import { getProductParameterList, getProductPlanDetail } from '@/api/admin/product'
|
||||||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||||||
import UserSelector from '@/components/UserSelector/index.vue'
|
import UserSelector from '@/components/UserSelector/index.vue'
|
||||||
import OrderSelector from '@/components/admin/OrderSelector.vue'
|
import OrderSelector from '@/components/admin/OrderSelector.vue'
|
||||||
@@ -188,7 +292,86 @@ const loadList = async () => {
|
|||||||
|
|
||||||
const handleSearch = () => { query.page = 1; loadList() }
|
const handleSearch = () => { query.page = 1; loadList() }
|
||||||
|
|
||||||
// ---- 详情 ----
|
// ---- 订单参数配置 ----
|
||||||
|
const argsSpecList = ref([])
|
||||||
|
const argsSpecLoading = ref(false)
|
||||||
|
const argsValues = reactive({})
|
||||||
|
const showArgsDialog = ref(false)
|
||||||
|
|
||||||
|
const argsCount = computed(() => {
|
||||||
|
try { const arr = JSON.parse(createForm.order_args || '[]'); return Array.isArray(arr) ? arr.length : 0 } catch { return 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const openArgsDialog = () => {
|
||||||
|
if (!createForm.good_id) return
|
||||||
|
showArgsDialog.value = true
|
||||||
|
if (argsSpecList.value.length === 0) loadArgsSpec(createForm.good_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearArgsConfig = () => {
|
||||||
|
argsSpecList.value = []
|
||||||
|
for (const k in argsValues) delete argsValues[k]
|
||||||
|
createForm.order_args = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadArgsSpec = async (goodId) => {
|
||||||
|
if (!goodId) return
|
||||||
|
argsSpecLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductParameterList({ good_id: goodId })
|
||||||
|
if (res?.data?.code === 200) {
|
||||||
|
argsSpecList.value = res.data.data || []
|
||||||
|
// 初始化 number 类型的默认值
|
||||||
|
for (const spec of argsSpecList.value) {
|
||||||
|
if (spec.type === 'number' && argsValues[spec.id] === undefined) {
|
||||||
|
argsValues[spec.id] = spec.min || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { argsSpecList.value = [] } finally { argsSpecLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildArgsJson = () => {
|
||||||
|
const result = []
|
||||||
|
for (const spec of argsSpecList.value) {
|
||||||
|
const val = argsValues[spec.id]
|
||||||
|
if (val === undefined || val === null || val === '') continue
|
||||||
|
if (spec.type === 'select') {
|
||||||
|
const attr = spec.attrs?.find(a => a.id === val)
|
||||||
|
if (attr) result.push({ arg_id: spec.id, name: spec.name, attr_id: attr.id, value: attr.value, number: 0 })
|
||||||
|
} else if (spec.type === 'number') {
|
||||||
|
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: '', number: val })
|
||||||
|
} else {
|
||||||
|
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: String(val), number: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createForm.order_args = result.length > 0 ? JSON.stringify(result) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择套餐后自动填入参数
|
||||||
|
const handlePlanSelectedForCreate = async (plan) => {
|
||||||
|
createForm.good_plan_id = plan.id
|
||||||
|
createForm._planName = plan.name
|
||||||
|
// 解析套餐的 args 字段自动填入参数值
|
||||||
|
if (plan.args) {
|
||||||
|
try {
|
||||||
|
const planArgs = typeof plan.args === 'string' ? JSON.parse(plan.args) : plan.args
|
||||||
|
if (Array.isArray(planArgs)) {
|
||||||
|
for (const arg of planArgs) {
|
||||||
|
if (arg.arg_id) {
|
||||||
|
if (arg.attr_id) argsValues[arg.arg_id] = arg.attr_id
|
||||||
|
else if (arg.number !== undefined) argsValues[arg.arg_id] = arg.number
|
||||||
|
else if (arg.value) argsValues[arg.arg_id] = arg.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildArgsJson()
|
||||||
|
}
|
||||||
|
} catch { /* */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择商品后加载参数
|
||||||
|
// watch 在 createForm 声明后定义,见下方
|
||||||
const handleDetail = (row) => {
|
const handleDetail = (row) => {
|
||||||
router.push({ path: '/user-goods/vm-detail', query: { id: row.id } })
|
router.push({ path: '/user-goods/vm-detail', query: { id: row.id } })
|
||||||
}
|
}
|
||||||
@@ -202,17 +385,80 @@ const showUserSelector = ref(false)
|
|||||||
const showOrderSelector = ref(false)
|
const showOrderSelector = ref(false)
|
||||||
const showPlanSelector = ref(false)
|
const showPlanSelector = ref(false)
|
||||||
const createForm = reactive({
|
const createForm = reactive({
|
||||||
good_id: 0, _goodName: '', user_id: 0, _userName: '',
|
good_id: 0, _goodName: '', _goodTag: '', user_id: 0, _userName: '',
|
||||||
order_id: 0, _orderName: '', good_plan_id: 0, _planName: '',
|
order_id: 0, _orderName: '', good_plan_id: 0, _planName: '',
|
||||||
_renewYuan: 0, _baseYuan: 0, note: '', expire_time: ''
|
item_id: 0, _itemName: '',
|
||||||
|
_renewYuan: 0, _baseYuan: 0, note: '', expire_time: '',
|
||||||
|
order_args: ''
|
||||||
})
|
})
|
||||||
const createRules = {
|
const createRules = {
|
||||||
good_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择商品')), trigger: 'change' }],
|
good_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择商品')), trigger: 'change' }],
|
||||||
user_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择用户')), trigger: 'change' }]
|
user_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择用户')), trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 商品选择确认处理
|
||||||
|
const handleProductSelected = (p) => {
|
||||||
|
createForm.good_id = p.id
|
||||||
|
createForm._goodName = p.name
|
||||||
|
createForm._goodTag = p.tag || ''
|
||||||
|
createForm.item_id = 0
|
||||||
|
createForm._itemName = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 归属项选择(item_id) ----
|
||||||
|
const showVmListDialog = ref(false)
|
||||||
|
const vmListForItem = ref([])
|
||||||
|
const vmListLoading = ref(false)
|
||||||
|
const vmListTotal = ref(0)
|
||||||
|
const vmListSelected = ref(null)
|
||||||
|
const vmListQuery = reactive({ page: 1, count: 10, key: '' })
|
||||||
|
|
||||||
|
const handleItemSelect = () => {
|
||||||
|
if (!createForm.good_id) return
|
||||||
|
if (createForm._goodTag === '云服务器') {
|
||||||
|
vmListSelected.value = null
|
||||||
|
vmListQuery.page = 1
|
||||||
|
vmListQuery.key = ''
|
||||||
|
showVmListDialog.value = true
|
||||||
|
loadVmListForItem()
|
||||||
|
} else {
|
||||||
|
createForm.item_id = createForm.good_id
|
||||||
|
createForm._itemName = `商品 #${createForm.good_id}`
|
||||||
|
ElMessage.success('已将商品ID赋值为归属项')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVmListForItem = async () => {
|
||||||
|
vmListLoading.value = true
|
||||||
|
try {
|
||||||
|
const params = { page: vmListQuery.page, count: vmListQuery.count }
|
||||||
|
if (vmListQuery.key) params.key = vmListQuery.key
|
||||||
|
const res = await getUserVmList(params)
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const d = res.data.data
|
||||||
|
vmListForItem.value = d.data || (Array.isArray(d) ? d : [])
|
||||||
|
vmListTotal.value = d.all_count ?? d.total ?? vmListForItem.value.length
|
||||||
|
} else { vmListForItem.value = []; vmListTotal.value = 0 }
|
||||||
|
} catch { vmListForItem.value = []; vmListTotal.value = 0 } finally { vmListLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmVmForItem = () => {
|
||||||
|
if (!vmListSelected.value) return
|
||||||
|
const vm = vmListSelected.value
|
||||||
|
createForm.item_id = vm.id
|
||||||
|
createForm._itemName = `虚拟机 #${vm.id}${vm.good?.name ? ` (${vm.good.name})` : ''}`
|
||||||
|
showVmListDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择商品后加载参数
|
||||||
|
watch(() => createForm.good_id, (id) => {
|
||||||
|
if (id) loadArgsSpec(id)
|
||||||
|
else clearArgsConfig()
|
||||||
|
})
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
Object.assign(createForm, { good_id: 0, _goodName: '', user_id: 0, _userName: '', order_id: 0, _orderName: '', good_plan_id: 0, _planName: '', _renewYuan: 0, _baseYuan: 0, note: '', expire_time: '' })
|
Object.assign(createForm, { good_id: 0, _goodName: '', _goodTag: '', user_id: 0, _userName: '', order_id: 0, _orderName: '', good_plan_id: 0, _planName: '', item_id: 0, _itemName: '', _renewYuan: 0, _baseYuan: 0, note: '', expire_time: '', order_args: '' })
|
||||||
|
clearArgsConfig()
|
||||||
createVisible.value = true
|
createVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,10 +470,12 @@ const submitCreate = () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
good_id: createForm.good_id, user_id: createForm.user_id,
|
good_id: createForm.good_id, user_id: createForm.user_id,
|
||||||
order_id: createForm.order_id, good_plan_id: createForm.good_plan_id,
|
order_id: createForm.order_id, good_plan_id: createForm.good_plan_id,
|
||||||
|
item_id: createForm.item_id || 0,
|
||||||
note: createForm.note,
|
note: createForm.note,
|
||||||
renew_price: Math.round((createForm._renewYuan || 0) * 100),
|
renew_price: Math.round((createForm._renewYuan || 0) * 100),
|
||||||
base_price: Math.round((createForm._baseYuan || 0) * 100)
|
base_price: Math.round((createForm._baseYuan || 0) * 100)
|
||||||
}
|
}
|
||||||
|
if (createForm.order_args) payload.order_args = createForm.order_args
|
||||||
if (createForm.expire_time) payload.expire_time = formatToApiTime(createForm.expire_time)
|
if (createForm.expire_time) payload.expire_time = formatToApiTime(createForm.expire_time)
|
||||||
const res = await createUserGoods(payload)
|
const res = await createUserGoods(payload)
|
||||||
if (res?.data?.code === 200) { ElMessage.success('新增成功'); createVisible.value = false; loadList() }
|
if (res?.data?.code === 200) { ElMessage.success('新增成功'); createVisible.value = false; loadList() }
|
||||||
@@ -288,4 +536,14 @@ onMounted(loadList)
|
|||||||
.toolbar-left, .toolbar-right { display: flex; gap: 8px; align-items: center; }
|
.toolbar-left, .toolbar-right { display: flex; gap: 8px; align-items: center; }
|
||||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
|
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||||
.selector-row { display: flex; align-items: center; width: 100%; }
|
.selector-row { display: flex; align-items: center; width: 100%; }
|
||||||
|
|
||||||
|
:global(.scrollable-dialog .el-dialog__body) {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
:global(.scrollable-dialog .el-dialog__body::-webkit-scrollbar) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -905,7 +905,7 @@ const loadChildren = async (row) => {
|
|||||||
try {
|
try {
|
||||||
const groupId = row.data.id
|
const groupId = row.data.id
|
||||||
console.log('Loading children for group:', groupId)
|
console.log('Loading children for group:', groupId)
|
||||||
const res = await getSettingList({ group_id: groupId, page: 1, count: 100 })
|
const res = await getSettingList({ group_id: groupId, page: 1, count: 10 })
|
||||||
console.log('getSettingList response:', res.data)
|
console.log('getSettingList response:', res.data)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
let settings = res.data.data.data || []
|
let settings = res.data.data.data || []
|
||||||
@@ -958,7 +958,7 @@ const toggleExpand = async (row) => {
|
|||||||
// 初始化加载配置组
|
// 初始化加载配置组
|
||||||
const loadGroups = async () => {
|
const loadGroups = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getSettingGroupList({ page: 1, count: 100 })
|
const res = await getSettingGroupList({ page: 1, count: 10 })
|
||||||
console.log('getSettingGroupList response:', res.data)
|
console.log('getSettingGroupList response:', res.data)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
const groups = res.data.data.data || []
|
const groups = res.data.data.data || []
|
||||||
@@ -1322,7 +1322,7 @@ const fetchGroupList = async () => {
|
|||||||
|
|
||||||
const fetchAllGroupList = async () => {
|
const fetchAllGroupList = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getSettingGroupList({ page: 1, count: 100 })
|
const res = await getSettingGroupList({ page: 1, count: 10 })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
allGroupList.value = res.data.data.data || []
|
allGroupList.value = res.data.data.data || []
|
||||||
}
|
}
|
||||||
@@ -2560,7 +2560,7 @@ const handleCopyGroupSettings = async (row) => {
|
|||||||
if (row._expanded && row._children && row._children.length > 0) {
|
if (row._expanded && row._children && row._children.length > 0) {
|
||||||
settings = row._children.map(child => child.data)
|
settings = row._children.map(child => child.data)
|
||||||
} else {
|
} else {
|
||||||
const res = await getSettingList({ group_id: groupId, page: 1, count: 100 })
|
const res = await getSettingList({ group_id: groupId, page: 1, count: 10 })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
settings = res.data.data.data || []
|
settings = res.data.data.data || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ const handleUserSearch = () => {
|
|||||||
userSearchTimer.value = setTimeout(async () => {
|
userSearchTimer.value = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
isSearchingUser.value = true
|
isSearchingUser.value = true
|
||||||
const res = await getUserList({ page: 1, count: 20, key: keyword })
|
const res = await getUserList({ page: 1, count: 10, key: keyword })
|
||||||
|
|
||||||
console.log('用户搜索响应:', res)
|
console.log('用户搜索响应:', res)
|
||||||
|
|
||||||
|
|||||||
+828
-100
File diff suppressed because it is too large
Load Diff
@@ -48,13 +48,13 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="续费价" width="90">
|
<el-table-column label="续费价" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.renewPrice">¥{{ (row.renewPrice).toFixed(2) }}</span>
|
<span v-if="row.renewPrice">¥{{ (row.renewPrice / 100).toFixed(2) }}</span>
|
||||||
<span v-else style="color:#c0c4cc">-</span>
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="基础价" width="90">
|
<el-table-column label="基础价" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.basePrice">¥{{ (row.basePrice ).toFixed(2) }}</span>
|
<span v-if="row.basePrice">¥{{ (row.basePrice / 100).toFixed(2) }}</span>
|
||||||
<span v-else style="color:#c0c4cc">-</span>
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ const currentBalanceDisplay = computed(() => {
|
|||||||
// 获取用户列表(用于显示用户名)
|
// 获取用户列表(用于显示用户名)
|
||||||
const getUserListData = async () => {
|
const getUserListData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getUserList({ page: 1, count: 1000, key: '' })
|
const res = await getUserList({ page: 1, count: 10, key: '' })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
userList.value = res.data.data.data || []
|
userList.value = res.data.data.data || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -866,7 +866,7 @@ const handleGroupManage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchUserGroupList = async () => {
|
const fetchUserGroupList = async () => {
|
||||||
const res = await getUserGroupList({ page: 1, count: 100 })
|
const res = await getUserGroupList({ page: 1, count: 10 })
|
||||||
if (res.data.code == 200) {
|
if (res.data.code == 200) {
|
||||||
userGroupList.value = res.data.data.data
|
userGroupList.value = res.data.data.data
|
||||||
}
|
}
|
||||||
@@ -1276,7 +1276,7 @@ const handleTokenManage = async () => {
|
|||||||
const fetchAdminGroupList = async () => {
|
const fetchAdminGroupList = async () => {
|
||||||
adminGroupLoading.value = true
|
adminGroupLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getAdminGroupList({ params: { page: 1, count: 100 } })
|
const res = await getAdminGroupList({ params: { page: 1, count: 10 } })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
adminGroupList.value = res.data.data?.data || res.data.data || []
|
adminGroupList.value = res.data.data?.data || res.data.data || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const vmOptionsLoading = ref(false)
|
|||||||
const loadVmOptions = async () => {
|
const loadVmOptions = async () => {
|
||||||
vmOptionsLoading.value = true
|
vmOptionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 500 })
|
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
const inner = res.data.data
|
const inner = res.data.data
|
||||||
vmOptions.value = inner.vms || inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
vmOptions.value = inner.vms || inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const vmOptionsLoading = ref(false)
|
|||||||
const loadVmOptions = async () => {
|
const loadVmOptions = async () => {
|
||||||
vmOptionsLoading.value = true
|
vmOptionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 500 })
|
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
const inner = res.data.data
|
const inner = res.data.data
|
||||||
vmOptions.value = inner.vms || inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
vmOptions.value = inner.vms || inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ const getHostLabel = (hid) => {
|
|||||||
|
|
||||||
const loadHostOptions = async () => {
|
const loadHostOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 200 })
|
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||||
const body = res?.data
|
const body = res?.data
|
||||||
if (body?.code === 200 && body?.data) {
|
if (body?.code === 200 && body?.data) {
|
||||||
const inner = body.data
|
const inner = body.data
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
|
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
|
||||||
<el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
|
<el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
|
||||||
<el-dropdown-item divided command="migrateVm">迁移虚拟机</el-dropdown-item>
|
<el-dropdown-item divided command="migrateVm">迁移虚拟机</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="dataMigrateVm">数据迁移</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@@ -225,7 +226,7 @@
|
|||||||
</el-table-column> -->
|
</el-table-column> -->
|
||||||
<el-table-column label="状态" width="80">
|
<el-table-column label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.status === 'ready' ? 'success' : 'info'" size="small">{{ row.status === 'ready' ? '就绪' : (row.status || '-') }}</el-tag>
|
<el-tag :type="volumeStatusType(row.status)" size="small">{{ volumeStatusLabel(row.status) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="path" label="路径" min-width="160" show-overflow-tooltip>
|
<el-table-column prop="path" label="路径" min-width="160" show-overflow-tooltip>
|
||||||
@@ -259,7 +260,7 @@
|
|||||||
<div style="display: flex; gap: 8px">
|
<div style="display: flex; gap: 8px">
|
||||||
<el-button size="small" type="primary" @click="handleSgCreate"><el-icon><Plus /></el-icon>创建安全组</el-button>
|
<el-button size="small" type="primary" @click="handleSgCreate"><el-icon><Plus /></el-icon>创建安全组</el-button>
|
||||||
<el-button size="small" @click="handleSgBind">绑定安全组</el-button>
|
<el-button size="small" @click="handleSgBind">绑定安全组</el-button>
|
||||||
<el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
|
<el-button size="small" :icon="Refresh" @click="async () => { await loadDetail(); loadSgLockInfo() }">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="pagedSecurityGroups" size="small" stripe>
|
<el-table :data="pagedSecurityGroups" size="small" stripe>
|
||||||
@@ -789,8 +790,8 @@
|
|||||||
<el-form-item label="上行带宽(Mbps)">
|
<el-form-item label="上行带宽(Mbps)">
|
||||||
<el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
<el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="每月最大流量(MB)">
|
<el-form-item label="流量上限(GB)">
|
||||||
<el-input-number v-model="trafficForm.traffic_max" :min="0" controls-position="right" style="width: 100%" />
|
<el-input-number v-model="trafficForm._trafficGB" :min="0" :precision="2" controls-position="right" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -841,6 +842,66 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 数据迁移弹窗 -->
|
||||||
|
<el-dialog v-model="dataMigrateVisible" title="发起虚拟机数据迁移" width="580px" destroy-on-close>
|
||||||
|
<el-alert type="info" :closable="false" style="margin-bottom:16px">
|
||||||
|
源服务的虚拟机数据将通过 rsync 传输到目标服务的宿主机上,完成后在目标宿主机上校验并恢复虚拟机。
|
||||||
|
</el-alert>
|
||||||
|
<el-form :model="dataMigrateForm" label-width="130px" v-loading="dataMigrateLoading">
|
||||||
|
<el-form-item label="源主控服务">
|
||||||
|
<el-input :model-value="`${serviceName} (ID: ${serviceId})`" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="源虚拟机">
|
||||||
|
<el-input :model-value="`${detail?.name || ''} (ID: ${vmId})`" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标主控服务" required>
|
||||||
|
<el-select v-model="dataMigrateForm.target_service_id" placeholder="选择目标主控服务" filterable style="width:100%"
|
||||||
|
@change="dataMigrateForm.target_host_id = null; loadDataMigrateHosts()">
|
||||||
|
<el-option v-for="s in dataMigrateServiceOptions" :key="s.id" :label="`${s.name} (ID:${s.id})`" :value="s.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标宿主机" required>
|
||||||
|
<el-select v-model="dataMigrateForm.target_host_id" placeholder="选择目标宿主机" filterable style="width:100%"
|
||||||
|
:disabled="!dataMigrateForm.target_service_id" :loading="dataMigrateHostsLoading">
|
||||||
|
<el-option v-for="h in dataMigrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="IPv4数量">
|
||||||
|
<el-input-number v-model="dataMigrateForm.ipv4_num" :min="0" controls-position="right" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="IPv6数量">
|
||||||
|
<el-input-number v-model="dataMigrateForm.ipv6_num" :min="0" controls-position="right" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dataMigrateVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="actionLoading" @click="submitDataMigrate">发起迁移</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 数据迁移进度弹窗 -->
|
||||||
|
<el-dialog v-model="dataMigrateProgressVisible" title="数据迁移进度" width="480px" destroy-on-close>
|
||||||
|
<div v-loading="dataMigrateProgressLoading">
|
||||||
|
<el-descriptions :column="1" border size="small" v-if="dataMigrateProgressData">
|
||||||
|
<el-descriptions-item label="迁移ID"><span style="font-family:monospace;font-size:12px">{{ dataMigrateProgressData.migration_id || '-' }}</span></el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态"><el-tag :type="taskStatusType(dataMigrateProgressData.status)" size="small">{{ dataMigrateProgressData.status || '-' }}</el-tag></el-descriptions-item>
|
||||||
|
<el-descriptions-item label="进度" v-if="dataMigrateProgressData.progress != null">{{ dataMigrateProgressData.progress }}%</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="信息" v-if="dataMigrateProgressData.message">{{ dataMigrateProgressData.message }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-empty v-else-if="!dataMigrateProgressLoading" description="暂无进度信息" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dataMigrateProgressVisible = false">关闭</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="loadDataMigrateProgress" :loading="dataMigrateProgressLoading">刷新进度</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 绑定外网选择器(bridge) -->
|
<!-- 绑定外网选择器(bridge) -->
|
||||||
<NetworkSelectorPopup v-model="showNetBindBridgeSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindBridgeConfirm" @create="() => handleNetCreate('bindBridge')" />
|
<NetworkSelectorPopup v-model="showNetBindBridgeSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindBridgeConfirm" @create="() => handleNetCreate('bindBridge')" />
|
||||||
<!-- 绑定内网选择器(nat) -->
|
<!-- 绑定内网选择器(nat) -->
|
||||||
@@ -1175,10 +1236,13 @@ import {
|
|||||||
getVmList, bindSecurityGroup, unbindSecurityGroup,
|
getVmList, bindSecurityGroup, unbindSecurityGroup,
|
||||||
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
|
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
|
||||||
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit,
|
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit,
|
||||||
migrateVm, getRemoteHostGroupList, getRemoteHostDetail
|
migrateVm, getRemoteHostGroupList, getRemoteHostDetail,
|
||||||
|
dataMigrateVm, getDataMigrateProgress,
|
||||||
|
getKvmServiceList
|
||||||
} from '@/api/admin/kvmService'
|
} from '@/api/admin/kvmService'
|
||||||
import { getUserInfo } from '@/api/admin/user'
|
import { getUserInfo } from '@/api/admin/user'
|
||||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||||
|
import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||||
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
|
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
|
||||||
@@ -1264,10 +1328,11 @@ const handleMoreCommand = (cmd) => {
|
|||||||
migrateVm: handleMigrateVm
|
migrateVm: handleMigrateVm
|
||||||
}
|
}
|
||||||
if (actionMap[cmd]) actionMap[cmd]()
|
if (actionMap[cmd]) actionMap[cmd]()
|
||||||
|
if (cmd === 'dataMigrateVm') handleDataMigrateVm()
|
||||||
}
|
}
|
||||||
|
|
||||||
const vmStatusType = (s) => ({ running: 'success', ready: 'success', creating: 'warning', pending: 'info', stopped: 'danger', stop: 'danger', shutoff: 'danger', error: 'danger', paused: 'warning', reboot: 'warning', poweroff: 'info', unknown: 'info' }[s] || 'info')
|
const vmStatusType = (s) => vmStatusTypeUtil(s)
|
||||||
const vmStatusLabel = (s) => ({ running: '运行中', ready: '就绪', creating: '创建中', pending: '等待中', stopped: '已停止', stop: '已停止', shutoff: '已关闭', error: '错误', paused: '已暂停', reboot: '重启中', poweroff: '已关机', unknown: '未知' }[s] || s || '-')
|
const vmStatusLabel = (s) => vmStatusLabelUtil(s)
|
||||||
const imgStatusType = (s) => ({ ready: 'success', downloading: 'warning', pending: 'info', error: 'danger' }[s] || 'info')
|
const imgStatusType = (s) => ({ ready: 'success', downloading: 'warning', pending: 'info', error: 'danger' }[s] || 'info')
|
||||||
const imgStatusLabel = (s) => ({ ready: '就绪', downloading: '下载中', pending: '等待中', error: '错误' }[s] || s || '-')
|
const imgStatusLabel = (s) => ({ ready: '就绪', downloading: '下载中', pending: '等待中', error: '错误' }[s] || s || '-')
|
||||||
|
|
||||||
@@ -1696,14 +1761,14 @@ const submitRefactorVm = async () => {
|
|||||||
|
|
||||||
// ---- 修改带宽 ----
|
// ---- 修改带宽 ----
|
||||||
const trafficDialogVisible = ref(false)
|
const trafficDialogVisible = ref(false)
|
||||||
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, traffic_max: 0 })
|
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficGB: 0 })
|
||||||
|
|
||||||
const handleUpdateTraffic = () => {
|
const handleUpdateTraffic = () => {
|
||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
Object.assign(trafficForm, {
|
Object.assign(trafficForm, {
|
||||||
rx_bandwidth: detail.value.rx_bandwidth || 0,
|
rx_bandwidth: detail.value.rx_bandwidth || 0,
|
||||||
tx_bandwidth: detail.value.tx_bandwidth || 0,
|
tx_bandwidth: detail.value.tx_bandwidth || 0,
|
||||||
traffic_max: detail.value.traffic_max || 0
|
_trafficGB: ((detail.value.traffic_max || 0) / 1024).toFixed(2) * 1
|
||||||
})
|
})
|
||||||
trafficDialogVisible.value = true
|
trafficDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -1716,7 +1781,7 @@ const submitUpdateTraffic = async () => {
|
|||||||
fd.append('vm_id', vmId.value)
|
fd.append('vm_id', vmId.value)
|
||||||
fd.append('rx_bandwidth', trafficForm.rx_bandwidth)
|
fd.append('rx_bandwidth', trafficForm.rx_bandwidth)
|
||||||
fd.append('tx_bandwidth', trafficForm.tx_bandwidth)
|
fd.append('tx_bandwidth', trafficForm.tx_bandwidth)
|
||||||
if (trafficForm.traffic_max) fd.append('traffic_max', trafficForm.traffic_max)
|
if (trafficForm._trafficGB) fd.append('traffic_max', Math.round(trafficForm._trafficGB * 1024)) // GB → Mb
|
||||||
const res = await updateVmTraffic(fd)
|
const res = await updateVmTraffic(fd)
|
||||||
if (res?.data?.code === 200) { ElMessage.success('带宽修改成功'); trafficDialogVisible.value = false; loadDetail() }
|
if (res?.data?.code === 200) { ElMessage.success('带宽修改成功'); trafficDialogVisible.value = false; loadDetail() }
|
||||||
else ElMessage.error(extractApiError(res?.data, '修改失败'))
|
else ElMessage.error(extractApiError(res?.data, '修改失败'))
|
||||||
@@ -1770,6 +1835,78 @@ const submitMigrateVm = async () => {
|
|||||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 数据迁移 ----
|
||||||
|
const dataMigrateVisible = ref(false)
|
||||||
|
const dataMigrateLoading = ref(false)
|
||||||
|
const dataMigrateHostsLoading = ref(false)
|
||||||
|
const dataMigrateServiceOptions = ref([])
|
||||||
|
const dataMigrateHostOptions = ref([])
|
||||||
|
const dataMigrateProgressVisible = ref(false)
|
||||||
|
const dataMigrateProgressLoading = ref(false)
|
||||||
|
const dataMigrateProgressData = ref(null)
|
||||||
|
const dataMigrationId = ref('')
|
||||||
|
const dataMigrateForm = reactive({ target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||||
|
|
||||||
|
const handleDataMigrateVm = async () => {
|
||||||
|
Object.assign(dataMigrateForm, { target_service_id: null, target_host_id: null, ipv4_num: 0, ipv6_num: 0 })
|
||||||
|
dataMigrateVisible.value = true
|
||||||
|
dataMigrateLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getKvmServiceList({ page: 1, count: 10 })
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const inner = res.data.data
|
||||||
|
const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||||||
|
dataMigrateServiceOptions.value = raw.map(s => ({ id: s.id ?? s.Id, name: s.name ?? s.Name }))
|
||||||
|
}
|
||||||
|
} catch { /* */ } finally { dataMigrateLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDataMigrateHosts = async () => {
|
||||||
|
if (!dataMigrateForm.target_service_id) return
|
||||||
|
dataMigrateHostsLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getRemoteHostList({ service_id: dataMigrateForm.target_service_id, page: 1, page_size: 10 })
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const inner = res.data.data
|
||||||
|
dataMigrateHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.data || [])
|
||||||
|
}
|
||||||
|
} catch { /* */ } finally { dataMigrateHostsLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitDataMigrate = async () => {
|
||||||
|
if (!dataMigrateForm.target_service_id) { ElMessage.warning('请选择目标主控服务'); return }
|
||||||
|
if (!dataMigrateForm.target_host_id) { ElMessage.warning('请选择目标宿主机'); return }
|
||||||
|
actionLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('source_service_id', serviceId.value)
|
||||||
|
fd.append('source_vm_id', vmId.value)
|
||||||
|
fd.append('target_service_id', dataMigrateForm.target_service_id)
|
||||||
|
fd.append('target_host_id', dataMigrateForm.target_host_id)
|
||||||
|
if (dataMigrateForm.ipv4_num) fd.append('ipv4_num', dataMigrateForm.ipv4_num)
|
||||||
|
if (dataMigrateForm.ipv6_num) fd.append('ipv6_num', dataMigrateForm.ipv6_num)
|
||||||
|
const res = await dataMigrateVm(fd)
|
||||||
|
if (res?.data?.code === 200) {
|
||||||
|
ElMessage.success('数据迁移已发起')
|
||||||
|
dataMigrationId.value = res.data.data?.migration_id || ''
|
||||||
|
dataMigrateVisible.value = false
|
||||||
|
// 自动打开进度弹窗
|
||||||
|
dataMigrateProgressData.value = res.data.data
|
||||||
|
dataMigrateProgressVisible.value = true
|
||||||
|
} else ElMessage.error(extractApiError(res?.data, '发起迁移失败'))
|
||||||
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '发起迁移失败')) } finally { actionLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDataMigrateProgress = async () => {
|
||||||
|
if (!dataMigrationId.value) return
|
||||||
|
dataMigrateProgressLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getDataMigrateProgress({ migration_id: dataMigrationId.value, service_id: serviceId.value })
|
||||||
|
if (res?.data?.code === 200) dataMigrateProgressData.value = res.data.data
|
||||||
|
else ElMessage.warning('暂无进度信息')
|
||||||
|
} catch { /* */ } finally { dataMigrateProgressLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
// ---- VNC 连接 ----
|
// ---- VNC 连接 ----
|
||||||
const vncDialogVisible = ref(false)
|
const vncDialogVisible = ref(false)
|
||||||
const vncNodeId = ref(null)
|
const vncNodeId = ref(null)
|
||||||
@@ -2048,7 +2185,7 @@ const handleVolUnmount = (row) => {
|
|||||||
}
|
}
|
||||||
const loadVmListOptions = async () => {
|
const loadVmListOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 200 })
|
const res = await getVmList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
const inner = res.data.data
|
const inner = res.data.data
|
||||||
vmListOptions.value = inner.vms || inner.data || (Array.isArray(inner) ? inner : [])
|
vmListOptions.value = inner.vms || inner.data || (Array.isArray(inner) ? inner : [])
|
||||||
@@ -2790,6 +2927,22 @@ const triggerTabLoad = (tab) => {
|
|||||||
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
|
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
|
||||||
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
|
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
|
||||||
if (tab === 'userNetworking') loadVmNetworkingList()
|
if (tab === 'userNetworking') loadVmNetworkingList()
|
||||||
|
if (tab === 'security') loadSgLockInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求安全组详情补充 lock 字段
|
||||||
|
const loadSgLockInfo = async () => {
|
||||||
|
const groups = [vmPortGroup.value, vmOutPortGroup.value].filter(Boolean)
|
||||||
|
for (const sg of groups) {
|
||||||
|
try {
|
||||||
|
const res = await getSecurityGroupDetail({ service_id: serviceId.value, id: sg.id })
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const d = res.data.data.group || res.data.data.data || res.data.data
|
||||||
|
if (vmPortGroup.value?.id === sg.id) vmPortGroup.value = { ...vmPortGroup.value, lock: d.lock ?? sg.lock }
|
||||||
|
if (vmOutPortGroup.value?.id === sg.id) vmOutPortGroup.value = { ...vmOutPortGroup.value, lock: d.lock ?? sg.lock }
|
||||||
|
}
|
||||||
|
} catch { /* */ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(vmId, () => { if (isPageActive) initPage() })
|
watch(vmId, () => { if (isPageActive) initPage() })
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ const formatTimestamp = (ts) => {
|
|||||||
|
|
||||||
const loadHostOptions = async () => {
|
const loadHostOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 100 })
|
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 })
|
||||||
const body = res?.data
|
const body = res?.data
|
||||||
if (body?.code === 200 && body?.data) {
|
if (body?.code === 200 && body?.data) {
|
||||||
const inner = body.data
|
const inner = body.data
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
要让 Cursor 能够高效地完成这项复杂任务,你需要将“需求”转化为“结构化指令”。不要一次性丢给它所有任务,建议按照**“调研-分析-开发-优化”**的逻辑分阶段进行。
|
||||||
|
|
||||||
|
以下是为你设计的 **Prompt 模板**,你可以根据实际情况稍作修改:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第一阶段:现状分析与评估(核心提示词)
|
||||||
|
|
||||||
|
**使用场景:** 将 `openapi.json` 拖入 Cursor,打开你的项目根目录,发送以下 Prompt:
|
||||||
|
|
||||||
|
> **Prompt:**
|
||||||
|
> 我现在正在对接用户商品管理模块,API 定义在 `@ApiServer-web-admin_dashboard_pc/默认模块.openapi.json` 中。
|
||||||
|
>
|
||||||
|
> 请你执行以下任务:
|
||||||
|
> 1. **接口完整性对比:** 分析该 OpenAPI 文件中所有以 `/product` 或 `user` 开头的接口。对比我当前项目中 `src/api/product.ts`(或对应目录)的实现,列出缺少实现的接口、参数定义不一致的接口。
|
||||||
|
> 2. **逻辑可行性判断:** 检查这些接口的请求方式、入参结构和响应字段,判断是否存在潜在的逻辑错误(如字段类型不匹配、缺失必要的分页参数等)。
|
||||||
|
> 3. **输出报告:** 请整理出一份表格,列出:接口路径 | 功能描述 | 是否已实现 | 潜在风险/待修复点。
|
||||||
|
>
|
||||||
|
> 请先完成这一步,不要急于写代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第二阶段:功能开发与组件化(架构层)
|
||||||
|
|
||||||
|
**使用场景:** 在第一阶段确认无误后,让 Cursor 介入代码实现。
|
||||||
|
|
||||||
|
> **Prompt:**
|
||||||
|
> 基于上一步的分析结果,我们需要进行代码落地。请遵循以下工程化标准:
|
||||||
|
> 1. **请求实现:** 按照我现有项目的请求风格(例如 `axios` + `ts-interface`),补全缺失的接口请求函数。
|
||||||
|
> 2. **组件化拆分:** 在实现业务页面时,请评估哪些逻辑可以抽离为公共组件(例如:商品详情预览框、批量操作栏、规格选择器)。如果某个功能在多个页面有重复逻辑,请将其提取为独立的 Component,并说明该组件的 Props 定义。
|
||||||
|
> 3. **嵌套与快捷入口:** 针对“商品管理”模块,请思考是否存在需要嵌套展示的功能(如:点击列表行展开详细信息,或弹窗式管理)。如果是,请直接使用 `ant-design` (或你使用的框架) 的组件来实现这种交互,并保证良好的用户体验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第三阶段:样式与交互优化(视觉层)
|
||||||
|
|
||||||
|
**使用场景:** 页面雏形出来后,让 Cursor 优化细节。
|
||||||
|
|
||||||
|
> **Prompt:**
|
||||||
|
> 现在页面已经实现了基本功能,请重点优化 UX/UI:
|
||||||
|
> 1. **布局优化:** 请检查当前的表格布局,确保字段对齐合理、间距符合 Material Design 或 Ant Design 标准。
|
||||||
|
> 2. **交互美化:** 增加必要的 Loading 状态、空状态(Empty Data)、操作反馈(Toast/Message)。
|
||||||
|
> 3. **响应式评估:** 检查该 Dashboard 页面在不同分辨率下的表现,是否需要将部分复杂的筛选表单折叠到“高级筛选”面板中,以保持界面整洁。
|
||||||
|
> 4. **视觉建议:** 请评估当前样式是否过于简单,建议增加一些层级感(例如卡片式布局、阴影、不同层级的按钮样式)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 给 Cursor 的“提效秘诀”(必读):
|
||||||
|
|
||||||
|
为了让 Cursor 表现得更好,请务必配合以下操作:
|
||||||
|
|
||||||
|
1. **利用 `@Codebase`:** 在 Prompt 中提到 `@Codebase`,让 Cursor 全局检索你的代码规范(例如:你以前写的 `api` 请求是怎么写的,`component` 是怎么封装的),这样它生成的代码才不会“违和”。
|
||||||
|
2. **小步快跑:** 如果 API 接口很多(比如超过 10 个),**千万不要**让它一次性全写完。一次让它写 2-3 个接口或一个组件,改完一个确认一个。
|
||||||
|
3. **强制提供 Interface:** 在提示词中加上一句:“请确保所有接口请求和响应都定义了对应的 `TypeScript Interface`,严禁使用 `any` 类型。”
|
||||||
|
4. **指定参考文件:** 如果你有已经写好的“完美的页面”,在对话框中输入 `#` 引用该文件,告诉 Cursor:“请参照 `src/pages/TemplatePage.tsx` 的代码规范和交互逻辑来实现商品管理页面。”
|
||||||
|
|
||||||
|
### 总结你的检查清单(作为你的最终验收标准):
|
||||||
|
* [ ] OpenAPI 定义的字段是否全覆盖?
|
||||||
|
* [ ] 是否每个接口都写了 try-catch 或统一的错误处理?
|
||||||
|
* [ ] 是否有复用性强的组件?(避免重复代码)
|
||||||
|
* [ ] 是否有交互反馈?(Loading/Success Tips)
|
||||||
|
* [ ] 样式是否统一?(使用了全局定义的颜色/间距变量)
|
||||||
|
|
||||||
|
如果你直接丢给它一个庞大的任务,Cursor 往往会产生“幻觉”或者代码质量下降,**分步骤引导**是使用 AI 开发复杂后台的最佳实践。
|
||||||
+381
-1
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.1",
|
"openapi": "3.0.1",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "默认模块",
|
"title": "默认模块",
|
||||||
"description": "",
|
"description": "新增跨服务虚拟机数据迁移接口,支持通过 rsync + SSH tunneluser 在两个 kvm-virtctl 之间进行虚拟机数据传输和恢复。",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -35,6 +35,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "admin-用户虚拟机"
|
"name": "admin-用户虚拟机"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UserGoods"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -4060,6 +4063,383 @@
|
|||||||
},
|
},
|
||||||
"security": []
|
"security": []
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/good/user_goods/list": {
|
||||||
|
"get": {
|
||||||
|
"summary": "获取用户商品列表",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [
|
||||||
|
"UserGoods"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "page",
|
||||||
|
"in": "query",
|
||||||
|
"description": "页码",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "count",
|
||||||
|
"in": "query",
|
||||||
|
"description": "每页数量",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query",
|
||||||
|
"description": "按用户 ID 筛选",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "good_id",
|
||||||
|
"in": "query",
|
||||||
|
"description": "按商品 ID 筛选",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"in": "query",
|
||||||
|
"description": "关键词搜索(按商品名称模糊匹配)",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer {{token}}",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserGoods"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"all_count": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "总数"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"headers": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/good/user_goods/detail": {
|
||||||
|
"get": {
|
||||||
|
"summary": "获取用户商品详情",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [
|
||||||
|
"UserGoods"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "query",
|
||||||
|
"description": "用户商品 ID",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer {{token}}",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/UserGoods"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"headers": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/good/user_goods/create": {
|
||||||
|
"post": {
|
||||||
|
"summary": "新增用户商品",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [
|
||||||
|
"UserGoods"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer {{token}}",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"good_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "商品 ID",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户 ID",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "订单 ID",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"good_plan_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "商品套餐 ID",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"item_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "归属项 ID",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"renew_price": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "续费价格",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"base_price": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "基础价格",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "备注说明",
|
||||||
|
"example": ""
|
||||||
|
},
|
||||||
|
"expire_time": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "到期时间,格式:2006-01-02 15:04:05",
|
||||||
|
"example": ""
|
||||||
|
},
|
||||||
|
"order_args": {
|
||||||
|
"description": "订单参数 json",
|
||||||
|
"example": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"good_id",
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"examples": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "创建成功",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/UserGoods"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"headers": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/good/user_goods/update": {
|
||||||
|
"post": {
|
||||||
|
"summary": "修改用户商品",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [
|
||||||
|
"UserGoods"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer {{token}}",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户商品 ID",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "备注说明",
|
||||||
|
"example": ""
|
||||||
|
},
|
||||||
|
"renew_price": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "续费价格",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"base_price": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "基础价格",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
"expire_time": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "到期时间,格式:2006-01-02 15:04:05",
|
||||||
|
"example": ""
|
||||||
|
},
|
||||||
|
"item_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "归属项 ID",
|
||||||
|
"example": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "修改成功",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/UserGoods"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"headers": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/admin/good/user_goods/delete": {
|
||||||
|
"delete": {
|
||||||
|
"summary": "删除用户商品",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [
|
||||||
|
"UserGoods"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "query",
|
||||||
|
"description": "用户商品 ID",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer {{token}}",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer {{token}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "删除成功",
|
||||||
|
"headers": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
Reference in New Issue
Block a user