This commit is contained in:
@@ -147,49 +147,32 @@
|
||||
<div class="flow-stat-item">
|
||||
<span class="flow-stat-label">已使用</span>
|
||||
<span class="flow-stat-value">
|
||||
{{
|
||||
formatDataSize(
|
||||
currentPackage.enable_virtual_data
|
||||
? currentPackage.virtual_used_mb || 0
|
||||
: currentPackage.real_used_mb || 0
|
||||
)
|
||||
}}
|
||||
{{ formatDataSize(getDisplayedUsedMb(currentPackage)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flow-stat-item">
|
||||
<span class="flow-stat-label">总量</span>
|
||||
<span class="flow-stat-value">
|
||||
{{ formatDataSize(currentPackage.real_total_mb || 0) }}
|
||||
{{ formatDataSize(getDisplayedTotalMb(currentPackage)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flow-stat-item">
|
||||
<span class="flow-stat-label">剩余</span>
|
||||
<span class="flow-stat-value">
|
||||
{{
|
||||
formatDataSize(
|
||||
(currentPackage.real_total_mb || 0) - (currentPackage.real_used_mb || 0)
|
||||
)
|
||||
}}
|
||||
{{ formatDataSize(getDisplayedRemainingMb(currentPackage)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-progress-bar" style="margin-top: 16px">
|
||||
<ElProgress
|
||||
:percentage="
|
||||
getUsageProgress(
|
||||
currentPackage.enable_virtual_data
|
||||
? currentPackage.virtual_used_mb || 0
|
||||
: currentPackage.real_used_mb || 0,
|
||||
currentPackage.real_total_mb || 0
|
||||
)
|
||||
getUsageProgress(getDisplayedUsedMb(currentPackage), getDisplayedTotalMb(currentPackage))
|
||||
"
|
||||
:color="
|
||||
getProgressColorByPercentage(
|
||||
getUsageProgress(
|
||||
currentPackage.enable_virtual_data
|
||||
? currentPackage.virtual_used_mb || 0
|
||||
: currentPackage.real_used_mb || 0,
|
||||
currentPackage.real_total_mb || 0
|
||||
getDisplayedUsedMb(currentPackage),
|
||||
getDisplayedTotalMb(currentPackage)
|
||||
)
|
||||
)
|
||||
"
|
||||
@@ -286,6 +269,14 @@
|
||||
navigateToDevice: [deviceNo: string]
|
||||
}>()
|
||||
|
||||
const getDisplayedTotalMb = (pkg: AssetPackageUsageRecord) => pkg.real_total_mb || 0
|
||||
|
||||
const getDisplayedUsedMb = (pkg: AssetPackageUsageRecord) =>
|
||||
pkg.enable_virtual_data ? pkg.virtual_used_mb || 0 : pkg.real_used_mb || 0
|
||||
|
||||
const getDisplayedRemainingMb = (pkg: AssetPackageUsageRecord) =>
|
||||
getDisplayedTotalMb(pkg) - getDisplayedUsedMb(pkg)
|
||||
|
||||
const getPackageTypeName = (type?: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
formal: '正式套餐',
|
||||
|
||||
@@ -148,9 +148,7 @@
|
||||
>已使用:
|
||||
{{
|
||||
formatDataSize(
|
||||
scope.row.enable_virtual_data
|
||||
? scope.row.virtual_used_mb || 0
|
||||
: scope.row.real_used_mb || 0
|
||||
getDisplayedUsedMb(scope.row)
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
@@ -161,7 +159,7 @@
|
||||
>剩余:
|
||||
{{
|
||||
formatDataSize(
|
||||
(scope.row.real_total_mb || 0) - (scope.row.real_used_mb || 0)
|
||||
getDisplayedRemainingMb(scope.row)
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
@@ -170,19 +168,15 @@
|
||||
style="margin-top: 8px"
|
||||
:percentage="
|
||||
getUsageProgress(
|
||||
scope.row.enable_virtual_data
|
||||
? scope.row.virtual_used_mb || 0
|
||||
: scope.row.real_used_mb || 0,
|
||||
scope.row.real_total_mb || 0
|
||||
getDisplayedUsedMb(scope.row),
|
||||
getDisplayedTotalMb(scope.row)
|
||||
)
|
||||
"
|
||||
:color="
|
||||
getProgressColorByPercentage(
|
||||
getUsageProgress(
|
||||
scope.row.enable_virtual_data
|
||||
? scope.row.virtual_used_mb || 0
|
||||
: scope.row.real_used_mb || 0,
|
||||
scope.row.real_total_mb || 0
|
||||
getDisplayedUsedMb(scope.row),
|
||||
getDisplayedTotalMb(scope.row)
|
||||
)
|
||||
)
|
||||
"
|
||||
@@ -409,6 +403,14 @@
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
const getDisplayedTotalMb = (pkg: PackageInfo) => pkg.real_total_mb || 0
|
||||
|
||||
const getDisplayedUsedMb = (pkg: PackageInfo) =>
|
||||
pkg.enable_virtual_data ? pkg.virtual_used_mb || 0 : pkg.real_used_mb || 0
|
||||
|
||||
const getDisplayedRemainingMb = (pkg: PackageInfo) =>
|
||||
getDisplayedTotalMb(pkg) - getDisplayedUsedMb(pkg)
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
emit('page-change', page)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
style="width: 360px"
|
||||
/>
|
||||
<ElButton type="primary" @click="handleQuery">查询</ElButton>
|
||||
<ElButton @click="handleReset">重置</ElButton>
|
||||
<ElButton @click="handleReset()">重置</ElButton>
|
||||
<ElTag type="info" size="small">共 {{ total }} 条</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,14 +64,8 @@
|
||||
</template>
|
||||
</ElEmpty>
|
||||
|
||||
<div v-else class="wallet-table-wrapper">
|
||||
<ElTable
|
||||
v-if="transactionList && transactionList.length > 0"
|
||||
:data="transactionList"
|
||||
class="wallet-table"
|
||||
border
|
||||
v-loading="loading"
|
||||
>
|
||||
<div v-else v-loading="loading" class="wallet-table-wrapper">
|
||||
<ElTable v-if="hasTransactions" :data="transactionList" class="wallet-table" border>
|
||||
<ElTableColumn label="交易类型" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTransactionTypeTag(scope.row.transaction_type)" size="small">
|
||||
@@ -128,7 +122,9 @@
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<ElEmpty v-else-if="!loading" description="暂无钱包流水数据" :image-size="100" />
|
||||
<div v-else class="wallet-empty-state">
|
||||
<ElEmpty v-if="hasLoadedTransactions" description="暂无钱包流水数据" :image-size="100" />
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div v-if="total > 0" class="pagination-wrapper">
|
||||
@@ -147,7 +143,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
ElCard,
|
||||
ElTable,
|
||||
@@ -166,6 +162,8 @@
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import type {
|
||||
AssetWalletTransactionItem,
|
||||
AssetWalletTransactionListResponse,
|
||||
AssetWalletResponse,
|
||||
AssetWalletTransactionParams,
|
||||
TransactionType
|
||||
@@ -173,6 +171,7 @@
|
||||
|
||||
interface Props {
|
||||
assetIdentifier: string
|
||||
searchToken?: number
|
||||
walletInfo: AssetWalletResponse | null
|
||||
walletLoading?: boolean
|
||||
boundDeviceId?: number | null
|
||||
@@ -190,39 +189,80 @@
|
||||
}>()
|
||||
|
||||
const { formatAmount, getTransactionTypeTag, getWalletStatusType } = useAssetFormatters()
|
||||
const DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
const createDefaultPagination = () => ({ page: 1, page_size: DEFAULT_PAGE_SIZE })
|
||||
|
||||
const createDefaultFilterForm = () => ({
|
||||
transaction_type: null as TransactionType | null,
|
||||
date_range: null as [Date, Date] | null
|
||||
})
|
||||
|
||||
const createDefaultQueryParams = (): AssetWalletTransactionParams => ({
|
||||
page: 1,
|
||||
page_size: DEFAULT_PAGE_SIZE,
|
||||
transaction_type: null,
|
||||
start_time: null,
|
||||
end_time: null
|
||||
})
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const transactionList = ref<any[]>([])
|
||||
const hasLoadedTransactions = ref(false)
|
||||
const transactionList = ref<AssetWalletTransactionItem[]>([])
|
||||
const total = ref(0)
|
||||
const pagination = ref({ page: 1, page_size: 20 })
|
||||
const filterForm = ref<{
|
||||
transaction_type: TransactionType | null
|
||||
date_range: [Date, Date] | null
|
||||
}>({
|
||||
transaction_type: null,
|
||||
date_range: null
|
||||
})
|
||||
const queryParams = ref<AssetWalletTransactionParams>({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
transaction_type: null as TransactionType | null,
|
||||
start_time: null as string | null,
|
||||
end_time: null as string | null
|
||||
const hasTransactions = computed(() => transactionList.value.length > 0)
|
||||
const pagination = ref(createDefaultPagination())
|
||||
const filterForm = ref(createDefaultFilterForm())
|
||||
const queryParams = ref<AssetWalletTransactionParams>(createDefaultQueryParams())
|
||||
|
||||
const clearTransactions = () => {
|
||||
hasLoadedTransactions.value = false
|
||||
transactionList.value = []
|
||||
total.value = 0
|
||||
pagination.value = createDefaultPagination()
|
||||
}
|
||||
|
||||
const normalizeTransactionResponse = (data?: AssetWalletTransactionListResponse | null) => ({
|
||||
items: data?.items ?? data?.list ?? [],
|
||||
total: data?.total ?? 0,
|
||||
page: data?.page ?? 1,
|
||||
pageSize: data?.size ?? data?.page_size ?? queryParams.value.page_size ?? DEFAULT_PAGE_SIZE
|
||||
})
|
||||
|
||||
const loadTransactions = async () => {
|
||||
if (!props.assetIdentifier) return
|
||||
const loadTransactions = async (preserveDisplayState: boolean = true) => {
|
||||
if (!props.assetIdentifier || props.boundDeviceId) {
|
||||
clearTransactions()
|
||||
return
|
||||
}
|
||||
|
||||
if (!preserveDisplayState) {
|
||||
clearTransactions()
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await AssetService.getWalletTransactions(
|
||||
props.assetIdentifier,
|
||||
queryParams.value
|
||||
)
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
transactionList.value = response.data.list || []
|
||||
total.value = response.data.total || 0
|
||||
const normalized = normalizeTransactionResponse(response.data)
|
||||
transactionList.value = normalized.items
|
||||
total.value = normalized.total
|
||||
pagination.value = {
|
||||
page: normalized.page,
|
||||
page_size: normalized.pageSize
|
||||
}
|
||||
queryParams.value.page = normalized.page
|
||||
queryParams.value.page_size = normalized.pageSize
|
||||
} else {
|
||||
transactionList.value = []
|
||||
total.value = 0
|
||||
pagination.value = createDefaultPagination()
|
||||
}
|
||||
hasLoadedTransactions.value = true
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status === 403) {
|
||||
ElMessage.warning('企业账号禁止查看钱包流水')
|
||||
@@ -234,6 +274,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
const resetQueryState = () => {
|
||||
filterForm.value = createDefaultFilterForm()
|
||||
queryParams.value = createDefaultQueryParams()
|
||||
pagination.value = createDefaultPagination()
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.value.page = 1
|
||||
pagination.value.page = 1
|
||||
@@ -245,41 +291,45 @@
|
||||
queryParams.value.start_time = null
|
||||
queryParams.value.end_time = null
|
||||
}
|
||||
loadTransactions()
|
||||
void loadTransactions(true)
|
||||
}
|
||||
|
||||
const handleFilterChange = () => handleQuery()
|
||||
|
||||
const handleReset = () => {
|
||||
filterForm.value = { transaction_type: null, date_range: null }
|
||||
queryParams.value = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
transaction_type: null,
|
||||
start_time: null,
|
||||
end_time: null
|
||||
const handleReset = (preserveDisplayState: boolean = true) => {
|
||||
resetQueryState()
|
||||
if (props.boundDeviceId) {
|
||||
clearTransactions()
|
||||
return
|
||||
}
|
||||
pagination.value = { page: 1, page_size: 20 }
|
||||
loadTransactions()
|
||||
void loadTransactions(preserveDisplayState)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
queryParams.value.page_size = size
|
||||
queryParams.value.page = 1
|
||||
pagination.value.page = 1
|
||||
loadTransactions()
|
||||
void loadTransactions(true)
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
queryParams.value.page = page
|
||||
loadTransactions()
|
||||
void loadTransactions(true)
|
||||
}
|
||||
|
||||
// 仅监听 assetIdentifier 变化加载数据(去掉 onMounted 避免重复调用)
|
||||
watch(
|
||||
() => props.assetIdentifier,
|
||||
(newVal) => {
|
||||
if (newVal) handleReset()
|
||||
() => [props.assetIdentifier, props.searchToken, props.boundDeviceId] as const,
|
||||
([assetIdentifier], previousValue) => {
|
||||
if (!assetIdentifier) {
|
||||
resetQueryState()
|
||||
clearTransactions()
|
||||
return
|
||||
}
|
||||
|
||||
const previousAssetIdentifier = previousValue?.[0]
|
||||
const preserveDisplayState =
|
||||
Boolean(previousAssetIdentifier) && assetIdentifier === previousAssetIdentifier
|
||||
handleReset(preserveDisplayState)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -326,6 +376,13 @@
|
||||
}
|
||||
|
||||
.wallet-table-wrapper {
|
||||
.wallet-empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.wallet-table {
|
||||
:deep(.el-table__header) {
|
||||
th {
|
||||
@@ -334,18 +391,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.reference-no-link {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
|
||||
@@ -8,6 +8,13 @@ import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import type { AssetWalletResponse, DeviceGatewayInfo, AssetPackageUsageRecord } from '@/types/api'
|
||||
|
||||
type TrafficDisplaySource = {
|
||||
real_total_mb?: number | null
|
||||
real_used_mb?: number | null
|
||||
virtual_used_mb?: number | null
|
||||
enable_virtual_data?: boolean | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数据大小(MB -> GB/MB)
|
||||
*/
|
||||
@@ -18,6 +25,20 @@ const formatDataSize = (mb: number) => {
|
||||
return `${mb.toFixed(2)}MB`
|
||||
}
|
||||
|
||||
const getDisplayedTotalMb = (traffic?: TrafficDisplaySource | null) => traffic?.real_total_mb || 0
|
||||
|
||||
const getDisplayedUsedMb = (traffic?: TrafficDisplaySource | null) =>
|
||||
traffic?.enable_virtual_data ? traffic?.virtual_used_mb || 0 : traffic?.real_used_mb || 0
|
||||
|
||||
const getDisplayedRemainingMb = (traffic?: TrafficDisplaySource | null) =>
|
||||
getDisplayedTotalMb(traffic) - getDisplayedUsedMb(traffic)
|
||||
|
||||
const getDisplayedUsagePercentage = (traffic?: TrafficDisplaySource | null) => {
|
||||
const totalMb = getDisplayedTotalMb(traffic)
|
||||
const usedMb = getDisplayedUsedMb(traffic)
|
||||
return totalMb > 0 ? `${((usedMb / totalMb) * 100).toFixed(2)}%` : '0.00%'
|
||||
}
|
||||
|
||||
export function useAssetInfo() {
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
@@ -105,13 +126,10 @@ export function useAssetInfo() {
|
||||
// 流量/套餐信息
|
||||
current_package: data.current_package || '',
|
||||
packageSeries: data.series_name || '-',
|
||||
packageTotalFlow: formatDataSize(data.real_total_mb || 0),
|
||||
usedFlow: formatDataSize(data.real_used_mb || 0),
|
||||
remainFlow: formatDataSize((data.real_total_mb || 0) - (data.real_used_mb || 0)),
|
||||
usedFlowPercentage:
|
||||
data.real_total_mb > 0
|
||||
? `${((data.real_used_mb / data.real_total_mb) * 100).toFixed(2)}%`
|
||||
: '0.00%',
|
||||
packageTotalFlow: formatDataSize(getDisplayedTotalMb(data)),
|
||||
usedFlow: formatDataSize(getDisplayedUsedMb(data)),
|
||||
remainFlow: formatDataSize(getDisplayedRemainingMb(data)),
|
||||
usedFlowPercentage: getDisplayedUsagePercentage(data),
|
||||
packageList: [] // 套餐列表需要单独调用API获取
|
||||
}
|
||||
|
||||
@@ -209,23 +227,11 @@ export function useAssetInfo() {
|
||||
}
|
||||
|
||||
// 同时更新 cardInfo 中的流量显示信息(根据是否启用虚流量选择数据源)
|
||||
const useVirtual = pkg.enable_virtual_data
|
||||
cardInfo.value.current_package = pkg.package_name
|
||||
cardInfo.value.packageTotalFlow = formatDataSize(
|
||||
useVirtual ? pkg.virtual_total_mb || 0 : pkg.real_total_mb || 0
|
||||
)
|
||||
cardInfo.value.usedFlow = formatDataSize(
|
||||
useVirtual ? pkg.virtual_used_mb || 0 : pkg.real_used_mb || 0
|
||||
)
|
||||
cardInfo.value.remainFlow = formatDataSize(
|
||||
useVirtual
|
||||
? (pkg.virtual_total_mb || 0) - (pkg.virtual_used_mb || 0)
|
||||
: (pkg.real_total_mb || 0) - (pkg.real_used_mb || 0)
|
||||
)
|
||||
const totalMb = useVirtual ? pkg.virtual_total_mb || 0 : pkg.real_total_mb || 0
|
||||
const usedMb = useVirtual ? pkg.virtual_used_mb || 0 : pkg.real_used_mb || 0
|
||||
cardInfo.value.usedFlowPercentage =
|
||||
totalMb > 0 ? `${((usedMb / totalMb) * 100).toFixed(2)}%` : '0.00%'
|
||||
cardInfo.value.packageTotalFlow = formatDataSize(getDisplayedTotalMb(pkg))
|
||||
cardInfo.value.usedFlow = formatDataSize(getDisplayedUsedMb(pkg))
|
||||
cardInfo.value.remainFlow = formatDataSize(getDisplayedRemainingMb(pkg))
|
||||
cardInfo.value.usedFlowPercentage = getDisplayedUsagePercentage(pkg)
|
||||
} else {
|
||||
// code 为 0 但 data 为空,表示暂无当前生效套餐(正常情况)
|
||||
currentPackage.value = null
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<div class="row full-width">
|
||||
<WalletTransactionCard
|
||||
:asset-identifier="cardInfo.identifier"
|
||||
:search-token="walletTransactionSearchToken"
|
||||
:wallet-info="walletInfo"
|
||||
:wallet-loading="walletLoading"
|
||||
:bound-device-id="cardInfo.bound_device_id"
|
||||
@@ -295,6 +296,7 @@
|
||||
pageSize: 5,
|
||||
total: 0
|
||||
})
|
||||
const walletTransactionSearchToken = ref(0)
|
||||
|
||||
// ========== 轮询状态 ==========
|
||||
const pollingEnabled = ref(false)
|
||||
@@ -325,6 +327,10 @@
|
||||
await fetchAssetDetail(deviceIdentifier)
|
||||
}
|
||||
|
||||
if (cardInfo.value) {
|
||||
walletTransactionSearchToken.value += 1
|
||||
}
|
||||
|
||||
// 从接口数据初始化轮询开关,不触发更新接口
|
||||
pollingInitializing = true
|
||||
pollingEnabled.value = cardInfo.value?.enable_polling ?? false
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAgentAccount = computed(() => userStore.info.user_type === 3)
|
||||
const currentUserType = computed(() => Number(userStore.info.user_type ?? 0))
|
||||
const isAgentAccount = computed(() => currentUserType.value === 3)
|
||||
const shouldHideVirtualTrafficFields = computed(() => [3, 4].includes(currentUserType.value))
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<PackageResponse | null>(null)
|
||||
@@ -58,7 +60,7 @@
|
||||
{ label: '套餐系列', prop: 'series_name' },
|
||||
{
|
||||
label: '套餐类型',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
formal: '正式套餐',
|
||||
addon: '附加套餐'
|
||||
@@ -68,13 +70,13 @@
|
||||
},
|
||||
{
|
||||
label: '成本价',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return `¥${(data.cost_price / 100).toFixed(2)}`
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '建议售价',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (data.suggested_retail_price === null || data.suggested_retail_price === undefined)
|
||||
return '-'
|
||||
return `¥${(data.suggested_retail_price / 100).toFixed(2)}`
|
||||
@@ -84,14 +86,14 @@
|
||||
? [
|
||||
{
|
||||
label: '代理零售价',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (data.retail_price === null || data.retail_price === undefined) return '-'
|
||||
return `¥${(data.retail_price / 100).toFixed(2)}`
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '利润空间',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (data.profit_margin === null || data.profit_margin === undefined) return '-'
|
||||
return `¥${(data.profit_margin / 100).toFixed(2)}`
|
||||
}
|
||||
@@ -100,13 +102,13 @@
|
||||
: [
|
||||
{
|
||||
label: '价格配置状态',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return data.price_config_status_name || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '生效零售价',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (
|
||||
data.effective_retail_price === null ||
|
||||
data.effective_retail_price === undefined
|
||||
@@ -117,20 +119,20 @@
|
||||
},
|
||||
{
|
||||
label: '是否赠送',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return data.is_gift ? '是' : '否'
|
||||
}
|
||||
}
|
||||
]),
|
||||
{
|
||||
label: '套餐时长',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return `${data.duration_months} 个月`
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '套餐周期类型',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.calendar_type) return '-'
|
||||
const typeMap: Record<string, string> = {
|
||||
natural_month: '自然月',
|
||||
@@ -141,14 +143,14 @@
|
||||
},
|
||||
{
|
||||
label: '套餐天数',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (data.calendar_type !== 'by_day' || !data.duration_days) return '-'
|
||||
return `${data.duration_days} 天`
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '流量重置周期',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.data_reset_cycle) return '-'
|
||||
const cycleMap: Record<string, string> = {
|
||||
daily: '每日',
|
||||
@@ -161,20 +163,20 @@
|
||||
},
|
||||
{
|
||||
label: '到期计时基准',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.expiry_base) return '--'
|
||||
return data.expiry_base === 'from_activation' ? '实名激活时起算' : '购买时起算'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return getStatusLabel(data.status ?? 0)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '上架状态',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return getShelfStatusText(data.shelf_status ?? 0)
|
||||
}
|
||||
},
|
||||
@@ -186,30 +188,34 @@
|
||||
title: '流量配置',
|
||||
fields: [
|
||||
{
|
||||
label: '真流量额度',
|
||||
formatter: (_, data) => {
|
||||
label: '总流量',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (data.real_data_mb >= 1024) {
|
||||
return `${(data.real_data_mb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
return `${data.real_data_mb} MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '启用虚流量',
|
||||
formatter: (_, data) => {
|
||||
return data.enable_virtual_data ? '是' : '否'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '虚流量额度',
|
||||
formatter: (_, data) => {
|
||||
if (!data.enable_virtual_data) return '-'
|
||||
if (data.virtual_data_mb >= 1024) {
|
||||
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
return `${data.virtual_data_mb} MB`
|
||||
}
|
||||
}
|
||||
...(shouldHideVirtualTrafficFields.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: '启用虚流量',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return data.enable_virtual_data ? '是' : '否'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '虚流量额度',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.enable_virtual_data) return '-'
|
||||
if (data.virtual_data_mb >= 1024) {
|
||||
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
return `${data.virtual_data_mb} MB`
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -217,7 +223,7 @@
|
||||
fields: [
|
||||
{
|
||||
label: '一次性佣金金额',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (
|
||||
data.one_time_commission_amount === null ||
|
||||
data.one_time_commission_amount === undefined
|
||||
@@ -228,21 +234,21 @@
|
||||
},
|
||||
{
|
||||
label: '当前返佣比例',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.tier_info || !data.tier_info.current_rate) return '-'
|
||||
return data.tier_info.current_rate
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '下一档位比例',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.tier_info || !data.tier_info.next_rate) return '-'
|
||||
return data.tier_info.next_rate
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '下一档位阈值',
|
||||
formatter: (_, data) => {
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (
|
||||
!data.tier_info ||
|
||||
data.tier_info.next_threshold === null ||
|
||||
|
||||
@@ -353,14 +353,18 @@
|
||||
<ElInputNumber
|
||||
v-model="retailPriceForm.retail_price"
|
||||
:min="0"
|
||||
:max="999999"
|
||||
:max="retailPriceUpperLimit"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
placeholder="请输入零售价"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<!--<div style="margin-top: 5px; font-size: 12px; color: #909399">-->
|
||||
<!-- 成本价:{{ formatRetailPriceYuan(retailPriceForm.cost_price) }},单位:元-->
|
||||
<!--</div>-->
|
||||
<div style="margin-top: 5px; font-size: 12px; color: #909399">
|
||||
单位:元 | 存储值:{{ Math.round(retailPriceForm.retail_price * 100) }} 分
|
||||
<!--零售价不能大于建议售价 * 2 = {{ formatRetailPriceYuan(retailPriceUpperLimit) }}-->
|
||||
销售价格不能大于官方建议销售价格的2倍
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
@@ -417,7 +421,9 @@
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isAgentAccount = computed(() => userStore.info.user_type === 3)
|
||||
const currentUserType = computed(() => Number(userStore.info.user_type ?? 0))
|
||||
const isAgentAccount = computed(() => currentUserType.value === 3)
|
||||
const shouldHideVirtualTrafficColumns = computed(() => [3, 4].includes(currentUserType.value))
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -435,16 +441,40 @@
|
||||
const retailPriceForm = reactive({
|
||||
id: 0,
|
||||
package_name: '',
|
||||
retail_price: 0
|
||||
retail_price: 0,
|
||||
cost_price: 0,
|
||||
suggested_retail_price: 0
|
||||
})
|
||||
const retailPriceUpperLimit = computed(() =>
|
||||
Number((Math.max(retailPriceForm.suggested_retail_price, 0) * 2).toFixed(2))
|
||||
)
|
||||
const formatRetailPriceYuan = (price: number | null | undefined) =>
|
||||
`¥${Number(price ?? 0).toFixed(2)}`
|
||||
const retailPriceRules = reactive<FormRules>({
|
||||
retail_price: [
|
||||
{ required: true, message: '请输入零售价', trigger: 'blur' },
|
||||
{ type: 'number', message: '零售价必须为数字', trigger: 'blur' }
|
||||
{ type: 'number', message: '零售价必须为数字', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: any, value: number, callback: any) => {
|
||||
if (value === undefined || value === null) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
if (value > retailPriceUpperLimit.value) {
|
||||
callback(
|
||||
new Error(`零售价不能大于建议售价 * 2 = ${formatRetailPriceYuan(retailPriceUpperLimit.value)}`)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
callback()
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
package_name: '',
|
||||
@@ -549,9 +579,13 @@
|
||||
]),
|
||||
{ label: '套餐周期类型', prop: 'calendar_type' },
|
||||
{ label: '有效期', prop: 'duration_months' },
|
||||
{ label: '真流量', prop: 'real_data_mb' },
|
||||
{ label: '虚流量', prop: 'virtual_data_mb' },
|
||||
{ label: '虚流量比例', prop: 'virtual_ratio' },
|
||||
{ label: '总流量', prop: 'real_data_mb' },
|
||||
...(!shouldHideVirtualTrafficColumns.value
|
||||
? [
|
||||
{ label: '虚流量', prop: 'virtual_data_mb' },
|
||||
{ label: '虚流量比例', prop: 'virtual_ratio' }
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: '启用虚流量',
|
||||
prop: 'enable_virtual_data',
|
||||
@@ -692,7 +726,7 @@
|
||||
})
|
||||
|
||||
// GB 转 MB 处理
|
||||
const handleRealDataChange = (value: number | null) => {
|
||||
const handleRealDataChange = (value: number | null | undefined) => {
|
||||
if (value === null || value === undefined) {
|
||||
form.real_data_mb = 0
|
||||
return
|
||||
@@ -703,7 +737,7 @@
|
||||
}
|
||||
|
||||
// 虚流量比例变化处理
|
||||
const handleVirtualRatioChange = (value: number | null) => {
|
||||
const handleVirtualRatioChange = (value: number | null | undefined) => {
|
||||
if (value === null || value === undefined || realDataGb.value === 0) {
|
||||
form.virtual_data_mb = 0
|
||||
return
|
||||
@@ -863,7 +897,7 @@
|
||||
},
|
||||
{
|
||||
prop: 'real_data_mb',
|
||||
label: '真流量',
|
||||
label: '总流量',
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
const mb = row.real_data_mb ?? 0
|
||||
@@ -873,40 +907,44 @@
|
||||
return `${mb}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'virtual_data_mb',
|
||||
label: '虚流量',
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
// 如果没有启用虚流量,显示 "-"
|
||||
if (!row.enable_virtual_data) {
|
||||
return '-'
|
||||
}
|
||||
const mb = row.virtual_data_mb ?? 0
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(2)}GB`
|
||||
}
|
||||
return `${mb}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'virtual_ratio',
|
||||
label: '虚流量比例',
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
// 如果启用虚流量且真流量大于0,计算虚量百分比
|
||||
const virtualData = row.virtual_data_mb ?? 0
|
||||
const realData = row.real_data_mb ?? 0
|
||||
if (row.enable_virtual_data && realData > 0 && virtualData > 0) {
|
||||
// 虚量百分比 = (1 - 虚流量/真流量) * 100%
|
||||
// 例如:真流量100G,虚流量70G,则虚量百分比 = (1 - 70/100) * 100% = 30%
|
||||
const ratio = (1 - virtualData / realData) * 100
|
||||
return `${ratio.toFixed(2)}%`
|
||||
}
|
||||
// 否则返回 0%
|
||||
return '0%'
|
||||
}
|
||||
},
|
||||
...(!shouldHideVirtualTrafficColumns.value
|
||||
? [
|
||||
{
|
||||
prop: 'virtual_data_mb',
|
||||
label: '虚流量',
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
// 如果没有启用虚流量,显示 "-"
|
||||
if (!row.enable_virtual_data) {
|
||||
return '-'
|
||||
}
|
||||
const mb = row.virtual_data_mb ?? 0
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(2)}GB`
|
||||
}
|
||||
return `${mb}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'virtual_ratio',
|
||||
label: '虚流量比例',
|
||||
width: 120,
|
||||
formatter: (row: PackageResponse) => {
|
||||
// 如果启用虚流量且真流量大于0,计算虚量百分比
|
||||
const virtualData = row.virtual_data_mb ?? 0
|
||||
const realData = row.real_data_mb ?? 0
|
||||
if (row.enable_virtual_data && realData > 0 && virtualData > 0) {
|
||||
// 虚量百分比 = (1 - 虚流量/真流量) * 100%
|
||||
// 例如:真流量100G,虚流量70G,则虚量百分比 = (1 - 70/100) * 100% = 30%
|
||||
const ratio = (1 - virtualData / realData) * 100
|
||||
return `${ratio.toFixed(2)}%`
|
||||
}
|
||||
// 否则返回 0%
|
||||
return '0%'
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
prop: 'duration_months',
|
||||
label: '有效期',
|
||||
@@ -922,7 +960,9 @@
|
||||
natural_month: '自然月',
|
||||
by_day: '按天'
|
||||
}
|
||||
return map[row.calendar_type] || row.calendar_type || '-'
|
||||
const calendarType = row.calendar_type
|
||||
if (!calendarType) return '-'
|
||||
return map[calendarType] || calendarType
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -936,7 +976,9 @@
|
||||
monthly: '每月',
|
||||
none: '不重置'
|
||||
}
|
||||
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
|
||||
const dataResetCycle = row.data_reset_cycle
|
||||
if (!dataResetCycle) return '-'
|
||||
return map[dataResetCycle] || dataResetCycle
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -948,7 +990,9 @@
|
||||
from_activation: '实名激活',
|
||||
from_purchase: '购买时'
|
||||
}
|
||||
return map[row.expiry_base] || row.expiry_base || '-'
|
||||
const expiryBase = row.expiry_base
|
||||
if (!expiryBase) return '-'
|
||||
return map[expiryBase] || expiryBase
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -968,7 +1012,7 @@
|
||||
}
|
||||
|
||||
// 只有代理账号(user_type=3)才显示修改零售价
|
||||
if (hasAuth('package:update_retail_price') && userStore.getUserInfo.user_type === 3) {
|
||||
if (hasAuth('package:update_retail_price') && isAgentAccount.value) {
|
||||
actions.push({
|
||||
label: '修改零售价',
|
||||
handler: () => showRetailPriceDialog(row),
|
||||
@@ -1022,8 +1066,8 @@
|
||||
)
|
||||
|
||||
// 监听 is_gift 变化,自动设置 suggested_retail_price
|
||||
const handleIsGiftChange = (isGift: boolean) => {
|
||||
if (isGift) {
|
||||
const handleIsGiftChange = (isGift: string | number | boolean) => {
|
||||
if (Boolean(isGift)) {
|
||||
form.suggested_retail_price = 0
|
||||
} else {
|
||||
form.suggested_retail_price = undefined
|
||||
@@ -1189,10 +1233,11 @@
|
||||
virtualRatioPercent.value = null
|
||||
}
|
||||
form.cost_price = (data.cost_price ?? 0) / 100 // 分转换为元显示
|
||||
form.suggested_retail_price = data.suggested_retail_price
|
||||
? data.suggested_retail_price / 100
|
||||
: undefined
|
||||
originalSuggestedRetailPrice.value = data.suggested_retail_price
|
||||
form.suggested_retail_price =
|
||||
data.suggested_retail_price !== null && data.suggested_retail_price !== undefined
|
||||
? data.suggested_retail_price / 100
|
||||
: undefined
|
||||
originalSuggestedRetailPrice.value = data.suggested_retail_price ?? null
|
||||
form.is_gift = data.is_gift || false
|
||||
form.description = data.description || ''
|
||||
|
||||
@@ -1417,8 +1462,10 @@
|
||||
const showRetailPriceDialog = (row: PackageResponse) => {
|
||||
retailPriceForm.id = row.id
|
||||
retailPriceForm.package_name = row.package_name
|
||||
retailPriceForm.cost_price = (row.cost_price ?? 0) / 100
|
||||
retailPriceForm.suggested_retail_price = (row.suggested_retail_price ?? 0) / 100
|
||||
// 零售价值来自代理零售价
|
||||
retailPriceForm.retail_price = row.retail_price ? row.retail_price / 100 : 0
|
||||
retailPriceForm.retail_price = (row.retail_price ?? 0) / 100
|
||||
retailPriceDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1449,6 +1496,11 @@
|
||||
const handleCloseRetailPriceDialog = () => {
|
||||
retailPriceDialogVisible.value = false
|
||||
retailPriceFormRef.value?.resetFields()
|
||||
retailPriceForm.id = 0
|
||||
retailPriceForm.package_name = ''
|
||||
retailPriceForm.retail_price = 0
|
||||
retailPriceForm.cost_price = 0
|
||||
retailPriceForm.suggested_retail_price = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -256,13 +256,16 @@
|
||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
||||
<ElInputNumber
|
||||
v-model="packageForm.cost_price_yuan"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入成本价"
|
||||
/>
|
||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
||||
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价:
|
||||
¥{{ packageForm.original_cost_price.toFixed(2) }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
@@ -280,7 +283,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElCard,
|
||||
@@ -335,6 +338,7 @@
|
||||
package_id?: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
original_cost_price?: number
|
||||
cost_price_yuan: number
|
||||
}>({
|
||||
cost_price_yuan: 0
|
||||
@@ -352,6 +356,26 @@
|
||||
}))
|
||||
|
||||
// 梯度配置表格数据(合并授权数据和系列数据)
|
||||
watch(
|
||||
() => packageForm.value.package_id,
|
||||
(packageId) => {
|
||||
if (packageDialogType.value !== 'add' || !packageId) return
|
||||
|
||||
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
|
||||
if (!selectedPackage) return
|
||||
|
||||
const originalCostPrice =
|
||||
selectedPackage.cost_price !== undefined && selectedPackage.cost_price !== null
|
||||
? selectedPackage.cost_price / 100
|
||||
: 0
|
||||
|
||||
packageForm.value.package_name = selectedPackage.package_name
|
||||
packageForm.value.package_code = selectedPackage.package_code
|
||||
packageForm.value.original_cost_price = originalCostPrice
|
||||
packageForm.value.cost_price_yuan = originalCostPrice
|
||||
}
|
||||
)
|
||||
|
||||
const tierTableData = computed(() => {
|
||||
if (
|
||||
!detailData.value?.commission_tiers ||
|
||||
@@ -435,6 +459,9 @@
|
||||
packageDialogType.value = 'add'
|
||||
packageForm.value = {
|
||||
package_id: undefined,
|
||||
package_name: undefined,
|
||||
package_code: undefined,
|
||||
original_cost_price: undefined,
|
||||
cost_price_yuan: 0
|
||||
}
|
||||
|
||||
@@ -453,6 +480,7 @@
|
||||
package_id: row.package_id,
|
||||
package_name: row.package_name,
|
||||
package_code: row.package_code,
|
||||
original_cost_price: row.cost_price / 100,
|
||||
cost_price_yuan: row.cost_price / 100
|
||||
}
|
||||
|
||||
@@ -581,6 +609,9 @@
|
||||
const handlePackageDialogClosed = () => {
|
||||
packageFormRef.value?.resetFields()
|
||||
packageForm.value = {
|
||||
package_name: undefined,
|
||||
package_code: undefined,
|
||||
original_cost_price: undefined,
|
||||
cost_price_yuan: 0
|
||||
}
|
||||
availablePackages.value = []
|
||||
|
||||
@@ -45,166 +45,6 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 套餐列表对话框 -->
|
||||
<ElDialog
|
||||
v-model="packageListDialogVisible"
|
||||
title="套餐列表"
|
||||
width="65%"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handlePackageListDialogClosed"
|
||||
>
|
||||
<div class="package-list-dialog-content">
|
||||
<!-- 添加授权套餐按钮 -->
|
||||
<div class="package-list-header">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="showAddPackageDialog"
|
||||
v-permission="'series_grants:manage_packages'"
|
||||
>
|
||||
添加授权套餐
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 套餐列表 -->
|
||||
<ElTable
|
||||
v-if="currentGrantPackages.length > 0"
|
||||
:data="currentGrantPackages"
|
||||
border
|
||||
stripe
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<ElTableColumn prop="package_name" label="套餐名称" />
|
||||
<ElTableColumn prop="package_code" label="套餐编码" />
|
||||
<ElTableColumn label="成本价">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上架状态" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
||||
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
@click="showEditPackageDialog(row)"
|
||||
v-permission="'series_grants:edit_packages'"
|
||||
>
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="handleDeletePackage(row)"
|
||||
v-permission="'series_grants:delete_packages'"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<ElEmpty v-else description="暂无套餐" :image-size="80" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 添加/编辑套餐对话框 -->
|
||||
<ElDialog
|
||||
v-model="packageDialogVisible"
|
||||
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handlePackageDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="packageFormRef"
|
||||
:model="packageForm"
|
||||
:rules="packageFormRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<!-- 添加模式:选择套餐 -->
|
||||
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
|
||||
<ElSelect
|
||||
v-model="packageForm.package_id"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchAvailablePackages"
|
||||
:loading="packageLoading"
|
||||
clearable
|
||||
>
|
||||
<template
|
||||
v-if="availablePackages.length === 0 && !packageLoading && currentGrantSeriesId"
|
||||
>
|
||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="pkg in availablePackages"
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
||||
:value="pkg.id"
|
||||
:disabled="isPackageAlreadyAdded(pkg.id)"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 编辑模式:显示套餐信息 -->
|
||||
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
||||
<span>{{ packageForm.package_name }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
|
||||
<span>{{ packageForm.package_code }}</span>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 成本价 -->
|
||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
||||
<ElInputNumber
|
||||
v-model="packageForm.cost_price_yuan"
|
||||
:min="packageForm.original_cost_price || 0"
|
||||
:max="
|
||||
packageForm.original_cost_price
|
||||
? packageForm.original_cost_price * 1.5
|
||||
: undefined
|
||||
"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入成本价"
|
||||
/>
|
||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
||||
原价: ¥{{ packageForm.original_cost_price.toFixed(2) }}, 最高: ¥{{
|
||||
(packageForm.original_cost_price * 1.5).toFixed(2)
|
||||
}}
|
||||
(50%溢价)
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="packageDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSavePackage" :loading="packageSubmitLoading">
|
||||
保存
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -318,9 +158,7 @@
|
||||
<ElInputNumber
|
||||
v-model="pkg.cost_price"
|
||||
:min="pkg.original_cost_price || 0"
|
||||
:max="
|
||||
pkg.original_cost_price ? pkg.original_cost_price * 1.5 : undefined
|
||||
"
|
||||
:max="getPackageCostPriceMax(pkg)"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
@@ -670,66 +508,6 @@
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 套餐配置(编辑模式下禁用) -->
|
||||
<!-- <div class="form-section-title">
|
||||
<span class="title-text">套餐配置(可选)</span>
|
||||
</div> -->
|
||||
|
||||
<!-- 编辑模式下不显示套餐配置区域,请使用右键菜单中的"添加授权套餐"功能 -->
|
||||
<!-- <ElFormItem label="选择套餐">
|
||||
<ElSelect
|
||||
v-model="selectedPackageIds"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchPackages"
|
||||
:loading="packageLoading"
|
||||
clearable
|
||||
>
|
||||
<template v-if="packageOptions.length === 0 && !packageLoading && form.series_id">
|
||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="pkg.package_name"
|
||||
:value="pkg.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="form-tip">选择该授权下包含的套餐</div>
|
||||
</ElFormItem> -->
|
||||
|
||||
<!-- 套餐成本价 -->
|
||||
<!-- <ElFormItem label="套餐成本价" v-if="form.packages.length > 0">
|
||||
<div class="package-list">
|
||||
<div
|
||||
v-for="(pkg, index) in form.packages"
|
||||
:key="pkg.package_id"
|
||||
class="package-item"
|
||||
>
|
||||
<span class="package-name">{{ getPackageName(pkg.package_id) }}</span>
|
||||
<div class="cost-price-input-wrapper">
|
||||
<ElInputNumber
|
||||
v-model="pkg.cost_price"
|
||||
:min="pkg.original_cost_price || 0"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
placeholder="成本价(元)"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<span v-if="pkg.original_cost_price" class="min-cost-hint">
|
||||
(成本价: ¥{{ pkg.original_cost_price.toFixed(2) }})
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="danger" size="small" @click="removePackage(index)">删除</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tip">设置每个套餐的成本价(单位:元),不能低于套餐原始成本价</div>
|
||||
</ElFormItem> -->
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
@@ -789,9 +567,7 @@
|
||||
PackageSeriesResponse,
|
||||
ShopResponse,
|
||||
PackageResponse,
|
||||
CommissionTier,
|
||||
GrantPackageItem,
|
||||
GrantPackageInfo
|
||||
CommissionTier
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -826,68 +602,6 @@
|
||||
const formRef = ref<FormInstance>()
|
||||
const currentStep = ref(0) // 当前步骤,0 为第一步,1 为第二步
|
||||
|
||||
// 套餐列表对话框相关
|
||||
const packageListDialogVisible = ref(false)
|
||||
const currentGrantId = ref<number>(0)
|
||||
const currentGrantSeriesId = ref<number>(0)
|
||||
const currentGrantPackages = ref<GrantPackageInfo[]>([])
|
||||
|
||||
// 套餐管理相关
|
||||
const packageDialogVisible = ref(false)
|
||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||
const packageSubmitLoading = ref(false)
|
||||
const availablePackages = ref<PackageResponse[]>([])
|
||||
const packageFormRef = ref<FormInstance>()
|
||||
|
||||
// 套餐表单
|
||||
const packageForm = ref<{
|
||||
package_id?: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price_yuan: number
|
||||
original_cost_price?: number // 原始成本价(元)
|
||||
}>({
|
||||
cost_price_yuan: 0
|
||||
})
|
||||
|
||||
// 套餐表单验证规则
|
||||
const packageFormRules = computed<FormRules>(() => ({
|
||||
package_id: [
|
||||
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
|
||||
],
|
||||
cost_price_yuan: [
|
||||
{ required: true, message: '请输入成本价', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
callback(new Error('请输入成本价'))
|
||||
} else if (value < 0) {
|
||||
callback(new Error('成本价不能小于0'))
|
||||
} else if (
|
||||
packageForm.value.original_cost_price &&
|
||||
value < packageForm.value.original_cost_price
|
||||
) {
|
||||
callback(
|
||||
new Error(`成本价不能低于原价 ¥${packageForm.value.original_cost_price.toFixed(2)}`)
|
||||
)
|
||||
} else if (
|
||||
packageForm.value.original_cost_price &&
|
||||
value > packageForm.value.original_cost_price * 1.5
|
||||
) {
|
||||
callback(
|
||||
new Error(
|
||||
`成本价不能高于原价的50%溢价(最高:¥${(packageForm.value.original_cost_price * 1.5).toFixed(2)})`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const shopTreeData = ref<ShopResponse[]>([])
|
||||
@@ -907,7 +621,10 @@
|
||||
const parentId = level === 0 ? undefined : value
|
||||
|
||||
try {
|
||||
const res = await ShopService.getShopsCascade({ parent_id: parentId })
|
||||
const res = await ShopService.getShopsCascade({
|
||||
parent_id: parentId,
|
||||
exclude_self: true
|
||||
} as any)
|
||||
if (res.code === 0) {
|
||||
const nodes = (res.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
@@ -1401,6 +1118,19 @@
|
||||
return pkg ? pkg.package_name : `套餐ID: ${packageId}`
|
||||
}
|
||||
|
||||
const getPackageCostPriceMax = (pkg?: {
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number | null
|
||||
}) => {
|
||||
if (pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null) {
|
||||
return Number((pkg.suggested_retail_price * 1.5).toFixed(2))
|
||||
}
|
||||
if (pkg?.original_cost_price !== undefined && pkg.original_cost_price !== null) {
|
||||
return Number((pkg.original_cost_price * 1.5).toFixed(2))
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const formatSuggestedRetailPrice = (suggestedRetailPrice?: number | null) => {
|
||||
if (suggestedRetailPrice === undefined || suggestedRetailPrice === null) {
|
||||
return '-'
|
||||
@@ -1536,24 +1266,6 @@
|
||||
}
|
||||
)
|
||||
|
||||
// 监听套餐选择,自动设置原始成本价和成本价
|
||||
watch(
|
||||
() => packageForm.value.package_id,
|
||||
(packageId) => {
|
||||
if (packageDialogType.value === 'add' && packageId) {
|
||||
// 从 availablePackages 中查找选中的套餐
|
||||
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
|
||||
if (selectedPackage) {
|
||||
const originalCostPrice = selectedPackage.cost_price
|
||||
? selectedPackage.cost_price / 100
|
||||
: 0
|
||||
packageForm.value.original_cost_price = originalCostPrice
|
||||
packageForm.value.cost_price_yuan = originalCostPrice
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 加载系列选项(用于新增对话框,默认加载10条)
|
||||
const loadSeriesOptions = async (seriesName?: string) => {
|
||||
seriesLoading.value = true
|
||||
@@ -1630,7 +1342,10 @@
|
||||
const loadShopListForDialog = async () => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getShopsCascade({ parent_id: undefined })
|
||||
const res = await ShopService.getShopsCascade({
|
||||
parent_id: undefined,
|
||||
exclude_self: true
|
||||
} as any)
|
||||
if (res.code === 0) {
|
||||
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
@@ -2034,10 +1749,11 @@
|
||||
for (const pkg of form.packages) {
|
||||
const costPrice = pkg.cost_price ?? 0
|
||||
const originalCostPrice = pkg.original_cost_price ?? 0
|
||||
const maxCostPrice = getPackageCostPriceMax(pkg)
|
||||
|
||||
if (originalCostPrice > 0 && costPrice > originalCostPrice * 1.5) {
|
||||
if (maxCostPrice !== undefined && costPrice > maxCostPrice) {
|
||||
ElMessage.error(
|
||||
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能高于原价的50%溢价(最高:¥${(originalCostPrice * 1.5).toFixed(2)})`
|
||||
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能高于当前上限(最高:¥${maxCostPrice.toFixed(2)})`
|
||||
)
|
||||
submitLoading.value = false
|
||||
return
|
||||
@@ -2095,212 +1811,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 显示套餐列表对话框
|
||||
const showPackageListDialog = async (row: ShopSeriesGrantResponse) => {
|
||||
currentGrantId.value = row.id
|
||||
currentGrantSeriesId.value = row.series_id
|
||||
|
||||
// 获取该授权的详细信息,包括套餐列表
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(row.id)
|
||||
if (res.code === 0) {
|
||||
currentGrantPackages.value = res.data.packages || []
|
||||
packageListDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取套餐列表失败:', error)
|
||||
console.log('获取套餐列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加套餐对话框
|
||||
const showAddPackageDialog = () => {
|
||||
packageDialogType.value = 'add'
|
||||
packageForm.value = {
|
||||
package_id: undefined,
|
||||
cost_price_yuan: 0
|
||||
}
|
||||
|
||||
// 加载可用套餐
|
||||
if (currentGrantSeriesId.value) {
|
||||
loadAvailablePackages()
|
||||
}
|
||||
|
||||
packageDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑套餐对话框
|
||||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||||
packageDialogType.value = 'edit'
|
||||
|
||||
// 从 packageOptions 中查找原始成本价
|
||||
const pkgOption = packageOptions.value.find((p) => p.id === row.package_id)
|
||||
const originalCostPrice = pkgOption?.cost_price ? pkgOption.cost_price / 100 : 0
|
||||
|
||||
packageForm.value = {
|
||||
package_id: row.package_id,
|
||||
package_name: row.package_name,
|
||||
package_code: row.package_code,
|
||||
cost_price_yuan: row.cost_price / 100,
|
||||
original_cost_price: originalCostPrice
|
||||
}
|
||||
|
||||
packageDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 加载可用套餐
|
||||
const loadAvailablePackages = async (packageName?: string) => {
|
||||
if (!currentGrantSeriesId.value) return
|
||||
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
series_id: currentGrantSeriesId.value
|
||||
}
|
||||
|
||||
if (packageName) {
|
||||
params.package_name = packageName
|
||||
}
|
||||
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
// 过滤掉赠送套餐
|
||||
availablePackages.value = res.data.items.filter((pkg) => !pkg.is_gift)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载套餐选项失败:', error)
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索可用套餐
|
||||
const searchAvailablePackages = (query: string) => {
|
||||
if (query) {
|
||||
loadAvailablePackages(query)
|
||||
} else {
|
||||
loadAvailablePackages()
|
||||
}
|
||||
}
|
||||
|
||||
// 检查套餐是否已添加
|
||||
const isPackageAlreadyAdded = (packageId: number) => {
|
||||
return currentGrantPackages.value.some((p) => p.package_id === packageId)
|
||||
}
|
||||
|
||||
// 保存套餐
|
||||
const handleSavePackage = async () => {
|
||||
if (!packageFormRef.value || !currentGrantId.value) return
|
||||
|
||||
await packageFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
packageSubmitLoading.value = true
|
||||
try {
|
||||
const packages: GrantPackageItem[] = []
|
||||
|
||||
if (packageDialogType.value === 'add') {
|
||||
// 添加模式:添加新套餐
|
||||
packages.push({
|
||||
package_id: packageForm.value.package_id,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
})
|
||||
} else {
|
||||
// 编辑模式:更新套餐成本价
|
||||
packages.push({
|
||||
package_id: packageForm.value.package_id,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
})
|
||||
}
|
||||
|
||||
await ShopSeriesGrantService.manageGrantPackages(currentGrantId.value, { packages })
|
||||
ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
packageDialogVisible.value = false
|
||||
|
||||
// 刷新套餐列表
|
||||
await refreshPackageList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log(packageDialogType.value === 'add' ? '添加失败' : '更新失败')
|
||||
} finally {
|
||||
packageSubmitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除套餐
|
||||
const handleDeletePackage = (row: GrantPackageInfo) => {
|
||||
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
if (!currentGrantId.value) return
|
||||
|
||||
packageSubmitLoading.value = true
|
||||
try {
|
||||
const packages: GrantPackageItem[] = [
|
||||
{
|
||||
package_id: row.package_id,
|
||||
remove: true
|
||||
}
|
||||
]
|
||||
|
||||
await ShopSeriesGrantService.manageGrantPackages(currentGrantId.value, { packages })
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 刷新套餐列表
|
||||
await refreshPackageList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log('删除失败')
|
||||
} finally {
|
||||
packageSubmitLoading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新套餐列表
|
||||
const refreshPackageList = async () => {
|
||||
if (!currentGrantId.value) return
|
||||
|
||||
try {
|
||||
const res = await ShopSeriesGrantService.getShopSeriesGrantDetail(currentGrantId.value)
|
||||
if (res.code === 0) {
|
||||
currentGrantPackages.value = res.data.packages || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新套餐列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭套餐对话框
|
||||
const handlePackageDialogClosed = () => {
|
||||
packageFormRef.value?.resetFields()
|
||||
packageForm.value = {
|
||||
cost_price_yuan: 0
|
||||
}
|
||||
availablePackages.value = []
|
||||
}
|
||||
|
||||
// 关闭套餐列表对话框
|
||||
const handlePackageListDialogClosed = () => {
|
||||
currentGrantId.value = 0
|
||||
currentGrantSeriesId.value = 0
|
||||
currentGrantPackages.value = []
|
||||
availablePackages.value = []
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
// 下一步
|
||||
const handleNextStep = async () => {
|
||||
if (!formRef.value) return
|
||||
@@ -2483,16 +1993,4 @@
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.package-list-dialog-content {
|
||||
.package-list-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="150"
|
||||
height="70vh"
|
||||
:inlineActionsCount="2"
|
||||
>
|
||||
<template #default>
|
||||
@@ -99,10 +100,6 @@
|
||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
||||
<ElInputNumber
|
||||
v-model="packageForm.cost_price_yuan"
|
||||
:min="packageForm.original_cost_price || 0"
|
||||
:max="
|
||||
packageForm.original_cost_price ? packageForm.original_cost_price * 1.5 : undefined
|
||||
"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
@@ -110,10 +107,8 @@
|
||||
placeholder="请输入成本价"
|
||||
/>
|
||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
||||
原价: ¥{{ packageForm.original_cost_price.toFixed(2) }}, 最高: ¥{{
|
||||
(packageForm.original_cost_price * 1.5).toFixed(2)
|
||||
}}
|
||||
(50%溢价)
|
||||
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价:
|
||||
¥{{ packageForm.original_cost_price.toFixed(2) }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
@@ -132,7 +127,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElCard,
|
||||
@@ -176,15 +171,42 @@
|
||||
const packageDialogVisible = ref(false)
|
||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||
|
||||
const packageForm = ref<{
|
||||
type PackageFormState = {
|
||||
package_id?: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price_yuan: number
|
||||
original_cost_price?: number
|
||||
}>({
|
||||
suggested_retail_price?: number
|
||||
}
|
||||
|
||||
const createDefaultPackageForm = (): PackageFormState => ({
|
||||
package_id: undefined,
|
||||
package_name: undefined,
|
||||
package_code: undefined,
|
||||
cost_price_yuan: 0
|
||||
})
|
||||
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
|
||||
|
||||
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
|
||||
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
||||
}
|
||||
|
||||
const resolvePackagePricing = (row?: GrantPackageInfo) => {
|
||||
const pkgOption = availablePackages.value.find((p) => p.id === row?.package_id)
|
||||
const currentCostPrice = row?.cost_price !== undefined ? row.cost_price / 100 : 0
|
||||
|
||||
return {
|
||||
original_cost_price:
|
||||
pkgOption?.cost_price !== undefined && pkgOption?.cost_price !== null
|
||||
? pkgOption.cost_price / 100
|
||||
: currentCostPrice,
|
||||
suggested_retail_price:
|
||||
pkgOption?.suggested_retail_price !== undefined && pkgOption?.suggested_retail_price !== null
|
||||
? pkgOption.suggested_retail_price / 100
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
const packageRules = computed<FormRules>(() => ({
|
||||
package_id: [
|
||||
@@ -198,22 +220,6 @@
|
||||
callback(new Error('请输入成本价'))
|
||||
} else if (value < 0) {
|
||||
callback(new Error('成本价不能小于0'))
|
||||
} else if (
|
||||
packageForm.value.original_cost_price &&
|
||||
value < packageForm.value.original_cost_price
|
||||
) {
|
||||
callback(
|
||||
new Error(`成本价不能低于原价 ¥${packageForm.value.original_cost_price.toFixed(2)}`)
|
||||
)
|
||||
} else if (
|
||||
packageForm.value.original_cost_price &&
|
||||
value > packageForm.value.original_cost_price * 1.5
|
||||
) {
|
||||
callback(
|
||||
new Error(
|
||||
`成本价不能高于原价的50%溢价(最高:¥${(packageForm.value.original_cost_price * 1.5).toFixed(2)})`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@@ -270,28 +276,26 @@
|
||||
|
||||
const showAddPackageDialog = () => {
|
||||
packageDialogType.value = 'add'
|
||||
packageForm.value = {
|
||||
package_id: undefined,
|
||||
cost_price_yuan: 0
|
||||
}
|
||||
assignPackageForm()
|
||||
loadAvailablePackages()
|
||||
packageDialogVisible.value = true
|
||||
nextTick(() => packageFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||||
packageDialogType.value = 'edit'
|
||||
const packagePricing = resolvePackagePricing(row)
|
||||
|
||||
const pkgOption = availablePackages.value.find((p) => p.id === row.package_id)
|
||||
const originalCostPrice = pkgOption?.cost_price ? pkgOption.cost_price / 100 : 0
|
||||
|
||||
packageForm.value = {
|
||||
assignPackageForm({
|
||||
package_id: row.package_id,
|
||||
package_name: row.package_name,
|
||||
package_code: row.package_code,
|
||||
cost_price_yuan: row.cost_price / 100,
|
||||
original_cost_price: originalCostPrice
|
||||
}
|
||||
original_cost_price: packagePricing.original_cost_price,
|
||||
suggested_retail_price: packagePricing.suggested_retail_price
|
||||
})
|
||||
packageDialogVisible.value = true
|
||||
nextTick(() => packageFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const loadAvailablePackages = async (packageName?: string) => {
|
||||
@@ -330,6 +334,28 @@
|
||||
return packageList.value.some((p) => p.package_id === packageId)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => packageForm.value.package_id,
|
||||
(packageId) => {
|
||||
if (packageDialogType.value === 'add' && packageId) {
|
||||
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
|
||||
if (selectedPackage) {
|
||||
const originalCostPrice = selectedPackage.cost_price
|
||||
? selectedPackage.cost_price / 100
|
||||
: 0
|
||||
const suggestedRetailPrice =
|
||||
selectedPackage.suggested_retail_price !== undefined &&
|
||||
selectedPackage.suggested_retail_price !== null
|
||||
? selectedPackage.suggested_retail_price / 100
|
||||
: undefined
|
||||
packageForm.value.original_cost_price = originalCostPrice
|
||||
packageForm.value.suggested_retail_price = suggestedRetailPrice
|
||||
packageForm.value.cost_price_yuan = originalCostPrice
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleSavePackage = async () => {
|
||||
if (!packageFormRef.value || !grantId.value) return
|
||||
|
||||
@@ -338,19 +364,16 @@
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const packages: GrantPackageItem[] = []
|
||||
|
||||
if (packageDialogType.value === 'add') {
|
||||
packages.push({
|
||||
package_id: packageForm.value.package_id,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
})
|
||||
} else {
|
||||
packages.push({
|
||||
package_id: packageForm.value.package_id,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
})
|
||||
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
||||
if (Number.isNaN(currentCostPriceYuan)) {
|
||||
throw new Error('当前成本价无效,无法保存')
|
||||
}
|
||||
const packages: GrantPackageItem[] = [
|
||||
{
|
||||
package_id: packageForm.value.package_id,
|
||||
cost_price: Math.round(currentCostPriceYuan * 100)
|
||||
}
|
||||
]
|
||||
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages })
|
||||
packageDialogVisible.value = false
|
||||
@@ -392,10 +415,8 @@
|
||||
}
|
||||
|
||||
const handlePackageDialogClosed = () => {
|
||||
packageFormRef.value?.resetFields()
|
||||
packageForm.value = {
|
||||
cost_price_yuan: 0
|
||||
}
|
||||
packageFormRef.value?.clearValidate()
|
||||
assignPackageForm()
|
||||
availablePackages.value = []
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user