Merge remote-tracking branch 'origin/huangzhenbao-admin'
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">卡密会员</text>
|
||||
<text class="page-subtitle">Component: UserGradeCard</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 卡密会员 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">会员记录</text>
|
||||
<text class="page-subtitle">Component: UserGradeRecord</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 会员记录 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">会员权益</text>
|
||||
<text class="page-subtitle">Component: UserGradeRight</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 会员权益 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">会员类型</text>
|
||||
<text class="page-subtitle">Component: UserGradeType</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 会员类型 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@@ -3,12 +3,7 @@
|
||||
<view class="content-card">
|
||||
<!-- 操作按钮行 -->
|
||||
<view class="action-bar">
|
||||
<view class="filter-item">
|
||||
<input class="filter-input" v-model="searchText" placeholder="搜索分组名称/备注" @confirm="handleSearch" />
|
||||
</view>
|
||||
<button class="btn primary small" @click="onAddGroup">添加分组</button>
|
||||
<button class="btn ghost small" @click="handleSearch">查询</button>
|
||||
<button class="btn ghost small" @click="handleReset">重置</button>
|
||||
</view>
|
||||
|
||||
<!-- 分组列表表格 -->
|
||||
@@ -16,48 +11,41 @@
|
||||
<!-- 表头 -->
|
||||
<view class="table-header">
|
||||
<view class="col col-id"><text>ID</text></view>
|
||||
<view class="col col-name"><text>分组名称</text></view>
|
||||
<view class="col col-remark"><text>备注</text></view>
|
||||
<view class="col col-status"><text>状态</text></view>
|
||||
<view class="col col-name"><text>分组</text></view>
|
||||
<view class="col col-ops"><text>操作</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<view class="table-body">
|
||||
<view v-if="loading" class="table-loading"><text>加载中...</text></view>
|
||||
<view v-else-if="groupList.length === 0" class="table-empty"><text>暂无数据</text></view>
|
||||
<view v-else v-for="(group, index) in groupList" :key="group.id" class="table-row">
|
||||
<view v-for="group in groupList" :key="group.id" class="table-row">
|
||||
<view class="col col-id"><text>{{ group.id }}</text></view>
|
||||
<view class="col col-name"><text>{{ group.name }}</text></view>
|
||||
<view class="col col-remark"><text>{{ group.remark || '-' }}</text></view>
|
||||
<view class="col col-status">
|
||||
<view :class="['switch-box', group.status === 1 ? 'active' : '']" @click="onToggleStatus(index)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="col col-ops">
|
||||
<text class="op-link" @click="onEditGroup(group)">修改</text>
|
||||
<view class="op-divider">|</view>
|
||||
<text class="op-link text-danger" @click="onDeleteGroup(group.id)">删除</text>
|
||||
<text class="op-link" @click="onDeleteGroup(group)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-row">
|
||||
<text class="total-text">共 {{ total }} 条</text>
|
||||
<view class="page-btns">
|
||||
<view :class="['page-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"><text>‹</text></view>
|
||||
<view class="page-btn active"><text>{{ page }}</text></view>
|
||||
<view :class="['page-btn', page >= totalPages ? 'disabled' : '']" @click="nextPage"><text>›</text></view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text>前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" @confirm="goToJumpPage" />
|
||||
<text>页</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="groupList.length > 0"
|
||||
:total="groupList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 添加/修改分组弹窗 -->
|
||||
@@ -77,26 +65,9 @@
|
||||
class="form-input"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入分组名称"
|
||||
autofocus
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item" style="margin-top: 20px;">
|
||||
<view class="label-box">
|
||||
<text class="label">备注说明:</text>
|
||||
</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注说明"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item" style="margin-top: 20px;">
|
||||
<view class="label-box">
|
||||
<text class="label">状态:</text>
|
||||
</view>
|
||||
<view :class="['switch-box', formData.status === 1 ? 'active' : '']" @click="formData.status = (formData.status === 1 ? 0 : 1)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn ghost" @click="closeModal">取消</button>
|
||||
@@ -108,51 +79,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import {
|
||||
fetchAdminUserGroupPage,
|
||||
saveAdminUserGroup,
|
||||
deleteAdminUserGroup,
|
||||
setAdminUserGroupStatus,
|
||||
type AdminUserGroup
|
||||
} from '@/services/admin/userGroupService.uts'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const searchText = ref('')
|
||||
const total = ref(0)
|
||||
const groupList = ref<AdminUserGroup[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const totalPages = computed((): number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
const pages = Math.ceil(total.value / pageSize.value)
|
||||
return pages <= 0 ? 1 : pages
|
||||
})
|
||||
|
||||
const loadGroupList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAdminUserGroupPage(page.value, pageSize.value, {
|
||||
search: searchText.value || null,
|
||||
status: null,
|
||||
includeDeleted: false
|
||||
})
|
||||
|
||||
groupList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadGroupList()
|
||||
})
|
||||
// 分组数据
|
||||
const groupList = ref([
|
||||
{ id: 251, name: 'A类客户' },
|
||||
{ id: 252, name: 'B类客户' },
|
||||
{ id: 253, name: 'C类客户' },
|
||||
{ id: 254, name: 'D类客户' }
|
||||
])
|
||||
|
||||
// 弹窗状态
|
||||
const showModal = ref(false)
|
||||
@@ -161,101 +97,35 @@ const modalTitle = computed(() => isEdit.value ? '修改分组' : '添加分组'
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
id: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 搜索与重置
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadGroupList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchText.value = ''
|
||||
page.value = 1
|
||||
loadGroupList()
|
||||
}
|
||||
|
||||
// 分页控制
|
||||
function prevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadGroupList()
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadGroupList()
|
||||
}
|
||||
}
|
||||
|
||||
function goToJumpPage() {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadGroupList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '请输入有效的页码', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
async function onToggleStatus(index: number) {
|
||||
const group = groupList.value[index]
|
||||
const targetStatus = group.status === 1 ? 0 : 1
|
||||
try {
|
||||
const ok = await setAdminUserGroupStatus(group.id, targetStatus)
|
||||
if (ok) {
|
||||
group.status = targetStatus
|
||||
uni.showToast({ title: '状态更新成功' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '状态更新失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 添加分组
|
||||
function onAddGroup() {
|
||||
isEdit.value = false
|
||||
formData.id = ''
|
||||
formData.id = 0
|
||||
formData.name = ''
|
||||
formData.remark = ''
|
||||
formData.status = 1
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 修改分组
|
||||
function onEditGroup(group: AdminUserGroup) {
|
||||
function onEditGroup(group: any) {
|
||||
isEdit.value = true
|
||||
formData.id = group.id
|
||||
formData.name = group.name
|
||||
formData.remark = group.remark || ''
|
||||
formData.status = group.status
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 删除分组
|
||||
function onDeleteGroup(id: string) {
|
||||
function onDeleteGroup(group: any) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该分组吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const ok = await deleteAdminUserGroup(id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadGroupList()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
groupList.value = groupList.value.filter(item => item.id !== group.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -267,66 +137,79 @@ function closeModal() {
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
function submitForm() {
|
||||
if (!formData.name) {
|
||||
uni.showToast({ title: '请输入分组名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resId = await saveAdminUserGroup({
|
||||
id: isEdit.value ? formData.id : null,
|
||||
name: formData.name,
|
||||
remark: formData.remark,
|
||||
status: formData.status
|
||||
})
|
||||
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: isEdit.value ? '修改成功' : '添加成功' })
|
||||
closeModal()
|
||||
loadGroupList()
|
||||
if (isEdit.value) {
|
||||
// 模拟修改
|
||||
const index = groupList.value.findIndex(item => item.id === formData.id)
|
||||
if (index > -1) {
|
||||
groupList.value[index].name = formData.name
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} else {
|
||||
// 模拟添加
|
||||
const newId = groupList.value.length > 0 ? Math.max(...groupList.value.map(g => g.id)) + 1 : 1
|
||||
groupList.value.push({
|
||||
id: newId,
|
||||
name: formData.name
|
||||
})
|
||||
uni.showToast({ title: '添加成功', icon: 'success' })
|
||||
}
|
||||
|
||||
closeModal()
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(groupList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-group-page {
|
||||
padding: 16px;
|
||||
background-color: #f0f2f5;
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
padding: var(--admin-card-padding);
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
@@ -420,28 +303,6 @@ async function submitForm() {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.col-remark {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.col-status {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table-loading,
|
||||
.table-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.op-link {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
@@ -459,112 +320,7 @@ async function submitForm() {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Switch 开关 */
|
||||
.switch-box {
|
||||
width: 44px;
|
||||
height: 22px;
|
||||
background-color: #dcdfe6;
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch-box.active {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.switch-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #fff;
|
||||
border-radius: 9px;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.switch-box.active .switch-dot {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* 分页样式 */
|
||||
.pagination-row {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.page-size-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
margin-right: 16px;
|
||||
|
||||
.arrow {
|
||||
font-size: 10px;
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.page-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.page-jump {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.jump-input {
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
margin: 0 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-mask {
|
||||
@@ -652,20 +408,6 @@ async function submitForm() {
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
flex: 1;
|
||||
height: 80px;
|
||||
padding: 4px 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
border-color: #40a9ff;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<template>
|
||||
<view class="user-label-page">
|
||||
<view class="content-card">
|
||||
<!-- 搜索与操作栏 -->
|
||||
<!-- 操作按钮行 -->
|
||||
<view class="action-bar">
|
||||
<view class="filter-item">
|
||||
<input class="filter-input" v-model="searchText" placeholder="搜索标签名称/备注" @confirm="handleSearch" />
|
||||
</view>
|
||||
<button class="btn primary small" @click="onAddLabel">添加标签</button>
|
||||
<button class="btn ghost small" @click="handleSearch">查询</button>
|
||||
<button class="btn ghost small" @click="handleReset">重置</button>
|
||||
</view>
|
||||
|
||||
<!-- 标签列表表格 -->
|
||||
@@ -17,49 +12,40 @@
|
||||
<view class="table-header">
|
||||
<view class="col col-id"><text>ID</text></view>
|
||||
<view class="col col-name"><text>标签名称</text></view>
|
||||
<view class="col col-remark"><text>备注</text></view>
|
||||
<view class="col col-status"><text>状态</text></view>
|
||||
<view class="col col-ops"><text>操作</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<view class="table-body">
|
||||
<view v-if="loading" class="table-loading"><text>加载中...</text></view>
|
||||
<view v-else-if="labelList.length === 0" class="table-empty"><text>暂无数据</text></view>
|
||||
<view v-else v-for="(label, index) in labelList" :key="label.id" class="table-row">
|
||||
<view v-for="label in labelList" :key="label.id" class="table-row">
|
||||
<view class="col col-id"><text>{{ label.id }}</text></view>
|
||||
<view class="col col-name">
|
||||
<text :style="{ color: label.color || '#666' }">{{ label.name }}</text>
|
||||
</view>
|
||||
<view class="col col-remark"><text>{{ label.remark || '-' }}</text></view>
|
||||
<view class="col col-status">
|
||||
<view :class="['switch-box', label.status === 1 ? 'active' : '']" @click="onToggleStatus(index)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="col col-name"><text>{{ label.name }}</text></view>
|
||||
<view class="col col-ops">
|
||||
<text class="op-link" @click="onEditLabel(label)">修改</text>
|
||||
<view class="op-divider">|</view>
|
||||
<text class="op-link text-danger" @click="onDeleteLabel(label.id)">删除</text>
|
||||
<text class="op-link" @click="onDeleteLabel(label)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-row">
|
||||
<text class="total-text">共 {{ total }} 条</text>
|
||||
<view class="page-btns">
|
||||
<view :class="['page-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"><text>‹</text></view>
|
||||
<view class="page-btn active"><text>{{ page }}</text></view>
|
||||
<view :class="['page-btn', page >= totalPages ? 'disabled' : '']" @click="nextPage"><text>›</text></view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text>前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" @confirm="goToJumpPage" />
|
||||
<text>页</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="labelList.length > 0"
|
||||
:total="labelList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 添加/修改标签弹窗 -->
|
||||
@@ -79,36 +65,9 @@
|
||||
class="form-input"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入标签名称"
|
||||
autofocus
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item" style="margin-top: 20px;">
|
||||
<view class="label-box">
|
||||
<text class="label">标签颜色:</text>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="formData.color"
|
||||
placeholder="例如: #1890ff"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item" style="margin-top: 20px;">
|
||||
<view class="label-box">
|
||||
<text class="label">备注说明:</text>
|
||||
</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注说明"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item" style="margin-top: 20px;">
|
||||
<view class="label-box">
|
||||
<text class="label">状态:</text>
|
||||
</view>
|
||||
<view :class="['switch-box', formData.status === 1 ? 'active' : '']" @click="formData.status = (formData.status === 1 ? 0 : 1)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn ghost" @click="closeModal">取消</button>
|
||||
@@ -120,86 +79,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import {
|
||||
fetchAdminUserLabelPage,
|
||||
saveAdminUserLabel,
|
||||
deleteAdminUserLabel,
|
||||
setAdminUserLabelStatus,
|
||||
type AdminUserLabel
|
||||
} from '@/services/admin/userLabelService.uts'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const searchText = ref('')
|
||||
const total = ref(0)
|
||||
const labelList = ref<AdminUserLabel[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const totalPages = computed((): number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
const pages = Math.ceil(total.value / pageSize.value)
|
||||
return pages <= 0 ? 1 : pages
|
||||
})
|
||||
|
||||
const loadLabelList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAdminUserLabelPage(page.value, pageSize.value, {
|
||||
search: searchText.value || null,
|
||||
status: null,
|
||||
includeDeleted: false
|
||||
})
|
||||
labelList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLabelList()
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadLabelList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchText.value = ''
|
||||
page.value = 1
|
||||
loadLabelList()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadLabelList()
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadLabelList()
|
||||
}
|
||||
}
|
||||
|
||||
function goToJumpPage() {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadLabelList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '请输入有效的页码', icon: 'none' })
|
||||
}
|
||||
}
|
||||
// 标签数据
|
||||
const labelList = ref([
|
||||
{ id: 1, name: '新客户' },
|
||||
{ id: 2, name: '老客户' },
|
||||
{ id: 3, name: '活跃客户' },
|
||||
{ id: 4, name: '潜在客户' }
|
||||
])
|
||||
|
||||
// 弹窗状态
|
||||
const showModal = ref(false)
|
||||
@@ -208,138 +97,117 @@ const modalTitle = computed(() => isEdit.value ? '修改标签' : '添加标签'
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
color: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
id: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 添加标签
|
||||
function onAddLabel() {
|
||||
isEdit.value = false
|
||||
formData.id = ''
|
||||
formData.id = 0
|
||||
formData.name = ''
|
||||
formData.color = ''
|
||||
formData.remark = ''
|
||||
formData.status = 1
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onEditLabel(label: AdminUserLabel) {
|
||||
// 修改标签
|
||||
function onEditLabel(label: any) {
|
||||
isEdit.value = true
|
||||
formData.id = label.id
|
||||
formData.name = label.name
|
||||
formData.color = label.color ?? ''
|
||||
formData.remark = label.remark ?? ''
|
||||
formData.status = label.status
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onDeleteLabel(id: string) {
|
||||
// 删除标签
|
||||
function onDeleteLabel(label: any) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该标签吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const ok = await deleteAdminUserLabel(id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadLabelList()
|
||||
} else {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
labelList.value = labelList.value.filter(item => item.id !== label.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onToggleStatus(index: number) {
|
||||
const item = labelList.value[index]
|
||||
const targetStatus = item.status === 1 ? 0 : 1
|
||||
try {
|
||||
const ok = await setAdminUserLabelStatus(item.id, targetStatus)
|
||||
if (ok) {
|
||||
item.status = targetStatus
|
||||
uni.showToast({ title: '状态更新成功' })
|
||||
} else {
|
||||
uni.showToast({ title: '状态更新失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '状态更新失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
// 提交表单
|
||||
function submitForm() {
|
||||
if (!formData.name) {
|
||||
uni.showToast({ title: '请输入标签名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resId = await saveAdminUserLabel({
|
||||
id: isEdit.value ? formData.id : null,
|
||||
name: formData.name,
|
||||
color: formData.color || null,
|
||||
remark: formData.remark || null,
|
||||
status: formData.status
|
||||
})
|
||||
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: isEdit.value ? '修改成功' : '添加成功' })
|
||||
closeModal()
|
||||
loadLabelList()
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
|
||||
if (isEdit.value) {
|
||||
const index = labelList.value.findIndex(item => item.id === formData.id)
|
||||
if (index > -1) {
|
||||
labelList.value[index].name = formData.name
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
} else {
|
||||
const newId = labelList.value.length > 0 ? Math.max(...labelList.value.map(g => g.id)) + 1 : 1
|
||||
labelList.value.push({
|
||||
id: newId,
|
||||
name: formData.name
|
||||
})
|
||||
uni.showToast({ title: '添加成功', icon: 'success' })
|
||||
}
|
||||
|
||||
closeModal()
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(labelList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-label-page {
|
||||
padding: 16px;
|
||||
background-color: #f0f2f5;
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
padding: var(--admin-card-padding);
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
@@ -426,16 +294,6 @@ async function submitForm() {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.col-remark {
|
||||
flex: 1.5;
|
||||
}
|
||||
|
||||
.col-status {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-ops {
|
||||
width: 150px;
|
||||
display: flex;
|
||||
@@ -443,14 +301,6 @@ async function submitForm() {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table-loading,
|
||||
.table-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.op-link {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
@@ -468,99 +318,7 @@ async function submitForm() {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* Switch 开关 */
|
||||
.switch-box {
|
||||
width: 44px;
|
||||
height: 22px;
|
||||
background-color: #dcdfe6;
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch-box.active {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.switch-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #fff;
|
||||
border-radius: 9px;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.switch-box.active .switch-dot {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* 分页样式 */
|
||||
.pagination-row {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.page-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.page-jump {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.jump-input {
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
margin: 0 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-mask {
|
||||
@@ -648,20 +406,6 @@ async function submitForm() {
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
flex: 1;
|
||||
height: 80px;
|
||||
padding: 4px 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
border-color: #40a9ff;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
@@ -73,24 +73,22 @@
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-select">
|
||||
<text class="page-val">15条/页 ▼</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn disabled"><</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn">></text>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" placeholder="1" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -157,13 +155,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import {
|
||||
fetchAdminUserLevelPage,
|
||||
saveAdminUserLevel,
|
||||
deleteAdminUserLevel,
|
||||
setAdminUserLevelVisible
|
||||
} from '@/services/admin/userLevelService.uts'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
interface LevelItem {
|
||||
id: number
|
||||
@@ -178,76 +171,48 @@ interface LevelItem {
|
||||
}
|
||||
|
||||
const filterName = ref('')
|
||||
const total = ref(0)
|
||||
const levelList = ref<AdminUserLevel[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const totalPages = computed((): number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
const pages = Math.ceil(total.value / pageSize.value)
|
||||
return pages <= 0 ? 1 : pages
|
||||
})
|
||||
|
||||
const loadLevelList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAdminUserLevelPage(page.value, pageSize.value, {
|
||||
search: filterName.value || null,
|
||||
isVisible: null, // 全部
|
||||
status: null
|
||||
})
|
||||
|
||||
levelList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLevelList()
|
||||
})
|
||||
|
||||
const handleQuery = () => {
|
||||
page.value = 1
|
||||
loadLevelList()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadLevelList()
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadLevelList()
|
||||
}
|
||||
}
|
||||
|
||||
const goToJumpPage = () => {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadLevelList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '请输入有效的页码', icon: 'none' })
|
||||
}
|
||||
}
|
||||
const total = ref(5)
|
||||
const levelList = ref<LevelItem[]>([
|
||||
{ id: 1, name: 'V1', level: 1, iconBg: '#fdf6ec', iconSymbol: '👑', bgGradient: 'linear-gradient(to bottom right, #f5e6d3, #e8d5bc)', discount: 99, experience: 500, isShow: true },
|
||||
{ id: 2, name: 'V2', level: 2, iconBg: '#ecf5ff', iconSymbol: '💎', bgGradient: 'linear-gradient(to bottom right, #d3e9f5, #bcd9e8)', discount: 97, experience: 1000, isShow: true },
|
||||
{ id: 3, name: 'V3', level: 3, iconBg: '#f4f4f5', iconSymbol: '⭐', bgGradient: 'linear-gradient(to bottom right, #e3e3e3, #cbcbcb)', discount: 95, experience: 3000, isShow: true },
|
||||
{ id: 4, name: 'V4', level: 4, iconBg: '#fef0f0', iconSymbol: '👑', bgGradient: 'linear-gradient(to bottom right, #f5dfd3, #e8c6bc)', discount: 93, experience: 8000, isShow: true },
|
||||
{ id: 5, name: 'V5', level: 5, iconBg: '#f0f9eb', iconSymbol: '💠', bgGradient: 'linear-gradient(to bottom right, #d3e1f5, #bccce8)', discount: 70, experience: 15000, isShow: true }
|
||||
])
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const isClosing = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
level: 1,
|
||||
@@ -256,11 +221,11 @@ const form = reactive({
|
||||
isShow: true
|
||||
})
|
||||
|
||||
const handleQuery = () => { console.log('Querying...') }
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
editingId.value = null
|
||||
form.name = ''
|
||||
form.level = 1
|
||||
form.level = levelList.value.length + 1
|
||||
form.discount = 100
|
||||
form.experience = 0
|
||||
form.isShow = true
|
||||
@@ -268,99 +233,55 @@ const handleAdd = () => {
|
||||
isClosing.value = false
|
||||
}
|
||||
|
||||
const handleEdit = (item: AdminUserLevel) => {
|
||||
const handleEdit = (item: LevelItem) => {
|
||||
isEdit.value = true
|
||||
editingId.value = item.id
|
||||
form.name = item.name
|
||||
form.level = item.level_weight
|
||||
form.discount = item.discount_percent
|
||||
form.experience = item.min_experience
|
||||
form.isShow = item.is_visible
|
||||
form.level = item.level
|
||||
form.discount = item.discount
|
||||
form.experience = item.experience
|
||||
form.isShow = item.isShow
|
||||
showDrawer.value = true
|
||||
isClosing.value = false
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该用户等级吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteAdminUserLevel(id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadLevelList()
|
||||
} else {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const closeDrawer = () => {
|
||||
isClosing.value = true
|
||||
setTimeout(() => {
|
||||
showDrawer.value = false
|
||||
isClosing.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const toggleShow = async (index: number) => {
|
||||
const item = levelList.value[index]
|
||||
const targetVisible = !item.is_visible
|
||||
const ok = await setAdminUserLevelVisible(item.id, targetVisible)
|
||||
if (ok) {
|
||||
item.is_visible = targetVisible
|
||||
} else {
|
||||
uni.showToast({ title: '修改失败', icon: 'none' })
|
||||
}
|
||||
const toggleShow = (index: number) => {
|
||||
levelList.value[index].isShow = !levelList.value[index].isShow
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.name) {
|
||||
uni.showToast({ title: '请输入等级名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resId = await saveAdminUserLevel({
|
||||
id: editingId.value,
|
||||
name: form.name,
|
||||
level_weight: form.level,
|
||||
min_experience: form.experience,
|
||||
discount_percent: form.discount,
|
||||
is_visible: form.isShow,
|
||||
status: 1 // 默认启用
|
||||
})
|
||||
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
closeDrawer()
|
||||
loadLevelList()
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存出错', icon: 'none' })
|
||||
}
|
||||
const handleSave = () => {
|
||||
console.log('Saving...', form)
|
||||
closeDrawer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-user-level {
|
||||
background-color: #f0f2f5;
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.content-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: var(--admin-section-gap);
|
||||
}
|
||||
|
||||
/* 过滤栏 */
|
||||
.filter-card {
|
||||
padding: 24px;
|
||||
padding: var(--admin-card-padding);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -508,34 +429,7 @@ const handleSave = async () => {
|
||||
.op-split { color: #e8eaec; margin: 0 5px; }
|
||||
.text-danger { color: #ed4014; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* Drawer Styles */
|
||||
.drawer-mask {
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<view class="table-body">
|
||||
<view v-for="user in userList" :key="user.id" class="table-row"
|
||||
<view v-for="user in pagedList" :key="user.id" class="table-row"
|
||||
:style="{ zIndex: activeDropdownId === user.id ? 1000 : 1 }"
|
||||
>
|
||||
<view class="col col-check"><checkbox :checked="user.checked" /></view>
|
||||
@@ -138,19 +138,34 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 (模拟) -->
|
||||
<view class="pagination">
|
||||
<text class="page-info">共 80834 条数据</text>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="true"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val: string) => { jumpPageInput.value = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const activeTab = ref(0)
|
||||
// ========== MOCK DATA START ==========
|
||||
const tabs = ['全部', '微信公众号', '微信小程序', 'H5', 'PC', 'APP']
|
||||
const isAllChecked = ref(false)
|
||||
const activeDropdownId = ref<string | null>(null)
|
||||
@@ -164,8 +179,51 @@ const userList = ref([
|
||||
{ id: '75289', avatar: '/static/logo.png', nickname: '小二上酒', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75257', avatar: '/static/logo.png', nickname: '5+7', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75226', avatar: '/static/logo.png', nickname: '慢步前行', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75211', avatar: '/static/logo.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }
|
||||
{ id: '75211', avatar: '/static/logo.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
|
||||
{ id: '75100', avatar: '/static/logo.png', nickname: '山河远阔', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '138****5566', userType: '小程序', balance: '9800.00', checked: false },
|
||||
{ id: '74988', avatar: '/static/logo.png', nickname: '野草菓芙', isMember: '是', level: '金等', group: 'B类客户', spreadLevel: '一级', phone: '155****7788', userType: '公众号', balance: '25600.00', checked: false },
|
||||
{ id: '74833', avatar: '/static/logo.png', nickname: '星花雨月', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '', userType: 'H5', balance: '320.00', checked: false },
|
||||
{ id: '74701', avatar: '/static/logo.png', nickname: '天道酬勤', isMember: '是', level: '银等', group: 'A类客户', spreadLevel: '二级', phone: '186****3344', userType: '小程序', balance: '7700.00', checked: false },
|
||||
{ id: '74590', avatar: '/static/logo.png', nickname: '南风知劲草', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '177****1122', userType: 'APP', balance: '150.00', checked: false },
|
||||
{ id: '74422', avatar: '/static/logo.png', nickname: '大漠孤烟', isMember: '是', level: '金等', group: 'A类客户', spreadLevel: '一级', phone: '139****9900', userType: '公众号', balance: '88800.00', checked: false },
|
||||
{ id: '74310', avatar: '/static/logo.png', nickname: '小桥流水', isMember: '否', level: '无', group: 'B类客户', spreadLevel: '', phone: '', userType: 'PC', balance: '600.00', checked: false },
|
||||
{ id: '74198', avatar: '/static/logo.png', nickname: '日暗香残', isMember: '是', level: '银等', group: '无', spreadLevel: '一级', phone: '135****6677', userType: '小程序', balance: '4300.00', checked: false },
|
||||
{ id: '74056', avatar: '/static/logo.png', nickname: '梦里花开', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '', userType: '公众号', balance: '200.00', checked: false },
|
||||
{ id: '73900', avatar: '/static/logo.png', nickname: '春风十里', isMember: '是', level: '金等', group: 'A类客户', spreadLevel: '二级', phone: '151****4455', userType: 'APP', balance: '56000.00', checked: false }
|
||||
])
|
||||
// ========== MOCK DATA END ==========
|
||||
|
||||
// ========== PAGINATION STATE ==========
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const jumpPageInput = ref('')
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => { const idx = pageSizeOptions.indexOf(pageSize.value); return idx >= 0 ? idx : 0 })
|
||||
const total = computed(() => userList.value.length)
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const pagedList = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return userList.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
const visiblePages = computed((): number[] => {
|
||||
const t = totalPage.value; const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput.value)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
// ========== END PAGINATION STATE ==========
|
||||
|
||||
function onSearch() {
|
||||
uni.showToast({ title: '搜索中...', icon: 'none' })
|
||||
@@ -185,8 +243,14 @@ function onDetail(user: any) {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-page {
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 筛选卡片 */
|
||||
.filter-card {
|
||||
padding: var(--admin-card-padding);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
@@ -507,16 +571,5 @@ function onDetail(user: any) {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<view class="statistic-page">
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-card">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">用户渠道:</text>
|
||||
<view class="admin-page">
|
||||
<view class="admin-sections">
|
||||
<!-- 筛选栏 -->
|
||||
<view class="admin-card filter-card">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">用户渠道:</text>
|
||||
<view class="select-box">
|
||||
<text class="select-text">全部</text>
|
||||
<text class="select-arrow">▼</text>
|
||||
@@ -11,13 +12,11 @@
|
||||
</view>
|
||||
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">选择月份:</text>
|
||||
<AnalyticsDateRangePicker
|
||||
:initialStartDate="startDate"
|
||||
:initialEndDate="endDate"
|
||||
@apply="onDateRangeApply"
|
||||
@clear="onDateRangeClear"
|
||||
/>
|
||||
<text class="filter-label">选择时间:</text>
|
||||
<view class="date-picker-box">
|
||||
<text class="date-icon">📅</text>
|
||||
<text class="date-text">2026/01/04 - 2026/02/02</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-btns">
|
||||
@@ -26,14 +25,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户概况卡片区 -->
|
||||
<view class="section-card">
|
||||
<!-- 用户概况卡片区 (使用 6-2-1 响应式网格) -->
|
||||
<view class="admin-card section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">用户概况</text>
|
||||
<text class="info-icon">ⓘ</text>
|
||||
<text class="info-icon">❓</text>
|
||||
</view>
|
||||
|
||||
<view class="kpi-row">
|
||||
<view class="kpi-grid-6">
|
||||
<view class="kpi-card" v-for="item in kpiData" :key="item.title">
|
||||
<view class="kpi-icon-box" :style="{ backgroundColor: item.bg }">
|
||||
<text class="kpi-icon">{{ item.icon }}</text>
|
||||
@@ -43,7 +42,10 @@
|
||||
<text class="kpi-value">{{ item.value }}</text>
|
||||
<view class="kpi-meta">
|
||||
<text class="meta-label">环比增长:</text>
|
||||
<text class="meta-value" :class="item.trend">{{ item.percent }} {{ item.trend === 'up' ? '▲' : '▼' }}</text>
|
||||
<text class="meta-value" :class="item.trend">
|
||||
{{ item.percent }}
|
||||
<text class="trend-icon">{{ item.trend === 'up' ? '▲' : '▼' }}</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -65,227 +67,66 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地域分布与性别比例 -->
|
||||
<view class="analysis-row">
|
||||
<view class="map-col">
|
||||
<AnalyticsUserMapTable />
|
||||
</view>
|
||||
<view class="gender-col">
|
||||
<AnalyticsUserGenderSection :startDate="startDate" :endDate="endDate" />
|
||||
<!-- 地域分布与性别比例 -->
|
||||
<view class="analysis-row">
|
||||
<view class="map-col">
|
||||
<AnalyticsUserMapTable />
|
||||
</view>
|
||||
<view class="gender-col">
|
||||
<AnalyticsUserGenderSection />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AnalyticsMultiLineChart from '@/components/analytics/AnalyticsMultiLineChart.uvue'
|
||||
import AnalyticsUserMapTable from '@/components/analytics/AnalyticsUserMapTable.uvue'
|
||||
import AnalyticsUserGenderSection from '@/components/analytics/AnalyticsUserGenderSection.uvue'
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||||
import { ensureAnalyticsLogin } from '@/services/analytics/authGuard.uts'
|
||||
import { getCurrentUser } from '@/utils/store.uts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
type KpiCard = {
|
||||
title: string
|
||||
value: string
|
||||
percent: string
|
||||
trend: string
|
||||
icon: string
|
||||
bg: string
|
||||
}
|
||||
const kpiData = [
|
||||
{ title: '累计用户', value: '80887', percent: '0.79%', trend: 'up', icon: '👥', bg: '#efebff' },
|
||||
{ title: '访客数', value: '1076', percent: '11.65%', trend: 'down', icon: '👤', bg: '#e8f4ff' },
|
||||
{ title: '浏览量', value: '8843', percent: '12.09%', trend: 'down', icon: '📁', bg: '#e6fff1' },
|
||||
{ title: '新增用户数', value: '635', percent: '14.65%', trend: 'down', icon: '👤', bg: '#fff7e6' },
|
||||
{ title: '成交用户数', value: '122', percent: '0.81%', trend: 'down', icon: '👥', bg: '#f2f0ff' },
|
||||
{ title: '付费会员数', value: '76', percent: '13.63%', trend: 'down', icon: '💎', bg: '#f2f0ff' }
|
||||
]
|
||||
|
||||
type LineSeries = {
|
||||
name: string
|
||||
color: string
|
||||
data: Array<number>
|
||||
}
|
||||
|
||||
const selectedMonth = ref<string>('')
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
|
||||
const dateRangeText = computed((): string => {
|
||||
if (!startDate.value || !endDate.value) return '请选择月份'
|
||||
const fmt = (s: string): string => s.replace(/-/g, '/')
|
||||
return `${fmt(startDate.value)} - ${fmt(endDate.value)}`
|
||||
})
|
||||
|
||||
const kpiData = ref<Array<KpiCard>>([
|
||||
{ title: '累计用户', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#f3e8ff' },
|
||||
{ title: '活跃用户', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#e0f2fe' },
|
||||
{ title: '新增用户数', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#ffedd5' },
|
||||
{ title: '下单用户数', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#dcfce7' },
|
||||
{ title: '支付用户数', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#f3e8ff' },
|
||||
{ title: '复购率', value: '-', percent: '-', trend: 'up', icon: '💎', bg: '#f3e8ff' }
|
||||
])
|
||||
|
||||
const chartData = ref<{ x: Array<string>; series: Array<LineSeries> }>({
|
||||
x: [],
|
||||
const chartData = {
|
||||
x: ['01-04', '01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13', '01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23', '01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02'],
|
||||
series: [
|
||||
{ name: '新增用户数', color: '#1890ff', data: [] },
|
||||
{ name: '活跃用户', color: '#52c41a', data: [] }
|
||||
{ name: '新增用户数', color: '#1890ff', data: [40, 30, 25, 30, 22, 10, 20, 32, 28, 15, 8, 12, 18, 22, 15, 12, 25, 30, 28, 25, 35, 20, 18, 22, 20, 15, 10, 8, 15, 38] },
|
||||
{ name: '访客数', color: '#52c41a', data: [70, 75, 65, 55, 65, 50, 45, 35, 50, 68, 72, 65, 50, 48, 55, 65, 75, 62, 58, 85, 70, 55, 48, 58, 65, 72, 68, 60, 45, 50] },
|
||||
{ name: '浏览量', color: '#fa8c16', data: [520, 500, 420, 280, 580, 180, 220, 100, 180, 450, 500, 400, 320, 340, 150, 280, 450, 320, 440, 460, 320, 260, 320, 280, 380, 400, 320, 330, 250, 300] },
|
||||
{ name: '成交用户数', color: '#722ed1', data: [15, 12, 10, 8, 18, 5, 8, 4, 6, 12, 15, 10, 8, 9, 4, 10, 12, 8, 10, 12, 8, 6, 10, 8, 12, 14, 10, 8, 5, 8] },
|
||||
{ name: '新增付费用户数', color: '#f5222d', data: [5, 4, 3, 2, 6, 1, 2, 1, 2, 4, 5, 3, 2, 3, 1, 3, 4, 2, 3, 4, 2, 2, 3, 2, 4, 5, 3, 2, 1, 3] }
|
||||
]
|
||||
})
|
||||
|
||||
function safeNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
function formatInt(v: any): string {
|
||||
const n = Math.round(safeNumber(v))
|
||||
return n.toString()
|
||||
}
|
||||
|
||||
function formatPct(v: any): string {
|
||||
const n = safeNumber(v)
|
||||
const s = n.toFixed(2) + '%'
|
||||
return s
|
||||
}
|
||||
|
||||
function trendOf(pct: number): string {
|
||||
return pct >= 0 ? 'up' : 'down'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
await ensureSupabaseReady()
|
||||
|
||||
if (!startDate.value || !endDate.value) {
|
||||
uni.showToast({ title: '请先选择月份', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const startStr = startDate.value
|
||||
const endStr = endDate.value
|
||||
|
||||
const p = new UTSJSONObject()
|
||||
p.set('p_start_date', startStr)
|
||||
p.set('p_end_date', endStr)
|
||||
|
||||
const kRes: any = await supa.rpc('rpc_analytics_user_kpis', p)
|
||||
const row = Array.isArray(kRes.data) && kRes.data.length > 0 ? kRes.data[0] : (kRes.data || {})
|
||||
|
||||
const totalUsers = safeNumber(row.total_users)
|
||||
const totalGrowth = safeNumber(row.user_growth)
|
||||
const newUsers = safeNumber(row.new_users)
|
||||
const newGrowth = safeNumber(row.new_user_growth)
|
||||
const activeUsers = safeNumber(row.active_users)
|
||||
const activeGrowth = safeNumber(row.active_growth)
|
||||
const orderingUsers = safeNumber(row.ordering_users)
|
||||
const orderingGrowth = safeNumber(row.ordering_growth)
|
||||
const paidUsers = safeNumber(row.paid_users)
|
||||
const paidGrowth = safeNumber(row.paid_growth)
|
||||
const repurchaseRate = safeNumber(row.repurchase_rate)
|
||||
const repurchaseGrowth = safeNumber(row.repurchase_growth)
|
||||
|
||||
kpiData.value = [
|
||||
{ title: '累计用户', value: formatInt(totalUsers), percent: formatPct(totalGrowth), trend: trendOf(totalGrowth), icon: '👤', bg: '#f3e8ff' },
|
||||
{ title: '活跃用户', value: formatInt(activeUsers), percent: formatPct(activeGrowth), trend: trendOf(activeGrowth), icon: '👤', bg: '#e0f2fe' },
|
||||
{ title: '新增用户数', value: formatInt(newUsers), percent: formatPct(newGrowth), trend: trendOf(newGrowth), icon: '👤', bg: '#ffedd5' },
|
||||
{ title: '下单用户数', value: formatInt(orderingUsers), percent: formatPct(orderingGrowth), trend: trendOf(orderingGrowth), icon: '👤', bg: '#dcfce7' },
|
||||
{ title: '支付用户数', value: formatInt(paidUsers), percent: formatPct(paidGrowth), trend: trendOf(paidGrowth), icon: '👤', bg: '#f3e8ff' },
|
||||
{ title: '复购率', value: formatPct(repurchaseRate), percent: formatPct(repurchaseGrowth), trend: trendOf(repurchaseGrowth), icon: '💎', bg: '#f3e8ff' }
|
||||
]
|
||||
|
||||
const tRes: any = await supa.rpc('rpc_analytics_user_growth_trend', p)
|
||||
const rows: Array<any> = Array.isArray(tRes.data) ? (tRes.data as Array<any>) : []
|
||||
|
||||
const x: Array<string> = []
|
||||
const newArr: Array<number> = []
|
||||
const activeArr: Array<number> = []
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const d = `${rows[i].date}`
|
||||
x.push(d.slice(5))
|
||||
newArr.push(Number(rows[i].new_users) || 0)
|
||||
activeArr.push(Number(rows[i].active_users) || 0)
|
||||
}
|
||||
|
||||
chartData.value = {
|
||||
x,
|
||||
series: [
|
||||
{ name: '新增用户数', color: '#1890ff', data: newArr },
|
||||
{ name: '活跃用户', color: '#52c41a', data: activeArr }
|
||||
]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('admin user statistic loadData failed', e)
|
||||
uni.showToast({ title: '数据加载失败', icon: 'none', duration: 2000 })
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
loadData()
|
||||
uni.showToast({ title: '搜索中...' })
|
||||
}
|
||||
|
||||
function onExport() {
|
||||
uni.showToast({ title: '导出功能待接入', icon: 'none' })
|
||||
uni.showToast({ title: '导出中...' })
|
||||
}
|
||||
|
||||
function onDateRangeApply(range: { start: string; end: string }) {
|
||||
startDate.value = range.start
|
||||
endDate.value = range.end
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onDateRangeClear() {
|
||||
startDate.value = ''
|
||||
endDate.value = ''
|
||||
kpiData.value.forEach(item => {
|
||||
item.value = '-'
|
||||
item.percent = '-'
|
||||
})
|
||||
chartData.value.x = []
|
||||
chartData.value.series.forEach(s => s.data = [])
|
||||
}
|
||||
|
||||
// 初始化默认月份为当前月
|
||||
function initCurrentMonth() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = now.getMonth() + 1
|
||||
const firstDay = new Date(year, month - 1, 1)
|
||||
const lastDay = new Date(year, month, 0)
|
||||
startDate.value = firstDay.toISOString().slice(0, 10)
|
||||
endDate.value = lastDay.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
if (!ensureAnalyticsLogin({ toastTitle: '请先登录以访问用户统计' })) return
|
||||
|
||||
const profile = await getCurrentUser()
|
||||
const role = profile?.role
|
||||
if (!role || !['admin', 'analytics'].includes(role)) {
|
||||
uni.showToast({ title: '权限不足', icon: 'none' })
|
||||
setTimeout(() => uni.switchTab({ url: '/pages/mall/consumer/index' }), 800)
|
||||
return
|
||||
}
|
||||
|
||||
initCurrentMonth()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.statistic-page {
|
||||
padding: 16px;
|
||||
background-color: #f0f2f5;
|
||||
.admin-page {
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
margin-bottom: 16px;
|
||||
padding: var(--admin-card-padding);
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
@@ -357,116 +198,77 @@ onLoad(async () => {
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
margin-left: 6px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.kpi-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.kpi-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.kpi-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* kpi-row 已废弃,采用全局 kpi-grid-6 */
|
||||
.kpi-card {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px;
|
||||
padding: 16px 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kpi-icon-box {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kpi-icon { font-size: 14px; }
|
||||
.kpi-icon { font-size: 20px; }
|
||||
|
||||
.kpi-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.kpi-label { font-size: 13px; color: #666; margin-bottom: 4px; }
|
||||
.kpi-value { font-size: 24px; font-weight: 700; color: #333; line-height: 1.2; margin-bottom: 4px; }
|
||||
|
||||
.kpi-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #8c8c8c;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.meta-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.meta-value.up { color: #ff4d4f; }
|
||||
.meta-label { font-size: 12px; color: #999; }
|
||||
.meta-value { font-size: 12px; font-weight: 500; }
|
||||
.meta-value.up { color: #f5222d; }
|
||||
.meta-value.down { color: #52c41a; }
|
||||
|
||||
.trend-icon {
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 24px;
|
||||
@@ -486,18 +288,39 @@ onLoad(async () => {
|
||||
}
|
||||
|
||||
.analysis-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: var(--admin-section-gap);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map-col {
|
||||
flex: 7;
|
||||
.map-col, .gender-col {
|
||||
min-width: 0;
|
||||
display: flex; /* 让子元素可以正常撑开 */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gender-col {
|
||||
flex: 3;
|
||||
@media (max-width: 1199.98px) {
|
||||
.analysis-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.filter-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select-box, .date-picker-box {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* @media (max-width: 1199.98px) 已将 analysis-row 移出,与 filter-card 逻辑解耦 */
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user