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,628 @@
<template>
<ArtTableFullScreen>
<div class="shop-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="800px"
>
<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="请输入店铺编号" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="dialogType === 'add'">
<ElCol :span="12">
<ElFormItem label="上级店铺ID" prop="parent_id">
<ElInputNumber v-model="formData.parent_id" :min="1" placeholder="一级店铺可不填" style="width: 100%" clearable />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="省份" prop="province">
<ElInput v-model="formData.province" placeholder="请输入省份" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="城市" prop="city">
<ElInput v-model="formData.city" placeholder="请输入城市" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="区县" prop="district">
<ElInput v-model="formData.district" placeholder="请输入区县" />
</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>
<!-- 新增店铺时的初始账号信息 -->
<template v-if="dialogType === 'add'">
<ElDivider content-position="left">初始账号信息</ElDivider>
<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>
</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>
</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 { ShopService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { ShopResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
defineOptions({ name: 'Shop' })
const dialogType = ref('add')
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
shop_name: '',
shop_code: '',
parent_id: undefined as number | undefined,
level: 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<ShopResponse[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.currentPage = 1
getShopList()
}
// 搜索处理
const handleSearch = () => {
pagination.currentPage = 1
getShopList()
}
// 表单配置项
const searchFormItems: SearchFormItem[] = [
{
label: '店铺名称',
prop: 'shop_name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入店铺名称'
}
},
{
label: '店铺编号',
prop: 'shop_code',
type: 'input',
config: {
clearable: true,
placeholder: '请输入店铺编号'
}
},
{
label: '上级ID',
prop: 'parent_id',
type: 'input',
config: {
clearable: true,
placeholder: '请输入上级店铺ID'
}
},
{
label: '店铺层级',
prop: 'level',
type: 'select',
options: [
{ label: '1级', value: 1 },
{ label: '2级', value: 2 },
{ label: '3级', value: 3 },
{ label: '4级', value: 4 },
{ label: '5级', value: 5 },
{ label: '6级', value: 6 },
{ label: '7级', value: 7 }
],
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: 'shop_name' },
{ label: '店铺编号', prop: 'shop_code' },
{ label: '层级', prop: 'level' },
{ label: '上级ID', prop: 'parent_id' },
{ label: '所在地区', prop: 'region' },
{ label: '联系人', prop: 'contact_name' },
{ label: '联系电话', prop: 'contact_phone' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
// 显示对话框
const showDialog = (type: string, row?: ShopResponse) => {
dialogVisible.value = true
dialogType.value = type
// 重置表单验证状态
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.id = row.id
formData.shop_name = row.shop_name
formData.shop_code = ''
formData.parent_id = null
formData.province = row.province || ''
formData.city = row.city || ''
formData.district = row.district || ''
formData.address = row.address || ''
formData.contact_name = row.contact_name || ''
formData.contact_phone = row.contact_phone || ''
formData.status = row.status
formData.init_username = ''
formData.init_password = ''
formData.init_phone = ''
} else {
formData.id = 0
formData.shop_name = ''
formData.shop_code = ''
formData.parent_id = null
formData.province = ''
formData.city = ''
formData.district = ''
formData.address = ''
formData.contact_name = ''
formData.contact_phone = ''
formData.status = CommonStatus.ENABLED
formData.init_username = ''
formData.init_password = ''
formData.init_phone = ''
}
}
// 删除店铺
const deleteShop = (row: ShopResponse) => {
ElMessageBox.confirm(`确定要删除店铺 ${row.shop_name} 吗?`, '删除店铺', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await ShopService.deleteShop(row.id)
ElMessage.success('删除成功')
getShopList()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150
},
{
prop: 'shop_code',
label: '店铺编号',
width: 120
},
{
prop: 'level',
label: '层级',
width: 80,
formatter: (row: ShopResponse) => {
return h(ElTag, { type: 'info', size: 'small' }, () => `${row.level}`)
}
},
{
prop: 'parent_id',
label: '上级ID',
width: 100,
formatter: (row: ShopResponse) => row.parent_id || '-'
},
{
prop: 'region',
label: '所在地区',
minWidth: 180,
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)
return parts.length > 0 ? parts.join(' / ') : '-'
}
},
{
prop: 'contact_name',
label: '联系人',
width: 100
},
{
prop: 'contact_phone',
label: '联系电话',
width: 130
},
{
prop: 'status',
label: '状态',
width: 100,
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: number) => handleStatusChange(row, val)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: ShopResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 150,
fixed: 'right',
formatter: (row: ShopResponse) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteShop(row)
})
])
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: 0,
shop_name: '',
shop_code: '',
parent_id: null as number | null,
province: '',
city: '',
district: '',
address: '',
contact_name: '',
contact_phone: '',
status: CommonStatus.ENABLED,
init_username: '',
init_password: '',
init_phone: ''
})
onMounted(() => {
getShopList()
})
// 获取店铺列表
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,
parent_id: searchForm.parent_id,
level: searchForm.level,
status: searchForm.status
}
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: 3, max: 50, message: '长度在 3 到 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' }
]
})
// 提交表单
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
}
// 可选字段
if (formData.parent_id) data.parent_id = 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
}
// 可选字段
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
getShopList()
} 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 handleStatusChange = async (row: ShopResponse, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await ShopService.updateShop(row.id, { status: newStatus })
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
</script>
<style lang="scss" scoped>
.shop-page {
// 店铺管理页面样式
}
</style>