Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

View File

@@ -0,0 +1,522 @@
<template>
<ArtTableFullScreen>
<div class="account-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showDialog('add')">新增账号</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="ID"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
width="500px"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
<ElFormItem label="账号名称" prop="username">
<ElInput v-model="formData.username" placeholder="请输入账号名称" />
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
<ElInput
v-model="formData.password"
type="password"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem label="账号类型" prop="user_type">
<ElSelect
v-model="formData.user_type"
placeholder="请选择账号类型"
style="width: 100%"
>
<ElOption label="超级管理员" :value="1" />
<ElOption label="平台用户" :value="2" />
<ElOption label="代理账号" :value="3" />
<ElOption label="企业账号" :value="4" />
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
<ElRadioGroup v-model="selectedRole" class="role-radio-group">
<div v-for="role in allRoles" :key="role.ID" class="role-radio-item">
<ElRadio :label="role.ID">
{{ role.role_name }}
<ElTag
:type="role.role_type === 1 ? 'primary' : 'success'"
size="small"
style="margin-left: 8px"
>
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</ElRadio>
</div>
</ElRadioGroup>
<template #footer>
<div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
>提交</ElButton
>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { FormInstance, ElSwitch } from 'element-plus'
import { ElMessageBox, ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role'
import type { SearchFormItem } from '@/types'
import type { PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
const dialogType = ref('add')
const dialogVisible = ref(false)
const roleDialogVisible = ref(false)
const loading = ref(false)
const roleSubmitLoading = ref(false)
const currentAccountId = ref<number>(0)
const selectedRole = ref<number | null>(null)
const allRoles = ref<PlatformRole[]>([])
// 定义表单搜索初始值
const initialSearchState = {
name: '',
phone: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1 // 重置到第一页
getAccountList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1 // 搜索时重置到第一页
getAccountList()
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '账号名称',
prop: 'name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入账号名称'
}
},
{
label: '手机号',
prop: 'phone',
type: 'input',
config: {
clearable: true,
placeholder: '请输入手机号'
}
}
]
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'ID' },
{ label: '账号名称', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '账号类型', prop: 'user_type_name' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' },
{ label: '操作', prop: 'operation' }
]
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
// 重置表单验证状态
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.id = row.ID
formData.username = row.username
formData.phone = row.phone
formData.user_type = row.user_type
formData.password = ''
} else {
formData.id = ''
formData.username = ''
formData.phone = ''
formData.user_type = 2
formData.password = ''
}
}
// 删除账号
const deleteAccount = (row: any) => {
ElMessageBox.confirm(`确定要删除账号 ${row.username} 吗?`, '删除账号', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await AccountService.deleteAccount(row.ID)
ElMessage.success('删除成功')
getAccountList()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 账号取消删除
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'ID',
label: 'ID',
width: 80
},
{
prop: 'username',
label: '账号名称',
minWidth: 120
},
{
prop: 'phone',
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '账号类型',
width: 120,
formatter: (row: any) => {
const typeMap: Record<number, string> = {
1: '超级管理员',
2: '平台用户',
3: '代理账号',
4: '企业账号'
}
return typeMap[row.user_type] || '-'
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
})
}
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt)
},
{
prop: 'operation',
label: '操作',
width: 180,
fixed: 'right',
formatter: (row: any) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
icon: '&#xe72b;',
onClick: () => showRoleDialog(row)
}),
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteAccount(row)
})
])
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: '',
username: '',
password: '',
phone: '',
user_type: 2
})
onMounted(() => {
getAccountList()
loadAllRoles()
})
// 加载所有角色列表
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 showRoleDialog = async (row: any) => {
currentAccountId.value = row.ID
selectedRole.value = null
// 先加载当前账号的角色,再打开对话框
try {
const res = await AccountService.getAccountRoles(row.ID)
if (res.code === 0) {
// 提取角色ID只取第一个角色
const roles = res.data || []
selectedRole.value = roles.length > 0 ? roles[0].ID : null
// 数据加载完成后再打开对话框
roleDialogVisible.value = true
}
} catch (error) {
console.error('获取账号角色失败:', error)
}
}
// 提交分配角色
const handleAssignRoles = async () => {
if (selectedRole.value === null) {
ElMessage.warning('请选择一个角色')
return
}
roleSubmitLoading.value = true
try {
// 将单个角色ID包装成数组传给后端
await AccountService.assignRolesToAccount(currentAccountId.value, [selectedRole.value])
ElMessage.success('分配角色成功')
roleDialogVisible.value = false
} catch (error) {
console.error(error)
} finally {
roleSubmitLoading.value = false
}
}
// 获取账号列表
const getAccountList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
keyword: formFilters.name || formFilters.phone || undefined
}
const res = await AccountService.getAccounts(params)
if (res.code === 0) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('获取账号列表失败:', error)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getAccountList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
const data: any = {
username: formData.username,
phone: formData.phone,
user_type: formData.user_type
}
if (dialogType.value === 'add') {
data.password = formData.password
await AccountService.createAccount(data)
ElMessage.success('添加成功')
} else {
await AccountService.updateAccount(Number(formData.id), data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
getAccountList()
} catch (error) {
console.error(error)
}
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getAccountList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getAccountList()
}
// 状态切换
const handleStatusChange = async (row: any, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await AccountService.updateAccount(row.ID, { status: newStatus })
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
</script>
<style lang="scss" scoped>
.account-page {
// 账号管理页面样式
}
.role-radio-group {
width: 100%;
}
.role-radio-item {
margin-bottom: 16px;
padding: 8px;
}
</style>

View File

@@ -0,0 +1,468 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="代理商名称/联系人" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="levelFilter" placeholder="代理商等级" clearable style="width: 100%">
<ElOption label="一级代理" value="1" />
<ElOption label="二级代理" value="2" />
<ElOption label="三级代理" value="3" />
</ElSelect>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="showDialog('add')">新增代理商</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index row-key="id">
<template #default>
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
<ElTableColumn label="代理商编码" prop="agentCode" />
<ElTableColumn label="等级" prop="level">
<template #default="scope">
<ElTag :type="getLevelTagType(scope.row.level)">
{{ getLevelText(scope.row.level) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="上级代理" prop="parentName" show-overflow-tooltip />
<ElTableColumn label="联系人" prop="contactPerson" />
<ElTableColumn label="联系电话" prop="contactPhone" />
<ElTableColumn label="账号数量" prop="accountCount" />
<ElTableColumn label="网卡数量" prop="simCardCount" />
<ElTableColumn label="佣金总额" prop="totalCommission">
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="280">
<template #default="scope">
<el-button link @click="viewAccountList(scope.row)">账号管理</el-button>
<el-button link @click="viewCommission(scope.row)">佣金配置</el-button>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button link @click="handleDelete(scope.row)">删除</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理商' : '编辑代理商'"
width="700px"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="代理商名称" prop="agentName">
<ElInput v-model="form.agentName" placeholder="请输入代理商名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="代理商编码" prop="agentCode">
<ElInput v-model="form.agentCode" placeholder="请输入代理商编码" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="代理商等级" prop="level">
<ElSelect v-model="form.level" placeholder="请选择代理商等级" style="width: 100%">
<ElOption label="一级代理" :value="1" />
<ElOption label="二级代理" :value="2" />
<ElOption label="三级代理" :value="3" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="上级代理" prop="parentId">
<ElSelect v-model="form.parentId" placeholder="请选择上级代理" clearable style="width: 100%">
<ElOption label="无" :value="null" />
<ElOption
v-for="agent in parentAgentOptions"
:key="agent.id"
:label="agent.agentName"
:value="agent.id"
/>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="联系人" prop="contactPerson">
<ElInput v-model="form.contactPerson" placeholder="请输入联系人" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="联系电话" prop="contactPhone">
<ElInput v-model="form.contactPhone" placeholder="请输入联系电话" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="form.email" placeholder="请输入邮箱" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="公司地址" prop="address">
<ElInput v-model="form.address" placeholder="请输入公司地址" />
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 账号管理对话框 -->
<ElDialog v-model="accountDialogVisible" title="代理商账号管理" width="900px" align-center>
<div style="margin-bottom: 16px">
<ElButton type="primary" size="small" @click="addSubAccount">创建子账号</ElButton>
</div>
<ArtTable :data="currentAgentAccounts" index>
<template #default>
<ElTableColumn label="账号" prop="username" />
<ElTableColumn label="真实姓名" prop="realName" />
<ElTableColumn label="手机号" prop="phone" />
<ElTableColumn label="角色" prop="role" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '正常' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" />
<ElTableColumn fixed="right" label="操作" width="150">
<template #default="scope">
<el-button link>编辑</el-button>
<el-button link>禁用</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
</ElDialog>
<!-- 佣金配置对话框 -->
<ElDialog v-model="commissionDialogVisible" title="佣金配置" width="600px" align-center>
<ElForm label-width="120px">
<ElFormItem label="佣金模式">
<ElRadioGroup v-model="commissionForm.mode">
<ElRadio value="fixed">固定佣金</ElRadio>
<ElRadio value="percent">比例佣金</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="commissionForm.mode === 'fixed'" label="固定金额">
<ElInputNumber v-model="commissionForm.fixedAmount" :min="0" :precision="2" />
<span style="margin-left: 8px">/</span>
</ElFormItem>
<ElFormItem v-if="commissionForm.mode === 'percent'" label="佣金比例">
<ElInputNumber v-model="commissionForm.percent" :min="0" :max="100" :precision="2" />
<span style="margin-left: 8px">%</span>
</ElFormItem>
<ElFormItem label="结算周期">
<ElSelect v-model="commissionForm.settlementCycle" style="width: 100%">
<ElOption label="实时结算" value="realtime" />
<ElOption label="日结" value="daily" />
<ElOption label="周结" value="weekly" />
<ElOption label="月结" value="monthly" />
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="commissionDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="saveCommissionConfig">保存</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'AgentManagement' })
interface Agent {
id?: string
agentName: string
agentCode: string
level: number
parentId?: string | null
parentName?: string
contactPerson: string
contactPhone: string
email?: string
address?: string
remark?: string
accountCount?: number
simCardCount?: number
totalCommission?: number
status: 'active' | 'inactive'
createTime?: string
}
// Mock 数据
const mockData = ref<Agent[]>([
{
id: '1',
agentName: '华东区总代理',
agentCode: 'AGENT_HD_001',
level: 1,
parentId: null,
parentName: '无',
contactPerson: '张三',
contactPhone: '13800138001',
email: 'zhangsan@example.com',
address: '上海市浦东新区',
accountCount: 5,
simCardCount: 1000,
totalCommission: 158900.50,
status: 'active',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
agentName: '江苏省代理',
agentCode: 'AGENT_JS_001',
level: 2,
parentId: '1',
parentName: '华东区总代理',
contactPerson: '李四',
contactPhone: '13800138002',
email: 'lisi@example.com',
address: '江苏省南京市',
accountCount: 3,
simCardCount: 500,
totalCommission: 78500.00,
status: 'active',
createTime: '2026-01-05 11:00:00'
},
{
id: '3',
agentName: '南京市代理',
agentCode: 'AGENT_NJ_001',
level: 3,
parentId: '2',
parentName: '江苏省代理',
contactPerson: '王五',
contactPhone: '13800138003',
email: 'wangwu@example.com',
address: '江苏省南京市玄武区',
accountCount: 2,
simCardCount: 200,
totalCommission: 32800.00,
status: 'active',
createTime: '2026-01-10 12:00:00'
}
])
const searchQuery = ref('')
const levelFilter = ref('')
const dialogVisible = ref(false)
const accountDialogVisible = ref(false)
const commissionDialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const form = reactive<Agent>({
agentName: '',
agentCode: '',
level: 1,
parentId: null,
contactPerson: '',
contactPhone: '',
email: '',
address: '',
remark: '',
status: 'active'
})
const commissionForm = reactive({
mode: 'fixed',
fixedAmount: 10,
percent: 5,
settlementCycle: 'monthly'
})
const currentAgentAccounts = ref([
{
username: 'agent001',
realName: '张三',
phone: '13800138001',
role: '管理员',
status: 'active',
createTime: '2026-01-01 10:00:00'
}
])
const rules = reactive<FormRules>({
agentName: [{ required: true, message: '请输入代理商名称', trigger: 'blur' }],
agentCode: [{ required: true, message: '请输入代理商编码', trigger: 'blur' }],
level: [{ required: true, message: '请选择代理商等级', trigger: 'change' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
})
const parentAgentOptions = computed(() => {
return mockData.value.filter((item) => item.level < form.level)
})
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.agentName.includes(searchQuery.value) || item.contactPerson.includes(searchQuery.value)
)
}
if (levelFilter.value) {
data = data.filter((item) => item.level === parseInt(levelFilter.value))
}
return data
})
const getLevelText = (level: number) => {
const levelMap: Record<number, string> = { 1: '一级代理', 2: '二级代理', 3: '三级代理' }
return levelMap[level] || '未知'
}
const getLevelTagType = (level: number) => {
const typeMap: Record<number, string> = { 1: '', 2: 'success', 3: 'warning' }
return typeMap[level] || 'info'
}
const handleSearch = () => {
// 搜索逻辑已通过 computed 实现
}
const showDialog = (type: 'add' | 'edit', row?: Agent) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
Object.assign(form, row)
} else {
resetForm()
}
}
const resetForm = () => {
Object.assign(form, {
agentName: '',
agentCode: '',
level: 1,
parentId: null,
contactPerson: '',
contactPhone: '',
email: '',
address: '',
remark: '',
status: 'active'
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
mockData.value.push({
...form,
id: Date.now().toString(),
parentName: parentAgent ? parentAgent.agentName : '无',
accountCount: 0,
simCardCount: 0,
totalCommission: 0,
createTime: new Date().toLocaleString('zh-CN')
})
ElMessage.success('新增成功')
} else {
const index = mockData.value.findIndex((item) => item.id === form.id)
if (index !== -1) {
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
mockData.value[index] = {
...form,
parentName: parentAgent ? parentAgent.agentName : '无'
}
}
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const handleDelete = (row: Agent) => {
ElMessageBox.confirm('确定删除该代理商吗?删除后其下属代理商和账号也将被删除。', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
const index = mockData.value.findIndex((item) => item.id === row.id)
if (index !== -1) {
mockData.value.splice(index, 1)
}
ElMessage.success('删除成功')
})
}
const viewAccountList = (row: Agent) => {
accountDialogVisible.value = true
// 实际应用中应该根据代理商ID加载账号列表
}
const viewCommission = (row: Agent) => {
commissionDialogVisible.value = true
// 实际应用中应该根据代理商ID加载佣金配置
}
const addSubAccount = () => {
ElMessage.info('创建子账号功能')
}
const saveCommissionConfig = () => {
ElMessage.success('佣金配置保存成功')
commissionDialogVisible.value = false
}
</script>
<style lang="scss" scoped>
.page-content {
// 自定义样式
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="账号/姓名/手机号" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="typeFilter" placeholder="客户类型" clearable style="width: 100%">
<ElOption label="代理商" value="agent" />
<ElOption label="企业客户" value="enterprise" />
</ElSelect>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="statusFilter" placeholder="账号状态" clearable style="width: 100%">
<ElOption label="正常" value="active" />
<ElOption label="禁用" value="disabled" />
<ElOption label="已锁定" value="locked" />
</ElSelect>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index>
<template #default>
<ElTableColumn label="账号" prop="username" min-width="120" />
<ElTableColumn label="真实姓名" prop="realName" />
<ElTableColumn label="客户类型" prop="customerType">
<template #default="scope">
<ElTag :type="scope.row.customerType === 'agent' ? '' : 'success'">
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="所属组织" prop="organizationName" show-overflow-tooltip />
<ElTableColumn label="手机号" prop="phone" />
<ElTableColumn label="邮箱" prop="email" show-overflow-tooltip />
<ElTableColumn label="登录次数" prop="loginCount" />
<ElTableColumn label="最后登录" prop="lastLoginTime" width="180" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="getStatusTagType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="280">
<template #default="scope">
<el-button link @click="viewDetail(scope.row)">详情</el-button>
<el-button link @click="unbindPhone(scope.row)">解绑手机</el-button>
<el-button link @click="resetPassword(scope.row)">重置密码</el-button>
<el-button
link
:type="scope.row.status === 'active' ? 'danger' : 'primary'"
@click="toggleStatus(scope.row)"
>
{{ scope.row.status === 'active' ? '禁用' : '启用' }}
</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 操作记录对话框 -->
<ElDialog v-model="detailDialogVisible" title="账号详情" width="800px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="账号">{{ currentAccount?.username }}</ElDescriptionsItem>
<ElDescriptionsItem label="真实姓名">{{ currentAccount?.realName }}</ElDescriptionsItem>
<ElDescriptionsItem label="客户类型">{{
currentAccount?.customerType === 'agent' ? '代理商' : '企业客户'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="所属组织">{{
currentAccount?.organizationName
}}</ElDescriptionsItem>
<ElDescriptionsItem label="手机号">{{ currentAccount?.phone }}</ElDescriptionsItem>
<ElDescriptionsItem label="邮箱">{{ currentAccount?.email }}</ElDescriptionsItem>
<ElDescriptionsItem label="注册时间">{{ currentAccount?.createTime }}</ElDescriptionsItem>
<ElDescriptionsItem label="最后登录">{{
currentAccount?.lastLoginTime
}}</ElDescriptionsItem>
<ElDescriptionsItem label="登录次数">{{ currentAccount?.loginCount }}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getStatusTagType(currentAccount?.status || 'active')">
{{ getStatusText(currentAccount?.status || 'active') }}
</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<ElDivider>操作记录</ElDivider>
<ArtTable :data="operationRecords" index max-height="300">
<template #default>
<ElTableColumn label="操作类型" prop="operationType" />
<ElTableColumn label="操作描述" prop="description" />
<ElTableColumn label="操作人" prop="operator" />
<ElTableColumn label="操作时间" prop="operateTime" width="180" />
</template>
</ArtTable>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({ name: 'CustomerAccount' })
interface CustomerAccount {
id: string
username: string
realName: string
customerType: 'agent' | 'enterprise'
organizationName: string
phone: string
email: string
loginCount: number
lastLoginTime: string
status: 'active' | 'disabled' | 'locked'
createTime: string
}
// Mock 数据
const mockData = ref<CustomerAccount[]>([
{
id: '1',
username: 'agent001',
realName: '张三',
customerType: 'agent',
organizationName: '华东区总代理',
phone: '13800138001',
email: 'zhangsan@example.com',
loginCount: 328,
lastLoginTime: '2026-01-09 08:30:00',
status: 'active',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
username: 'enterprise001',
realName: '李四',
customerType: 'enterprise',
organizationName: '某某科技有限公司',
phone: '13800138002',
email: 'lisi@example.com',
loginCount: 156,
lastLoginTime: '2026-01-08 18:45:00',
status: 'active',
createTime: '2026-01-03 11:00:00'
},
{
id: '3',
username: 'agent002',
realName: '王五',
customerType: 'agent',
organizationName: '江苏省代理',
phone: '13800138003',
email: 'wangwu@example.com',
loginCount: 89,
lastLoginTime: '2026-01-07 14:20:00',
status: 'disabled',
createTime: '2026-01-05 12:00:00'
}
])
const operationRecords = ref([
{
operationType: '重置密码',
description: '管理员重置登录密码',
operator: 'admin',
operateTime: '2026-01-08 10:00:00'
},
{
operationType: '解绑手机',
description: '解绑原手机号并重新绑定',
operator: 'admin',
operateTime: '2026-01-06 15:30:00'
}
])
const searchQuery = ref('')
const typeFilter = ref('')
const statusFilter = ref('')
const detailDialogVisible = ref(false)
const currentAccount = ref<CustomerAccount | null>(null)
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
data = data.filter(
(item) =>
item.username.toLowerCase().includes(query) ||
item.realName.includes(query) ||
item.phone.includes(query)
)
}
if (typeFilter.value) {
data = data.filter((item) => item.customerType === typeFilter.value)
}
if (statusFilter.value) {
data = data.filter((item) => item.status === statusFilter.value)
}
return data
})
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
active: '正常',
disabled: '禁用',
locked: '已锁定'
}
return statusMap[status] || '未知'
}
const getStatusTagType = (status: string) => {
const typeMap: Record<string, string> = {
active: 'success',
disabled: 'info',
locked: 'danger'
}
return typeMap[status] || 'info'
}
const handleSearch = () => {
// 搜索逻辑已通过 computed 实现
}
const viewDetail = (row: CustomerAccount) => {
currentAccount.value = row
detailDialogVisible.value = true
}
const unbindPhone = (row: CustomerAccount) => {
ElMessageBox.confirm(`确定要解绑账号 ${row.username} 的手机号吗?`, '解绑确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage.success('手机号解绑成功')
})
}
const resetPassword = (row: CustomerAccount) => {
ElMessageBox.confirm(`确定要重置账号 ${row.username} 的密码吗?新密码将发送至其手机。`, '重置密码', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage.success('密码重置成功,新密码已发送至手机')
})
}
const toggleStatus = (row: CustomerAccount) => {
const action = row.status === 'active' ? '禁用' : '启用'
ElMessageBox.confirm(`确定要${action}账号 ${row.username} 吗?`, `${action}确认`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
row.status = row.status === 'active' ? 'disabled' : 'active'
ElMessage.success(`${action}成功`)
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-descriptions__label) {
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,441 @@
<template>
<div class="page-content">
<!-- 统计卡片 -->
<ElRow :gutter="20" style="margin-bottom: 20px">
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">客户总数</div>
<div class="stat-value">{{ statistics.totalCustomers }}</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><User /></el-icon>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">累计佣金</div>
<div class="stat-value" style="color: var(--el-color-success)">
¥{{ statistics.totalCommission.toFixed(2) }}
</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-success)"><Money /></el-icon>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">已提现</div>
<div class="stat-value" style="color: var(--el-color-warning)">
¥{{ statistics.totalWithdrawn.toFixed(2) }}
</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><WalletFilled /></el-icon>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">待提现</div>
<div class="stat-value" style="color: var(--el-color-danger)">
¥{{ statistics.pendingWithdrawal.toFixed(2) }}
</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><Wallet /></el-icon>
</ElCard>
</ElCol>
</ElRow>
<!-- 搜索和筛选区 -->
<ElRow :gutter="12">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="客户名称/手机号" clearable />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="customerTypeFilter" placeholder="客户类型" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="代理商" value="agent" />
<ElOption label="企业客户" value="enterprise" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="commissionRangeFilter" placeholder="佣金范围" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="0-1000元" value="0-1000" />
<ElOption label="1000-5000元" value="1000-5000" />
<ElOption label="5000-10000元" value="5000-10000" />
<ElOption label="10000元以上" value="10000+" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="exportData">导出</ElButton>
</ElCol>
</ElRow>
<!-- 客户佣金列表 -->
<ArtTable :data="filteredData" index style="margin-top: 20px">
<template #default>
<ElTableColumn label="客户名称" prop="customerName" min-width="150" show-overflow-tooltip />
<ElTableColumn label="客户类型" prop="customerType" width="120">
<template #default="scope">
<ElTag :type="scope.row.customerType === 'agent' ? 'warning' : 'primary'">
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="联系电话" prop="phone" width="130" />
<ElTableColumn label="累计佣金" prop="totalCommission" width="130" sortable>
<template #default="scope">
<span style="color: var(--el-color-success); font-weight: 600">
¥{{ scope.row.totalCommission.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="已提现" prop="withdrawnAmount" width="130" sortable>
<template #default="scope">
<span style="color: var(--el-color-warning)">
¥{{ scope.row.withdrawnAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="待提现" prop="pendingAmount" width="130" sortable>
<template #default="scope">
<span style="color: var(--el-color-danger); font-weight: 600">
¥{{ scope.row.pendingAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="提现次数" prop="withdrawalCount" width="100" align="center" />
<ElTableColumn label="卡片数量" prop="cardCount" width="100" align="center">
<template #default="scope">
<span style="color: var(--el-color-primary)">{{ scope.row.cardCount }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="最近提现" prop="lastWithdrawalTime" width="180" />
<ElTableColumn label="注册时间" prop="registerTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="220">
<template #default="scope">
<el-button link :icon="View" @click="viewCommissionDetail(scope.row)">佣金详情</el-button>
<el-button link :icon="List" @click="viewWithdrawalHistory(scope.row)">提现记录</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 佣金详情对话框 -->
<ElDialog v-model="commissionDialogVisible" title="佣金详情" width="900px" align-center>
<ElDescriptions :column="2" border style="margin-bottom: 20px">
<ElDescriptionsItem label="客户名称">{{ currentCustomer.customerName }}</ElDescriptionsItem>
<ElDescriptionsItem label="客户类型">
<ElTag :type="currentCustomer.customerType === 'agent' ? 'warning' : 'primary'">
{{ currentCustomer.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="累计佣金">
<span style="color: var(--el-color-success); font-weight: 600">
¥{{ currentCustomer.totalCommission.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="已提现">
<span style="color: var(--el-color-warning)">
¥{{ currentCustomer.withdrawnAmount.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="待提现">
<span style="color: var(--el-color-danger); font-weight: 600">
¥{{ currentCustomer.pendingAmount.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="提现次数">{{ currentCustomer.withdrawalCount }}</ElDescriptionsItem>
</ElDescriptions>
<ElDivider content-position="left">佣金明细</ElDivider>
<ArtTable :data="commissionDetails" index>
<template #default>
<ElTableColumn label="来源" prop="source" width="120" />
<ElTableColumn label="订单号" prop="orderNo" width="180" />
<ElTableColumn label="佣金金额" prop="amount" width="120">
<template #default="scope">
<span style="color: var(--el-color-success)">+¥{{ scope.row.amount.toFixed(2) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="佣金比例" prop="rate" width="100" />
<ElTableColumn label="订单金额" prop="orderAmount" width="120" />
<ElTableColumn label="获得时间" prop="createTime" width="180" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'completed'" type="success">已结算</ElTag>
<ElTag v-else-if="scope.row.status === 'pending'" type="warning">待结算</ElTag>
</template>
</ElTableColumn>
</template>
</ArtTable>
<template #footer>
<ElButton @click="commissionDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
<!-- 提现记录对话框 -->
<ElDialog v-model="withdrawalDialogVisible" title="提现记录" width="900px" align-center>
<ArtTable :data="withdrawalHistory" index>
<template #default>
<ElTableColumn label="提现单号" prop="withdrawalNo" width="180" />
<ElTableColumn label="提现金额" prop="amount" width="120">
<template #default="scope">
<span style="color: var(--el-color-danger); font-weight: 600">
¥{{ scope.row.amount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="手续费" prop="fee" width="100">
<template #default="scope"> ¥{{ scope.row.fee.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="实际到账" prop="actualAmount" width="120">
<template #default="scope">
<span style="color: var(--el-color-success)">
¥{{ scope.row.actualAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="提现方式" prop="method" width="100" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
<ElTag v-else-if="scope.row.status === 'processing'" type="warning">处理中</ElTag>
<ElTag v-else-if="scope.row.status === 'pending'" type="info">待审核</ElTag>
<ElTag v-else type="danger">已拒绝</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
<ElTableColumn label="完成时间" prop="completeTime" width="180">
<template #default="scope">
{{ scope.row.completeTime || '-' }}
</template>
</ElTableColumn>
</template>
</ArtTable>
<template #footer>
<ElButton @click="withdrawalDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ElMessage } from 'element-plus'
import { View, List, User, Money, WalletFilled, Wallet } from '@element-plus/icons-vue'
defineOptions({ name: 'CustomerCommission' })
interface CustomerCommission {
id: string
customerName: string
customerType: 'agent' | 'enterprise'
phone: string
totalCommission: number
withdrawnAmount: number
pendingAmount: number
withdrawalCount: number
cardCount: number
lastWithdrawalTime: string
registerTime: string
}
const searchQuery = ref('')
const customerTypeFilter = ref('')
const commissionRangeFilter = ref('')
const commissionDialogVisible = ref(false)
const withdrawalDialogVisible = ref(false)
const statistics = reactive({
totalCustomers: 156,
totalCommission: 580000.00,
totalWithdrawn: 420000.00,
pendingWithdrawal: 160000.00
})
const mockData = ref<CustomerCommission[]>([
{
id: '1',
customerName: '华东区总代理',
customerType: 'agent',
phone: '13800138000',
totalCommission: 85000.00,
withdrawnAmount: 70000.00,
pendingAmount: 15000.00,
withdrawalCount: 12,
cardCount: 500,
lastWithdrawalTime: '2026-01-08 10:00:00',
registerTime: '2025-06-01 09:00:00'
},
{
id: '2',
customerName: '深圳市科技有限公司',
customerType: 'enterprise',
phone: '13900139000',
totalCommission: 45000.00,
withdrawnAmount: 30000.00,
pendingAmount: 15000.00,
withdrawalCount: 8,
cardCount: 300,
lastWithdrawalTime: '2026-01-05 14:30:00',
registerTime: '2025-07-15 10:00:00'
},
{
id: '3',
customerName: '北京智能制造',
customerType: 'enterprise',
phone: '13700137000',
totalCommission: 68000.00,
withdrawnAmount: 55000.00,
pendingAmount: 13000.00,
withdrawalCount: 10,
cardCount: 450,
lastWithdrawalTime: '2026-01-07 16:00:00',
registerTime: '2025-08-20 11:00:00'
}
])
const currentCustomer = ref<CustomerCommission>({
id: '',
customerName: '',
customerType: 'agent',
phone: '',
totalCommission: 0,
withdrawnAmount: 0,
pendingAmount: 0,
withdrawalCount: 0,
cardCount: 0,
lastWithdrawalTime: '',
registerTime: ''
})
const commissionDetails = ref([
{
id: '1',
source: '套餐销售',
orderNo: 'ORD202601090001',
amount: 150.00,
rate: '10%',
orderAmount: '¥1,500.00',
createTime: '2026-01-09 09:30:00',
status: 'completed'
},
{
id: '2',
source: '卡片激活',
orderNo: 'ORD202601080025',
amount: 80.00,
rate: '8%',
orderAmount: '¥1,000.00',
createTime: '2026-01-08 15:20:00',
status: 'completed'
}
])
const withdrawalHistory = ref([
{
id: '1',
withdrawalNo: 'WD202601080001',
amount: 10000.00,
fee: 20.00,
actualAmount: 9980.00,
method: '银行卡',
status: 'completed',
applyTime: '2026-01-08 10:00:00',
completeTime: '2026-01-08 15:30:00'
},
{
id: '2',
withdrawalNo: 'WD202601050002',
amount: 5000.00,
fee: 10.00,
actualAmount: 4990.00,
method: '支付宝',
status: 'completed',
applyTime: '2026-01-05 14:00:00',
completeTime: '2026-01-05 18:20:00'
}
])
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.customerName.includes(searchQuery.value) ||
item.phone.includes(searchQuery.value)
)
}
if (customerTypeFilter.value) {
data = data.filter((item) => item.customerType === customerTypeFilter.value)
}
if (commissionRangeFilter.value) {
data = data.filter((item) => {
const commission = item.totalCommission
if (commissionRangeFilter.value === '0-1000') return commission >= 0 && commission < 1000
if (commissionRangeFilter.value === '1000-5000') return commission >= 1000 && commission < 5000
if (commissionRangeFilter.value === '5000-10000') return commission >= 5000 && commission < 10000
if (commissionRangeFilter.value === '10000+') return commission >= 10000
return true
})
}
return data
})
const handleSearch = () => {}
const exportData = () => {
ElMessage.success('数据导出中...')
}
const viewCommissionDetail = (row: CustomerCommission) => {
currentCustomer.value = { ...row }
commissionDialogVisible.value = true
}
const viewWithdrawalHistory = (row: CustomerCommission) => {
currentCustomer.value = { ...row }
withdrawalDialogVisible.value = true
}
</script>
<style lang="scss" scoped>
.page-content {
.stat-card {
:deep(.el-card__body) {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
}
.stat-content {
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.stat-icon {
font-size: 40px;
opacity: 0.6;
}
}
}
</style>

View File

@@ -0,0 +1,240 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="角色名称" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="showDialog('add')">新增角色</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index>
<template #default>
<ElTableColumn label="角色名称" prop="roleName" />
<ElTableColumn label="角色编码" prop="roleCode" />
<ElTableColumn label="能力边界" prop="abilities">
<template #default="scope">
<ElTag
v-for="(ability, index) in scope.row.abilities"
:key="index"
size="small"
style="margin-right: 4px"
>
{{ ability }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="描述" prop="description" show-overflow-tooltip />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="180">
<template #default="scope">
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button link @click="handleDelete(scope.row)">删除</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增客户角色' : '编辑客户角色'"
width="600px"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="角色名称" prop="roleName">
<ElInput v-model="form.roleName" placeholder="请输入角色名称" />
</ElFormItem>
<ElFormItem label="角色编码" prop="roleCode">
<ElInput v-model="form.roleCode" placeholder="请输入角色编码" />
</ElFormItem>
<ElFormItem label="能力边界" prop="abilities">
<ElCheckboxGroup v-model="form.abilities">
<ElCheckbox label="查看网卡">查看网卡</ElCheckbox>
<ElCheckbox label="操作网卡">操作网卡</ElCheckbox>
<ElCheckbox label="查看套餐">查看套餐</ElCheckbox>
<ElCheckbox label="购买套餐">购买套餐</ElCheckbox>
<ElCheckbox label="查看设备">查看设备</ElCheckbox>
<ElCheckbox label="管理设备">管理设备</ElCheckbox>
<ElCheckbox label="查看佣金">查看佣金</ElCheckbox>
<ElCheckbox label="提现佣金">提现佣金</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="请输入角色描述" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'CustomerRole' })
interface CustomerRole {
id?: string
roleName: string
roleCode: string
abilities: string[]
description: string
status: 'active' | 'inactive'
createTime?: string
}
// Mock 数据
const mockData = ref<CustomerRole[]>([
{
id: '1',
roleName: '标准客户',
roleCode: 'CUSTOMER_STANDARD',
abilities: ['查看网卡', '查看套餐', '查看设备'],
description: '标准客户角色,拥有基本查看权限',
status: 'active',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
roleName: 'VIP客户',
roleCode: 'CUSTOMER_VIP',
abilities: ['查看网卡', '操作网卡', '查看套餐', '购买套餐', '查看设备', '管理设备'],
description: 'VIP客户角色拥有更多操作权限',
status: 'active',
createTime: '2026-01-02 11:00:00'
},
{
id: '3',
roleName: '企业客户',
roleCode: 'CUSTOMER_ENTERPRISE',
abilities: ['查看网卡', '操作网卡', '查看套餐', '购买套餐', '查看设备', '管理设备', '查看佣金'],
description: '企业客户角色,拥有完整业务权限',
status: 'active',
createTime: '2026-01-03 12:00:00'
}
])
const searchQuery = ref('')
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const form = reactive<CustomerRole>({
roleName: '',
roleCode: '',
abilities: [],
description: '',
status: 'active'
})
const rules = reactive<FormRules>({
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
abilities: [{ required: true, message: '请至少选择一项能力', trigger: 'change' }]
})
const filteredData = computed(() => {
if (!searchQuery.value) return mockData.value
return mockData.value.filter((item) =>
item.roleName.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const handleSearch = () => {
// 搜索逻辑已通过 computed 实现
}
const showDialog = (type: 'add' | 'edit', row?: CustomerRole) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
form.id = row.id
form.roleName = row.roleName
form.roleCode = row.roleCode
form.abilities = [...row.abilities]
form.description = row.description
form.status = row.status
} else {
resetForm()
}
}
const resetForm = () => {
form.id = undefined
form.roleName = ''
form.roleCode = ''
form.abilities = []
form.description = ''
form.status = 'active'
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
mockData.value.push({
...form,
id: Date.now().toString(),
createTime: new Date().toLocaleString('zh-CN')
})
ElMessage.success('新增成功')
} else {
const index = mockData.value.findIndex((item) => item.id === form.id)
if (index !== -1) {
mockData.value[index] = { ...form }
}
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const handleDelete = (row: CustomerRole) => {
ElMessageBox.confirm('确定删除该客户角色吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
const index = mockData.value.findIndex((item) => item.id === row.id)
if (index !== -1) {
mockData.value.splice(index, 1)
}
ElMessage.success('删除成功')
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-checkbox) {
margin-right: 20px;
margin-bottom: 10px;
}
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<ArtTableFullScreen>
<div class="customer-management-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">查询</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElMessage } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CustomerManagement' })
const loading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
distributorName: '',
distributorAccount: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 模拟数据 - 客户管理(代理商)
const mockData = [
{
id: 1,
distributorName: '北京优享科技有限公司',
distributorAccount: 'bjyx2024',
totalCommission: 12560.8,
instantCommission: 8320.5,
pendingCommission: 4240.3,
prepaidCommission: 2400.0,
withdrawnCommission: 6800.2,
withdrawingCommission: 1520.3,
availableCommission: 2000.0
},
{
id: 2,
distributorName: '上海智联通信技术公司',
distributorAccount: 'shzl2024',
totalCommission: 18750.25,
instantCommission: 12500.75,
pendingCommission: 6249.5,
prepaidCommission: 3600.0,
withdrawnCommission: 8900.25,
withdrawingCommission: 2100.0,
availableCommission: 3750.0
},
{
id: 3,
distributorName: '广州物联网络有限公司',
distributorAccount: 'gzwl2024',
totalCommission: 9876.4,
instantCommission: 6584.3,
pendingCommission: 3292.1,
prepaidCommission: 1800.0,
withdrawnCommission: 4938.2,
withdrawingCommission: 980.2,
availableCommission: 1958.0
},
{
id: 4,
distributorName: '深圳云联科技股份公司',
distributorAccount: 'szyl2024',
totalCommission: 24500.6,
instantCommission: 16333.73,
pendingCommission: 8166.87,
prepaidCommission: 4800.0,
withdrawnCommission: 11225.3,
withdrawingCommission: 2450.06,
availableCommission: 4900.12
},
{
id: 5,
distributorName: '杭州通达网络服务公司',
distributorAccount: 'hztd2024',
totalCommission: 15200.35,
instantCommission: 10133.57,
pendingCommission: 5066.78,
prepaidCommission: 2900.0,
withdrawnCommission: 7600.18,
withdrawingCommission: 1520.04,
availableCommission: 3040.07
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCustomerList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCustomerList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '代理商名称',
prop: 'distributorName',
type: 'input',
config: {
clearable: true,
placeholder: '请输入代理商名称'
},
onChange: handleFormChange
},
{
label: '代理商账号',
prop: 'distributorAccount',
type: 'input',
config: {
clearable: true,
placeholder: '请输入代理商账号'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '代理商名称', prop: 'distributorName' },
{ label: '代理商账号', prop: 'distributorAccount' },
{ label: '总佣金', prop: 'totalCommission' },
{ label: '已秒返佣金', prop: 'instantCommission' },
{ label: '未秒返佣金', prop: 'pendingCommission' },
{ label: '预存佣金', prop: 'prepaidCommission' },
{ label: '已提现佣金', prop: 'withdrawnCommission' },
{ label: '提现中佣金', prop: 'withdrawingCommission' },
{ label: '可提现佣金', prop: 'availableCommission' },
{ label: '操作', prop: 'operation' }
]
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条客户记录`)
}
// 查看提现详情
const viewWithdrawDetail = (row: any) => {
ElMessage.info(`查看 ${row.distributorName} 的提现详情`)
}
// 查看佣金明细
const viewCommissionDetail = (row: any) => {
ElMessage.info(`查看 ${row.distributorName} 的佣金明细`)
}
// 查看自动提现详情
const viewAutoWithdrawDetail = (row: any) => {
ElMessage.info(`查看 ${row.distributorName} 的自动提现详情`)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'distributorName',
label: '代理商名称',
minWidth: 180
},
{
prop: 'distributorAccount',
label: '代理商账号',
width: 120
},
{
prop: 'totalCommission',
label: '总佣金',
width: 120,
formatter: (row) => `¥${row.totalCommission.toFixed(2)}`
},
{
prop: 'instantCommission',
label: '已秒返佣金',
width: 120,
formatter: (row) => `¥${row.instantCommission.toFixed(2)}`
},
{
prop: 'pendingCommission',
label: '未秒返佣金',
width: 120,
formatter: (row) => `¥${row.pendingCommission.toFixed(2)}`
},
{
prop: 'prepaidCommission',
label: '预存佣金',
width: 120,
formatter: (row) => `¥${row.prepaidCommission.toFixed(2)}`
},
{
prop: 'withdrawnCommission',
label: '已提现佣金',
width: 120,
formatter: (row) => `¥${row.withdrawnCommission.toFixed(2)}`
},
{
prop: 'withdrawingCommission',
label: '提现中佣金',
width: 120,
formatter: (row) => `¥${row.withdrawingCommission.toFixed(2)}`
},
{
prop: 'availableCommission',
label: '可提现佣金',
width: 120,
formatter: (row) => `¥${row.availableCommission.toFixed(2)}`
},
{
prop: 'operation',
label: '操作',
width: 200,
formatter: (row: any) => {
return h('div', { style: 'display: flex; gap: 5px; flex-wrap: wrap;' }, [
h(ArtButtonTable, {
type: 'view',
text: '提现详情',
onClick: () => viewWithdrawDetail(row)
}),
h(ArtButtonTable, {
type: 'detail',
text: '佣金明细',
onClick: () => viewCommissionDetail(row)
}),
h(ArtButtonTable, {
type: 'info',
text: '自动提现详情',
onClick: () => viewAutoWithdrawDetail(row)
})
])
}
}
])
onMounted(() => {
getCustomerList()
})
// 获取客户列表
const getCustomerList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取客户列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCustomerList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCustomerList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCustomerList()
}
</script>
<style lang="scss" scoped>
.customer-management-page {
// 可以添加特定样式
}
</style>

View File

@@ -0,0 +1,498 @@
<template>
<div class="page-content">
<!-- 搜索和操作区 -->
<ElRow :gutter="12">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="企业名称/联系人" clearable />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="正常" value="active" />
<ElOption label="禁用" value="disabled" />
<ElOption label="待审核" value="pending" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="roleFilter" placeholder="角色筛选" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="企业管理员" value="admin" />
<ElOption label="企业用户" value="user" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple type="primary" @click="showDialog('add')">新增企业客户</ElButton>
</ElCol>
</ElRow>
<!-- 企业客户列表 -->
<ArtTable :data="filteredData" index style="margin-top: 20px">
<template #default>
<ElTableColumn label="企业名称" prop="enterpriseName" min-width="180" show-overflow-tooltip />
<ElTableColumn label="统一社会信用代码" prop="creditCode" width="180" />
<ElTableColumn label="联系人" prop="contactPerson" width="120" />
<ElTableColumn label="联系电话" prop="contactPhone" width="130" />
<ElTableColumn label="所属角色" prop="roleName" width="120">
<template #default="scope">
<ElTag>{{ scope.row.roleName }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="卡片数量" prop="cardCount" width="100">
<template #default="scope">
<span style="color: var(--el-color-primary)">{{ scope.row.cardCount }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="设备数量" prop="deviceCount" width="100">
<template #default="scope">
<span style="color: var(--el-color-success)">{{ scope.row.deviceCount }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="账户余额" prop="balance" width="120">
<template #default="scope"> ¥{{ scope.row.balance.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'active'" type="success">正常</ElTag>
<ElTag v-else-if="scope.row.status === 'disabled'" type="danger">禁用</ElTag>
<ElTag v-else-if="scope.row.status === 'pending'" type="warning">待审核</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="260">
<template #default="scope">
<el-button link :icon="View" @click="viewDetail(scope.row)">详情</el-button>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button
link
:type="scope.row.status === 'active' ? 'warning' : 'success'"
@click="toggleStatus(scope.row)"
>
{{ scope.row.status === 'active' ? '禁用' : '启用' }}
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 新增/编辑企业客户对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增企业客户' : '编辑企业客户'"
width="700px"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="140px">
<ElDivider content-position="left">企业信息</ElDivider>
<ElFormItem label="企业名称" prop="enterpriseName">
<ElInput v-model="form.enterpriseName" placeholder="请输入企业名称" />
</ElFormItem>
<ElFormItem label="统一社会信用代码" prop="creditCode">
<ElInput v-model="form.creditCode" placeholder="请输入统一社会信用代码" maxlength="18" />
</ElFormItem>
<ElFormItem label="企业地址" prop="address">
<ElInput v-model="form.address" placeholder="请输入企业地址" />
</ElFormItem>
<ElFormItem label="营业执照" prop="businessLicense">
<ElUpload
:action="uploadUrl"
:limit="1"
list-type="picture-card"
accept="image/*"
>
<el-icon><Plus /></el-icon>
</ElUpload>
<div style="color: var(--el-text-color-secondary); font-size: 12px">支持 JPGPNG 格式</div>
</ElFormItem>
<ElDivider content-position="left">联系人信息</ElDivider>
<ElFormItem label="联系人" prop="contactPerson">
<ElInput v-model="form.contactPerson" placeholder="请输入联系人姓名" />
</ElFormItem>
<ElFormItem label="联系电话" prop="contactPhone">
<ElInput v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="11" />
</ElFormItem>
<ElFormItem label="联系邮箱" prop="contactEmail">
<ElInput v-model="form.contactEmail" placeholder="请输入联系邮箱" />
</ElFormItem>
<ElDivider content-position="left">账号配置</ElDivider>
<ElFormItem label="登录账号" prop="username">
<ElInput v-model="form.username" placeholder="请输入登录账号" :disabled="dialogType === 'edit'" />
<div style="color: var(--el-text-color-secondary); font-size: 12px">只能登录企业端</div>
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="登录密码" prop="password">
<ElInput v-model="form.password" type="password" placeholder="请输入登录密码" show-password />
</ElFormItem>
<ElFormItem label="分配角色" prop="roleId">
<ElSelect v-model="form.roleId" placeholder="请选择客户角色" style="width: 100%">
<ElOption
v-for="role in availableRoles"
:key="role.id"
:label="role.roleName"
:value="role.id"
/>
</ElSelect>
<div style="color: var(--el-text-color-secondary); font-size: 12px">角色决定企业客户的能力边界</div>
</ElFormItem>
<ElFormItem label="初始余额" prop="initialBalance">
<ElInputNumber v-model="form.initialBalance" :min="0" :precision="2" style="width: 100%" />
<span style="margin-left: 8px"></span>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="active" inactive-value="disabled" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 企业详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="企业客户详情" width="800px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="企业名称">{{ currentDetail.enterpriseName }}</ElDescriptionsItem>
<ElDescriptionsItem label="统一社会信用代码">{{ currentDetail.creditCode }}</ElDescriptionsItem>
<ElDescriptionsItem label="联系人">{{ currentDetail.contactPerson }}</ElDescriptionsItem>
<ElDescriptionsItem label="联系电话">{{ currentDetail.contactPhone }}</ElDescriptionsItem>
<ElDescriptionsItem label="联系邮箱">{{ currentDetail.contactEmail }}</ElDescriptionsItem>
<ElDescriptionsItem label="企业地址" :span="2">{{ currentDetail.address }}</ElDescriptionsItem>
<ElDescriptionsItem label="所属角色">
<ElTag>{{ currentDetail.roleName }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="登录账号">{{ currentDetail.username }}</ElDescriptionsItem>
<ElDescriptionsItem label="卡片数量">
<span style="color: var(--el-color-primary)">{{ currentDetail.cardCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="设备数量">
<span style="color: var(--el-color-success)">{{ currentDetail.deviceCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="账户余额">¥{{ currentDetail.balance.toFixed(2) }}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag v-if="currentDetail.status === 'active'" type="success">正常</ElTag>
<ElTag v-else-if="currentDetail.status === 'disabled'" type="danger">禁用</ElTag>
<ElTag v-else-if="currentDetail.status === 'pending'" type="warning">待审核</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ currentDetail.createTime }}</ElDescriptionsItem>
<ElDescriptionsItem label="最后登录">{{ currentDetail.lastLoginTime || '未登录' }}</ElDescriptionsItem>
<ElDescriptionsItem label="备注" :span="2">{{ currentDetail.remark || '无' }}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { View, Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'EnterpriseCustomer' })
interface EnterpriseCustomer {
id: string
enterpriseName: string
creditCode: string
address: string
contactPerson: string
contactPhone: string
contactEmail: string
username: string
roleId: string
roleName: string
cardCount: number
deviceCount: number
balance: number
status: 'active' | 'disabled' | 'pending'
createTime: string
lastLoginTime?: string
remark?: string
}
const searchQuery = ref('')
const statusFilter = ref('')
const roleFilter = ref('')
const dialogVisible = ref(false)
const detailDialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const uploadUrl = ref('/api/upload/image')
const form = reactive({
enterpriseName: '',
creditCode: '',
address: '',
businessLicense: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
username: '',
password: '',
roleId: '',
initialBalance: 0,
remark: '',
status: 'active'
})
const rules = reactive<FormRules>({
enterpriseName: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
creditCode: [
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
{ len: 18, message: '统一社会信用代码为18位', trigger: 'blur' }
],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
contactEmail: [
{ required: true, message: '请输入联系邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
username: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
password: [
{ required: true, message: '请输入登录密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' }
],
roleId: [{ required: true, message: '请选择客户角色', trigger: 'change' }]
})
const availableRoles = ref([
{ id: '1', roleName: '企业管理员' },
{ id: '2', roleName: '企业用户' },
{ id: '3', roleName: '企业财务' }
])
const mockData = ref<EnterpriseCustomer[]>([
{
id: '1',
enterpriseName: '深圳市科技有限公司',
creditCode: '91440300MA5DXXXX01',
address: '深圳市南山区科技园',
contactPerson: '张经理',
contactPhone: '13800138000',
contactEmail: 'zhang@company.com',
username: 'shenzhen_tech',
roleId: '1',
roleName: '企业管理员',
cardCount: 500,
deviceCount: 450,
balance: 15000.00,
status: 'active',
createTime: '2026-01-01 10:00:00',
lastLoginTime: '2026-01-09 09:30:00',
remark: '重要客户'
},
{
id: '2',
enterpriseName: '北京智能制造有限公司',
creditCode: '91110000MA5DXXXX02',
address: '北京市海淀区中关村',
contactPerson: '李总监',
contactPhone: '13900139000',
contactEmail: 'li@manufacturing.com',
username: 'beijing_smart',
roleId: '1',
roleName: '企业管理员',
cardCount: 800,
deviceCount: 750,
balance: 28000.00,
status: 'active',
createTime: '2026-01-03 14:00:00',
lastLoginTime: '2026-01-09 08:15:00'
},
{
id: '3',
enterpriseName: '上海物联网科技公司',
creditCode: '91310000MA5DXXXX03',
address: '上海市浦东新区张江高科技园区',
contactPerson: '王主管',
contactPhone: '13700137000',
contactEmail: 'wang@iot-shanghai.com',
username: 'shanghai_iot',
roleId: '2',
roleName: '企业用户',
cardCount: 300,
deviceCount: 280,
balance: 8500.00,
status: 'pending',
createTime: '2026-01-08 16:00:00'
}
])
const currentDetail = ref<EnterpriseCustomer>({
id: '',
enterpriseName: '',
creditCode: '',
address: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
username: '',
roleId: '',
roleName: '',
cardCount: 0,
deviceCount: 0,
balance: 0,
status: 'active',
createTime: ''
})
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.enterpriseName.includes(searchQuery.value) ||
item.contactPerson.includes(searchQuery.value) ||
item.contactPhone.includes(searchQuery.value)
)
}
if (statusFilter.value) {
data = data.filter((item) => item.status === statusFilter.value)
}
if (roleFilter.value) {
data = data.filter((item) => item.roleId === roleFilter.value)
}
return data
})
const handleSearch = () => {}
const showDialog = (type: 'add' | 'edit', row?: EnterpriseCustomer) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
Object.assign(form, {
enterpriseName: row.enterpriseName,
creditCode: row.creditCode,
address: row.address,
contactPerson: row.contactPerson,
contactPhone: row.contactPhone,
contactEmail: row.contactEmail,
username: row.username,
roleId: row.roleId,
initialBalance: row.balance,
remark: row.remark,
status: row.status
})
} else {
Object.assign(form, {
enterpriseName: '',
creditCode: '',
address: '',
businessLicense: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
username: '',
password: '',
roleId: '',
initialBalance: 0,
remark: '',
status: 'active'
})
}
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
const selectedRole = availableRoles.value.find((r) => r.id === form.roleId)
mockData.value.unshift({
id: Date.now().toString(),
enterpriseName: form.enterpriseName,
creditCode: form.creditCode,
address: form.address,
contactPerson: form.contactPerson,
contactPhone: form.contactPhone,
contactEmail: form.contactEmail,
username: form.username,
roleId: form.roleId,
roleName: selectedRole?.roleName || '',
cardCount: 0,
deviceCount: 0,
balance: form.initialBalance,
status: form.status as any,
createTime: new Date().toLocaleString('zh-CN'),
remark: form.remark
})
ElMessage.success('企业客户创建成功')
} else {
ElMessage.success('企业客户更新成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const viewDetail = (row: EnterpriseCustomer) => {
currentDetail.value = { ...row }
detailDialogVisible.value = true
}
const toggleStatus = (row: EnterpriseCustomer) => {
const action = row.status === 'active' ? '禁用' : '启用'
ElMessageBox.confirm(`确定${action}该企业客户吗?`, `${action}确认`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
row.status = row.status === 'active' ? 'disabled' : 'active'
ElMessage.success(`${action}成功`)
})
}
const handleDelete = (row: EnterpriseCustomer) => {
ElMessageBox.confirm('删除后无法恢复,确定要删除该企业客户吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
const index = mockData.value.findIndex((item) => item.id === row.id)
if (index !== -1) mockData.value.splice(index, 1)
ElMessage.success('删除成功')
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 148px;
height: 148px;
}
}
</style>

View File

@@ -0,0 +1,669 @@
<template>
<ArtTableFullScreen>
<div class="platform-account-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showDialog('add')">新增平台账号</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="ID"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增平台账号' : '编辑平台账号'"
width="500px"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="账号名称" prop="username">
<ElInput
v-model="formData.username"
placeholder="请输入账号名称"
:disabled="dialogType === 'edit'"
/>
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
<ElInput
v-model="formData.password"
type="password"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem label="账号类型" prop="user_type">
<ElSelect
v-model="formData.user_type"
placeholder="请选择账号类型"
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<ElOption label="超级管理员" :value="1" />
<ElOption label="平台用户" :value="2" />
<ElOption label="代理账号" :value="3" />
<ElOption label="企业账号" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="formData.user_type === 3" label="关联店铺ID" prop="shop_id">
<ElInputNumber
v-model="formData.shop_id"
:min="1"
placeholder="请输入店铺ID"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem v-if="formData.user_type === 4" label="关联企业ID" prop="enterprise_id">
<ElInputNumber
v-model="formData.enterprise_id"
:min="1"
placeholder="请输入企业ID"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem v-if="dialogType === 'edit'" label="状态">
<ElSwitch
v-model="formData.status"
:active-value="CommonStatus.ENABLED"
:inactive-value="CommonStatus.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" title="分配角色" width="500px">
<ElCheckboxGroup v-model="selectedRoles">
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px">
<ElCheckbox :label="role.ID">{{ role.role_name }}</ElCheckbox>
</div>
</ElCheckboxGroup>
<template #footer>
<div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
>提交</ElButton
>
</div>
</template>
</ElDialog>
<!-- 修改密码对话框 -->
<ElDialog v-model="passwordDialogVisible" title="修改密码" width="400px">
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
<ElFormItem label="新密码" prop="new_password">
<ElInput
v-model="passwordForm.new_password"
type="password"
placeholder="请输入新密码"
show-password
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleChangePassword"
:loading="passwordSubmitLoading"
>提交</ElButton
>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { FormInstance, ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { PlatformAccountService, RoleService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformRole, PlatformAccountResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
defineOptions({ name: 'PlatformAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
const dialogType = ref('add')
const dialogVisible = ref(false)
const roleDialogVisible = ref(false)
const passwordDialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const roleSubmitLoading = ref(false)
const passwordSubmitLoading = ref(false)
const currentAccountId = ref<number>(0)
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
// 定义表单搜索初始值
const initialSearchState = {
username: '',
phone: '',
status: undefined as number | undefined
}
// 响应式表单数据
const searchForm = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<PlatformAccountResponse[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.currentPage = 1 // 重置到第一页
getAccountList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', searchForm)
pagination.currentPage = 1 // 搜索时重置到第一页
getAccountList()
}
// 表单配置项
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',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
}
]
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'ID' },
{ label: '账号名称', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '账号类型', prop: 'user_type' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' },
{ label: '操作', prop: 'operation' }
]
// 显示对话框
const showDialog = (type: string, row?: PlatformAccountResponse) => {
dialogVisible.value = true
dialogType.value = type
// 重置表单验证状态
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.id = row.ID
formData.username = row.username
formData.phone = row.phone
formData.user_type = row.user_type
formData.enterprise_id = row.enterprise_id || null
formData.shop_id = row.shop_id || null
formData.status = row.status
formData.password = ''
} else {
formData.id = 0
formData.username = ''
formData.phone = ''
formData.user_type = 2
formData.enterprise_id = null
formData.shop_id = null
formData.status = CommonStatus.ENABLED
formData.password = ''
}
}
// 删除账号
const deleteAccount = (row: PlatformAccountResponse) => {
ElMessageBox.confirm(`确定要删除平台账号 ${row.username} 吗?`, '删除平台账号', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await PlatformAccountService.deletePlatformAccount(row.ID)
ElMessage.success('删除成功')
getAccountList()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 账号取消删除
})
}
// 显示修改密码对话框
const showPasswordDialog = (row: PlatformAccountResponse) => {
currentAccountId.value = row.ID
passwordForm.new_password = ''
passwordDialogVisible.value = true
if (passwordFormRef.value) {
passwordFormRef.value.resetFields()
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'ID',
label: 'ID',
width: 80
},
{
prop: 'username',
label: '账号名称',
minWidth: 120
},
{
prop: 'phone',
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '账号类型',
width: 120,
formatter: (row: PlatformAccountResponse) => {
const typeMap: Record<number, string> = {
1: '超级管理员',
2: '平台用户',
3: '代理账号',
4: '企业账号'
}
return typeMap[row.user_type] || '-'
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PlatformAccountResponse) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
})
}
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: PlatformAccountResponse) => formatDateTime(row.CreatedAt)
},
{
prop: 'operation',
label: '操作',
width: 240,
fixed: 'right',
formatter: (row: PlatformAccountResponse) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
icon: '&#xe72b;',
onClick: () => showRoleDialog(row)
}),
h(ArtButtonTable, {
icon: '&#xe722;',
onClick: () => showPasswordDialog(row)
}),
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteAccount(row)
})
])
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
const passwordFormRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: 0,
username: '',
password: '',
phone: '',
user_type: 2,
enterprise_id: null as number | null,
shop_id: null as number | null,
status: CommonStatus.ENABLED
})
// 密码表单数据
const passwordForm = reactive({
new_password: ''
})
onMounted(() => {
getAccountList()
loadAllRoles()
})
// 加载所有角色列表
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 showRoleDialog = async (row: PlatformAccountResponse) => {
currentAccountId.value = row.ID
selectedRoles.value = []
// 先加载当前账号的角色,再打开对话框
try {
const res = await PlatformAccountService.getPlatformAccountRoles(row.ID)
if (res.code === 0) {
// 提取角色ID数组
const roles = res.data || []
selectedRoles.value = roles.map((role: any) => role.ID)
// 数据加载完成后再打开对话框
roleDialogVisible.value = true
}
} catch (error) {
console.error('获取账号角色失败:', error)
}
}
// 提交分配角色
const handleAssignRoles = async () => {
roleSubmitLoading.value = true
try {
await PlatformAccountService.assignRolesToPlatformAccount(currentAccountId.value, {
role_ids: selectedRoles.value
})
ElMessage.success('分配角色成功')
roleDialogVisible.value = false
} catch (error) {
console.error(error)
} finally {
roleSubmitLoading.value = false
}
}
// 提交修改密码
const handleChangePassword = async () => {
if (!passwordFormRef.value) return
await passwordFormRef.value.validate(async (valid) => {
if (valid) {
passwordSubmitLoading.value = true
try {
await PlatformAccountService.changePlatformAccountPassword(currentAccountId.value, {
new_password: passwordForm.new_password
})
ElMessage.success('修改密码成功')
passwordDialogVisible.value = false
} catch (error) {
console.error(error)
} finally {
passwordSubmitLoading.value = false
}
}
})
}
// 获取账号列表
const getAccountList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
page_size: pagination.pageSize,
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
status: searchForm.status
}
const res = await PlatformAccountService.getPlatformAccounts(params)
if (res.code === 0) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('获取平台账号列表失败:', error)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getAccountList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
shop_id: [
{
validator: (rule: any, value: any, callback: any) => {
if (formData.user_type === 3 && !value) {
callback(new Error('代理账号必须关联店铺ID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
enterprise_id: [
{
validator: (rule: any, value: any, callback: any) => {
if (formData.user_type === 4 && !value) {
callback(new Error('企业账号必须关联企业ID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 密码验证规则
const passwordRules = reactive<FormRules>({
new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (dialogType.value === 'add') {
const data: any = {
username: formData.username,
password: formData.password,
phone: formData.phone,
user_type: formData.user_type
}
// 根据账号类型添加相应的字段
if (formData.user_type === 3) {
data.shop_id = formData.shop_id
} else if (formData.user_type === 4) {
data.enterprise_id = formData.enterprise_id
}
await PlatformAccountService.createPlatformAccount(data)
ElMessage.success('新增成功')
} else {
const data: any = {
username: formData.username,
phone: formData.phone,
status: formData.status
}
await PlatformAccountService.updatePlatformAccount(formData.id, data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
getAccountList()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getAccountList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getAccountList()
}
// 状态切换
const handleStatusChange = async (row: any, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await PlatformAccountService.updatePlatformAccount(row.ID, { status: newStatus })
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
</script>
<style lang="scss" scoped>
.platform-account-page {
// 平台账号管理页面样式
}
</style>

View File

@@ -0,0 +1,482 @@
<template>
<ArtTableFullScreen>
<div class="shop-account-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showDialog('add')">新增代理账号</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理账号' : '编辑代理账号'"
width="500px"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="用户名" prop="username">
<ElInput v-model="formData.username" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
<ElInput v-model="formData.password" type="password" placeholder="请输入密码" show-password />
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem label="店铺ID" prop="shop_id">
<ElInputNumber v-model="formData.shop_id" :min="1" placeholder="请输入店铺ID" style="width: 100%" />
</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="passwordDialogVisible"
title="重置密码"
width="400px"
>
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
<ElFormItem label="新密码" prop="new_password">
<ElInput v-model="passwordForm.new_password" type="password" placeholder="请输入新密码" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleChangePassword" :loading="passwordSubmitLoading">提交</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { FormInstance, ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { ShopAccountService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { ShopAccountResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
defineOptions({ name: 'ShopAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
const dialogType = ref('add')
const dialogVisible = ref(false)
const passwordDialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const passwordSubmitLoading = ref(false)
const currentAccountId = ref<number>(0)
// 定义表单搜索初始值
const initialSearchState = {
username: '',
phone: '',
shop_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 响应式表单数据
const searchForm = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<ShopAccountResponse[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.currentPage = 1 // 重置到第一页
getAccountList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', searchForm)
pagination.currentPage = 1 // 搜索时重置到第一页
getAccountList()
}
// 表单配置项
const searchFormItems: SearchFormItem[] = [
{
label: '用户名',
prop: 'username',
type: 'input',
config: {
clearable: true,
placeholder: '请输入用户名'
}
},
{
label: '手机号',
prop: 'phone',
type: 'input',
config: {
clearable: true,
placeholder: '请输入手机号'
}
},
{
label: '店铺ID',
prop: 'shop_id',
type: 'input',
config: {
clearable: true,
placeholder: '请输入店铺ID'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
}
]
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '用户名', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '店铺ID', prop: 'shop_id' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
// 显示对话框
const showDialog = (type: string, row?: ShopAccountResponse) => {
dialogVisible.value = true
dialogType.value = type
// 重置表单验证状态
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.id = row.id
formData.username = row.username
formData.phone = row.phone
formData.shop_id = row.shop_id
formData.password = ''
} else {
formData.id = 0
formData.username = ''
formData.phone = ''
formData.shop_id = 0
formData.password = ''
}
}
// 显示修改密码对话框
const showPasswordDialog = (row: ShopAccountResponse) => {
currentAccountId.value = row.id
passwordForm.new_password = ''
passwordDialogVisible.value = true
if (passwordFormRef.value) {
passwordFormRef.value.resetFields()
}
}
// 状态切换处理
const handleStatusChange = async (row: ShopAccountResponse, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await ShopAccountService.updateShopAccountStatus(row.id, { status: newStatus })
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'username',
label: '用户名',
minWidth: 120
},
{
prop: 'phone',
label: '手机号',
width: 130
},
{
prop: 'shop_id',
label: '店铺ID',
width: 100
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: ShopAccountResponse) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: ShopAccountResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: ShopAccountResponse) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
icon: '&#xe722;',
onClick: () => showPasswordDialog(row)
}),
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
})
])
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
const passwordFormRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: 0,
username: '',
password: '',
phone: '',
shop_id: 0
})
// 密码表单数据
const passwordForm = reactive({
new_password: ''
})
onMounted(() => {
getAccountList()
})
// 提交修改密码
const handleChangePassword = async () => {
if (!passwordFormRef.value) return
await passwordFormRef.value.validate(async (valid) => {
if (valid) {
passwordSubmitLoading.value = true
try {
await ShopAccountService.updateShopAccountPassword(currentAccountId.value, {
new_password: passwordForm.new_password
})
ElMessage.success('重置密码成功')
passwordDialogVisible.value = false
} catch (error) {
console.error(error)
} finally {
passwordSubmitLoading.value = false
}
}
})
}
// 获取账号列表
const getAccountList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
page_size: pagination.pageSize,
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
shop_id: searchForm.shop_id,
status: searchForm.status
}
const res = await ShopAccountService.getShopAccounts(params)
if (res.code === 0) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('获取代理账号列表失败:', error)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getAccountList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
shop_id: [
{ required: true, message: '请输入店铺ID', trigger: 'blur' },
{ type: 'number', min: 1, message: '店铺ID必须大于0', trigger: 'blur' }
]
})
// 密码验证规则
const passwordRules = reactive<FormRules>({
new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (dialogType.value === 'add') {
const data = {
username: formData.username,
password: formData.password,
phone: formData.phone,
shop_id: formData.shop_id
}
await ShopAccountService.createShopAccount(data)
ElMessage.success('新增成功')
} else {
const data = {
username: formData.username
}
await ShopAccountService.updateShopAccount(formData.id, data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
getAccountList()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getAccountList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getAccountList()
}
</script>
<style lang="scss" scoped>
.shop-account-page {
// 代理账号管理页面样式
}
</style>