Files
art-design/src/views/admin/vehicle/brand.vue
T

655 lines
22 KiB
Vue
Raw Normal View History

2025-09-04 15:09:17 +08:00
<template>
<div class="page-content">
<div v-if="!isMobile" class="dict-layout">
<ElRow :gutter="16">
<ElCol :xs="24" :sm="10" :lg="10">
<div class="dict-type-card">
<div class="card-header">
<div style="display: flex; align-items: center">
<ElInput
v-model="brandKeyword"
placeholder="搜索品牌名称/编码"
clearable
@keyup.enter="getBrandList"
/>
<ElButton style="margin-left: 8px" @click="getBrandList" v-ripple>搜索</ElButton>
</div>
<ElButton
type="primary"
style="margin-left: 8px"
@click="showBrandDialog('add')"
v-if="hasAuth('brand@add')"
>新增品牌</ElButton
>
</div>
<el-table
:data="brandTableData"
v-loading="brandLoading"
height="calc(100vh - 280px)"
@row-click="handleBrandRowClick"
:highlight-current-row="true"
:row-key="'id'"
>
<el-table-column prop="brand_code" label="品牌编码" />
<el-table-column prop="brand_name" label="品牌名称" />
<el-table-column label="Logo" width="80">
<template #default="scope">
<div class="brand-logo-cell">
<img
v-if="scope.row.logo_full"
:src="scope.row.logo_full"
:alt="scope.row.brand_name"
/>
<div v-else class="logo-placeholder">{{ scope.row.brand_name?.charAt(0) }}</div>
</div>
</template>
</el-table-column>
<!-- <el-table-column prop="description" label="描述" show-overflow-tooltip /> -->
<!-- <el-table-column prop="sort" label="排序" width="80" /> -->
2025-09-17 15:10:28 +08:00
<el-table-column label="是否热门" prop="hot">
<template #default="scope">
<ElTag :type="scope.row.hot == 1 ? 'danger' : 'info'">
{{ scope.row.hot == 1 ? '是' : '否' }}
</ElTag>
</template>
</el-table-column>
2025-09-04 15:09:17 +08:00
<el-table-column label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" min-width="140" align="center">
<template #default="scope">
<el-button
link
size="small"
@click.stop="showBrandDialog('edit', scope.row)"
v-if="hasAuth('brand@update')"
>编辑</el-button
>
<el-button
link
type="danger"
size="small"
@click.stop="handleBrandDelete(scope.row)"
v-if="hasAuth('brand@destroy')"
>删除</el-button
>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无品牌" v-show="!brandLoading" />
</template>
</el-table>
<div class="type-pagination">
<el-pagination
background
2025-09-16 15:57:17 +08:00
layout="total, prev, pager, next"
2025-09-04 15:09:17 +08:00
:current-page="brandPagination.currentPage"
:page-size="brandPagination.pageSize"
:total="brandPagination.total"
:page-sizes="[10, 20, 30, 50]"
2025-09-16 15:57:17 +08:00
:pager-count="4"
2025-09-04 15:09:17 +08:00
@current-change="handleBrandCurrentChange"
@size-change="handleBrandSizeChange"
/>
</div>
</div>
</ElCol>
<ElCol :xs="24" :sm="14" :lg="14">
<div class="dict-item-card">
<div class="card-header">
<div class="left">
<span v-if="activeBrand"
>当前品牌:{{ activeBrand.brand_name }}({{ activeBrand.brand_code }})</span
>
<span v-else class="muted">请选择左侧车辆品牌</span>
2025-09-12 17:49:07 +08:00
<ElButton
type="primary"
style="margin-left: 8px"
@click="showSeriesDialog('add')"
:disabled="!activeBrand"
v-if="hasAuth('series@add')"
>新增系列</ElButton
>
2025-09-04 15:09:17 +08:00
</div>
<div class="right">
<ElInput
v-model="seriesKeyword"
placeholder="搜索系列名称/编码"
clearable
style="width: 240px"
@keyup.enter="getSeriesList"
/>
<ElButton style="margin-left: 8px" @click="getSeriesList" v-ripple>搜索</ElButton>
</div>
</div>
<ArtTable
ref="seriesTableRef"
row-key="id"
:loading="seriesLoading"
:data="seriesTableData"
:currentPage="seriesPagination.currentPage"
:pageSize="seriesPagination.pageSize"
:total="seriesPagination.total"
@size-change="handleSeriesSizeChange"
@current-change="handleSeriesCurrentChange"
>
<template #default>
<ElTableColumn label="系列编码" prop="series_code" min-width="120" />
<ElTableColumn label="系列名称" prop="series_name" min-width="140" />
<ElTableColumn
label="描述"
prop="description"
min-width="160"
show-overflow-tooltip
/>
<!-- <ElTableColumn label="排序" prop="sort" width="80" /> -->
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
scope.row.status ? '启用' : '停用'
}}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn fixed="right" label="操作" min-width="140px" align="center">
<template #default="scope">
<ElRow>
<el-button
link
@click="showSeriesDialog('edit', scope.row)"
v-if="hasAuth('series@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="handleSeriesDelete(scope.row)"
v-if="hasAuth('series@destroy')"
>删除</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
</ElCol>
</ElRow>
<ElDialog
v-model="brandDialogVisible"
:title="brandDialogType === 'add' ? '新增品牌' : '编辑品牌'"
width="750px"
align-center
>
<ElForm ref="brandFormRef" :model="brandForm" :rules="brandRules" label-width="100px">
<ElFormItem label="品牌编码" prop="brand_code">
<ElInput v-model="brandForm.brand_code" />
</ElFormItem>
<ElFormItem label="品牌名称" prop="brand_name">
<ElInput v-model="brandForm.brand_name" />
</ElFormItem>
<ElFormItem label="品牌Logo" prop="logo">
<ElUpload
class="logo-uploader"
:action="actionImageUplode"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleLogoSuccess"
:before-upload="beforeLogoUpload"
accept="image/*"
>
<img v-if="brandForm.logo" :src="logourl" class="logo-preview" />
<ElIcon v-else class="logo-uploader-icon"><Plus /></ElIcon>
</ElUpload>
</ElFormItem>
2025-09-17 15:10:28 +08:00
<ElFormItem label="是否热门" prop="hot">
<el-radio-group v-model="brandForm.hot">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</ElFormItem>
2025-09-04 15:09:17 +08:00
<ElFormItem label="品牌描述" prop="description">
<ElInput v-model="brandForm.description" type="textarea" :rows="3" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="brandForm.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="brandForm.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="brandDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleBrandSubmit(brandFormRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="seriesDialogVisible"
:title="seriesDialogType === 'add' ? '新增系列' : '编辑系列'"
width="750px"
align-center
>
<ElForm ref="seriesFormRef" :model="seriesForm" :rules="seriesRules" label-width="100px">
<ElFormItem label="所属品牌">
<ElInput
:model-value="
activeBrand ? `${activeBrand.brand_name}(${activeBrand.brand_code})` : ''
"
disabled
/>
</ElFormItem>
2025-09-16 15:57:17 +08:00
<!-- <ElFormItem label="系列编码" prop="series_code">
2025-09-04 15:09:17 +08:00
<ElInput v-model="seriesForm.series_code" />
2025-09-16 15:57:17 +08:00
</ElFormItem> -->
2025-09-04 15:09:17 +08:00
<ElFormItem label="系列名称" prop="series_name">
<ElInput v-model="seriesForm.series_name" />
</ElFormItem>
<ElFormItem label="系列描述" prop="description">
<ElInput v-model="seriesForm.description" type="textarea" :rows="3" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="seriesForm.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="seriesForm.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="seriesDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSeriesSubmit(seriesFormRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { vehicleService } from '@/api/vehicleApi'
import { useAuth } from '@/composables/useAuth'
import { useWindowSize } from '@vueuse/core'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const { hasAuth } = useAuth()
2025-09-15 17:14:38 +08:00
defineOptions({ name: 'AdminVehicleBrand' })
2025-09-04 15:09:17 +08:00
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const logourl = ref('')
// 左侧 品牌
const brandLoading = ref(false)
const brandKeyword = ref<string>('')
const brandTableData = ref<any[]>([])
const brandPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const activeBrand = ref<any | null>(null)
const brandFormRef = ref<FormInstance>()
const brandDialogVisible = ref(false)
const brandDialogType = ref<'add' | 'edit'>('add')
const brandForm = reactive<any>({
id: '',
brand_code: '',
brand_name: '',
logo: '',
2025-09-17 15:10:28 +08:00
hot: 0,
2025-09-04 15:09:17 +08:00
description: '',
sort: 0,
status: 1
})
const brandRules = reactive<FormRules>({
brand_code: [{ required: true, message: '请输入品牌编码', trigger: 'blur' }],
brand_name: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }]
})
const getBrandList = async () => {
brandLoading.value = true
try {
const { currentPage, pageSize } = brandPagination
const { records, current, size, total } = await vehicleService.getBrandList({
current: currentPage,
size: pageSize,
keyword: brandKeyword.value
})
brandTableData.value = records
Object.assign(brandPagination, { currentPage: current, pageSize: size, total })
if (!activeBrand.value && records && records.length) {
activeBrand.value = records[0]
await getSeriesList()
}
} finally {
brandLoading.value = false
}
}
const handleBrandRowClick = async (row: any) => {
activeBrand.value = row
seriesPagination.currentPage = 1
await getSeriesList()
}
const handleBrandSizeChange = (val: number) => {
brandPagination.pageSize = val
getBrandList()
}
const handleBrandCurrentChange = (val: number) => {
brandPagination.currentPage = val
getBrandList()
}
const showBrandDialog = (type: 'add' | 'edit', row?: any) => {
brandDialogVisible.value = true
brandDialogType.value = type
if (type === 'edit' && row) {
logourl.value = row.logo_full
Object.assign(brandForm, {
id: row.id,
brand_code: row.brand_code,
brand_name: row.brand_name,
2025-09-17 15:10:28 +08:00
hot: row.hot,
2025-09-04 15:09:17 +08:00
logo: row.logo,
description: row.description,
sort: row.sort ?? 0,
status: row.status ?? 1
})
} else {
Object.assign(brandForm, {
id: '',
brand_code: '',
brand_name: '',
logo: '',
2025-09-17 15:10:28 +08:00
hot: 0,
2025-09-04 15:09:17 +08:00
description: '',
sort: 0,
status: 1
})
}
}
const handleBrandDelete = (row: any) => {
ElMessageBox.confirm('确定删除该品牌吗?删除后其系列也可能被清理。', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await vehicleService.brandDelete({ id: row.id })
ElMessage.success(message)
if (activeBrand.value && activeBrand.value.id === row.id) {
activeBrand.value = null
seriesTableData.value = []
seriesPagination.total = 0
}
getBrandList()
})
}
const handleBrandSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
if (brandDialogType.value === 'add') {
const { message } = await vehicleService.brandAdd({
brand_code: brandForm.brand_code,
brand_name: brandForm.brand_name,
logo: brandForm.logo,
2025-09-17 15:10:28 +08:00
hot: brandForm.hot,
2025-09-04 15:09:17 +08:00
description: brandForm.description,
sort: brandForm.sort,
status: brandForm.status
})
ElMessage.success(message)
} else {
const { message } = await vehicleService.brandEdit(brandForm)
ElMessage.success(message)
}
brandDialogVisible.value = false
getBrandList()
})
}
// 右侧 系列
const seriesLoading = ref(false)
const seriesKeyword = ref<string>('')
const seriesTableData = ref<any[]>([])
const seriesTableRef = ref()
const seriesPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const seriesFormRef = ref<FormInstance>()
const seriesDialogVisible = ref(false)
const seriesDialogType = ref<'add' | 'edit'>('add')
const seriesForm = reactive<any>({
series_code: '',
series_name: '',
description: '',
sort: 0,
status: 1
})
const seriesRules = reactive<FormRules>({
series_name: [{ required: true, message: '请输入系列名称', trigger: 'blur' }]
})
const getSeriesList = async () => {
if (!activeBrand.value) return
seriesLoading.value = true
try {
const { currentPage, pageSize } = seriesPagination
const { records, current, size, total } = await vehicleService.getSeriesList({
current: currentPage,
size: pageSize,
keyword: seriesKeyword.value,
brand_id: activeBrand.value.id
})
seriesTableData.value = records
Object.assign(seriesPagination, { currentPage: current, pageSize: size, total })
} finally {
seriesLoading.value = false
}
}
const handleSeriesSizeChange = (val: number) => {
seriesPagination.pageSize = val
getSeriesList()
}
const handleSeriesCurrentChange = (val: number) => {
seriesPagination.currentPage = val
getSeriesList()
}
const showSeriesDialog = (type: 'add' | 'edit', row?: any) => {
seriesDialogVisible.value = true
seriesDialogType.value = type
if (type === 'edit' && row) {
Object.assign(seriesForm, {
id: row.id,
series_code: row.series_code,
series_name: row.series_name,
description: row.description,
sort: row.sort ?? 0,
status: row.status ?? 1
})
} else {
Object.assign(seriesForm, {
id: '',
series_code: '',
series_name: '',
description: '',
sort: 0,
status: 1
})
}
}
const handleSeriesDelete = (row: any) => {
ElMessageBox.confirm('确定删除该系列吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await vehicleService.seriesDelete({ id: row.id })
ElMessage.success(message)
getSeriesList()
})
}
const handleSeriesSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl || !activeBrand.value) return
await formEl.validate(async (valid) => {
if (!valid) return
if (seriesDialogType.value === 'add') {
const { message } = await vehicleService.seriesAdd({
brand_id: activeBrand.value.id,
series_name: seriesForm.series_name,
description: seriesForm.description,
sort: seriesForm.sort,
status: seriesForm.status
})
ElMessage.success(message)
} else {
const { message } = await vehicleService.seriesEdit({
id: seriesForm.id,
brand_id: activeBrand.value.id,
series_name: seriesForm.series_name,
description: seriesForm.description,
sort: seriesForm.sort,
status: seriesForm.status
})
ElMessage.success(message)
}
seriesDialogVisible.value = false
getSeriesList()
})
}
// Logo上传处理
const handleLogoSuccess = (response: any) => {
brandForm.logo = response.url || (response.data && response.data.file_name) || ''
// 如需同步到 form.picture
logourl.value = response.url || (response.data && response.data.file_url) || ''
ElMessage.success('Logo上传成功')
}
const beforeLogoUpload = (file: File) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
onMounted(async () => {
await getBrandList()
})
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.dict-layout {
.dict-type-card,
.dict-item-card {
background: var(--el-bg-color);
padding: 12px;
border-radius: 8px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.left {
font-weight: 500;
}
.right {
display: flex;
align-items: center;
}
.muted {
color: var(--el-text-color-secondary);
}
}
.type-pagination {
margin-top: 8px;
text-align: center;
}
}
.brand-logo-cell {
display: flex;
justify-content: center;
align-items: center;
img {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.logo-placeholder {
width: 40px;
height: 40px;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
border-radius: 4px;
}
}
.logo-uploader {
:deep(.el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
}
}
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
}
.logo-preview {
width: 100px;
height: 100px;
display: block;
object-fit: cover;
}
.dialog-footer {
text-align: right;
}
</style>