feat: 产品/客户/订单批量删除;接入 GeoLite2 城市库(地理分布/激活记录/审计日志显示归属地);仪表盘趋势图美化

This commit is contained in:
Logan.Li committed 2026-09-14 22:48:14 +08:00
1 parent 252ae14aef
commit 473dcd0aab
24 files changed
+431 -34

No files matched your search

+5
View File
@@ -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 })
}
+5
View File
@@ -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 })
}
+5
View File
@@ -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 })
}
+31 -3
View File
@@ -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([])
+22 -2
View File
@@ -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)' },
],
},
},
}],
}))
+3
View File
@@ -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>
+1
View File
@@ -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>
+32 -4
View File
@@ -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'
+31 -3
View File
@@ -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)