Files
one-pipe-system/src/views/common/account-list.vue
sexygoat 2b3119c549
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m51s
fix(operator): fix operator edit issue
2026-04-10 14:00:21 +08:00

843 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ArtTableFullScreen>
<div class="account-list-page" id="table-full-screen">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon
><ElIcon><ArrowLeft /></ElIcon
></template>
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
</div>
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<!-- 操作按钮 -->
<div style="margin: 10px 0">
<ElButton
v-if="hasAuth(isShopType ? 'shop_account:add' : 'enterprise_account:add')"
@click="showAddAccountDialog"
>
{{ isShopType ? '新增店铺账号' : '新增企业账号' }}
</ElButton>
</div>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="ID"
:loading="loading"
:data="accountList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
v
height="60vh"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn
v-for="col in columns"
:key="col.prop || (col as any).type"
v-bind="col"
/>
</template>
</ArtTable>
<!-- 新增账号对话框 -->
<ElDialog
v-model="dialogVisible"
:title="isShopType ? '新增店铺账号' : '新增企业账号'"
width="500px"
>
<ElForm ref="formRef" :model="accountForm" :rules="accountRules" label-width="100px">
<ElFormItem label="用户名" prop="username">
<ElInput v-model="accountForm.username" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem label="密码" prop="password">
<ElInput
v-model="accountForm.password"
type="password"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="accountForm.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem label="账号类型" prop="user_type">
<ElInput :value="isShopType ? '代理账号' : '企业账号'" disabled />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" width="900px">
<template #header>
<div class="dialog-header">
<span class="dialog-title">{{
isShopType ? '分配店铺账号角色' : '分配企业客户角色'
}}</span>
<div class="account-info">
<span class="account-name">{{ currentAccountName }}</span>
</div>
</div>
</template>
<div class="role-transfer-container">
<!-- 左侧可分配角色列表 -->
<div class="transfer-panel">
<div class="panel-header">
<span class="panel-title">可分配角色</span>
<ElInput
v-model="leftRoleFilter"
placeholder="搜索角色"
clearable
size="small"
style="width: 180px"
/>
</div>
<div class="panel-body">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElCheckbox>
</div>
</ElCheckboxGroup>
</div>
</div>
<!-- 中间操作按钮 -->
<div class="transfer-buttons">
<ElButton
type="primary"
:icon="'ArrowRight'"
@click="addRoles"
:disabled="rolesToAdd.length === 0"
>
添加
</ElButton>
</div>
<!-- 右侧已分配角色列表 -->
<div class="transfer-panel">
<div class="panel-header">
<span class="panel-title">已分配角色</span>
<ElInput
v-model="rightRoleFilter"
placeholder="搜索角色"
clearable
size="small"
style="width: 180px"
/>
</div>
<div class="panel-body">
<div class="role-list">
<div
v-for="role in filteredAssignedRoles"
:key="role.ID"
class="role-item assigned-role-item"
>
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
移除
</ElButton>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h, computed, ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ElMessage,
ElTag,
ElIcon,
ElButton,
ElCard,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElCheckboxGroup
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { AccountService, RoleService } from '@/api/modules'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import type { SearchFormItem } from '@/types'
import type { PlatformAccount, PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'CommonAccountList' })
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
// 判断页面类型:通过 query 参数判断
const isShopType = computed(() => route.query.type === 'shop')
// 页面标题
const pageTitle = computed(() => (isShopType.value ? '店铺账号列表' : '企业客户账号列表'))
// 过滤参数键名
const filterParamKey = computed(() => (isShopType.value ? 'shop_id' : 'enterprise_id'))
// 用户类型映射
const getUserTypeName = (type: number): string => {
const typeMap: Record<number, string> = {
1: '超级管理员',
2: '平台用户',
3: '代理账号',
4: '企业账号'
}
return typeMap[type] || '未知'
}
// 状态名称映射
const getStatusName = (status: number): string => {
return status === 1 ? '启用' : '禁用'
}
const loading = ref(false)
const tableRef = ref()
const accountList = ref<PlatformAccount[]>([])
// 分配角色相关变量
const roleDialogVisible = ref(false)
const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('')
const currentAccountType = ref<number>(0)
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 搜索表单初始值
const initialSearchState = {
username: '',
phone: '',
user_type: undefined as number | undefined,
status: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '用户名',
prop: 'username',
type: 'input',
config: {
clearable: true,
placeholder: '请输入用户名'
}
},
{
label: '手机号',
prop: 'phone',
type: 'input',
config: {
clearable: true,
placeholder: '请输入手机号'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
}
]
// 获取用户类型标签类型
const getUserTypeTag = (type: number) => {
return type === 3 ? 'success' : 'primary'
}
// 获取状态标签类型
const getStatusTag = (status: number) => {
return status === 1 ? 'success' : 'info'
}
// 列配置
const columns = computed(() => {
const baseColumns = [
{
prop: 'username',
label: '用户名'
},
{
prop: 'phone',
label: '手机号'
},
{
prop: 'user_type',
label: '用户类型',
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () =>
getUserTypeName(row.user_type)
)
}
}
]
// 根据页面类型添加不同的列
if (isShopType.value) {
// 店铺类型:显示店铺名称
baseColumns.push({
prop: 'shop_name',
label: '店铺名称',
formatter: (row: PlatformAccount) => (row as any).shop_name || '-'
})
} else {
// 企业类型:显示企业名称
baseColumns.push({
prop: 'enterprise_name',
label: '企业名称',
formatter: (row: PlatformAccount) => (row as any).enterprise_name || '-'
})
}
// 添加状态和创建时间
baseColumns.push(
{
prop: 'status',
label: '状态',
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
}
},
{
prop: 'created_at',
label: '创建时间',
formatter: (row: PlatformAccount) => formatDateTime((row as any).created_at)
},
{
prop: 'operation',
label: '操作',
width: 160,
fixed: 'right',
formatter: (row: PlatformAccount) => {
// 根据类型检查权限
const permission = isShopType.value
? 'shop_account:patch_role'
: 'customer_accounts:patch_role'
if (!hasAuth(permission)) {
return h('span', '-')
}
return h(ArtButtonTable, {
text: isShopType.value ? '分配店铺账号角色' : '分配企业客户角色',
onClick: () => showRoleDialog(row)
})
}
}
)
return baseColumns
})
// 获取账号列表
const getTableData = async () => {
loading.value = true
try {
const entityId = Number(route.params.id)
const params: any = {
page: pagination.page,
pageSize: pagination.pageSize,
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
user_type: isShopType.value ? 3 : 4, // 根据页面类型自动设置: 3:代理账号, 4:企业账号
status: searchForm.status,
[filterParamKey.value]: entityId // 动态设置 shop_id 或 enterprise_id
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const res = await AccountService.getAccounts(params)
if (res.code === 0) {
accountList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
console.log('获取账号列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 返回上一页
const handleBack = () => {
router.back()
}
// 对话框相关
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 账号表单数据
const accountForm = reactive({
username: '',
password: '',
phone: ''
})
// 表单验证规则
const accountRules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_-]+$/,
message: '用户名只能包含字母、数字、下划线和横线',
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为 6-20 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
]
})
// 显示新增账号对话框
const showAddAccountDialog = () => {
// 重置表单
accountForm.username = ''
accountForm.password = ''
accountForm.phone = ''
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
dialogVisible.value = true
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const entityId = Number(route.params.id)
const data: any = {
username: accountForm.username,
password: accountForm.password,
phone: accountForm.phone,
user_type: isShopType.value ? 3 : 4 // 3:代理账号, 4:企业账号
}
// 根据类型添加关联ID
if (isShopType.value) {
data.shop_id = entityId
} else {
data.enterprise_id = entityId
}
await AccountService.createAccount(data)
ElMessage.success('添加账号成功')
dialogVisible.value = false
// 刷新列表
getTableData()
} catch (error) {
console.error(error)
console.log('添加账号失败')
} finally {
submitLoading.value = false
}
}
})
}
// 加载所有角色列表
const loadAllRoles = async () => {
try {
const res = await RoleService.getRoles({ page: 1, pageSize: 100 })
if (res.code === 0) {
allRoles.value = res.data.items || []
}
} catch (error) {
console.error('获取角色列表失败:', error)
}
}
// 计算属性:过滤后的可分配角色(根据账号类型过滤)
const filteredAvailableRoles = computed(() => {
let roles = allRoles.value
// 根据账号类型过滤角色
if (currentAccountType.value === 3) {
// 代理账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
} else if (currentAccountType.value === 2) {
// 平台用户:只显示平台角色
roles = roles.filter((role) => role.role_type === 1)
} else if (currentAccountType.value === 4) {
// 企业账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
}
// 根据搜索关键词过滤
if (!leftRoleFilter.value) return roles
const keyword = leftRoleFilter.value.toLowerCase()
return roles.filter((role) => role.role_name.toLowerCase().includes(keyword))
})
// 计算属性:过滤后的已分配角色
const filteredAssignedRoles = computed(() => {
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID))
if (!rightRoleFilter.value) return assignedRolesList
const keyword = rightRoleFilter.value.toLowerCase()
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
})
// 显示分配角色对话框
const showRoleDialog = async (row: PlatformAccount) => {
currentAccountId.value = (row as any).id
currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
leftRoleFilter.value = ''
rightRoleFilter.value = ''
try {
// 每次打开对话框时重新加载最新的角色列表
await loadAllRoles()
// 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles((row as any).id)
if (res.code === 0) {
// 提取角色ID数组
const roles = res.data || []
// 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
// 数据加载完成后再打开对话框
roleDialogVisible.value = true
}
} catch (error) {
console.error('获取账号角色失败:', error)
}
}
// 批量添加角色
const addRoles = async () => {
if (rolesToAdd.value.length === 0) return
try {
let newRoles: number[]
// 代理账号只能分配一个角色,会覆盖之前的角色
if (currentAccountType.value === 3) {
if (rolesToAdd.value.length > 1) {
ElMessage.warning('代理账号只能分配一个角色')
return
}
// 只保留新选择的一个角色
newRoles = rolesToAdd.value
} else {
// 其他账号类型可以分配多个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
}
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
ElMessage.success('角色分配成功')
// 刷新列表以更新角色显示
await getTableData()
} catch (error) {
console.error('分配角色失败:', error)
}
}
// 移除单个角色
const removeSingleRole = async (roleId: number) => {
try {
// 从已分配列表中移除该角色
const newRoles = selectedRoles.value.filter((id) => id !== roleId)
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
ElMessage.success('角色移除成功')
// 刷新列表以更新角色显示
await getTableData()
} catch (error) {
console.error('移除角色失败:', error)
}
}
onMounted(() => {
getTableData()
loadAllRoles()
})
</script>
<style lang="scss" scoped>
.account-list-page {
.detail-header {
display: flex;
gap: 16px;
align-items: center;
padding-bottom: 16px;
.detail-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.dialog-title {
font-size: 18px;
font-weight: 600;
}
.account-info {
display: flex;
gap: 8px;
align-items: center;
.account-name {
font-size: 14px;
color: var(--el-text-color-regular);
}
}
}
.role-transfer-container {
display: flex;
gap: 20px;
align-items: stretch;
justify-content: space-between;
min-height: 500px;
padding: 20px 0;
.transfer-panel {
display: flex;
flex: 1;
flex-direction: column;
max-width: 380px;
overflow: hidden;
border: 1px solid var(--el-border-color);
border-radius: 4px;
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color);
.panel-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.panel-body {
flex: 1;
padding: 12px;
overflow-y: auto;
.role-list {
display: flex;
flex-direction: column;
gap: 8px;
.role-item {
padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
transition: all 0.2s;
&:hover {
background: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
:deep(.el-checkbox) {
width: 100%;
.el-checkbox__label {
display: flex;
width: 100%;
.role-info {
display: flex;
flex: 1;
gap: 8px;
align-items: center;
justify-content: space-between;
width: 100%;
> span:first-child {
flex: 1;
min-width: 0;
}
}
}
}
}
.assigned-role-item {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
.role-info {
display: flex;
flex: 1;
gap: 8px;
align-items: center;
justify-content: space-between;
> span:first-child {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.el-button {
flex-shrink: 0;
}
}
}
}
}
.transfer-buttons {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
padding: 0 10px;
.el-button {
width: 100px;
}
}
}
</style>