This commit is contained in:
qsh
2026-02-04 15:10:50 +08:00
parent d97a222637
commit 5f1ba629ab
9 changed files with 1103 additions and 169 deletions

42
src/api/account/index.js Normal file
View File

@@ -0,0 +1,42 @@
import request from '@/utils/request';
// 添加增删改查接口
export const addAccount = async data => {
return request({
url: '/applet/xunjia/account/create',
method: 'post',
data
});
};
export const deleteAccount = async id => {
return request({
url: '/applet/xunjia/account/delete',
method: 'delete',
params: { id }
});
};
export const updateAccount = async data => {
return request({
url: '/applet/xunjia/account/update',
method: 'put',
data
});
};
export const getAccountPage = async params => {
return request({
url: '/applet/xunjia/account/page',
method: 'get',
params
});
};
export const getAccountInfo = async id => {
return request({
url: '/applet/xunjia/account/get',
method: 'get',
params: { id }
});
};

View File

@@ -0,0 +1,167 @@
<template>
<view>
<view class="submenu-item">
<view class="submenu-item-content" @click.stop="handleMenuClick">
<view
class="custom-checkbox"
:class="{ 'checked': isMenuChecked }"
></view>
<view class="submenu-name">{{ menu[nameField] }}</view>
</view>
<view v-if="hasChildren" class="menu-arrow" :class="{ 'expanded': isExpanded }" @click.stop="handleToggleMenu">{{ isExpanded ? '' : '' }}</view>
<view v-else class="menu-arrow-placeholder"></view>
</view>
<view v-if="hasChildren && isExpanded" class="submenu-list">
<TreeSelectItem
v-for="submenu in menu.children"
:key="submenu[idField]"
:menu="submenu"
:id-field="idField"
:name-field="nameField"
:selected-ids="selectedIds"
@update:selected-ids="$emit('update:selected-ids', $event)"
:expanded-menus="expandedMenus"
@toggle-menu="$emit('toggle-menu', $event)"
/>
</view>
</view>
</template>
<script>
export default {
name: 'TreeSelectItem'
}
</script>
<script setup>
import { computed } from "vue"
import TreeSelectItem from './tree-select-item.vue'
const props = defineProps({
menu: {
type: Object,
required: true
},
idField: {
type: String,
default: 'id'
},
nameField: {
type: String,
default: 'name'
},
selectedIds: {
type: Array,
default: () => []
},
expandedMenus: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:selected-ids', 'toggle-menu'])
// 计算属性
const hasChildren = computed(() => {
return props.menu.children && props.menu.children.length > 0
})
const isMenuChecked = computed(() => {
return props.selectedIds.includes(props.menu[props.idField])
})
const isExpanded = computed(() => {
return props.expandedMenus.includes(props.menu[props.idField])
})
// 处理菜单点击
function handleMenuClick() {
const menuId = props.menu[props.idField]
const checked = !isMenuChecked.value
emit('update:selected-ids', { menuId, checked })
}
// 处理菜单展开/折叠
function handleToggleMenu() {
const menuId = props.menu[props.idField]
emit('toggle-menu', menuId)
}
</script>
<style lang="scss" scoped>
/* 自定义checkbox样式 */
.custom-checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #dcdfe6;
border-radius: 8rpx;
background-color: #ffffff;
transition: all 0.3s ease-in-out;
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.custom-checkbox.checked {
border-color: #409eff;
background-color: #409eff;
}
.custom-checkbox.checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 10rpx;
height: 16rpx;
border: 2rpx solid #ffffff;
border-top: none;
border-left: none;
transform: translate(-50%, -60%) rotate(45deg);
}
.submenu-item {
display: flex;
align-items: center;
padding: 12rpx 16rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 8rpx;
}
.submenu-item-content {
flex: 1;
display: flex;
align-items: center;
cursor: pointer;
}
.submenu-name {
flex: 1;
font-size: 22rpx;
color: #606266;
margin-left: 16rpx;
}
.menu-arrow {
font-size: 20rpx;
color: #909399;
cursor: pointer;
}
.menu-arrow.expanded {
transform: rotate(0deg);
}
.menu-arrow-placeholder {
width: 20rpx;
}
.submenu-list {
margin-left: 40rpx;
margin-top: 8rpx;
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<view class="tree-select">
<!-- 递归渲染菜单树 -->
<template v-for="menu in treeData" :key="getMenuId(menu)">
<view class="menu-item">
<view class="menu-header" @click="toggleMenu(getMenuId(menu))">
<view
class="custom-checkbox"
:class="{ 'checked': isMenuChecked(getMenuId(menu)) }"
@click.stop="onMenuChange(getMenuId(menu), !isMenuChecked(getMenuId(menu)))">
</view>
<view class="menu-name">{{ getMenuName(menu) }}</view>
<view v-if="hasChildren(menu)" class="menu-arrow" :class="{ 'expanded': expandedMenus.includes(getMenuId(menu)) }">{{ expandedMenus.includes(getMenuId(menu)) ? '' : '' }}</view>
<view v-else class="menu-arrow-placeholder"></view>
</view>
<view v-if="hasChildren(menu) && expandedMenus.includes(getMenuId(menu))" class="submenu-list">
<!-- 递归渲染子菜单 -->
<TreeSelectItem
v-for="submenu in menu.children"
:key="getMenuId(submenu)"
:menu="submenu"
:id-field="idField"
:name-field="nameField"
:selected-ids="modelValue"
@update:selected-ids="handleSubMenuChange"
:expanded-menus="expandedMenus"
@toggle-menu="toggleMenu"
/>
</view>
</view>
</template>
</view>
</template>
<script setup>
import TreeSelectItem from './tree-select-item.vue'
import { ref, computed, watch } from "vue"
// 组件props
const props = defineProps({
// 树数据
treeData: {
type: Array,
default: () => []
},
// id字段名
idField: {
type: String,
default: 'id'
},
// name字段名
nameField: {
type: String,
default: 'name'
},
// 选中字段(双向绑定)
modelValue: {
type: Array,
default: () => []
}
})
// 事件
const emit = defineEmits(['update:modelValue'])
// 展开的菜单
const expandedMenus = ref([])
// 获取菜单ID
function getMenuId(menu) {
return menu[props.idField]
}
// 获取菜单名称
function getMenuName(menu) {
return menu[props.nameField]
}
// 检查菜单是否有子菜单
function hasChildren(menu) {
return menu.children && menu.children.length > 0
}
// 检查菜单是否被选中
function isMenuChecked(menuId) {
return props.modelValue.includes(menuId)
}
// 切换菜单展开状态
function toggleMenu(menuId) {
const index = expandedMenus.value.indexOf(menuId)
if (index > -1) {
expandedMenus.value.splice(index, 1)
} else {
expandedMenus.value.push(menuId)
}
}
// 菜单权限变更
function onMenuChange(menuId, checked) {
// 创建新的选中数组
const newSelected = [...props.modelValue]
if (checked) {
// 选中当前菜单
if (!newSelected.includes(menuId)) {
newSelected.push(menuId)
}
// 找到当前菜单
const menu = findMenuById(menuId, props.treeData)
// 如果是上级菜单,选中所有子菜单
if (menu && hasChildren(menu)) {
menu.children.forEach(child => {
const childId = getMenuId(child)
if (!newSelected.includes(childId)) {
newSelected.push(childId)
}
})
}
// 检查并选中上级菜单
checkParentMenu(menuId, newSelected)
} else {
// 取消选中当前菜单
const index = newSelected.indexOf(menuId)
if (index > -1) {
newSelected.splice(index, 1)
}
// 找到当前菜单
const menu = findMenuById(menuId, props.treeData)
// 如果是上级菜单,取消选中所有子菜单
if (menu && hasChildren(menu)) {
menu.children.forEach(child => {
const childId = getMenuId(child)
const childIndex = newSelected.indexOf(childId)
if (childIndex > -1) {
newSelected.splice(childIndex, 1)
}
})
}
// 检查并取消选中上级菜单
uncheckParentMenu(menuId, newSelected)
}
// 触发更新事件
emit('update:modelValue', newSelected)
}
// 根据ID查找菜单
function findMenuById(menuId, menus) {
for (const menu of menus) {
if (getMenuId(menu) === menuId) {
return menu
}
if (hasChildren(menu)) {
const found = findMenuById(menuId, menu.children)
if (found) {
return found
}
}
}
return null
}
// 检查并选中上级菜单
function checkParentMenu(menuId, selected) {
// 找到当前菜单的父菜单
const parentMenu = findParentMenu(menuId, props.treeData)
if (parentMenu) {
const parentId = getMenuId(parentMenu)
if (!selected.includes(parentId)) {
selected.push(parentId)
}
// 递归检查更上级的菜单
checkParentMenu(parentId, selected)
}
}
// 检查并取消选中上级菜单
function uncheckParentMenu(menuId, selected) {
// 找到当前菜单的父菜单
const parentMenu = findParentMenu(menuId, props.treeData)
if (parentMenu) {
const parentId = getMenuId(parentMenu)
// 检查父菜单的所有子菜单是否都未选中
const allChildrenUnchecked = parentMenu.children.every(child => {
return !selected.includes(getMenuId(child))
})
if (allChildrenUnchecked) {
const parentIndex = selected.indexOf(parentId)
if (parentIndex > -1) {
selected.splice(parentIndex, 1)
// 递归检查更上级的菜单
uncheckParentMenu(parentId, selected)
}
}
}
}
// 查找父菜单
function findParentMenu(menuId, menus) {
for (const menu of menus) {
if (hasChildren(menu)) {
if (menu.children.some(child => getMenuId(child) === menuId)) {
return menu
}
const found = findParentMenu(menuId, menu.children)
if (found) {
return found
}
}
}
return null
}
// 处理子菜单变更
function handleSubMenuChange(data) {
onMenuChange(data.menuId, data.checked)
}
</script>
<style lang="scss" scoped>
/* 自定义checkbox样式 */
.custom-checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #dcdfe6;
border-radius: 8rpx;
background-color: #ffffff;
transition: all 0.3s ease-in-out;
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.custom-checkbox.checked {
border-color: #409eff;
background-color: #409eff;
}
.custom-checkbox.checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 10rpx;
height: 16rpx;
border: 2rpx solid #ffffff;
border-top: none;
border-left: none;
transform: translate(-50%, -60%) rotate(45deg);
}
/* 菜单列表 */
.menu-list {
space-y: 16rpx;
}
.menu-item {
margin-bottom: 16rpx;
}
.menu-header {
display: flex;
align-items: center;
padding: 16rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
cursor: pointer;
}
.menu-name {
flex: 1;
font-size: 24rpx;
color: #303133;
margin-left: 16rpx;
}
.menu-arrow {
font-size: 20rpx;
color: #909399;
}
.menu-arrow.expanded {
transform: rotate(0deg);
}
.menu-arrow-placeholder {
width: 20rpx;
}
.submenu-list {
margin-left: 40rpx;
margin-top: 8rpx;
}
.submenu-item {
display: flex;
align-items: center;
padding: 12rpx 16rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 8rpx;
}
.submenu-item-content {
flex: 1;
display: flex;
align-items: center;
cursor: pointer;
}
.submenu-name {
flex: 1;
font-size: 22rpx;
color: #606266;
margin-left: 16rpx;
}
</style>

View File

@@ -102,6 +102,12 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "pages/account/accountForm",
"style": {
"navigationStyle": "custom"
}
},
{ {
"path": "pages/student/list", "path": "pages/student/list",
"style": { "style": {

View File

@@ -0,0 +1,341 @@
<template>
<view class="account-form-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-left" @click="goBack">
<view class="back-icon"></view>
</view>
<view class="header-title">{{ isEdit ? '编辑账号' : '添加账号' }}</view>
<view class="header-right" @click="submitForm">
<view class="save-btn">保存</view>
</view>
</view>
<!-- 表单内容 -->
<view class="form-content">
<!-- 基本信息 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<view class="form-item">
<view class="form-label">姓名</view>
<view class="form-control">
<input
v-model="form.nickname"
class="form-input"
placeholder="请输入姓名"
/>
</view>
</view>
<view class="form-item">
<view class="form-label">手机号</view>
<view class="form-control">
<input
v-model="form.mobile"
class="form-input"
maxlength="11"
placeholder="请输入手机号"
type="number"
/>
</view>
</view>
</view>
<!-- 菜单权限 -->
<view class="form-section">
<view class="section-title">菜单权限</view>
<view class="menu-list">
<!-- 使用tree-select组件 -->
<tree-select
:tree-data="menuList"
id-field="id"
name-field="name"
v-model="form.menuIds"
/>
</view>
</view>
<!-- 数据权限 -->
<view class="form-section">
<view class="section-title">数据权限</view>
<view class="data-scope-list">
<radio-group v-model="form.dataScope">
<view
v-for="scope in dataScopeOptions"
:key="scope.value"
class="data-scope-item"
>
<radio
:value="scope.value"
:checked="form.dataScope == scope.value"
/>
<view class="data-scope-label">
<view class="scope-name">{{ scope.label }}</view>
<view class="scope-desc">{{ scope.desc }}</view>
</view>
</view>
</radio-group>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue"
import { addAccount, updateAccount, getAccountInfo } from "@/api/account/index.js"
import { useUserStore }from "@/store/modules/user.js"
import treeSelect from "@/components/tree-select.vue"
const userStore = useUserStore()
// 路由参数
const id = ref('');
const isEdit = computed(() => !!id.value);
// 表单数据
const form = ref({
nickname: '',
mobile: '',
menuIds: [],
dataScope: '1' // 默认全部数据
});
// 菜单列表
const menuList = computed(() => userStore.userMenus);
// 数据权限选项
const dataScopeOptions = [
{ value: '1', label: '全部数据', desc: '可以查看和管理所有数据' },
{ value: '5', label: '个人数据', desc: '只能查看和管理自己及下级数据' },
];
// 页面加载时获取参数
onMounted(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
id.value = currentPage.options.id || '';
if (isEdit.value) {
// 编辑模式,加载账号数据
loadAccountData();
}
});
// 加载账号数据
function loadAccountData() {
getAccountInfo(id.value).then(res => {
if (res.code == '0000') {
form.value = res.data;
} else {
uni.showToast({ title: res.msg || '加载账号数据失败', icon: 'none' });
}
});
}
// 提交表单
function submitForm() {
// 表单验证
if (!form.value.nickname) {
uni.showToast({ title: '请输入姓名', icon: 'none' });
return;
}
if (!form.value.mobile || form.value.mobile.length !== 11) {
uni.showToast({ title: '请输入有效的手机号', icon: 'none' });
return;
}
// 实际项目中应调用接口提交表单
if (isEdit.value) {
updateAccount(form.value).then(res => {
if (res.code == '0000') {
uni.showToast({ title: '保存成功', icon: 'success' });
setTimeout(() => {
goBack();
}, 1000);
} else {
uni.showToast({ title: res.msg || '保存失败', icon: 'none' });
}
});
} else {
addAccount(form.value).then(res => {
if (res.code == '0000') {
uni.showToast({ title: '添加成功', icon: 'success' });
setTimeout(() => {
goBack();
}, 1000);
} else {
uni.showToast({ title: res.msg || '添加失败', icon: 'none' });
}
});
}
}
// 返回上一页
function goBack() {
uni.navigateBack({ delta: 1 });
}
</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-form-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;
}
.save-btn {
font-size: 24rpx;
color: #409eff;
font-weight: 600;
cursor: pointer;
}
/* 表单内容 */
.form-content {
flex: 1;
padding: 24rpx;
}
/* 表单 section */
.form-section {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
}
/* 表单 item */
.form-item {
display: flex;
margin-bottom: 24rpx;
}
.form-label {
width: 120rpx;
font-size: 24rpx;
color: #606266;
display: flex;
align-items: center;
}
.form-control {
flex: 1;
background-color: #f9f9f9;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 16rpx;
}
.form-input {
width: 100%;
font-size: 24rpx;
color: #303133;
}
/* 选择器 */
.picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
font-size: 24rpx;
color: #303133;
}
/* 数据权限 */
.data-scope-list {
space-y: 16rpx;
}
.data-scope-item {
display: flex;
align-items: flex-start;
padding: 16rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.data-scope-label {
flex: 1;
margin-left: 16rpx;
}
.scope-name {
font-size: 24rpx;
color: #303133;
margin-bottom: 4rpx;
}
.scope-desc {
font-size: 20rpx;
color: #909399;
}
</style>

View File

@@ -7,7 +7,7 @@
</view> </view>
<view class="header-title">账号创建与管控</view> <view class="header-title">账号创建与管控</view>
<view class="header-right" @click="addAccount"> <view class="header-right" @click="addAccount">
<view class="add-btn">+ 添加</view> <view class="add-btn">添加</view>
</view> </view>
</view> </view>
@@ -15,25 +15,23 @@
<view class="filter-section"> <view class="filter-section">
<view class="filter-row"> <view class="filter-row">
<view class="filter-item"> <view class="filter-item">
<view class="filter-label">账号状态</view> <view class="filter-label">姓名</view>
<view class="filter-control"> <view class="filter-control">
<picker <input
:range="statusOptions" v-model="nickname"
:value="statusIndex" class="search-input"
@change="onStatusChange" placeholder="请输入姓名"
class="picker" @input="onSearch"
> />
<view class="picker-text">{{ statusOptions[statusIndex] }}</view>
</picker>
</view> </view>
</view> </view>
<view class="filter-item"> <view class="filter-item">
<view class="filter-label">搜索</view> <view class="filter-label">手机号</view>
<view class="filter-control"> <view class="filter-control">
<input <input
v-model="searchKeyword" v-model="mobile"
class="search-input" class="search-input"
placeholder="请输入账号名称或手机号" placeholder="请输入手机号"
@input="onSearch" @input="onSearch"
/> />
</view> </view>
@@ -49,11 +47,11 @@
class="account-item" class="account-item"
> >
<view class="account-info"> <view class="account-info">
<view class="account-name">{{ account.name }}</view> <view class="account-name">{{ account.nickname }}</view>
<view class="account-meta"> <view class="account-meta">
<view class="meta-item">{{ account.phone }}</view> <view class="meta-item">{{ account.mobile }}</view>
<view class="meta-item">{{ account.role }}</view> <view class="meta-item">{{ account.isDistributor ? '分销员' : '' }}</view>
<view class="meta-item status-{{ account.status }}">{{ account.statusText }}</view> <view class="meta-item" :class="`status-${account.status}`">{{ ['启用', '禁用'][account.status] }}</view>
</view> </view>
</view> </view>
<view class="account-actions"> <view class="account-actions">
@@ -61,11 +59,10 @@
编辑 编辑
</view> </view>
<view <view
class="action-btn" class="action-btn delete-btn"
:class="account.status === 'active' ? 'disable-btn' : 'enable-btn'" @click="handleDelete(account.id)"
@click="toggleAccountStatus(account.id, account.status)"
> >
{{ account.status === 'active' ? '冻结' : '启用' }} 删除
</view> </view>
</view> </view>
</view> </view>
@@ -76,52 +73,57 @@
<view class="empty-icon">👥</view> <view class="empty-icon">👥</view>
<view class="empty-text">暂无账号数据</view> <view class="empty-text">暂无账号数据</view>
</view> </view>
<!-- 分页信息 -->
<view class="pagination-info" v-if="accountList.length > 0">
<view class="info-text"> {{ total }} 条记录</view>
<view class="page-controls">
<view class="page-btn" :disabled="pageNo <= 1" @click="changePage(pageNo - 1)">上一页</view>
<view class="page-info">{{ pageNo }} / {{ totalPages }}</view>
<view class="page-btn" :disabled="pageNo >= totalPages" @click="changePage(pageNo + 1)">下一页</view>
</view>
</view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue" import { ref, onMounted, computed } from "vue"
import { getAccountPage, deleteAccount } from '@/api/account'
// 状态选项 // 筛选条件
const statusOptions = ['全部', '启用', '冻结'] const nickname = ref('')
const statusIndex = ref(0) const mobile = ref('')
// 搜索关键词 // 分页参数
const searchKeyword = ref('') const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(total.value / pageSize.value)
})
// 账号列表 // 账号列表
const accountList = 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(() => { onMounted(() => {
// 实际项目中应从接口获取账号列表 // 实际项目中应从接口获取账号列表
// loadAccountList() loadAccountList()
}) })
const loadAccountList = async () => {
const params = {
nickname: nickname.value,
mobile: mobile.value,
pageNo: pageNo.value,
pageSize: pageSize.value
}
const { data } = await getAccountPage(params)
accountList.value = data.list
total.value = data.total
}
// 返回上一页 // 返回上一页
function goBack() { function goBack() {
uni.navigateBack({ delta: 1 }) uni.navigateBack({ delta: 1 })
@@ -130,55 +132,59 @@
// 添加账号 // 添加账号
function addAccount() { function addAccount() {
uni.navigateTo({ uni.navigateTo({
url: '/pages/account/add' url: '/pages/account/accountForm'
}) })
} }
// 编辑账号 // 编辑账号
function editAccount(accountId) { function editAccount(accountId) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/account/edit?id=${accountId}` url: `/pages/account/accountForm?id=${accountId}`
}) })
} }
// 切换账号状态 // 删除账号
function toggleAccountStatus(accountId, currentStatus) { function handleDelete(accountId) {
const newStatus = currentStatus === 'active' ? 'inactive' : 'active'
const newStatusText = newStatus === 'active' ? '启用' : '冻结'
uni.showModal({ uni.showModal({
title: '确认操作', title: '确认删除',
content: `确定要${newStatusText}该账号吗?`, content: '确定要删除该账号吗?',
success: function(res) { success: async function(res) {
if (res.confirm) { if (res.confirm) {
// 实际项目中应调用接口切换账号状态 try {
const account = accountList.value.find(item => item.id === accountId) await deleteAccount(accountId)
if (account) { uni.showToast({
account.status = newStatus title: '删除成功',
account.statusText = newStatusText icon: 'success'
})
// 删除成功后刷新账号列表
loadAccountList()
} catch (error) {
uni.showToast({
title: error.message || '删除失败',
icon: 'none'
})
} }
uni.showToast({
title: `账号已${newStatusText}`,
icon: 'success'
})
} }
} }
}) })
} }
// 状态变更
function onStatusChange(e) {
const value = e.detail.value
statusIndex.value = value
// 实际项目中应根据状态筛选账号列表
// filterAccountList()
}
// 搜索 // 搜索
function onSearch() { function onSearch() {
// 重置页码
pageNo.value = 1
// 实际项目中应根据关键词搜索账号列表 // 实际项目中应根据关键词搜索账号列表
// searchAccountList() // searchAccountList()
} }
// 切换页码
function changePage(newPage) {
if (newPage >= 1 && newPage <= totalPages.value) {
pageNo.value = newPage
// 实际项目中应根据新页码加载账号列表
// loadAccountList()
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -325,6 +331,7 @@
display: flex; display: flex;
gap: 16rpx; gap: 16rpx;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
} }
.meta-item { .meta-item {
@@ -332,11 +339,11 @@
color: #606266; color: #606266;
} }
.status-active { .status-0 {
color: #67c23a; color: #67c23a;
} }
.status-inactive { .status-1 {
color: #909399; color: #909399;
} }
@@ -358,16 +365,11 @@
color: #409eff; color: #409eff;
} }
.disable-btn { .delete-btn {
background-color: #fef0f0; background-color: #fef0f0;
color: #f56c6c; color: #f56c6c;
} }
.enable-btn {
background-color: #f0f9eb;
color: #67c23a;
}
/* 空状态 */ /* 空状态 */
.empty-state { .empty-state {
flex: 1; flex: 1;
@@ -388,61 +390,56 @@
color: #909399; color: #909399;
} }
/* 平板和大屏响应式 */ /* 分页信息样式 */
@media screen and (min-width: 768px) { .pagination-info {
.account-manage-container { display: flex;
max-width: 1000px; justify-content: space-between;
margin: 0 auto; align-items: center;
width: 100%; padding: 16rpx 32rpx;
} background-color: #fff;
border-top: 1rpx solid #e4e7ed;
.filter-section { margin-top: 16rpx;
margin: 0 32rpx 24rpx; }
padding: 32rpx;
} .info-text {
font-size: 20rpx;
.account-list { color: #606266;
padding: 0 32rpx; }
}
.page-controls {
.account-item { display: flex;
margin-bottom: 24rpx; align-items: center;
padding: 32rpx; gap: 16rpx;
} }
.filter-row { .page-btn {
gap: 32rpx; padding: 6rpx 16rpx;
} border: 1rpx solid #dcdfe6;
border-radius: 4rpx;
.filter-label { font-size: 20rpx;
font-size: 26rpx; color: #606266;
} cursor: pointer;
transition: all 0.3s ease;
.picker-text, }
.search-input {
font-size: 26rpx; .page-btn:hover {
} border-color: #409eff;
color: #409eff;
.filter-control { }
padding: 20rpx;
} .page-btn[disabled] {
opacity: 0.5;
.account-name { cursor: not-allowed;
font-size: 32rpx; }
}
.page-btn[disabled]:hover {
.meta-item { border-color: #dcdfe6;
font-size: 22rpx; color: #606266;
} }
.action-btn { .page-info {
font-size: 22rpx; font-size: 20rpx;
padding: 12rpx 24rpx; color: #303133;
}
.add-btn {
font-size: 28rpx;
}
} }
/* 大屏设备响应式 */ /* 大屏设备响应式 */

View File

@@ -91,15 +91,11 @@
<view class="vip-list"> <view class="vip-list">
<label v-for="vip in vipList" :key="vip.memberId" class="vip-item checkbox-item" <label v-for="vip in vipList" :key="vip.memberId" class="vip-item checkbox-item"
@click="onVipChange(idx, vip.memberId)"> @click="onVipChange(idx, vip.memberId)">
<checkbox <view class="custom-checkbox" :class="{ 'checked': rule.selectedVips && rule.selectedVips.includes(vip.memberId), 'disabled': isVipSelectedInOtherRules(idx, vip.memberId) }"></view>
:value="vip.memberId"
:checked="rule.selectedVips.includes(vip.memberId)"
:disabled="isVipSelectedInOtherRules(idx, vip.memberId)"
/>
<view class="checkbox-label" :class="{ 'disabled': isVipSelectedInOtherRules(idx, vip.memberId) }"> <view class="checkbox-label" :class="{ 'disabled': isVipSelectedInOtherRules(idx, vip.memberId) }">
<text>{{ vip.carName || '' }}</text> <view v-if="vip.carName" class="car-tag">{{ vip.carName }}</view>
<text>{{ vip.memberName || '' }}</text> <text>{{ vip.memberName || '' }}</text>
<text>{{ vip.discount ? ' ' + vip.discount : '' }}</text> <text v-if="vip.discount" class="discount-text">¥{{ vip.discount }}</text>
</view> </view>
</label> </label>
</view> </view>
@@ -227,17 +223,22 @@ const vipList = ref([])
// VIP选择变更确保会员类型不重复 // VIP选择变更确保会员类型不重复
function onVipChange(idx, vipId) { function onVipChange(idx, vipId) {
if (profitRules.value[idx]) { if (profitRules.value[idx]) {
const vipIndex = profitRules.value[idx].selectedVips.indexOf(vipId) const rule = profitRules.value[idx]
// 确保selectedVips存在
if (!rule.selectedVips) {
rule.selectedVips = []
}
const vipIndex = rule.selectedVips.indexOf(vipId)
if (vipIndex > -1) { if (vipIndex > -1) {
// 取消选择 // 取消选择
profitRules.value[idx].selectedVips.splice(vipIndex, 1) rule.selectedVips.splice(vipIndex, 1)
console.log('取消选择',idx+1, profitRules.value[idx].selectedVips.length)
} else { } else {
// 检查是否在其他消费分润规则中已选中 // 检查是否在其他消费分润规则中已选中
const isDuplicate = isVipSelectedInOtherRules(idx, vipId) const isDuplicate = isVipSelectedInOtherRules(idx, vipId)
if (!isDuplicate) { if (!isDuplicate) {
profitRules.value[idx].selectedVips.push(vipId) rule.selectedVips.push(vipId)
} else { } else {
uni.showToast({ uni.showToast({
title: '该会员类型已在其他规则中选中', title: '该会员类型已在其他规则中选中',
@@ -307,7 +308,6 @@ const vipList = ref([])
} }
.header-right { .header-right {
width: 60rpx;
text-align: right; text-align: right;
} }
@@ -540,24 +540,75 @@ const vipList = ref([])
white-space: nowrap; white-space: nowrap;
} }
/* 自定义checkbox样式 */
.custom-checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #dcdfe6;
border-radius: 8rpx;
background-color: #ffffff;
transition: all 0.3s ease-in-out;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.custom-checkbox.checked {
border-color: #409eff;
background-color: #409eff;
}
.custom-checkbox.checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 12rpx;
height: 20rpx;
border: 3rpx solid #ffffff;
border-top: none;
border-left: none;
transform: translate(-50%, -60%) rotate(45deg);
}
.custom-checkbox.disabled {
border-color: #dcdfe6;
background-color: #f5f7fa;
opacity: 0.5;
}
.custom-checkbox.disabled.checked {
border-color: #dcdfe6;
background-color: #dcdfe6;
}
.checkbox-label {
margin: 0 10rpx;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
/* 禁用状态样式 */ /* 禁用状态样式 */
.checkbox-label.disabled { .checkbox-label.disabled {
color: #c0c4cc !important; color: #c0c4cc !important;
opacity: 0.5 !important; opacity: 0.5 !important;
} }
/* 禁用的checkbox样式 */ /* 车辆标签样式 */
checkbox[disabled] { .car-tag {
opacity: 0.5 !important; display: inline-block;
padding: 2rpx 8rpx;
background-color: #ecf5ff;
color: #409eff;
font-size: 16rpx;
border-radius: 4rpx;
margin-right: 8rpx;
} }
checkbox[disabled] .uni-checkbox-input { /* 折扣价格样式 */
border-color: #dcdfe6 !important; .discount-text {
background-color: #f5f7fa !important; color: #67c23a !important;
} font-weight: 500;
checkbox[disabled] .uni-checkbox-input.uni-checkbox-input-checked {
border-color: #dcdfe6 !important;
background-color: #dcdfe6 !important;
} }
</style> </style>

View File

@@ -52,7 +52,7 @@
</view> </view>
</view> </view>
<view v-if="checkPermi(['work:distribution:profitRule'])" class="feature-card" @click="goToProfitRule"> <!-- <view v-if="checkPermi(['work:distribution:profitRule'])" class="feature-card" @click="goToProfitRule">
<view class="feature-icon user-icon"> <view class="feature-icon user-icon">
<view class="icon-text">💰</view> <view class="icon-text">💰</view>
</view> </view>
@@ -60,7 +60,7 @@
<view class="feature-title">分润规则配置</view> <view class="feature-title">分润规则配置</view>
<view class="feature-desc">配置扫码注册购买会员分润比例</view> <view class="feature-desc">配置扫码注册购买会员分润比例</view>
</view> </view>
</view> </view> -->
<view v-if="checkPermi(['work:distribution:profitRecord'])" class="feature-card" @click="goToDistributionData"> <view v-if="checkPermi(['work:distribution:profitRecord'])" class="feature-card" @click="goToDistributionData">
<view class="feature-icon dept-icon"> <view class="feature-icon dept-icon">

View File

@@ -18,6 +18,7 @@ export const useUserStore = defineStore('user', () => {
const avatar = ref(storage.get(constant.avatar)); const avatar = ref(storage.get(constant.avatar));
const roles = ref(storage.get(constant.roles)); const roles = ref(storage.get(constant.roles));
const permissions = ref(storage.get(constant.permissions)); const permissions = ref(storage.get(constant.permissions));
const userMenus = ref([]);
const SET_TOKEN = val => { const SET_TOKEN = val => {
token.value = val; token.value = val;
@@ -77,6 +78,7 @@ export const useUserStore = defineStore('user', () => {
const userid = isEmpty(user) || isEmpty(user.id) ? '' : user.id; const userid = isEmpty(user) || isEmpty(user.id) ? '' : user.id;
const username = isEmpty(user) || isEmpty(user.nickname) ? '' : user.nickname; const username = isEmpty(user) || isEmpty(user.nickname) ? '' : user.nickname;
const role = isEmpty(user) || isEmpty(user.currentRole) ? '' : user.currentRole; const role = isEmpty(user) || isEmpty(user.currentRole) ? '' : user.currentRole;
userMenus.value = res.data.menus || [];
if (res.data.roles && res.data.roles.length > 0) { if (res.data.roles && res.data.roles.length > 0) {
SET_ROLES(res.data.roles); SET_ROLES(res.data.roles);
SET_PERMISSIONS(res.data.permissions); SET_PERMISSIONS(res.data.permissions);
@@ -121,6 +123,7 @@ export const useUserStore = defineStore('user', () => {
currentRole, currentRole,
avatar, avatar,
roles, roles,
userMenus,
permissions, permissions,
SET_AVATAR, SET_AVATAR,
login: loginAction, login: loginAction,