增加产品和服务界面

This commit is contained in:
李志强 2026-03-20 19:48:50 +08:00
parent e3ab9c05bd
commit 0ad1dc31a1
6 changed files with 706 additions and 60 deletions

53
src/api/products.js Normal file
View File

@ -0,0 +1,53 @@
import request from '@/utils/request'
/**
* 获取特色产品列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getProductsList(params) {
return request({
url: '/admin/productsList',
method: 'get',
params
})
}
/**
* 添加特色产品
* @param {Object} data - 产品数据
* @returns {Promise}
*/
export function addProducts(data) {
return request({
url: '/admin/addProducts',
method: 'post',
data
})
}
/**
* 更新特色产品
* @param {number} id - 产品ID
* @param {Object} data - 产品数据
* @returns {Promise}
*/
export function updateProducts(id, data) {
return request({
url: `/admin/editProducts/${id}`,
method: 'put',
data
})
}
/**
* 删除特色产品
* @param {number} id - 产品ID
* @returns {Promise}
*/
export function deleteProducts(id) {
return request({
url: `/admin/deleteProducts/${id}`,
method: 'delete'
})
}

View File

@ -7,7 +7,7 @@ import request from '@/utils/request'
*/ */
export function getServiceList(params) { export function getServiceList(params) {
return request({ return request({
url: '/admin/services', url: '/admin/servicesList',
method: 'get', method: 'get',
params params
}) })
@ -20,7 +20,7 @@ export function getServiceList(params) {
*/ */
export function addService(data) { export function addService(data) {
return request({ return request({
url: '/admin/services', url: '/admin/addServices',
method: 'post', method: 'post',
data data
}) })
@ -34,7 +34,7 @@ export function addService(data) {
*/ */
export function updateService(id, data) { export function updateService(id, data) {
return request({ return request({
url: `/admin/services/${id}`, url: `/admin/editServices/${id}`,
method: 'put', method: 'put',
data data
}) })
@ -47,7 +47,7 @@ export function updateService(id, data) {
*/ */
export function deleteService(id) { export function deleteService(id) {
return request({ return request({
url: `/admin/services/${id}`, url: `/admin/deleteServices/${id}`,
method: 'delete' method: 'delete'
}) })
} }

View File

@ -0,0 +1,321 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="600px"
destroy-on-close
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="产品名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="产品描述" prop="desc">
<el-input
v-model="formData.desc"
type="textarea"
:rows="5"
placeholder="请输入产品描述"
/>
</el-form-item>
<el-form-item label="产品内容" prop="content">
<div class="editor-container">
<WangEditor v-model="formData.content" style="height: 450px;" />
</div>
</el-form-item>
<el-form-item label="产品图片" prop="thumb">
<div class="flex-direction">
<el-upload
class="image-uploader"
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleImageSuccess"
:on-error="handleImageError"
:before-upload="beforeImageUpload"
accept="image/*"
>
<img
v-if="formData.thumb"
:src="getImageUrl(formData.thumb)"
class="image-preview"
/>
<div v-else class="upload-placeholder">
<el-icon class="image-uploader-icon"><Plus /></el-icon>
<div class="el-upload__text">点击上传产品图片</div>
</div>
</el-upload>
<div
style="
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
"
>
建议尺寸200x200支持 jpgpnggif 格式大小不超过 2MB
</div>
</div>
<el-button
v-if="formData.thumb"
size="small"
type="danger"
text
@click="handleRemoveImage"
style="margin-top: 8px"
>
删除产品图片
</el-button>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="999" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from "vue";
import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { addProducts, updateProducts } from "@/api/products";
import WangEditor from "@/views/components/WangEditor.vue";
// @ts-ignore
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "添加产品",
},
isEdit: {
type: Boolean,
default: false,
},
rowData: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["update:modelValue", "success"]);
const visible = ref(false);
const submitLoading = ref(false);
const formRef = ref(null);
//
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
const uploadHeaders = ref({
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
});
//
const beforeImageUpload = (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;
};
//
const handleImageSuccess = (response) => {
if (response.code === 200 || response.code === 201) {
formData.thumb = response.data.url || response.data.path;
ElMessage.success(
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
);
} else {
ElMessage.error(response.msg || "图片上传失败");
}
};
//
const handleImageError = () => {
ElMessage.error("图片上传失败,请重试");
};
//
const handleRemoveImage = () => {
formData.thumb = "";
ElMessage.success("图片已删除");
};
// URL
const getImageUrl = (imagePath) => {
if (!imagePath) return "";
if (imagePath.startsWith("http")) return imagePath;
return API_BASE_URL + imagePath;
};
const formData = reactive({
title: "",
desc: "",
thumb: "",
url: "",
sort: 0,
status: 1,
});
const formRules = {
title: [
{ required: true, message: "请输入产品名称", trigger: "blur" },
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
],
desc: [
{ required: true, message: "请输入产品描述", trigger: "blur" },
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
],
};
// modelValue
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
resetForm();
if (props.isEdit && props.rowData) {
Object.assign(formData, props.rowData);
}
}
},
);
// visible
watch(visible, (val) => {
emit("update:modelValue", val);
});
//
const resetForm = () => {
formData.title = "";
formData.desc = "";
formData.thumb = "";
formData.url = "";
formData.sort = 0;
formData.status = 1;
};
//
const handleClose = () => {
visible.value = false;
};
//
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
let res;
if (props.isEdit) {
res = await updateProducts(props.rowData.id, formData);
} else {
res = await addProducts(formData);
}
if (res.code === 200) {
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
handleClose();
emit("success");
} else {
ElMessage.error(res.msg || "操作失败");
}
} catch (error) {
console.error("提交失败:", error);
ElMessage.error("操作失败");
} finally {
submitLoading.value = false;
}
}
});
};
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 图片上传样式 */
.image-uploader {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
width: 120px;
height: 120px;
}
.image-uploader:hover {
border-color: var(--el-color-primary);
}
.upload-placeholder {
width: 120px;
height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
}
.image-uploader-icon {
font-size: 28px;
color: #8c939d;
margin-bottom: 8px;
}
.el-upload__text {
font-size: 12px;
}
.image-preview {
width: 120px;
height: 120px;
object-fit: contain;
display: block;
}
.flex-direction {
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>

View File

@ -0,0 +1,304 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>企业产品管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAdd">
<el-icon>
<Plus />
</el-icon>
添加产品
</el-button>
<el-button @click="refresh" :loading="loading">
<el-icon>
<Refresh />
</el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="searchForm.keyword" placeholder="请输入产品名称搜索" clearable style="width: 200px; margin-right: 10px"
@keyup.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态筛选" clearable style="width: 120px; margin-right: 10px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
<el-button type="primary" @click="handleSearch">
<el-icon>
<Search />
</el-icon>
搜索
</el-button>
<el-button @click="resetSearch">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="productsList" style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="thumb" label="产品图片" min-width="150" align="center">
<template #default="{ row }">
<el-image v-if="row.thumb" :src="getImageUrl(row.thumb)" :preview-src-list="[getImageUrl(row.thumb)]"
fit="cover" class="product-thumb" :preview-teleported="true" lazy>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
<span v-else class="text-gray">暂无图片</span>
</template>
</el-table-column>
<el-table-column prop="title" label="产品名称" min-width="150" align="center" />
<el-table-column prop="desc" label="产品描述" min-width="150" align="center" />
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
<el-icon>
<Edit />
</el-icon>
编辑
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
<el-icon>
<Delete />
</el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handlePageChange" />
</div>
<!-- 添加/编辑组件 -->
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
@success="handleSuccess" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
import {
getProductsList,
updateProducts,
deleteProducts
} from '@/api/products'
import EditDialog from './components/edit.vue'
// @ts-ignore
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// URL
//
// 1) http(s)://...
// 2) / /storage/uploads/xxx.png
// 3) / storage/uploads/xxx.png
const getImageUrl = (imagePath) => {
if (!imagePath) return "";
if (typeof imagePath !== 'string') imagePath = String(imagePath);
if (imagePath.startsWith("http")) return imagePath;
const base = (API_BASE_URL ?? '').replace(/\/$/, '');
const path = imagePath.startsWith('/') ? imagePath : `/${imagePath}`;
return base + path;
};
//
const loading = ref(false)
//
const searchForm = reactive({
keyword: '',
status: ''
})
//
const pagination = reactive({
page: 1,
limit: 10,
total: 0
})
//
const productsList = ref([])
const selectedIds = ref([])
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const currentRow = ref({})
//
const fetchList = async () => {
loading.value = true
try {
const res = await getProductsList({
page: pagination.page,
limit: pagination.limit,
keyword: searchForm.keyword,
status: searchForm.status
})
if (res.code === 200) {
productsList.value = res.data.list
pagination.total = res.data.total
}
} catch (error) {
console.error('获取企业产品列表失败:', error)
ElMessage.error('获取企业产品列表失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
pagination.page = 1
fetchList()
}
//
const resetSearch = () => {
searchForm.keyword = ''
searchForm.status = ''
pagination.page = 1
fetchList()
}
//
const refresh = () => {
fetchList()
}
//
const handleSizeChange = (val) => {
pagination.limit = val
fetchList()
}
const handlePageChange = (val) => {
pagination.page = val
fetchList()
}
//
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map(item => item.id)
}
//
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '添加企业产品'
currentRow.value = {}
dialogVisible.value = true
}
//
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑企业产品'
currentRow.value = row
dialogVisible.value = true
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该企业产品吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await deleteProducts(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除企业产品失败:', error)
ElMessage.error('删除企业产品失败')
}
}
}
//
const handleStatusChange = async (row, val) => {
try {
const res = await updateProducts(row.id, { status: val })
if (res.code === 200) {
ElMessage.success('状态更新成功')
} else {
ElMessage.error(res.msg || '状态更新失败')
row.status = val === 1 ? 0 : 1
}
} catch (error) {
console.error('更新企业产品状态失败:', error)
ElMessage.error('更新企业产品状态失败')
row.status = val === 1 ? 0 : 1
}
}
//
const handleSuccess = () => {
fetchList()
}
//
onMounted(() => {
fetchList()
})
</script>
<style scoped>
.container-box {
padding: 20px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-bar h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.search-bar {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -69,12 +69,6 @@
<el-form-item label="排序" prop="sort"> <el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="999" /> <el-input-number v-model="formData.sort" :min="0" :max="999" />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">

View File

@ -4,11 +4,15 @@
<h2>特色服务管理</h2> <h2>特色服务管理</h2>
<div class="header-actions"> <div class="header-actions">
<el-button type="primary" @click="handleAdd"> <el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon> <el-icon>
<Plus />
</el-icon>
添加内容 添加内容
</el-button> </el-button>
<el-button @click="refresh" :loading="loading"> <el-button @click="refresh" :loading="loading">
<el-icon><Refresh /></el-icon> <el-icon>
<Refresh />
</el-icon>
刷新 刷新
</el-button> </el-button>
</div> </div>
@ -18,61 +22,42 @@
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-bar"> <div class="search-bar">
<el-input <el-input v-model="searchForm.keyword" placeholder="请输入内容搜索" clearable style="width: 200px; margin-right: 10px"
v-model="searchForm.keyword" @keyup.enter="handleSearch" />
placeholder="请输入内容搜索" <el-select v-model="searchForm.status" placeholder="状态筛选" clearable style="width: 120px; margin-right: 10px">
clearable
style="width: 200px; margin-right: 10px"
@keyup.enter="handleSearch"
/>
<el-select
v-model="searchForm.status"
placeholder="状态筛选"
clearable
style="width: 120px; margin-right: 10px"
>
<el-option label="启用" :value="1" /> <el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" /> <el-option label="禁用" :value="0" />
</el-select> </el-select>
<el-button type="primary" @click="handleSearch"> <el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon> <el-icon>
<Search />
</el-icon>
搜索 搜索
</el-button> </el-button>
<el-button @click="resetSearch">重置</el-button> <el-button @click="resetSearch">重置</el-button>
</div> </div>
<!-- 数据表格 --> <!-- 数据表格 -->
<el-table <el-table :data="servicesList" style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange">
:data="servicesList"
style="width: 100%"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column prop="id" label="ID" width="80" align="center" /> <el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="thumb" label="服务图片" min-width="150" align="center" /> <el-table-column prop="thumb" label="服务图片" min-width="150" align="center" />
<el-table-column prop="title" label="服务名称" min-width="150" align="center" /> <el-table-column prop="title" label="服务名称" min-width="150" align="center" />
<el-table-column prop="desc" label="服务描述" min-width="150" align="center" /> <el-table-column prop="desc" label="服务描述" min-width="150" align="center" />
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" /> <el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" /> <el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="(val) => handleStatusChange(row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" align="center" /> <el-table-column prop="create_time" label="创建时间" width="180" align="center" />
<el-table-column label="操作" width="200" align="center" fixed="right"> <el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)"> <el-button type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon> <el-icon>
<Edit />
</el-icon>
编辑 编辑
</el-button> </el-button>
<el-button type="danger" link @click="handleDelete(row)"> <el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon> <el-icon>
<Delete />
</el-icon>
删除 删除
</el-button> </el-button>
</template> </template>
@ -81,25 +66,14 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
v-model:current-page="pagination.page" :page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
v-model:page-size="pagination.limit" @size-change="handleSizeChange" @current-change="handlePageChange" />
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div> </div>
<!-- 添加/编辑组件 --> <!-- 添加/编辑组件 -->
<EditDialog <EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
v-model="dialogVisible" @success="handleSuccess" />
:title="dialogTitle"
:is-edit="isEdit"
:row-data="currentRow"
@success="handleSuccess"
/>
</div> </div>
</template> </template>