修改工单
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m26s

This commit is contained in:
sexygoat
2026-02-25 16:14:38 +08:00
parent ca3184857e
commit dccad819cf
20 changed files with 2163 additions and 1229 deletions

View File

@@ -4,7 +4,9 @@
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon><ElIcon><ArrowLeft /></ElIcon></template>
<template #icon
><ElIcon><ArrowLeft /></ElIcon
></template>
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
@@ -19,6 +21,13 @@
@search="handleSearch"
></ArtSearchBar>
<!-- 操作按钮 -->
<div style="margin: 10px 0">
<ElButton @click="showAddAccountDialog">
{{ isShopType ? '新增店铺账号' : '新增企业账号' }}
</ElButton>
</div>
<!-- 表格 -->
<ArtTable
ref="tableRef"
@@ -28,28 +37,176 @@
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"v
: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" />
<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">分配角色</span>
<div class="account-info">
<span class="account-name">{{ currentAccountName }}</span>
<ElTag v-if="currentAccountType === 3" type="warning" size="small" style="margin-left: 8px">
代理账号只能分配一个客户角色
</ElTag>
</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 } from 'vue'
import { h, computed, ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElTag, ElIcon, ElButton, ElCard } from 'element-plus'
import {
ElMessage,
ElTag,
ElIcon,
ElButton,
ElCard,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElCheckboxGroup
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { AccountService } from '@/api/modules'
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 } from '@/types/api'
import type { PlatformAccount, PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'CommonAccountList' })
@@ -86,6 +243,17 @@
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: '',
@@ -124,19 +292,6 @@
placeholder: '请输入手机号'
}
},
{
label: '用户类型',
prop: 'user_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
]
},
{
label: '状态',
prop: 'status',
@@ -163,52 +318,74 @@
}
// 列配置
const columns = computed(() => [
{
prop: 'username',
label: '用户名',
minWidth: 150
},
{
prop: 'phone',
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '用户类型',
width: 110,
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () => getUserTypeName(row.user_type))
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)
)
}
}
},
{
prop: 'shop_id',
label: '店铺ID',
width: 100,
formatter: (row: PlatformAccount) => row.shop_id || '-'
},
{
prop: 'enterprise_id',
label: '企业ID',
width: 100,
formatter: (row: PlatformAccount) => row.enterprise_id || '-'
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
}
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
]
// 根据页面类型添加不同的列
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: 120,
fixed: 'right',
formatter: (row: PlatformAccount) => {
return h(ArtButtonTable, {
text: '分配角色',
onClick: () => showRoleDialog(row)
})
}
}
)
return baseColumns
})
// 获取账号列表
const getTableData = async () => {
@@ -220,7 +397,7 @@
pageSize: pagination.pageSize,
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
user_type: searchForm.user_type,
user_type: isShopType.value ? 3 : 4, // 根据页面类型自动设置: 3:代理账号, 4:企业账号
status: searchForm.status,
[filterParamKey.value]: entityId // 动态设置 shop_id 或 enterprise_id
}
@@ -274,8 +451,214 @@
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' }
],
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)
ElMessage.error('添加账号失败')
} 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)
ElMessage.error('角色移除失败')
}
}
onMounted(() => {
getTableData()
loadAllRoles()
})
</script>
@@ -285,9 +668,7 @@
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
.detail-title {
margin: 0;
@@ -297,4 +678,128 @@
}
}
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.dialog-title {
font-size: 18px;
font-weight: 600;
}
.account-info {
display: flex;
align-items: center;
gap: 8px;
.account-name {
font-size: 14px;
color: var(--el-text-color-regular);
}
}
}
.role-transfer-container {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px;
padding: 20px 0;
min-height: 500px;
.transfer-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
max-width: 380px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
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;
overflow-y: auto;
padding: 12px;
.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);
}
.role-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
:deep(.el-checkbox) {
width: 100%;
.el-checkbox__label {
width: 100%;
}
}
}
.assigned-role-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
.role-info {
flex: 1;
}
.el-button {
flex-shrink: 0;
}
}
}
}
}
.transfer-buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
padding: 0 10px;
.el-button {
width: 100px;
}
}
}
</style>