Files
one-pipe-system/src/views/common/account-list.vue
luo b2a76adc9a
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s
fix: some
2026-10-13 11:25:03 +08:00

946 lines
28 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="30%"
>
<ElForm ref="formRef" :model="accountForm" :rules="accountRules" label-width="80">
<ElFormItem label="用户名" prop="username">
<ElInput v-model="accountForm.username" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem label="密码" prop="password">
<ElInput
v-model="accountForm.password"
type="password"
:placeholder="NEW_LOGIN_PASSWORD_HINT"
maxlength="12"
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"
@input="handleLeftRoleSearch"
/>
</div>
<div class="panel-body">
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
<template v-else-if="isPlatformUser">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div
v-for="role in filteredAvailableRoles"
:key="getRoleId(role)"
class="role-item"
>
<ElCheckbox
:label="getRoleId(role)"
:disabled="selectedRoles.includes(getRoleId(role))"
>
<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>
</template>
<template v-else>
<ElRadioGroup v-model="roleToAdd" class="role-list">
<div
v-for="role in filteredAvailableRoles"
:key="getRoleId(role)"
class="role-item"
>
<ElRadio
:label="getRoleId(role)"
:disabled="selectedRoles.includes(getRoleId(role))"
>
<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>
</ElRadio>
</div>
</ElRadioGroup>
</template>
</div>
</div>
<!-- 中间:操作按钮 -->
<div class="transfer-buttons">
<ElButton
type="primary"
:icon="'ArrowRight'"
@click="addRoles"
:disabled="isPlatformUser ? rolesToAdd.length === 0 : !roleToAdd"
>
添加
</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="getRoleId(role)"
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(getRoleId(role))"
>
移除
</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 {
createNewLoginPasswordRule,
NEW_LOGIN_PASSWORD_HINT
} from '@/utils/validation/loginPassword'
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,
ElRadioGroup,
ElRadio,
ElEmpty
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { AccountService, RoleService, ShopService, EnterpriseService } 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 { getCompatibleNumericId } from '@/utils/business/id'
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 entityName = ref('')
const pageTitle = computed(() => {
const baseTitle = isShopType.value ? '店铺账号列表' : '企业客户账号列表'
return entityName.value ? `${baseTitle} - ${entityName.value}` : baseTitle
})
// 过滤参数键名
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 roleToAdd = ref<number | undefined>(undefined) // 单选时使用
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 是否为平台用户(平台用户可多选,其他单选)
const isPlatformUser = computed(() => currentAccountType.value === 2)
const getAccountId = (account: PlatformAccount | any) => getCompatibleNumericId(account) ?? 0
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
// 搜索表单初始值
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<any[]>(() => {
const baseColumns: any[] = [
{
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) => h('span', (row as any).shop_name || '-')
})
} else {
// 企业类型:显示企业名称
baseColumns.push({
prop: 'enterprise_name',
label: '企业名称',
formatter: (row: PlatformAccount) => h('span', (row as any).enterprise_name || '-')
})
}
baseColumns.push(
{
prop: 'wecom_bound',
label: '企微绑定',
formatter: (row: PlatformAccount) =>
h(ElTag, { type: row.wecom_bound ? 'success' : 'info' }, () =>
row.wecom_bound ? '已绑定' : '未绑定'
)
},
{
prop: 'wecom_name',
label: '企微用户名称',
formatter: (row: PlatformAccount) => h('span', row.wecom_name || '-')
}
)
// 添加状态和创建时间
baseColumns.push(
{
prop: 'status',
label: '状态',
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
}
},
{
prop: 'created_at',
label: '创建时间',
width: 160,
showOverflowTooltip: true,
formatter: (row: PlatformAccount) => h('span', 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 loadEntityName = async () => {
const entityId = Number(route.params.id)
if (!entityId) return
try {
if (isShopType.value) {
const res = await ShopService.getShops({ id: entityId, page: 1, page_size: 1 })
if (res.code === 0 && res.data.items?.length) {
entityName.value = res.data.items[0].shop_name || ''
}
} else {
const res = await EnterpriseService.getEnterprises({ id: entityId, page: 1, page_size: 1 })
if (res.code === 0 && res.data.items?.length) {
entityName.value = res.data.items[0].enterprise_name || ''
}
}
} catch (error) {
console.error('获取名称失败:', error)
}
}
// 从账号数据中提取店铺/企业名称
const extractEntityNameFromAccounts = () => {
if (entityName.value || !accountList.value.length) return
const firstAccount = accountList.value[0]
if (firstAccount) {
entityName.value =
(firstAccount as any).shop_name || (firstAccount as any).enterprise_name || ''
}
}
// 获取账号列表
const getTableData = async () => {
loading.value = true
try {
const entityId = Number(route.params.id)
const params: any = {
page: pagination.page,
page_size: 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
extractEntityNameFromAccounts()
}
} 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' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
createNewLoginPasswordRule()
],
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
}
const res = await AccountService.createAccount(data)
if (res.code !== 0) {
ElMessage.error(res.msg || res.message || '添加账号失败')
return
}
ElMessage.success('添加账号成功')
dialogVisible.value = false
// 刷新列表
await getTableData()
} catch {
// HTTP 层已提示,不输出包含新密码的请求错误对象。
} finally {
submitLoading.value = false
}
}
})
}
// 加载所有角色列表
const loadAllRoles = async (keyword: string = '', roleType?: number) => {
try {
const params: any = {
page: 1,
page_size: keyword ? 50 : 20, // 搜索时加载更多,默认20条
role_type: roleType !== undefined ? roleType : 2 // 默认为客户角色
}
if (keyword) {
params.role_name = keyword
}
const res = await RoleService.getRoles(params)
if (res.code === 0) {
allRoles.value = res.data.items || []
}
} catch (error) {
console.error('获取角色列表失败:', error)
}
}
// 左侧角色搜索防抖
let leftSearchTimer: ReturnType<typeof setTimeout> | null = null
const handleLeftRoleSearch = (val: string) => {
if (leftSearchTimer) {
clearTimeout(leftSearchTimer)
}
leftSearchTimer = setTimeout(() => {
// 根据当前账号类型决定加载哪种角色
const roleType = currentAccountType.value === 2 ? 1 : 2 // 平台用户(2)加载平台角色(1),其他加载客户角色(2)
loadAllRoles(val, roleType)
}, 300)
}
// 计算属性:过滤后的可分配角色
const filteredAvailableRoles = computed(() => {
return allRoles.value
})
// 计算属性:过滤后的已分配角色
const filteredAssignedRoles = computed(() => {
const assignedRolesList = allRoles.value.filter((role) =>
selectedRoles.value.includes(getRoleId(role))
)
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) => {
const accountId = getAccountId(row)
if (!accountId) {
ElMessage.error('当前账号缺少账号标识,无法分配角色')
return
}
currentAccountId.value = accountId
currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
roleToAdd.value = undefined
leftRoleFilter.value = ''
rightRoleFilter.value = ''
try {
// 根据账号类型加载对应的角色列表
// 平台用户(user_type=2) -> 平台角色(role_type=1)
// 企业账号/代理账号(user_type=3,4) -> 客户角色(role_type=2)
const roleType = row.user_type === 2 ? 1 : 2
await loadAllRoles('', roleType)
// 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles(accountId)
if (res.code === 0) {
// 提取角色ID数组
const roles = res.data || []
// 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => getRoleId(role)).filter((id) => id > 0)
// 数据加载完成后再打开对话框
roleDialogVisible.value = true
}
} catch (error) {
console.error('获取账号角色失败:', error)
}
}
// 批量添加角色
const addRoles = async () => {
try {
let newRoles: number[]
// 平台用户可以分配多个角色,其他用户只能分配一个角色
if (isPlatformUser.value) {
if (rolesToAdd.value.length === 0) return
// 平台用户可以分配多个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
} else {
// 其他用户只能分配一个角色(单选)
if (!roleToAdd.value) return
newRoles = [...selectedRoles.value, roleToAdd.value]
}
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
roleToAdd.value = undefined
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(() => {
loadEntityName()
getTableData()
})
</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: 440px;
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;
width: 100%;
.role-item {
box-sizing: border-box;
display: block;
width: 100%;
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);
}
.role-info {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
}
:deep(.el-checkbox),
:deep(.el-radio) {
display: flex;
width: 100%;
.el-checkbox__label,
.el-radio__label {
display: flex;
width: 100%;
}
}
}
.assigned-role-item {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
.role-info {
flex: 1;
}
.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>