页面初始化

This commit is contained in:
qsh
2026-01-29 18:29:34 +08:00
parent db42252b6c
commit 35f5621d7e
30 changed files with 13297 additions and 813 deletions

View File

@@ -108,6 +108,96 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/distribution/data",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/distribution/profit-rule",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/distribution/qrcode",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/account/manage",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/account/quota",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/account/log",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/account/personal",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/student/list",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/student/analysis",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/student/remind",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/member/order",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/member/gift",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/member/general-code",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/member/personal-order",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/member/personal-gift",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/mine/school-info",
"style": {
@@ -167,6 +257,30 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/stats/distribution",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/stats/member",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/stats/student",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/student/detail",
"style": {
"navigationStyle": "custom"
}
}
],
"tabBar": {

490
src/pages/account/log.vue Normal file
View File

@@ -0,0 +1,490 @@
<template>
<view class="log-manage-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">操作日志查看</view>
<view class="header-right" @click="exportLog">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">操作类型</view>
<view class="filter-control">
<picker
:range="operationOptions"
:value="operationIndex"
@change="onOperationChange"
class="picker"
>
<view class="picker-text">{{ operationOptions[operationIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
mode="date"
start="2026-01-01"
end="2026-12-31"
:value="dateRange"
@change="onDateChange"
class="picker"
>
<view class="picker-text">{{ dateRangeDisplay }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入操作人或账号"
@input="onSearch"
/>
</view>
</view>
</view>
</view>
<!-- 日志列表 -->
<view class="log-list">
<view
v-for="(log, index) in logList"
:key="index"
class="log-item"
>
<view class="log-header">
<view class="log-time">{{ log.time }}</view>
<view class="log-status" :class="log.statusClass">{{ log.status }}</view>
</view>
<view class="log-content">
<view class="log-operation">{{ log.operation }}</view>
<view class="log-detail">{{ log.detail }}</view>
</view>
<view class="log-meta">
<view class="log-operator">{{ log.operator }}</view>
<view class="log-ip">{{ log.ip }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="logList.length === 0" class="empty-state">
<view class="empty-icon">📋</view>
<view class="empty-text">暂无操作日志</view>
</view>
<!-- 分页 -->
<view v-if="logList.length > 0" class="pagination">
<view class="page-info">
{{ totalLogs }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 操作类型选项
const operationOptions = ['全部', '账号创建', '账号冻结', '账号启用', '密码修改', '权限变更']
const operationIndex = ref(0)
// 日期范围
const dateRange = ref('2026-01-29')
const dateRangeDisplay = ref('2026-01-29')
// 搜索关键词
const searchKeyword = ref('')
// 日志列表
const logList = ref([
{
id: 1,
time: '2026-01-29 15:30:45',
operation: '账号创建',
detail: '创建了新的分销员账号:张教练',
operator: '系统管理员',
ip: '192.168.1.100',
status: '成功',
statusClass: 'status-success'
},
{
id: 2,
time: '2026-01-29 14:20:30',
operation: '账号冻结',
detail: '冻结了分销员账号:王教练',
operator: '系统管理员',
ip: '192.168.1.100',
status: '成功',
statusClass: 'status-success'
},
{
id: 3,
time: '2026-01-29 10:15:20',
operation: '密码修改',
detail: '修改了账号密码:李教练',
operator: '李教练',
ip: '192.168.1.101',
status: '成功',
statusClass: 'status-success'
},
{
id: 4,
time: '2026-01-28 16:45:10',
operation: '权限变更',
detail: '修改了分销员权限:张教练',
operator: '系统管理员',
ip: '192.168.1.100',
status: '成功',
statusClass: 'status-success'
},
{
id: 5,
time: '2026-01-28 09:30:00',
operation: '账号创建',
detail: '创建了新的分销员账号:刘教练',
operator: '系统管理员',
ip: '192.168.1.100',
status: '成功',
statusClass: 'status-success'
}
])
// 分页信息
const currentPage = ref(1)
const totalLogs = ref(100)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalLogs.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取操作日志
// loadLogList()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出日志
function exportLog() {
uni.showModal({
title: '导出日志',
content: '确定要导出当前筛选条件的操作日志吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 实际项目中应调用接口导出日志
// exportLogData()
}, 1500)
}
}
})
}
// 操作类型变更
function onOperationChange(e) {
const value = e.detail.value
operationIndex.value = value
// 实际项目中应根据操作类型筛选日志
// filterLogList()
}
// 日期变更
function onDateChange(e) {
dateRange.value = e.detail.value
dateRangeDisplay.value = e.detail.value
// 实际项目中应根据日期筛选日志
// filterLogList()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索日志
// searchLogList()
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的日志
// loadLogList(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.log-manage-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 200rpx;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
/* 日志列表 */
.log-list {
flex: 1;
padding: 0 16rpx;
}
.log-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.log-time {
font-size: 20rpx;
color: #606266;
}
.log-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-success {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.status-fail {
color: #f56c6c;
background-color: rgba(245, 108, 108, 0.1);
}
.log-content {
margin-bottom: 16rpx;
}
.log-operation {
font-size: 24rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.log-detail {
font-size: 20rpx;
color: #606266;
line-height: 1.4;
}
.log-meta {
display: flex;
justify-content: space-between;
font-size: 20rpx;
color: #909399;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,390 @@
<template>
<view class="account-manage-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">账号创建与管控</view>
<view class="header-right" @click="addAccount">
<view class="add-btn">+ 添加</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">账号状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入账号名称或手机号"
@input="onSearch"
/>
</view>
</view>
</view>
</view>
<!-- 账号列表 -->
<view class="account-list">
<view
v-for="(account, index) in accountList"
:key="index"
class="account-item"
>
<view class="account-info">
<view class="account-name">{{ account.name }}</view>
<view class="account-meta">
<view class="meta-item">{{ account.phone }}</view>
<view class="meta-item">{{ account.role }}</view>
<view class="meta-item status-{{ account.status }}">{{ account.statusText }}</view>
</view>
</view>
<view class="account-actions">
<view class="action-btn edit-btn" @click="editAccount(account.id)">
编辑
</view>
<view
class="action-btn"
:class="account.status === 'active' ? 'disable-btn' : 'enable-btn'"
@click="toggleAccountStatus(account.id, account.status)"
>
{{ account.status === 'active' ? '冻结' : '启用' }}
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="accountList.length === 0" class="empty-state">
<view class="empty-icon">👥</view>
<view class="empty-text">暂无账号数据</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 状态选项
const statusOptions = ['全部', '启用', '冻结']
const statusIndex = ref(0)
// 搜索关键词
const searchKeyword = ref('')
// 账号列表
const accountList = ref([
{
id: 1,
name: '张教练',
phone: '138****1234',
role: '分销员',
status: 'active',
statusText: '启用'
},
{
id: 2,
name: '李教练',
phone: '139****5678',
role: '分销员',
status: 'active',
statusText: '启用'
},
{
id: 3,
name: '王教练',
phone: '137****9012',
role: '分销员',
status: 'inactive',
statusText: '冻结'
}
])
onMounted(() => {
// 实际项目中应从接口获取账号列表
// loadAccountList()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 添加账号
function addAccount() {
uni.navigateTo({
url: '/pages/account/add'
})
}
// 编辑账号
function editAccount(accountId) {
uni.navigateTo({
url: `/pages/account/edit?id=${accountId}`
})
}
// 切换账号状态
function toggleAccountStatus(accountId, currentStatus) {
const newStatus = currentStatus === 'active' ? 'inactive' : 'active'
const newStatusText = newStatus === 'active' ? '启用' : '冻结'
uni.showModal({
title: '确认操作',
content: `确定要${newStatusText}该账号吗?`,
success: function(res) {
if (res.confirm) {
// 实际项目中应调用接口切换账号状态
const account = accountList.value.find(item => item.id === accountId)
if (account) {
account.status = newStatus
account.statusText = newStatusText
}
uni.showToast({
title: `账号已${newStatusText}`,
icon: 'success'
})
}
}
})
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选账号列表
// filterAccountList()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索账号列表
// searchAccountList()
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.account-manage-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 100rpx;
text-align: right;
}
.add-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
/* 账号列表 */
.account-list {
flex: 1;
padding: 0 16rpx;
}
.account-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.account-info {
flex: 1;
margin-right: 24rpx;
}
.account-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.account-meta {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.meta-item {
font-size: 20rpx;
color: #606266;
}
.status-active {
color: #67c23a;
}
.status-inactive {
color: #909399;
}
.account-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
cursor: pointer;
}
.edit-btn {
background-color: #ecf5ff;
color: #409eff;
}
.disable-btn {
background-color: #fef0f0;
color: #f56c6c;
}
.enable-btn {
background-color: #f0f9eb;
color: #67c23a;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
</style>

View File

@@ -0,0 +1,410 @@
<template>
<view class="personal-account-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">个人账号管理</view>
<view class="header-right" @click="saveChanges">
<view class="save-btn">保存</view>
</view>
</view>
<!-- 个人信息 -->
<view class="info-section">
<view class="section-title">个人信息</view>
<view class="info-card">
<view class="info-row">
<view class="info-item">
<view class="info-label">姓名</view>
<view class="info-control">
<input
v-model="userInfo.name"
class="info-input"
placeholder="请输入姓名"
maxlength="20"
/>
</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">手机号码</view>
<view class="info-control">
<input
v-model="userInfo.phone"
class="info-input"
placeholder="请输入手机号码"
maxlength="11"
type="number"
/>
</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">邮箱</view>
<view class="info-control">
<input
v-model="userInfo.email"
class="info-input"
placeholder="请输入邮箱"
maxlength="50"
type="email"
/>
</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">性别</view>
<view class="info-control">
<picker
:range="genders"
:value="genderIndex"
@change="onGenderChange"
class="picker"
>
<view class="picker-text">{{ userInfo.gender || '请选择性别' }}</view>
</picker>
</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">所属驾校</view>
<view class="info-control">
<view class="info-text">{{ userInfo.school || '顺达驾校' }}</view>
</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">职位</view>
<view class="info-control">
<input
v-model="userInfo.position"
class="info-input"
placeholder="请输入职位"
maxlength="20"
/>
</view>
</view>
</view>
</view>
</view>
<!-- 账号安全 -->
<view class="security-section">
<view class="section-title">账号安全</view>
<view class="security-card">
<view class="security-item" @click="goToPasswordChange">
<view class="security-label">修改登录密码</view>
<view class="security-arrow"></view>
</view>
<view class="security-item" @click="bindPhone">
<view class="security-label">绑定手机号</view>
<view class="security-status">{{ userInfo.phone ? '已绑定' : '未绑定' }}</view>
<view class="security-arrow"></view>
</view>
<view class="security-item" @click="bindEmail">
<view class="security-label">绑定邮箱</view>
<view class="security-status">{{ userInfo.email ? '已绑定' : '未绑定' }}</view>
<view class="security-arrow"></view>
</view>
</view>
</view>
<!-- 账号信息 -->
<view class="account-info-section">
<view class="section-title">账号信息</view>
<view class="info-card">
<view class="info-row">
<view class="info-item">
<view class="info-label">账号ID</view>
<view class="info-value">{{ userInfo.id || '10001' }}</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">账号类型</view>
<view class="info-value">{{ userInfo.role || '分销员' }}</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">注册时间</view>
<view class="info-value">{{ userInfo.registerTime || '2026-01-15' }}</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">最后登录</view>
<view class="info-value">{{ userInfo.lastLogin || '2026-01-29 10:30:00' }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 性别选项
const genders = ['男', '女', '保密']
// 用户信息
const userInfo = ref({
id: '10001',
name: '张教练',
phone: '13800138000',
email: 'zhang@shunda.com',
gender: '男',
school: '顺达驾校',
position: '科目二教练',
role: '分销员',
registerTime: '2026-01-15',
lastLogin: '2026-01-29 10:30:00'
})
// 计算性别索引
const genderIndex = computed(() => {
return genders.indexOf(userInfo.value.gender)
})
onMounted(() => {
// 实际项目中应从接口获取用户信息
// loadUserInfo()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 保存修改
function saveChanges() {
uni.showModal({
title: '保存修改',
content: '确定要保存个人信息修改吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
// 实际项目中应调用接口保存用户信息
// saveUserInfo()
}, 1500)
}
}
})
}
// 性别变更
function onGenderChange(e) {
const index = e.detail.value
userInfo.value.gender = genders[index]
}
// 跳转到修改密码页面
function goToPasswordChange() {
uni.navigateTo({
url: '/pages/mine/password-change'
})
}
// 绑定手机号
function bindPhone() {
uni.showToast({
title: '绑定手机号功能开发中',
icon: 'none'
})
}
// 绑定邮箱
function bindEmail() {
uni.showToast({
title: '绑定邮箱功能开发中',
icon: 'none'
})
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.personal-account-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.save-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.info-section,
.security-section,
.account-info-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 个人信息 */
.info-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.info-row {
display: flex;
}
.info-item {
flex: 1;
}
.info-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.info-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.info-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
.info-text {
font-size: 24rpx;
color: #303133;
}
.info-value {
font-size: 24rpx;
color: #303133;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 账号安全 */
.security-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.security-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
cursor: pointer;
}
.security-label {
flex: 1;
font-size: 24rpx;
color: #303133;
}
.security-status {
font-size: 20rpx;
color: #67c23a;
margin-right: 16rpx;
}
.security-arrow {
font-size: 24rpx;
color: #909399;
}
</style>

454
src/pages/account/quota.vue Normal file
View File

@@ -0,0 +1,454 @@
<template>
<view class="quota-manage-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">赠会员额度分配</view>
<view class="header-right" @click="saveQuota">
<view class="save-btn">保存</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">选择月份</view>
<view class="filter-control">
<picker
mode="date"
fields="month"
:value="currentMonth"
start="2026-01"
end="2026-12"
@change="onMonthChange"
class="picker"
>
<view class="picker-text">{{ currentMonthDisplay }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 总额度设置 -->
<view class="total-quota-section">
<view class="section-title">总额度设置</view>
<view class="quota-card">
<view class="quota-item">
<view class="quota-label">月度赠会员总额度</view>
<view class="quota-control">
<input
v-model="totalQuota"
type="number"
class="quota-input"
placeholder="请输入总额度"
min="0"
max="1000"
/>
<view class="quota-unit"></view>
</view>
</view>
<view class="quota-hint">
总额度将分配给所有分销员
单个分销员最高可分配额度为总额度的50%
</view>
</view>
</view>
<!-- 分销员额度分配 -->
<view class="distributor-quota-section">
<view class="section-title">分销员额度分配</view>
<view class="quota-list">
<view
v-for="(distributor, index) in distributors"
:key="distributor.id"
class="quota-item"
>
<view class="distributor-info">
<view class="distributor-name">{{ distributor.name }}</view>
<view class="distributor-id">ID: {{ distributor.id }}</view>
</view>
<view class="quota-control">
<input
v-model="distributor.quota"
type="number"
class="quota-input"
placeholder="0"
min="0"
:max="maxDistributorQuota"
/>
<view class="quota-unit"></view>
</view>
</view>
</view>
</view>
<!-- 配额统计 -->
<view class="quota-stats">
<view class="stats-item">
<view class="stats-label">已分配额度</view>
<view class="stats-value">{{ allocatedQuota }} </view>
</view>
<view class="stats-item">
<view class="stats-label">剩余额度</view>
<view class="stats-value" :class="{ 'warning': remainingQuota < 0 }">
{{ remainingQuota }}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 当前月份
const currentMonth = ref('2026-01')
const currentMonthDisplay = ref('2026年01月')
// 总额度
const totalQuota = ref(100)
// 分销员列表
const distributors = ref([
{
id: 1,
name: '张分销',
quota: 30
},
{
id: 2,
name: '李分销',
quota: 25
},
{
id: 3,
name: '王分销',
quota: 20
},
{
id: 4,
name: '刘分销',
quota: 15
}
])
// 计算单个分销员最大可分配额度
const maxDistributorQuota = computed(() => {
return Math.floor(totalQuota.value * 0.5)
})
// 计算已分配额度
const allocatedQuota = computed(() => {
return distributors.value.reduce((sum, distributor) => {
return sum + parseInt(distributor.quota || 0)
}, 0)
})
// 计算剩余额度
const remainingQuota = computed(() => {
return totalQuota.value - allocatedQuota.value
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 月份变更
function onMonthChange(e) {
currentMonth.value = e.detail.value
const date = new Date(e.detail.value)
currentMonthDisplay.value = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`
// 实际项目中应根据选择的月份加载对应的额度配置
// loadQuotaByMonth(currentMonth.value)
}
// 保存配额
function saveQuota() {
if (!totalQuota.value || totalQuota.value <= 0) {
uni.showToast({
title: '请设置有效的总额度',
icon: 'none'
})
return
}
if (remainingQuota.value < 0) {
uni.showToast({
title: '已分配额度超过总额度',
icon: 'none'
})
return
}
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
// 实际项目中应调用接口保存配额配置
// saveQuotaConfig()
}, 1500)
}
onMounted(() => {
// 实际项目中应从接口获取分销员列表和当前月份的配额配置
// loadDistributors()
// loadCurrentMonthQuota()
})
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.quota-manage-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.save-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 通用部分样式 */
.total-quota-section,
.distributor-quota-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 总额度设置 */
.quota-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.quota-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.quota-label {
font-size: 24rpx;
color: #606266;
}
.quota-control {
display: flex;
align-items: center;
gap: 8rpx;
}
.quota-input {
width: 120rpx;
height: 60rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 0 16rpx;
text-align: center;
font-size: 24rpx;
}
.quota-unit {
font-size: 24rpx;
color: #606266;
}
.quota-hint {
font-size: 20rpx;
color: #909399;
margin-top: 16rpx;
line-height: 1.4;
}
/* 分销员额度分配 */
.quota-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.distributor-info {
flex: 1;
margin-right: 24rpx;
}
.distributor-name {
font-size: 24rpx;
font-weight: 600;
color: #303133;
margin-bottom: 4rpx;
}
.distributor-id {
font-size: 20rpx;
color: #909399;
}
/* 配额统计 */
.quota-stats {
display: flex;
justify-content: space-around;
padding: 24rpx;
background-color: #fff;
margin: 0 16rpx 32rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.stats-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.stats-label {
font-size: 24rpx;
color: #606266;
}
.stats-value {
font-size: 28rpx;
font-weight: bold;
color: #303133;
}
.stats-value.warning {
color: #f56c6c;
}
/* 响应式设计 */
@media screen and (min-width: 500px) {
.quota-manage-container {
max-width: 900px;
margin: 0 auto;
}
.quota-input {
width: 160rpx;
height: 72rpx;
font-size: 28rpx;
}
.distributor-name {
font-size: 28rpx;
}
}
</style>

View File

@@ -0,0 +1,672 @@
<template>
<view class="distribution-data-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">分销数据查看</view>
<view class="header-right" @click="exportData">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
:range="dateOptions"
:value="dateIndex"
@change="onDateChange"
class="picker"
>
<view class="picker-text">{{ dateOptions[dateIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">分销员</view>
<view class="filter-control">
<picker
:range="distributorOptions"
:value="distributorIndex"
@change="onDistributorChange"
class="picker"
>
<view class="picker-text">{{ distributorOptions[distributorIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入推广码或学员姓名"
@input="onSearch"
/>
</view>
</view>
</view>
</view>
<!-- 核心指标 -->
<view class="metrics-section">
<view class="section-title">核心指标</view>
<view class="metrics-grid">
<view class="metric-card">
<view class="metric-icon">📱</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.scanCount }}</view>
<view class="metric-label">扫码数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">👥</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.registerCount }}</view>
<view class="metric-label">注册数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">🎓</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.memberCount }}</view>
<view class="metric-label">会员数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">💰</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.totalProfit }}</view>
<view class="metric-label">分润总额</view>
</view>
</view>
</view>
</view>
<!-- 数据趋势 -->
<view class="trend-section">
<view class="section-title">数据趋势</view>
<view class="chart-container">
<!-- 模拟图表实际项目中应使用图表库 -->
<view class="mock-chart">
<view class="chart-header">
<view class="chart-title">近7天数据趋势</view>
</view>
<view class="chart-body">
<view class="chart-bars">
<view v-for="(day, index) in trendData" :key="index" class="chart-bar">
<view class="bar-container">
<view class="bar" :style="{ height: day.value + '%' }"></view>
</view>
<view class="bar-label">{{ day.date }}</view>
<view class="bar-value">{{ day.count }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 推广明细 -->
<view class="detail-section">
<view class="section-title">推广明细</view>
<view class="detail-list">
<view
v-for="(item, index) in promotionList"
:key="index"
class="detail-item"
>
<view class="detail-header">
<view class="detail-time">{{ item.time }}</view>
<view class="detail-status" :class="item.statusClass">{{ item.status }}</view>
</view>
<view class="detail-content">
<view class="detail-info">
<view class="info-item">
<view class="info-label">推广码</view>
<view class="info-value">{{ item.promotionCode }}</view>
</view>
<view class="info-item">
<view class="info-label">学员</view>
<view class="info-value">{{ item.studentName }}</view>
</view>
<view class="info-item">
<view class="info-label">分销员</view>
<view class="info-value">{{ item.distributorName }}</view>
</view>
</view>
<view class="detail-amount">
<view class="amount-label">分润金额</view>
<view class="amount-value">{{ item.profitAmount }}</view>
</view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination">
<view class="page-info">
{{ totalItems }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 时间范围选项
const dateOptions = ['近7天', '近30天', '近90天', '今年', '全部']
const dateIndex = ref(0)
// 分销员选项
const distributorOptions = ['全部分销员', '张分销', '李分销', '王分销', '刘分销']
const distributorIndex = ref(0)
// 搜索关键词
const searchKeyword = ref('')
// 核心指标
const metrics = ref({
scanCount: 1234,
registerCount: 890,
memberCount: 456,
totalProfit: '¥12,345.67'
})
// 趋势数据
const trendData = ref([
{ date: '1/23', count: 120, value: 60 },
{ date: '1/24', count: 150, value: 75 },
{ date: '1/25', count: 100, value: 50 },
{ date: '1/26', count: 200, value: 100 },
{ date: '1/27', count: 180, value: 90 },
{ date: '1/28', count: 160, value: 80 },
{ date: '1/29', count: 190, value: 95 }
])
// 推广明细
const promotionList = ref([
{
id: 1,
time: '2026-01-29 15:30:45',
promotionCode: 'SD20260129001',
studentName: '张**',
distributorName: '张分销',
profitAmount: '¥128.00',
status: '已完成',
statusClass: 'status-success'
},
{
id: 2,
time: '2026-01-29 14:20:30',
promotionCode: 'SD20260129002',
studentName: '李**',
distributorName: '李分销',
profitAmount: '¥99.00',
status: '已完成',
statusClass: 'status-success'
},
{
id: 3,
time: '2026-01-29 10:15:20',
promotionCode: 'SD20260129003',
studentName: '王**',
distributorName: '王分销',
profitAmount: '¥128.00',
status: '已完成',
statusClass: 'status-success'
}
])
// 分页信息
const currentPage = ref(1)
const totalItems = ref(100)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalItems.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取分销数据
// loadDistributionData()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出数据
function exportData() {
uni.showModal({
title: '导出数据',
content: '确定要导出当前筛选条件的分销数据吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 实际项目中应调用接口导出数据
// exportDistributionData()
}, 1500)
}
}
})
}
// 时间范围变更
function onDateChange(e) {
const value = e.detail.value
dateIndex.value = value
// 实际项目中应根据时间范围筛选数据
// filterData()
}
// 分销员变更
function onDistributorChange(e) {
const value = e.detail.value
distributorIndex.value = value
// 实际项目中应根据分销员筛选数据
// filterData()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索数据
// searchData()
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的数据
// loadPageData(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.distribution-data-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 200rpx;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
/* 通用部分样式 */
.metrics-section,
.trend-section,
.detail-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 核心指标 */
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.metric-card {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.metric-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 24rpx;
color: #606266;
}
/* 数据趋势 */
.chart-container {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.mock-chart {
display: flex;
flex-direction: column;
}
.chart-header {
margin-bottom: 24rpx;
}
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
text-align: center;
}
.chart-bars {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 300rpx;
gap: 16rpx;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.bar-container {
flex: 1;
width: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
margin-bottom: 12rpx;
}
.bar {
width: 40rpx;
background-color: #409eff;
border-radius: 4rpx 4rpx 0 0;
transition: height 0.5s ease;
}
.bar-label {
font-size: 20rpx;
color: #606266;
margin-bottom: 8rpx;
}
.bar-value {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
/* 推广明细 */
.detail-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 24rpx;
}
.detail-item {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.detail-time {
font-size: 20rpx;
color: #606266;
}
.detail-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-success {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.detail-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.detail-info {
flex: 1;
margin-right: 24rpx;
}
.info-item {
display: flex;
margin-bottom: 8rpx;
}
.info-label {
font-size: 20rpx;
color: #606266;
width: 80rpx;
}
.info-value {
font-size: 20rpx;
color: #303133;
}
.detail-amount {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.amount-label {
font-size: 20rpx;
color: #606266;
margin-bottom: 4rpx;
}
.amount-value {
font-size: 28rpx;
font-weight: bold;
color: #f56c6c;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,535 @@
<template>
<view class="personal-promotion-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">个人推广管理</view>
<view class="header-right" @click="sharePromotion">
<view class="share-btn">分享</view>
</view>
</view>
<!-- 推广码信息 -->
<view class="promotion-info-section">
<view class="section-title">推广码信息</view>
<view class="info-card">
<view class="info-row">
<view class="info-label">推广码</view>
<view class="info-value">{{ promotionCode }}</view>
</view>
<view class="info-row">
<view class="info-label">生成时间</view>
<view class="info-value">{{ createTime }}</view>
</view>
<view class="info-row">
<view class="info-label">有效期</view>
<view class="info-value">{{ validityPeriod }}</view>
</view>
</view>
</view>
<!-- 推广二维码 -->
<view class="qrcode-section">
<view class="section-title">推广二维码</view>
<view class="qrcode-card">
<view class="qrcode-image">
<!-- 模拟二维码图片 -->
<view class="mock-qrcode">
<view class="qrcode-pattern"></view>
<view class="qrcode-text">{{ promotionCode }}</view>
</view>
</view>
<view class="qrcode-hint">
请保存二维码并分享给学员学员扫码注册后将自动绑定为您的推广学员
</view>
<view class="qrcode-actions">
<view class="action-btn download-btn" @click="downloadQrcode">
下载二维码
</view>
<view class="action-btn refresh-btn" @click="refreshQrcode">
刷新二维码
</view>
</view>
</view>
</view>
<!-- 推广数据 -->
<view class="promotion-data-section">
<view class="section-title">推广数据</view>
<view class="data-card">
<view class="data-row">
<view class="data-item">
<view class="data-value">{{ totalScanCount }}</view>
<view class="data-label">扫码次数</view>
</view>
<view class="data-item">
<view class="data-value">{{ totalRegisterCount }}</view>
<view class="data-label">注册人数</view>
</view>
<view class="data-item">
<view class="data-value">{{ totalMemberCount }}</view>
<view class="data-label">会员人数</view>
</view>
</view>
<view class="data-row">
<view class="data-item">
<view class="data-value">{{ totalProfit }}</view>
<view class="data-label">累计分润</view>
</view>
<view class="data-item">
<view class="data-value">{{ todayScanCount }}</view>
<view class="data-label">今日扫码</view>
</view>
<view class="data-item">
<view class="data-value">{{ todayRegisterCount }}</view>
<view class="data-label">今日注册</view>
</view>
</view>
</view>
</view>
<!-- 推广记录 -->
<view class="promotion-record-section">
<view class="section-title">推广记录</view>
<view class="record-card">
<view class="record-header">
<view class="header-item">时间</view>
<view class="header-item">类型</view>
<view class="header-item">状态</view>
<view class="header-item">收益</view>
</view>
<view class="record-list">
<view
v-for="(record, index) in promotionRecords"
:key="index"
class="record-item"
>
<view class="record-cell">{{ record.time }}</view>
<view class="record-cell">{{ record.type }}</view>
<view class="record-cell" :class="record.statusClass">{{ record.status }}</view>
<view class="record-cell profit">{{ record.profit }}</view>
</view>
</view>
<view class="record-footer">
<view class="footer-text">
{{ totalRecords }} 条记录显示最近 {{ promotionRecords.length }}
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 推广码信息
const promotionCode = ref('SD20260129001')
const createTime = ref('2026-01-29 10:00:00')
const validityPeriod = ref('永久有效')
// 推广数据
const totalScanCount = ref(120)
const totalRegisterCount = ref(89)
const totalMemberCount = ref(45)
const totalProfit = ref('¥1,234.56')
const todayScanCount = ref(12)
const todayRegisterCount = ref(8)
// 推广记录
const promotionRecords = ref([
{
time: '2026-01-29 15:30:00',
type: '扫码注册',
status: '成功',
statusClass: 'status-success',
profit: '¥10.00'
},
{
time: '2026-01-29 14:20:00',
type: '购买会员',
status: '成功',
statusClass: 'status-success',
profit: '¥7.98'
},
{
time: '2026-01-29 13:10:00',
type: '扫码注册',
status: '成功',
statusClass: 'status-success',
profit: '¥10.00'
},
{
time: '2026-01-29 12:00:00',
type: '扫码注册',
status: '失败',
statusClass: 'status-failed',
profit: '¥0.00'
}
])
const totalRecords = ref(100)
onMounted(() => {
// 实际项目中应从接口获取个人推广信息
// loadPersonalPromotionInfo()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 分享推广
function sharePromotion() {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制推广链接'],
success: function(res) {
switch(res.tapIndex) {
case 0:
uni.showToast({ title: '分享到微信', duration: 1000 })
break
case 1:
uni.showToast({ title: '分享到朋友圈', duration: 1000 })
break
case 2:
uni.setClipboardData({
data: `https://example.com/register?code=${promotionCode.value}`,
success: function() {
uni.showToast({ title: '推广链接已复制', duration: 1000 })
}
})
break
}
}
})
}
// 下载二维码
function downloadQrcode() {
uni.showLoading({ title: '下载中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '二维码已下载',
icon: 'success'
})
// 实际项目中应调用接口下载二维码
// downloadQrcodeById()
}, 1000)
}
// 刷新二维码
function refreshQrcode() {
uni.showModal({
title: '刷新二维码',
content: '刷新二维码后,原二维码将失效,确定要刷新吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '刷新中...' })
setTimeout(() => {
uni.hideLoading()
// 模拟刷新后生成新的推广码
promotionCode.value = 'SD' + new Date().getTime().toString().substring(0, 10)
createTime.value = new Date().toLocaleString('zh-CN')
uni.showToast({
title: '二维码已刷新',
icon: 'success'
})
// 实际项目中应调用接口刷新二维码
// refreshQrcodeById()
}, 1500)
}
}
})
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.personal-promotion-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.share-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.promotion-info-section,
.qrcode-section,
.promotion-data-section,
.promotion-record-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 推广码信息 */
.info-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 24rpx;
color: #606266;
}
.info-value {
font-size: 24rpx;
color: #303133;
font-weight: 600;
}
/* 推广二维码 */
.qrcode-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.qrcode-image {
display: flex;
justify-content: center;
}
.mock-qrcode {
width: 300rpx;
height: 300rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.qrcode-pattern {
width: 240rpx;
height: 240rpx;
background-color: #303133;
margin-bottom: 16rpx;
}
.qrcode-text {
font-size: 20rpx;
color: #606266;
}
.qrcode-hint {
font-size: 20rpx;
color: #606266;
text-align: center;
line-height: 1.4;
padding: 0 24rpx;
}
.qrcode-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
cursor: pointer;
}
.download-btn {
background-color: #ecf5ff;
color: #409eff;
}
.refresh-btn {
background-color: #f0f9eb;
color: #67c23a;
}
/* 推广数据 */
.data-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.data-row {
display: flex;
justify-content: space-between;
margin-bottom: 24rpx;
}
.data-row:last-child {
margin-bottom: 0;
}
.data-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.data-value {
font-size: 28rpx;
font-weight: bold;
color: #303133;
}
.data-label {
font-size: 18rpx;
color: #606266;
}
/* 推广记录 */
.record-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.record-header {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.header-item {
flex: 1;
font-size: 20rpx;
font-weight: bold;
color: #606266;
text-align: center;
}
.record-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 16rpx;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: #fff;
border-radius: 8rpx;
}
.record-cell {
flex: 1;
font-size: 20rpx;
color: #303133;
text-align: center;
}
.record-cell.profit {
color: #f56c6c;
font-weight: 600;
}
.status-success {
color: #67c23a;
}
.status-failed {
color: #f56c6c;
}
.record-footer {
text-align: center;
}
.footer-text {
font-size: 18rpx;
color: #909399;
}
</style>

View File

@@ -0,0 +1,533 @@
<template>
<view class="profit-rule-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">分润规则配置</view>
<view class="header-right" @click="saveRules">
<view class="save-btn">保存</view>
</view>
</view>
<!-- 规则说明 -->
<view class="rule-info-section">
<view class="section-title">规则说明</view>
<view class="info-card">
<view class="info-item"> 分润规则适用于所有分销员</view>
<view class="info-item"> 扫码注册分润学员通过推广码扫码注册后分销员可获得的分润</view>
<view class="info-item"> 购买会员分润学员通过推广码购买会员后分销员可获得的分润</view>
<view class="info-item"> 分润比例按会员价格的百分比计算最高不超过50%</view>
</view>
</view>
<!-- 分润规则配置 -->
<view class="rule-config-section">
<view class="section-title">分润规则配置</view>
<view class="rule-card">
<!-- 扫码注册分润 -->
<view class="rule-item">
<view class="rule-header">
<view class="rule-title">扫码注册分润</view>
<view class="rule-toggle">
<switch
:checked="scanRegisterRule.enabled"
@change="onScanRegisterToggle"
class="toggle-switch"
active-color="#409eff"
/>
</view>
</view>
<view v-if="scanRegisterRule.enabled" class="rule-content">
<view class="rule-row">
<view class="rule-label">分润金额</view>
<view class="rule-control">
<input
v-model="scanRegisterRule.amount"
type="number"
class="rule-input"
placeholder="请输入分润金额"
min="0"
max="100"
step="0.1"
/>
<view class="rule-unit"></view>
</view>
</view>
<view class="rule-hint">
固定金额分润学员扫码注册后立即发放
建议设置为5-20
</view>
</view>
</view>
<!-- 购买会员分润 -->
<view class="rule-item">
<view class="rule-header">
<view class="rule-title">购买会员分润</view>
<view class="rule-toggle">
<switch
:checked="memberPurchaseRule.enabled"
@change="onMemberPurchaseToggle"
class="toggle-switch"
active-color="#409eff"
/>
</view>
</view>
<view v-if="memberPurchaseRule.enabled" class="rule-content">
<view class="rule-row">
<view class="rule-label">分润比例</view>
<view class="rule-control">
<input
v-model="memberPurchaseRule.percentage"
type="number"
class="rule-input"
placeholder="请输入分润比例"
min="0"
max="50"
step="0.1"
/>
<view class="rule-unit">%</view>
</view>
</view>
<view class="rule-row">
<view class="rule-label">会员类型</view>
<view class="rule-control">
<picker
:range="memberTypes"
:value="memberTypeIndex"
@change="onMemberTypeChange"
class="picker"
>
<view class="picker-text">{{ memberTypes[memberTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="rule-hint">
按会员价格的百分比计算分润
最高分润比例为50%
建议设置为10%-30%
</view>
</view>
</view>
</view>
</view>
<!-- 会员价格设置 -->
<view class="price-section">
<view class="section-title">会员价格设置</view>
<view class="price-card">
<view class="price-item">
<view class="price-label">月度会员价格</view>
<view class="price-control">
<input
v-model="memberPrices.monthly"
type="number"
class="price-input"
placeholder="请输入月度会员价格"
min="0"
max="1000"
step="0.1"
/>
<view class="price-unit"></view>
</view>
</view>
<view class="price-item">
<view class="price-label">季度会员价格</view>
<view class="price-control">
<input
v-model="memberPrices.quarterly"
type="number"
class="price-input"
placeholder="请输入季度会员价格"
min="0"
max="3000"
step="0.1"
/>
<view class="price-unit"></view>
</view>
</view>
<view class="price-item">
<view class="price-label">年度会员价格</view>
<view class="price-control">
<input
v-model="memberPrices.yearly"
type="number"
class="price-input"
placeholder="请输入年度会员价格"
min="0"
max="10000"
step="0.1"
/>
<view class="price-unit"></view>
</view>
</view>
</view>
</view>
<!-- 分润预览 -->
<view class="preview-section">
<view class="section-title">分润预览</view>
<view class="preview-card">
<view class="preview-item">
<view class="preview-label">月度会员分润</view>
<view class="preview-value">{{ calculatePreview('monthly') }}</view>
</view>
<view class="preview-item">
<view class="preview-label">季度会员分润</view>
<view class="preview-value">{{ calculatePreview('quarterly') }}</view>
</view>
<view class="preview-item">
<view class="preview-label">年度会员分润</view>
<view class="preview-value">{{ calculatePreview('yearly') }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 扫码注册分润规则
const scanRegisterRule = ref({
enabled: true,
amount: 10
})
// 购买会员分润规则
const memberPurchaseRule = ref({
enabled: true,
percentage: 20
})
// 会员类型选项
const memberTypes = ['全部会员类型', '月度会员', '季度会员', '年度会员']
const memberTypeIndex = ref(0)
// 会员价格
const memberPrices = ref({
monthly: 39.9,
quarterly: 99.9,
yearly: 299.9
})
// 计算分润预览
function calculatePreview(type) {
if (!memberPurchaseRule.value.enabled) {
return '¥0.00'
}
const price = memberPrices.value[type]
const percentage = memberPurchaseRule.value.percentage
const amount = (price * percentage) / 100
return `¥${amount.toFixed(2)}`
}
onMounted(() => {
// 实际项目中应从接口获取分润规则配置
// loadProfitRules()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 保存规则
function saveRules() {
uni.showModal({
title: '保存规则',
content: '确定要保存分润规则配置吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
// 实际项目中应调用接口保存分润规则
// saveProfitRules()
}, 1500)
}
}
})
}
// 扫码注册分润开关
function onScanRegisterToggle(e) {
scanRegisterRule.value.enabled = e.detail.value
}
// 购买会员分润开关
function onMemberPurchaseToggle(e) {
memberPurchaseRule.value.enabled = e.detail.value
}
// 会员类型变更
function onMemberTypeChange(e) {
memberTypeIndex.value = e.detail.value
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.profit-rule-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.save-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.rule-info-section,
.rule-config-section,
.price-section,
.preview-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 规则说明 */
.info-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.info-item {
font-size: 20rpx;
color: #606266;
margin-bottom: 8rpx;
line-height: 1.4;
}
/* 分润规则配置 */
.rule-card {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.rule-item {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.rule-title {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.rule-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.rule-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.rule-label {
font-size: 20rpx;
color: #606266;
}
.rule-control {
display: flex;
align-items: center;
gap: 8rpx;
}
.rule-input {
width: 120rpx;
height: 60rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 0 16rpx;
text-align: center;
font-size: 24rpx;
}
.rule-unit {
font-size: 24rpx;
color: #606266;
}
.rule-hint {
font-size: 20rpx;
color: #909399;
margin-top: 8rpx;
line-height: 1.4;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
min-width: 200rpx;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 会员价格设置 */
.price-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.price-label {
font-size: 24rpx;
color: #606266;
}
.price-control {
display: flex;
align-items: center;
gap: 8rpx;
}
.price-input {
width: 160rpx;
height: 60rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 0 16rpx;
text-align: center;
font-size: 24rpx;
}
.price-unit {
font-size: 24rpx;
color: #606266;
}
/* 分润预览 */
.preview-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.preview-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.preview-label {
font-size: 24rpx;
color: #606266;
}
.preview-value {
font-size: 28rpx;
font-weight: bold;
color: #f56c6c;
}
</style>

View File

@@ -0,0 +1,528 @@
<template>
<view class="profit-query-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">分润查询</view>
<view class="header-right" @click="exportProfit">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 分润概览 -->
<view class="profit-overview-section">
<view class="section-title">分润概览</view>
<view class="overview-card">
<view class="overview-row">
<view class="overview-item">
<view class="overview-value">{{ totalProfit }}</view>
<view class="overview-label">累计分润</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ todayProfit }}</view>
<view class="overview-label">今日分润</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ monthProfit }}</view>
<view class="overview-label">本月分润</view>
</view>
</view>
<view class="overview-row">
<view class="overview-item">
<view class="overview-value">{{ pendingProfit }}</view>
<view class="overview-label">待结算</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ settledProfit }}</view>
<view class="overview-label">已结算</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ withdrawableProfit }}</view>
<view class="overview-label">可提现</view>
</view>
</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
:range="timeRangeOptions"
:value="timeRangeIndex"
@change="onTimeRangeChange"
class="picker"
>
<view class="picker-text">{{ timeRangeOptions[timeRangeIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">分润类型</view>
<view class="filter-control">
<picker
:range="profitTypeOptions"
:value="profitTypeIndex"
@change="onProfitTypeChange"
class="picker"
>
<view class="picker-text">{{ profitTypeOptions[profitTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 分润明细 -->
<view class="profit-detail-section">
<view class="section-title">分润明细</view>
<view class="detail-card">
<view class="detail-header">
<view class="header-item">时间</view>
<view class="header-item">类型</view>
<view class="header-item">金额</view>
<view class="header-item">状态</view>
</view>
<view class="detail-list">
<view
v-for="(item, index) in profitList"
:key="index"
class="detail-item"
>
<view class="detail-cell">{{ item.time }}</view>
<view class="detail-cell">{{ item.type }}</view>
<view class="detail-cell amount">{{ item.amount }}</view>
<view class="detail-cell" :class="item.statusClass">{{ item.status }}</view>
</view>
</view>
<view class="detail-footer">
<view class="footer-text">
{{ totalItems }} 条记录当前第 {{ currentPage }} / {{ totalPages }}
</view>
<view class="footer-controls">
<view
class="control-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="control-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 分润概览数据
const totalProfit = ref('¥1,234.56')
const todayProfit = ref('¥123.45')
const monthProfit = ref('¥3,456.78')
const pendingProfit = ref('¥567.89')
const settledProfit = ref('¥666.67')
const withdrawableProfit = ref('¥456.78')
// 筛选条件
const timeRangeOptions = ['全部时间', '今日', '本周', '本月', '本季度', '本年', '自定义']
const timeRangeIndex = ref(3)
const profitTypeOptions = ['全部类型', '扫码注册', '购买会员']
const profitTypeIndex = ref(0)
const statusOptions = ['全部状态', '待结算', '已结算', '已提现']
const statusIndex = ref(0)
// 分润明细数据
const profitList = ref([
{
time: '2026-01-29 15:30:00',
type: '扫码注册',
amount: '¥10.00',
status: '待结算',
statusClass: 'status-pending'
},
{
time: '2026-01-29 14:20:00',
type: '购买会员',
amount: '¥7.98',
status: '待结算',
statusClass: 'status-pending'
},
{
time: '2026-01-28 10:15:00',
type: '扫码注册',
amount: '¥10.00',
status: '已结算',
statusClass: 'status-settled'
},
{
time: '2026-01-27 09:45:00',
type: '购买会员',
amount: '¥7.98',
status: '已结算',
statusClass: 'status-settled'
},
{
time: '2026-01-26 16:20:00',
type: '扫码注册',
amount: '¥10.00',
status: '已提现',
statusClass: 'status-withdrawn'
}
])
// 分页信息
const currentPage = ref(1)
const totalItems = ref(100)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalItems.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取分润数据
// loadProfitData()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出分润
function exportProfit() {
uni.showModal({
title: '导出分润明细',
content: '确定要导出当前筛选条件下的分润明细吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 实际项目中应调用接口导出分润
// exportProfitData()
}, 1500)
}
}
})
}
// 时间范围变更
function onTimeRangeChange(e) {
const value = e.detail.value
timeRangeIndex.value = value
// 实际项目中应根据时间范围筛选分润数据
// filterProfitData()
}
// 分润类型变更
function onProfitTypeChange(e) {
const value = e.detail.value
profitTypeIndex.value = value
// 实际项目中应根据分润类型筛选分润数据
// filterProfitData()
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选分润数据
// filterProfitData()
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的分润数据
// loadProfitPage(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.profit-query-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.profit-overview-section,
.filter-section,
.profit-detail-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 分润概览 */
.overview-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.overview-row {
display: flex;
justify-content: space-between;
margin-bottom: 24rpx;
}
.overview-row:last-child {
margin-bottom: 0;
}
.overview-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.overview-value {
font-size: 28rpx;
font-weight: bold;
color: #303133;
}
.overview-label {
font-size: 18rpx;
color: #606266;
}
/* 筛选条件 */
.filter-row {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 200rpx;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 分润明细 */
.detail-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.detail-header {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.header-item {
flex: 1;
font-size: 20rpx;
font-weight: bold;
color: #606266;
text-align: center;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 24rpx;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: #fff;
border-radius: 8rpx;
}
.detail-cell {
flex: 1;
font-size: 20rpx;
color: #303133;
text-align: center;
}
.detail-cell.amount {
color: #f56c6c;
font-weight: 600;
}
.status-pending {
color: #e6a23c;
}
.status-settled {
color: #67c23a;
}
.status-withdrawn {
color: #409eff;
}
.detail-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16rpx;
border-top: 1rpx solid #e0e0e0;
}
.footer-text {
font-size: 20rpx;
color: #606266;
}
.footer-controls {
display: flex;
gap: 16rpx;
}
.control-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #409eff;
cursor: pointer;
}
.control-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,654 @@
<template>
<view class="qrcode-manage-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">专属二维码管理</view>
<view class="header-right" @click="generateBatchQrcode">
<view class="batch-btn">批量生成</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">分销员</view>
<view class="filter-control">
<picker
:range="distributorOptions"
:value="distributorIndex"
@change="onDistributorChange"
class="picker"
>
<view class="picker-text">{{ distributorOptions[distributorIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入推广码或备注"
@input="onSearch"
/>
</view>
</view>
</view>
</view>
<!-- 二维码列表 -->
<view class="qrcode-list-section">
<view class="section-title">二维码列表</view>
<view class="qrcode-grid">
<view
v-for="(qrcode, index) in qrcodeList"
:key="index"
class="qrcode-item"
>
<view class="qrcode-card">
<view class="qrcode-header">
<view class="qrcode-code">{{ qrcode.code }}</view>
<view class="qrcode-status" :class="qrcode.statusClass">{{ qrcode.status }}</view>
</view>
<view class="qrcode-body">
<view class="qrcode-image">
<!-- 模拟二维码图片 -->
<view class="mock-qrcode">
<view class="qrcode-pattern"></view>
<view class="qrcode-text">{{ qrcode.code }}</view>
</view>
</view>
<view class="qrcode-info">
<view class="info-item">
<view class="info-label">分销员</view>
<view class="info-value">{{ qrcode.distributor }}</view>
</view>
<view class="info-item">
<view class="info-label">生成时间</view>
<view class="info-value">{{ qrcode.createTime }}</view>
</view>
<view class="info-item">
<view class="info-label">扫码次数</view>
<view class="info-value">{{ qrcode.scanCount }}</view>
</view>
<view class="info-item">
<view class="info-label">备注</view>
<view class="info-value">{{ qrcode.remark }}</view>
</view>
</view>
</view>
<view class="qrcode-footer">
<view class="action-btn download-btn" @click="downloadQrcode(qrcode.id)">
下载
</view>
<view class="action-btn preview-btn" @click="previewQrcode(qrcode.id)">
预览
</view>
<view class="action-btn" :class="qrcode.status === '启用' ? 'disable-btn' : 'enable-btn'"
@click="toggleQrcodeStatus(qrcode.id, qrcode.status)">
{{ qrcode.status === '启用' ? '禁用' : '启用' }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="qrcodeList.length === 0" class="empty-state">
<view class="empty-icon">🔗</view>
<view class="empty-text">暂无二维码数据</view>
</view>
<!-- 分页 -->
<view v-if="qrcodeList.length > 0" class="pagination">
<view class="page-info">
{{ totalQrcodes }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 分销员选项
const distributorOptions = ['全部分销员', '张分销', '李分销', '王分销', '刘分销']
const distributorIndex = ref(0)
// 状态选项
const statusOptions = ['全部状态', '启用', '禁用']
const statusIndex = ref(0)
// 搜索关键词
const searchKeyword = ref('')
// 二维码列表
const qrcodeList = ref([
{
id: 1,
code: 'SD20260129001',
distributor: '张分销',
createTime: '2026-01-29 10:00:00',
scanCount: 120,
status: '启用',
statusClass: 'status-active',
remark: '推广活动专用'
},
{
id: 2,
code: 'SD20260129002',
distributor: '李分销',
createTime: '2026-01-29 11:00:00',
scanCount: 89,
status: '启用',
statusClass: 'status-active',
remark: '日常推广'
},
{
id: 3,
code: 'SD20260129003',
distributor: '王分销',
createTime: '2026-01-29 12:00:00',
scanCount: 45,
status: '禁用',
statusClass: 'status-inactive',
remark: '备用二维码'
},
{
id: 4,
code: 'SD20260129004',
distributor: '刘分销',
createTime: '2026-01-29 13:00:00',
scanCount: 67,
status: '启用',
statusClass: 'status-active',
remark: '线下活动专用'
}
])
// 分页信息
const currentPage = ref(1)
const totalQrcodes = ref(100)
const pageSize = ref(12)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalQrcodes.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取二维码列表
// loadQrcodeList()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 批量生成二维码
function generateBatchQrcode() {
uni.showModal({
title: '批量生成二维码',
content: '请输入要生成的二维码数量',
editable: true,
placeholderText: '请输入数量',
success: function(res) {
if (res.confirm && res.content) {
const count = parseInt(res.content)
if (isNaN(count) || count <= 0 || count > 100) {
uni.showToast({
title: '请输入1-100之间的数字',
icon: 'none'
})
return
}
uni.showLoading({ title: '生成中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: `成功生成${count}个二维码`,
icon: 'success'
})
// 实际项目中应调用接口批量生成二维码
// generateBatchQrcodes(count)
}, 1500)
}
}
})
}
// 分销员变更
function onDistributorChange(e) {
const value = e.detail.value
distributorIndex.value = value
// 实际项目中应根据分销员筛选二维码
// filterQrcodeList()
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选二维码
// filterQrcodeList()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索二维码
// searchQrcodeList()
}
// 下载二维码
function downloadQrcode(id) {
uni.showLoading({ title: '下载中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '下载成功',
icon: 'success'
})
// 实际项目中应调用接口下载二维码
// downloadQrcodeById(id)
}, 1000)
}
// 预览二维码
function previewQrcode(id) {
uni.showToast({
title: '预览功能开发中',
icon: 'none'
})
}
// 切换二维码状态
function toggleQrcodeStatus(id, currentStatus) {
const newStatus = currentStatus === '启用' ? '禁用' : '启用'
uni.showModal({
title: '切换状态',
content: `确定要${newStatus === '启用' ? '启用' : '禁用'}该二维码吗?`,
success: function(res) {
if (res.confirm) {
// 实际项目中应调用接口切换二维码状态
const qrcode = qrcodeList.value.find(item => item.id === id)
if (qrcode) {
qrcode.status = newStatus
qrcode.statusClass = newStatus === '启用' ? 'status-active' : 'status-inactive'
}
uni.showToast({
title: `二维码已${newStatus}`,
icon: 'success'
})
}
}
})
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的二维码
// loadQrcodePage(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.qrcode-manage-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 120rpx;
text-align: right;
}
.batch-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 200rpx;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
/* 二维码列表 */
.qrcode-list-section {
flex: 1;
padding: 0 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.qrcode-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300rpx, 1fr));
gap: 16rpx;
}
.qrcode-item {
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.qrcode-card {
padding: 24rpx;
}
.qrcode-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.qrcode-code {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
.qrcode-status {
font-size: 16rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-active {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.status-inactive {
color: #909399;
background-color: rgba(144, 147, 153, 0.1);
}
.qrcode-body {
margin-bottom: 16rpx;
}
.qrcode-image {
display: flex;
justify-content: center;
margin-bottom: 16rpx;
}
.mock-qrcode {
width: 200rpx;
height: 200rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.qrcode-pattern {
width: 160rpx;
height: 160rpx;
background-color: #303133;
margin-bottom: 8rpx;
}
.qrcode-text {
font-size: 16rpx;
color: #606266;
}
.qrcode-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
display: flex;
font-size: 18rpx;
}
.info-label {
color: #606266;
width: 80rpx;
}
.info-value {
color: #303133;
flex: 1;
}
.qrcode-footer {
display: flex;
justify-content: space-between;
}
.action-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 16rpx;
font-weight: 600;
cursor: pointer;
}
.download-btn {
background-color: #ecf5ff;
color: #409eff;
}
.preview-btn {
background-color: #f0f9eb;
color: #67c23a;
}
.disable-btn {
background-color: #fef0f0;
color: #f56c6c;
}
.enable-btn {
background-color: #f0f9eb;
color: #67c23a;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -1,224 +1,314 @@
<template>
<view class="add-container">
<!-- 头部 -->
<view class="header">
<uni-icons type="back" size="28" color="#303133" @click="handleCancel" />
<text class="title">新增分销员</text>
<view class="header-right"></view>
<view class="distributor-add-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">添加分销员</view>
<view class="header-right" @click="saveDistributor">
<view class="save-btn">保存</view>
</view>
</view>
<!-- 表单容 -->
<view class="form-container">
<uni-forms :model="formData" ref="formRef" labelWidth="100rpx">
<uni-forms-item label="姓名" required name="name">
<uni-easyinput
v-model="formData.name"
placeholder="请输入姓名"
class="form-input"
:focus="true"
/>
</uni-forms-item>
<uni-forms-item label="手机号" required name="phone">
<uni-easyinput
v-model="formData.phone"
placeholder="请输入手机号"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="OpenID" required name="openid">
<uni-easyinput
v-model="formData.openid"
placeholder="请输入OpenID"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="父ID" required name="parent_id">
<uni-easyinput
v-model="formData.parent_id"
placeholder="请输入父ID"
type="number"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="租户ID" required name="tenant_id">
<uni-easyinput
v-model="formData.tenant_id"
placeholder="请输入租户ID"
type="number"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="苹果URL" name="applet_url">
<uni-easyinput
v-model="formData.applet_url"
placeholder="请输入苹果URL"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="状态" name="is_active">
<uni-data-checkbox
v-model="formData.is_active"
:localdata="statusOptions"
style="width: 100%"
/>
</uni-forms-item>
</uni-forms>
</view>
<!-- 按钮容器 -->
<view class="btn-container">
<button class="cancel-btn" @click="handleCancel">
<uni-icons type="close" size="20" color="#fff" />
<text class="btn-text">取消</text>
</button>
<button class="submit-btn" @click="handleSubmit">
<uni-icons type="checkmark" size="20" color="#fff" />
<text class="btn-text">提交</text>
</button>
<!-- 表单 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<view class="form-card">
<view class="form-row">
<view class="form-label">姓名</view>
<view class="form-control">
<input
v-model="distributorForm.name"
class="form-input"
placeholder="请输入姓名"
/>
</view>
</view>
<view class="form-row">
<view class="form-label">手机号</view>
<view class="form-control">
<input
v-model="distributorForm.phone"
type="number"
class="form-input"
placeholder="请输入手机号"
maxlength="11"
/>
</view>
</view>
<view class="form-row">
<view class="form-label">角色</view>
<view class="form-control">
<picker
:range="roleOptions"
:value="roleIndex"
@change="onRoleChange"
class="picker"
>
<view class="picker-text">{{ roleOptions[roleIndex] }}</view>
</picker>
</view>
</view>
<view class="form-row">
<view class="form-label">状态</view>
<view class="form-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="form-row">
<view class="form-label">备注</view>
<view class="form-control">
<textarea
v-model="distributorForm.remark"
class="form-textarea"
placeholder="请输入备注信息"
maxlength="200"
></textarea>
<view class="textarea-count">{{ distributorForm.remark.length }}/200</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue"
import { ref, onMounted } from "vue"
const formRef = ref(null)
const formData = ref({
// 分销员表单数据
const distributorForm = ref({
name: '',
phone: '',
openid: '',
parent_id: '',
tenant_id: '',
applet_url: '',
is_active: 'Y'
role: '初级分销员',
status: '启用',
remark: ''
})
const statusOptions = [
{ text: '激活', value: 'Y' },
{ text: '未激活', value: 'N' }
]
// 取消
function handleCancel() {
uni.navigateBack()
// 角色选项
const roleOptions = ['初级分销员', '中级分销员', '高级分销员']
const roleIndex = ref(0)
// 状态选项
const statusOptions = ['启用', '禁用']
const statusIndex = ref(0)
onMounted(() => {
// 初始化表单数据
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 提交
function handleSubmit() {
formRef.value.validate().then(() => {
// 这里应该调用后端API提交数据
uni.showToast({ title: '新增成功' })
uni.navigateTo({ url: '/pages/distributor/index' })
}).catch(err => {
console.log('表单验证失败', err)
})
// 保存分销员
function saveDistributor() {
if (!distributorForm.value.name) {
uni.showToast({
title: '请输入姓名',
icon: 'none'
})
return
}
if (!distributorForm.value.phone || distributorForm.value.phone.length !== 11) {
uni.showToast({
title: '请输入有效的手机号',
icon: 'none'
})
return
}
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '添加成功',
icon: 'success'
})
// 实际项目中应调用接口保存分销员
// saveDistributorData()
// 保存成功后返回上一页
setTimeout(() => {
goBack()
}, 1000)
}, 1500)
}
// 角色变更
function onRoleChange(e) {
const value = e.detail.value
roleIndex.value = value
distributorForm.value.role = roleOptions[value]
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
distributorForm.value.status = statusOptions[value]
}
</script>
<style lang="scss" scoped>
.add-container {
padding: 20rpx;
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100vh;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.distributor-add-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.header {
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #ecf5ff;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.title {
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: 600;
font-weight: bold;
color: #303133;
}
.header-right {
width: 28px;
width: 60rpx;
text-align: right;
}
/* 表单容器样式 */
.form-container {
.save-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 表单内容 */
.form-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.form-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.form-row {
display: flex;
flex-direction: column;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
/* 表单输入框样式 */
.form-row:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.form-control {
position: relative;
}
.form-input {
border-radius: 8rpx !important;
border: 1rpx solid #dcdfe6 !important;
transition: all 0.3s ease !important;
}
.form-input:focus {
border-color: #409eff !important;
box-shadow: 0 0 0 2rpx rgba(64, 158, 255, 0.2) !important;
}
/* 按钮容器样式 */
.btn-container {
display: flex;
gap: 16rpx;
margin-top: 32rpx;
}
.cancel-btn,
.submit-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 16rpx 0;
width: 100%;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
font-size: 28rpx;
transition: all 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
padding: 16rpx;
font-size: 24rpx;
color: #303133;
}
.cancel-btn:active,
.submit-btn:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
.picker {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.cancel-btn {
background-color: #909399;
color: #fff;
.picker-text {
font-size: 24rpx;
color: #303133;
}
.submit-btn {
background-color: #409eff;
color: #fff;
.form-textarea {
width: 100%;
height: 160rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #303133;
resize: none;
}
.btn-text {
margin-left: 8rpx;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.form-container {
padding: 20rpx;
}
.btn-container {
margin-top: 24rpx;
}
.textarea-count {
position: absolute;
bottom: 8rpx;
right: 16rpx;
font-size: 18rpx;
color: #909399;
}
</style>

View File

@@ -1,85 +1,88 @@
<template>
<view class="edit-container">
<!-- 头部 -->
<view class="header">
<uni-icons type="back" size="28" color="#303133" @click="handleCancel" />
<text class="title">修改分销员</text>
<view class="header-right"></view>
<view class="distributor-edit-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">编辑分销员</view>
<view class="header-right" @click="saveDistributor">
<view class="save-btn">保存</view>
</view>
</view>
<!-- 表单容 -->
<view class="form-container">
<uni-forms :model="formData" ref="formRef" labelWidth="100rpx">
<uni-forms-item label="姓名" required name="name">
<uni-easyinput
v-model="formData.name"
placeholder="请输入姓名"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="手机号" required name="phone">
<uni-easyinput
v-model="formData.phone"
placeholder="请输入手机号"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="OpenID" required name="openid">
<uni-easyinput
v-model="formData.openid"
placeholder="请输入OpenID"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="父ID" required name="parent_id">
<uni-easyinput
v-model="formData.parent_id"
placeholder="请输入父ID"
type="number"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="租户ID" required name="tenant_id">
<uni-easyinput
v-model="formData.tenant_id"
placeholder="请输入租户ID"
type="number"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="苹果URL" name="applet_url">
<uni-easyinput
v-model="formData.applet_url"
placeholder="请输入苹果URL"
class="form-input"
/>
</uni-forms-item>
<uni-forms-item label="状态" name="is_active">
<uni-data-checkbox
v-model="formData.is_active"
:localdata="statusOptions"
style="width: 100%"
/>
</uni-forms-item>
</uni-forms>
<!-- 表单 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<view class="form-card">
<view class="form-row">
<view class="form-label">姓名</view>
<view class="form-control">
<input
v-model="distributorForm.name"
class="form-input"
placeholder="请输入姓名"
/>
</view>
</view>
<view class="form-row">
<view class="form-label">手机号</view>
<view class="form-control">
<input
v-model="distributorForm.phone"
type="number"
class="form-input"
placeholder="请输入手机号"
maxlength="11"
/>
</view>
</view>
<view class="form-row">
<view class="form-label">角色</view>
<view class="form-control">
<picker
:range="roleOptions"
:value="roleIndex"
@change="onRoleChange"
class="picker"
>
<view class="picker-text">{{ roleOptions[roleIndex] }}</view>
</picker>
</view>
</view>
<view class="form-row">
<view class="form-label">状态</view>
<view class="form-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="form-row">
<view class="form-label">备注</view>
<view class="form-control">
<textarea
v-model="distributorForm.remark"
class="form-textarea"
placeholder="请输入备注信息"
maxlength="200"
></textarea>
<view class="textarea-count">{{ distributorForm.remark.length }}/200</view>
</view>
</view>
</view>
</view>
<!-- 按钮容器 -->
<view class="btn-container">
<button class="cancel-btn" @click="handleCancel">
<uni-icons type="close" size="20" color="#fff" />
<text class="btn-text">取消</text>
</button>
<button class="submit-btn" @click="handleSubmit">
<uni-icons type="checkmark" size="20" color="#fff" />
<text class="btn-text">提交</text>
</button>
<!-- 操作按钮 -->
<view class="action-section">
<view class="action-btn delete-btn" @click="deleteDistributor">
删除分销员
</view>
</view>
</view>
</template>
@@ -87,162 +90,286 @@
<script setup>
import { ref, onMounted } from "vue"
const formRef = ref(null)
const distributorId = ref(uni.getStorageSync('__uni_route_query')?.id || '')
const formData = ref({
// 分销员表单数据
const distributorForm = ref({
id: '',
name: '',
phone: '',
openid: '',
parent_id: '',
tenant_id: '',
applet_url: '',
is_active: 'Y'
name: '张三',
phone: '13800138001',
role: '初级分销员',
status: '启用',
remark: '默认分销员'
})
const statusOptions = [
{ text: '激活', value: 'Y' },
{ text: '未激活', value: 'N' }
]
// 初始化加载数据
// 角色选项
const roleOptions = ['初级分销员', '中级分销员', '高级分销员']
const roleIndex = ref(0)
// 状态选项
const statusOptions = ['启用', '禁用']
const statusIndex = ref(0)
onMounted(() => {
fetchDistributorDetail()
// 实际项目中应从接口获取分销员详情
// loadDistributorDetail()
})
// 获取分销员详情
function fetchDistributorDetail() {
// 这里应该调用后端API获取数据
// 模拟数据
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 保存分销员
function saveDistributor() {
if (!distributorForm.value.name) {
uni.showToast({
title: '请输入姓名',
icon: 'none'
})
return
}
if (!distributorForm.value.phone || distributorForm.value.phone.length !== 11) {
uni.showToast({
title: '请输入有效的手机号',
icon: 'none'
})
return
}
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
formData.value = {
id: distributorId.value,
name: '张三',
phone: '13800138001',
openid: 'openid123456',
parent_id: '0',
tenant_id: '1',
applet_url: 'https://example.com',
is_active: 'Y'
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
// 实际项目中应调用接口保存分销员
// updateDistributorData()
// 保存成功后返回上一页
setTimeout(() => {
goBack()
}, 1000)
}, 1500)
}
// 删除分销员
function deleteDistributor() {
uni.showModal({
title: '删除分销员',
content: '确定要删除该分销员吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '删除成功',
icon: 'success'
})
// 实际项目中应调用接口删除分销员
// deleteDistributorData()
// 删除成功后返回上一页
setTimeout(() => {
goBack()
}, 1000)
}, 1500)
}
}
}, 500)
}
// 取消
function handleCancel() {
uni.navigateBack()
}
// 提交
function handleSubmit() {
formRef.value.validate().then(() => {
// 这里应该调用后端API提交数据
uni.showToast({ title: '修改成功' })
uni.navigateTo({ url: '/pages/distributor/index' })
}).catch(err => {
console.log('表单验证失败', err)
})
}
// 角色变更
function onRoleChange(e) {
const value = e.detail.value
roleIndex.value = value
distributorForm.value.role = roleOptions[value]
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
distributorForm.value.status = statusOptions[value]
}
</script>
<style lang="scss" scoped>
.edit-container {
padding: 20rpx;
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100vh;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.distributor-edit-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.header {
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #ecf5ff;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.title {
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: 600;
font-weight: bold;
color: #303133;
}
.header-right {
width: 28px;
width: 60rpx;
text-align: right;
}
/* 表单容器样式 */
.form-container {
.save-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 表单内容 */
.form-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.form-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.form-row {
display: flex;
flex-direction: column;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
/* 表单输入框样式 */
.form-row:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.form-control {
position: relative;
}
.form-input {
border-radius: 8rpx !important;
border: 1rpx solid #dcdfe6 !important;
transition: all 0.3s ease !important;
}
.form-input:focus {
border-color: #409eff !important;
box-shadow: 0 0 0 2rpx rgba(64, 158, 255, 0.2) !important;
}
/* 按钮容器样式 */
.btn-container {
display: flex;
gap: 16rpx;
margin-top: 32rpx;
}
.cancel-btn,
.submit-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 16rpx 0;
width: 100%;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
font-size: 28rpx;
transition: all 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
padding: 16rpx;
font-size: 24rpx;
color: #303133;
}
.cancel-btn:active,
.submit-btn:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
.picker {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.cancel-btn {
background-color: #909399;
color: #fff;
.picker-text {
font-size: 24rpx;
color: #303133;
}
.submit-btn {
background-color: #409eff;
color: #fff;
.form-textarea {
width: 100%;
height: 160rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #303133;
resize: none;
}
.btn-text {
margin-left: 8rpx;
.textarea-count {
position: absolute;
bottom: 8rpx;
right: 16rpx;
font-size: 18rpx;
color: #909399;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.form-container {
padding: 20rpx;
}
.btn-container {
margin-top: 24rpx;
}
/* 操作按钮 */
.action-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.action-btn {
width: 100%;
text-align: center;
padding: 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 600;
cursor: pointer;
}
.delete-btn {
background-color: #fef0f0;
color: #f56c6c;
}
</style>

View File

@@ -1,398 +1,495 @@
<template>
<view class="distributor-container">
<!-- 头部 -->
<view class="header">
<text class="title">分销员管理</text>
<button class="add-btn" @click="handleAdd">
<uni-icons type="plus" size="20" color="#fff" />
<text class="add-btn-text">新增分销员</text>
</button>
<view class="distributor-list-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">分销员管理</view>
<view class="header-right" @click="addDistributor">
<view class="add-btn">添加</view>
</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<view class="search-input-wrapper">
<uni-icons type="search" size="20" color="#909399" class="search-icon" />
<input
v-model="searchForm.name"
type="text"
placeholder="请输入分销员姓名"
class="search-input"
/>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入姓名或手机号"
@input="onSearch"
/>
</view>
</view>
</view>
<view class="search-input-wrapper">
<uni-icons type="phone" size="20" color="#909399" class="search-icon" />
<input
v-model="searchForm.phone"
type="text"
placeholder="请输入手机号"
class="search-input"
/>
</view>
<button class="search-btn" @click="handleSearch">
<uni-icons type="search" size="20" color="#fff" />
<text class="search-btn-text">查询</text>
</button>
</view>
<!-- 格容器 -->
<view class="table-container">
<uni-table :loading="loading" border stripe emptyText="暂无数据">
<uni-tr>
<uni-th width="120">姓名</uni-th>
<uni-th width="150">手机号</uni-th>
<uni-th width="100">总收益</uni-th>
<uni-th width="100">可用收益</uni-th>
<uni-th width="80">状态</uni-th>
<uni-th width="180">操作</uni-th>
</uni-tr>
<uni-tr v-for="item in distributorList" :key="item.id" hover>
<uni-td>{{ item.name }}</uni-td>
<uni-td>{{ item.phone }}</uni-td>
<uni-td>{{ item.total_income }}</uni-td>
<uni-td>{{ item.available_income }}</uni-td>
<uni-td>
<view class="status-badge" :class="item.is_active === 'Y' ? 'active' : 'inactive'">
{{ item.is_active === 'Y' ? '激活' : '未激活' }}
<!-- 分销员列 -->
<view class="distributor-list-section">
<view class="section-title">分销员列表</view>
<view class="distributor-list">
<view
v-for="(distributor, index) in distributorList"
:key="index"
class="distributor-item"
>
<view class="distributor-avatar">
<view class="avatar-placeholder">{{ getInitials(distributor.name) }}</view>
</view>
<view class="distributor-info">
<view class="distributor-name">{{ distributor.name }}</view>
<view class="distributor-meta">
<view class="meta-item">{{ distributor.phone }}</view>
<view class="meta-item">{{ distributor.role }}</view>
<view class="meta-item status-{{ distributor.statusClass }}">{{ distributor.status }}</view>
</view>
</uni-td>
<uni-td>
<view class="action-buttons">
<button class="action-btn edit" @click="handleEdit(item)">
<uni-icons type="compose" size="18" color="#409eff" />
<text>修改</text>
</button>
<button class="action-btn delete" @click="handleDelete(item.id)">
<uni-icons type="trash" size="18" color="#f56c6c" />
<text>删除</text>
</button>
<button class="action-btn rule" @click="handleRule(item.id)">
<uni-icons type="settings" size="18" color="#e6a23c" />
<text>分润</text>
</button>
</view>
<view class="distributor-actions">
<view class="action-btn edit-btn" @click="editDistributor(distributor.id)">
编辑
</view>
</uni-td>
</uni-tr>
</uni-table>
<view class="action-btn delete-btn" @click="deleteDistributor(distributor.id)">
删除
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="distributorList.length === 0" class="empty-state">
<view class="empty-icon">👥</view>
<view class="empty-text">暂无分销员数据</view>
</view>
<!-- 分页 -->
<view class="pagination-container">
<uni-pagination
:current="currentPage"
:pageSize="pageSize"
:total="total"
@change="handlePageChange"
show-icon
prev-text="上一页"
next-text="下一页"
/>
<view v-if="distributorList.length > 0" class="pagination">
<view class="page-info">
{{ totalDistributors }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
import { ref, computed, onMounted } from "vue"
const loading = ref(false)
const distributorList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 筛选条件
const statusOptions = ['全部状态', '启用', '禁用']
const statusIndex = ref(0)
const searchKeyword = ref('')
const searchForm = ref({
name: '',
phone: ''
// 分销员列表数据
const distributorList = ref([
{
id: 1,
name: '张三',
phone: '13800138001',
role: '初级分销员',
status: '启用',
statusClass: 'active'
},
{
id: 2,
name: '李四',
phone: '13800138002',
role: '中级分销员',
status: '启用',
statusClass: 'active'
},
{
id: 3,
name: '王五',
phone: '13800138003',
role: '高级分销员',
status: '禁用',
statusClass: 'inactive'
}
])
// 分页信息
const currentPage = ref(1)
const totalDistributors = ref(50)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalDistributors.value / pageSize.value)
})
// 初始化加载数据
onMounted(() => {
fetchDistributorList()
// 实际项目中应从接口获取分销员列表
// loadDistributorList()
})
// 获取分销员列表
function fetchDistributorList() {
loading.value = true
// 这里应该调用后端API获取数据
// 模拟数据
setTimeout(() => {
distributorList.value = [
{
id: 1,
name: '张三',
phone: '13800138001',
total_income: 1000.00,
available_income: 800.00,
is_active: 'Y'
},
{
id: 2,
name: '李四',
phone: '13900139002',
total_income: 2000.00,
available_income: 1500.00,
is_active: 'Y'
}
]
total.value = 2
loading.value = false
}, 500)
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 搜索
function handleSearch() {
currentPage.value = 1
fetchDistributorList()
}
// 分页
function handlePageChange(e) {
currentPage.value = e.current
fetchDistributorList()
}
// 新增分销员
function handleAdd() {
// 跳转到新增页面
uni.navigateTo({ url: '/pages/distributor/add' })
}
// 修改分销员
function handleEdit(item) {
// 跳转到修改页面,并传递参数
uni.navigateTo({
url: `/pages/distributor/edit?id=${item.id}`
// 添加分销员
function addDistributor() {
uni.showToast({
title: '添加分销员功能开发中',
icon: 'none'
})
}
// 编辑分销员
function editDistributor(id) {
uni.showToast({
title: '编辑分销员功能开发中',
icon: 'none'
})
}
// 删除分销员
function handleDelete(id) {
function deleteDistributor(id) {
uni.showModal({
title: '提示',
title: '删除分销员',
content: '确定要删除该分销员吗?',
success: (res) => {
success: function(res) {
if (res.confirm) {
// 这里应该调用后端API删除数据
fetchDistributorList()
uni.showToast({ title: '删除成功' })
// 实际项目中应调用接口删除分销员
const index = distributorList.value.findIndex(item => item.id === id)
if (index !== -1) {
distributorList.value.splice(index, 1)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
}
// 配置分润规则
function handleRule(id) {
// 跳转到分润规则配置页面
uni.navigateTo({
url: `/pages/distributor/rule?id=${id}`
})
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选分销员
// filterDistributorList()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索分销员
// searchDistributorList()
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的分销员
// loadDistributorPage(page)
}
// 获取姓名首字母
function getInitials(name) {
return name.charAt(0).toUpperCase()
}
</script>
<style lang="scss" scoped>
.distributor-container {
padding: 20rpx;
background-color: #f5f7fa;
min-height: 100vh;
}
/* 头部样式 */
.header {
/* #ifndef APP-NVUE */
page {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #ecf5ff;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.distributor-list-container {
flex: 1;
display: flex;
flex-direction: column;
}
.title {
font-size: 32rpx;
font-weight: 600;
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.add-btn {
display: flex;
align-items: center;
background-color: #409eff;
color: #fff;
border: none;
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 24rpx;
transition: all 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(64, 158, 255, 0.3);
color: #409eff;
font-weight: 600;
cursor: pointer;
}
.add-btn:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(64, 158, 255, 0.3);
}
.add-btn-text {
margin-left: 8rpx;
}
/* 搜索框样式 */
.search-box {
display: flex;
margin-bottom: 24rpx;
gap: 16rpx;
align-items: center;
}
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 0 16rpx;
transition: all 0.3s ease;
padding: 16rpx;
}
.search-input-wrapper:focus-within {
border-color: #409eff;
box-shadow: 0 0 0 2rpx rgba(64, 158, 255, 0.2);
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-icon {
margin-right: 12rpx;
}
.search-input {
flex: 1;
padding: 16rpx 0;
border: none;
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input::placeholder {
color: #909399;
}
.search-btn {
display: flex;
align-items: center;
background-color: #67c23a;
color: #fff;
border: none;
padding: 16rpx 24rpx;
border-radius: 8rpx;
.search-input {
font-size: 24rpx;
transition: all 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(103, 194, 58, 0.3);
color: #303133;
width: 100%;
}
.search-btn:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(103, 194, 58, 0.3);
/* 分销员列表 */
.distributor-list-section {
flex: 1;
padding: 0 16rpx;
}
.search-btn-text {
margin-left: 8rpx;
}
/* 表格容器样式 */
.table-container {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 状态标签样式 */
.status-badge {
display: inline-block;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 500;
}
.status-badge.active {
background-color: #f0f9eb;
color: #67c23a;
}
.status-badge.inactive {
background-color: #fef0f0;
color: #f56c6c;
}
/* 操作按钮样式 */
.action-buttons {
.distributor-list {
display: flex;
gap: 8rpx;
align-items: center;
flex-direction: column;
gap: 16rpx;
}
.action-btn {
.distributor-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.distributor-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
background-color: #409eff;
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 8rpx 12rpx;
border-radius: 6rpx;
margin-right: 24rpx;
}
.avatar-placeholder {
font-size: 32rpx;
font-weight: bold;
color: #fff;
}
.distributor-info {
flex: 1;
}
.distributor-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.distributor-meta {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.meta-item {
font-size: 20rpx;
transition: all 0.3s ease;
color: #606266;
}
.action-btn:active {
transform: translateY(1rpx);
.meta-item.status-active {
color: #67c23a;
}
.action-btn.edit {
.meta-item.status-inactive {
color: #909399;
}
.distributor-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 16rpx;
font-weight: 600;
cursor: pointer;
}
.edit-btn {
background-color: #ecf5ff;
color: #409eff;
}
.action-btn.delete {
.delete-btn {
background-color: #fef0f0;
color: #f56c6c;
}
.action-btn.rule {
background-color: #fdf6ec;
color: #e6a23c;
}
.action-btn text {
margin-left: 4rpx;
}
/* 分页样式 */
.pagination-container {
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
justify-content: flex-end;
padding: 16rpx 0;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.search-box {
flex-direction: column;
align-items: stretch;
}
.search-input-wrapper {
width: 100%;
}
.action-buttons {
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 80rpx;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -1,40 +1,153 @@
<template>
<view class="rule-container">
<view class="header">
<text class="title">分润规则配置</text>
<view class="distributor-rule-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">分销规则配置</view>
<view class="header-right" @click="saveRule">
<view class="save-btn">保存</view>
</view>
</view>
<view class="form-container">
<uni-forms :model="ruleForm" ref="formRef">
<uni-forms-item label="分销员ID" required>
<uni-easyinput v-model="ruleForm.distributor_id" placeholder="请输入分销员ID" type="number" />
</uni-forms-item>
<uni-forms-item label="分润比例" required>
<uni-easyinput v-model="ruleForm.profit_ratio" placeholder="请输入分润比例(%" type="number" />
</uni-forms-item>
<uni-forms-item label="规则名称" required>
<uni-easyinput v-model="ruleForm.rule_name" placeholder="请输入规则名称" />
</uni-forms-item>
<uni-forms-item label="规则描述">
<uni-easyinput v-model="ruleForm.rule_desc" placeholder="请输入规则描述" type="textarea" />
</uni-forms-item>
<uni-forms-item label="生效时间">
<uni-datetime-picker v-model="ruleForm.effective_time" type="datetime" placeholder="请选择生效时间" />
</uni-forms-item>
<uni-forms-item label="失效时间">
<uni-datetime-picker v-model="ruleForm.expire_time" type="datetime" placeholder="请选择失效时间" />
</uni-forms-item>
</uni-forms>
<!-- 规则配置 -->
<view class="rule-section">
<view class="section-title">基本规则</view>
<view class="rule-card">
<view class="rule-row">
<view class="rule-label">分销员申请审核</view>
<view class="rule-control">
<switch
:checked="ruleForm.needAudit"
@change="onAuditChange"
class="switch"
/>
</view>
</view>
<view class="rule-row">
<view class="rule-label">分销员等级制度</view>
<view class="rule-control">
<switch
:checked="ruleForm.enableLevel"
@change="onLevelChange"
class="switch"
/>
</view>
</view>
<view class="rule-row">
<view class="rule-label">分销员自购优惠</view>
<view class="rule-control">
<switch
:checked="ruleForm.enableSelfBuy"
@change="onSelfBuyChange"
class="switch"
/>
</view>
</view>
<view class="rule-row">
<view class="rule-label">分销员邀请奖励</view>
<view class="rule-control">
<switch
:checked="ruleForm.enableInviteReward"
@change="onInviteRewardChange"
class="switch"
/>
</view>
</view>
</view>
</view>
<view class="btn-container">
<button class="cancel-btn" @click="handleCancel">取消</button>
<button class="submit-btn" @click="handleSubmit">提交</button>
<!-- 等级配置 -->
<view class="rule-section" v-if="ruleForm.enableLevel">
<view class="section-title">等级配置</view>
<view class="rule-card">
<view class="level-config">
<view
v-for="(level, index) in levelList"
:key="index"
class="level-item"
>
<view class="level-header">
<view class="level-title">{{ level.name }}</view>
<view class="level-condition">
<view class="condition-label">升级条件</view>
<view class="condition-value">{{ level.condition }}</view>
</view>
</view>
<view class="level-benefit">
<view class="benefit-item">
<view class="benefit-label">分销佣金比例</view>
<view class="benefit-value">{{ level.commissionRate }}%</view>
</view>
<view class="benefit-item">
<view class="benefit-label">自购优惠比例</view>
<view class="benefit-value">{{ level.selfBuyRate }}%</view>
</view>
<view class="benefit-item">
<view class="benefit-label">邀请奖励</view>
<view class="benefit-value">¥{{ level.inviteReward.toFixed(2) }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 佣金规则 -->
<view class="rule-section">
<view class="section-title">佣金规则</view>
<view class="rule-card">
<view class="rule-row">
<view class="rule-label">佣金计算方式</view>
<view class="rule-control">
<picker
:range="commissionTypeOptions"
:value="commissionTypeIndex"
@change="onCommissionTypeChange"
class="picker"
>
<view class="picker-text">{{ commissionTypeOptions[commissionTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="rule-row">
<view class="rule-label">佣金结算周期</view>
<view class="rule-control">
<picker
:range="settlementCycleOptions"
:value="settlementCycleIndex"
@change="onSettlementCycleChange"
class="picker"
>
<view class="picker-text">{{ settlementCycleOptions[settlementCycleIndex] }}</view>
</picker>
</view>
</view>
<view class="rule-row">
<view class="rule-label">最低提现金额</view>
<view class="rule-control">
<input
v-model="ruleForm.minWithdrawAmount"
type="number"
class="form-input"
placeholder="请输入最低提现金额"
/>
</view>
</view>
<view class="rule-row">
<view class="rule-label">提现手续费</view>
<view class="rule-control">
<input
v-model="ruleForm.withdrawFee"
type="number"
class="form-input"
placeholder="请输入提现手续费比例"
/>
<view class="input-suffix">%</view>
</view>
</view>
</view>
</view>
</view>
</template>
@@ -42,104 +155,319 @@
<script setup>
import { ref, onMounted } from "vue"
const formRef = ref(null)
const distributorId = ref(uni.getStorageSync('__uni_route_query')?.id || '')
// 规则表单数据
const ruleForm = ref({
distributor_id: distributorId.value,
profit_ratio: '',
rule_name: '',
rule_desc: '',
effective_time: '',
expire_time: ''
needAudit: true,
enableLevel: true,
enableSelfBuy: true,
enableInviteReward: true,
minWithdrawAmount: 100,
withdrawFee: 0.5
})
// 初始化加载数据
// 佣金计算方式选项
const commissionTypeOptions = ['按比例', '固定金额']
const commissionTypeIndex = ref(0)
// 结算周期选项
const settlementCycleOptions = ['实时结算', '每日结算', '每周结算', '每月结算']
const settlementCycleIndex = ref(3)
// 等级配置
const levelList = ref([
{
name: '初级分销员',
condition: '邀请1人注册',
commissionRate: 10,
selfBuyRate: 5,
inviteReward: 5
},
{
name: '中级分销员',
condition: '邀请5人注册',
commissionRate: 15,
selfBuyRate: 8,
inviteReward: 8
},
{
name: '高级分销员',
condition: '邀请10人注册',
commissionRate: 20,
selfBuyRate: 10,
inviteReward: 10
}
])
onMounted(() => {
fetchRuleDetail()
// 实际项目中应从接口获取规则配置
// loadRuleConfig()
})
// 获取分润规则详情
function fetchRuleDetail() {
// 这里应该调用后端API获取数据
// 模拟数据
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 保存规则
function saveRule() {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
ruleForm.value = {
distributor_id: distributorId.value,
profit_ratio: '10',
rule_name: '默认分润规则',
rule_desc: '基础分润规则',
effective_time: new Date().toISOString(),
expire_time: ''
}
}, 500)
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
// 实际项目中应调用接口保存规则配置
// saveRuleConfig()
}, 1500)
}
// 取消
function handleCancel() {
uni.navigateBack()
// 审核开关变更
function onAuditChange(e) {
const value = e.detail.value
ruleForm.value.needAudit = value
}
// 提交
function handleSubmit() {
formRef.value.validate().then(() => {
// 这里应该调用后端API提交数据
uni.showToast({ title: '配置成功' })
uni.navigateTo({ url: '/pages/distributor/index' })
}).catch(err => {
console.log('表单验证失败', err)
})
// 等级制度开关变更
function onLevelChange(e) {
const value = e.detail.value
ruleForm.value.enableLevel = value
}
// 自购优惠开关变更
function onSelfBuyChange(e) {
const value = e.detail.value
ruleForm.value.enableSelfBuy = value
}
// 邀请奖励开关变更
function onInviteRewardChange(e) {
const value = e.detail.value
ruleForm.value.enableInviteReward = value
}
// 佣金计算方式变更
function onCommissionTypeChange(e) {
const value = e.detail.value
commissionTypeIndex.value = value
}
// 结算周期变更
function onSettlementCycleChange(e) {
const value = e.detail.value
settlementCycleIndex.value = value
}
</script>
<style lang="scss" scoped>
.rule-container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.distributor-rule-container {
flex: 1;
display: flex;
flex-direction: column;
}
.header {
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #e5e5e5;
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.title {
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
color: #303133;
}
.form-container {
.header-right {
width: 60rpx;
text-align: right;
}
.save-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 规则配置 */
.rule-section {
padding: 32rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.btn-container {
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.rule-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.rule-row {
display: flex;
gap: 20rpx;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.cancel-btn {
flex: 1;
background-color: #909399;
color: #fff;
border: none;
padding: 20rpx;
border-radius: 4rpx;
font-size: 28rpx;
.rule-row:last-child {
margin-bottom: 0;
}
.submit-btn {
flex: 1;
background-color: #409eff;
color: #fff;
border: none;
padding: 20rpx;
border-radius: 4rpx;
font-size: 28rpx;
.rule-label {
font-size: 24rpx;
color: #606266;
}
.rule-control {
position: relative;
}
.switch {
transform: scale(0.8);
}
.picker {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
min-width: 200rpx;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.form-input {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #303133;
min-width: 150rpx;
}
.input-suffix {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #606266;
}
/* 等级配置 */
.level-config {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.level-item {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.level-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.level-title {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.level-condition {
display: flex;
align-items: center;
}
.condition-label {
font-size: 18rpx;
color: #606266;
}
.condition-value {
font-size: 18rpx;
color: #409eff;
margin-left: 4rpx;
}
.level-benefit {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.benefit-item {
display: flex;
align-items: center;
}
.benefit-label {
font-size: 18rpx;
color: #606266;
}
.benefit-value {
font-size: 18rpx;
color: #303133;
margin-left: 4rpx;
}
</style>

View File

@@ -0,0 +1,433 @@
<template>
<view class="general-code-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">通用会员推广码</view>
<view class="header-right" @click="refreshCode">
<view class="refresh-btn">刷新</view>
</view>
</view>
<!-- 推广码信息 -->
<view class="code-info-section">
<view class="section-title">推广码信息</view>
<view class="info-card">
<view class="info-row">
<view class="info-label">推广码</view>
<view class="info-value">{{ generalCode }}</view>
</view>
<view class="info-row">
<view class="info-label">生成时间</view>
<view class="info-value">{{ createTime }}</view>
</view>
<view class="info-row">
<view class="info-label">有效期</view>
<view class="info-value">{{ validityPeriod }}</view>
</view>
<view class="info-row">
<view class="info-label">使用次数</view>
<view class="info-value">{{ usageCount }}</view>
</view>
</view>
</view>
<!-- 推广二维码 -->
<view class="qrcode-section">
<view class="section-title">推广二维码</view>
<view class="qrcode-card">
<view class="qrcode-image">
<!-- 模拟二维码图片 -->
<view class="mock-qrcode">
<view class="qrcode-pattern"></view>
<view class="qrcode-text">{{ generalCode }}</view>
</view>
</view>
<view class="qrcode-hint">
这是通用推广二维码适用于所有渠道的推广活动
</view>
<view class="qrcode-actions">
<view class="action-btn download-btn" @click="downloadQrcode">
下载二维码
</view>
<view class="action-btn share-btn" @click="shareQrcode">
分享二维码
</view>
</view>
</view>
</view>
<!-- 使用记录 -->
<view class="usage-record-section">
<view class="section-title">使用记录</view>
<view class="record-card">
<view class="record-header">
<view class="header-item">时间</view>
<view class="header-item">使用人</view>
<view class="header-item">操作</view>
</view>
<view class="record-list">
<view
v-for="(record, index) in usageRecords"
:key="index"
class="record-item"
>
<view class="record-cell">{{ record.time }}</view>
<view class="record-cell">{{ record.user }}</view>
<view class="record-cell">{{ record.action }}</view>
</view>
</view>
<view class="record-footer">
<view class="footer-text">
{{ totalRecords }} 条记录显示最近 {{ usageRecords.length }}
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 推广码信息
const generalCode = ref('GC202601290001')
const createTime = ref('2026-01-29 10:00:00')
const validityPeriod = ref('永久有效')
const usageCount = ref(123)
// 使用记录
const usageRecords = ref([
{
time: '2026-01-29 15:30:00',
user: '张三',
action: '扫码注册'
},
{
time: '2026-01-29 14:20:00',
user: '李四',
action: '购买会员'
},
{
time: '2026-01-29 13:10:00',
user: '王五',
action: '扫码注册'
}
])
const totalRecords = ref(100)
onMounted(() => {
// 实际项目中应从接口获取通用推广码信息
// loadGeneralCodeInfo()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 刷新推广码
function refreshCode() {
uni.showModal({
title: '刷新推广码',
content: '刷新推广码后,原推广码将失效,确定要刷新吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '刷新中...' })
setTimeout(() => {
uni.hideLoading()
// 模拟刷新后生成新的推广码
generalCode.value = 'GC' + new Date().getTime().toString().substring(0, 12)
createTime.value = new Date().toLocaleString('zh-CN')
usageCount.value = 0
uni.showToast({
title: '推广码已刷新',
icon: 'success'
})
// 实际项目中应调用接口刷新推广码
// refreshGeneralCode()
}, 1500)
}
}
})
}
// 下载二维码
function downloadQrcode() {
uni.showLoading({ title: '下载中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '二维码已下载',
icon: 'success'
})
// 实际项目中应调用接口下载二维码
// downloadGeneralQrcode()
}, 1000)
}
// 分享二维码
function shareQrcode() {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制推广链接'],
success: function(res) {
switch(res.tapIndex) {
case 0:
uni.showToast({ title: '分享到微信', duration: 1000 })
break
case 1:
uni.showToast({ title: '分享到朋友圈', duration: 1000 })
break
case 2:
uni.setClipboardData({
data: `https://example.com/register?code=${generalCode.value}`,
success: function() {
uni.showToast({ title: '推广链接已复制', duration: 1000 })
}
})
break
}
}
})
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.general-code-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.refresh-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.code-info-section,
.qrcode-section,
.usage-record-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 推广码信息 */
.info-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 24rpx;
color: #606266;
}
.info-value {
font-size: 24rpx;
color: #303133;
font-weight: 600;
}
/* 推广二维码 */
.qrcode-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.qrcode-image {
display: flex;
justify-content: center;
}
.mock-qrcode {
width: 300rpx;
height: 300rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.qrcode-pattern {
width: 240rpx;
height: 240rpx;
background-color: #303133;
margin-bottom: 16rpx;
}
.qrcode-text {
font-size: 20rpx;
color: #606266;
}
.qrcode-hint {
font-size: 20rpx;
color: #606266;
text-align: center;
line-height: 1.4;
padding: 0 24rpx;
}
.qrcode-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
cursor: pointer;
}
.download-btn {
background-color: #ecf5ff;
color: #409eff;
}
.share-btn {
background-color: #f0f9eb;
color: #67c23a;
}
/* 使用记录 */
.record-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.record-header {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.header-item {
flex: 1;
font-size: 20rpx;
font-weight: bold;
color: #606266;
text-align: center;
}
.record-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 16rpx;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: #fff;
border-radius: 8rpx;
}
.record-cell {
flex: 1;
font-size: 20rpx;
color: #303133;
text-align: center;
}
.record-footer {
text-align: center;
}
.footer-text {
font-size: 18rpx;
color: #909399;
}
</style>

633
src/pages/member/gift.vue Normal file
View File

@@ -0,0 +1,633 @@
<template>
<view class="gift-member-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">赠送会员管理</view>
<view class="header-right" @click="generateBatchCode">
<view class="batch-btn">批量生成</view>
</view>
</view>
<!-- 生成核验码 -->
<view class="generate-section">
<view class="section-title">生成核验码</view>
<view class="generate-card">
<view class="generate-row">
<view class="generate-label">会员类型</view>
<view class="generate-control">
<picker
:range="memberTypeOptions"
:value="memberTypeIndex"
@change="onMemberTypeChange"
class="picker"
>
<view class="picker-text">{{ memberTypeOptions[memberTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="generate-row">
<view class="generate-label">生成数量</view>
<view class="generate-control">
<input
v-model="generateCount"
type="number"
class="count-input"
placeholder="请输入生成数量"
min="1"
max="100"
/>
</view>
</view>
<view class="generate-row">
<view class="generate-label">有效期</view>
<view class="generate-control">
<input
v-model="validityPeriod"
class="period-input"
placeholder="请选择有效期"
@click="showDatePicker"
/>
</view>
</view>
<view class="generate-action">
<view class="action-btn primary-btn" @click="generateCode">
生成核验码
</view>
</view>
</view>
</view>
<!-- 核验码列表 -->
<view class="code-list-section">
<view class="section-title">核验码列表</view>
<view class="code-list">
<view
v-for="(code, index) in codeList"
:key="index"
class="code-item"
>
<view class="code-header">
<view class="code-value">{{ code.code }}</view>
<view class="code-status" :class="code.statusClass">{{ code.status }}</view>
</view>
<view class="code-body">
<view class="code-info">
<view class="info-item">
<view class="info-label">会员类型</view>
<view class="info-value">{{ code.memberType }}</view>
</view>
<view class="info-item">
<view class="info-label">生成时间</view>
<view class="info-value">{{ code.createTime }}</view>
</view>
<view class="info-item">
<view class="info-label">有效期至</view>
<view class="info-value">{{ code.validUntil }}</view>
</view>
<view class="info-item" v-if="code.usedTime">
<view class="info-label">使用时间</view>
<view class="info-value">{{ code.usedTime }}</view>
</view>
</view>
</view>
<view class="code-footer">
<view class="code-actions">
<view class="action-btn copy-btn" @click="copyCode(code.code)">
复制
</view>
<view class="action-btn delete-btn" @click="deleteCode(code.id)">
删除
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="codeList.length === 0" class="empty-state">
<view class="empty-icon">🎁</view>
<view class="empty-text">暂无核验码数据</view>
</view>
<!-- 分页 -->
<view v-if="codeList.length > 0" class="pagination">
<view class="page-info">
{{ totalCodes }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 会员类型选项
const memberTypeOptions = ['月度会员', '季度会员', '年度会员']
const memberTypeIndex = ref(0)
// 生成数量
const generateCount = ref('1')
// 有效期
const validityPeriod = ref('')
// 核验码列表数据
const codeList = ref([
{
id: 1,
code: 'GC20260129001',
memberType: '月度会员',
createTime: '2026-01-29 15:30:00',
validUntil: '2026-02-29 23:59:59',
status: '未使用',
statusClass: 'status-unused'
},
{
id: 2,
code: 'GC20260129002',
memberType: '季度会员',
createTime: '2026-01-29 14:20:00',
validUntil: '2026-04-29 23:59:59',
status: '已使用',
statusClass: 'status-used',
usedTime: '2026-01-29 14:30:00'
},
{
id: 3,
code: 'GC20260129003',
memberType: '年度会员',
createTime: '2026-01-29 13:10:00',
validUntil: '2027-01-29 23:59:59',
status: '已过期',
statusClass: 'status-expired'
}
])
// 分页信息
const currentPage = ref(1)
const totalCodes = ref(100)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalCodes.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取核验码列表
// loadCodeList()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 批量生成
function generateBatchCode() {
uni.showModal({
title: '批量生成核验码',
content: '请输入要生成的核验码数量',
editable: true,
placeholderText: '请输入数量',
success: function(res) {
if (res.confirm && res.content) {
const count = parseInt(res.content)
if (isNaN(count) || count <= 0 || count > 100) {
uni.showToast({
title: '请输入1-100之间的数字',
icon: 'none'
})
return
}
uni.showLoading({ title: '生成中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: `成功生成${count}个核验码`,
icon: 'success'
})
// 实际项目中应调用接口批量生成核验码
// generateBatchCodes(count)
}, 1500)
}
}
})
}
// 会员类型变更
function onMemberTypeChange(e) {
const value = e.detail.value
memberTypeIndex.value = value
}
// 显示日期选择器
function showDatePicker() {
const now = new Date()
uni.showDatePicker({
year: now.getFullYear(),
month: now.getMonth(),
day: now.getDate(),
success: function(res) {
validityPeriod.value = `${res.year}-${res.month + 1}-${res.day}`
}
})
}
// 生成核验码
function generateCode() {
if (!generateCount.value || parseInt(generateCount.value) <= 0) {
uni.showToast({
title: '请输入有效的生成数量',
icon: 'none'
})
return
}
if (!validityPeriod.value) {
uni.showToast({
title: '请选择有效期',
icon: 'none'
})
return
}
uni.showLoading({ title: '生成中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '核验码生成成功',
icon: 'success'
})
// 实际项目中应调用接口生成核验码
// generateVerificationCode()
}, 1500)
}
// 复制核验码
function copyCode(code) {
uni.setClipboardData({
data: code,
success: function() {
uni.showToast({
title: '核验码已复制',
icon: 'success'
})
}
})
}
// 删除核验码
function deleteCode(id) {
uni.showModal({
title: '删除核验码',
content: '确定要删除该核验码吗?',
success: function(res) {
if (res.confirm) {
// 实际项目中应调用接口删除核验码
const index = codeList.value.findIndex(item => item.id === id)
if (index !== -1) {
codeList.value.splice(index, 1)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的核验码
// loadCodePage(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.gift-member-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 120rpx;
text-align: right;
}
.batch-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.generate-section,
.code-list-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 生成核验码 */
.generate-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.generate-row {
display: flex;
flex-direction: column;
margin-bottom: 24rpx;
}
.generate-row:last-child {
margin-bottom: 0;
}
.generate-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.generate-control {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.count-input,
.period-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
.generate-action {
margin-top: 24rpx;
}
.action-btn {
padding: 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 600;
text-align: center;
cursor: pointer;
}
.primary-btn {
background-color: #409eff;
color: #fff;
}
/* 核验码列表 */
.code-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.code-item {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.code-value {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.code-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-unused {
color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
.status-used {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.status-expired {
color: #909399;
background-color: rgba(144, 147, 153, 0.1);
}
.code-body {
margin-bottom: 16rpx;
}
.code-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
display: flex;
font-size: 20rpx;
}
.info-label {
color: #606266;
width: 120rpx;
}
.info-value {
color: #303133;
flex: 1;
}
.code-footer {
border-top: 1rpx solid #e0e0e0;
padding-top: 16rpx;
}
.code-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
}
.copy-btn {
background-color: #ecf5ff;
color: #409eff;
}
.delete-btn {
background-color: #fef0f0;
color: #f56c6c;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

623
src/pages/member/order.vue Normal file
View File

@@ -0,0 +1,623 @@
<template>
<view class="member-order-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">会员订单管理</view>
<view class="header-right" @click="exportOrders">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">会员类型</view>
<view class="filter-control">
<picker
:range="memberTypeOptions"
:value="memberTypeIndex"
@change="onMemberTypeChange"
class="picker"
>
<view class="picker-text">{{ memberTypeOptions[memberTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入订单号或学员姓名"
@input="onSearch"
/>
</view>
</view>
</view>
</view>
<!-- 订单列表 -->
<view class="order-list-section">
<view class="section-title">订单列表</view>
<view class="order-list">
<view
v-for="(order, index) in orderList"
:key="index"
class="order-item"
>
<view class="order-header">
<view class="order-code">{{ order.code }}</view>
<view class="order-status" :class="order.statusClass">{{ order.status }}</view>
</view>
<view class="order-body">
<view class="order-info">
<view class="info-item">
<view class="info-label">学员姓名</view>
<view class="info-value">{{ order.studentName }}</view>
</view>
<view class="info-item">
<view class="info-label">会员类型</view>
<view class="info-value">{{ order.memberType }}</view>
</view>
<view class="info-item">
<view class="info-label">订单金额</view>
<view class="info-value price">{{ order.amount }}</view>
</view>
<view class="info-item">
<view class="info-label">下单时间</view>
<view class="info-value">{{ order.createTime }}</view>
</view>
</view>
</view>
<view class="order-footer">
<view class="order-actions">
<view v-if="order.status === '待处理'" class="action-btn process-btn" @click="processOrder(order.id)">
处理
</view>
<view class="action-btn detail-btn" @click="viewOrderDetail(order.id)">
详情
</view>
<view class="action-btn refund-btn" @click="refundOrder(order.id)">
退款
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="orderList.length === 0" class="empty-state">
<view class="empty-icon">🛒</view>
<view class="empty-text">暂无订单数据</view>
</view>
<!-- 分页 -->
<view v-if="orderList.length > 0" class="pagination">
<view class="page-info">
{{ totalOrders }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 筛选条件
const statusOptions = ['全部状态', '待处理', '已完成', '已退款', '异常']
const statusIndex = ref(0)
const memberTypeOptions = ['全部类型', '月度会员', '季度会员', '年度会员']
const memberTypeIndex = ref(0)
const searchKeyword = ref('')
// 订单列表数据
const orderList = ref([
{
id: 1,
code: 'MO20260129001',
studentName: '张三',
memberType: '年度会员',
amount: '¥299.90',
createTime: '2026-01-29 15:30:00',
status: '待处理',
statusClass: 'status-pending'
},
{
id: 2,
code: 'MO20260129002',
studentName: '李四',
memberType: '月度会员',
amount: '¥39.90',
createTime: '2026-01-29 14:20:00',
status: '已完成',
statusClass: 'status-completed'
},
{
id: 3,
code: 'MO20260129003',
studentName: '王五',
memberType: '季度会员',
amount: '¥99.90',
createTime: '2026-01-29 13:10:00',
status: '异常',
statusClass: 'status-exception'
},
{
id: 4,
code: 'MO20260129004',
studentName: '赵六',
memberType: '年度会员',
amount: '¥299.90',
createTime: '2026-01-29 12:00:00',
status: '已退款',
statusClass: 'status-refunded'
}
])
// 分页信息
const currentPage = ref(1)
const totalOrders = ref(100)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalOrders.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取订单列表
// loadOrderList()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出订单
function exportOrders() {
uni.showModal({
title: '导出订单',
content: '确定要导出当前筛选条件下的订单吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 实际项目中应调用接口导出订单
// exportOrderList()
}, 1500)
}
}
})
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选订单
// filterOrderList()
}
// 会员类型变更
function onMemberTypeChange(e) {
const value = e.detail.value
memberTypeIndex.value = value
// 实际项目中应根据会员类型筛选订单
// filterOrderList()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索订单
// searchOrderList()
}
// 处理订单
function processOrder(id) {
uni.showModal({
title: '处理订单',
content: '确定要处理该订单吗?',
success: function(res) {
if (res.confirm) {
// 实际项目中应调用接口处理订单
const order = orderList.value.find(item => item.id === id)
if (order) {
order.status = '已完成'
order.statusClass = 'status-completed'
}
uni.showToast({
title: '订单处理成功',
icon: 'success'
})
}
}
})
}
// 查看订单详情
function viewOrderDetail(id) {
uni.showToast({
title: '查看订单详情功能开发中',
icon: 'none'
})
}
// 退款订单
function refundOrder(id) {
uni.showModal({
title: '退款订单',
content: '确定要退款该订单吗?',
success: function(res) {
if (res.confirm) {
// 实际项目中应调用接口退款订单
const order = orderList.value.find(item => item.id === id)
if (order) {
order.status = '已退款'
order.statusClass = 'status-refunded'
}
uni.showToast({
title: '订单退款成功',
icon: 'success'
})
}
}
})
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的订单
// loadOrderPage(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.member-order-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 200rpx;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
/* 订单列表 */
.order-list-section {
flex: 1;
padding: 0 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.order-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.order-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.order-code {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.order-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-pending {
color: #e6a23c;
background-color: rgba(230, 162, 60, 0.1);
}
.status-completed {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.status-exception {
color: #f56c6c;
background-color: rgba(245, 108, 108, 0.1);
}
.status-refunded {
color: #909399;
background-color: rgba(144, 147, 153, 0.1);
}
.order-body {
margin-bottom: 16rpx;
}
.order-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
display: flex;
font-size: 20rpx;
}
.info-label {
color: #606266;
width: 120rpx;
}
.info-value {
color: #303133;
flex: 1;
}
.info-value.price {
color: #f56c6c;
font-weight: 600;
}
.order-footer {
border-top: 1rpx solid #e0e0e0;
padding-top: 16rpx;
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
}
.action-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 16rpx;
font-weight: 600;
cursor: pointer;
}
.process-btn {
background-color: #ecf5ff;
color: #409eff;
}
.detail-btn {
background-color: #f0f9eb;
color: #67c23a;
}
.refund-btn {
background-color: #fef0f0;
color: #f56c6c;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,548 @@
<template>
<view class="personal-gift-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">赠送会员操作</view>
<view class="header-right" @click="viewHistory">
<view class="history-btn">历史</view>
</view>
</view>
<!-- 可用额度 -->
<view class="quota-section">
<view class="section-title">可用额度</view>
<view class="quota-card">
<view class="quota-info">
<view class="quota-label">本月可用赠送额度</view>
<view class="quota-value">{{ availableQuota }}</view>
</view>
<view class="quota-hint">
剩余 {{ availableQuota }} 个赠送会员名额请合理使用
</view>
</view>
</view>
<!-- 生成核验码 -->
<view class="generate-section">
<view class="section-title">生成核验码</view>
<view class="generate-card">
<view class="generate-row">
<view class="generate-label">会员类型</view>
<view class="generate-control">
<picker
:range="memberTypeOptions"
:value="memberTypeIndex"
@change="onMemberTypeChange"
class="picker"
>
<view class="picker-text">{{ memberTypeOptions[memberTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="generate-row">
<view class="generate-label">生成数量</view>
<view class="generate-control">
<input
v-model="generateCount"
type="number"
class="count-input"
placeholder="请输入生成数量"
min="1"
:max="availableQuota"
/>
</view>
</view>
<view class="generate-action">
<view class="action-btn primary-btn" @click="generateCode">
生成核验码
</view>
</view>
</view>
</view>
<!-- 核验码列表 -->
<view class="code-list-section">
<view class="section-title">我的核验码</view>
<view class="code-list">
<view
v-for="(code, index) in codeList"
:key="index"
class="code-item"
>
<view class="code-header">
<view class="code-value">{{ code.code }}</view>
<view class="code-status" :class="code.statusClass">{{ code.status }}</view>
</view>
<view class="code-body">
<view class="code-info">
<view class="info-item">
<view class="info-label">会员类型</view>
<view class="info-value">{{ code.memberType }}</view>
</view>
<view class="info-item">
<view class="info-label">生成时间</view>
<view class="info-value">{{ code.createTime }}</view>
</view>
<view class="info-item">
<view class="info-label">有效期至</view>
<view class="info-value">{{ code.validUntil }}</view>
</view>
</view>
</view>
<view class="code-footer">
<view class="code-actions">
<view class="action-btn copy-btn" @click="copyCode(code.code)">
复制
</view>
<view class="action-btn share-btn" @click="shareCode(code.code)">
分享
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="codeList.length === 0" class="empty-state">
<view class="empty-icon">🎁</view>
<view class="empty-text">暂无核验码数据</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 可用额度
const availableQuota = ref(10)
// 会员类型选项
const memberTypeOptions = ['月度会员', '季度会员', '年度会员']
const memberTypeIndex = ref(0)
// 生成数量
const generateCount = ref('1')
// 核验码列表数据
const codeList = ref([
{
id: 1,
code: 'PG20260129001',
memberType: '月度会员',
createTime: '2026-01-29 15:30:00',
validUntil: '2026-02-29 23:59:59',
status: '未使用',
statusClass: 'status-unused'
},
{
id: 2,
code: 'PG20260129002',
memberType: '季度会员',
createTime: '2026-01-29 14:20:00',
validUntil: '2026-04-29 23:59:59',
status: '已使用',
statusClass: 'status-used'
}
])
onMounted(() => {
// 实际项目中应从接口获取可用额度和核验码列表
// loadQuotaAndCodes()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 查看历史
function viewHistory() {
uni.showToast({
title: '查看历史功能开发中',
icon: 'none'
})
}
// 会员类型变更
function onMemberTypeChange(e) {
const value = e.detail.value
memberTypeIndex.value = value
}
// 生成核验码
function generateCode() {
const count = parseInt(generateCount.value)
if (!count || count <= 0) {
uni.showToast({
title: '请输入有效的生成数量',
icon: 'none'
})
return
}
if (count > availableQuota) {
uni.showToast({
title: '生成数量超过可用额度',
icon: 'none'
})
return
}
uni.showLoading({ title: '生成中...' })
setTimeout(() => {
uni.hideLoading()
// 模拟生成核验码
const newCode = {
id: codeList.value.length + 1,
code: 'PG' + new Date().getTime().toString().substring(0, 10),
memberType: memberTypeOptions[memberTypeIndex.value],
createTime: new Date().toLocaleString('zh-CN'),
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('zh-CN'),
status: '未使用',
statusClass: 'status-unused'
}
codeList.value.unshift(newCode)
availableQuota.value -= count
uni.showToast({
title: '核验码生成成功',
icon: 'success'
})
// 实际项目中应调用接口生成核验码
// generateVerificationCode(count)
}, 1500)
}
// 复制核验码
function copyCode(code) {
uni.setClipboardData({
data: code,
success: function() {
uni.showToast({
title: '核验码已复制',
icon: 'success'
})
}
})
}
// 分享核验码
function shareCode(code) {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
success: function(res) {
switch(res.tapIndex) {
case 0:
uni.showToast({ title: '分享到微信', duration: 1000 })
break
case 1:
uni.showToast({ title: '分享到朋友圈', duration: 1000 })
break
case 2:
uni.setClipboardData({
data: `https://example.com/activate?code=${code}`,
success: function() {
uni.showToast({ title: '链接已复制', duration: 1000 })
}
})
break
}
}
})
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.personal-gift-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.history-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.quota-section,
.generate-section,
.code-list-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 可用额度 */
.quota-card {
background-color: #ecf5ff;
border-radius: 12rpx;
padding: 24rpx;
}
.quota-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.quota-label {
font-size: 24rpx;
color: #409eff;
}
.quota-value {
font-size: 32rpx;
font-weight: bold;
color: #409eff;
}
.quota-hint {
font-size: 20rpx;
color: #606266;
line-height: 1.4;
}
/* 生成核验码 */
.generate-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.generate-row {
display: flex;
flex-direction: column;
margin-bottom: 24rpx;
}
.generate-row:last-child {
margin-bottom: 0;
}
.generate-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.generate-control {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.count-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
.generate-action {
margin-top: 24rpx;
}
.action-btn {
padding: 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 600;
text-align: center;
cursor: pointer;
}
.primary-btn {
background-color: #409eff;
color: #fff;
}
/* 核验码列表 */
.code-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.code-item {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.code-value {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.code-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-unused {
color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
.status-used {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.code-body {
margin-bottom: 16rpx;
}
.code-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
display: flex;
font-size: 20rpx;
}
.info-label {
color: #606266;
width: 120rpx;
}
.info-value {
color: #303133;
flex: 1;
}
.code-footer {
border-top: 1rpx solid #e0e0e0;
padding-top: 16rpx;
}
.code-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
}
.copy-btn {
background-color: #ecf5ff;
color: #409eff;
}
.share-btn {
background-color: #f0f9eb;
color: #67c23a;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
</style>

View File

@@ -0,0 +1,548 @@
<template>
<view class="personal-order-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">订单查看</view>
<view class="header-right" @click="exportOrders">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
:range="timeRangeOptions"
:value="timeRangeIndex"
@change="onTimeRangeChange"
class="picker"
>
<view class="picker-text">{{ timeRangeOptions[timeRangeIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">订单状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 订单概览 -->
<view class="overview-section">
<view class="section-title">订单概览</view>
<view class="overview-card">
<view class="overview-row">
<view class="overview-item">
<view class="overview-value">{{ totalOrders }}</view>
<view class="overview-label">总订单数</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ completedOrders }}</view>
<view class="overview-label">已完成</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ totalAmount }}</view>
<view class="overview-label">总金额</view>
</view>
<view class="overview-item">
<view class="overview-value">{{ totalProfit }}</view>
<view class="overview-label">总收益</view>
</view>
</view>
</view>
</view>
<!-- 订单列表 -->
<view class="order-list-section">
<view class="section-title">订单列表</view>
<view class="order-list">
<view
v-for="(order, index) in orderList"
:key="index"
class="order-item"
>
<view class="order-header">
<view class="order-code">{{ order.code }}</view>
<view class="order-status" :class="order.statusClass">{{ order.status }}</view>
</view>
<view class="order-body">
<view class="order-info">
<view class="info-item">
<view class="info-label">学员姓名</view>
<view class="info-value">{{ order.studentName }}</view>
</view>
<view class="info-item">
<view class="info-label">会员类型</view>
<view class="info-value">{{ order.memberType }}</view>
</view>
<view class="info-item">
<view class="info-label">订单金额</view>
<view class="info-value price">{{ order.amount }}</view>
</view>
<view class="info-item">
<view class="info-label">下单时间</view>
<view class="info-value">{{ order.createTime }}</view>
</view>
<view class="info-item">
<view class="info-label">收益</view>
<view class="info-value profit">{{ order.profit }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="orderList.length === 0" class="empty-state">
<view class="empty-icon">📋</view>
<view class="empty-text">暂无订单数据</view>
</view>
<!-- 分页 -->
<view v-if="orderList.length > 0" class="pagination">
<view class="page-info">
{{ totalOrders }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 筛选条件
const timeRangeOptions = ['全部时间', '今日', '本周', '本月', '本季度', '本年']
const timeRangeIndex = ref(3)
const statusOptions = ['全部状态', '待处理', '已完成', '已退款']
const statusIndex = ref(0)
// 订单概览数据
const totalOrders = ref(100)
const completedOrders = ref(85)
const totalAmount = ref('¥12,345.67')
const totalProfit = ref('¥1,234.56')
// 订单列表数据
const orderList = ref([
{
id: 1,
code: 'MO20260129001',
studentName: '张三',
memberType: '年度会员',
amount: '¥299.90',
profit: '¥29.99',
createTime: '2026-01-29 15:30:00',
status: '已完成',
statusClass: 'status-completed'
},
{
id: 2,
code: 'MO20260129002',
studentName: '李四',
memberType: '月度会员',
amount: '¥39.90',
profit: '¥3.99',
createTime: '2026-01-29 14:20:00',
status: '已完成',
statusClass: 'status-completed'
},
{
id: 3,
code: 'MO20260129003',
studentName: '王五',
memberType: '季度会员',
amount: '¥99.90',
profit: '¥9.99',
createTime: '2026-01-29 13:10:00',
status: '待处理',
statusClass: 'status-pending'
}
])
// 分页信息
const currentPage = ref(1)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalOrders.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取订单数据
// loadOrderData()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出订单
function exportOrders() {
uni.showModal({
title: '导出订单',
content: '确定要导出当前筛选条件下的订单吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 实际项目中应调用接口导出订单
// exportOrderData()
}, 1500)
}
}
})
}
// 时间范围变更
function onTimeRangeChange(e) {
const value = e.detail.value
timeRangeIndex.value = value
// 实际项目中应根据时间范围筛选订单
// filterOrderData()
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选订单
// filterOrderData()
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的订单
// loadOrderPage(page)
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.personal-order-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.filter-section,
.overview-section,
.order-list-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 筛选条件 */
.filter-row {
display: flex;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 订单概览 */
.overview-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.overview-row {
display: flex;
justify-content: space-between;
}
.overview-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.overview-value {
font-size: 28rpx;
font-weight: bold;
color: #303133;
}
.overview-label {
font-size: 18rpx;
color: #606266;
}
/* 订单列表 */
.order-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.order-item {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.order-code {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.order-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-pending {
color: #e6a23c;
background-color: rgba(230, 162, 60, 0.1);
}
.status-completed {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.status-refunded {
color: #909399;
background-color: rgba(144, 147, 153, 0.1);
}
.order-body {
margin-bottom: 16rpx;
}
.order-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
display: flex;
font-size: 20rpx;
}
.info-label {
color: #606266;
width: 120rpx;
}
.info-value {
color: #303133;
flex: 1;
}
.info-value.price,
.info-value.profit {
color: #f56c6c;
font-weight: 600;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -55,9 +55,10 @@
<view class="form-label">字段类型</view>
<view class="form-control">
<picker
v-model="field.type"
:value="getFieldTypeIndex(field.type)"
:range="fieldTypes"
:range-key="'label'"
@change="onFieldTypeChange"
class="form-picker"
>
<view class="picker-display">{{ getFieldTypeName(field.type) }}</view>
@@ -82,7 +83,8 @@
<view class="form-label">是否必填</view>
<view class="form-control">
<switch
v-model="field.required"
:checked="field.required"
@change="onRequiredChange($event, index)"
class="form-switch"
active-color="#409eff"
/>
@@ -113,7 +115,8 @@
<view class="rule-label">启用字段筛选</view>
<view class="rule-control">
<switch
v-model="filterRules.enabled"
:checked="filterRules.enabled"
@change="onFilterEnabledChange"
class="form-switch"
active-color="#409eff"
/>
@@ -123,18 +126,19 @@
<view class="rule-item">
<view class="rule-label">默认显示字段</view>
<view class="rule-control">
<picker
v-model="filterRules.defaultFields"
:range="availableFields"
:range-key="'name'"
mode="multiSelector"
class="form-picker"
>
<view class="picker-display">
{{ filterRules.defaultFields.length }} 个字段
<picker
:value="getDefaultFieldsIndex()"
:range="availableFields"
:range-key="'name'"
mode="multiSelector"
@change="onDefaultFieldsChange"
class="form-picker"
>
<view class="picker-display">
{{ filterRules.defaultFields.length }} 个字段
</view>
</picker>
</view>
</picker>
</view>
</view>
</view>
</view>
@@ -188,6 +192,9 @@
defaultFields: [0, 1]
})
// 当前编辑的字段索引
const currentEditIndex = ref(null)
// 计算可用字段
const availableFields = computed(() => {
return customFields.value.map((field, index) => ({
@@ -237,6 +244,39 @@
})
}
// 获取字段类型索引
function getFieldTypeIndex(type) {
return fieldTypes.value.findIndex(item => item.value === type)
}
// 处理字段类型变更
function onFieldTypeChange(e) {
const index = e.detail.value
if (currentEditIndex.value !== null) {
customFields.value[currentEditIndex.value].type = fieldTypes.value[index].value
}
}
// 获取默认字段索引
function getDefaultFieldsIndex() {
return filterRules.value.defaultFields
}
// 处理默认字段变更
function onDefaultFieldsChange(e) {
filterRules.value.defaultFields = e.detail.value
}
// 处理必填状态变更
function onRequiredChange(e, index) {
customFields.value[index].required = e.detail.value
}
// 处理筛选启用状态变更
function onFilterEnabledChange(e) {
filterRules.value.enabled = e.detail.value
}
// 移动字段
function moveField(index, direction) {
if (direction === 'up' && index > 0) {

View File

@@ -69,16 +69,16 @@
<view class="tip-item" :class="{ active: passwordForm.newPassword.length >= 8 }">
密码长度至少8位
</view>
<view class="tip-item" :class="{ active: /[A-Z]/.test(passwordForm.newPassword) }">
<view class="tip-item" :class="{ active: hasUpperCase }">
包含大写字母
</view>
<view class="tip-item" :class="{ active: /[a-z]/.test(passwordForm.newPassword) }">
<view class="tip-item" :class="{ active: hasLowerCase }">
包含小写字母
</view>
<view class="tip-item" :class="{ active: /[0-9]/.test(passwordForm.newPassword) }">
<view class="tip-item" :class="{ active: hasNumber }">
包含数字
</view>
<view class="tip-item" :class="{ active: /[!@#$%^&*(),.?":{}|<>]/.test(passwordForm.newPassword) }">
<view class="tip-item" :class="{ active: hasSpecialChar }">
包含特殊字符
</view>
</view>
@@ -119,7 +119,13 @@
confirmPassword: ''
})
// 显示密码
// 密码强度计算属性
const hasUpperCase = computed(() => /[A-Z]/.test(passwordForm.value.newPassword))
const hasLowerCase = computed(() => /[a-z]/.test(passwordForm.value.newPassword))
const hasNumber = computed(() => /[0-9]/.test(passwordForm.value.newPassword))
const hasSpecialChar = computed(() => /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(passwordForm.value.newPassword))
// 密码强度文本显示密码
const showPassword = ref(false)
// 切换密码显示状态

View File

@@ -67,8 +67,9 @@
<view class="form-label">性别</view>
<view class="form-control">
<picker
v-model="userInfo.gender"
:value="getGenderIndex()"
:range="genders"
@change="onGenderChange"
class="form-picker"
>
<view class="picker-display">{{ userInfo.gender || '请选择性别' }}</view>
@@ -165,6 +166,17 @@
uni.navigateBack({ delta: 1 })
}
// 获取性别索引
function getGenderIndex() {
return genders.indexOf(userInfo.value.gender)
}
// 处理性别变更
function onGenderChange(e) {
const index = e.detail.value
userInfo.value.gender = genders[index]
}
// 选择头像
function chooseAvatar() {
uni.chooseImage({

View File

@@ -0,0 +1,589 @@
<template>
<view class="distribution-stats-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">分销业绩统计</view>
<view class="header-right" @click="exportData">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker-view
:value="dateRangeValue"
@change="onDateRangeChange"
class="date-picker"
>
<picker-view-column>
<view v-for="(date, index) in dateOptions" :key="index">{{ date }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
<view class="filter-item">
<view class="filter-label">分销员</view>
<view class="filter-control">
<picker
:range="distributorOptions"
:value="distributorIndex"
@change="onDistributorChange"
class="picker"
>
<view class="picker-text">{{ distributorOptions[distributorIndex] }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 核心指标 -->
<view class="metrics-section">
<view class="metrics-grid">
<view class="metric-card">
<view class="metric-icon">💰</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.totalProfit }}</view>
<view class="metric-label">总提成</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">📱</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.scanCount }}</view>
<view class="metric-label">扫码数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">👥</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.paidCount }}</view>
<view class="metric-label">付费人数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">📈</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.conversionRate }}</view>
<view class="metric-label">转化率</view>
</view>
</view>
</view>
</view>
<!-- 趋势图表 -->
<view class="chart-section">
<view class="section-title">业绩趋势</view>
<view class="chart-container">
<!-- 模拟图表实际项目中应使用图表库 -->
<view class="mock-chart">
<view class="chart-header">
<view class="chart-title">近7天业绩趋势</view>
</view>
<view class="chart-body">
<view class="chart-bars">
<view v-for="(day, index) in trendData" :key="index" class="chart-bar">
<view class="bar-container">
<view class="bar" :style="{ height: day.value + '%' }"></view>
</view>
<view class="bar-label">{{ day.date }}</view>
<view class="bar-value">{{ day.amount }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 排行榜 -->
<view class="ranking-section">
<view class="section-title">分销员排行榜</view>
<view class="ranking-list">
<view
v-for="(item, index) in rankingData"
:key="index"
class="ranking-item"
:class="{ 'top-three': index < 3 }"
>
<view class="ranking-number">{{ index + 1 }}</view>
<view class="ranking-info">
<view class="ranking-name">{{ item.name }}</view>
<view class="ranking-stats">
<view class="stat-item">
<view class="stat-label">提成</view>
<view class="stat-value">{{ item.profit }}</view>
</view>
<view class="stat-item">
<view class="stat-label">扫码</view>
<view class="stat-value">{{ item.scans }}</view>
</view>
</view>
</view>
<view class="ranking-arrow" @click="viewDistributorDetail(item.id)">
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 筛选条件
const dateOptions = ['全部时间', '近7天', '近30天', '近90天', '今年', '去年']
const dateRangeValue = ref([0])
const distributorOptions = ['全部分销员', '张三', '李四', '王五', '赵六']
const distributorIndex = ref(0)
// 核心指标
const metrics = ref({
totalProfit: '¥12,345.67',
scanCount: 1234,
paidCount: 456,
conversionRate: '36.9%'
})
// 趋势数据
const trendData = ref([
{ date: '1/23', amount: '¥1,234', value: 30 },
{ date: '1/24', amount: '¥1,567', value: 40 },
{ date: '1/25', amount: '¥987', value: 25 },
{ date: '1/26', amount: '¥2,109', value: 55 },
{ date: '1/27', amount: '¥1,876', value: 48 },
{ date: '1/28', amount: '¥2,345', value: 60 },
{ date: '1/29', amount: '¥1,987', value: 50 }
])
// 排行榜数据
const rankingData = ref([
{ id: 1, name: '张三', profit: '¥3,456.78', scans: 345 },
{ id: 2, name: '李四', profit: '¥2,890.12', scans: 298 },
{ id: 3, name: '王五', profit: '¥2,123.45', scans: 215 },
{ id: 4, name: '赵六', profit: '¥1,876.54', scans: 189 },
{ id: 5, name: '钱七', profit: '¥1,543.21', scans: 156 }
])
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出数据
function exportData() {
uni.showModal({
title: '导出数据',
content: '是否导出当前统计数据?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '导出成功', icon: 'success' })
}, 1500)
}
}
})
}
// 日期范围变化
function onDateRangeChange(e) {
const value = e.detail.value
dateRangeValue.value = value
// 实际项目中应根据选择的日期范围重新获取数据
refreshData()
}
// 分销员变化
function onDistributorChange(e) {
const value = e.detail.value
distributorIndex.value = value
// 实际项目中应根据选择的分销员重新获取数据
refreshData()
}
// 刷新数据
function refreshData() {
uni.showLoading({ title: '加载中...' })
setTimeout(() => {
uni.hideLoading()
// 实际项目中应从接口获取数据
}, 1000)
}
// 查看分销员详情
function viewDistributorDetail(distributorId) {
uni.navigateTo({
url: `/pages/distributor/detail?id=${distributorId}`
})
}
onMounted(() => {
// 实际项目中应从接口获取数据
// loadStatsData()
})
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.distribution-stats-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.date-picker {
height: 150rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 核心指标 */
.metrics-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.metric-card {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.metric-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 24rpx;
color: #606266;
}
/* 图表区域 */
.chart-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.chart-container {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.mock-chart {
display: flex;
flex-direction: column;
}
.chart-header {
margin-bottom: 24rpx;
}
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
text-align: center;
}
.chart-bars {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 300rpx;
gap: 16rpx;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.bar-container {
flex: 1;
width: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
margin-bottom: 12rpx;
}
.bar {
width: 40rpx;
background-color: #409eff;
border-radius: 4rpx 4rpx 0 0;
transition: height 0.5s ease;
}
.bar-label {
font-size: 20rpx;
color: #606266;
margin-bottom: 8rpx;
}
.bar-value {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
/* 排行榜 */
.ranking-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 32rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.ranking-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.ranking-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
transition: all 0.3s ease;
}
.ranking-item:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.ranking-item.top-three {
background-color: #f0f9eb;
border-left: 8rpx solid #67c23a;
}
.ranking-number {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
background-color: #409eff;
color: #fff;
font-size: 24rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.ranking-item.top-three .ranking-number {
background-color: #67c23a;
}
.ranking-info {
flex: 1;
}
.ranking-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.ranking-stats {
display: flex;
gap: 32rpx;
}
.stat-item {
display: flex;
align-items: center;
}
.stat-label {
font-size: 20rpx;
color: #606266;
margin-right: 8rpx;
}
.stat-value {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
.ranking-arrow {
font-size: 32rpx;
color: #909399;
margin-left: 16rpx;
}
/* 响应式设计 */
@media screen and (min-width: 500px) {
.distribution-stats-container {
max-width: 900px;
margin: 0 auto;
}
.metrics-grid {
grid-template-columns: repeat(4, 1fr);
}
.filter-row {
gap: 24rpx;
}
.chart-bars {
height: 400rpx;
}
.bar {
width: 60rpx;
}
}
</style>

743
src/pages/stats/member.vue Normal file
View File

@@ -0,0 +1,743 @@
<template>
<view class="member-stats-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">会员运营统计</view>
<view class="header-right" @click="exportData">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
:range="dateOptions"
:value="dateIndex"
@change="onDateChange"
class="picker"
>
<view class="picker-text">{{ dateOptions[dateIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">会员类型</view>
<view class="filter-control">
<picker
:range="memberTypeOptions"
:value="memberTypeIndex"
@change="onMemberTypeChange"
class="picker"
>
<view class="picker-text">{{ memberTypeOptions[memberTypeIndex] }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 核心指标 -->
<view class="metrics-section">
<view class="metrics-grid">
<view class="metric-card">
<view class="metric-icon">🛒</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.totalOrders }}</view>
<view class="metric-label">总订单数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">💰</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.totalAmount }}</view>
<view class="metric-label">总金额</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">📈</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.averageAmount }}</view>
<view class="metric-label">平均金额</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon"></view>
<view class="metric-content">
<view class="metric-value">{{ metrics.refundRate }}</view>
<view class="metric-label">退款率</view>
</view>
</view>
</view>
</view>
<!-- 订单趋势 -->
<view class="chart-section">
<view class="section-title">订单趋势</view>
<view class="chart-container">
<!-- 模拟图表实际项目中应使用图表库 -->
<view class="mock-chart">
<view class="chart-header">
<view class="chart-title">近30天订单趋势</view>
</view>
<view class="chart-body">
<view class="chart-lines">
<view class="chart-line-container">
<view class="chart-line order-line"></view>
</view>
<view class="chart-line-container">
<view class="chart-line amount-line"></view>
</view>
</view>
<view class="chart-legend">
<view class="legend-item">
<view class="legend-color order-color"></view>
<view class="legend-text">订单数</view>
</view>
<view class="legend-item">
<view class="legend-color amount-color"></view>
<view class="legend-text">金额</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 会员类型分布 -->
<view class="distribution-section">
<view class="section-title">会员类型分布</view>
<view class="distribution-grid">
<view
v-for="(item, index) in memberTypeDistribution"
:key="index"
class="distribution-card"
>
<view class="distribution-icon">{{ item.icon }}</view>
<view class="distribution-content">
<view class="distribution-title">{{ item.type }}</view>
<view class="distribution-stats">
<view class="stat-item">
<view class="stat-label">订单</view>
<view class="stat-value">{{ item.orders }}</view>
</view>
<view class="stat-item">
<view class="stat-label">占比</view>
<view class="stat-value">{{ item.percentage }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 最近订单 -->
<view class="recent-orders-section">
<view class="section-title">最近订单</view>
<view class="order-list">
<view
v-for="(order, index) in recentOrders"
:key="index"
class="order-item"
@click="viewOrderDetail(order.id)"
>
<view class="order-header">
<view class="order-no">订单号{{ order.orderNo }}</view>
<view class="order-status" :class="order.status">{{ order.statusText }}</view>
</view>
<view class="order-info">
<view class="info-row">
<view class="info-label">会员类型</view>
<view class="info-value">{{ order.memberType }}</view>
</view>
<view class="info-row">
<view class="info-label">学员</view>
<view class="info-value">{{ order.studentName }}</view>
</view>
<view class="info-row">
<view class="info-label">金额</view>
<view class="info-value price">{{ order.amount }}</view>
</view>
<view class="info-row">
<view class="info-label">时间</view>
<view class="info-value">{{ order.createTime }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 筛选条件
const dateOptions = ['近7天', '近30天', '近90天', '今年', '全部']
const dateIndex = ref(0)
const memberTypeOptions = ['全部类型', '月度会员', '季度会员', '年度会员']
const memberTypeIndex = ref(0)
// 核心指标
const metrics = ref({
totalOrders: 789,
totalAmount: '¥89,765.43',
averageAmount: '¥113.77',
refundRate: '2.1%'
})
// 会员类型分布
const memberTypeDistribution = ref([
{
type: '月度会员',
icon: '📅',
orders: 345,
percentage: '43.7%'
},
{
type: '季度会员',
icon: '📊',
orders: 234,
percentage: '29.7%'
},
{
type: '年度会员',
icon: '📈',
orders: 210,
percentage: '26.6%'
}
])
// 最近订单
const recentOrders = ref([
{
id: 1,
orderNo: 'M202601290001',
memberType: '年度会员',
studentName: '张**',
amount: '¥299.00',
status: 'success',
statusText: '支付成功',
createTime: '2026-01-29 10:30:45'
},
{
id: 2,
orderNo: 'M202601290002',
memberType: '季度会员',
studentName: '李**',
amount: '¥99.00',
status: 'success',
statusText: '支付成功',
createTime: '2026-01-29 09:15:30'
},
{
id: 3,
orderNo: 'M202601290003',
memberType: '月度会员',
studentName: '王**',
amount: '¥39.00',
status: 'pending',
statusText: '待支付',
createTime: '2026-01-29 08:45:12'
}
])
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出数据
function exportData() {
uni.showModal({
title: '导出数据',
content: '是否导出当前统计数据?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '导出成功', icon: 'success' })
}, 1500)
}
}
})
}
// 日期变化
function onDateChange(e) {
const value = e.detail.value
dateIndex.value = value
// 实际项目中应根据选择的日期重新获取数据
refreshData()
}
// 会员类型变化
function onMemberTypeChange(e) {
const value = e.detail.value
memberTypeIndex.value = value
// 实际项目中应根据选择的会员类型重新获取数据
refreshData()
}
// 刷新数据
function refreshData() {
uni.showLoading({ title: '加载中...' })
setTimeout(() => {
uni.hideLoading()
// 实际项目中应从接口获取数据
}, 1000)
}
// 查看订单详情
function viewOrderDetail(orderId) {
uni.navigateTo({
url: `/pages/member/order-detail?id=${orderId}`
})
}
onMounted(() => {
// 实际项目中应从接口获取数据
// loadStatsData()
})
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.member-stats-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 核心指标 */
.metrics-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.metric-card {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.metric-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 24rpx;
color: #606266;
}
/* 图表区域 */
.chart-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.chart-container {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.mock-chart {
display: flex;
flex-direction: column;
}
.chart-header {
margin-bottom: 24rpx;
}
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
text-align: center;
}
.chart-body {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.chart-lines {
height: 300rpx;
position: relative;
}
.chart-line-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.chart-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60%;
background: linear-gradient(to top, rgba(64, 158, 255, 0.2), transparent);
border-radius: 8rpx;
}
.chart-line.amount-line {
height: 40%;
background: linear-gradient(to top, rgba(103, 194, 58, 0.2), transparent);
}
.chart-legend {
display: flex;
justify-content: center;
gap: 32rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.legend-color {
width: 16rpx;
height: 16rpx;
border-radius: 8rpx;
}
.legend-color.order-color {
background-color: #409eff;
}
.legend-color.amount-color {
background-color: #67c23a;
}
.legend-text {
font-size: 20rpx;
color: #606266;
}
/* 会员类型分布 */
.distribution-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.distribution-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.distribution-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.distribution-icon {
font-size: 48rpx;
margin-bottom: 16rpx;
}
.distribution-content {
display: flex;
flex-direction: column;
align-items: center;
}
.distribution-title {
font-size: 24rpx;
font-weight: 600;
color: #303133;
margin-bottom: 16rpx;
}
.distribution-stats {
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: center;
}
.stat-item {
display: flex;
align-items: center;
}
.stat-label {
font-size: 20rpx;
color: #606266;
margin-right: 8rpx;
}
.stat-value {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
/* 最近订单 */
.recent-orders-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 32rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.order-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.order-item {
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
transition: all 0.3s ease;
}
.order-item:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.order-no {
font-size: 24rpx;
color: #303133;
}
.order-status {
padding: 4rpx 12rpx;
border-radius: 4rpx;
font-size: 18rpx;
font-weight: 600;
}
.order-status.success {
background-color: #f0f9eb;
color: #67c23a;
}
.order-status.pending {
background-color: #ecf5ff;
color: #409eff;
}
.order-status.refund {
background-color: #fef0f0;
color: #f56c6c;
}
.order-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-row {
display: flex;
align-items: center;
}
.info-label {
font-size: 20rpx;
color: #606266;
width: 120rpx;
}
.info-value {
font-size: 20rpx;
color: #303133;
flex: 1;
}
.info-value.price {
font-weight: 600;
color: #f56c6c;
}
/* 响应式设计 */
@media screen and (min-width: 500px) {
.member-stats-container {
max-width: 900px;
margin: 0 auto;
}
.metrics-grid {
grid-template-columns: repeat(4, 1fr);
}
.distribution-grid {
gap: 24rpx;
}
.order-item {
padding: 32rpx;
}
}
</style>

762
src/pages/stats/student.vue Normal file
View File

@@ -0,0 +1,762 @@
<template>
<view class="student-stats-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">学员学情统计</view>
<view class="header-right" @click="exportData">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
:range="dateOptions"
:value="dateIndex"
@change="onDateChange"
class="picker"
>
<view class="picker-text">{{ dateOptions[dateIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">班级</view>
<view class="filter-control">
<picker
:range="classOptions"
:value="classIndex"
@change="onClassChange"
class="picker"
>
<view class="picker-text">{{ classOptions[classIndex] }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 核心指标 -->
<view class="metrics-section">
<view class="metrics-grid">
<view class="metric-card">
<view class="metric-icon">🎓</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.totalStudents }}</view>
<view class="metric-label">总学员数</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">👥</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.memberStudents }}</view>
<view class="metric-label">会员学员</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">📊</view>
<view class="metric-content">
<view class="metric-value">{{ metrics.avgCorrectRate }}</view>
<view class="metric-label">平均正确率</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon"></view>
<view class="metric-content">
<view class="metric-value">{{ metrics.avgExamScore }}</view>
<view class="metric-label">平均模考分</view>
</view>
</view>
</view>
</view>
<!-- 学习趋势 -->
<view class="chart-section">
<view class="section-title">学习趋势</view>
<view class="chart-container">
<!-- 模拟图表实际项目中应使用图表库 -->
<view class="mock-chart">
<view class="chart-header">
<view class="chart-title">近30天学习时长趋势</view>
</view>
<view class="chart-body">
<view class="chart-bars">
<view v-for="(day, index) in trendData" :key="index" class="chart-bar">
<view class="bar-container">
<view class="bar" :style="{ height: day.value + '%' }"></view>
</view>
<view class="bar-label">{{ day.date }}</view>
<view class="bar-value">{{ day.hours }}h</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 学习情况分布 -->
<view class="distribution-section">
<view class="section-title">学习情况分布</view>
<view class="distribution-grid">
<view
v-for="(item, index) in learningDistribution"
:key="index"
class="distribution-card"
>
<view class="distribution-icon">{{ item.icon }}</view>
<view class="distribution-content">
<view class="distribution-title">{{ item.level }}</view>
<view class="distribution-stats">
<view class="stat-item">
<view class="stat-label">人数</view>
<view class="stat-value">{{ item.count }}</view>
</view>
<view class="stat-item">
<view class="stat-label">占比</view>
<view class="stat-value">{{ item.percentage }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 学员列表 -->
<view class="student-section">
<view class="section-title">学员列表</view>
<view class="student-list">
<view
v-for="(student, index) in studentList"
:key="index"
class="student-item"
@click="viewStudentDetail(student.id)"
>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
<view class="student-meta">
<view class="meta-item">
<view class="meta-label">班级</view>
<view class="meta-value">{{ student.class }}</view>
</view>
<view class="meta-item">
<view class="meta-label">会员</view>
<view class="meta-value" :class="{ 'member': student.isMember }">{{ student.isMember ? '是' : '否' }}</view>
</view>
</view>
</view>
<view class="student-stats">
<view class="stat-item">
<view class="stat-label">正确率</view>
<view class="stat-value">{{ student.correctRate }}</view>
</view>
<view class="stat-item">
<view class="stat-label">模考分</view>
<view class="stat-value">{{ student.examScore }}</view>
</view>
</view>
<view class="student-arrow">
</view>
</view>
</view>
<view class="load-more" @click="loadMoreStudents">
加载更多
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 筛选条件
const dateOptions = ['近7天', '近30天', '近90天', '今年', '全部']
const dateIndex = ref(0)
const classOptions = ['全部班级', '一班', '二班', '三班', '四班']
const classIndex = ref(0)
// 核心指标
const metrics = ref({
totalStudents: 1234,
memberStudents: 890,
avgCorrectRate: '78.5%',
avgExamScore: '82.3'
})
// 学习趋势数据
const trendData = ref([
{ date: '1/20', hours: 2.5, value: 30 },
{ date: '1/21', hours: 3.2, value: 40 },
{ date: '1/22', hours: 2.8, value: 35 },
{ date: '1/23', hours: 4.5, value: 60 },
{ date: '1/24', hours: 3.8, value: 50 },
{ date: '1/25', hours: 4.2, value: 55 },
{ date: '1/26', hours: 3.5, value: 45 }
])
// 学习情况分布
const learningDistribution = ref([
{
level: '优秀',
icon: '🏆',
count: 345,
percentage: '28.0%'
},
{
level: '良好',
icon: '📈',
count: 456,
percentage: '37.0%'
},
{
level: '一般',
icon: '📊',
count: 321,
percentage: '26.0%'
},
{
level: '待提高',
icon: '⚠️',
count: 112,
percentage: '9.0%'
}
])
// 学员列表
const studentList = ref([
{
id: 1,
name: '张**',
class: '一班',
isMember: true,
correctRate: '92.5%',
examScore: '95'
},
{
id: 2,
name: '李**',
class: '二班',
isMember: true,
correctRate: '88.2%',
examScore: '90'
},
{
id: 3,
name: '王**',
class: '一班',
isMember: false,
correctRate: '76.8%',
examScore: '82'
},
{
id: 4,
name: '刘**',
class: '三班',
isMember: true,
correctRate: '94.3%',
examScore: '97'
},
{
id: 5,
name: '陈**',
class: '二班',
isMember: false,
correctRate: '72.1%',
examScore: '78'
}
])
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出数据
function exportData() {
uni.showModal({
title: '导出数据',
content: '是否导出当前统计数据?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '导出成功', icon: 'success' })
}, 1500)
}
}
})
}
// 日期变化
function onDateChange(e) {
const value = e.detail.value
dateIndex.value = value
// 实际项目中应根据选择的日期重新获取数据
refreshData()
}
// 班级变化
function onClassChange(e) {
const value = e.detail.value
classIndex.value = value
// 实际项目中应根据选择的班级重新获取数据
refreshData()
}
// 刷新数据
function refreshData() {
uni.showLoading({ title: '加载中...' })
setTimeout(() => {
uni.hideLoading()
// 实际项目中应从接口获取数据
}, 1000)
}
// 查看学员详情
function viewStudentDetail(studentId) {
uni.navigateTo({
url: `/pages/student/detail?id=${studentId}`
})
}
// 加载更多学员
function loadMoreStudents() {
uni.showLoading({ title: '加载中...' })
setTimeout(() => {
uni.hideLoading()
// 实际项目中应从接口获取更多学员数据
uni.showToast({ title: '已加载全部数据', icon: 'none' })
}, 1000)
}
onMounted(() => {
// 实际项目中应从接口获取数据
// loadStatsData()
})
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.student-stats-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 核心指标 */
.metrics-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.metric-card {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.metric-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 24rpx;
color: #606266;
}
/* 图表区域 */
.chart-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.chart-container {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.mock-chart {
display: flex;
flex-direction: column;
}
.chart-header {
margin-bottom: 24rpx;
}
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
text-align: center;
}
.chart-bars {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 300rpx;
gap: 16rpx;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.bar-container {
flex: 1;
width: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
margin-bottom: 12rpx;
}
.bar {
width: 40rpx;
background-color: #409eff;
border-radius: 4rpx 4rpx 0 0;
transition: height 0.5s ease;
}
.bar-label {
font-size: 20rpx;
color: #606266;
margin-bottom: 8rpx;
}
.bar-value {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
/* 学习情况分布 */
.distribution-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.distribution-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.distribution-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.distribution-icon {
font-size: 48rpx;
margin-bottom: 16rpx;
}
.distribution-content {
display: flex;
flex-direction: column;
align-items: center;
}
.distribution-title {
font-size: 24rpx;
font-weight: 600;
color: #303133;
margin-bottom: 16rpx;
}
.distribution-stats {
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: center;
}
.stat-item {
display: flex;
align-items: center;
}
.stat-label {
font-size: 20rpx;
color: #606266;
margin-right: 8rpx;
}
.stat-value {
font-size: 20rpx;
font-weight: 600;
color: #303133;
}
/* 学员列表 */
.student-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 32rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.student-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 24rpx;
}
.student-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
transition: all 0.3s ease;
}
.student-item:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.student-info {
flex: 1;
margin-right: 24rpx;
}
.student-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.student-meta {
display: flex;
gap: 32rpx;
}
.meta-item {
display: flex;
align-items: center;
}
.meta-label {
font-size: 20rpx;
color: #606266;
margin-right: 8rpx;
}
.meta-value {
font-size: 20rpx;
color: #303133;
}
.meta-value.member {
color: #67c23a;
font-weight: 600;
}
.student-stats {
display: flex;
flex-direction: column;
gap: 8rpx;
margin-right: 24rpx;
}
.student-arrow {
font-size: 32rpx;
color: #909399;
}
.load-more {
text-align: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
font-size: 24rpx;
color: #409eff;
cursor: pointer;
}
/* 响应式设计 */
@media screen and (min-width: 500px) {
.student-stats-container {
max-width: 900px;
margin: 0 auto;
}
.metrics-grid {
grid-template-columns: repeat(4, 1fr);
}
.chart-bars {
height: 400rpx;
}
.bar {
width: 60rpx;
}
.student-item {
padding: 32rpx;
}
}
</style>

View File

@@ -0,0 +1,475 @@
<template>
<view class="learning-analysis-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">学情统计分析</view>
<view class="header-right" @click="exportAnalysis">
<view class="export-btn">导出</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">时间范围</view>
<view class="filter-control">
<picker
:range="timeRangeOptions"
:value="timeRangeIndex"
@change="onTimeRangeChange"
class="picker"
>
<view class="picker-text">{{ timeRangeOptions[timeRangeIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">学员类型</view>
<view class="filter-control">
<picker
:range="studentTypeOptions"
:value="studentTypeIndex"
@change="onStudentTypeChange"
class="picker"
>
<view class="picker-text">{{ studentTypeOptions[studentTypeIndex] }}</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 核心数据 -->
<view class="core-data-section">
<view class="section-title">核心数据</view>
<view class="data-card">
<view class="data-row">
<view class="data-item">
<view class="data-value">{{ totalStudents }}</view>
<view class="data-label">总学员数</view>
</view>
<view class="data-item">
<view class="data-value">{{ activeStudents }}</view>
<view class="data-label">活跃学员</view>
</view>
<view class="data-item">
<view class="data-value">{{ memberStudents }}</view>
<view class="data-label">会员学员</view>
</view>
<view class="data-item">
<view class="data-value">{{ avgStudyTime }}</view>
<view class="data-label">平均学习时长</view>
</view>
</view>
</view>
</view>
<!-- 学习趋势 -->
<view class="trend-section">
<view class="section-title">学习趋势</view>
<view class="trend-card">
<view class="chart-placeholder">
<view class="chart-rect"></view>
<view class="chart-text">学习时长趋势图</view>
</view>
<view class="trend-hint">
近7天学员学习时长变化趋势
</view>
</view>
</view>
<!-- 学习分布 -->
<view class="distribution-section">
<view class="section-title">学习分布</view>
<view class="distribution-card">
<view class="chart-placeholder">
<view class="chart-rect"></view>
<view class="chart-text">学习分布饼图</view>
</view>
<view class="distribution-hint">
学员学习科目分布
</view>
</view>
</view>
<!-- 学习排行 -->
<view class="ranking-section">
<view class="section-title">学习排行</view>
<view class="ranking-card">
<view class="ranking-header">
<view class="header-item">排名</view>
<view class="header-item">学员姓名</view>
<view class="header-item">学习时长</view>
<view class="header-item">完成题目</view>
</view>
<view class="ranking-list">
<view
v-for="(item, index) in rankingList"
:key="index"
class="ranking-item"
>
<view class="ranking-cell rank">{{ index + 1 }}</view>
<view class="ranking-cell name">{{ item.name }}</view>
<view class="ranking-cell time">{{ item.studyTime }}</view>
<view class="ranking-cell count">{{ item.completedCount }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 筛选条件
const timeRangeOptions = ['最近7天', '最近30天', '最近90天', '本学期', '自定义']
const timeRangeIndex = ref(0)
const studentTypeOptions = ['全部学员', '普通学员', '会员学员', '分销员学员']
const studentTypeIndex = ref(0)
// 核心数据
const totalStudents = ref(1200)
const activeStudents = ref(850)
const memberStudents = ref(450)
const avgStudyTime = ref('2.5小时')
// 学习排行
const rankingList = ref([
{
name: '张三',
studyTime: '15.5小时',
completedCount: 120
},
{
name: '李四',
studyTime: '12.3小时',
completedCount: 98
},
{
name: '王五',
studyTime: '10.8小时',
completedCount: 85
},
{
name: '赵六',
studyTime: '9.2小时',
completedCount: 75
},
{
name: '钱七',
studyTime: '8.5小时',
completedCount: 68
}
])
onMounted(() => {
// 实际项目中应从接口获取学情数据
// loadLearningData()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 导出分析
function exportAnalysis() {
uni.showModal({
title: '导出分析报告',
content: '确定要导出当前分析报告吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '导出中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 实际项目中应调用接口导出分析报告
// exportAnalysisReport()
}, 1500)
}
}
})
}
// 时间范围变更
function onTimeRangeChange(e) {
const value = e.detail.value
timeRangeIndex.value = value
// 实际项目中应根据时间范围筛选数据
// filterLearningData()
}
// 学员类型变更
function onStudentTypeChange(e) {
const value = e.detail.value
studentTypeIndex.value = value
// 实际项目中应根据学员类型筛选数据
// filterLearningData()
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.learning-analysis-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.export-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.filter-section,
.core-data-section,
.trend-section,
.distribution-section,
.ranking-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 筛选条件 */
.filter-row {
display: flex;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 核心数据 */
.data-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.data-row {
display: flex;
justify-content: space-between;
}
.data-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.data-value {
font-size: 28rpx;
font-weight: bold;
color: #303133;
}
.data-label {
font-size: 18rpx;
color: #606266;
}
/* 学习趋势 */
.trend-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.chart-placeholder {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 48rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.chart-rect {
width: 100%;
height: 300rpx;
background-color: #e0e0e0;
border-radius: 8rpx;
}
.chart-text {
font-size: 24rpx;
color: #909399;
}
.trend-hint {
font-size: 20rpx;
color: #606266;
text-align: center;
}
/* 学习分布 */
.distribution-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.distribution-hint {
font-size: 20rpx;
color: #606266;
text-align: center;
}
/* 学习排行 */
.ranking-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.ranking-header {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.header-item {
flex: 1;
font-size: 20rpx;
font-weight: bold;
color: #606266;
text-align: center;
}
.ranking-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.ranking-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: #fff;
border-radius: 8rpx;
}
.ranking-cell {
flex: 1;
font-size: 20rpx;
color: #303133;
text-align: center;
}
.ranking-cell.rank {
font-weight: bold;
}
.ranking-cell.time {
color: #409eff;
}
.ranking-cell.count {
color: #67c23a;
}
</style>

View File

@@ -0,0 +1,642 @@
<template>
<view class="student-detail-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">学员详情</view>
<view class="header-right">
<view class="action-btn" @click="editStudent">编辑</view>
</view>
</view>
<!-- 学员基本信息 -->
<view class="info-section">
<view class="section-title">基本信息</view>
<view class="info-card">
<view class="info-row">
<view class="info-item">
<view class="info-label">学员姓名</view>
<view class="info-value">{{ studentInfo.name }}</view>
</view>
<view class="info-item">
<view class="info-label">性别</view>
<view class="info-value">{{ studentInfo.gender }}</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">手机号码</view>
<view class="info-value">{{ studentInfo.phone }}</view>
</view>
<view class="info-item">
<view class="info-label">年龄</view>
<view class="info-value">{{ studentInfo.age }}</view>
</view>
</view>
<view class="info-row">
<view class="info-item">
<view class="info-label">所在班级</view>
<view class="info-value">{{ studentInfo.class }}</view>
</view>
<view class="info-item">
<view class="info-label">会员状态</view>
<view class="info-value" :class="{ 'member': studentInfo.isMember }">{{ studentInfo.isMember ? '会员' : '非会员' }}</view>
</view>
</view>
<view class="info-row">
<view class="info-item full-width">
<view class="info-label">注册时间</view>
<view class="info-value">{{ studentInfo.registerTime }}</view>
</view>
</view>
</view>
</view>
<!-- 学习情况 -->
<view class="learning-section">
<view class="section-title">学习情况</view>
<view class="learning-card">
<view class="learning-metrics">
<view class="metric-item">
<view class="metric-icon">📚</view>
<view class="metric-content">
<view class="metric-value">{{ learningStats.totalQuestions }}</view>
<view class="metric-label">总练习题数</view>
</view>
</view>
<view class="metric-item">
<view class="metric-icon">🎯</view>
<view class="metric-content">
<view class="metric-value">{{ learningStats.correctRate }}</view>
<view class="metric-label">正确率</view>
</view>
</view>
<view class="metric-item">
<view class="metric-icon"></view>
<view class="metric-content">
<view class="metric-value">{{ learningStats.totalExams }}</view>
<view class="metric-label">总模考次数</view>
</view>
</view>
<view class="metric-item">
<view class="metric-icon">🏆</view>
<view class="metric-content">
<view class="metric-value">{{ learningStats.avgScore }}</view>
<view class="metric-label">平均模考分</view>
</view>
</view>
</view>
<!-- 学习进度 -->
<view class="progress-section">
<view class="progress-title">学习进度</view>
<view class="progress-items">
<view
v-for="(progress, index) in learningProgress"
:key="index"
class="progress-item"
>
<view class="progress-header">
<view class="progress-subject">{{ progress.subject }}</view>
<view class="progress-percentage">{{ progress.percentage }}</view>
</view>
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: progress.percentage }"
></view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 最近学习记录 -->
<view class="record-section">
<view class="section-title">最近学习记录</view>
<view class="record-list">
<view
v-for="(record, index) in recentRecords"
:key="index"
class="record-item"
>
<view class="record-time">{{ record.time }}</view>
<view class="record-content">
<view class="record-type">{{ record.type }}</view>
<view class="record-detail">{{ record.detail }}</view>
</view>
<view class="record-status" :class="record.statusClass">{{ record.status }}</view>
</view>
</view>
<view class="view-all-btn" @click="viewAllRecords">
查看全部记录
</view>
</view>
<!-- 操作按钮 -->
<view class="action-section">
<view class="action-grid">
<view class="action-item" @click="sendMessage">
<view class="action-icon">💬</view>
<view class="action-label">发送消息</view>
</view>
<view class="action-item" @click="callStudent">
<view class="action-icon">📞</view>
<view class="action-label">拨打电话</view>
</view>
<view class="action-item" @click="viewExamRecords">
<view class="action-icon">📝</view>
<view class="action-label">模考记录</view>
</view>
<view class="action-item" @click="viewErrorSets">
<view class="action-icon"></view>
<view class="action-label">错题本</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 学员基本信息
const studentInfo = ref({
name: '张**',
gender: '男',
phone: '138****1234',
age: 25,
class: '一班',
isMember: true,
registerTime: '2023-10-01 10:00:00'
})
// 学习统计
const learningStats = ref({
totalQuestions: 1256,
correctRate: '88.5%',
totalExams: 24,
avgScore: '92.3'
})
// 学习进度
const learningProgress = ref([
{
subject: '科目一',
percentage: '95%'
},
{
subject: '科目二',
percentage: '80%'
},
{
subject: '科目三',
percentage: '65%'
},
{
subject: '科目四',
percentage: '75%'
}
])
// 最近学习记录
const recentRecords = ref([
{
time: '2024-01-26 14:30:00',
type: '模考',
detail: '科目一模拟考试',
status: '通过',
statusClass: 'status-pass'
},
{
time: '2024-01-26 10:15:00',
type: '练习',
detail: '科目一专项练习-交通标志',
status: '完成',
statusClass: 'status-complete'
},
{
time: '2024-01-25 16:45:00',
type: '模考',
detail: '科目一模拟考试',
status: '通过',
statusClass: 'status-pass'
},
{
time: '2024-01-25 09:30:00',
type: '练习',
detail: '科目一专项练习-安全驾驶',
status: '完成',
statusClass: 'status-complete'
}
])
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 编辑学员信息
function editStudent() {
uni.showToast({
title: '编辑功能开发中',
icon: 'none'
})
}
// 发送消息
function sendMessage() {
uni.showModal({
title: '发送消息',
content: '确认发送消息给学员?',
success: function(res) {
if (res.confirm) {
uni.showToast({
title: '消息发送成功',
icon: 'success'
})
}
}
})
}
// 拨打电话
function callStudent() {
uni.makePhoneCall({
phoneNumber: '13800138000', // 实际项目中应使用真实手机号
success: function() {
console.log('拨打电话成功')
},
fail: function() {
console.log('拨打电话失败')
}
})
}
// 查看模考记录
function viewExamRecords() {
uni.navigateTo({
url: `/pages/student/exam-records?id=${1}` // 实际项目中应传入学员ID
})
}
// 查看错题本
function viewErrorSets() {
uni.navigateTo({
url: `/pages/student/error-sets?id=${1}` // 实际项目中应传入学员ID
})
}
// 查看全部记录
function viewAllRecords() {
uni.navigateTo({
url: `/pages/student/all-records?id=${1}` // 实际项目中应传入学员ID
})
}
onMounted(() => {
// 实际项目中应从接口获取学员详情数据
// const studentId = getCurrentPages().pop().options.id
// loadStudentDetail(studentId)
})
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.student-detail-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.action-btn {
font-size: 28rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.info-section,
.learning-section,
.record-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 学员基本信息 */
.info-card {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.info-row {
display: flex;
gap: 32rpx;
}
.info-item {
flex: 1;
}
.info-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.info-value {
font-size: 28rpx;
font-weight: 600;
color: #303133;
}
.info-item.full-width {
flex: 2;
}
/* 学习情况 */
.learning-card {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.learning-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.metric-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.metric-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 24rpx;
color: #606266;
}
/* 学习进度 */
.progress-section {
margin-top: 16rpx;
}
.progress-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 16rpx;
}
.progress-items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.progress-item {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-subject {
font-size: 24rpx;
color: #303133;
}
.progress-percentage {
font-size: 24rpx;
font-weight: 600;
color: #409eff;
}
.progress-bar {
height: 12rpx;
background-color: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #409eff;
border-radius: 6rpx;
transition: width 0.5s ease;
}
/* 最近学习记录 */
.record-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 24rpx;
}
.record-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.record-time {
font-size: 20rpx;
color: #606266;
margin-right: 24rpx;
width: 160rpx;
}
.record-content {
flex: 1;
margin-right: 24rpx;
}
.record-type {
font-size: 24rpx;
font-weight: 600;
color: #303133;
margin-bottom: 4rpx;
}
.record-detail {
font-size: 20rpx;
color: #606266;
}
.record-status {
font-size: 20rpx;
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-pass {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.status-complete {
color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
.view-all-btn {
text-align: center;
padding: 16rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
font-size: 24rpx;
color: #409eff;
cursor: pointer;
}
/* 操作按钮 */
.action-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 32rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.action-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 32rpx 16rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
transition: all 0.3s ease;
}
.action-item:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.action-icon {
font-size: 48rpx;
margin-bottom: 16rpx;
}
.action-label {
font-size: 20rpx;
color: #303133;
text-align: center;
}
/* 响应式设计 */
@media screen and (min-width: 500px) {
.student-detail-container {
max-width: 900px;
margin: 0 auto;
}
.learning-metrics {
grid-template-columns: repeat(4, 1fr);
}
.info-row {
gap: 48rpx;
}
}
</style>

511
src/pages/student/list.vue Normal file
View File

@@ -0,0 +1,511 @@
<template>
<view class="student-list-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">学员列表管理</view>
<view class="header-right" @click="addStudent">
<view class="add-btn">添加</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<view class="filter-label">状态</view>
<view class="filter-control">
<picker
:range="statusOptions"
:value="statusIndex"
@change="onStatusChange"
class="picker"
>
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view>
</view>
<view class="filter-item">
<view class="filter-label">搜索</view>
<view class="filter-control">
<input
v-model="searchKeyword"
class="search-input"
placeholder="请输入姓名或手机号"
@input="onSearch"
/>
</view>
</view>
</view>
</view>
<!-- 学员列表 -->
<view class="student-list-section">
<view class="section-title">学员列表</view>
<view class="student-list">
<view
v-for="(student, index) in studentList"
:key="index"
class="student-item"
@click="goToStudentDetail(student.id)"
>
<view class="student-avatar">
<view class="avatar-placeholder">{{ getInitials(student.name) }}</view>
</view>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
<view class="student-meta">
<view class="meta-item">{{ student.phone }}</view>
<view class="meta-item">{{ student.school }}</view>
<view class="meta-item status-{{ student.statusClass }}">{{ student.status }}</view>
</view>
</view>
<view class="student-actions">
<view class="action-btn edit-btn" @click.stop="editStudent(student.id)">
编辑
</view>
<view class="action-btn delete-btn" @click.stop="deleteStudent(student.id)">
删除
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="studentList.length === 0" class="empty-state">
<view class="empty-icon">🎓</view>
<view class="empty-text">暂无学员数据</view>
</view>
<!-- 分页 -->
<view v-if="studentList.length > 0" class="pagination">
<view class="page-info">
{{ totalStudents }} 当前第 {{ currentPage }}
</view>
<view class="page-controls">
<view
class="page-btn"
:class="{ disabled: currentPage === 1 }"
@click="goToPage(currentPage - 1)"
>
上一页
</view>
<view
class="page-btn"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage(currentPage + 1)"
>
下一页
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
// 筛选条件
const statusOptions = ['全部状态', '正常', '禁用']
const statusIndex = ref(0)
const searchKeyword = ref('')
// 学员列表数据
const studentList = ref([
{
id: 1,
name: '张三',
phone: '13800138001',
school: '北京大学',
status: '正常',
statusClass: 'active'
},
{
id: 2,
name: '李四',
phone: '13800138002',
school: '清华大学',
status: '正常',
statusClass: 'active'
},
{
id: 3,
name: '王五',
phone: '13800138003',
school: '复旦大学',
status: '禁用',
statusClass: 'inactive'
},
{
id: 4,
name: '赵六',
phone: '13800138004',
school: '上海交通大学',
status: '正常',
statusClass: 'active'
}
])
// 分页信息
const currentPage = ref(1)
const totalStudents = ref(100)
const pageSize = ref(10)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(totalStudents.value / pageSize.value)
})
onMounted(() => {
// 实际项目中应从接口获取学员列表
// loadStudentList()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 添加学员
function addStudent() {
uni.showToast({
title: '添加学员功能开发中',
icon: 'none'
})
}
// 跳转到学员详情
function goToStudentDetail(id) {
uni.navigateTo({
url: `/pages/student/detail?id=${id}`
})
}
// 编辑学员
function editStudent(id) {
uni.showToast({
title: '编辑学员功能开发中',
icon: 'none'
})
}
// 删除学员
function deleteStudent(id) {
uni.showModal({
title: '删除学员',
content: '确定要删除该学员吗?',
success: function(res) {
if (res.confirm) {
// 实际项目中应调用接口删除学员
const index = studentList.value.findIndex(item => item.id === id)
if (index !== -1) {
studentList.value.splice(index, 1)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
}
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选学员
// filterStudentList()
}
// 搜索
function onSearch() {
// 实际项目中应根据关键词搜索学员
// searchStudentList()
}
// 跳转到指定页码
function goToPage(page) {
if (page < 1 || page > totalPages.value) {
return
}
currentPage.value = page
// 实际项目中应加载指定页码的学员
// loadStudentPage(page)
}
// 获取姓名首字母
function getInitials(name) {
return name.charAt(0).toUpperCase()
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.student-list-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 60rpx;
text-align: right;
}
.add-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 筛选条件 */
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.filter-row {
display: flex;
gap: 16rpx;
}
.filter-item {
flex: 1;
}
.filter-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.filter-control {
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.search-input {
font-size: 24rpx;
color: #303133;
width: 100%;
}
/* 学员列表 */
.student-list-section {
flex: 1;
padding: 0 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
.student-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.student-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.student-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
background-color: #409eff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.avatar-placeholder {
font-size: 32rpx;
font-weight: bold;
color: #fff;
}
.student-info {
flex: 1;
}
.student-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 8rpx;
}
.student-meta {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.meta-item {
font-size: 20rpx;
color: #606266;
}
.meta-item.status-active {
color: #67c23a;
}
.meta-item.status-inactive {
color: #909399;
}
.student-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 16rpx;
font-weight: 600;
cursor: pointer;
}
.edit-btn {
background-color: #ecf5ff;
color: #409eff;
}
.delete-btn {
background-color: #fef0f0;
color: #f56c6c;
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 24rpx;
color: #909399;
}
/* 分页 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.page-info {
font-size: 20rpx;
color: #606266;
}
.page-controls {
display: flex;
gap: 16rpx;
}
.page-btn {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #409eff;
cursor: pointer;
}
.page-btn.disabled {
color: #909399;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,515 @@
<template>
<view class="learning-remind-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">学习提醒</view>
<view class="header-right" @click="sendBatchRemind">
<view class="batch-btn">批量发送</view>
</view>
</view>
<!-- 提醒设置 -->
<view class="remind-setting-section">
<view class="section-title">提醒设置</view>
<view class="setting-card">
<view class="setting-row">
<view class="setting-label">提醒类型</view>
<view class="setting-control">
<picker
:range="remindTypeOptions"
:value="remindTypeIndex"
@change="onRemindTypeChange"
class="picker"
>
<view class="picker-text">{{ remindTypeOptions[remindTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="setting-row">
<view class="setting-label">提醒内容</view>
<view class="setting-control">
<textarea
v-model="remindContent"
class="content-textarea"
placeholder="请输入提醒内容"
maxlength="200"
></textarea>
<view class="content-count">{{ remindContent.length }}/200</view>
</view>
</view>
<view class="setting-row">
<view class="setting-label">发送时间</view>
<view class="setting-control">
<input
v-model="sendTime"
class="time-input"
placeholder="请选择发送时间"
@click="showTimePicker"
/>
</view>
</view>
</view>
</view>
<!-- 学员选择 -->
<view class="student-select-section">
<view class="section-title">学员选择</view>
<view class="select-card">
<view class="select-row">
<view class="select-label">选择方式</view>
<view class="select-control">
<picker
:range="selectTypeOptions"
:value="selectTypeIndex"
@change="onSelectTypeChange"
class="picker"
>
<view class="picker-text">{{ selectTypeOptions[selectTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="select-row" v-if="selectTypeIndex === 1">
<view class="select-label">选择学员</view>
<view class="select-control">
<textarea
v-model="selectedStudents"
class="students-textarea"
placeholder="请输入学员ID多个ID用逗号分隔"
></textarea>
</view>
</view>
<view class="select-row" v-if="selectTypeIndex === 2">
<view class="select-label">上传文件</view>
<view class="select-control">
<view class="upload-btn" @click="uploadFile">
选择文件
</view>
<view class="file-name" v-if="uploadedFile">{{ uploadedFile }}</view>
</view>
</view>
</view>
</view>
<!-- 发送预览 -->
<view class="preview-section">
<view class="section-title">发送预览</view>
<view class="preview-card">
<view class="preview-header">
<view class="preview-title">提醒内容预览</view>
</view>
<view class="preview-content">
{{ remindContent || '请输入提醒内容' }}
</view>
<view class="preview-footer">
<view class="preview-info">
<view class="info-item">提醒类型{{ remindTypeOptions[remindTypeIndex] }}</view>
<view class="info-item">发送时间{{ sendTime || '立即发送' }}</view>
<view class="info-item">接收人数{{ estimatedCount }}</view>
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-section">
<view class="action-btn primary-btn" @click="sendRemind">
发送提醒
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue"
// 提醒类型选项
const remindTypeOptions = ['学习提醒', '作业提醒', '考试提醒', '活动提醒', '其他提醒']
const remindTypeIndex = ref(0)
// 提醒内容
const remindContent = ref('')
// 发送时间
const sendTime = ref('')
// 选择方式选项
const selectTypeOptions = ['全部学员', '指定学员', '上传文件']
const selectTypeIndex = ref(0)
// 选中的学员
const selectedStudents = ref('')
// 上传的文件
const uploadedFile = ref('')
// 估计接收人数
const estimatedCount = ref(100)
onMounted(() => {
// 实际项目中应从接口获取默认设置
// loadDefaultSettings()
})
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 })
}
// 批量发送
function sendBatchRemind() {
uni.showModal({
title: '批量发送提醒',
content: '确定要向所有学员批量发送提醒吗?',
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '发送中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '提醒发送成功',
icon: 'success'
})
// 实际项目中应调用接口批量发送提醒
// sendBatchRemindApi()
}, 1500)
}
}
})
}
// 提醒类型变更
function onRemindTypeChange(e) {
const value = e.detail.value
remindTypeIndex.value = value
}
// 选择方式变更
function onSelectTypeChange(e) {
const value = e.detail.value
selectTypeIndex.value = value
// 根据选择方式更新估计接收人数
if (value === 0) {
estimatedCount.value = 100
} else if (value === 1) {
estimatedCount.value = 10
} else {
estimatedCount.value = 50
}
}
// 显示时间选择器
function showTimePicker() {
const now = new Date()
uni.showDateTimePicker({
year: now.getFullYear(),
month: now.getMonth(),
day: now.getDate(),
hour: now.getHours(),
minute: now.getMinutes(),
success: function(res) {
sendTime.value = `${res.year}-${res.month + 1}-${res.day} ${res.hour}:${res.minute}`
}
})
}
// 上传文件
function uploadFile() {
uni.chooseFile({
count: 1,
extension: ['.txt', '.csv', '.xlsx'],
success: function(res) {
uploadedFile.value = res.tempFiles[0].name
uni.showToast({
title: '文件选择成功',
icon: 'success'
})
}
})
}
// 发送提醒
function sendRemind() {
if (!remindContent.value) {
uni.showToast({
title: '请输入提醒内容',
icon: 'none'
})
return
}
uni.showModal({
title: '发送提醒',
content: `确定要发送提醒给${estimatedCount.value}名学员吗?`,
success: function(res) {
if (res.confirm) {
uni.showLoading({ title: '发送中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '提醒发送成功',
icon: 'success'
})
// 实际项目中应调用接口发送提醒
// sendRemindApi()
}, 1500)
}
}
})
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f5f7fa;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.learning-remind-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 120rpx;
background-color: #fff;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.header-left {
width: 60rpx;
}
.back-icon {
font-size: 40rpx;
color: #303133;
cursor: pointer;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.header-right {
width: 100rpx;
text-align: right;
}
.batch-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 通用部分样式 */
.remind-setting-section,
.student-select-section,
.preview-section {
padding: 32rpx;
background-color: #fff;
margin: 0 16rpx 16rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
padding-left: 12rpx;
border-left: 8rpx solid #409eff;
line-height: 1.2;
}
/* 提醒设置 */
.setting-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.setting-row {
display: flex;
flex-direction: column;
margin-bottom: 24rpx;
}
.setting-row:last-child {
margin-bottom: 0;
}
.setting-label {
font-size: 24rpx;
color: #606266;
margin-bottom: 8rpx;
}
.setting-control {
position: relative;
}
.picker {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
.content-textarea {
width: 100%;
height: 160rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #303133;
resize: none;
}
.content-count {
position: absolute;
bottom: 8rpx;
right: 16rpx;
font-size: 18rpx;
color: #909399;
}
.time-input {
width: 100%;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #303133;
}
.students-textarea {
width: 100%;
height: 120rpx;
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #303133;
resize: none;
}
.upload-btn {
background-color: #ecf5ff;
color: #409eff;
padding: 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 600;
text-align: center;
cursor: pointer;
}
.file-name {
margin-top: 8rpx;
font-size: 20rpx;
color: #606266;
}
/* 发送预览 */
.preview-card {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 24rpx;
}
.preview-header {
margin-bottom: 16rpx;
}
.preview-title {
font-size: 24rpx;
font-weight: 600;
color: #303133;
}
.preview-content {
background-color: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 24rpx;
font-size: 24rpx;
color: #303133;
min-height: 120rpx;
line-height: 1.4;
margin-bottom: 16rpx;
}
.preview-footer {
border-top: 1rpx solid #e0e0e0;
padding-top: 16rpx;
}
.preview-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
font-size: 20rpx;
color: #606266;
}
/* 操作按钮 */
.action-section {
padding: 32rpx;
margin: 0 16rpx 16rpx;
}
.action-btn {
width: 100%;
padding: 24rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: bold;
text-align: center;
cursor: pointer;
}
.primary-btn {
background-color: #409eff;
color: #fff;
}
</style>

View File

@@ -235,17 +235,6 @@
</view>
</view>
<view class="feature-card" @click="goToAiRechargeException">
<view class="feature-icon notice-icon">
<view class="icon-text">🤖</view>
<view v-if="exceptionCount.ai > 0" class="exception-badge">{{ exceptionCount.ai }}</view>
</view>
<view class="feature-info">
<view class="feature-title">AI充值订单异常</view>
<view class="feature-desc">处理AI充值订单失败等异常</view>
</view>
</view>
<view class="feature-card" @click="goToMemberOrderException">
<view class="feature-icon notice-icon">
<view class="icon-text">🛒</view>
@@ -268,7 +257,6 @@
const userRole = ref('admin') // 模拟角色,实际应从登录状态获取
const exceptionCount = ref({ // 模拟异常数量
profit: 2, // 分润异常数量
ai: 1, // AI充值订单异常数量
member: 3 // 会员订单异常数量
})
@@ -457,12 +445,6 @@
})
}
function goToAiRechargeException() {
uni.navigateTo({
url: '/pages/exception/ai-recharge'
})
}
function goToMemberOrderException() {
uni.navigateTo({
url: '/pages/exception/member-order'