1209 lines
38 KiB
Vue
1209 lines
38 KiB
Vue
<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_id;level > 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>
|