Files
one-pipe-system/src/views/shop-management/list/index.vue
luo 5079f97326
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m47s
feat: 店铺列表新增条件
2026-07-21 18:20:35 +08:00

1209 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ArtTableFullScreen>
<div class="shop-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
show-expand
@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')" v-permission="'shop: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"
:actions="getActions"
:actionsWidth="160"
@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="50%"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="店铺名称" prop="shop_name">
<ElInput v-model="formData.shop_name" placeholder="请输入店铺名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="dialogType === 'add'">
<ElFormItem label="店铺编号" prop="shop_code">
<ElInput
v-model="formData.shop_code"
placeholder="请输入店铺编号或点击生成"
clearable
>
<template #append>
<ElButton @click="handleGenerateShopCode">生成编码</ElButton>
</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="dialogType === 'add'">
<ElCol :span="12">
<ElFormItem label="上级店铺" prop="parent_id">
<ElCascader
v-model="formData.parent_id"
:options="parentShopCascadeOptions"
:props="cascadeProps"
placeholder="一级店铺可不选"
filterable
clearable
style="width: 100%"
@change="handleParentShopChange"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="所在地区" prop="region">
<ElCascader
v-model="formData.region"
:options="regionData as any"
placeholder="请选择省/市/区"
clearable
filterable
style="width: 100%"
@change="handleRegionChange"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="详细地址" prop="address">
<ElInput v-model="formData.address" placeholder="请输入详细地址" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="联系人" prop="contact_name">
<ElInput v-model="formData.contact_name" placeholder="请输入联系人姓名" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="联系电话" prop="contact_phone">
<ElInput
v-model="formData.contact_phone"
placeholder="请输入联系电话"
maxlength="11"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="平台业务员">
<ElSelect
v-model="formData.business_owner_account_id"
filterable
remote
clearable
placeholder="请选择或搜索平台业务员"
:remote-method="searchPlatformSalespeople"
:loading="salespersonLoading"
style="width: 100%"
>
<ElOption
v-for="salesperson in salespersonOptions"
:key="salesperson.account_id"
:label="salesperson.account_name"
:value="salesperson.account_id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ salesperson.account_name }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ salesperson.phone_masked }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 新增店铺时的初始账号信息 -->
<template v-if="dialogType === 'add'">
<div class="form-section-title">
<span class="title-text">初始账号信息</span>
</div>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="用户名" prop="init_username">
<ElInput v-model="formData.init_username" placeholder="请输入初始账号用户名" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="密码" prop="init_password">
<ElInput
v-model="formData.init_password"
type="password"
placeholder="请输入初始账号密码"
show-password
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="手机号" prop="init_phone">
<ElInput
v-model="formData.init_phone"
placeholder="请输入初始账号手机号"
maxlength="11"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="默认角色" prop="default_role_id">
<ElSelect
v-model="formData.default_role_id"
placeholder="请选择默认角色"
filterable
remote
:remote-method="searchDefaultRoles"
:loading="defaultRoleLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="role in defaultRoleList"
:key="role.ID"
:label="role.role_name"
:value="role.ID"
>
<div
style="display: flex; align-items: center; justify-content: space-between"
>
<span>{{ role.role_name }}</span>
<ElTag type="success" size="small">客户角色</ElTag>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
</template>
<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="detailDialogVisible" title="店铺详情" width="500px">
<ElDescriptions v-if="detailShop" :column="1" border>
<ElDescriptionsItem label="店铺名称">{{ detailShop.shop_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺编号">{{ detailShop.shop_code }}</ElDescriptionsItem>
<ElDescriptionsItem label="平台业务员">
{{ formatBusinessOwner(detailShop) }}
</ElDescriptionsItem>
</ElDescriptions>
</ElDialog>
<!-- 店铺默认角色管理对话框 -->
<ElDialog
v-model="defaultRolesDialogVisible"
:title="`设置默认角色 - ${currentShop?.shop_name || ''}`"
width="50%"
>
<div v-loading="defaultRolesLoading || rolesLoading">
<!-- 当前默认角色显示 -->
<div v-if="selectedRoleId" class="current-role-display">
<div class="current-role-label">当前默认角色</div>
<div class="current-role-value">
{{
availableRoles.find((r) => r.role_id === selectedRoleId)?.role_name || '未知角色'
}}
</div>
</div>
<div v-else class="current-role-display no-role">
<div class="current-role-label">当前默认角色</div>
<div class="current-role-value"> 暂未设置 </div>
</div>
<!-- 默认角色选择 -->
<div class="default-roles-section">
<div class="section-header">
<span style="color: white">选择默认角色</span>
</div>
<ElSelect
v-model="selectedRoleId"
filterable
placeholder="请选择默认角色"
style="width: 100%; margin-top: 12px"
@change="handleRoleChange"
:loading="rolesLoading"
>
<ElOption
v-for="role in availableRoles"
:key="role.role_id"
:label="role.role_name"
:value="role.role_id"
>
<div style="display: flex; gap: 8px; align-items: center">
<span>{{ role.role_name }}</span>
<ElTag type="success" size="small">客户角色</ElTag>
</div>
</ElOption>
</ElSelect>
<div style="margin-top: 8px; font-size: 12px; color: #909399">
只能选择一个客户角色选择后立即生效
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="defaultRolesDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import {
FormInstance,
ElMessage,
ElMessageBox,
ElTag,
ElSwitch,
ElSelect,
ElOption,
ElCascader
} from 'element-plus'
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { generateShopCode } from '@/utils/codeGenerator'
import { ShopService, RoleService, AccountService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformSalesperson, ShopResponse, ShopRoleResponse } from '@/types/api'
import { AccountStatus, RoleType, RoleStatus } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
import { regionData } from '@/utils/constants/regionData'
defineOptions({ name: 'Shop' })
const { hasAuth } = useAuth()
const canModifyShopStatus = hasAuth('shop:modify_status')
const router = useRouter()
const dialogType = ref('add')
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const defaultRoleLoading = ref(false)
const defaultRoleList = ref<any[]>([])
const salespersonLoading = ref(false)
const salespersonOptions = ref<PlatformSalesperson[]>([])
const detailDialogVisible = ref(false)
const detailShop = ref<ShopResponse | null>(null)
// 级联选择相关
const parentShopCascadeOptions = ref<any[]>([])
const cascadeProps = {
lazy: true,
checkStrictly: true, // 允许选择任意一级
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
// level 0 表示根节点,不传 parent_idlevel > 0 传 parent_id
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 默认角色管理相关状态
const defaultRolesDialogVisible = ref(false)
const defaultRolesLoading = ref(false)
const rolesLoading = ref(false)
const currentShop = ref<ShopResponse | null>(null)
const availableRoles = ref<ShopRoleResponse[]>([])
const selectedRoleId = ref<number | undefined>(undefined)
// 上级店铺搜索选项
const parentShopOptions = ref<any[]>([])
// 搜索上级店铺
const searchParentShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
parentShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
const searchPlatformSalespeople = async (query: string) => {
salespersonLoading.value = true
try {
const res = await AccountService.getPlatformSalespeople({
page: 1,
page_size: 20,
account_type: 'platform',
status: AccountStatus.ENABLED,
account_name: query || undefined
})
if (res.code === 0) {
salespersonOptions.value = res.data.items || []
}
} catch (error) {
console.error('获取平台业务员列表失败:', error)
salespersonOptions.value = []
} finally {
salespersonLoading.value = false
}
}
const formatBusinessOwner = (shop: ShopResponse) => {
if (!shop.business_owner_name) return '-'
return shop.business_owner_phone_masked
? `${shop.business_owner_name} (${shop.business_owner_phone_masked})`
: shop.business_owner_name
}
const showDetail = (row: ShopResponse) => {
detailShop.value = row
detailDialogVisible.value = true
}
// 定义表单搜索初始值
const initialSearchState = {
shop_name: '',
shop_code: '',
contact_phone: '',
parent_shop_name: '',
parent_id: undefined as number | undefined,
level: undefined as number | undefined,
status: undefined as number | undefined,
business_owner_account_id: undefined as number | undefined
}
// 响应式表单数据
const searchForm = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<ShopResponse[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.currentPage = 1
getShopList()
}
// 搜索处理
const handleSearch = () => {
if (searchForm.contact_phone && !/^\d{11}$/.test(searchForm.contact_phone)) {
ElMessage.warning('联系电话必须为11位数字')
return
}
pagination.currentPage = 1
getShopList()
}
// 表单配置项
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '店铺名称',
prop: 'shop_name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入店铺名称'
}
},
{
label: '店铺编号',
prop: 'shop_code',
type: 'input',
config: {
clearable: true,
placeholder: '请输入店铺编号'
}
},
{
label: '联系电话',
prop: 'contact_phone',
type: 'input',
config: {
clearable: true,
maxlength: 11,
inputmode: 'numeric',
placeholder: '请输入11位联系电话',
onInput: (event: InputEvent) => {
const input = event.target as HTMLInputElement
searchForm.contact_phone = input.value.replace(/\D/g, '').slice(0, 11)
}
}
},
{
label: '上级店铺',
prop: 'parent_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchParentShops(query),
placeholder: '请选择或搜索上级店铺'
},
options: () =>
parentShopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
},
{
label: '平台业务员',
prop: 'business_owner_account_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchPlatformSalespeople(query),
placeholder: '请选择或搜索平台业务员'
},
options: () =>
salespersonOptions.value.map((salesperson) => ({
label: `${salesperson.account_name} (${salesperson.phone_masked})`,
value: salesperson.account_id
}))
}
])
// 列配置
const columnOptions = [
{ label: '店铺名称', prop: 'shop_name' },
{ label: '店铺编号', prop: 'shop_code' },
{ label: '上级店铺', prop: 'parent_shop_name' },
{ label: '所在地区', prop: 'region' },
{ label: '联系人', prop: 'contact_name' },
{ label: '联系电话', prop: 'contact_phone' },
{ label: '平台业务员', prop: 'business_owner_name' },
...(canModifyShopStatus ? [{ label: '状态', prop: 'status' }] : []),
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
// 显示对话框
const showDialog = async (type: string, row?: ShopResponse) => {
dialogType.value = type
// 先清除验证状态
formRef.value?.clearValidate()
if (type === 'edit' && row) {
formData.id = row.id
formData.shop_name = row.shop_name
formData.shop_code = ''
formData.parent_id = undefined
formData.province = row.province || ''
formData.city = row.city || ''
formData.district = row.district || ''
// 编辑时回显地区
if (row.province && row.city && row.district) {
formData.region = [row.province, row.city, row.district]
} else {
formData.region = []
}
formData.address = row.address || ''
formData.contact_name = row.contact_name || ''
formData.contact_phone = row.contact_phone || ''
formData.business_owner_account_id = row.business_owner_account_id ?? null
formData.status = row.status
formData.init_username = ''
formData.init_password = ''
formData.init_phone = ''
formData.default_role_id = undefined
} else {
formData.id = 0
formData.shop_name = ''
formData.shop_code = ''
formData.parent_id = undefined
formData.region = []
formData.province = ''
formData.city = ''
formData.district = ''
formData.address = ''
formData.contact_name = ''
formData.contact_phone = ''
formData.business_owner_account_id = null
formData.status = CommonStatus.ENABLED
formData.init_username = ''
formData.init_password = ''
formData.init_phone = ''
formData.default_role_id = undefined
}
// 再次确保清除验证状态
await nextTick(() => {
formRef.value?.clearValidate()
})
dialogVisible.value = true
}
// 删除店铺
const deleteShop = (row: ShopResponse) => {
ElMessageBox.confirm(`确定要删除店铺 ${row.shop_name} 吗?`, '删除店铺', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await ShopService.deleteShop(row.id)
ElMessage.success('删除成功')
await getShopList()
// 删除成功后也重新加载顶级店铺
await loadTopLevelShops()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'shop_name',
label: '店铺名称',
width: 160,
showOverflowTooltip: true
},
{
prop: 'shop_code',
label: '店铺编号',
minWidth: 160,
showOverflowTooltip: true
},
{
prop: 'parent_shop_name',
label: '上级店铺',
width: 150,
showOverflowTooltip: true,
formatter: (row: ShopResponse) => row.parent_shop_name || '-'
},
{
prop: 'region',
label: '所在地区',
minWidth: 170,
showOverflowTooltip: true,
formatter: (row: ShopResponse) => {
const parts: string[] = []
if (row.province) parts.push(row.province)
if (row.city) parts.push(row.city)
if (row.district) parts.push(row.district)
if (row.address) parts.push(row.address)
return parts.length > 0 ? parts.join(' / ') : '-'
}
},
{
prop: 'contact_name',
label: '联系人',
width: 100
},
{
prop: 'contact_phone',
label: '联系电话',
width: 130
},
{
prop: 'business_owner_name',
label: '平台业务员',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: ShopResponse) => formatBusinessOwner(row)
},
...(canModifyShopStatus
? [
{
prop: 'status',
label: '状态',
width: 80,
formatter: (row: ShopResponse) => {
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: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
}
]
: []),
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: ShopResponse) => formatDateTime(row.created_at)
}
])
// 操作列配置
const getActions = (row: ShopResponse) => {
const actions: any[] = []
actions.push({
label: '详情',
handler: () => showDetail(row),
type: 'primary'
})
if (hasAuth('shop:look_customer')) {
actions.push({
label: '账号列表',
handler: () => viewCustomerAccounts(row),
type: 'primary'
})
}
if (hasAuth('shop:default_role')) {
actions.push({
label: '默认角色',
handler: () => showDefaultRolesDialog(row),
type: 'primary'
})
}
if (hasAuth('shop:edit')) {
actions.push({
label: '编辑',
handler: () => showDialog('edit', row),
type: 'primary'
})
}
if (hasAuth('shop:delete')) {
actions.push({
label: '删除',
handler: () => deleteShop(row),
type: 'danger'
})
}
return actions
}
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: 0,
shop_name: '',
shop_code: '',
parent_id: undefined as number | number[] | undefined, // 支持级联选择器的数组格式
region: [] as string[], // 省市区级联数据
province: '',
city: '',
district: '',
address: '',
contact_name: '',
contact_phone: '',
status: CommonStatus.ENABLED,
init_username: '',
init_password: '',
init_phone: '',
default_role_id: undefined as number | undefined,
business_owner_account_id: null as number | null
})
// 处理编码生成
const handleGenerateShopCode = () => {
formData.shop_code = generateShopCode()
// 清除该字段的验证错误提示
nextTick(() => {
formRef.value?.clearValidate('shop_code')
})
ElMessage.success('编码生成成功')
}
// 处理地区选择变化
const handleRegionChange = (value: any) => {
if (value && Array.isArray(value) && value.length === 3) {
formData.province = value[0]
formData.city = value[1]
formData.district = value[2]
} else {
formData.province = ''
formData.city = ''
formData.district = ''
}
}
// 搜索默认角色(仅加载客户角色)
const searchDefaultRoles = async (query: string) => {
defaultRoleLoading.value = true
try {
const params: any = {
page: 1,
pageSize: 20,
role_type: RoleType.CUSTOMER, // 仅客户角色
status: RoleStatus.ENABLED // 仅启用的角色
}
if (query) {
params.role_name = query
}
const res = await RoleService.getRoles(params)
if (res.code === 0) {
defaultRoleList.value = res.data.items || []
}
} catch (error) {
console.error('获取默认角色列表失败:', error)
} finally {
defaultRoleLoading.value = false
}
}
// 处理级联选择变化
const handleParentShopChange = (value: any) => {
// 级联选择器返回的是数组,取最后一个值作为 parent_id
if (Array.isArray(value) && value.length > 0) {
formData.parent_id = value[value.length - 1]
} else {
formData.parent_id = undefined
}
}
// 加载顶级店铺数据(用于级联选择器初始化)
const loadTopLevelShops = async () => {
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
parentShopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载顶级店铺失败:', error)
}
}
onMounted(() => {
getShopList()
loadTopLevelShops() // 加载顶级店铺用于级联选择
searchDefaultRoles('') // 加载初始默认角色列表
searchParentShops('') // 加载上级店铺选项
searchPlatformSalespeople('') // 加载启用的平台业务员选项
})
// 获取店铺列表
const getShopList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
page_size: pagination.pageSize,
shop_name: searchForm.shop_name || undefined,
shop_code: searchForm.shop_code || undefined,
contact_phone: searchForm.contact_phone || undefined,
parent_shop_name: searchForm.parent_shop_name || undefined,
parent_id: searchForm.parent_id,
level: searchForm.level,
status: searchForm.status,
business_owner_account_id: searchForm.business_owner_account_id
}
const res = await ShopService.getShops(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 = () => {
getShopList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
shop_name: [
{ required: true, message: '请输入店铺名称', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
shop_code: [
{ required: true, message: '请输入店铺编号', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
address: [{ max: 255, message: '地址不能超过255个字符', trigger: 'blur' }],
contact_name: [{ max: 50, message: '联系人姓名不能超过50个字符', trigger: 'blur' }],
contact_phone: [
{ len: 11, message: '联系电话必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
init_username: [
{ required: true, message: '请输入初始账号用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
init_password: [
{ required: true, message: '请输入初始账号密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
],
init_phone: [
{ required: true, message: '请输入初始账号手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
default_role_id: [{ required: true, message: '请选择默认角色', 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 = {
shop_name: formData.shop_name,
shop_code: formData.shop_code,
init_username: formData.init_username,
init_password: formData.init_password,
init_phone: formData.init_phone,
default_role_id: formData.default_role_id,
business_owner_account_id: formData.business_owner_account_id
}
// 可选字段 - parent_id 可能是数组(级联选择器)或数字
if (formData.parent_id) {
data.parent_id = Array.isArray(formData.parent_id)
? formData.parent_id[formData.parent_id.length - 1]
: formData.parent_id
}
if (formData.province) data.province = formData.province
if (formData.city) data.city = formData.city
if (formData.district) data.district = formData.district
if (formData.address) data.address = formData.address
if (formData.contact_name) data.contact_name = formData.contact_name
if (formData.contact_phone) data.contact_phone = formData.contact_phone
await ShopService.createShop(data)
ElMessage.success('新增成功')
} else {
const data: any = {
shop_name: formData.shop_name,
status: formData.status,
business_owner_account_id: formData.business_owner_account_id
}
// 可选字段
if (formData.province) data.province = formData.province
if (formData.city) data.city = formData.city
if (formData.district) data.district = formData.district
if (formData.address) data.address = formData.address
if (formData.contact_name) data.contact_name = formData.contact_name
if (formData.contact_phone) data.contact_phone = formData.contact_phone
await ShopService.updateShop(formData.id, data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
await getShopList()
// 提交成功后也重新加载顶级店铺
await loadTopLevelShops()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getShopList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getShopList()
}
// 查看客户账号
const viewCustomerAccounts = (row: ShopResponse) => {
// 跳转到账号列表页面,通过 query 参数区分店铺类型
router.push({
name: 'EnterpriseCustomerAccounts',
params: { id: row.id },
query: { type: 'shop' }
})
}
// 状态切换
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await ShopService.updateShop(row.id, {
shop_name: row.shop_name,
status: newStatus
})
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
// ========== 默认角色管理功能 ==========
// 显示默认角色管理对话框
const showDefaultRolesDialog = async (row: ShopResponse) => {
currentShop.value = row
defaultRolesDialogVisible.value = true
await loadAvailableRoles()
await loadShopDefaultRole(row.id)
}
// 加载店铺当前默认角色
const loadShopDefaultRole = async (shopId: number) => {
defaultRolesLoading.value = true
try {
const res = await ShopService.getShopRoles(shopId)
if (res.code === 0) {
const roles = res.data.roles || []
// 如果已有默认角色,预选第一个
selectedRoleId.value = roles.length > 0 ? roles[0].role_id : undefined
}
} catch (error) {
console.error('获取店铺默认角色失败:', error)
} finally {
defaultRolesLoading.value = false
}
}
// 加载可用角色列表(仅客户角色)
const loadAvailableRoles = async () => {
rolesLoading.value = true
try {
const res = await RoleService.getRoles({
page: 1,
page_size: 100,
role_type: RoleType.CUSTOMER, // 仅客户角色
status: RoleStatus.ENABLED // 仅启用的角色
})
if (res.code === 0) {
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式
availableRoles.value = (res.data.items || []).map((role) => ({
...role,
role_id: role.ID,
role_name: role.role_name,
role_desc: role.role_desc,
role_type: role.role_type,
shop_id: 0
}))
}
} catch (error) {
console.error('获取角色列表失败:', error)
console.log('获取角色列表失败')
} finally {
rolesLoading.value = false
}
}
// 处理角色变更 - 选择后立即生效
const handleRoleChange = async (roleId: number) => {
if (!roleId) {
ElMessage.warning('请选择默认角色')
return
}
if (!currentShop.value) {
ElMessage.error('店铺信息异常')
return
}
defaultRolesLoading.value = true
try {
// 传递数组但只包含一个角色ID
const res = await ShopService.assignShopRoles(currentShop.value.id, {
role_ids: [roleId]
})
if (res.code === 0) {
ElMessage.success('设置默认角色成功')
}
} catch (error) {
console.error('设置默认角色失败:', error)
// 失败时重新加载当前默认角色
await loadShopDefaultRole(currentShop.value.id)
} finally {
defaultRolesLoading.value = false
}
}
</script>
<style lang="scss" scoped>
.current-role-display {
padding: 16px 20px;
margin-bottom: 24px;
border: 2px solid var(--el-color-primary);
border-radius: 8px;
&.no-role {
border: 2px dashed var(--el-border-color);
.current-role-value {
color: var(--el-text-color-secondary);
}
}
.current-role-label {
margin-bottom: 12px;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.current-role-value {
display: flex;
align-items: center;
font-size: 20px;
font-weight: 600;
color: var(--el-color-primary);
}
}
.default-roles-section {
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
}
</style>