🎨 优化vip,文章列表页,兑换码管理页面

This commit is contained in:
2025-12-08 19:28:36 +08:00
parent 39a1165908
commit 387cdf8e14
4 changed files with 143 additions and 15 deletions

View File

@@ -4,7 +4,7 @@
:border="!!config?.border"
:data="computedData"
ref="table"
row-key="nanoId"
:row-key="config?.rowKey || 'ID'"
v-bind="computedStyle"
:show-overflow-tooltip="config.tooltip"
:header-row-style="headerRowStyle"
@@ -54,6 +54,7 @@
v-show="isShow"
v-model:current-page="current"
v-model:page-size="size"
:page-sizes="[10, 30, 50, 100,500]"
:background="props.background"
:layout="props.layout"
:total="props.total"

View File

@@ -187,6 +187,19 @@
</el-table-column>
</template>
</el-table>
<!-- 分页组件 -->
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
<el-pagination
v-model:current-page="itemQueryParams.page"
v-model:page-size="itemQueryParams.pageSize"
:page-sizes="[50, 100, 200, 500]"
:total="itemTotal"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleItemSizeChange"
@current-change="handleItemCurrentChange"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleSelectItemClose">取消</el-button>
@@ -288,8 +301,13 @@ const itemOptionsLoading = ref(false)
const selectedItem = ref(null)
const selectedItemName = ref('')
const itemSearchKeyword = ref('')
const itemQueryParams = ref({
page: 1,
pageSize: 100
})
const itemTotal = ref(0)
// 过滤后的项目选项
// 过滤后的项目选项(前端搜索过滤)
const filteredItemOptions = computed(() => {
if (!itemSearchKeyword.value) {
return itemOptions.value
@@ -401,7 +419,10 @@ const loadItemOptions = async () => {
itemOptionsLoading.value = true
try {
let res
const params = { page: 1, pageSize: 1000 }
const params = {
page: itemQueryParams.value.page,
pageSize: itemQueryParams.value.pageSize
}
switch (mkForm.value.type) {
case 1: // VIP
@@ -415,19 +436,23 @@ const loadItemOptions = async () => {
break
default:
itemOptions.value = []
itemTotal.value = 0
return
}
if (res.code === 0) {
itemOptions.value = res.data.list || []
itemTotal.value = res.data.total || 0
} else {
ElMessage.error(res.msg || '获取数据失败')
itemOptions.value = []
itemTotal.value = 0
}
} catch (error) {
console.error('加载项目选项失败:', error)
ElMessage.error('加载数据失败')
itemOptions.value = []
itemTotal.value = 0
} finally {
itemOptionsLoading.value = false
}
@@ -457,11 +482,33 @@ const handleSelectItemClose = () => {
selectItemDialogVisible.value = false
selectedItem.value = null
itemSearchKeyword.value = ''
// 重置分页参数
itemQueryParams.value = {
page: 1,
pageSize: 100
}
}
// 处理项目搜索
const handleItemSearch = () => {
// 搜索逻辑已在 computed 中处理
// 搜索逻辑已在 computed 中处理(前端过滤)
// 如果有搜索关键词,重置到第一页以便查看所有结果
if (itemSearchKeyword.value) {
itemQueryParams.value.page = 1
}
}
// 处理项目分页大小变化
const handleItemSizeChange = (size) => {
itemQueryParams.value.pageSize = size
itemQueryParams.value.page = 1
loadItemOptions()
}
// 处理项目分页变化
const handleItemCurrentChange = (page) => {
itemQueryParams.value.page = page
loadItemOptions()
}
// 获取项目显示名称
@@ -478,9 +525,9 @@ const loadItemNameCache = async () => {
try {
// 获取所有类型的数据并缓存名称
const [vipRes, teacherVipRes, goodsRes] = await Promise.all([
vipList({ page: 1, pageSize: 1000 }),
teacherVipList({ page: 1, pageSize: 1000 }),
goodsList({ page: 1, pageSize: 1000 })
vipList({ page: 1, pageSize: 100 }),
teacherVipList({ page: 1, pageSize: 100 }),
goodsList({ page: 1, pageSize: 100 })
])
// 缓存VIP名称
@@ -557,7 +604,7 @@ async function getMkList() {
// 获取兑换码库选项
async function getMkOptions() {
try {
const res = await mkList({ page: 1, pageSize: 1000 })
const res = await mkList({ page: 1, pageSize: 100 })
if (res.code === 0) {
mkOptions.value = res.data.list || []
}
@@ -597,7 +644,7 @@ const handleEditMk = async (row) => {
const handleManageCdk = (row) => {
// 跳转到兑换码管理页面传递兑换码库ID和名称
router.push({
path: 'cdkManage',
name: 'CdkManage',
query: {
id: row.ID,
name: encodeURIComponent(row.codeName)
@@ -650,7 +697,7 @@ const handleExportClose = () => {
const loadDomainOptions = async () => {
domainOptionsLoading.value = true
try {
const res = await domainList({ page: 1, pageSize: 1000, status: 1 })
const res = await domainList({ page: 1, pageSize: 100, status: 1 })
if (res.code === 0) {
domainOptions.value = res.data.list || []
} else {

View File

@@ -12,15 +12,19 @@
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="handleEdit()">新增</el-button>
<el-button type="danger" icon="delete" :disabled="!selectedIds.length" @click="handleBatchDelete">批量删除</el-button>
</div>
<Content
ref="contentRef"
@changePage="changePage"
@selection="handleSelection"
:total="total"
v-model:tabloading="tableLoading"
v-model:currentPage="queryParams.page"
v-model:pageSize="queryParams.pageSize"
:data="tableData"
:config="ARTICLE_TABLE_CONFIG"
:maxHeight="tableMaxHeight"
>
<template #status="{ row }">
<el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
@@ -65,6 +69,19 @@
</template>
</Content>
</div>
<!-- 悬浮回到顶部 -->
<el-backtop :right="40" :bottom="80" />
<!-- 悬浮滚动到底部按钮 -->
<div
class="scroll-to-bottom"
@click="scrollToBottom"
title="滚动到底部"
>
<svg viewBox="0 0 1024 1024" width="18" height="18">
<path d="M512 704L160 352h704z" fill="#409EFF"/>
</svg>
</div>
<!-- 在 template 的 Content 组件后添加 Dialog -->
<el-dialog
v-model="detailVisible"
@@ -121,6 +138,7 @@
const router = useRouter()
const EMPTY_STR = '- -'
const contentRef = ref(null)
// 在 script setup 中添加响应式数据
const detailVisible = ref(false)
const detailData = ref({})
@@ -130,6 +148,8 @@
const finalSearchConfig = ref(JSON.parse(JSON.stringify(ARTICLE_SEARCH_CONFIG))) // 最终的搜索配置
const tableLoading = ref(true), tableData = ref([])
const selectedIds = ref([])
const tableMaxHeight = ref('70vh')
const queryParams = ref({
page: 1,
pageSize: 10,
@@ -352,6 +372,10 @@
}
})
}
// 表格勾选变化
function handleSelection({ newSelection }) {
selectedIds.value = (newSelection || []).map(item => item.ID)
}
// 切换免费/付费状态
async function statusChangeBefore(row) {
@@ -377,6 +401,18 @@
row.loading = false
}
}
// 滚动到底部(针对表格滚动容器)
function scrollToBottom() {
try {
const bodyWrapper = contentRef?.value?.table?.$refs?.bodyWrapper
if (bodyWrapper) {
bodyWrapper.scrollTo({ top: bodyWrapper.scrollHeight, behavior: 'smooth' })
return
}
} catch (e) { console.warn('scrollToBottom error', e) }
// 兜底:滚动整个页面
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' })
}
async function handleDelete(row) {
try {
@@ -400,10 +436,47 @@
}
}
}
// 批量删除
async function handleBatchDelete() {
if (!selectedIds.value.length) return
try {
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
const res = await del({ ids: selectedIds.value })
if (res.code === 0) {
ElMessage.success('删除成功')
selectedIds.value = []
getList()
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败')
}
}
}
function changePage(data) {
queryParams.value.page = data
getList()
}
</script>
<style lang="scss" scoped>
.scroll-to-bottom{
position: fixed;
right: 40px;
bottom: 30px;
width: 40px;
height: 40px;
border-radius: 4px;
background: #ecf5ff;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 9;
}
</style>

View File

@@ -60,7 +60,7 @@
<template #default="scope">{{ formatLevel(scope.row.level) }}</template>
</el-table-column>
<el-table-column sortable align="left" label="价格" prop="price" width="120">
<template #default="scope">{{ Number(scope.row.price).toFixed(2) }}</template>
<template #default="scope">{{ (Number(scope.row.price) / 100).toFixed(2) }}</template>
</el-table-column>
<el-table-column sortable align="left" label="有效期(天)" prop="expiration" width="120" />
<el-table-column align="left" label="描述" prop="des" min-width="200" show-overflow-tooltip />
@@ -259,7 +259,9 @@ const updateRow = async (row) => {
const res = await apiDetail(row.ID)
type.value = 'update'
if (res.code === 0) {
formData.value = { ...res.data }
const detail = res.data || {}
const priceInYuan = detail.price != null ? Number((Number(detail.price) / 100).toFixed(2)) : undefined
formData.value = { ...detail, price: priceInYuan }
dialogFormVisible.value = true
}
}
@@ -319,15 +321,20 @@ const enterDialog = async () => {
elFormRef.value?.validate(async (valid) => {
if (!valid) return (btnLoading.value = false)
let res
const payload = {
...formData.value,
// 价格按元输入,提交时转为分,确保为整数
price: Math.round(Number(formData.value.price || 0) * 100),
}
switch (type.value) {
case 'create':
res = await apiAdd(formData.value)
res = await apiAdd(payload)
break
case 'update':
res = await apiEdit(formData.value)
res = await apiEdit(payload)
break
default:
res = await apiAdd(formData.value)
res = await apiAdd(payload)
break
}
btnLoading.value = false