|
|
@@ -1,578 +1,541 @@
|
|
|
<template>
|
|
|
- <div class="case-page">
|
|
|
- <div class="case-container">
|
|
|
- <!-- 标题区域(独立卡片,点击可编辑) -->
|
|
|
- <div class="section-header-card" @click="handleEditHeader">
|
|
|
- <div class="header-left">
|
|
|
- <span class="section-title">{{ headerConfig.title }}</span>
|
|
|
- <span class="section-subtitle">{{ headerConfig.subtitle }}</span>
|
|
|
- </div>
|
|
|
- <div class="header-right">
|
|
|
- <span class="more-link" @click.stop="handleLinkClick">{{ headerConfig.linkText }} ></span>
|
|
|
+ <div class="p-2">
|
|
|
+ <!-- 搜索区域 -->
|
|
|
+ <el-card shadow="never" class="mb-2">
|
|
|
+ <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
|
|
|
+ <el-form-item label="服务标题" prop="caseTitle">
|
|
|
+ <el-input v-model="queryParams.caseTitle" placeholder="请输入服务标题" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="服务类别" prop="projectTypeId">
|
|
|
+ <el-select v-model="queryParams.projectTypeId" placeholder="请选择" clearable style="width: 160px">
|
|
|
+ <el-option v-for="item in projectTypeOptions" :key="item.id" :label="item.projectTypeName" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleQuery">
|
|
|
+ <el-icon><Search /></el-icon>搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="resetQuery">
|
|
|
+ <el-icon><Refresh /></el-icon>重置
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 列表区域 -->
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>项目案例信息列表</span>
|
|
|
+ <el-button type="primary" @click="handleAdd">
|
|
|
+ <el-icon><Plus /></el-icon>新增
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 项目案例轮播展示区域 -->
|
|
|
- <div class="case-card">
|
|
|
- <el-carousel v-if="caseList.length > 0" :interval="5000" height="340px" :autoplay="true" arrow="always" indicator-position="outside">
|
|
|
- <el-carousel-item v-for="(group, gIndex) in caseGroups" :key="gIndex">
|
|
|
- <div class="case-row">
|
|
|
- <div v-for="item in group" :key="item.id" class="case-item">
|
|
|
- <el-icon class="close-icon" @click.stop="handleHideCase(item)"><CircleClose /></el-icon>
|
|
|
- <div class="case-image">
|
|
|
- <el-image :src="item.imageUrl" fit="cover" class="image">
|
|
|
- <template #error>
|
|
|
- <div class="image-slot"><el-icon><Picture /></el-icon></div>
|
|
|
- </template>
|
|
|
- </el-image>
|
|
|
- </div>
|
|
|
- <div class="case-info">
|
|
|
- <h4 class="case-name">{{ item.title }}</h4>
|
|
|
- <p class="case-desc">{{ item.description }}</p>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="tableData" border>
|
|
|
+ <el-table-column label="编号" prop="serviceCaseNo" align="center" width="100" />
|
|
|
+ <el-table-column label="封面图片" align="center" width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-image
|
|
|
+ :src="row.caseImage"
|
|
|
+ fit="cover"
|
|
|
+ style="width: 80px; height: 60px; border-radius: 4px"
|
|
|
+ :preview-src-list="[row.caseImage]"
|
|
|
+ preview-teleported
|
|
|
+ lazy
|
|
|
+ >
|
|
|
+ <template #error>
|
|
|
+ <div class="image-placeholder">
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-carousel-item>
|
|
|
- </el-carousel>
|
|
|
- <el-empty v-else description="暂无推荐案例,请点击搜索添加" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 项目案例搜索列表区域 -->
|
|
|
- <div class="list-card">
|
|
|
- <div class="list-title">项目案例搜索列表</div>
|
|
|
- <div class="search-area">
|
|
|
- <el-form :inline="true">
|
|
|
- <el-form-item>
|
|
|
- <el-input v-model="queryParams.keyword" placeholder="请输入项目案例编号/名称" clearable style="width: 280px" />
|
|
|
+ </template>
|
|
|
+ </el-image>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="标题" prop="caseTitle" align="center" min-width="200" show-overflow-tooltip />
|
|
|
+ <el-table-column label="分类" prop="projectTypeName" align="center" width="120" />
|
|
|
+ <el-table-column label="推荐" align="center" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="row.isRecommend === '1' ? 'status-active' : 'status-inactive'">
|
|
|
+ {{ row.isRecommend === '1' ? '推荐' : '推荐' }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="相关推荐" align="center" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="row.isRelatedRecommend === '1' ? 'status-active' : 'status-inactive'">
|
|
|
+ {{ row.isRelatedRecommend === '1' ? '推荐' : '推荐' }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="是否显示" align="center" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="row.isShow === '1' ? 'status-active' : 'status-inactive'">
|
|
|
+ {{ row.isShow === '1' ? '显示' : '显示' }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="发布时间" prop="createTime" align="center" width="120" />
|
|
|
+ <el-table-column label="相关" align="center" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ 阅读:{{ row.readCount || 0 }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" align="center" width="150" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link @click="handleView(row)">查看</el-button>
|
|
|
+ <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
|
|
+ <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <pagination
|
|
|
+ v-show="total > 0"
|
|
|
+ v-model:page="queryParams.pageNum"
|
|
|
+ v-model:limit="queryParams.pageSize"
|
|
|
+ :total="total"
|
|
|
+ @pagination="getList"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 新增/编辑/查看对话框 -->
|
|
|
+ <el-dialog v-model="dialog.visible" :title="dialog.title" width="800px" append-to-body destroy-on-close>
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" :disabled="dialog.isView">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="案例标题" prop="caseTitle">
|
|
|
+ <el-input v-model="form.caseTitle" placeholder="请输入案例标题" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <el-button type="primary" @click="handleOpenSelect">搜索</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="客户行业" prop="industryCategoryId">
|
|
|
+ <el-select v-model="form.industryCategoryId" placeholder="请选择客户行业" style="width: 100%">
|
|
|
+ <el-option v-for="item in industryOptions" :key="item.id" :label="item.industryCategoryName" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
</el-form-item>
|
|
|
- </el-form>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 编辑标题对话框 -->
|
|
|
- <el-dialog v-model="headerDialog.visible" title="编辑标题" width="600px" append-to-body>
|
|
|
- <el-form :model="headerForm" label-width="100px">
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="标题名称" required>
|
|
|
- <el-input v-model="headerForm.title" placeholder="请输入标题名称" />
|
|
|
+ <el-form-item label="项目类型" prop="projectTypeId">
|
|
|
+ <el-select v-model="form.projectTypeId" placeholder="请选择项目类型" style="width: 100%">
|
|
|
+ <el-option v-for="item in projectTypeOptions" :key="item.id" :label="item.projectTypeName" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="副标题" required>
|
|
|
- <el-input v-model="headerForm.subtitle" placeholder="请输入副标题" />
|
|
|
+ <el-form-item label="是否显示">
|
|
|
+ <el-switch v-model="form.isShow" active-value="1" inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="链接文字">
|
|
|
- <el-input v-model="headerForm.linkText" placeholder="请输入链接文字" />
|
|
|
+ <el-form-item label="是否推荐">
|
|
|
+ <el-switch v-model="form.isRecommend" active-value="1" inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="链接">
|
|
|
- <el-input v-model="headerForm.linkUrl" placeholder="请输入链接地址" />
|
|
|
+ <el-form-item label="相关推荐">
|
|
|
+ <el-switch v-model="form.isRelatedRecommend" active-value="1" inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
+ <el-form-item label="上传方案">
|
|
|
+ <file-upload v-model="form.planFile" :limit="1" :disabled="dialog.isView" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="案例图片">
|
|
|
+ <div class="case-images">
|
|
|
+ <div v-for="(img, index) in imageList" :key="index" class="image-item">
|
|
|
+ <el-image :src="img.url" fit="cover" class="image-preview" />
|
|
|
+ <div class="image-actions" v-if="!dialog.isView">
|
|
|
+ <el-button type="primary" link size="small" @click="setMainImage(index)">
|
|
|
+ {{ img.isMain ? '主图' : '主图' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button type="danger" link size="small" @click="removeImage(index)">删除</el-button>
|
|
|
+ </div>
|
|
|
+ <div v-if="img.isMain" class="main-tag">主图</div>
|
|
|
+ </div>
|
|
|
+ <el-upload
|
|
|
+ v-if="!dialog.isView && imageList.length < 5"
|
|
|
+ :action="uploadUrl"
|
|
|
+ :headers="uploadHeaders"
|
|
|
+ :show-file-list="false"
|
|
|
+ :on-success="handleImageSuccess"
|
|
|
+ :before-upload="handleBeforeImageUpload"
|
|
|
+ accept=".jpg,.jpeg,.png"
|
|
|
+ class="image-uploader"
|
|
|
+ >
|
|
|
+ <div class="upload-btn">
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ <span>上传图片</span>
|
|
|
+ </div>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="项目简介" prop="projectBrief">
|
|
|
+ <el-input v-model="form.projectBrief" type="textarea" :rows="3" placeholder="请输入项目简介" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="项目详情" prop="projectDetail">
|
|
|
+ <editor v-model="form.projectDetail" :min-height="200" :read-only="dialog.isView" />
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
- <el-button type="primary" @click="saveHeaderConfig">确 认</el-button>
|
|
|
- <el-button @click="headerDialog.visible = false">取 消</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <!-- 选择案例对话框 -->
|
|
|
- <el-dialog v-model="selectDialog.visible" title="选择项目案例" width="900px" append-to-body>
|
|
|
- <div class="search-bar">
|
|
|
- <el-input v-model="selectDialog.keyword" placeholder="请输入项目案例编号/名称" clearable style="width: 280px" />
|
|
|
- <el-button type="primary" @click="loadSelectList">搜索</el-button>
|
|
|
- </div>
|
|
|
- <el-table :data="selectList" border style="margin-top: 15px">
|
|
|
- <el-table-column label="案例编号" align="center" prop="serviceCaseNo" width="120" />
|
|
|
- <el-table-column label="案例图片" align="center" width="100">
|
|
|
- <template #default="scope">
|
|
|
- <el-image :src="scope.row.imageUrl" fit="cover" style="width: 60px; height: 60px; border-radius: 4px" lazy>
|
|
|
- <template #error><div class="image-placeholder-small"><el-icon><Picture /></el-icon></div></template>
|
|
|
- </el-image>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="案例名称" align="center" prop="title" :show-overflow-tooltip="true" min-width="200" />
|
|
|
- <el-table-column label="首页推荐" align="center" width="100">
|
|
|
- <template #default="scope">
|
|
|
- <span :class="scope.row.isLinked ? 'status-show' : 'status-hide'">
|
|
|
- {{ scope.row.isLinked ? '推荐' : '不推荐' }}
|
|
|
- </span>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="操作" align="center" width="120">
|
|
|
- <template #default="scope">
|
|
|
- <span v-if="scope.row.isLinked" class="action-link danger" @click="handleUnrecommend(scope.row)">不推荐</span>
|
|
|
- <span v-else class="action-link primary" @click="handleRecommend(scope.row)">推 荐</span>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- <pagination
|
|
|
- v-show="selectDialog.total > 0"
|
|
|
- v-model:page="selectDialog.pageNum"
|
|
|
- v-model:limit="selectDialog.pageSize"
|
|
|
- :total="selectDialog.total"
|
|
|
- @pagination="loadSelectList"
|
|
|
- />
|
|
|
- <template #footer>
|
|
|
- <el-button @click="selectDialog.visible = false">关 闭</el-button>
|
|
|
+ <el-button type="primary" v-if="!dialog.isView" @click="submitForm">确 认</el-button>
|
|
|
+ <el-button @click="dialog.visible = false">{{ dialog.isView ? '关 闭' : '取 消' }}</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
-
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="DecorationCase" lang="ts">
|
|
|
-import { ref, reactive, computed, onMounted } from 'vue';
|
|
|
-import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
-import { Picture, CircleClose } from '@element-plus/icons-vue';
|
|
|
-import { getFloorTitle, addFloorTitle, updateFloorTitle } from '@/api/system/floorTitle';
|
|
|
-import { listServiceCase } from '@/api/product/serviceCase';
|
|
|
-import { listRecommend, listRecommendLink, addRecommendLink, delRecommendLink } from '@/api/product/recommend';
|
|
|
-
|
|
|
-// 标题配置ID(项目案例用id=8)
|
|
|
-const TITLE_ID = 8;
|
|
|
-// 推荐位编号
|
|
|
-const RECOMMEND_NO = 'decoration_case';
|
|
|
-
|
|
|
-// 标题配置
|
|
|
-const headerConfig = ref({
|
|
|
- id: null as number | null,
|
|
|
- title: '项目案例',
|
|
|
- subtitle: '为近5000家终端企业提供电商化集采服务',
|
|
|
- linkText: '更多案例',
|
|
|
- linkUrl: ''
|
|
|
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
|
|
|
+import type { ComponentInternalInstance, Ref } from 'vue';
|
|
|
+import type { FormInstance } from 'element-plus';
|
|
|
+import { Search, Refresh, Plus, Picture } from '@element-plus/icons-vue';
|
|
|
+import { listServiceCase, getServiceCase, addServiceCase, updateServiceCase, delServiceCase } from '@/api/product/serviceCase';
|
|
|
+import { listIndustryCategory } from '@/api/customer/industryCategory';
|
|
|
+import { listProjectType } from '@/api/globalSetting/projectType';
|
|
|
+import { globalHeaders } from '@/utils/request';
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
+
|
|
|
+// 上传地址
|
|
|
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
|
|
|
+const uploadHeaders = globalHeaders();
|
|
|
+
|
|
|
+// 查询表单
|
|
|
+const queryFormRef = ref<FormInstance>();
|
|
|
+const queryParams = reactive({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ caseTitle: '',
|
|
|
+ projectTypeId: undefined as number | undefined
|
|
|
});
|
|
|
|
|
|
-const headerDialog = reactive({ visible: false });
|
|
|
-const headerForm = reactive({
|
|
|
- id: null as number | null,
|
|
|
+// 列表数据
|
|
|
+const loading = ref(false);
|
|
|
+const tableData = ref<any[]>([]);
|
|
|
+const total = ref(0);
|
|
|
+
|
|
|
+// 下拉选项
|
|
|
+const projectTypeOptions = ref<any[]>([]);
|
|
|
+const industryOptions = ref<any[]>([]);
|
|
|
+
|
|
|
+// 表单相关
|
|
|
+const formRef = ref<FormInstance>();
|
|
|
+const dialog = reactive({
|
|
|
+ visible: false,
|
|
|
title: '',
|
|
|
- subtitle: '',
|
|
|
- linkText: '',
|
|
|
- linkUrl: ''
|
|
|
+ isView: false
|
|
|
});
|
|
|
|
|
|
-// 加载标题配置
|
|
|
-const loadHeaderConfig = async () => {
|
|
|
- try {
|
|
|
- const res = await getFloorTitle(TITLE_ID);
|
|
|
- if (res.data) {
|
|
|
- const data = res.data;
|
|
|
- headerConfig.value = {
|
|
|
- id: data.id,
|
|
|
- title: data.title || '项目案例',
|
|
|
- subtitle: data.subtitle || '为近5000家终端企业提供电商化集采服务',
|
|
|
- linkText: data.linkWord || '更多案例',
|
|
|
- linkUrl: data.linkUrl || ''
|
|
|
- };
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('加载标题配置失败', error);
|
|
|
- }
|
|
|
+const initForm = {
|
|
|
+ id: undefined as number | undefined,
|
|
|
+ caseTitle: '',
|
|
|
+ industryCategoryId: undefined as number | undefined,
|
|
|
+ projectTypeId: undefined as number | undefined,
|
|
|
+ isShow: '1',
|
|
|
+ isRecommend: '0',
|
|
|
+ isRelatedRecommend: '0',
|
|
|
+ planFile: '',
|
|
|
+ caseImage: '',
|
|
|
+ projectBrief: '',
|
|
|
+ projectDetail: ''
|
|
|
};
|
|
|
+const form = ref({ ...initForm });
|
|
|
+const imageList = ref<{ url: string; ossId?: string; isMain: boolean }[]>([]);
|
|
|
|
|
|
-const handleEditHeader = () => {
|
|
|
- Object.assign(headerForm, headerConfig.value);
|
|
|
- headerDialog.visible = true;
|
|
|
+const rules = {
|
|
|
+ caseTitle: [{ required: true, message: '请输入案例标题', trigger: 'blur' }],
|
|
|
+ projectTypeId: [{ required: true, message: '请选择项目类型', trigger: 'change' }]
|
|
|
};
|
|
|
|
|
|
-const saveHeaderConfig = async () => {
|
|
|
+/** 获取列表 */
|
|
|
+const getList = async () => {
|
|
|
+ loading.value = true;
|
|
|
try {
|
|
|
- const data = {
|
|
|
- id: headerForm.id,
|
|
|
- title: headerForm.title,
|
|
|
- subtitle: headerForm.subtitle,
|
|
|
- linkWord: headerForm.linkText,
|
|
|
- linkUrl: headerForm.linkUrl
|
|
|
- };
|
|
|
- if (headerForm.id) {
|
|
|
- await updateFloorTitle(data);
|
|
|
- } else {
|
|
|
- await addFloorTitle(data);
|
|
|
- }
|
|
|
- await loadHeaderConfig();
|
|
|
- headerDialog.visible = false;
|
|
|
- ElMessage.success('标题配置已保存');
|
|
|
- } catch (error) {
|
|
|
- ElMessage.error('保存失败');
|
|
|
+ const res = await listServiceCase(queryParams);
|
|
|
+ tableData.value = res.rows || [];
|
|
|
+ total.value = res.total || 0;
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const handleLinkClick = () => {
|
|
|
- if (headerConfig.value.linkUrl) {
|
|
|
- window.open(headerConfig.value.linkUrl, '_blank');
|
|
|
+/** 加载下拉选项 */
|
|
|
+const loadOptions = async () => {
|
|
|
+ try {
|
|
|
+ const [typeRes, industryRes] = await Promise.all([
|
|
|
+ listProjectType({ pageSize: 100 }),
|
|
|
+ listIndustryCategory({ pageSize: 100 })
|
|
|
+ ]);
|
|
|
+ projectTypeOptions.value = typeRes.rows || [];
|
|
|
+ industryOptions.value = industryRes.rows || [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载选项失败', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 推荐位
|
|
|
-const recommendId = ref<number | null>(null);
|
|
|
-const linkedCaseIds = ref<Set<string>>(new Set());
|
|
|
+/** 搜索 */
|
|
|
+const handleQuery = () => {
|
|
|
+ queryParams.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
|
|
|
-// 项目案例展示列表
|
|
|
-const caseList = ref<any[]>([]);
|
|
|
+/** 重置 */
|
|
|
+const resetQuery = () => {
|
|
|
+ queryFormRef.value?.resetFields();
|
|
|
+ queryParams.caseTitle = '';
|
|
|
+ queryParams.projectTypeId = undefined;
|
|
|
+ handleQuery();
|
|
|
+};
|
|
|
|
|
|
-// 每4个一组,用于轮播
|
|
|
-const caseGroups = computed(() => {
|
|
|
- const groups: any[][] = [];
|
|
|
- for (let i = 0; i < caseList.value.length; i += 4) {
|
|
|
- groups.push(caseList.value.slice(i, i + 4));
|
|
|
- }
|
|
|
- return groups;
|
|
|
-});
|
|
|
+/** 新增 */
|
|
|
+const handleAdd = () => {
|
|
|
+ resetForm();
|
|
|
+ dialog.title = '新增项目案例';
|
|
|
+ dialog.isView = false;
|
|
|
+ dialog.visible = true;
|
|
|
+};
|
|
|
|
|
|
-// 获取推荐位ID
|
|
|
-const loadRecommendId = async () => {
|
|
|
- try {
|
|
|
- const res: any = await listRecommend({ recommendNo: RECOMMEND_NO, pageSize: 1 });
|
|
|
- if (res.rows && res.rows.length > 0) {
|
|
|
- recommendId.value = res.rows[0].id;
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('获取推荐位失败', error);
|
|
|
- }
|
|
|
+/** 查看 */
|
|
|
+const handleView = async (row: any) => {
|
|
|
+ resetForm();
|
|
|
+ dialog.title = '查看项目案例';
|
|
|
+ dialog.isView = true;
|
|
|
+ dialog.visible = true;
|
|
|
+ await loadDetail(row.id);
|
|
|
};
|
|
|
|
|
|
-// 加载已推荐的案例
|
|
|
-const loadCaseList = async () => {
|
|
|
+/** 编辑 */
|
|
|
+const handleEdit = async (row: any) => {
|
|
|
+ resetForm();
|
|
|
+ dialog.title = '编辑项目案例';
|
|
|
+ dialog.isView = false;
|
|
|
+ dialog.visible = true;
|
|
|
+ await loadDetail(row.id);
|
|
|
+};
|
|
|
+
|
|
|
+/** 加载详情 */
|
|
|
+const loadDetail = async (id: number) => {
|
|
|
try {
|
|
|
- if (!recommendId.value) await loadRecommendId();
|
|
|
- if (!recommendId.value) {
|
|
|
- caseList.value = [];
|
|
|
- linkedCaseIds.value = new Set();
|
|
|
- return;
|
|
|
- }
|
|
|
- const linkRes: any = await listRecommendLink({ recommendId: recommendId.value, pageSize: 100 });
|
|
|
- const links = linkRes.rows || [];
|
|
|
- linkedCaseIds.value = new Set(links.map((l: any) => String(l.serviceCaseId)));
|
|
|
- if (links.length === 0) {
|
|
|
- caseList.value = [];
|
|
|
- return;
|
|
|
+ const res = await getServiceCase(id);
|
|
|
+ const data = res.data || {};
|
|
|
+ form.value = {
|
|
|
+ id: data.id,
|
|
|
+ caseTitle: data.caseTitle || '',
|
|
|
+ industryCategoryId: data.industryCategoryId,
|
|
|
+ projectTypeId: data.projectTypeId,
|
|
|
+ isShow: data.isShow || '0',
|
|
|
+ isRecommend: data.isRecommend || '0',
|
|
|
+ isRelatedRecommend: data.isRelatedRecommend || '0',
|
|
|
+ planFile: data.planFile || '',
|
|
|
+ caseImage: data.caseImage || '',
|
|
|
+ projectBrief: data.projectBrief || '',
|
|
|
+ projectDetail: data.projectDetail || ''
|
|
|
+ };
|
|
|
+ // 解析图片列表
|
|
|
+ if (data.caseImage) {
|
|
|
+ const images = data.caseImage.split(',').filter((s: string) => s);
|
|
|
+ imageList.value = images.map((url: string, index: number) => ({
|
|
|
+ url,
|
|
|
+ isMain: index === 0
|
|
|
+ }));
|
|
|
}
|
|
|
- const caseIds = links.map((link: any) => link.serviceCaseId);
|
|
|
- const caseRes: any = await listServiceCase({ ids: caseIds.join(','), pageSize: 100 });
|
|
|
- const caseMap = new Map((caseRes.rows || []).map((c: any) => [String(c.id), c]));
|
|
|
- caseList.value = links.map((link: any) => {
|
|
|
- const serviceCase: any = caseMap.get(String(link.serviceCaseId)) || {};
|
|
|
- return {
|
|
|
- id: serviceCase.id || link.serviceCaseId,
|
|
|
- linkId: link.id,
|
|
|
- serviceCaseNo: serviceCase.serviceCaseNo || '',
|
|
|
- title: serviceCase.caseTitle || `案例${link.serviceCaseId}`,
|
|
|
- description: serviceCase.projectBrief || '',
|
|
|
- imageUrl: serviceCase.caseImage
|
|
|
- };
|
|
|
- });
|
|
|
} catch (error) {
|
|
|
- console.error('加载案例列表失败', error);
|
|
|
- caseList.value = [];
|
|
|
+ console.error('获取详情失败', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 查询参数
|
|
|
-const queryParams = reactive({ keyword: '' });
|
|
|
-
|
|
|
-// 选择案例对话框
|
|
|
-const selectDialog = reactive({
|
|
|
- visible: false,
|
|
|
- keyword: '',
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 10,
|
|
|
- total: 0
|
|
|
-});
|
|
|
-const selectList = ref<any[]>([]);
|
|
|
+/** 重置表单 */
|
|
|
+const resetForm = () => {
|
|
|
+ form.value = { ...initForm };
|
|
|
+ imageList.value = [];
|
|
|
+ formRef.value?.resetFields();
|
|
|
+};
|
|
|
|
|
|
-const handleOpenSelect = () => {
|
|
|
- selectDialog.keyword = queryParams.keyword;
|
|
|
- selectDialog.pageNum = 1;
|
|
|
- selectDialog.visible = true;
|
|
|
- loadSelectList();
|
|
|
+/** 删除 */
|
|
|
+const handleDelete = (row: any) => {
|
|
|
+ proxy?.$modal.confirm(`是否确认删除案例"${row.caseTitle}"?`).then(async () => {
|
|
|
+ await delServiceCase(row.id);
|
|
|
+ proxy?.$modal.msgSuccess('删除成功');
|
|
|
+ getList();
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-const loadSelectList = async () => {
|
|
|
- try {
|
|
|
- const params: any = { pageNum: selectDialog.pageNum, pageSize: selectDialog.pageSize };
|
|
|
- if (selectDialog.keyword) {
|
|
|
- // 用caseTitle做模糊查询
|
|
|
- params.caseTitle = selectDialog.keyword;
|
|
|
- }
|
|
|
- const res: any = await listServiceCase(params);
|
|
|
- selectList.value = (res.rows || []).map((item: any) => ({
|
|
|
- id: item.id,
|
|
|
- serviceCaseNo: item.serviceCaseNo,
|
|
|
- title: item.caseTitle,
|
|
|
- imageUrl: item.caseImage,
|
|
|
- isLinked: linkedCaseIds.value.has(String(item.id)),
|
|
|
- linkId: null
|
|
|
- }));
|
|
|
- // 查找linkId
|
|
|
- if (recommendId.value) {
|
|
|
- const linkRes: any = await listRecommendLink({ recommendId: recommendId.value, pageSize: 100 });
|
|
|
- const linkMap = new Map((linkRes.rows || []).map((l: any) => [String(l.serviceCaseId), l.id]));
|
|
|
- selectList.value.forEach((c: any) => {
|
|
|
- c.linkId = linkMap.get(String(c.id)) || null;
|
|
|
- });
|
|
|
- }
|
|
|
- selectDialog.total = res.total || 0;
|
|
|
- } catch (error) {
|
|
|
- console.error('加载案例失败', error);
|
|
|
- selectList.value = [];
|
|
|
+/** 图片上传前校验 */
|
|
|
+const handleBeforeImageUpload = (file: any) => {
|
|
|
+ const isImage = ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type);
|
|
|
+ const isLt5M = file.size / 1024 / 1024 < 5;
|
|
|
+ if (!isImage) {
|
|
|
+ proxy?.$modal.msgError('只能上传 JPG/PNG 格式图片!');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!isLt5M) {
|
|
|
+ proxy?.$modal.msgError('图片大小不能超过 5MB!');
|
|
|
+ return false;
|
|
|
}
|
|
|
+ proxy?.$modal.loading('正在上传...');
|
|
|
+ return true;
|
|
|
};
|
|
|
|
|
|
-const resetSelectQuery = () => {
|
|
|
- selectDialog.keyword = '';
|
|
|
- selectDialog.pageNum = 1;
|
|
|
- loadSelectList();
|
|
|
+/** 图片上传成功 */
|
|
|
+const handleImageSuccess = (res: any) => {
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
+ if (res.code === 200) {
|
|
|
+ imageList.value.push({
|
|
|
+ url: res.data.url,
|
|
|
+ ossId: res.data.ossId,
|
|
|
+ isMain: imageList.value.length === 0
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgError(res.msg || '上传失败');
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
-// 推荐案例
|
|
|
-const handleRecommend = async (row: any) => {
|
|
|
- try {
|
|
|
- if (!recommendId.value) {
|
|
|
- ElMessage.error('推荐位不存在');
|
|
|
- return;
|
|
|
- }
|
|
|
- await addRecommendLink({ recommendId: recommendId.value, serviceCaseId: row.id });
|
|
|
- row.isLinked = true;
|
|
|
- linkedCaseIds.value.add(String(row.id));
|
|
|
- await loadCaseList();
|
|
|
- ElMessage.success('已推荐');
|
|
|
- } catch (error) {
|
|
|
- ElMessage.error('操作失败');
|
|
|
- }
|
|
|
+/** 设置主图 */
|
|
|
+const setMainImage = (index: number) => {
|
|
|
+ imageList.value.forEach((img, i) => {
|
|
|
+ img.isMain = i === index;
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-// 取消推荐
|
|
|
-const handleUnrecommend = async (row: any) => {
|
|
|
- try {
|
|
|
- if (row.linkId) {
|
|
|
- await delRecommendLink(row.linkId);
|
|
|
- }
|
|
|
- row.isLinked = false;
|
|
|
- linkedCaseIds.value.delete(String(row.id));
|
|
|
- await loadCaseList();
|
|
|
- ElMessage.success('已取消推荐');
|
|
|
- } catch (error) {
|
|
|
- ElMessage.error('操作失败');
|
|
|
+/** 删除图片 */
|
|
|
+const removeImage = (index: number) => {
|
|
|
+ const wasMain = imageList.value[index].isMain;
|
|
|
+ imageList.value.splice(index, 1);
|
|
|
+ if (wasMain && imageList.value.length > 0) {
|
|
|
+ imageList.value[0].isMain = true;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 从展示区隐藏案例
|
|
|
-const handleHideCase = async (item: any) => {
|
|
|
+/** 提交表单 */
|
|
|
+const submitForm = async () => {
|
|
|
+ const valid = await formRef.value?.validate();
|
|
|
+ if (!valid) return;
|
|
|
+
|
|
|
+ // 组装图片URL,主图放第一个
|
|
|
+ const mainImg = imageList.value.find(img => img.isMain);
|
|
|
+ const otherImgs = imageList.value.filter(img => !img.isMain);
|
|
|
+ const allImgs = mainImg ? [mainImg, ...otherImgs] : otherImgs;
|
|
|
+ form.value.caseImage = allImgs.map(img => img.url).join(',');
|
|
|
+
|
|
|
try {
|
|
|
- if (item.linkId) {
|
|
|
- await delRecommendLink(item.linkId);
|
|
|
- linkedCaseIds.value.delete(String(item.id));
|
|
|
- await loadCaseList();
|
|
|
- ElMessage.success('已移除');
|
|
|
+ if (form.value.id) {
|
|
|
+ await updateServiceCase(form.value);
|
|
|
+ proxy?.$modal.msgSuccess('修改成功');
|
|
|
+ } else {
|
|
|
+ await addServiceCase(form.value);
|
|
|
+ proxy?.$modal.msgSuccess('添加成功');
|
|
|
}
|
|
|
+ dialog.visible = false;
|
|
|
+ getList();
|
|
|
} catch (error) {
|
|
|
- ElMessage.error('移除失败');
|
|
|
+ console.error('保存失败', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
- loadHeaderConfig();
|
|
|
- loadRecommendId().then(() => loadCaseList());
|
|
|
+ loadOptions();
|
|
|
+ getList();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-.case-page {
|
|
|
- min-height: 100vh;
|
|
|
- background: #f5f5f5;
|
|
|
- padding: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.case-container {
|
|
|
- max-width: 1200px;
|
|
|
- margin: 0 auto;
|
|
|
-}
|
|
|
-
|
|
|
-.section-header-card {
|
|
|
+.card-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- padding: 15px 20px;
|
|
|
- background: #fff;
|
|
|
- border-radius: 4px;
|
|
|
- margin-bottom: 12px;
|
|
|
- cursor: pointer;
|
|
|
-
|
|
|
- &:hover { background: #fafafa; }
|
|
|
-
|
|
|
- .header-left {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
- gap: 12px;
|
|
|
-
|
|
|
- .section-title { font-size: 16px; font-weight: 600; color: #333; }
|
|
|
- .section-subtitle { font-size: 12px; color: #999; }
|
|
|
- }
|
|
|
-
|
|
|
- .header-right {
|
|
|
- .more-link { font-size: 12px; color: #666; cursor: pointer; &:hover { color: #333; } }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.case-card {
|
|
|
- background: #fff;
|
|
|
- border-radius: 4px;
|
|
|
- padding: 20px;
|
|
|
- margin-bottom: 12px;
|
|
|
-
|
|
|
- :deep(.el-carousel) {
|
|
|
- .el-carousel__arrow {
|
|
|
- background-color: rgba(0, 0, 0, 0.3);
|
|
|
- width: 36px;
|
|
|
- height: 36px;
|
|
|
- font-size: 14px;
|
|
|
- &:hover { background-color: rgba(0, 0, 0, 0.5); }
|
|
|
- }
|
|
|
- .el-carousel__arrow--left { left: 10px; }
|
|
|
- .el-carousel__arrow--right { right: 10px; }
|
|
|
- .el-carousel__indicators--outside { margin-top: 10px; }
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
-.case-row {
|
|
|
+.image-placeholder {
|
|
|
+ width: 80px;
|
|
|
+ height: 60px;
|
|
|
display: flex;
|
|
|
- gap: 15px;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #f5f5f5;
|
|
|
+ color: #ccc;
|
|
|
+ font-size: 24px;
|
|
|
}
|
|
|
|
|
|
-.case-item {
|
|
|
- flex: 0 0 calc(25% - 12px);
|
|
|
- max-width: calc(25% - 12px);
|
|
|
- position: relative;
|
|
|
+.status-active {
|
|
|
+ color: #409eff;
|
|
|
cursor: pointer;
|
|
|
- border-radius: 4px;
|
|
|
- overflow: hidden;
|
|
|
- background: #f5f5f5;
|
|
|
+}
|
|
|
|
|
|
- .close-icon {
|
|
|
- position: absolute;
|
|
|
- top: 8px;
|
|
|
- right: 8px;
|
|
|
- font-size: 20px;
|
|
|
- color: #fff;
|
|
|
- cursor: pointer;
|
|
|
- z-index: 10;
|
|
|
- background: rgba(0, 0, 0, 0.5);
|
|
|
- border-radius: 50%;
|
|
|
- padding: 3px;
|
|
|
-
|
|
|
- &:hover { background: rgba(245, 108, 108, 0.8); }
|
|
|
- }
|
|
|
+.status-inactive {
|
|
|
+ color: #909399;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
|
|
|
- .case-image {
|
|
|
- width: 100%;
|
|
|
- height: 160px;
|
|
|
+.case-images {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
|
|
|
- .image { width: 100%; height: 100%; }
|
|
|
+ .image-item {
|
|
|
+ position: relative;
|
|
|
+ width: 100px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
|
|
|
- .image-slot {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- background: #e8e8e8;
|
|
|
- color: #909399;
|
|
|
- font-size: 40px;
|
|
|
+ .image-preview {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .case-info {
|
|
|
- padding: 12px;
|
|
|
- background: #f5f5f5;
|
|
|
-
|
|
|
- .case-name {
|
|
|
- margin: 0 0 8px 0;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
- color: #303133;
|
|
|
- line-height: 1.4;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
+ .image-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ padding: 5px 0;
|
|
|
+ background: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
- .case-desc {
|
|
|
- margin: 0;
|
|
|
+ .main-tag {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ background: #409eff;
|
|
|
+ color: #fff;
|
|
|
font-size: 12px;
|
|
|
- color: #606266;
|
|
|
- line-height: 1.6;
|
|
|
- height: 58px;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- display: -webkit-box;
|
|
|
- -webkit-line-clamp: 3;
|
|
|
- -webkit-box-orient: vertical;
|
|
|
+ padding: 2px 6px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .case-actions {
|
|
|
- padding: 5px 12px 10px;
|
|
|
- background: #f5f5f5;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.list-card {
|
|
|
- background: #fff;
|
|
|
- border-radius: 4px;
|
|
|
- padding: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.list-title {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
- margin-bottom: 20px;
|
|
|
- padding-bottom: 15px;
|
|
|
- border-bottom: 1px solid #ebeef5;
|
|
|
-}
|
|
|
-
|
|
|
-.search-area {
|
|
|
- margin-bottom: 15px;
|
|
|
-}
|
|
|
+ .image-uploader {
|
|
|
+ width: 100px;
|
|
|
+ height: 130px;
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
-.search-bar {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
+ &:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ }
|
|
|
|
|
|
-.status-show { color: #409eff; }
|
|
|
-.status-hide { color: #f56c6c; }
|
|
|
+ .upload-btn {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ color: #8c939d;
|
|
|
|
|
|
-.action-link {
|
|
|
- cursor: pointer;
|
|
|
-
|
|
|
- &.primary { color: #409eff; &:hover { color: #66b1ff; } }
|
|
|
- &.danger { color: #f56c6c; &:hover { color: #f78989; } }
|
|
|
-}
|
|
|
+ .el-icon {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
|
|
|
-.image-placeholder-small {
|
|
|
- width: 60px;
|
|
|
- height: 60px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- background: #f5f5f5;
|
|
|
- color: #ccc;
|
|
|
- font-size: 20px;
|
|
|
+ span {
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|