163 lines
6.3 KiB
Vue
163 lines
6.3 KiB
Vue
<template>
|
||
<div>
|
||
<el-card shadow="never">
|
||
<div class="toolbar">
|
||
<el-input v-model="keyword" placeholder="搜索产品名称/编码" clearable style="width: 240px" @keyup.enter="loadData" @clear="loadData" />
|
||
<div>
|
||
<el-button type="danger" plain :disabled="!selectedRows.length" @click="batchDelete">
|
||
批量删除{{ selectedRows.length ? `(${selectedRows.length})` : '' }}
|
||
</el-button>
|
||
<el-button type="primary" @click="dialogVisible = true">新增产品</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table :data="list" v-loading="loading" stripe @selection-change="onSelectionChange">
|
||
<el-table-column type="selection" width="48" />
|
||
<el-table-column prop="name" label="产品名称" min-width="140" />
|
||
<el-table-column prop="code" label="编码" width="120" />
|
||
<el-table-column prop="version" label="版本" width="90" />
|
||
<el-table-column prop="price" label="价格" width="90">
|
||
<template #default="{ row }">{{ row.price }} {{ row.currency || '' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="price_model" label="计费模式" width="110" />
|
||
<el-table-column label="功能" min-width="180">
|
||
<template #default="{ row }">
|
||
<el-tag v-for="f in row.features" :key="f" size="small" style="margin: 2px">{{ f }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-switch :model-value="row.status === 1" @change="(v) => toggleStatus(row, v)" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button size="small" @click="edit(row)">编辑</el-button>
|
||
<el-button size="small" type="danger" @click="remove(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
v-model:current-page="page" v-model:page-size="pageSize"
|
||
:total="total" layout="total, prev, pager, next" style="margin-top: 16px"
|
||
@current-change="loadData"
|
||
/>
|
||
</el-card>
|
||
|
||
<!-- 新增/编辑对话框 -->
|
||
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑产品' : '新增产品'" width="560px">
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||
<el-form-item label="名称" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||
<el-form-item label="编码" prop="code"><el-input v-model="form.code" :disabled="!!form.id" /></el-form-item>
|
||
<el-form-item label="版本"><el-input v-model="form.version" /></el-form-item>
|
||
<el-form-item label="价格"><el-input-number v-model="form.price" :min="0" :precision="2" /></el-form-item>
|
||
<el-form-item label="计费模式">
|
||
<el-select v-model="form.price_model" style="width: 100%">
|
||
<el-option value="subscription" label="订阅" /><el-option value="perpetual" label="买断" /><el-option value="trial" label="试用" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="功能特性">
|
||
<el-select v-model="form.features" multiple filterable allow-create default-first-option style="width: 100%" placeholder="输入后回车添加" />
|
||
</el-form-item>
|
||
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="2" /></el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submit">确认</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
|
||
const selectedRows = ref([])
|
||
|
||
function onSelectionChange(rows) {
|
||
selectedRows.value = rows || []
|
||
}
|
||
|
||
// 批量删除:被 License 引用的产品由后端自动跳过 // Logan.Li
|
||
async function batchDelete() {
|
||
const ids = selectedRows.value.map((r) => r.id).filter(Boolean)
|
||
if (!ids.length) return
|
||
await ElMessageBox.confirm(
|
||
`已选 ${ids.length} 个产品;被 License 引用的将自动跳过,未引用的连同其定价计划一并删除。确认继续?`,
|
||
'批量删除',
|
||
{ type: 'warning' },
|
||
)
|
||
const res = await batchDeleteProducts(ids)
|
||
const data = res.data || {}
|
||
ElMessage.success(`删除 ${data.deleted || 0} 条,跳过 ${data.skipped || 0} 条${data.failed ? `,失败 ${data.failed}` : ''}`)
|
||
selectedRows.value = []
|
||
loadData()
|
||
}
|
||
import { getProducts, createProduct, updateProduct, deleteProduct, updateProductStatus, batchDeleteProducts } from '@/api/product'
|
||
|
||
const list = ref([])
|
||
const loading = ref(false)
|
||
const keyword = ref('')
|
||
const page = ref(1)
|
||
const pageSize = ref(10)
|
||
const total = ref(0)
|
||
|
||
const dialogVisible = ref(false)
|
||
const formRef = ref(null)
|
||
const form = reactive({ id: null, name: '', code: '', version: '', price: 0, price_model: 'subscription', features: [], description: '' })
|
||
const rules = {
|
||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||
code: [{ required: true, message: '请输入编码', trigger: 'blur' }],
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await getProducts({ page: page.value, page_size: pageSize.value, keyword: keyword.value })
|
||
list.value = res.data.list || []
|
||
total.value = res.data.total
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function edit(row) {
|
||
Object.assign(form, row, { features: row.features || [] })
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
async function submit() {
|
||
await formRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
try {
|
||
if (form.id) {
|
||
await updateProduct(form.id, form)
|
||
} else {
|
||
await createProduct(form)
|
||
}
|
||
dialogVisible.value = false
|
||
loadData()
|
||
} catch { /* 拦截器处理 */ }
|
||
})
|
||
}
|
||
|
||
async function remove(row) {
|
||
await ElMessageBox.confirm(`确定删除产品「${row.name}」?`, '提示', { type: 'warning' })
|
||
await deleteProduct(row.id)
|
||
loadData()
|
||
}
|
||
|
||
async function toggleStatus(row, val) {
|
||
await updateProductStatus(row.id, val ? 1 : 0)
|
||
loadData()
|
||
}
|
||
|
||
loadData()
|
||
</script>
|
||
|
||
<style scoped>
|
||
.toolbar { display: flex; justify-content: space-between; margin-bottom: 16px; }
|
||
</style>
|