Files
one-pipe-system/src/views/package-management/package-assign/index.vue
sexygoat 4470a4ef04
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m47s
修改: bug
2026-02-27 17:40:02 +08:00

886 lines
25 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="package-assign-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="true"
label-width="85"
@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="showDialog('add')" v-permission="'package_assign:add'"
>新增分配</ElButton
>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="allocationList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增分配' : '编辑分配'"
width="30%"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="90px">
<ElFormItem label="选择套餐" prop="package_id" v-if="dialogType === 'add'">
<ElSelect
v-model="form.package_id"
placeholder="请选择套餐"
style="width: 100%"
filterable
remote
:remote-method="searchPackage"
:loading="packageLoading"
clearable
@change="handlePackageChange"
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="`${pkg.package_name} (${pkg.series_name})`"
:value="pkg.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'">
<ElTreeSelect
v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺"
style="width: 100%"
filterable
clearable
:loading="shopLoading"
check-strictly
:render-after-expand="false"
/>
</ElFormItem>
<ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber
v-model="form.cost_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import {
ShopPackageAllocationService,
PackageManageService,
ShopService,
ShopSeriesAllocationService
} from '@/api/modules'
import { ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { ShopPackageAllocationResponse, PackageResponse, ShopResponse } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend
} from '@/config/constants'
defineOptions({ name: 'PackageAssign' })
const { hasAuth } = useAuth()
const router = useRouter()
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const packageLoading = ref(false)
const shopLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopPackageAllocationResponse | null>(null)
const packageOptions = ref<PackageResponse[]>([])
const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
const searchPackageOptions = ref<PackageResponse[]>([])
const searchShopOptions = ref<ShopResponse[]>([])
const searchAllocatorShopOptions = ref<ShopResponse[]>([])
const searchSeriesAllocationOptions = ref<any[]>([])
// 搜索表单初始值
const initialSearchState = {
shop_id: undefined as number | undefined,
package_id: undefined as number | undefined,
series_allocation_id: undefined as number | undefined,
allocator_shop_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '被分配店铺',
prop: 'shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchShop,
loading: shopLoading.value,
placeholder: '请选择或搜索店铺'
},
options: () =>
searchShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
},
{
label: '套餐',
prop: 'package_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchPackage,
loading: packageLoading.value,
placeholder: '请选择或搜索套餐'
},
options: () =>
searchPackageOptions.value.map((p) => ({
label: p.package_name,
value: p.id
}))
},
{
label: '系列分配',
prop: 'series_allocation_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeriesAllocation,
loading: loading.value,
placeholder: '请选择或搜索系列分配'
},
options: () =>
searchSeriesAllocationOptions.value.map((s) => ({
label: `${s.series_name} - ${s.shop_name}`,
value: s.id
}))
},
{
label: '分配者店铺',
prop: 'allocator_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchAllocatorShop,
loading: shopLoading.value,
placeholder: '请选择或搜索分配者店铺'
},
options: () => [
{ label: '平台', value: 0 },
...searchAllocatorShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
]
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
}
])
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' },
{ label: '系列名称', prop: 'series_name' },
{ label: '被分配店铺', prop: 'shop_name' },
{ label: '分配者', prop: 'allocator_shop_name' },
{ label: '成本价', prop: 'cost_price' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }
]
// 表单验证规则
const rules = reactive<FormRules>({
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }],
cost_price: [
{ required: true, message: '请输入成本价', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入成本价'))
} else if (form.package_base_price && value < form.package_base_price / 100) {
callback(
new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`)
)
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 表单数据
const form = reactive<any>({
id: 0,
package_id: undefined,
shop_id: undefined,
cost_price: 0,
package_base_price: 0 // 存储选中套餐的成本价,用于验证
})
const allocationList = ref<ShopPackageAllocationResponse[]>([])
const dialogType = ref('add')
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'package_code',
label: '套餐编码',
minWidth: 200,
showOverflowTooltip: true
},
{
prop: 'package_name',
label: '套餐名称',
minWidth: 180
},
{
prop: 'series_name',
label: '系列名称',
minWidth: 150
},
{
prop: 'shop_name',
label: '被分配店铺',
minWidth: 180
},
{
prop: 'allocator_shop_name',
label: '分配者',
formatter: (row: ShopPackageAllocationResponse) => {
// 如果是平台分配(allocator_shop_id为0),显示"平台"标签
if (row.allocator_shop_id === 0) {
return h(
'span',
{ style: 'color: #409eff; font-weight: bold' },
row.allocator_shop_name || '平台'
)
}
return row.allocator_shop_name
}
},
{
prop: 'cost_price',
label: '成本价',
width: 100,
formatter: (row: ShopPackageAllocationResponse) => {
return h(
'span',
{ style: 'color: #f56c6c; font-weight: bold' },
`¥${(row.cost_price / 100).toFixed(2)}`
)
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: ShopPackageAllocationResponse) => {
const frontendStatus = apiStatusToFrontend(row.status)
return h(ElSwitch, {
modelValue: frontendStatus,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
disabled: !hasAuth('package_assign:update_status'),
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: ShopPackageAllocationResponse) => formatDateTime(row.created_at)
}
])
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
items.push({
key: 'detail',
label: '详情'
})
if (hasAuth('package_assign:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
if (hasAuth('package_assign:delete')) {
items.push({
key: 'delete',
label: '删除'
})
}
return items
})
// 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
onMounted(() => {
loadPackageOptions()
loadShopOptions()
loadSearchPackageOptions()
loadSearchShopOptions()
loadSearchAllocatorShopOptions()
loadSearchSeriesAllocationOptions()
getTableData()
})
// 加载套餐选项(用于新增对话框,默认加载10条
const loadPackageOptions = async (packageName?: string) => {
packageLoading.value = true
try {
const params: any = {
page: 1,
page_size: 10
}
if (packageName) {
params.package_name = packageName
}
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
packageOptions.value = res.data.items
}
} catch (error) {
console.error('加载套餐选项失败:', error)
} finally {
packageLoading.value = false
}
}
// 加载店铺选项(用于新增对话框,加载所有店铺并构建树形结构)
const loadShopOptions = async () => {
shopLoading.value = true
try {
// 加载所有店铺,不分页
const res = await ShopService.getShops({
page: 1,
page_size: 10000 // 使用较大的值获取所有店铺
})
if (res.code === 0) {
shopOptions.value = res.data.items
// 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value)
}
} catch (error) {
console.error('加载店铺选项失败:', error)
} finally {
shopLoading.value = false
}
}
// 加载搜索栏套餐选项(默认加载10条)
const loadSearchPackageOptions = async () => {
try {
const res = await PackageManageService.getPackages({
page: 1,
page_size: 10
})
if (res.code === 0) {
searchPackageOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏套餐选项失败:', error)
}
}
// 加载搜索栏店铺选项(默认加载10条)
const loadSearchShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchShopOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏店铺选项失败:', error)
}
}
// 搜索套餐(用于新增对话框)
const searchPackage = (query: string) => {
if (query) {
loadPackageOptions(query)
} else {
loadPackageOptions()
}
}
// 搜索套餐(用于搜索栏)
const handleSearchPackage = async (query: string) => {
if (!query) {
loadSearchPackageOptions()
return
}
try {
const res = await PackageManageService.getPackages({
page: 1,
page_size: 10,
package_name: query
})
if (res.code === 0) {
searchPackageOptions.value = res.data.items
}
} catch (error) {
console.error('搜索套餐失败:', error)
}
}
// 搜索店铺(用于搜索栏)
const handleSearchShop = async (query: string) => {
if (!query) {
loadSearchShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchShopOptions.value = res.data.items
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
// 加载搜索栏分配者店铺选项(默认加载10条)
const loadSearchAllocatorShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏分配者店铺选项失败:', error)
}
}
// 搜索分配者店铺(用于搜索栏)
const handleSearchAllocatorShop = async (query: string) => {
if (!query) {
loadSearchAllocatorShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items
}
} catch (error) {
console.error('搜索分配者店铺失败:', error)
}
}
// 加载搜索栏系列分配选项(默认加载10条)
const loadSearchSeriesAllocationOptions = async () => {
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
page: 1,
page_size: 10
})
if (res.code === 0) {
searchSeriesAllocationOptions.value = res.data.items
}
} catch (error) {
console.error('加载搜索栏系列分配选项失败:', error)
}
}
// 搜索系列分配(用于搜索栏)
const handleSearchSeriesAllocation = async (query: string) => {
if (!query) {
loadSearchSeriesAllocationOptions()
return
}
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
page: 1,
page_size: 10,
series_name: query
})
if (res.code === 0) {
searchSeriesAllocationOptions.value = res.data.items
}
} catch (error) {
console.error('搜索系列分配失败:', error)
}
}
// 获取分配列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: searchForm.shop_id || undefined,
package_id: searchForm.package_id || undefined,
series_allocation_id: searchForm.series_allocation_id || undefined,
allocator_shop_id: searchForm.allocator_shop_id || undefined,
status: searchForm.status || undefined
}
const res = await ShopPackageAllocationService.getShopPackageAllocations(params)
if (res.code === 0) {
allocationList.value = res.data.items
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取分配列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示新增/编辑对话框
const showDialog = (type: string, row?: ShopPackageAllocationResponse) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.id = row.id
form.package_id = row.package_id
form.shop_id = row.shop_id
form.cost_price = row.cost_price / 100 // 转换为元显示
form.package_base_price = 0
} else {
form.id = 0
form.package_id = undefined
form.shop_id = undefined
form.cost_price = 0
form.package_base_price = 0
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 处理套餐选择变化
const handlePackageChange = (packageId: number | undefined) => {
if (packageId) {
// 从套餐选项中找到选中的套餐
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
if (selectedPackage) {
// 将套餐的成本价(分)转换为元显示
form.cost_price = selectedPackage.cost_price / 100
form.package_base_price = selectedPackage.cost_price // 保持原始值(分)用于验证
}
} else {
// 清空时重置成本价
form.cost_price = 0
form.package_base_price = 0
}
}
// 处理弹窗关闭事件
const handleDialogClosed = () => {
// 清除表单验证状态
formRef.value?.clearValidate()
// 重置表单数据
form.id = 0
form.package_id = undefined
form.shop_id = undefined
form.cost_price = 0
form.package_base_price = 0
}
// 删除分配
const deleteAllocation = (row: ShopPackageAllocationResponse) => {
ElMessageBox.confirm(
`确定删除套餐 ${row.package_name} 对店铺 ${row.shop_name} 的分配吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}
)
.then(async () => {
try {
await ShopPackageAllocationService.deleteShopPackageAllocation(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
// 将元转换为分提交给后端
const costPriceInCents = Math.round(form.cost_price * 100)
const data = {
package_id: form.package_id,
shop_id: form.shop_id,
cost_price: costPriceInCents
}
if (dialogType.value === 'add') {
await ShopPackageAllocationService.createShopPackageAllocation(data)
ElMessage.success('新增成功')
} else {
await ShopPackageAllocationService.updateShopPackageAllocation(form.id, {
cost_price: costPriceInCents
})
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 状态切换
const handleStatusChange = async (
row: ShopPackageAllocationResponse,
newFrontendStatus: number
) => {
const oldStatus = row.status
const newApiStatus = frontendStatusToApi(newFrontendStatus)
row.status = newApiStatus
try {
await ShopPackageAllocationService.updateShopPackageAllocationStatus(row.id, newApiStatus)
ElMessage.success('状态切换成功')
} catch (error) {
row.status = oldStatus
console.error(error)
}
}
// 查看详情
const handleViewDetail = (row: ShopPackageAllocationResponse) => {
router.push(`${RoutesAlias.PackageAssignDetail}/${row.id}`)
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: ShopPackageAllocationResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
handleViewDetail(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAllocation(currentRow.value)
break
}
}
</script>
<style lang="scss" scoped>
.package-assign-page {
// 可以添加特定样式
}
.dialog-footer {
text-align: right;
}
</style>