This commit is contained in:
qsh
2026-02-02 15:58:11 +08:00
parent 1f3b7e6506
commit a5977aa41f
3 changed files with 388 additions and 211 deletions

47
src/api/member/index.js Normal file
View File

@@ -0,0 +1,47 @@
import request from '@/utils/request';
export const getVipTypeList = async params => {
return await request({
url: '/applet/xunjia/member/list',
method: 'get',
params: params
});
};
export const addVipType = async data => {
return await request({
url: '/applet/xunjia/member/add',
method: 'post',
data
});
};
export const updateVipType = async data => {
return await request({
url: '/applet/xunjia/member/update',
method: 'post',
data
});
};
export const deleteVipType = async id => {
return await request({
url: '/applet/xunjia/member/delete?memberId=' + id,
method: 'delete'
});
};
export const getVipTypeOptions = async params => {
return await request({
url: '/applet/xunjia/member/simple-list',
method: 'get',
params: params
});
};
export const getVipType = async id => {
return await request({
url: '/applet/xunjia/member/get?memberId=' + id,
method: 'get'
});
};

View File

@@ -12,44 +12,61 @@
<!-- 会员名称 --> <!-- 会员名称 -->
<view class="form-item"> <view class="form-item">
<view class="label">会员名称</view> <view class="label">会员名称</view>
<input type="text" v-model="formData.name" placeholder="请输入会员名称" class="input" /> <input type="text" v-model="formData.memberName" placeholder="请输入会员名称" class="input" />
</view> </view>
<!-- 车型 --> <!-- 车型 -->
<view class="form-item"> <view class="form-item">
<view class="label">车型</view> <view class="label">车型</view>
<picker @change="bindCarTypeChange" :range="carTypes" :value="carTypeIndex" class="picker"> <radio-group @change="handleCarTypeChange">
<view class="picker-text">{{ carTypes[carTypeIndex] || '请选择车型' }}</view> <label
</picker> v-for="(carType, index) in carTypes"
:key="carType.value"
class="radio-item"
>
<radio :value="carType.value" :checked="formData.carTypeId == carType.value" />
<text class="radio-label">{{ carType.label }}</text>
</label>
</radio-group>
</view> </view>
<!-- 科目 --> <!-- 科目 -->
<view class="form-item"> <view class="form-item">
<view class="label">科目</view> <view class="label">科目</view>
<picker @change="bindSubjectChange" :range="subjects" :value="subjectIndex" class="picker"> <checkbox-group @change="handleSubjectChange">
<view class="picker-text">{{ subjects[subjectIndex] || '请选择科目' }}</view> <label
</picker> v-for="(subject, index) in subjectOptions"
:key="subject.value"
class="checkbox-item"
>
<checkbox :value="subject.value" :checked="formData.subjectList.includes(subject.value)" />
<text class="checkbox-label">{{ subject.label }}</text>
</label>
</checkbox-group>
</view> </view>
<!-- 原价 --> <!-- 原价 -->
<view class="form-item"> <view class="form-item">
<view class="label">原价</view> <view class="label">原价</view>
<input type="number" v-model="formData.originalPrice" placeholder="请输入原价" class="input" /> <input type="number" v-model="formData.price" placeholder="请输入原价" class="input" />
</view> </view>
<!-- 折扣价 --> <!-- 折扣价 -->
<view class="form-item"> <view class="form-item">
<view class="label">折扣价</view> <view class="label">折扣价</view>
<input type="number" v-model="formData.discountPrice" placeholder="请输入折扣价" class="input" /> <input type="number" v-model="formData.discount" placeholder="请输入折扣价" class="input" />
</view> </view>
<!-- 有效期 --> <!-- 有效期 -->
<view class="form-item"> <view class="form-item">
<view class="label">有效期</view> <view class="label">有效期</view>
<view class="有效期-input"> <view class="有效期-input">
<input type="number" v-model="formData.validityPeriod" placeholder="请输入数字" class="validity-input" /> <input type="number" v-model="formData.duration" placeholder="请输入数字" class="validity-input" />
<picker @change="bindUnitChange" :range="units" :value="unitIndex" class="unit-picker"> <picker @change="bindUnitChange" :range="units" range-key="label" :value="unitIndex" class="unit-picker">
<view class="unit-text">{{ units[unitIndex] }}</view> <view class="unit-text">
{{ units[unitIndex].label }}
<view class="dropdown-arrow"></view>
</view>
</picker> </picker>
</view> </view>
</view> </view>
@@ -57,16 +74,16 @@
<!-- 使用方式 --> <!-- 使用方式 -->
<view class="form-item"> <view class="form-item">
<view class="label">使用方式</view> <view class="label">使用方式</view>
<view class="use-type-container"> <checkbox-group @change="handleUseTypeChange">
<view class="use-type-item" @click="toggleUseType('student')"> <label class="checkbox-item">
<view class="checkbox" :class="{ checked: formData.useTypes.includes('student') }"></view> <checkbox value="1" :checked="formData.useTypes.includes('1')" />
<view class="use-type-text">学员购买</view> <text class="checkbox-label">学员购买</text>
</view> </label>
<view class="use-type-item" @click="toggleUseType('admin')"> <label class="checkbox-item">
<view class="checkbox" :class="{ checked: formData.useTypes.includes('admin') }"></view> <checkbox value="2" :checked="formData.useTypes.includes('2')" />
<view class="use-type-text">客服赠送</view> <text class="checkbox-label">客服赠送</text>
</view> </label>
</view> </checkbox-group>
</view> </view>
<!-- 包含权益 --> <!-- 包含权益 -->
@@ -75,12 +92,12 @@
<view class="rights-container"> <view class="rights-container">
<view v-if="loading" class="loading">加载中...</view> <view v-if="loading" class="loading">加载中...</view>
<view v-else-if="rights.length === 0" class="no-rights">暂无权益</view> <view v-else-if="rights.length === 0" class="no-rights">暂无权益</view>
<view v-else class="rights-list"> <checkbox-group v-else @change="handleRightsChange" class="rights-list">
<view v-for="right in rights" :key="right.id" class="right-item"> <label v-for="right in rights" :key="right.value" class="checkbox-item">
<view class="checkbox" :class="{ checked: formData.rights.includes(right.id) }" @click="toggleRight(right.id)"></view> <checkbox :value="right.value" :checked="formData.memberBenefits.includes(right.value)" />
<view class="right-text">{{ right.name }}</view> <text class="checkbox-label">{{ right.label }}</text>
</view> </label>
</view> </checkbox-group>
</view> </view>
</view> </view>
@@ -94,31 +111,40 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { getVipType, addVipType, updateVipType } from '@/api/member'
// 表单数据 // 表单数据
const formData = ref({ const formData = ref({
id: '', memberId: '',
name: '', memberName: '',
carType: '', carTypeId: '1001',
subject: '', subjectList: ['1'],
originalPrice: '', price: '',
discountPrice: '', discount: '',
validityPeriod: '', duration: '',
unit: '天', unit: 1,
useTypes: [], useTypes: ['1'],
rights: [] memberBenefits: ['JX', 'REAL_EXAM', 'SECRET', 'AI']
}) })
// 车型选项 // 车型选项
const carTypes = ref(['C1', 'C2', 'A1', 'A2', 'B1', 'B2']) const carTypes = ref([
{ value: '1001', label: '小车' },
{ value: '1002', label: '摩托车' }
])
const carTypeIndex = ref(0) const carTypeIndex = ref(0)
// 科目选项 // 科目选项
const subjects = ref(['科目一', '科目二', '科目三', '科目四', '全科']) const subjectOptions = ref([
const subjectIndex = ref(0) { value: '1', label: '科一' },
{ value: '4', label: '科四' }
])
// 单位选项 // 单位选项
const units = ref(['天', '年']) const units = ref([
{ value: '1', label: '天' },
{ value: '3', label: '年' }
])
const unitIndex = ref(0) const unitIndex = ref(0)
// 包含权益 // 包含权益
@@ -140,10 +166,10 @@
const currentPage = pages[pages.length - 1] const currentPage = pages[pages.length - 1]
const options = currentPage.options const options = currentPage.options
if (options.id) { if (options.memberId) {
// 编辑模式,获取会员详情 // 编辑模式,获取会员详情
isEditMode.value = true isEditMode.value = true
getMemberDetail(options.id) getMemberDetail(options.memberId)
} else { } else {
// 新增模式 // 新增模式
isEditMode.value = false isEditMode.value = false
@@ -152,67 +178,30 @@
// 获取会员详情 // 获取会员详情
function getMemberDetail(id) { function getMemberDetail(id) {
// 实际项目中应调用接口获取会员详情 getVipType(id).then(res => {
setTimeout(() => { if (res.code == '0000') {
const mockData = { formData.value = res.data
id: id, formData.value.useTypes = res.data.useTypes.map(type => type.toString())
name: '基础会员', // 设置选择器索引
carType: 'C1', carTypeIndex.value = carTypes.value.findIndex(type => type.value == res.data.carTypeId)
subject: '科目一', unitIndex.value = units.value.findIndex(unit => unit.value == res.data.unit)
originalPrice: 199,
discountPrice: 99,
validityPeriod: 30,
unit: '天',
useTypes: ['student'],
rights: [1, 4]
} }
})
formData.value = {
id: mockData.id,
name: mockData.name,
carType: mockData.carType,
subject: mockData.subject,
originalPrice: mockData.originalPrice,
discountPrice: mockData.discountPrice,
validityPeriod: mockData.validityPeriod,
unit: mockData.unit,
useTypes: [...mockData.useTypes],
rights: [...mockData.rights]
}
// 设置选择器索引
carTypeIndex.value = carTypes.value.indexOf(mockData.carType)
subjectIndex.value = subjects.value.indexOf(mockData.subject)
unitIndex.value = units.value.indexOf(mockData.unit)
}, 300)
} }
// 获取权益列表 // 获取权益列表
function getRightsList() { function getRightsList() {
loading.value = true rights.value = [
// 实际项目中应调用接口获取权益列表 { value: 'JX', label: '精选题库' },
setTimeout(() => { { value: 'REAL_EXAM', label: '真实考场' },
rights.value = [ { value: 'SECRET', label: '考前密卷' },
{ id: 1, name: '无限次模拟考试' }, { value: 'AI', label: 'AI助手' },
{ id: 2, name: '视频课程免费看' }, ]
{ id: 3, name: '一对一在线辅导' },
{ id: 4, name: '错题本' },
{ id: 5, name: '考试技巧指导' }
]
loading.value = false
}, 500)
} }
// 车型选择 // 车型选择变化处理
function bindCarTypeChange(e) { function handleCarTypeChange(e) {
carTypeIndex.value = e.detail.value formData.value.carTypeId = e.detail.value
formData.value.carType = carTypes.value[e.detail.value]
}
// 科目选择
function bindSubjectChange(e) {
subjectIndex.value = e.detail.value
formData.value.subject = subjects.value[e.detail.value]
} }
// 单位选择 // 单位选择
@@ -221,40 +210,42 @@
formData.value.unit = units.value[e.detail.value] formData.value.unit = units.value[e.detail.value]
} }
// 切换使用方式 // 科目选择变化处理
function toggleUseType(type) { function handleSubjectChange(e) {
const index = formData.value.useTypes.indexOf(type) formData.value.subjectList = e.detail.value
if (index === -1) {
formData.value.useTypes.push(type)
} else {
formData.value.useTypes.splice(index, 1)
}
} }
// 切换权益 // 使用方式选择变化处理
function toggleRight(rightId) { function handleUseTypeChange(e) {
const index = formData.value.rights.indexOf(rightId) formData.value.useTypes = e.detail.value
if (index === -1) { }
formData.value.rights.push(rightId)
} else { // 权益选择变化处理
formData.value.rights.splice(index, 1) function handleRightsChange(e) {
} formData.value.memberBenefits = e.detail.value
} }
// 提交表单 // 提交表单
function submitForm() { function submitForm() {
// 实际项目中应调用接口提交表单 // 实际项目中应调用接口提交表单
console.log('表单数据:', formData.value)
if (isEditMode.value) { if (isEditMode.value) {
uni.showToast({ updateVipType(formData.value).then(res => {
title: '修改成功', if (res.code == '0000') {
icon: 'success' uni.showToast({
title: '修改成功',
icon: 'success'
})
}
}) })
} else { } else {
uni.showToast({ addVipType(formData.value).then(res => {
title: '新增成功', if (res.code == '0000') {
icon: 'success' uni.showToast({
title: '新增成功',
icon: 'success'
})
}
}) })
} }
@@ -377,46 +368,55 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
background-color: #f9f9f9;
}
.unit-picker:hover {
background-color: #f0f0f0;
border-color: #007aff;
} }
.unit-text { .unit-text {
font-size: 14px; font-size: 14px;
color: #333; color: #333;
}
.use-type-container {
display: flex; display: flex;
gap: 20px; align-items: center;
gap: 6px;
} }
.use-type-item { .dropdown-arrow {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #666;
transition: transform 0.2s ease;
}
.unit-picker:hover .dropdown-arrow {
border-top-color: #007aff;
transform: translateY(1px);
}
/* Radio和Checkbox样式 */
radio-group,
checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.radio-item,
.checkbox-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.checkbox { .radio-label,
width: 20px; .checkbox-label {
height: 20px;
border: 1px solid #e5e5e5;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.checkbox.checked {
background-color: #007aff;
border-color: #007aff;
}
.checkbox.checked::after {
content: '✓';
color: #fff;
font-size: 12px;
}
.use-type-text {
font-size: 14px; font-size: 14px;
color: #333; color: #333;
} }
@@ -440,19 +440,6 @@
.rights-list { .rights-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px;
}
.right-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.right-text {
font-size: 14px;
color: #333;
} }
.submit-btn { .submit-btn {

View File

@@ -12,6 +12,24 @@
</view> </view>
</view> </view>
<!-- 车型筛选 -->
<view class="filter-container">
<view class="filter-row">
<!-- 车型平铺单选 -->
<view class="filter-options">
<view
v-for="(option, index) in carTypeOptions"
:key="option.value"
class="filter-option"
:class="{ 'active': searchForm.carTypeId === option.value }"
@click="selectCarType(option.value)"
>
{{ option.label }}
</view>
</view>
</view>
</view>
<!-- 列表视图 --> <!-- 列表视图 -->
<view class="list-container"> <view class="list-container">
<view v-if="loading" class="loading">加载中...</view> <view v-if="loading" class="loading">加载中...</view>
@@ -26,40 +44,41 @@
<view v-for="member in memberList" :key="member.id" class="member-item"> <view v-for="member in memberList" :key="member.id" class="member-item">
<view class="member-info"> <view class="member-info">
<view class="flex justify-between" style="border-bottom: 1px solid #f0f0f0;"> <view class="flex justify-between" style="border-bottom: 1px solid #f0f0f0;">
<view class="member-name">{{ member.name }}</view> <view class="member-name">{{ member.memberName }}</view>
<view class="member-actions"> <view class="member-actions">
<view class="action-btn edit-btn" @click="goToEdit(member)"> <view class="action-btn edit-btn" @click="goToEdit(member)">
<text>编辑</text> <text>编辑</text>
</view> </view>
<view class="action-btn delete-btn" @click="deleteMember(member.id)"> <view class="action-btn delete-btn" @click="deleteMember(member.memberId)">
<text>删除</text> <text>删除</text>
</view> </view>
</view> </view>
</view> </view>
<view class="member-details"> <view class="member-details">
<view class="detail-row"> <view class="detail-row">
<view class="detail-item">车型{{ member.carType }}</view> <view class="detail-item">车型{{ member.carName }}</view>
<view class="detail-item">科目{{ member.subject }}</view> <view class="detail-item">科目{{ member.subjects }}</view>
</view> </view>
<view class="detail-item price-item"> <view class="detail-item price-item">
<span class="price-label">价格</span> <span class="price-label">价格</span>
<span class="discount-price">¥{{ member.discountPrice }}</span> <span class="discount-price">¥{{ member.discount }}</span>
<span class="original-price"> (原价¥{{ member.originalPrice }})</span> <span class="original-price"> (原价¥{{ member.price }})</span>
</view> </view>
<view class="detail-item">有效期{{ member.validityPeriod }}{{ member.unit }}</view> <!-- 100 -->
<view class="detail-item">有效期{{ member.duration }}{{ unitLabelOptions[member.unit] }}</view>
<view class="detail-item"> <view class="detail-item">
<span class="detail-label">使用方式</span> <span class="detail-label">使用方式</span>
<view class="use-type-tags"> <view class="use-type-tags">
<span v-for="(type, index) in member.useTypes" :key="index" class="use-type-tag"> <span v-for="(type, index) in member.useTypes" :key="index" class="use-type-tag">
{{ type === 'student' ? '学员购买' : '客服赠送' }} {{ type == 1 ? '用户购买' : '客服赠送' }}
</span> </span>
</view> </view>
</view> </view>
<view class="detail-item"> <view class="detail-item" style="align-items: baseline;">
<span class="detail-label">包含权益</span> <span class="detail-label">包含权益</span>
<view class="rights-tags"> <view class="rights-tags">
<span v-for="(right, index) in member.rights" :key="index" class="right-tag"> <span v-for="(right, index) in member.memberBenefits" :key="index" class="right-tag">
{{ right }} {{ rights.find(item => item.value == right)?.label || right }}
</span> </span>
</view> </view>
</view> </view>
@@ -72,50 +91,48 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { getVipTypeList, deleteVipType } from '@/api/member'
// 会员列表 // 会员列表
const memberList = ref([]) const memberList = ref([])
const loading = ref(false) const loading = ref(false)
const searchForm = ref({
carTypeId: '1001',
pageNo: 1,
pageSize: 10
})
const unitLabelOptions = ['', '天', '月', '年']
// 筛选选项
const carTypeOptions = [
{ value: '1001', label: '小车' },
{ value: '1002', label: '摩托车' }
]
const rights = [
{ value: 'JX', label: '精选题库' },
{ value: 'REAL_EXAM', label: '真实考场' },
{ value: 'SECRET', label: '考前密卷' },
{ value: 'AI', label: 'AI助手' },
]
// 生命周期 // 生命周期
onMounted(() => { onShow(() => {
getMemberList() getMemberList()
}) })
// 获取会员列表 // 获取会员列表
function getMemberList() { function getMemberList() {
loading.value = true loading.value = true
// 实际项目中应调用接口获取会员列表 getVipTypeList(searchForm.value).then(res => {
setTimeout(() => { if (res.code == '0000') {
memberList.value = [ memberList.value = res.data.list || []
{ loading.value = false
id: 1, }
name: '基础会员', })
carType: 'C1',
subject: '科目一',
originalPrice: 199,
discountPrice: 99,
validityPeriod: 30,
unit: '天',
useTypes: ['student'],
rights: ['无限次模拟考试', '错题本']
},
{
id: 2,
name: '高级会员',
carType: 'C1',
subject: '全科',
originalPrice: 599,
discountPrice: 299,
validityPeriod: 1,
unit: '年',
useTypes: ['student', 'admin'],
rights: ['无限次模拟考试', '视频课程免费看', '一对一在线辅导', '错题本', '考试技巧指导']
}
]
loading.value = false
}, 500)
} }
// 跳转到新增页面 // 跳转到新增页面
@@ -128,7 +145,7 @@
// 跳转到编辑页面 // 跳转到编辑页面
function goToEdit(member) { function goToEdit(member) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/member/setting-form?id=${member.id}` url: `/pages/member/setting-form?memberId=${member.memberId}`
}) })
} }
@@ -139,11 +156,14 @@
content: '确定要删除该会员配置吗?', content: '确定要删除该会员配置吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// 实际项目中应调用接口删除会员 deleteVipType(id).then(res => {
memberList.value = memberList.value.filter(item => item.id !== id) if (res.code == '0000') {
uni.showToast({ uni.showToast({
title: '删除成功', title: '删除成功',
icon: 'success' icon: 'success'
})
getMemberList()
}
}) })
} }
} }
@@ -154,6 +174,14 @@
function goBack() { function goBack() {
uni.navigateBack() uni.navigateBack()
} }
// 选择车型(平铺单选)
function selectCarType(value) {
searchForm.value.carTypeId = value
getMemberList()
}
</script> </script>
<style scoped> <style scoped>
@@ -172,6 +200,121 @@
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
} }
/* 筛选区域样式 */
.filter-container {
background-color: #fff;
padding: 12px 16px;
border-bottom: 1px solid #e5e5e5;
}
.filter-row {
display: flex;
align-items: center;
gap: 12px;
justify-content: flex-start;
}
/* 车型Tabs样式 */
.filter-options {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px;
padding: 4px;
gap: 2px;
}
.filter-option {
padding: 8px 16px;
background-color: transparent;
border: none;
border-radius: 6px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
flex: 1;
text-align: center;
}
.filter-option:hover {
color: #007aff;
background-color: rgba(0, 122, 255, 0.05);
}
.filter-option.active {
background-color: #007aff;
color: #fff;
box-shadow: 0 2px 4px rgba(0, 122, 255, 0.2);
}
/* 筛选选择器样式 */
.filter-select {
flex: 1;
max-width: 180px;
position: relative;
}
.picker-text {
padding: 10px 16px;
background-color: #f8f9fa;
border-radius: 8px;
font-size: 14px;
color: #333;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
border: 1px solid #e9ecef;
transition: all 0.2s ease;
}
.picker-text:hover {
border-color: #007aff;
box-shadow: 0 1px 3px rgba(0, 122, 255, 0.1);
}
.dropdown-icon {
font-size: 12px;
color: #666;
margin-left: 8px;
transition: transform 0.2s ease;
}
.picker-text:hover .dropdown-icon {
color: #007aff;
transform: translateY(1px);
}
/* 响应式调整 */
@media (max-width: 480px) {
.filter-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.filter-options {
justify-content: flex-start;
}
.filter-select {
flex: 1;
max-width: none;
}
.filter-option {
padding: 6px 12px;
font-size: 13px;
}
.picker-text {
padding: 8px 12px;
font-size: 13px;
}
}
.back-btn { .back-btn {
width: 44px; width: 44px;
height: 44px; height: 44px;