feat: 产品/客户/订单批量删除;接入 GeoLite2 城市库(地理分布/激活记录/审计日志显示归属地);仪表盘趋势图美化
This commit is contained in:
1 parent
252ae14aef
commit
473dcd0aab
24 files changed
+431
-34
No files matched your search
@@ -29,3 +29,8 @@ export function deleteCustomer(id) {
|
||||
export function updateCustomerStatus(id, status) {
|
||||
return request({ url: `/customers/${id}/status`, method: 'patch', data: { status } })
|
||||
}
|
||||
|
||||
// batchDeleteCustomers 批量删除 // Logan.Li
|
||||
export function batchDeleteCustomers(ids) {
|
||||
return request.post('/customers/batch-delete', { ids })
|
||||
}
|
||||
@@ -13,3 +13,8 @@ export function getOrder(id) {
|
||||
export function manualGrant(data) {
|
||||
return request({ url: '/orders/manual-grant', method: 'post', data })
|
||||
}
|
||||
|
||||
// batchDeleteOrders 批量删除 // Logan.Li
|
||||
export function batchDeleteOrders(ids) {
|
||||
return request.post('/orders/batch-delete', { ids })
|
||||
}
|
||||
@@ -29,3 +29,8 @@ export function deleteProduct(id) {
|
||||
export function updateProductStatus(id, status) {
|
||||
return request({ url: `/products/${id}/status`, method: 'patch', data: { status } })
|
||||
}
|
||||
|
||||
// batchDeleteProducts 批量删除 // Logan.Li
|
||||
export function batchDeleteProducts(ids) {
|
||||
return request.post('/products/batch-delete', { ids })
|
||||
}
|
||||
@@ -24,7 +24,12 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button :disabled="!productFilter || list.length === 0" @click="copyEmails">复制本页邮箱</el-button>
|
||||
<el-button type="primary" @click="openCreate">新增客户</el-button>
|
||||
<div>
|
||||
<el-button type="danger" plain :disabled="!selectedRows.length" @click="batchDelete">
|
||||
批量删除{{ selectedRows.length ? `(${selectedRows.length})` : '' }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openCreate">新增客户</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +41,8 @@
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe>
|
||||
<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="120" />
|
||||
<el-table-column prop="company" label="公司" min-width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" />
|
||||
@@ -107,7 +113,29 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getCustomers, createCustomer, updateCustomer, deleteCustomer, updateCustomerStatus } from '@/api/customer'
|
||||
|
||||
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 batchDeleteCustomers(ids)
|
||||
const data = res.data || {}
|
||||
ElMessage.success(`删除 ${data.deleted || 0} 条,跳过 ${data.skipped || 0} 条${data.failed ? `,失败 ${data.failed}` : ''}`)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}
|
||||
import { getCustomers, createCustomer, updateCustomer, deleteCustomer, updateCustomerStatus, batchDeleteCustomers } from '@/api/customer'
|
||||
import { getAllProducts } from '@/api/product'
|
||||
|
||||
const list = ref([])
|
||||
|
||||
@@ -148,15 +148,26 @@ const growthOption = computed(() => ({
|
||||
{
|
||||
name: '新增',
|
||||
type: 'bar',
|
||||
barMaxWidth: 22,
|
||||
itemStyle: { borderRadius: [3, 3, 0, 0], color: '#409eff' },
|
||||
data: growthData.value.map((d) => d.new_count ?? d.count ?? d.value ?? 0),
|
||||
color: '#409eff',
|
||||
},
|
||||
{
|
||||
name: '累计',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: growthData.value.map((d) => d.total_count ?? d.total ?? null),
|
||||
color: '#67c23a',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(103,194,58,0.28)' },
|
||||
{ offset: 1, color: 'rgba(103,194,58,0.02)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -170,8 +181,17 @@ const revenueOption = computed(() => ({
|
||||
data: revenueData.value.map((d) => d.amount ?? d.revenue ?? d.value ?? 0),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: { opacity: 0.15 },
|
||||
showSymbol: false,
|
||||
color: '#f56c6c',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(245,108,108,0.30)' },
|
||||
{ offset: 1, color: 'rgba(245,108,108,0.02)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}],
|
||||
}))
|
||||
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
<el-table-column prop="device_id" label="设备 ID" min-width="120" />
|
||||
<el-table-column prop="device_name" label="设备名" min-width="120" />
|
||||
<el-table-column prop="ip" label="IP" width="120" />
|
||||
<el-table-column label="归属地" min-width="120">
|
||||
<template #default="{ row }">{{ row.location || "—" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="在线" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_online ? 'success' : 'info'" size="small">{{ row.is_online ? '在线' : '离线' }}</el-tag>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ip" label="IP" width="130" />
|
||||
<el-table-column label="归属地" min-width="140"><template #default="{ row }">{{ row.location || "—" }}</template></el-table-column>
|
||||
<el-table-column prop="created_at" label="时间" width="170" />
|
||||
</el-table>
|
||||
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button type="primary" @click="grantVisible = true">手动发放</el-button>
|
||||
<div>
|
||||
<el-button type="danger" plain :disabled="!selectedRows.length" @click="batchDelete">
|
||||
批量删除{{ selectedRows.length ? `(${selectedRows.length})` : '' }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="grantVisible = true">手动发放</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe>
|
||||
<el-table :data="list" v-loading="loading" stripe @selection-change="onSelectionChange">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="order_no" label="订单号" min-width="200" />
|
||||
<el-table-column prop="customer_name" label="客户" min-width="100" />
|
||||
<el-table-column prop="product_name" label="产品" min-width="100" />
|
||||
@@ -61,8 +67,30 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrders, manualGrant } from '@/api/order'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const selectedRows = ref([])
|
||||
|
||||
function onSelectionChange(rows) {
|
||||
selectedRows.value = rows || []
|
||||
}
|
||||
|
||||
// 批量删除:已支付订单为财务凭证,后端自动跳过 // Logan.Li
|
||||
async function batchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(
|
||||
`已选 ${ids.length} 条订单;已支付(财务凭证)将自动跳过,其余删除后不可恢复。确认继续?`,
|
||||
'批量删除',
|
||||
{ type: 'warning' },
|
||||
)
|
||||
const res = await batchDeleteOrders(ids)
|
||||
const data = res.data || {}
|
||||
ElMessage.success(`删除 ${data.deleted || 0} 条,跳过 ${data.skipped || 0} 条${data.failed ? `,失败 ${data.failed}` : ''}`)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}
|
||||
import { getOrders, manualGrant, batchDeleteOrders } from '@/api/order'
|
||||
import { getAllProducts } from '@/api/product'
|
||||
import request from '@/api/request'
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索产品名称/编码" clearable style="width: 240px" @keyup.enter="loadData" @clear="loadData" />
|
||||
<el-button type="primary" @click="dialogVisible = true">新增产品</el-button>
|
||||
<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>
|
||||
<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" />
|
||||
@@ -67,7 +73,29 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getProducts, createProduct, updateProduct, deleteProduct, updateProductStatus } from '@/api/product'
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user