feat: 用户详情代金券管理与优惠模块修复
Build and Deploy Vue3 / build (push) Successful in 1m39s
Build and Deploy Vue3 / deploy (push) Successful in 42s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shiran
2026-06-26 16:46:57 +08:00
co-authored by Cursor
parent 6f82e5e79d
commit 4bf7c4857b
15 changed files with 862 additions and 111 deletions
+33 -9
View File
@@ -122,6 +122,12 @@
<el-tag v-else-if="row.isProduct" type="success" size="small" style="margin-left: 8px;">商品</el-tag>
<span class="group-name">{{ row.name }}</span>
<el-tag
v-if="(row.isGroup && row.recommendWords) || (row.isProduct && row.data?.recommendWords)"
type="danger"
size="small"
style="margin-left: 6px;"
>{{ row.isGroup ? row.recommendWords : row.data?.recommendWords }}</el-tag>
</div>
</template>
</el-table-column>
@@ -207,6 +213,7 @@
<div class="group-name-cell">
<el-avatar v-if="row.cover" :size="32" :src="row.cover" />
<span class="group-name">{{ row.name }}</span>
<el-tag v-if="row.recommendWords" type="danger" size="small" style="margin-left: 6px;">{{ row.recommendWords }}</el-tag>
</div>
</template>
</el-table-column>
@@ -393,6 +400,11 @@
</div>
</div>
<!-- 推荐词 -->
<el-form-item prop="recommend_words" label="推荐词">
<el-input v-model="groupForm.recommend_words" placeholder="推荐词(可选,如:热销、新品)" clearable />
</el-form-item>
<!-- 备注 -->
<el-form-item prop="note" label="备注" class="note-form-item">
<el-input
@@ -761,6 +773,9 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="推荐词" prop="recommend_words">
<el-input v-model="productForm.recommend_words" placeholder="推荐词(可选,如:热销、新品)" clearable style="width: 100%" />
</el-form-item>
<el-form-item label="购买通知" prop="send_notice">
<el-switch
v-model="productForm.send_notice"
@@ -893,7 +908,8 @@ const groupForm = reactive({
cover_id: undefined,
cover_url: '',
tag_id: undefined,
index: 0
index: 0,
recommend_words: ''
})
const groupRules = {
@@ -949,7 +965,8 @@ const productForm = reactive({
max_first_purchase_duration: 0,
send_notice: false,
renew_price: 0,
renew_recommend_rebate: 0
renew_recommend_rebate: 0,
recommend_words: ''
})
const productRules = {
@@ -1496,7 +1513,8 @@ const handleAdd = (parentRow) => {
cover_id: undefined,
cover_url: '',
tag_id: parentTagId || undefined,
index: 0
index: 0,
recommend_words: ''
})
console.log('添加子级,父级信息:', parentRow.name, 'ID:', parentRow.id, 'Level:', parentRow.level, '标签:', parentRow.tag)
} else {
@@ -1511,7 +1529,8 @@ const handleAdd = (parentRow) => {
cover_id: undefined,
cover_url: '',
tag_id: undefined,
index: 0
index: 0,
recommend_words: ''
})
}
@@ -1532,7 +1551,8 @@ const handleEdit = (row) => {
cover_id: row.coverId || undefined,
cover_url: row.cover || '',
tag_id: row.tag?.id || row.tagId || undefined,
index: row.index || 0
index: row.index || 0,
recommend_words: row.recommendWords || ''
})
dialogVisible.value = true
@@ -1602,7 +1622,8 @@ const handleAddProduct = () => {
arg_type: 'all',
require_real_name: false,
max_per_user: 0,
max_first_purchase_duration: 0
max_first_purchase_duration: 0,
recommend_words: ''
})
selectedProductGroup.value = null
@@ -1642,7 +1663,8 @@ const handleEditProduct = (product, parentGroupId) => {
max_first_purchase_duration: product.maxFirstPurchaseDuration ?? product.max_first_purchase_duration ?? 0,
send_notice: !!product.sendNotice,
renew_price: (product.renewPrice ?? product.renew_price ?? 0) / 100,
renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0
renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0,
recommend_words: product.recommendWords ?? product.recommend_words ?? ''
})
productDialogVisible.value = true
@@ -1674,7 +1696,8 @@ const submitProductForm = () => {
max_first_purchase_duration: Number(productForm.max_first_purchase_duration) || 0,
send_notice: productForm.send_notice === true,
renew_price: Number(productForm.renew_price) || 0,
renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0
renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0,
recommend_words: productForm.recommend_words || ''
}
let res
@@ -1813,7 +1836,8 @@ const submitForm = () => {
name: groupForm.name.trim(),
note: groupForm.note || '',
disable: groupForm.disable,
index: Number(groupForm.index) || 0
index: Number(groupForm.index) || 0,
recommend_words: groupForm.recommend_words || ''
}
if (groupForm.parent_id) {
+14 -3
View File
@@ -24,6 +24,13 @@
<div class="stat-value">{{ counts.expired }}</div>
</div>
</div>
<div class="stat-card" :class="{ active: query.status === 'pending' }" @click="handleStatusCard('pending')">
<div class="stat-icon stat-icon-pending"><el-icon><Timer /></el-icon></div>
<div class="stat-body">
<div class="stat-label">待开通</div>
<div class="stat-value">{{ counts.pending }}</div>
</div>
</div>
<div class="stat-card" :class="{ active: query.status === 'deleted' }" @click="handleStatusCard('deleted')">
<div class="stat-icon stat-icon-deleted"><el-icon><Delete /></el-icon></div>
<div class="stat-body">
@@ -60,6 +67,7 @@
<el-option label="全部" value="all" />
<el-option label="正常" value="normal" />
<el-option label="已到期" value="expired" />
<el-option label="待开通" value="pending" />
<el-option label="已删除" value="deleted" />
</el-select>
</el-form-item>
@@ -133,6 +141,7 @@
<el-tag size="small" type="danger">已删除</el-tag>
</el-tooltip>
<el-tag v-else-if="isExpired(row)" size="small" type="warning">已到期</el-tag>
<el-tag v-else-if="row.status === 'pending'" size="small" type="info">待开通</el-tag>
<el-tag v-else size="small" type="success">正常</el-tag>
</template>
</el-table-column>
@@ -513,7 +522,7 @@
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, ArrowDown, Box, CircleCheck, Clock, Delete } from '@element-plus/icons-vue'
import { Plus, Refresh, Search, ArrowDown, Box, CircleCheck, Clock, Delete, Timer } from '@element-plus/icons-vue'
import { getUserGoodsList, getUserGoodsCount, createUserGoods, updateUserGoods, deleteUserGoods, getUserVmList, getExpireRemindList, sendExpireRemind } from '@/api/admin/userVm'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { formatToApiTime } from '@/utils/tool'
@@ -536,8 +545,8 @@ const showFilterUserSelector = ref(false)
const showFilterProductSelector = ref(false)
//
const counts = reactive({ normal: 0, deleted: 0, expired: 0 })
const countTotal = computed(() => (counts.normal || 0) + (counts.deleted || 0) + (counts.expired || 0))
const counts = reactive({ normal: 0, deleted: 0, expired: 0, pending: 0 })
const countTotal = computed(() => (counts.normal || 0) + (counts.deleted || 0) + (counts.expired || 0) + (counts.pending || 0))
const formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
@@ -616,6 +625,7 @@ const loadCount = async () => {
counts.normal = d.normal ?? 0
counts.deleted = d.deleted ?? 0
counts.expired = d.expired ?? 0
counts.pending = d.pending ?? 0
}
} catch { /* 统计失败不阻断列表 */ }
}
@@ -1054,6 +1064,7 @@ onMounted(() => { loadList(); loadCount() })
.stat-icon-normal { background: #f0faf0; color: #52c41a; }
.stat-icon-expired { background: #fff7e6; color: #fa8c16; }
.stat-icon-deleted { background: #fff1f0; color: #f5222d; }
.stat-icon-pending { background: #e6f7ff; color: #1890ff; }
.stat-body { display: flex; flex-direction: column; }
.stat-label { font-size: 13px; color: #909399; margin-bottom: 2px; }
@@ -47,6 +47,9 @@
<span class="price-symbol">¥</span>
<span class="price-amount">{{ formatPlanPrice(row) }}</span>
</div>
<div v-if="isFixedPrice(row) && getRenewPrice(row) > 0" class="plan-renew-price">
续费 ¥{{ formatRenewPrice(row) }}
</div>
</div>
<div class="plan-stat plan-stat-inventory" :class="getInventoryClass(row)">
<div class="plan-stat-label">
@@ -471,6 +474,14 @@ const formatPlanPrice = (row) => {
return (Number(raw) / 100).toFixed(2)
}
const getRenewPrice = (row) => {
return Number(row.renewFixedPrice ?? row.renew_fixed_price ?? 0)
}
const formatRenewPrice = (row) => {
return (getRenewPrice(row) / 100).toFixed(2)
}
const getInventoryNum = (row) => {
return Number(row.inventory ?? 0) || 0
}
@@ -1018,6 +1029,14 @@ watch(() => props.visible, (val) => {
color: #f56c6c;
font-weight: 700;
}
.plan-renew-price {
font-size: 11px;
color: #e6a23c;
font-weight: 500;
margin-top: 2px;
line-height: 1;
}
.plan-stat-price.is-dynamic {
background: linear-gradient(135deg, #f0f7ff 0%, #e6f2ff 100%);
border-color: #c6e2ff;