优化企业网站功能
This commit is contained in:
+686
-686
File diff suppressed because it is too large
Load Diff
+349
-349
@@ -1,349 +1,349 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="文章预览" size="60%">
|
||||
<div class="article-preview">
|
||||
<div class="article-header">
|
||||
<h1 class="article-title">{{ model?.title || "无标题" }}</h1>
|
||||
<div class="article-meta">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-price-tag"></i>
|
||||
<el-tag type="primary">{{ getCategoryName(model?.cate) }}</el-tag>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-user"></i>
|
||||
作者:{{ model?.author }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
发布人:{{ model?.publisher || "暂未发布" }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
阅读量:{{ model?.views || 0 }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
收藏量:{{ model?.likes || 0 }}
|
||||
</span>
|
||||
<span class="meta-item" v-if="model?.publish_time">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ formatDate(model.publish_time) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
创建日期:{{ model?.create_time }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
<div
|
||||
v-if="model?.content"
|
||||
v-html="model.content"
|
||||
class="content-html"
|
||||
></div>
|
||||
<div v-else class="no-content">
|
||||
<el-empty description="暂无内容"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { listCategories } from "@/api/article";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const categoryOptions = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await listCategories({ page: 1, limit: 1000 });
|
||||
if (res.code === 200) {
|
||||
categoryOptions.value = Array.isArray(res.data)
|
||||
? res.data
|
||||
: Array.isArray(res.data?.list)
|
||||
? res.data.list
|
||||
: [];
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "获取分类列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取分类列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 根据分类ID获取分类名称
|
||||
const getCategoryName = (cate) => {
|
||||
if (!cate && cate !== 0) return "无分类";
|
||||
// 确保比较时类型一致
|
||||
const cateId = Number(cate);
|
||||
// 传入的不是数字(已经是分类名),直接展示
|
||||
if (Number.isNaN(cateId)) return String(cate);
|
||||
const category = categoryOptions.value.find(
|
||||
(item) => Number(item.id) === cateId,
|
||||
);
|
||||
return category?.name || "无分类";
|
||||
};
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-preview {
|
||||
padding: 20px;
|
||||
|
||||
.article-header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.article-title {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
/* 确保article-content直接子元素的样式 */
|
||||
& > div {
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 支持所有子元素的内联样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
ul,
|
||||
ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
blockquote {
|
||||
border-left: 4px solid #ebeef5;
|
||||
padding-left: 15px;
|
||||
margin: 15px 0;
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
:deep(code) {
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 13px;
|
||||
color: #f06b6b;
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
:deep(pre) {
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d3f 100%);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
margin: 20px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 图片样式 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保内联样式能够生效 */
|
||||
[style] {
|
||||
/* 允许内联样式正常工作 */
|
||||
all: unset;
|
||||
/* 重新应用基本样式 */
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
/* 允许特定内联样式覆盖 */
|
||||
&[style*="text-align"] {
|
||||
text-align: var(--text-align, inherit) !important;
|
||||
}
|
||||
&[style*="color"] {
|
||||
color: var(--color, inherit) !important;
|
||||
}
|
||||
&[style*="font-size"] {
|
||||
font-size: var(--font-size, inherit) !important;
|
||||
}
|
||||
&[style*="font-weight"] {
|
||||
font-weight: var(--font-weight, inherit) !important;
|
||||
}
|
||||
&[style*="font-style"] {
|
||||
font-style: var(--font-style, inherit) !important;
|
||||
}
|
||||
&[style*="text-decoration"] {
|
||||
text-decoration: var(--text-decoration, inherit) !important;
|
||||
}
|
||||
&[style*="margin"] {
|
||||
margin: var(--margin, inherit) !important;
|
||||
}
|
||||
&[style*="padding"] {
|
||||
padding: var(--padding, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 直接支持text-align属性 */
|
||||
[align] {
|
||||
text-align: attr(align) !important;
|
||||
}
|
||||
|
||||
/* 确保content-html类的样式 */
|
||||
.content-html {
|
||||
& > * {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
& > h1,
|
||||
& > h2,
|
||||
& > h3,
|
||||
& > h4,
|
||||
& > h5,
|
||||
& > h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 支持居中样式 */
|
||||
& > p[style*="text-align: center"],
|
||||
& > p[align="center"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* 图片居中 */
|
||||
& > p[style*="text-align: center"] img,
|
||||
& > p[align="center"] img {
|
||||
display: inline-block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.no-content {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="文章预览" size="60%">
|
||||
<div class="article-preview">
|
||||
<div class="article-header">
|
||||
<h1 class="article-title">{{ model?.title || "无标题" }}</h1>
|
||||
<div class="article-meta">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-price-tag"></i>
|
||||
<el-tag type="primary">{{ getCategoryName(model?.cate) }}</el-tag>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-user"></i>
|
||||
作者:{{ model?.author }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
发布人:{{ model?.publisher || "暂未发布" }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
阅读量:{{ model?.views || 0 }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
收藏量:{{ model?.likes || 0 }}
|
||||
</span>
|
||||
<span class="meta-item" v-if="model?.publish_time">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ formatDate(model.publish_time) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
创建日期:{{ model?.create_time }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
<div
|
||||
v-if="model?.content"
|
||||
v-html="model.content"
|
||||
class="content-html"
|
||||
></div>
|
||||
<div v-else class="no-content">
|
||||
<el-empty description="暂无内容"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { listCategories } from "@/api/article";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const categoryOptions = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await listCategories({ page: 1, limit: 1000 });
|
||||
if (res.code === 200) {
|
||||
categoryOptions.value = Array.isArray(res.data)
|
||||
? res.data
|
||||
: Array.isArray(res.data?.list)
|
||||
? res.data.list
|
||||
: [];
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "获取分类列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取分类列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 根据分类ID获取分类名称
|
||||
const getCategoryName = (cate) => {
|
||||
if (!cate && cate !== 0) return "无分类";
|
||||
// 确保比较时类型一致
|
||||
const cateId = Number(cate);
|
||||
// 传入的不是数字(已经是分类名),直接展示
|
||||
if (Number.isNaN(cateId)) return String(cate);
|
||||
const category = categoryOptions.value.find(
|
||||
(item) => Number(item.id) === cateId,
|
||||
);
|
||||
return category?.name || "无分类";
|
||||
};
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-preview {
|
||||
padding: 20px;
|
||||
|
||||
.article-header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.article-title {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
/* 确保article-content直接子元素的样式 */
|
||||
& > div {
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 支持所有子元素的内联样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
ul,
|
||||
ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
blockquote {
|
||||
border-left: 4px solid #ebeef5;
|
||||
padding-left: 15px;
|
||||
margin: 15px 0;
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
:deep(code) {
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 13px;
|
||||
color: #f06b6b;
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
:deep(pre) {
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d3f 100%);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
margin: 20px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 图片样式 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保内联样式能够生效 */
|
||||
[style] {
|
||||
/* 允许内联样式正常工作 */
|
||||
all: unset;
|
||||
/* 重新应用基本样式 */
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
/* 允许特定内联样式覆盖 */
|
||||
&[style*="text-align"] {
|
||||
text-align: var(--text-align, inherit) !important;
|
||||
}
|
||||
&[style*="color"] {
|
||||
color: var(--color, inherit) !important;
|
||||
}
|
||||
&[style*="font-size"] {
|
||||
font-size: var(--font-size, inherit) !important;
|
||||
}
|
||||
&[style*="font-weight"] {
|
||||
font-weight: var(--font-weight, inherit) !important;
|
||||
}
|
||||
&[style*="font-style"] {
|
||||
font-style: var(--font-style, inherit) !important;
|
||||
}
|
||||
&[style*="text-decoration"] {
|
||||
text-decoration: var(--text-decoration, inherit) !important;
|
||||
}
|
||||
&[style*="margin"] {
|
||||
margin: var(--margin, inherit) !important;
|
||||
}
|
||||
&[style*="padding"] {
|
||||
padding: var(--padding, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 直接支持text-align属性 */
|
||||
[align] {
|
||||
text-align: attr(align) !important;
|
||||
}
|
||||
|
||||
/* 确保content-html类的样式 */
|
||||
.content-html {
|
||||
& > * {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
& > h1,
|
||||
& > h2,
|
||||
& > h3,
|
||||
& > h4,
|
||||
& > h5,
|
||||
& > h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 支持居中样式 */
|
||||
& > p[style*="text-align: center"],
|
||||
& > p[align="center"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* 图片居中 */
|
||||
& > p[style*="text-align: center"] img,
|
||||
& > p[align="center"] img {
|
||||
display: inline-block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.no-content {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+677
-677
File diff suppressed because it is too large
Load Diff
+277
-277
@@ -1,278 +1,278 @@
|
||||
<template>
|
||||
<div class="category-node-wrapper">
|
||||
<div
|
||||
class="category-item"
|
||||
:class="{ 'child-item': level > 0, disabled: !item.status }"
|
||||
>
|
||||
<div
|
||||
class="category-main"
|
||||
:style="{ paddingLeft: (level * 24 + 20) + 'px' }"
|
||||
@click="handleRowClick"
|
||||
>
|
||||
<div class="expand-btn" v-if="item.children && item.children.length > 0">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="toggleExpand"
|
||||
class="expand-button"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.expanded ? 'ArrowDown' : 'ArrowRight'" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="expand-spacer" v-else></div>
|
||||
|
||||
<div class="category-info">
|
||||
<div class="color-dot" :style="{ backgroundColor: item.color }"></div>
|
||||
<div class="category-icon">
|
||||
<el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
|
||||
<el-icon v-else class="default-icon"><Document /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="category-text">
|
||||
<div class="category-title">
|
||||
<span class="name">{{ item.label }}</span>
|
||||
<el-tag
|
||||
:type="item.status ? 'success' : 'danger'"
|
||||
size="small"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ item.status ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="category-desc" v-if="item.remark">{{ item.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-actions">
|
||||
<el-button-group size="small">
|
||||
<el-button type="text" title="编辑" @click.stop="$emit('edit', item)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.tid !== 0"
|
||||
type="text"
|
||||
class="danger"
|
||||
title="删除"
|
||||
@click.stop="$emit('delete', item)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<el-tag v-else type="info" size="small" title="全局分类不可删除">
|
||||
全局
|
||||
</el-tag>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.children && item.children.length > 0 && item.expanded"
|
||||
class="category-children"
|
||||
>
|
||||
<category-node
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
@edit="$emit('edit', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown, ArrowRight, Edit, Plus, Delete, Document } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
level: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'add-child', 'delete']);
|
||||
|
||||
const toggleExpand = () => {
|
||||
props.item.expanded = !props.item.expanded;
|
||||
};
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (props.item.children && props.item.children.length > 0) {
|
||||
toggleExpand();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-children {
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
.category-main {
|
||||
padding: 12px !important;
|
||||
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<template>
|
||||
<div class="category-node-wrapper">
|
||||
<div
|
||||
class="category-item"
|
||||
:class="{ 'child-item': level > 0, disabled: !item.status }"
|
||||
>
|
||||
<div
|
||||
class="category-main"
|
||||
:style="{ paddingLeft: (level * 24 + 20) + 'px' }"
|
||||
@click="handleRowClick"
|
||||
>
|
||||
<div class="expand-btn" v-if="item.children && item.children.length > 0">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="toggleExpand"
|
||||
class="expand-button"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.expanded ? 'ArrowDown' : 'ArrowRight'" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="expand-spacer" v-else></div>
|
||||
|
||||
<div class="category-info">
|
||||
<div class="color-dot" :style="{ backgroundColor: item.color }"></div>
|
||||
<div class="category-icon">
|
||||
<el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
|
||||
<el-icon v-else class="default-icon"><Document /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="category-text">
|
||||
<div class="category-title">
|
||||
<span class="name">{{ item.label }}</span>
|
||||
<el-tag
|
||||
:type="item.status ? 'success' : 'danger'"
|
||||
size="small"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ item.status ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="category-desc" v-if="item.remark">{{ item.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-actions">
|
||||
<el-button-group size="small">
|
||||
<el-button type="text" title="编辑" @click.stop="$emit('edit', item)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.tid !== 0"
|
||||
type="text"
|
||||
class="danger"
|
||||
title="删除"
|
||||
@click.stop="$emit('delete', item)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<el-tag v-else type="info" size="small" title="全局分类不可删除">
|
||||
全局
|
||||
</el-tag>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.children && item.children.length > 0 && item.expanded"
|
||||
class="category-children"
|
||||
>
|
||||
<category-node
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
@edit="$emit('edit', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown, ArrowRight, Edit, Plus, Delete, Document } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
level: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'add-child', 'delete']);
|
||||
|
||||
const toggleExpand = () => {
|
||||
props.item.expanded = !props.item.expanded;
|
||||
};
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (props.item.children && props.item.children.length > 0) {
|
||||
toggleExpand();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-children {
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
.category-main {
|
||||
padding: 12px !important;
|
||||
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+324
-324
@@ -1,324 +1,324 @@
|
||||
<template>
|
||||
<el-dialog v-model="visibleInternal" :title="dialogTitle" width="600px" :close-on-click-modal="false"
|
||||
@close="closeDialog">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="分类名称" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入分类名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="父级分类" prop="parentId">
|
||||
<el-tree-select v-model="formData.parentId" :data="treeData"
|
||||
:props="{ label: 'label', value: 'id', children: 'children' }" placeholder="选择父级分类(可选)" clearable
|
||||
filterable check-strictly style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right"
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="默认图片" prop="image">
|
||||
<div class="uploads">
|
||||
<el-upload v-model:file-list="fileList" :auto-upload="false" :before-upload="beforeImgUpload" list-type="picture-card" :limit="1" :on-change="handleUploadChange">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<el-icon>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<div class="upload-tip">
|
||||
<span>建议尺寸:250px × 140px</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="4" placeholder="请输入分类描述..." maxlength="200"
|
||||
show-word-limit />
|
||||
</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>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
{{ isEdit ? '更新' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Plus, ZoomIn, Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
import { createCategory, editCategory, listCategories } from '@/api/article'
|
||||
import { uploadFile } from '@/api/file.js'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
category: { type: Object as () => any | null, default: null }, // 传入的分类对象,null 表示新增
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
// 可见性同步
|
||||
const visibleInternal = ref(props.modelValue)
|
||||
watch(() => props.modelValue, v => (visibleInternal.value = v))
|
||||
watch(visibleInternal, v => {
|
||||
emit('update:modelValue', v)
|
||||
if (v) loadTreeData()
|
||||
})
|
||||
|
||||
// 数据加载:父级分类树
|
||||
const treeData = ref<any[]>([])
|
||||
watch(treeData, () => {
|
||||
// 触发tree-select重新渲染已选 label
|
||||
formData.parentId = formData.parentId ?? 0
|
||||
})
|
||||
|
||||
async function loadTreeData() {
|
||||
const res = await listCategories({ page: 1, pageSize: 1000 })
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.records || res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建树形
|
||||
function buildTree(data: any[]) {
|
||||
const map = new Map()
|
||||
const roots: any[] = []
|
||||
data.forEach((item) => {
|
||||
map.set(item.id, { ...item, label: item.name ?? item.label, children: [] })
|
||||
})
|
||||
map.forEach((item: any) => {
|
||||
if (item.cid && map.has(item.cid)) {
|
||||
map.get(item.cid).children.push(item)
|
||||
} else {
|
||||
roots.push(item)
|
||||
}
|
||||
})
|
||||
return [{ id: 0, label: '顶级', children: roots }]
|
||||
}
|
||||
|
||||
// 表单
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
id: null as number | null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0 as number,
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
if (formRef.value) formRef.value.clearValidate()
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// 当传入 category 变化时同步
|
||||
watch(
|
||||
() => props.category,
|
||||
(val) => {
|
||||
if (val) {
|
||||
Object.assign(formData, {
|
||||
id: val.id,
|
||||
label: val.label,
|
||||
image: val.image,
|
||||
remark: val.remark,
|
||||
sort: val.sort,
|
||||
status: val.status,
|
||||
parentId: val.parentId ?? val.cid ?? 0,
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 上传相关
|
||||
const fileList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
function beforeImgUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isImage) ElMessage.error('仅支持图片格式')
|
||||
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
|
||||
return isImage && isLt10M
|
||||
}
|
||||
|
||||
function handleRemove(file: any) {
|
||||
fileList.value = []
|
||||
formData.image = ''
|
||||
}
|
||||
|
||||
async function handleUploadChange(file: any) {
|
||||
if (file.raw) {
|
||||
const isImage = file.raw.type.startsWith('image/')
|
||||
const isLt10M = file.raw.size / 1024 / 1024 < 10
|
||||
if (!isImage || !isLt10M) {
|
||||
fileList.value = []
|
||||
ElMessage.error(isImage ? '图片大小不能超过10MB' : '仅支持图片格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePictureCardPreview(file: any) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 校验
|
||||
const formRules = {
|
||||
label: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [{ max: 200, message: '描述不能超过200个字符', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const isEdit = computed(() => !!formData.id)
|
||||
// 预览对话框
|
||||
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑分类' : '新增分类'))
|
||||
const submitLoading = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
submitLoading.value = true
|
||||
|
||||
let imageUrl = formData.image
|
||||
|
||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', fileList.value[0].raw)
|
||||
uploadFormData.append('cate', 'category')
|
||||
|
||||
const uploadRes = await uploadFile(uploadFormData)
|
||||
if (uploadRes?.data?.url) {
|
||||
imageUrl = uploadRes.data.url
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: formData.label,
|
||||
image: imageUrl,
|
||||
desc: formData.remark,
|
||||
sort: formData.sort,
|
||||
status: formData.status,
|
||||
cid: formData.parentId,
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await editCategory(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createCategory(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
emit('saved')
|
||||
closeDialog()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visibleInternal.value = false
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 如果需要额外样式,可在此处编写 */
|
||||
.avatar-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 32px;
|
||||
color: #8c939d;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.uploads{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<el-dialog v-model="visibleInternal" :title="dialogTitle" width="600px" :close-on-click-modal="false"
|
||||
@close="closeDialog">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="分类名称" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入分类名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="父级分类" prop="parentId">
|
||||
<el-tree-select v-model="formData.parentId" :data="treeData"
|
||||
:props="{ label: 'label', value: 'id', children: 'children' }" placeholder="选择父级分类(可选)" clearable
|
||||
filterable check-strictly style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right"
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="默认图片" prop="image">
|
||||
<div class="uploads">
|
||||
<el-upload v-model:file-list="fileList" :auto-upload="false" :before-upload="beforeImgUpload" list-type="picture-card" :limit="1" :on-change="handleUploadChange">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<el-icon>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<div class="upload-tip">
|
||||
<span>建议尺寸:250px × 140px</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="4" placeholder="请输入分类描述..." maxlength="200"
|
||||
show-word-limit />
|
||||
</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>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
{{ isEdit ? '更新' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Plus, ZoomIn, Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
import { createCategory, editCategory, listCategories } from '@/api/article'
|
||||
import { uploadFile } from '@/api/file.js'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
category: { type: Object as () => any | null, default: null }, // 传入的分类对象,null 表示新增
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
// 可见性同步
|
||||
const visibleInternal = ref(props.modelValue)
|
||||
watch(() => props.modelValue, v => (visibleInternal.value = v))
|
||||
watch(visibleInternal, v => {
|
||||
emit('update:modelValue', v)
|
||||
if (v) loadTreeData()
|
||||
})
|
||||
|
||||
// 数据加载:父级分类树
|
||||
const treeData = ref<any[]>([])
|
||||
watch(treeData, () => {
|
||||
// 触发tree-select重新渲染已选 label
|
||||
formData.parentId = formData.parentId ?? 0
|
||||
})
|
||||
|
||||
async function loadTreeData() {
|
||||
const res = await listCategories({ page: 1, pageSize: 1000 })
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.records || res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建树形
|
||||
function buildTree(data: any[]) {
|
||||
const map = new Map()
|
||||
const roots: any[] = []
|
||||
data.forEach((item) => {
|
||||
map.set(item.id, { ...item, label: item.name ?? item.label, children: [] })
|
||||
})
|
||||
map.forEach((item: any) => {
|
||||
if (item.cid && map.has(item.cid)) {
|
||||
map.get(item.cid).children.push(item)
|
||||
} else {
|
||||
roots.push(item)
|
||||
}
|
||||
})
|
||||
return [{ id: 0, label: '顶级', children: roots }]
|
||||
}
|
||||
|
||||
// 表单
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
id: null as number | null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0 as number,
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
if (formRef.value) formRef.value.clearValidate()
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// 当传入 category 变化时同步
|
||||
watch(
|
||||
() => props.category,
|
||||
(val) => {
|
||||
if (val) {
|
||||
Object.assign(formData, {
|
||||
id: val.id,
|
||||
label: val.label,
|
||||
image: val.image,
|
||||
remark: val.remark,
|
||||
sort: val.sort,
|
||||
status: val.status,
|
||||
parentId: val.parentId ?? val.cid ?? 0,
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 上传相关
|
||||
const fileList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
function beforeImgUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isImage) ElMessage.error('仅支持图片格式')
|
||||
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
|
||||
return isImage && isLt10M
|
||||
}
|
||||
|
||||
function handleRemove(file: any) {
|
||||
fileList.value = []
|
||||
formData.image = ''
|
||||
}
|
||||
|
||||
async function handleUploadChange(file: any) {
|
||||
if (file.raw) {
|
||||
const isImage = file.raw.type.startsWith('image/')
|
||||
const isLt10M = file.raw.size / 1024 / 1024 < 10
|
||||
if (!isImage || !isLt10M) {
|
||||
fileList.value = []
|
||||
ElMessage.error(isImage ? '图片大小不能超过10MB' : '仅支持图片格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePictureCardPreview(file: any) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 校验
|
||||
const formRules = {
|
||||
label: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [{ max: 200, message: '描述不能超过200个字符', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const isEdit = computed(() => !!formData.id)
|
||||
// 预览对话框
|
||||
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑分类' : '新增分类'))
|
||||
const submitLoading = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
submitLoading.value = true
|
||||
|
||||
let imageUrl = formData.image
|
||||
|
||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', fileList.value[0].raw)
|
||||
uploadFormData.append('cate', 'category')
|
||||
|
||||
const uploadRes = await uploadFile(uploadFormData)
|
||||
if (uploadRes?.data?.url) {
|
||||
imageUrl = uploadRes.data.url
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: formData.label,
|
||||
image: imageUrl,
|
||||
desc: formData.remark,
|
||||
sort: formData.sort,
|
||||
status: formData.status,
|
||||
cid: formData.parentId,
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await editCategory(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createCategory(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
emit('saved')
|
||||
closeDialog()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visibleInternal.value = false
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 如果需要额外样式,可在此处编写 */
|
||||
.avatar-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 32px;
|
||||
color: #8c939d;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.uploads{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
+498
-498
@@ -1,498 +1,498 @@
|
||||
<template>
|
||||
<div class="category-manager">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h3>文章分类</h3>
|
||||
<p>共 {{ totalCount }} 个分类</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">
|
||||
新增分类
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-list" v-loading="loading">
|
||||
<div v-if="filteredCategories.length === 0" class="empty-state">
|
||||
<el-empty description="暂无文章分类" />
|
||||
</div>
|
||||
|
||||
<div v-else class="category-tree">
|
||||
<category-node
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:item="category"
|
||||
:level="0"
|
||||
@edit="handleEdit"
|
||||
@add-child="handleAddChild"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-cate
|
||||
v-model="dialogVisible"
|
||||
:category="currentEdit"
|
||||
@saved="fetchCategories"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Check,
|
||||
Close,
|
||||
Search,
|
||||
Refresh,
|
||||
Document,
|
||||
Picture,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { allCategories, deleteCategory } from "@/api/article";
|
||||
import EditCate from "@/views/apps/cms/articles/components/edit-cate.vue";
|
||||
import CategoryNode from "./components/CategoryNode.vue";
|
||||
|
||||
// 颜色预设
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const currentEdit = ref(null);
|
||||
const searchText = ref("");
|
||||
const categories = ref([]);
|
||||
|
||||
// 新增分类
|
||||
const addCategory = ref(null);
|
||||
const handleAdd = () => {
|
||||
addCategory.value = { parentId: 0 };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => categories.value.length);
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (!searchText.value) {
|
||||
return categories.value;
|
||||
}
|
||||
|
||||
const search = searchText.value.toLowerCase();
|
||||
return categories.value.filter(
|
||||
(category) =>
|
||||
category.label.toLowerCase().includes(search) ||
|
||||
(category.remark && category.remark.toLowerCase().includes(search)),
|
||||
);
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
function buildTree(data) {
|
||||
// 1. 标准化数据字段
|
||||
const transformed = data.map((item) => ({
|
||||
...item,
|
||||
label: item.name ?? item.label ?? "",
|
||||
remark: item.desc ?? item.remark ?? "",
|
||||
parentId: item.parentId ?? item.cid ?? 0,
|
||||
children: [],
|
||||
expanded: true, // 默认展开所有级
|
||||
}));
|
||||
|
||||
const map = new Map();
|
||||
transformed.forEach((item) => map.set(item.id, item));
|
||||
|
||||
const roots = [];
|
||||
transformed.forEach((item) => {
|
||||
const parent = map.get(item.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
// 如果没有父节点,或者是顶级节点 (parentId 为 0)
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function handleCreate() {
|
||||
currentEdit.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(category) {
|
||||
currentEdit.value = category;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleAddChild(parent) {
|
||||
currentEdit.value = { parentId: parent.id };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
await fetchCategories();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
function toggleExpand(category) {
|
||||
category.expanded = !category.expanded;
|
||||
}
|
||||
|
||||
function handleDelete(category) {
|
||||
if (Number(category.tid) === 0 || category.is_global === true) {
|
||||
ElMessage.warning("全局分类不可删除");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
deleteCategory(category.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
fetchCategories();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("删除分类失败:", error);
|
||||
ElMessage.error("删除失败");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// API调用
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await allCategories({ keyword: searchText.value });
|
||||
if (response.code === 200) {
|
||||
const categoryList = response.data || [];
|
||||
// 这里会递归生成三级、四级等
|
||||
categories.value = buildTree(categoryList);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-manager {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.page-title {
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏样式
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.el-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.search-stats {
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 分类列表容器
|
||||
.category-list {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.empty-state {
|
||||
padding: 80px 40px;
|
||||
.empty-icon {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归树结构核心样式
|
||||
.category-tree {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
// 层级背景区分 (可选:让子级背景稍微深一点点)
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px; // 左右 padding 保持,左侧缩进通过内联 style 控制
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
// 展开收起按钮占位
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分类具体内容
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮组
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4; // 默认低透明度,鼠标悬浮时高亮
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标悬停时显示按钮
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子分类容器样式
|
||||
.category-children {
|
||||
margin-left: 0;
|
||||
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus 对话框深度样式美化
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
.el-dialog__header {
|
||||
padding: 20px 24px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.category-manager {
|
||||
padding: 12px;
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
.page-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-list .category-tree .category-item .category-main {
|
||||
padding: 12px !important; // 移动端取消大缩进,改用其他视觉暗示
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.category-desc {
|
||||
display: none;
|
||||
} // 隐藏描述节省空间
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="category-manager">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h3>文章分类</h3>
|
||||
<p>共 {{ totalCount }} 个分类</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">
|
||||
新增分类
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-list" v-loading="loading">
|
||||
<div v-if="filteredCategories.length === 0" class="empty-state">
|
||||
<el-empty description="暂无文章分类" />
|
||||
</div>
|
||||
|
||||
<div v-else class="category-tree">
|
||||
<category-node
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:item="category"
|
||||
:level="0"
|
||||
@edit="handleEdit"
|
||||
@add-child="handleAddChild"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-cate
|
||||
v-model="dialogVisible"
|
||||
:category="currentEdit"
|
||||
@saved="fetchCategories"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Check,
|
||||
Close,
|
||||
Search,
|
||||
Refresh,
|
||||
Document,
|
||||
Picture,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { allCategories, deleteCategory } from "@/api/article";
|
||||
import EditCate from "./components/edit.vue";
|
||||
import CategoryNode from "./components/CategoryNode.vue";
|
||||
|
||||
// 颜色预设
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const currentEdit = ref(null);
|
||||
const searchText = ref("");
|
||||
const categories = ref([]);
|
||||
|
||||
// 新增分类
|
||||
const addCategory = ref(null);
|
||||
const handleAdd = () => {
|
||||
addCategory.value = { parentId: 0 };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => categories.value.length);
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (!searchText.value) {
|
||||
return categories.value;
|
||||
}
|
||||
|
||||
const search = searchText.value.toLowerCase();
|
||||
return categories.value.filter(
|
||||
(category) =>
|
||||
category.label.toLowerCase().includes(search) ||
|
||||
(category.remark && category.remark.toLowerCase().includes(search)),
|
||||
);
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
function buildTree(data) {
|
||||
// 1. 标准化数据字段
|
||||
const transformed = data.map((item) => ({
|
||||
...item,
|
||||
label: item.name ?? item.label ?? "",
|
||||
remark: item.desc ?? item.remark ?? "",
|
||||
parentId: item.parentId ?? item.cid ?? 0,
|
||||
children: [],
|
||||
expanded: true, // 默认展开所有级
|
||||
}));
|
||||
|
||||
const map = new Map();
|
||||
transformed.forEach((item) => map.set(item.id, item));
|
||||
|
||||
const roots = [];
|
||||
transformed.forEach((item) => {
|
||||
const parent = map.get(item.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
// 如果没有父节点,或者是顶级节点 (parentId 为 0)
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function handleCreate() {
|
||||
currentEdit.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(category) {
|
||||
currentEdit.value = category;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleAddChild(parent) {
|
||||
currentEdit.value = { parentId: parent.id };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
await fetchCategories();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
function toggleExpand(category) {
|
||||
category.expanded = !category.expanded;
|
||||
}
|
||||
|
||||
function handleDelete(category) {
|
||||
if (Number(category.tid) === 0 || category.is_global === true) {
|
||||
ElMessage.warning("全局分类不可删除");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
deleteCategory(category.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
fetchCategories();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("删除分类失败:", error);
|
||||
ElMessage.error("删除失败");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// API调用
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await allCategories({ keyword: searchText.value });
|
||||
if (response.code === 200) {
|
||||
const categoryList = response.data || [];
|
||||
// 这里会递归生成三级、四级等
|
||||
categories.value = buildTree(categoryList);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-manager {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.page-title {
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏样式
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.el-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.search-stats {
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 分类列表容器
|
||||
.category-list {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.empty-state {
|
||||
padding: 80px 40px;
|
||||
.empty-icon {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归树结构核心样式
|
||||
.category-tree {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
// 层级背景区分 (可选:让子级背景稍微深一点点)
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px; // 左右 padding 保持,左侧缩进通过内联 style 控制
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
// 展开收起按钮占位
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分类具体内容
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮组
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4; // 默认低透明度,鼠标悬浮时高亮
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标悬停时显示按钮
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子分类容器样式
|
||||
.category-children {
|
||||
margin-left: 0;
|
||||
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus 对话框深度样式美化
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
.el-dialog__header {
|
||||
padding: 20px 24px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.category-manager {
|
||||
padding: 12px;
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
.page-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-list .category-tree .category-item .category-main {
|
||||
padding: 12px !important; // 移动端取消大缩进,改用其他视觉暗示
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.category-desc {
|
||||
display: none;
|
||||
} // 隐藏描述节省空间
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -56,6 +56,9 @@
|
||||
class="menu-icon"
|
||||
></i>
|
||||
<span>{{ scope.row.title }}</span>
|
||||
<el-tag v-if="scope.row.tenant_id === 0" size="small" type="warning">
|
||||
系统默认
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -110,7 +113,12 @@
|
||||
<span>子菜单</span>
|
||||
</el-button>
|
||||
|
||||
<el-button size="small" text @click="handleEditMenu(scope.row)">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="handleEditMenu(scope.row)"
|
||||
:disabled="scope.row.tenant_id === 0"
|
||||
>
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
@@ -122,6 +130,7 @@
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDeleteMenu(scope.row)"
|
||||
:disabled="scope.row.tenant_id === 0"
|
||||
>
|
||||
<el-icon>
|
||||
<Delete />
|
||||
@@ -168,6 +177,7 @@ import MenuEdit from "./components/edit.vue";
|
||||
// 定义菜单数据类型
|
||||
interface Menu {
|
||||
id: number;
|
||||
tenant_id?: number; // 0 表示系统默认菜单(不可编辑/删除)
|
||||
pid: number;
|
||||
title: string;
|
||||
type: number;
|
||||
|
||||
+321
-319
@@ -1,319 +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">
|
||||
<UmoEditor 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,支持 jpg、png、gif 格式,大小不超过 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";
|
||||
// @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>
|
||||
<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">
|
||||
<UmoEditor 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,支持 jpg、png、gif 格式,大小不超过 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";
|
||||
// @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: "",
|
||||
content: "",
|
||||
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.content = "";
|
||||
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>
|
||||
+295
-295
@@ -1,295 +1,295 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加产品
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</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 { useRouter } from 'vue-router'
|
||||
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;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整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 handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 跳转到产品分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/products/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
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>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加产品
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</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 { useRouter } from 'vue-router'
|
||||
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;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整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 handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 跳转到产品分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/products/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
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>
|
||||
+211
-211
@@ -1,211 +1,211 @@
|
||||
<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="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</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 {
|
||||
addProductsTypes,
|
||||
updateProductsTypes,
|
||||
getProductsTypesList
|
||||
} from '@/api/products'
|
||||
|
||||
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 typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getProductsTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateProductsTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addProductsTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</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 {
|
||||
addProductsTypes,
|
||||
updateProductsTypes,
|
||||
getProductsTypesList
|
||||
} from '@/api/products'
|
||||
|
||||
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 typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getProductsTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateProductsTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addProductsTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+239
-239
@@ -1,240 +1,240 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</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-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-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 {
|
||||
getProductsTypesList,
|
||||
deleteProductsTypes
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = 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 getProductsTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.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 = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
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 deleteProductsTypes(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 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>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</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-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-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 {
|
||||
getProductsTypesList,
|
||||
deleteProductsTypes
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = 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 getProductsTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.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 = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
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 deleteProductsTypes(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 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>
|
||||
|
||||
+315
-315
@@ -1,315 +1,315 @@
|
||||
<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="Logo" 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,支持 jpg、png、gif 格式,大小不超过 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 { addService, updateService } from "@/api/services";
|
||||
|
||||
// @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.link_logo = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.link_logo = "";
|
||||
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 updateService(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addService(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>
|
||||
<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="Logo" 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,支持 jpg、png、gif 格式,大小不超过 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 { addService, updateService } from "@/api/services";
|
||||
|
||||
// @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.link_logo = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.link_logo = "";
|
||||
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 updateService(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addService(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>
|
||||
+295
-281
@@ -1,281 +1,295 @@
|
||||
<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="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="thumb" 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="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 {
|
||||
getServiceList,
|
||||
updateService,
|
||||
deleteService
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const servicesList = 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 getServiceList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
servicesList.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 deleteService(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 updateService(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>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>特色服务管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<el-icon>
|
||||
<Menu />
|
||||
</el-icon>
|
||||
分类管理
|
||||
</el-button>
|
||||
<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="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="thumb" 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="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, Menu } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
getServiceList,
|
||||
updateService,
|
||||
deleteService
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const servicesList = 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 getServiceList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
servicesList.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 deleteService(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 updateService(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()
|
||||
}
|
||||
|
||||
// 跳转到解决方案分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/solutions/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
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>
|
||||
@@ -0,0 +1,210 @@
|
||||
<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="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</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 {
|
||||
addServicesTypes,
|
||||
updateServicesTypes,
|
||||
getServicesTypesList
|
||||
} from '@/api/services'
|
||||
|
||||
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 typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getServicesTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateServicesTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addServicesTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>解决方案分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</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-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-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 { Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getServicesTypesList,
|
||||
deleteServicesTypes
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = 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 getServicesTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.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 = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
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 deleteServicesTypes(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 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>
|
||||
Reference in New Issue
Block a user