feat: add AWS and MF consumption management #38
@@ -0,0 +1,19 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
const base = '/api/v1/admin/server/aws_service'
|
||||
|
||||
export const getAwsServiceList = (params) => http2.get(`${base}/list`, { params })
|
||||
export const getAwsServiceDetail = (id) => http2.get(`${base}/detail`, { params: { id } })
|
||||
export const createAwsService = (data) => http2.post(`${base}/create`, data)
|
||||
export const updateAwsService = (data) => http2.post(`${base}/update`, data)
|
||||
export const testAwsService = (id) => http2.post(`${base}/test`, null, { params: { id } })
|
||||
export const deleteAwsService = (id) => http2.delete(`${base}/delete`, { params: { id } })
|
||||
|
||||
export const getAwsGoodsList = (params) => http2.get(`${base}/goods/list`, { params })
|
||||
export const getAwsGoodsDetail = (id) => http2.get(`${base}/goods/detail`, { params: { id } })
|
||||
export const createAwsGoods = (data) => http2.post(`${base}/goods/create`, data)
|
||||
export const updateAwsGoods = (data) => http2.post(`${base}/goods/update`, data)
|
||||
export const deleteAwsGoods = (id) => http2.delete(`${base}/goods/delete`, { params: { id } })
|
||||
|
||||
export const getAwsInstanceList = (params) => http2.get(`${base}/instances/list`, { params })
|
||||
export const retryAwsInstance = (instanceId) => http2.post(`${base}/instances/retry`, null, { params: { instance_id: instanceId } })
|
||||
@@ -0,0 +1,19 @@
|
||||
import { http2 } from '@/utils/request.js'
|
||||
|
||||
const base = '/api/v1/admin/activity/mf_consumption'
|
||||
|
||||
/** 获取魔方特殊消费活动列表 */
|
||||
export const getMFConsumptionPromotionList = (params) =>
|
||||
http2.get(`${base}/list`, { params })
|
||||
|
||||
/** 创建魔方特殊消费活动 */
|
||||
export const createMFConsumptionPromotion = (data) =>
|
||||
http2.post(`${base}/create`, data)
|
||||
|
||||
/** 软删除魔方特殊消费活动 */
|
||||
export const deleteMFConsumptionPromotion = (id) =>
|
||||
http2.delete(`${base}/delete`, { params: { id } })
|
||||
|
||||
/** 永久清理指定时间范围内的 MF 扣减型消费记录 */
|
||||
export const clearMFConsumptionHistories = (data) =>
|
||||
http2.delete(`${base}/histories/clear`, { data })
|
||||
@@ -91,6 +91,10 @@ export const menus = [
|
||||
{
|
||||
path: '/activity/groupbuy',
|
||||
title: '拼团管理'
|
||||
},
|
||||
{
|
||||
path: '/activity/mf-consumption',
|
||||
title: '魔方特殊消费'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -176,6 +180,16 @@ export const menus = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/aws',
|
||||
title: 'AWS 服务管理',
|
||||
icon: 'Cloudy',
|
||||
children: [
|
||||
{ path: '/aws/services', title: '服务配置' },
|
||||
{ path: '/aws/goods', title: '商品绑定' },
|
||||
{ path: '/aws/instances', title: '履约实例' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '虚拟化平台管理',
|
||||
icon: 'Platform',
|
||||
|
||||
@@ -374,6 +374,14 @@ const routes = [
|
||||
meta: {
|
||||
title: '拼团管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'mf-consumption',
|
||||
name: 'MFConsumptionPromotion',
|
||||
component: () => import('../views/activity/MFConsumptionPromotion.vue'),
|
||||
meta: {
|
||||
title: '魔方特殊消费'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -663,6 +671,33 @@ const routes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
// AWS 远端商品管理
|
||||
{
|
||||
path: 'aws',
|
||||
name: 'Aws',
|
||||
meta: { title: 'AWS 服务管理', icon: 'Cloudy' },
|
||||
redirect: '/aws/services',
|
||||
children: [
|
||||
{
|
||||
path: 'services',
|
||||
name: 'AwsServices',
|
||||
component: () => import('../views/aws/AwsServices.vue'),
|
||||
meta: { title: '服务配置' }
|
||||
},
|
||||
{
|
||||
path: 'goods',
|
||||
name: 'AwsGoods',
|
||||
component: () => import('../views/aws/AwsGoods.vue'),
|
||||
meta: { title: '商品绑定' }
|
||||
},
|
||||
{
|
||||
path: 'instances',
|
||||
name: 'AwsInstances',
|
||||
component: () => import('../views/aws/AwsInstances.vue'),
|
||||
meta: { title: '履约实例' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 站点审计路由
|
||||
{
|
||||
path: 'audit',
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div class="mf-consumption-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>魔方特殊消费</h2>
|
||||
<p>设置订单消费计入用户组累计消费的特殊倍率,活动时间按左闭右开区间生效。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="danger" plain @click="openClearDialog">
|
||||
<el-icon><Delete /></el-icon>清理消费记录
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon>新增活动
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
class="description-alert"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="倍率只影响消费记录计入用户组升级的累计值,不直接修改订单应付金额。"
|
||||
/>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="活动名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="tag" label="活动标签" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="消费倍率" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning" effect="plain">{{ row.multiplier }} 倍</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="promotionStatus(row).type">{{ promotionStatus(row).label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.startTime ?? row.start_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.endTime ?? row.end_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.CreatedAt ?? row.createdAt ?? row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="removePromotion(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无特殊消费活动" /></template>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.count"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="loadPromotions"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="createDialogVisible" title="新增魔方特殊消费活动" width="620px" destroy-on-close>
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="105px">
|
||||
<el-form-item label="活动名称" prop="name">
|
||||
<el-input v-model="createForm.name" maxlength="120" show-word-limit placeholder="例如:MF 周年消费三倍活动" />
|
||||
</el-form-item>
|
||||
<el-form-item label="活动标签" prop="tag">
|
||||
<el-input v-model="createForm.tag" maxlength="120" show-word-limit placeholder="必须唯一,例如:20260824-20260831/Price*3" />
|
||||
<div class="form-tip">标签会写入消费记录,创建后不可修改。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="timeRange">
|
||||
<el-date-picker
|
||||
v-model="createForm.timeRange"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间(包含)"
|
||||
end-placeholder="结束时间(不包含)"
|
||||
range-separator="至"
|
||||
:default-time="defaultTimes"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="消费倍率" prop="multiplier">
|
||||
<el-input-number v-model="createForm.multiplier" :min="1" :step="1" step-strictly style="width: 180px" />
|
||||
<span class="multiplier-suffix">倍</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="submitCreate">创建活动</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="clearDialogVisible" title="清理 MF 消费记录" width="620px" destroy-on-close>
|
||||
<el-alert
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="危险操作:记录将被永久删除且无法恢复"
|
||||
description="只清理 source=MF、非余额变更且位于所选时间范围内的扣减型消费记录;退款与补偿记录不会删除。清理后后端会重新计算受影响用户的消费用户组。"
|
||||
/>
|
||||
<el-form ref="clearFormRef" :model="clearForm" :rules="clearRules" label-width="105px" class="clear-form">
|
||||
<el-form-item label="清理范围" prop="timeRange">
|
||||
<el-date-picker
|
||||
v-model="clearForm.timeRange"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间(包含)"
|
||||
end-placeholder="结束时间(不包含)"
|
||||
range-separator="至"
|
||||
:default-time="defaultTimes"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认文字" prop="confirmation">
|
||||
<el-input v-model="clearForm.confirmation" autocomplete="off" placeholder="请输入:确认清理" />
|
||||
<div class="form-tip">输入“确认清理”后才能提交。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="clearDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="clearing" :disabled="clearForm.confirmation !== '确认清理'" @click="submitClear">
|
||||
永久清理
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
clearMFConsumptionHistories,
|
||||
createMFConsumptionPromotion,
|
||||
deleteMFConsumptionPromotion,
|
||||
getMFConsumptionPromotionList
|
||||
} from '@/api/admin/mfConsumption.js'
|
||||
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const clearing = ref(false)
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const query = reactive({ page: 1, count: 20 })
|
||||
const createDialogVisible = ref(false)
|
||||
const clearDialogVisible = ref(false)
|
||||
const createFormRef = ref()
|
||||
const clearFormRef = ref()
|
||||
const defaultTimes = [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
|
||||
const newCreateForm = () => ({ name: '', tag: '', timeRange: [], multiplier: 1 })
|
||||
const createForm = reactive(newCreateForm())
|
||||
const clearForm = reactive({ timeRange: [], confirmation: '' })
|
||||
|
||||
const validateTimeRange = (_rule, value, callback) => {
|
||||
if (!Array.isArray(value) || value.length !== 2) return callback(new Error('请选择完整的开始和结束时间'))
|
||||
if (new Date(value[1]).getTime() <= new Date(value[0]).getTime()) return callback(new Error('结束时间必须晚于开始时间'))
|
||||
callback()
|
||||
}
|
||||
|
||||
const createRules = {
|
||||
name: [{ required: true, whitespace: true, message: '请输入活动名称', trigger: 'blur' }],
|
||||
tag: [{ required: true, whitespace: true, message: '请输入唯一活动标签', trigger: 'blur' }],
|
||||
timeRange: [{ required: true, validator: validateTimeRange, trigger: 'change' }],
|
||||
multiplier: [{ required: true, type: 'integer', min: 1, message: '倍率必须是大于等于 1 的整数', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const clearRules = {
|
||||
timeRange: [{ required: true, validator: validateTimeRange, trigger: 'change' }],
|
||||
confirmation: [{ validator: (_rule, value, callback) => value === '确认清理' ? callback() : callback(new Error('请输入“确认清理”')), trigger: 'change' }]
|
||||
}
|
||||
|
||||
const responseBody = (response) => response?.data || {}
|
||||
const toUnixSeconds = (value) => Math.floor(new Date(value).getTime() / 1000)
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function promotionStatus(row) {
|
||||
const now = Date.now()
|
||||
const start = new Date(row.startTime ?? row.start_time).getTime()
|
||||
const end = new Date(row.endTime ?? row.end_time).getTime()
|
||||
if (Number.isFinite(start) && now < start) return { label: '未开始', type: 'info' }
|
||||
if (Number.isFinite(end) && now >= end) return { label: '已结束', type: 'info' }
|
||||
return { label: '进行中', type: 'success' }
|
||||
}
|
||||
|
||||
async function loadPromotions() {
|
||||
loading.value = true
|
||||
try {
|
||||
const body = responseBody(await getMFConsumptionPromotionList(query))
|
||||
if (body.code !== 200) return ElMessage.error(body.message || '获取活动列表失败')
|
||||
rows.value = body.data?.data || body.data?.list || []
|
||||
total.value = body.data?.all_count ?? rows.value.length
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.response?.data?.message || '获取活动列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
query.page = 1
|
||||
loadPromotions()
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
Object.assign(createForm, newCreateForm())
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
await createFormRef.value?.validate()
|
||||
creating.value = true
|
||||
try {
|
||||
const body = responseBody(await createMFConsumptionPromotion({
|
||||
name: createForm.name.trim(),
|
||||
tag: createForm.tag.trim(),
|
||||
start_time: toUnixSeconds(createForm.timeRange[0]),
|
||||
end_time: toUnixSeconds(createForm.timeRange[1]),
|
||||
multiplier: createForm.multiplier
|
||||
}))
|
||||
if (body.code !== 200) return ElMessage.error(body.message || '创建活动失败')
|
||||
ElMessage.success('活动创建成功')
|
||||
createDialogVisible.value = false
|
||||
query.page = 1
|
||||
loadPromotions()
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.response?.data?.message || '创建活动失败,请检查活动时间是否重叠、标签是否唯一')
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removePromotion(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除活动“${row.name}”吗?已导入的消费记录不会回溯修改。`,
|
||||
'删除活动',
|
||||
{ type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' }
|
||||
)
|
||||
const body = responseBody(await deleteMFConsumptionPromotion(row.id))
|
||||
if (body.code !== 200) return ElMessage.error(body.message || '删除活动失败')
|
||||
ElMessage.success('活动已删除')
|
||||
if (rows.value.length === 1 && query.page > 1) query.page--
|
||||
loadPromotions()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.response?.data?.message || '删除活动失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openClearDialog() {
|
||||
Object.assign(clearForm, { timeRange: [], confirmation: '' })
|
||||
clearDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitClear() {
|
||||
await clearFormRef.value?.validate()
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`即将永久清理 ${formatTime(clearForm.timeRange[0])} 至 ${formatTime(clearForm.timeRange[1])} 的 MF 消费记录,是否继续?`,
|
||||
'最后确认',
|
||||
{ type: 'error', confirmButtonText: '永久清理', cancelButtonText: '取消', distinguishCancelAndClose: true }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
clearing.value = true
|
||||
try {
|
||||
const body = responseBody(await clearMFConsumptionHistories({
|
||||
start_time: toUnixSeconds(clearForm.timeRange[0]),
|
||||
end_time: toUnixSeconds(clearForm.timeRange[1]),
|
||||
confirm: true
|
||||
}))
|
||||
if (body.code !== 200) return ElMessage.error(body.message || '清理消费记录失败')
|
||||
const result = body.data || {}
|
||||
ElMessage.success(`清理完成:删除 ${result.deleted_count ?? 0} 条记录,影响 ${result.affected_user_count ?? 0} 个用户`)
|
||||
clearDialogVisible.value = false
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.response?.data?.message || '清理消费记录失败')
|
||||
} finally {
|
||||
clearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadPromotions)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mf-consumption-page { padding: 20px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 20px 24px; margin-bottom: 16px; border: 1px solid #e4e7ed; border-radius: 12px; background: linear-gradient(135deg, #fff8ed, #fff); }
|
||||
.page-header h2 { margin: 0; color: #303133; }
|
||||
.page-header p { margin: 7px 0 0; color: #606266; }
|
||||
.header-actions { display: flex; flex-shrink: 0; }
|
||||
.description-alert { margin-bottom: 16px; }
|
||||
.pagination { display: flex; justify-content: flex-end; padding-top: 18px; }
|
||||
.form-tip { margin-top: 5px; color: #909399; font-size: 12px; line-height: 1.4; }
|
||||
.multiplier-suffix { margin-left: 8px; color: #606266; }
|
||||
.clear-form { margin-top: 20px; }
|
||||
@media (max-width: 768px) {
|
||||
.page-header { align-items: flex-start; flex-direction: column; }
|
||||
.header-actions { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="header"><div><h2>AWS 商品绑定</h2><p>配置商品可售目录、默认参数和强制策略</p></div><el-button type="primary" @click="openCreate"><el-icon><Plus /></el-icon>新增绑定</el-button></div>
|
||||
<div class="filters"><el-select v-model="query.service_id" clearable placeholder="全部 AWS 服务" @change="search"><el-option v-for="s in services" :key="s.id" :label="s.name" :value="s.id"/></el-select><el-button @click="reset">重置</el-button></div>
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70"/>
|
||||
<el-table-column label="商品 ID" width="105"><template #default="{row}"><el-link type="primary" @click="$router.push({path:'/product/manage',query:{good_id:row.goodId}})">{{row.goodId}}</el-link></template></el-table-column>
|
||||
<el-table-column label="服务" min-width="140"><template #default="{row}"><el-link type="primary" @click="$router.push({path:'/aws/services',query:{id:row.serviceId}})">{{serviceName(row.serviceId)}} (#{{row.serviceId}})</el-link></template></el-table-column>
|
||||
<el-table-column label="区域" min-width="190"><template #default="{row}"><el-tag v-for="v in row.allowedRegions||[]" :key="v" size="small" class="tag">{{v}}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="exchangeRate" label="汇率" width="90"/>
|
||||
<el-table-column prop="gracePeriodHours" label="宽限期(h)" width="100"/>
|
||||
<el-table-column label="强制策略" min-width="210"><template #default="{row}"><el-tag v-if="row.forceAssociateEip" size="small">EIP</el-tag><el-tag v-if="row.forceEncryptedRootVolume" size="small" type="success">加密系统盘</el-tag><el-tag v-if="row.forceInstallAgent" size="small" type="warning">Agent</el-tag></template></el-table-column>
|
||||
<el-table-column label="状态" width="80"><template #default="{row}"><el-tag :type="row.enabled?'success':'info'">{{row.enabled?'启用':'停用'}}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="135" fixed="right"><template #default="{row}"><el-button link type="primary" @click="openEdit(row)">编辑</el-button><el-popconfirm title="确认删除无历史记录的绑定?" @confirm="remove(row)"><template #reference><el-button link type="danger">删除</el-button></template></el-popconfirm></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pager"><el-pagination v-model:current-page="query.page" v-model:page-size="query.count" :total="total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next" @change="load"/></div>
|
||||
<el-dialog v-model="dialog" :title="editing?'编辑 AWS 商品绑定':'新增 AWS 商品绑定'" width="820px" top="4vh" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="145px">
|
||||
<el-row :gutter="18"><el-col :span="12"><el-form-item label="商品 ID" prop="good_id"><el-input-number v-model="form.good_id" :min="1" :disabled="editing" style="width:100%"/></el-form-item></el-col><el-col :span="12"><el-form-item label="AWS 服务" prop="service_id"><el-select v-model="form.service_id" style="width:100%"><el-option v-for="s in services" :key="s.id" :label="`${s.name} (#${s.id})`" :value="s.id"/></el-select></el-form-item></el-col></el-row>
|
||||
<el-form-item label="允许区域" prop="allowed_regions"><el-select v-model="form.allowed_regions" multiple filterable allow-create default-first-option placeholder="输入 Region 后回车" style="width:100%"/></el-form-item>
|
||||
<el-form-item label="镜像分类白名单"><el-select v-model="form.allowed_image_categories" multiple filterable allow-create default-first-option style="width:100%"/></el-form-item>
|
||||
<el-form-item label="镜像白名单"><el-select v-model="form.allowed_images" multiple filterable allow-create default-first-option style="width:100%"/></el-form-item>
|
||||
<el-form-item label="规格分类白名单"><el-select v-model="form.allowed_type_categories" multiple filterable allow-create default-first-option style="width:100%"/></el-form-item>
|
||||
<el-form-item label="规格白名单"><el-select v-model="form.allowed_types" multiple filterable allow-create default-first-option style="width:100%"/></el-form-item>
|
||||
<el-row :gutter="18"><el-col :span="12"><el-form-item label="人民币汇率" prop="exchange_rate"><el-input-number v-model="form.exchange_rate" :min="0.000001" :precision="6" style="width:100%"/></el-form-item></el-col><el-col :span="12"><el-form-item label="宽限期(小时)"><el-input-number v-model="form.grace_period_hours" :min="0" style="width:100%"/></el-form-item></el-col></el-row>
|
||||
<el-form-item label="强制策略"><el-checkbox v-model="form.force_associate_eip">绑定 EIP</el-checkbox><el-checkbox v-model="form.force_encrypted_root_volume">加密系统盘</el-checkbox><el-checkbox v-model="form.force_install_agent">安装 Agent</el-checkbox><el-checkbox v-model="form.enabled">启用绑定</el-checkbox></el-form-item>
|
||||
<el-form-item label="默认创建参数" prop="defaults_text"><el-input v-model="form.defaults_text" type="textarea" :rows="7" placeholder="{}" class="json"/></el-form-item>
|
||||
<el-form-item label="商品约束" prop="constraints_text"><el-input v-model="form.constraints_text" type="textarea" :rows="6" placeholder="{}" class="json"/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialog=false">取消</el-button><el-button type="primary" :loading="submitting" @click="submit">保存</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { createAwsGoods, deleteAwsGoods, getAwsGoodsDetail, getAwsGoodsList, getAwsServiceList, updateAwsGoods } from '@/api/admin/awsService.js'
|
||||
const route=useRoute(), loading=ref(false), submitting=ref(false), rows=ref([]), services=ref([]), total=ref(0), dialog=ref(false), editing=ref(false), formRef=ref()
|
||||
const query=reactive({page:1,count:20,service_id:route.query.service_id?Number(route.query.service_id):undefined})
|
||||
const empty=()=>({id:null,good_id:null,service_id:null,allowed_regions:[],allowed_image_categories:[],allowed_images:[],allowed_type_categories:[],allowed_types:[],defaults_text:'{}',constraints_text:'{}',force_associate_eip:false,force_encrypted_root_volume:true,force_install_agent:true,grace_period_hours:168,exchange_rate:1,enabled:true})
|
||||
const form=reactive(empty())
|
||||
const jsonRule=(_,value,cb)=>{try{const v=JSON.parse(value||'{}');if(!v||Array.isArray(v)||typeof v!=='object')throw new Error();cb()}catch{cb(new Error('请输入有效的 JSON 对象'))}}
|
||||
const rules={good_id:[{required:true,message:'请输入商品 ID'}],service_id:[{required:true,message:'请选择服务'}],allowed_regions:[{type:'array',required:true,min:1,message:'至少填写一个区域'}],exchange_rate:[{required:true,message:'请输入大于 0 的汇率'}],defaults_text:[{validator:jsonRule}],constraints_text:[{validator:jsonRule}]}
|
||||
const body=r=>r?.data||{}
|
||||
async function loadServices(){const b=body(await getAwsServiceList({page:1,count:100}));if(b.code===200)services.value=b.data?.data||b.data?.list||[]}
|
||||
async function load(){loading.value=true;try{const params={...query};if(!params.service_id)delete params.service_id;const b=body(await getAwsGoodsList(params));if(b.code===200){rows.value=b.data?.data||b.data?.list||[];total.value=b.data?.all_count||0}else ElMessage.error(b.message||'加载失败')}finally{loading.value=false}}
|
||||
const serviceName=id=>services.value.find(v=>v.id===id)?.name||'服务'
|
||||
function search(){query.page=1;load()} function reset(){query.service_id=undefined;search()}
|
||||
function openCreate(){editing.value=false;Object.assign(form,empty(),{service_id:query.service_id||null});dialog.value=true}
|
||||
async function openEdit(row){editing.value=true;let data=row;try{const b=body(await getAwsGoodsDetail(row.id));if(b.code===200)data=b.data}catch{}Object.assign(form,{id:data.id,good_id:data.goodId,service_id:data.serviceId,allowed_regions:[...(data.allowedRegions||[])],allowed_image_categories:[...(data.allowedImageCategories||[])],allowed_images:[...(data.allowedImages||[])],allowed_type_categories:[...(data.allowedTypeCategories||[])],allowed_types:[...(data.allowedTypes||[])],defaults_text:JSON.stringify(data.defaults||{},null,2),constraints_text:JSON.stringify(data.constraints||{},null,2),force_associate_eip:!!data.forceAssociateEip,force_encrypted_root_volume:!!data.forceEncryptedRootVolume,force_install_agent:!!data.forceInstallAgent,grace_period_hours:data.gracePeriodHours??168,exchange_rate:data.exchangeRate??1,enabled:!!data.enabled});dialog.value=true}
|
||||
function payload(){return{service_id:form.service_id,allowed_regions:form.allowed_regions,allowed_image_categories:form.allowed_image_categories,allowed_images:form.allowed_images,allowed_type_categories:form.allowed_type_categories,allowed_types:form.allowed_types,defaults:JSON.parse(form.defaults_text||'{}'),constraints:JSON.parse(form.constraints_text||'{}'),force_associate_eip:form.force_associate_eip,force_encrypted_root_volume:form.force_encrypted_root_volume,force_install_agent:form.force_install_agent,grace_period_hours:form.grace_period_hours,exchange_rate:form.exchange_rate,enabled:form.enabled}}
|
||||
async function submit(){await formRef.value?.validate();submitting.value=true;try{const data=payload();if(editing.value)data.id=form.id;else data.good_id=form.good_id;const b=body(await(editing.value?updateAwsGoods(data):createAwsGoods(data)));if(b.code===200){ElMessage.success('保存成功');dialog.value=false;load()}else ElMessage.error(b.message||'保存失败')}finally{submitting.value=false}}
|
||||
async function remove(row){const b=body(await deleteAwsGoods(row.id));if(b.code===200){ElMessage.success('删除成功');load()}else ElMessage.error(b.message||'删除失败')}
|
||||
onMounted(()=>{loadServices();load()})
|
||||
</script>
|
||||
<style scoped>
|
||||
.page{padding:20px}.header{display:flex;justify-content:space-between;align-items:center;padding:20px 24px;margin-bottom:18px;border:1px solid #d9ecff;border-radius:12px;background:linear-gradient(135deg,#f0f7ff,#f7fbff)}h2{margin:0}.header p{margin:6px 0 0;color:#909399}.filters{display:flex;gap:10px;margin-bottom:16px}.filters .el-select{width:260px}.pager{display:flex;justify-content:flex-end;padding-top:16px}.tag{margin:2px}.json :deep(textarea){font-family:Consolas,Monaco,monospace}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="header"><div><h2>AWS 履约实例</h2><p>查看远端实例快照并审核失败任务</p></div><el-button @click="load">刷新</el-button></div>
|
||||
<div class="filters"><el-select v-model="query.status" clearable placeholder="全部履约状态" @change="search"><el-option v-for="s in statuses" :key="s" :label="statusText(s)" :value="s"/></el-select><el-button @click="reset">重置</el-button></div>
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<el-table-column prop="id" label="实例 ID" width="90"/>
|
||||
<el-table-column label="用户商品" width="105"><template #default="{row}"><el-link type="primary" @click="$router.push(`/user-goods/detail/${row.userGoodsId}`)">{{row.userGoodsId}}</el-link></template></el-table-column>
|
||||
<el-table-column label="订单" width="100"><template #default="{row}"><el-link type="primary" @click="$router.push({path:'/order/list',query:{order_id:row.orderId}})">{{row.orderId}}</el-link></template></el-table-column>
|
||||
<el-table-column label="服务" width="95"><template #default="{row}"><el-link type="primary" @click="$router.push({path:'/aws/services',query:{id:row.serviceId}})">{{row.serviceId}}</el-link></template></el-table-column>
|
||||
<el-table-column prop="region" label="Region" min-width="125"/>
|
||||
<el-table-column prop="remoteAwsInstanceId" label="AWS 实例" min-width="160" show-overflow-tooltip/>
|
||||
<el-table-column label="履约状态" width="110"><template #default="{row}"><el-tag :type="statusType(row.provisionStatus)">{{statusText(row.provisionStatus)}}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="pendingAction" label="待执行" width="90"/>
|
||||
<el-table-column prop="retryCount" label="重试" width="65"/>
|
||||
<el-table-column prop="lastError" label="最近错误" min-width="180" show-overflow-tooltip/>
|
||||
<el-table-column label="最近同步" width="170"><template #default="{row}">{{formatTime(row.lastSyncedAt)}}</template></el-table-column>
|
||||
<el-table-column label="操作" width="135" fixed="right"><template #default="{row}"><el-button link type="primary" @click="showDetail(row)">详情</el-button><el-popconfirm v-if="row.provisionStatus==='failed'" title="确认审核并重试该履约?" @confirm="retry(row)"><template #reference><el-button link type="warning" :loading="retryingId===row.id">重试</el-button></template></el-popconfirm></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pager"><el-pagination v-model:current-page="query.page" v-model:page-size="query.count" :total="total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next" @change="load"/></div>
|
||||
<el-drawer v-model="drawer" title="AWS 履约详情" size="600px">
|
||||
<template v-if="current"><el-descriptions :column="2" border><el-descriptions-item label="实例 ID">{{current.id}}</el-descriptions-item><el-descriptions-item label="状态">{{statusText(current.provisionStatus)}}</el-descriptions-item><el-descriptions-item label="远端实例">{{current.remoteAwsInstanceId||'-'}}</el-descriptions-item><el-descriptions-item label="任务 ID">{{current.remoteTaskId||'-'}}</el-descriptions-item><el-descriptions-item label="错误代码">{{current.lastErrorCode||'-'}}</el-descriptions-item><el-descriptions-item label="下次重试">{{formatTime(current.nextRetryAt)}}</el-descriptions-item><el-descriptions-item label="错误信息" :span="2">{{current.lastError||'-'}}</el-descriptions-item></el-descriptions><h4>资源快照</h4><pre class="snapshot">{{snapshotText}}</pre></template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getAwsInstanceList, retryAwsInstance } from '@/api/admin/awsService.js'
|
||||
const statuses=['pending','submitting','provisioning','active','failed','stopping','stopped','terminating','terminated']
|
||||
const labels={pending:'等待中',submitting:'提交中',provisioning:'创建中',active:'运行中',failed:'失败',stopping:'停止中',stopped:'已停止',terminating:'终止中',terminated:'已终止'}
|
||||
const loading=ref(false), retryingId=ref(null), rows=ref([]), total=ref(0), drawer=ref(false), current=ref(null)
|
||||
const query=reactive({page:1,count:20,status:undefined})
|
||||
const body=r=>r?.data||{}
|
||||
const statusText=s=>labels[s]||s||'-'
|
||||
const statusType=s=>s==='active'?'success':s==='failed'?'danger':['pending','submitting','provisioning','stopping','terminating'].includes(s)?'warning':'info'
|
||||
const formatTime=t=>t?new Date(t).toLocaleString('zh-CN',{hour12:false}):'-'
|
||||
const snapshotText=computed(()=>{const raw=current.value?.resourceSnapshot;if(!raw)return '暂无资源快照';try{return JSON.stringify(typeof raw==='string'?JSON.parse(raw):raw,null,2)}catch{return String(raw)}})
|
||||
async function load(){loading.value=true;try{const params={...query};if(!params.status)delete params.status;const b=body(await getAwsInstanceList(params));if(b.code===200){rows.value=b.data?.list||b.data?.data||[];total.value=b.data?.all_count||0}else ElMessage.error(b.message||'加载失败')}finally{loading.value=false}}
|
||||
function search(){query.page=1;load()} function reset(){query.status=undefined;search()} function showDetail(row){current.value=row;drawer.value=true}
|
||||
async function retry(row){retryingId.value=row.id;try{const b=body(await retryAwsInstance(row.id));if(b.code===200){ElMessage.success('已接受重试');load()}else ElMessage.error(b.message||'重试失败')}finally{retryingId.value=null}}
|
||||
onMounted(load)
|
||||
</script>
|
||||
<style scoped>
|
||||
.page{padding:20px}.header{display:flex;justify-content:space-between;align-items:center;padding:20px 24px;margin-bottom:18px;border:1px solid #d9ecff;border-radius:12px;background:linear-gradient(135deg,#f0f7ff,#f7fbff)}h2{margin:0}.header p{margin:6px 0 0;color:#909399}.filters{display:flex;gap:10px;margin-bottom:16px}.filters .el-select{width:230px}.pager{display:flex;justify-content:flex-end;padding-top:16px}.snapshot{padding:16px;border-radius:8px;background:#f5f7fa;white-space:pre-wrap;word-break:break-all;max-height:55vh;overflow:auto}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="header"><div><h2>AWS 服务配置</h2><p>管理 aws-server-sdk 连接和目录缓存策略</p></div><el-button type="primary" @click="openCreate"><el-icon><Plus /></el-icon>新增服务</el-button></div>
|
||||
<div class="filters"><el-input v-model="query.key" clearable placeholder="搜索名称、地址或备注" @keyup.enter="search" @clear="search"/><el-button type="primary" @click="search">搜索</el-button><el-button @click="reset">重置</el-button></div>
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70"/>
|
||||
<el-table-column prop="name" label="名称" min-width="140"/>
|
||||
<el-table-column prop="baseUrl" label="服务地址" min-width="220" show-overflow-tooltip/>
|
||||
<el-table-column label="Token" width="150"><template #default="{ row }"><el-tag :type="row.tokenConfigured ? 'success' : 'danger'">{{ row.tokenConfigured ? (row.tokenPrefix || '已配置') : '未配置' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="timeoutSec" label="超时(秒)" width="95"/>
|
||||
<el-table-column prop="catalogTtl" label="目录TTL" width="95"/>
|
||||
<el-table-column label="状态" width="85"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="140" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="235" fixed="right"><template #default="{ row }"><el-button link type="success" :loading="testingId===row.id" @click="test(row)">测试</el-button><el-button link type="primary" @click="openEdit(row)">编辑</el-button><el-button link @click="$router.push({path:'/aws/goods',query:{service_id:row.id}})">商品</el-button><el-popconfirm title="确认删除未被绑定的服务?" @confirm="remove(row)"><template #reference><el-button link type="danger">删除</el-button></template></el-popconfirm></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pager"><el-pagination v-model:current-page="query.page" v-model:page-size="query.count" :total="total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next" @change="load"/></div>
|
||||
<el-dialog v-model="dialog" :title="editing ? '编辑 AWS 服务' : '新增 AWS 服务'" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="form.name"/></el-form-item>
|
||||
<el-form-item label="服务地址" prop="base_url"><el-input v-model="form.base_url" placeholder="https://aws.example.com"/></el-form-item>
|
||||
<el-form-item label="App Token" prop="app_token"><el-input v-model="form.app_token" show-password :placeholder="editing ? '留空保留原 Token' : 'awsapp_...'"/></el-form-item>
|
||||
<el-form-item label="请求超时" prop="timeout_sec"><el-input-number v-model="form.timeout_sec" :min="1" :max="300"/></el-form-item>
|
||||
<el-form-item label="目录缓存" prop="catalog_ttl"><el-input-number v-model="form.catalog_ttl" :min="0"/></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="form.enabled"/></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.note" type="textarea" :rows="3"/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialog=false">取消</el-button><el-button type="primary" :loading="submitting" @click="submit">保存</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { createAwsService, deleteAwsService, getAwsServiceList, testAwsService, updateAwsService } from '@/api/admin/awsService.js'
|
||||
|
||||
const loading=ref(false), submitting=ref(false), testingId=ref(null), rows=ref([]), total=ref(0), dialog=ref(false), editing=ref(false), formRef=ref()
|
||||
const query=reactive({page:1,count:20,key:''})
|
||||
const empty=()=>({id:null,name:'',base_url:'',app_token:'',enabled:true,timeout_sec:30,catalog_ttl:60,note:''})
|
||||
const form=reactive(empty())
|
||||
const rules={name:[{required:true,message:'请输入名称'}],base_url:[{required:true,message:'请输入服务地址'},{type:'url',message:'请输入有效 URL'}],app_token:[{validator:(_,v,cb)=>{if(!editing.value&&!v)return cb(new Error('请输入 App Token'));if(v&&!v.startsWith('awsapp_'))return cb(new Error('Token 必须以 awsapp_ 开头'));cb()}}]}
|
||||
const body=r=>r?.data||{}
|
||||
async function load(){loading.value=true;try{const b=body(await getAwsServiceList(query));if(b.code===200){rows.value=b.data?.data||b.data?.list||[];total.value=b.data?.all_count||0}else ElMessage.error(b.message||'加载失败')}finally{loading.value=false}}
|
||||
function search(){query.page=1;load()} function reset(){query.key='';search()}
|
||||
function openCreate(){editing.value=false;Object.assign(form,empty());dialog.value=true}
|
||||
function openEdit(row){editing.value=true;Object.assign(form,{id:row.id,name:row.name||'',base_url:row.baseUrl||row.base_url||'',app_token:'',enabled:!!row.enabled,timeout_sec:row.timeoutSec??30,catalog_ttl:row.catalogTtl??60,note:row.note||''});dialog.value=true}
|
||||
async function submit(){await formRef.value?.validate();submitting.value=true;try{const data={name:form.name.trim(),base_url:form.base_url.trim(),enabled:form.enabled,timeout_sec:form.timeout_sec,catalog_ttl:form.catalog_ttl,note:form.note};if(form.app_token)data.app_token=form.app_token; if(editing.value)data.id=form.id;const b=body(await (editing.value?updateAwsService(data):createAwsService(data)));if(b.code===200){ElMessage.success('保存成功');dialog.value=false;load()}else ElMessage.error(b.message||'保存失败')}finally{submitting.value=false}}
|
||||
async function test(row){testingId.value=row.id;try{const b=body(await testAwsService(row.id));b.code===200?ElMessage.success(`连接成功${b.data?.latency_ms?`,${b.data.latency_ms}ms`:''}`):ElMessage.error(b.message||'连接失败')}finally{testingId.value=null}}
|
||||
async function remove(row){const b=body(await deleteAwsService(row.id));if(b.code===200){ElMessage.success('删除成功');load()}else ElMessage.error(b.message||'删除失败')}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:20px}.header{display:flex;justify-content:space-between;align-items:center;padding:20px 24px;margin-bottom:18px;border:1px solid #d9ecff;border-radius:12px;background:linear-gradient(135deg,#f0f7ff,#f7fbff)}h2{margin:0;color:#303133}.header p{margin:6px 0 0;color:#909399}.filters{display:flex;gap:10px;margin-bottom:16px}.filters .el-input{width:320px}.pager{display:flex;justify-content:flex-end;padding-top:16px}
|
||||
</style>
|
||||
@@ -162,6 +162,7 @@
|
||||
<span v-if="row.isGroup">{{ row.note || '-' }}</span>
|
||||
<div v-else-if="row.isProduct" class="product-info-inline">
|
||||
<span class="product-price">¥{{ (row.data.price / 100).toFixed(2) }}</span>
|
||||
<el-tag v-if="row.data.remoteGoods || row.data.remote_goods" type="primary" size="small">远端商品</el-tag>
|
||||
<el-tag v-if="row.data.inventoryControl" type="info" size="small">库存: {{ row.data.inventory }}</el-tag>
|
||||
<el-tag v-if="row.data.recommend" type="warning" size="small">推荐</el-tag>
|
||||
<el-tag v-if="row.data.expireTime && row.data.expireTime > 0" type="info" size="small">有效期: {{ row.data.expireTime }}天</el-tag>
|
||||
@@ -682,6 +683,14 @@
|
||||
/>
|
||||
<div style="font-size:12px;color:#909399;margin-top:4px">启用后,用户购买/续费/升级此商品前须完成实名认证</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="远端商品" prop="remote_goods">
|
||||
<el-switch
|
||||
v-model="productForm.remote_goods"
|
||||
active-text="远端计价/履约"
|
||||
inactive-text="普通商品"
|
||||
/>
|
||||
<div style="font-size:12px;color:#909399;margin-top:4px">AWS 商品请同时将标签设为 aws,保存后到 AWS 商品绑定页配置目录规则</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -960,6 +969,7 @@ const productForm = reactive({
|
||||
arg_type: 'all',
|
||||
can_renew: true,
|
||||
require_real_name: false,
|
||||
remote_goods: false,
|
||||
sold_out: false,
|
||||
max_per_user: 0,
|
||||
max_first_purchase_duration: 0,
|
||||
@@ -1621,6 +1631,7 @@ const handleAddProduct = () => {
|
||||
recommend_rebate: 0,
|
||||
arg_type: 'all',
|
||||
require_real_name: false,
|
||||
remote_goods: false,
|
||||
max_per_user: 0,
|
||||
max_first_purchase_duration: 0,
|
||||
recommend_words: ''
|
||||
@@ -1658,6 +1669,7 @@ const handleEditProduct = (product, parentGroupId) => {
|
||||
arg_type: product.argType || 'all',
|
||||
can_renew: product.canRenew !== undefined ? product.canRenew : (product.can_renew !== undefined ? product.can_renew : true),
|
||||
require_real_name: product.requireRealName ?? product.require_real_name ?? false,
|
||||
remote_goods: product.remoteGoods ?? product.remote_goods ?? false,
|
||||
sold_out: !!product.soldOut,
|
||||
max_per_user: product.maxPerUser ?? product.max_per_user ?? 0,
|
||||
max_first_purchase_duration: product.maxFirstPurchaseDuration ?? product.max_first_purchase_duration ?? 0,
|
||||
@@ -1691,6 +1703,7 @@ const submitProductForm = () => {
|
||||
arg_type: productForm.arg_type,
|
||||
can_renew: productForm.can_renew,
|
||||
require_real_name: productForm.require_real_name,
|
||||
remote_goods: productForm.remote_goods === true,
|
||||
sold_out: productForm.sold_out === true,
|
||||
max_per_user: Number(productForm.max_per_user) || 0,
|
||||
max_first_purchase_duration: Number(productForm.max_first_purchase_duration) || 0,
|
||||
|
||||
Reference in New Issue
Block a user