feat(seller): 评价管理列表增强(回复状态/有无图片筛选、评价分级 Tab、会员头像列)及多页面样式优化(商品列表、部门管理、OSS 素材、运费模板、页头页脚等)

This commit is contained in:
pikachu1995@126.com
2026-10-27 16:05:36 +08:00
parent 700c709afd
commit b025ab804e
22 changed files with 948 additions and 627 deletions

View File

@@ -3,7 +3,7 @@ var BASE = {
* @description api请求基础路径 * @description api请求基础路径
*/ */
API_DEV: { API_DEV: {
seller: "https://admin-api.pickmall.cn", seller: "http://127.0.0.1:8887",
}, },
API_PROD: { API_PROD: {
seller: "https://admin-api.pickmall.cn", seller: "https://admin-api.pickmall.cn",

View File

@@ -295,7 +295,7 @@ export const deleteDepartment = (ids, params) => {
}; };
// 编辑部门 // 编辑部门
export const editDepartment = (ids, params) => { export const editDepartment = (ids, params) => {
return putRequest(`/department/${ids} `, params); return putRequest(`/department/${ids}`, params);
}; };
// 加载部门子级数据 // 加载部门子级数据
export const loadDepartment = (id) => { export const loadDepartment = (id) => {

View File

@@ -164,8 +164,14 @@ util.bootstrapDynamicRoutesFromCache = function () {
} }
}; };
/** 站点名称:优先取接口下发并缓存的站点名,兜底用本地配置 */
util.getSiteName = function () {
return window.localStorage.getItem("sellersiteName") || config.title;
};
util.title = function (title) { util.title = function (title) {
title = title || `${config.title} 商家后台`; const siteName = util.getSiteName();
title = title ? `${title} - ${siteName}商家后台` : `${siteName} 商家后台`;
window.document.title = title; window.document.title = title;
}; };
@@ -508,9 +514,8 @@ util.initRouterNode = function (routers, data) {
} }
const meta = {}; const meta = {};
meta.title = menu.title // 站点后缀统一由 util.title 在路由跳转时动态拼接,避免初始化时定死
? menu.title + " - " + config.title + "商家后台" meta.title = menu.title ? menu.title : null;
: null;
meta.firstRouterName = item.firstRouterName; meta.firstRouterName = item.firstRouterName;
meta.keepAlive = menu.keepAlive ? true : false; meta.keepAlive = menu.keepAlive ? true : false;
menu.meta = meta; menu.meta = meta;

View File

@@ -1,11 +1,11 @@
import Main from "@/views/Main.vue"; import Main from "@/views/Main.vue";
import config from "@/config/index";
// 不作为Main组件的子页面展示的页面单独写,如下 // 不作为Main组件的子页面展示的页面单独写,如下
export const loginRouter = { export const loginRouter = {
path: "/login", path: "/login",
name: "login", name: "login",
meta: { meta: {
title: `登录 - ${config.title}商家后台` // 站点后缀由 util.title 在路由跳转时动态拼接(站点名取自接口下发缓存)
title: "登录"
}, },
component: () => import("@/views/login.vue") component: () => import("@/views/login.vue")
}; };

View File

@@ -113,6 +113,11 @@
z-index: 4000 !important; z-index: 4000 !important;
} }
/* 全局禁止 textarea 拖拽改变大小 */
.el-textarea__inner {
resize: none;
}
/* 统一空状态:表格 / 树 / 列表无数据时的展示(未自定义 #empty 插槽的页面均生效) */ /* 统一空状态:表格 / 树 / 列表无数据时的展示(未自定义 #empty 插槽的页面均生效) */
.el-table__empty-block { .el-table__empty-block {
min-height: 160px; min-height: 160px;

View File

@@ -107,7 +107,7 @@
<el-row justify="center" class="copyright"> <el-row justify="center" class="copyright">
Copyright © {{ year }} - Present Copyright © {{ year }} - Present
<a href="https://pickmall.cn/" target="_blank" style="margin: 0 5px">{{ <a href="https://pickmall.cn/" target="_blank" style="margin: 0 5px">{{
config.title siteName
}}</a> }}</a>
版权所有 版权所有
</el-row> </el-row>
@@ -124,7 +124,7 @@ import { md5 } from "@/utils/md5.js";
import * as RegExp from "@/utils/regular.js"; import * as RegExp from "@/utils/regular.js";
import * as apiLogin from "@/api/index.js"; import * as apiLogin from "@/api/index.js";
import { sendSms } from "@/api/common.js"; import { sendSms } from "@/api/common.js";
import config from "@/config/index"; import util from "@/libs/util.js";
import verify from "@/views/my-components/verify"; import verify from "@/views/my-components/verify";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
@@ -146,6 +146,8 @@ const form = reactive({
oncePasd: "", oncePasd: "",
}); });
const year = new Date().getFullYear(); const year = new Date().getFullYear();
// 站点名优先取接口下发缓存,兜底用本地配置
const siteName = util.getSiteName();
const step = ref(0); const step = ref(0);
const ruleInline = { const ruleInline = {
mobile: [ mobile: [

View File

@@ -37,7 +37,12 @@
<el-tabs v-model="activeTab" @tab-click="switchTabs"> <el-tabs v-model="activeTab" @tab-click="switchTabs">
<el-tab-pane label="预警商品" name="warnList"> <el-tab-pane label="预警商品" name="warnList">
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%"> <el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" min-width="400" show-overflow-tooltip /> <el-table-column prop="goodsId" label="商品编号" min-width="160" show-overflow-tooltip />
<el-table-column prop="goodsName" label="商品名称" min-width="220" show-overflow-tooltip />
<el-table-column prop="sn" label="SKU编码" min-width="140" show-overflow-tooltip />
<el-table-column label="SKU信息" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.simpleSpecs || "-" }}</template>
</el-table-column>
<el-table-column label="库存" min-width="120"> <el-table-column label="库存" min-width="120">
<template #default="{ row }">{{ row.quantity || 0 }}</template> <template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column> </el-table-column>
@@ -54,7 +59,12 @@
<el-tab-pane label="设置预警" name="warnSetting"> <el-tab-pane label="设置预警" name="warnSetting">
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%"> <el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" /> <el-table-column prop="goodsId" label="商品编号" min-width="160" show-overflow-tooltip />
<el-table-column prop="goodsName" label="商品名称" min-width="220" show-overflow-tooltip />
<el-table-column prop="sn" label="SKU编码" min-width="140" show-overflow-tooltip />
<el-table-column label="SKU信息" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.simpleSpecs || "-" }}</template>
</el-table-column>
<el-table-column label="库存" width="200"> <el-table-column label="库存" width="200">
<template #default="{ row }">{{ row.quantity || 0 }}</template> <template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column> </el-table-column>

View File

@@ -6,10 +6,9 @@
:model="searchForm" :model="searchForm"
inline inline
label-width="70px" label-width="70px"
class="search-form" class="search-form goods-search-form"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<div class="search-form-row">
<el-form-item label="商品名称" prop="goodsName"> <el-form-item label="商品名称" prop="goodsName">
<el-input <el-input
v-model="searchForm.goodsName" v-model="searchForm.goodsName"
@@ -31,15 +30,13 @@
clearable clearable
/> />
</el-form-item> </el-form-item>
<el-form-item label="商品ID" prop="id"> <el-form-item label="SPU编码" prop="id">
<el-input <el-input
v-model="searchForm.id" v-model="searchForm.id"
placeholder="请输入商品ID" placeholder="请输入SPU编码"
clearable clearable
/> />
</el-form-item> </el-form-item>
</div>
<div class="search-form-row">
<el-form-item label="店铺分类" prop="storeCategoryPath"> <el-form-item label="店铺分类" prop="storeCategoryPath">
<el-cascader <el-cascader
v-model="searchForm.storeCategoryPath" v-model="searchForm.storeCategoryPath"
@@ -73,7 +70,6 @@
<el-option label="预约商品" value="APPOINTMENT_GOODS" /> <el-option label="预约商品" value="APPOINTMENT_GOODS" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</div>
<el-form-item label="创建时间"> <el-form-item label="创建时间">
<el-date-picker <el-date-picker
v-model="createRange" v-model="createRange"
@@ -81,7 +77,6 @@
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始" start-placeholder="开始"
end-placeholder="结束" end-placeholder="结束"
style="width: 360px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="库存状态" prop="stockStatus"> <el-form-item label="库存状态" prop="stockStatus">
@@ -89,7 +84,6 @@
v-model="searchForm.stockStatus" v-model="searchForm.stockStatus"
placeholder="全部" placeholder="全部"
clearable clearable
style="width: 240px"
@change="onStockStatusChange" @change="onStockStatusChange"
> >
<el-option label="有货" value="IN_STOCK" /> <el-option label="有货" value="IN_STOCK" />
@@ -97,34 +91,18 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="searchForm.stockStatus === 'IN_STOCK'" label="库存区间"> <el-form-item v-if="searchForm.stockStatus === 'IN_STOCK'" label="库存区间">
<el-input <div class="range-inputs">
v-model="searchForm.stockMin" <el-input v-model="searchForm.stockMin" placeholder="≥" clearable />
placeholder="≥" <span class="range-sep">~</span>
clearable <el-input v-model="searchForm.stockMax" placeholder="≤" clearable />
style="width: 110px" </div>
/>
<span style="margin: 0 6px">~</span>
<el-input
v-model="searchForm.stockMax"
placeholder="≤"
clearable
style="width: 110px"
/>
</el-form-item> </el-form-item>
<el-form-item label="售价区间"> <el-form-item label="售价区间">
<el-input <div class="range-inputs">
v-model="searchForm.priceMin" <el-input v-model="searchForm.priceMin" placeholder="≥" clearable />
placeholder="≥" <span class="range-sep">~</span>
clearable <el-input v-model="searchForm.priceMax" placeholder="≤" clearable />
style="width: 110px" </div>
/>
<span style="margin: 0 6px">~</span>
<el-input
v-model="searchForm.priceMax"
placeholder="≤"
clearable
style="width: 110px"
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button> <el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
@@ -172,7 +150,7 @@
@selection-change="onSelectionChange" @selection-change="onSelectionChange"
> >
<el-table-column type="selection" width="100" align="center" /> <el-table-column type="selection" width="100" align="center" />
<el-table-column prop="id" label="商品ID" width="200" show-overflow-tooltip /> <el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
<el-table-column label="商品图片" width="100" align="center"> <el-table-column label="商品图片" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<img alt="商品图片" <img alt="商品图片"
@@ -219,7 +197,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" show-overflow-tooltip /> <el-table-column prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
<el-table-column label="操作" width="320" align="center" fixed="right"> <el-table-column label="操作" width="320" align="right" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<template v-if="row.marketEnable === 'DOWN'"> <template v-if="row.marketEnable === 'DOWN'">
<a class="link-text" @click="upper(row)">上架</a> <a class="link-text" @click="upper(row)">上架</a>
@@ -693,28 +671,48 @@ onMounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-form-row { .goods-search-form {
display: flex; display: grid !important;
flex-wrap: nowrap; grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: flex-start; column-gap: 16px;
width: 100%; align-items: start;
margin-bottom: 18px;
:deep(.el-form-item) { > :deep(.el-form-item) {
flex: 1; display: flex;
width: auto;
min-width: 0; min-width: 0;
margin-right: 12px; margin: 8px 0 !important;
margin-bottom: 0;
} }
:deep(.el-form-item:last-child) { :deep(.el-form-item__content) {
margin-right: 0; flex: 1;
min-width: 0;
} }
:deep(.el-input), :deep(.el-input),
:deep(.el-select), :deep(.el-select),
:deep(.el-cascader) { :deep(.el-cascader),
:deep(.el-date-editor) {
width: 100% !important;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
}
.range-inputs {
display: flex;
align-items: center;
width: 100%; width: 100%;
:deep(.el-input) {
flex: 1;
min-width: 0;
}
.range-sep {
margin: 0 6px;
flex-shrink: 0;
}
} }
} }

View File

@@ -15,10 +15,10 @@
/> />
</div> </div>
<div class="filter-row-field"> <div class="filter-row-field">
<span class="filter-row-label">商品ID</span> <span class="filter-row-label">SPU编码</span>
<el-input <el-input
v-model="filterForm.goodsId" v-model="filterForm.goodsId"
placeholder="请输入商品ID" placeholder="请输入SPU编码"
clearable clearable
class="filter-control" class="filter-control"
/> />

View File

@@ -20,23 +20,28 @@
</a> </a>
</li> </li>
</div> </div>
<a class="company-link" :href="config.company.href" target="_blank"> <a class="company-link" :href="companyUrl" target="_blank">
<img class="zhizhao" :src="zhizhaoImg" alt="" /> <img class="zhizhao" :src="zhizhaoImg" alt="" />
{{ config.company.name }} {{ companyName }}
</a> </a>
</div> </div>
<div class="copyright"> <div class="copyright">
<p>Copyright © {{ year }} {{ config.title }}</p> <p>Copyright © {{ year }} {{ siteName }}</p>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import config from "@/config/index"; import config from "@/config/index";
import util from "@/libs/util.js";
import zhizhaoImg from "@/assets/images/zhizhao.jpg"; import zhizhaoImg from "@/assets/images/zhizhao.jpg";
const year = new Date().getFullYear(); const year = new Date().getFullYear();
// 站点名/公司信息优先取接口下发缓存,兜底用本地配置
const siteName = util.getSiteName();
const companyName = localStorage.getItem("sellerCompanyName") || config.company.name;
const companyUrl = localStorage.getItem("sellerCompanyUrl") || config.company.href;
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -47,6 +47,8 @@ function getSite() {
localStorage.setItem("sellersiteName", data.siteName); localStorage.setItem("sellersiteName", data.siteName);
localStorage.setItem("sellerlogoImg", data.storeSideLogo); localStorage.setItem("sellerlogoImg", data.storeSideLogo);
localStorage.setItem("sellerIconImg", data.storeSideIcon); localStorage.setItem("sellerIconImg", data.storeSideIcon);
localStorage.setItem("sellerCompanyName", data.companyName || "");
localStorage.setItem("sellerCompanyUrl", data.companyUrl || "");
storeSideLogo.value = data.storeSideLogo; storeSideLogo.value = data.storeSideLogo;
window.document.title = data.siteName; window.document.title = data.siteName;
applyFavicon(data.storeSideIcon); applyFavicon(data.storeSideIcon);

View File

@@ -10,16 +10,27 @@
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<el-form-item label="会员名称" prop="memberName"> <el-form-item label="会员名称" prop="memberName">
<el-input v-model="searchForm.memberName" clearable placeholder="请输入会员名称" style="width: 240px" /> <el-input v-model="searchForm.memberName" clearable placeholder="请输入会员名称" style="width: 200px" />
</el-form-item> </el-form-item>
<el-form-item label="商品名称" prop="goodsName"> <el-form-item label="商品名称" prop="goodsName">
<el-input v-model="searchForm.goodsName" clearable placeholder="请输入商品名" style="width: 240px" /> <el-input v-model="searchForm.goodsName" clearable placeholder="请输入商品名" style="width: 200px" />
</el-form-item> </el-form-item>
<el-form-item label="评价" prop="grade"> <el-form-item label="回复状态" prop="replyStatus">
<el-select v-model="searchForm.grade" placeholder="请选择" clearable style="width: 240px"> <el-select v-model="searchForm.replyStatus" placeholder="请选择" clearable style="width: 140px">
<el-option label="好评" value="GOOD" /> <el-option label="已回复" :value="true" />
<el-option label="中评" value="MODERATE" /> <el-option label="未回复" :value="false" />
<el-option label="差评" value="WORSE" /> </el-select>
</el-form-item>
<el-form-item label="有无图片" prop="haveImage">
<el-select v-model="searchForm.haveImage" placeholder="请选择" clearable style="width: 140px">
<el-option label="有图" value="1" />
<el-option label="无图" value="0" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 140px">
<el-option label="展示" value="OPEN" />
<el-option label="隐藏" value="CLOSE" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="评论日期"> <el-form-item label="评论日期">
@@ -42,24 +53,54 @@
</el-card> </el-card>
<el-card> <el-card>
<div class="comment-tab">
<el-tabs v-model="currentGrade" @tab-click="onGradeTabClick">
<el-tab-pane v-for="item in gradeTabs" :key="item.title" :label="item.title" :name="item.value" />
</el-tabs>
</div>
<el-table v-loading="loading" border :data="data" ref="tableRef" class="mt_10" style="width: 100%"> <el-table v-loading="loading" border :data="data" ref="tableRef" class="mt_10" style="width: 100%">
<el-table-column prop="memberName" label="会员名称" min-width="150" show-overflow-tooltip /> <el-table-column label="会员" min-width="160">
<el-table-column prop="goodsName" label="商品名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="content" label="评价内容" min-width="300" show-overflow-tooltip />
<el-table-column label="评价" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.grade === 'GOOD'" type="success">好评</el-tag> <div class="member-cell">
<el-tag v-else-if="row.grade === 'MODERATE'" type="warning">中评</el-tag> <el-avatar :size="32" :src="row.memberProfile" />
<el-tag v-else type="danger">差评</el-tag> <span class="cell-name" :title="row.memberName">{{ row.memberName }}</span>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100"> <el-table-column label="商品" min-width="200">
<template #default="{ row }">
<div class="goods-cell">
<el-image
v-if="row.goodsImage"
:src="row.goodsImage"
:preview-src-list="[row.goodsImage]"
preview-teleported
fit="cover"
style="width: 40px; height: 40px; flex-shrink: 0; border-radius: 4px"
/>
<span class="cell-name" :title="row.goodsName">{{ row.goodsName }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="content" label="评价内容" min-width="240" show-overflow-tooltip />
<el-table-column label="评价" width="200">
<template #default="{ row }">
<div class="grade-cell">
<el-rate :model-value="avgScore(row)" disabled allow-half size="small" />
<el-tag v-if="row.grade === 'GOOD'" type="success" size="small">好评</el-tag>
<el-tag v-else-if="row.grade === 'MODERATE'" type="warning" size="small">中评</el-tag>
<el-tag v-else type="danger" size="small">差评</el-tag>
<el-tag v-if="row.top" type="primary" size="small" effect="plain">置顶</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.status === 'OPEN'" type="success">展示</el-tag> <el-tag v-if="row.status === 'OPEN'" type="success">展示</el-tag>
<el-tag v-else type="danger">隐藏</el-tag> <el-tag v-else type="danger">隐藏</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="回复状态" width="110"> <el-table-column label="回复状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.replyStatus" type="success">已回复</el-tag> <el-tag v-if="row.replyStatus" type="success">已回复</el-tag>
<el-tag v-else type="primary">未回复</el-tag> <el-tag v-else type="primary">未回复</el-tag>
@@ -68,7 +109,7 @@
<el-table-column prop="createTime" label="创建日期" width="170" /> <el-table-column prop="createTime" label="创建日期" width="170" />
<el-table-column label="操作" align="center" fixed="right" width="100"> <el-table-column label="操作" align="center" fixed="right" width="100">
<template #default="{ row }"> <template #default="{ row }">
<a class="link-text" @click="detail(row)">详细</a> <a class="link-text" @click="detail(row)">{{ row.replyStatus ? "查看" : "回复" }}</a>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -87,42 +128,116 @@
</div> </div>
</el-card> </el-card>
<el-dialog v-model="modalVisible" :title="modalTitle" width="500px" :close-on-click-modal="false"> <el-drawer
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate"> v-model="modalVisible"
<el-form-item label="评价内容"> title="评价详情"
<span v-if="!content">暂无评价</span> size="600px"
<el-input v-else v-model="content" type="textarea" maxlength="200" disabled :rows="4" style="width: 90%" /> append-to-body
</el-form-item> :close-on-click-modal="false"
<el-form-item v-if="detailInfo.haveImage == 1" label="评价图片" style="padding-top: 10px"> >
<upload-pic-thumb v-model="image" :disable="true" :remove="false" :isView="true" /> <div v-loading="detailLoading" class="comment-detail">
</el-form-item> <template v-if="detailInfo.id">
<el-form-item label="回复内容" prop="reply"> <div class="detail-header">
<el-avatar :size="44" :src="detailInfo.memberProfile" />
<div class="detail-header-info">
<div class="detail-member-line">
<span class="detail-member-name">{{ detailInfo.memberName }}</span>
<el-tag v-if="detailInfo.grade === 'GOOD'" type="success" size="small" effect="light">好评</el-tag>
<el-tag v-else-if="detailInfo.grade === 'MODERATE'" type="warning" size="small" effect="light">
中评
</el-tag>
<el-tag v-else type="danger" size="small" effect="light">差评</el-tag>
</div>
</div>
<span class="detail-time">{{ detailInfo.createTime }}</span>
</div>
<p class="detail-content" :class="{ 'is-empty': !content }">
{{ content || "买家未填写评价内容" }}
</p>
<div v-if="image.length" class="image-list">
<el-image
v-for="(img, index) in image"
:key="index"
:src="img"
:preview-src-list="image"
:initial-index="index"
preview-teleported
fit="cover"
class="detail-image"
/>
</div>
<div class="detail-meta">
<div class="meta-scores">
<span class="score-item">
描述相符
<el-rate :model-value="detailInfo.descriptionScore" disabled size="small" />
</span>
<span class="score-item">
服务态度
<el-rate :model-value="detailInfo.serviceScore" disabled size="small" />
</span>
<span class="score-item">
物流服务
<el-rate :model-value="detailInfo.deliveryScore" disabled size="small" />
</span>
</div>
<div class="meta-goods">
<el-image
v-if="detailInfo.goodsImage"
:src="detailInfo.goodsImage"
:preview-src-list="[detailInfo.goodsImage]"
preview-teleported
fit="cover"
class="meta-goods-image"
/>
<span class="meta-goods-name" :title="detailInfo.goodsName">{{ detailInfo.goodsName }}</span>
<span class="meta-order-no">订单号:{{ detailInfo.orderNo || "-" }}</span>
</div>
</div>
<div class="reply-section">
<div class="reply-title">商家回复</div>
<div v-if="replyStatus" class="reply-bubble">
<p class="reply-text">{{ form.reply || "-" }}</p>
<div v-if="form.replyImage.length" class="image-list">
<el-image
v-for="(img, index) in form.replyImage"
:key="index"
:src="img"
:preview-src-list="form.replyImage"
:initial-index="index"
preview-teleported
fit="cover"
class="detail-image"
/>
</div>
</div>
<el-form v-else ref="formRef" :model="form" :rules="formValidate" label-position="top">
<el-form-item prop="reply">
<el-input <el-input
v-if="!replyStatus"
v-model="form.reply" v-model="form.reply"
type="textarea" type="textarea"
maxlength="200" maxlength="200"
:rows="4" :rows="4"
clearable resize="none"
style="width: 90%" show-word-limit
placeholder="请输入回复内容,回复后将展示在商品评价中"
/> />
<el-input v-else v-model="form.reply" type="textarea" maxlength="200" disabled :rows="4" style="width: 90%" />
</el-form-item> </el-form-item>
<el-form-item <el-form-item label="回复图片" class="reply-upload-item">
v-if="detailInfo.haveReplyImage == 1 || !replyStatus" <upload-pic-thumb v-model="form.replyImage" :limit="5" />
label="回复图片"
prop="replyImage"
style="padding-top: 18px"
>
<upload-pic-thumb v-if="!replyStatus" v-model="form.replyImage" :limit="5" />
<upload-pic-thumb v-else v-model="form.replyImage" :disable="true" :remove="false" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</div>
</template>
</div>
<template #footer> <template #footer>
<el-button @click="modalVisible = false">取消</el-button> <el-button @click="modalVisible = false">取消</el-button>
<el-button v-if="!replyStatus" type="primary" :loading="submitLoading" @click="handleSubmit">回复</el-button> <el-button v-if="!replyStatus" type="primary" :loading="submitLoading" @click="handleSubmit">回复</el-button>
</template> </template>
</el-dialog> </el-drawer>
</div> </div>
</template> </template>
@@ -138,10 +253,11 @@ const detailInfo = ref({});
const image = ref([]); const image = ref([]);
const replyStatus = ref(false); const replyStatus = ref(false);
const modalVisible = ref(false); const modalVisible = ref(false);
const modalTitle = ref("");
const loading = ref(true); const loading = ref(true);
const detailLoading = ref(false);
const content = ref(""); const content = ref("");
const searchForm = ref({
const defaultSearchForm = () => ({
pageNumber: 1, pageNumber: 1,
pageSize: 10, pageSize: 10,
sort: "createTime", sort: "createTime",
@@ -149,8 +265,17 @@ const searchForm = ref({
startTime: "", startTime: "",
endTime: "", endTime: "",
}); });
const searchForm = ref(defaultSearchForm());
const selectDate = ref(null); const selectDate = ref(null);
const gradeTabs = [
{ title: "全部评价", value: "" },
{ title: "好评", value: "GOOD" },
{ title: "中评", value: "MODERATE" },
{ title: "差评", value: "WORSE" },
];
const currentGrade = ref("");
const form = reactive({ const form = reactive({
id: "",
replyImage: [], replyImage: [],
reply: "", reply: "",
}); });
@@ -187,15 +312,15 @@ function handleSearch() {
getDataList(); getDataList();
} }
function handleReset() { function handleReset() {
searchForm.value = { searchForm.value = defaultSearchForm();
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
startTime: "",
endTime: "",
};
selectDate.value = null; selectDate.value = null;
currentGrade.value = "";
getDataList();
}
function onGradeTabClick(tab) {
currentGrade.value = tab.paneName;
searchForm.value.grade = tab.paneName || undefined;
searchForm.value.pageNumber = 1;
getDataList(); getDataList();
} }
function selectDateRange(v) { function selectDateRange(v) {
@@ -207,35 +332,52 @@ function selectDateRange(v) {
searchForm.value.endTime = ""; searchForm.value.endTime = "";
} }
} }
function avgScore(row) {
const scores = [row.deliveryScore, row.serviceScore, row.descriptionScore].filter(
(s) => typeof s === "number"
);
if (!scores.length) return 0;
return scores.reduce((a, b) => a + b, 0) / scores.length;
}
function getDataList() { function getDataList() {
loading.value = true; loading.value = true;
API_Member.getMemberReview(searchForm.value).then((res) => { API_Member.getMemberReview(searchForm.value)
loading.value = false; .then((res) => {
if (res.success) { if (res.success) {
data.value = res.result.records; data.value = res.result.records;
total.value = res.result.total; total.value = res.result.total;
} }
})
.finally(() => {
loading.value = false;
}); });
} }
function handleSubmit() { function handleSubmit() {
formRef.value.validate((valid) => { formRef.value.validate((valid) => {
if (valid) { if (!valid) return;
API_Member.replyMemberReview(form.id, form).then((res) => { submitLoading.value = true;
submitLoading.value = false; API_Member.replyMemberReview(form.id, form)
.then((res) => {
if (res.success) { if (res.success) {
Message.success("回复成功"); Message.success("回复成功");
getDataList(); getDataList();
modalVisible.value = false; modalVisible.value = false;
} }
})
.finally(() => {
submitLoading.value = false;
}); });
}
}); });
} }
function detail(v) { function detail(v) {
modalVisible.value = true;
detailInfo.value = {};
form.reply = "";
form.replyImage = []; form.replyImage = [];
loading.value = true; image.value = [];
API_Member.getMemberInfoReview(v.id).then((res) => { detailLoading.value = true;
loading.value = false; API_Member.getMemberInfoReview(v.id)
.then((res) => {
if (res.success) { if (res.success) {
form.id = res.result.id; form.id = res.result.id;
content.value = res.result.content; content.value = res.result.content;
@@ -244,18 +386,179 @@ function detail(v) {
image.value = res.result.images ? (res.result.images || "").split(",") : []; image.value = res.result.images ? (res.result.images || "").split(",") : [];
form.replyImage = res.result.replyImage ? (res.result.replyImage || "").split(",") : []; form.replyImage = res.result.replyImage ? (res.result.replyImage || "").split(",") : [];
detailInfo.value = res.result; detailInfo.value = res.result;
modalVisible.value = true;
modalTitle.value = "详细";
} }
})
.finally(() => {
detailLoading.value = false;
}); });
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
@import "@/styles/table-common.scss"; @import "@/styles/table-common.scss";
.link-text { .link-text {
color: #409eff; color: #409eff;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
} }
.comment-tab {
:deep(.el-tabs__item) {
font-size: 14px;
}
:deep(.el-tabs__header) {
margin-bottom: 0;
}
}
.member-cell,
.goods-cell {
display: flex;
align-items: center;
gap: 8px;
.cell-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.grade-cell {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.comment-detail {
min-height: 200px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
.detail-header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.detail-member-line {
display: flex;
align-items: center;
gap: 8px;
.detail-member-name {
font-size: 15px;
font-weight: 600;
}
}
}
.detail-time {
font-size: 12px;
color: #999;
flex-shrink: 0;
}
}
.detail-content {
margin: 14px 0 0;
font-size: 14px;
line-height: 1.7;
color: #333;
word-break: break-all;
white-space: pre-wrap;
&.is-empty {
color: #999;
}
}
.detail-meta {
margin-top: 14px;
padding: 10px 12px;
background: #f7f8fa;
border-radius: 6px;
.meta-scores {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
.score-item {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #888;
}
}
.meta-goods {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e4e7ed;
.meta-goods-image {
width: 32px;
height: 32px;
flex-shrink: 0;
border-radius: 4px;
}
.meta-goods-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: #606266;
}
.meta-order-no {
flex-shrink: 0;
font-size: 12px;
color: #999;
}
}
}
.reply-section {
margin-top: 16px;
.reply-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
&::before {
content: "";
width: 3px;
height: 14px;
border-radius: 2px;
background: #409eff;
}
}
.reply-bubble {
padding: 12px;
background: #ecf5ff;
border-radius: 6px;
.reply-text {
margin: 0;
font-size: 13px;
line-height: 1.7;
color: #333;
word-break: break-all;
white-space: pre-wrap;
}
}
.reply-upload-item {
margin-bottom: 0;
:deep(.el-form-item__label) {
font-size: 12px;
color: #999;
line-height: 1.4;
padding-bottom: 4px;
}
}
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
.detail-image {
width: 92px;
height: 92px;
border-radius: 6px;
}
}
</style> </style>

View File

@@ -103,7 +103,7 @@
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a> <a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
</div> </div>
<div style="color: #999; font-size: 12px; margin-top: 5px"> <div style="color: #999; font-size: 12px; margin-top: 5px">
商品ID: {{ row.goodsId }} SPU编码: {{ row.goodsId }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -96,7 +96,7 @@
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a> <a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
</div> </div>
<div style="color: #999; font-size: 12px; margin-top: 5px"> <div style="color: #999; font-size: 12px; margin-top: 5px">
商品ID: {{ row.goodsId }} SPU编码: {{ row.goodsId }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,6 +19,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center" /> <el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column prop="updateTime" label="修改时间" width="180" align="center" />
<el-table-column <el-table-column
v-if="searchForm.pageType !== 'SPECIAL'" v-if="searchForm.pageType !== 'SPECIAL'"
label="状态" label="状态"

View File

@@ -1,35 +1,30 @@
<template> <template>
<div class="search"> <div class="search">
<el-card> <el-card>
<el-row> <div>
<el-button @click="refresh">刷新</el-button> <el-button @click="refresh">刷新</el-button>
<el-button @click="add" type="primary">添加</el-button> <el-button @click="add" type="primary">添加</el-button>
</el-row> </div>
<el-tabs @tab-click="handleClickType" v-model="currentTab" style="margin-top: 10px"> <el-tabs @tab-click="handleClickType" v-model="currentTab" style="margin-top: 10px">
<el-tab-pane label="运费模板" name="INFO"> <el-tab-pane label="运费模板" name="INFO">
<el-empty v-if="!shipInfo.length" description="暂无运费模板,点击右上角「添加」创建" />
<table class="ncsc-default-table order m-b-30" :key="index" v-for="(item,index) in shipInfo"> <table class="ncsc-default-table order m-b-30" :key="index" v-for="(item,index) in shipInfo">
<tbody> <tbody>
<tr> <tr>
<td class="sep-row" colspan="20"></td> <td class="sep-row" colspan="20"></td>
</tr> </tr>
<tr> <tr>
<th colspan="20"> <th colspan="20" class="template-head">
<div class="template-head-inner">
<span class="temp-name">{{item.name}}</span> <span class="temp-name">{{item.name}}</span>
<el-tag v-if="item.pricingMethod==='FREE'" class="baoyou" color="warning">包邮</el-tag> <el-tag v-if="item.pricingMethod==='FREE'" type="warning" size="small">包邮</el-tag>
<span class="fr m-r-5"> <div class="template-head-right">
<time style="margin-right: 20px" title="最后编辑时间"> <time class="update-time" title="最后编辑时间">{{item.updateTime}}</time>
<i class="icon-time"></i>{{item.updateTime}} <a class="action-link" @click="edit(item)">修改</a>
</time> <span class="action-split">|</span>
<a <a class="action-link" @click="remove(item.id)">删除</a>
style="color:#2d8cf0;cursor:pointer;text-decoration:none" </div>
@click="edit(item)" </div>
>修改</a>
<span style="margin: 0 8px; color: #dcdee2">|</span>
<a
style="color:#2d8cf0;cursor:pointer;text-decoration:none"
@click="remove(item.id)"
>删除</a>
</span>
</th> </th>
</tr> </tr>
<tr v-if="item.pricingMethod!=='FREE'"> <tr v-if="item.pricingMethod!=='FREE'">
@@ -499,10 +494,10 @@ onMounted(() => {
} }
tbody td { tbody td {
color: #999; color: #606266;
background-color: #fff; background-color: #fff;
text-align: center; text-align: center;
padding: 6px 0; padding: 8px 0;
} }
} }
@@ -536,30 +531,41 @@ onMounted(() => {
border-left: 1px solid #e6e6e6; border-left: 1px solid #e6e6e6;
} }
.order tbody tr th .temp-name { .order tbody tr th.template-head {
float: left; padding: 0;
}
.template-head-inner {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
}
.temp-name {
font-size: 14px; font-size: 14px;
color: #555; font-weight: 600;
// line-height: 44px; color: #303133;
margin: 7px 0 0 10px;
} }
.m-r-5 { .template-head-right {
margin-right: 5px !important; margin-left: auto;
display: flex;
align-items: center;
font-weight: normal;
font-size: 13px;
} }
.fr { .update-time {
float: right !important; margin-right: 16px;
color: #909399;
font-size: 12px;
} }
.m-b-30 { .m-b-30 {
margin-bottom: 10px !important; margin-bottom: 10px !important;
} }
Button {
margin: 3px 5px 0px 5px;
}
thead { thead {
display: table-header-group; display: table-header-group;
vertical-align: middle; vertical-align: middle;
@@ -704,14 +710,8 @@ em {
font-size: 15px; font-size: 15px;
color: #f00; color: #f00;
} }
.baoyou {
margin: 6px 10px 0; .action-link:hover {
} text-decoration: underline;
/* 操作链接与分隔符不加粗 */
.order tbody tr th .fr a {
font-weight: normal;
}
.order tbody tr th .fr span {
font-weight: normal;
} }
</style> </style>

View File

@@ -12,6 +12,7 @@
<el-table-column prop="addressName" label="自提点名称" min-width="120" /> <el-table-column prop="addressName" label="自提点名称" min-width="120" />
<el-table-column prop="address" label="详细地址" min-width="280" /> <el-table-column prop="address" label="详细地址" min-width="280" />
<el-table-column prop="createTime" label="创建时间" min-width="120" sortable /> <el-table-column prop="createTime" label="创建时间" min-width="120" sortable />
<el-table-column prop="updateTime" label="修改时间" min-width="120" sortable />
<el-table-column label="操作" width="200" align="center"> <el-table-column label="操作" width="200" align="center">
<template #default="{ row }"> <template #default="{ row }">
<a class="link-text" @click="edit(row)">修改</a> <a class="link-text" @click="edit(row)">修改</a>

View File

@@ -1,56 +1,71 @@
<template> <template>
<div class="search"> <div class="search">
<el-card> <el-card>
<el-row class="operation"> <div class="operation">
<el-switch <el-button type="primary" @click="openAdd('0')">添加部门</el-button>
v-model="strict"
active-text="级联"
inactive-text="单选"
style="margin-right: 5px"
/>
<el-button @click="addRoot">添加部门</el-button>
<el-button type="primary" @click="add">添加子部门</el-button>
<el-button @click="delAll">批量删除</el-button> <el-button @click="delAll">批量删除</el-button>
<el-button @click="getParentList">刷新</el-button> <el-button @click="getList">刷新</el-button>
</el-row>
<el-row type="flex" justify="start">
<el-col :md="8" :lg="8" :xl="6">
<el-alert type="info" show-icon :closable="false">
当前选择编辑:
<span class="select-title">{{ editTitle }}</span>
<a v-if="form.id" class="select-clear" @click="cancelSelect">取消选择</a>
</el-alert>
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
<el-tree
ref="treeRef"
:data="data"
:props="treeProps"
node-key="id"
show-checkbox
highlight-current
default-expand-all
:check-strictly="!strict"
:current-node-key="currentNodeKey"
@node-click="onNodeClick"
@check="onCheckChange"
/>
</div> </div>
</el-col> <el-table
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px"> v-loading="loading"
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate"> :data="data"
<el-form-item label="部门名称" prop="title"> row-key="id"
<el-input v-model="form.title" /> border
default-expand-all
@selection-change="onSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="title" label="部门名称" min-width="180" />
<el-table-column label="上级部门" min-width="140">
<template #default="{ row }">{{ parentTitle(row) }}</template>
</el-table-column>
<el-table-column prop="sortOrder" label="排序值" width="90" align="center" />
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
<el-table-column prop="updateTime" label="修改时间" width="170" align="center" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="openAdd(row.id)">添加子级</a>
<span class="op-split">|</span>
<a class="link-text" @click="openEdit(row)">编辑</a>
<span class="op-split">|</span>
<a class="link-text" @click="delOne(row)">删除</a>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog
v-model="modalVisible"
:title="modalTitle"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="formRef" :model="form" label-width="85px" :rules="formValidate">
<el-form-item label="上级部门">
<el-tree-select
v-model="form.parentId"
:data="parentOptions"
:props="{ label: 'title', children: 'children' }"
node-key="id"
check-strictly
default-expand-all
style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item label="选择角色"> <el-form-item label="部门名称" prop="title">
<el-input v-model="form.title" placeholder="请输入部门名称" />
</el-form-item>
<el-form-item v-if="modalType === 1" label="选择角色">
<el-select <el-select
v-model="selectedRole" v-model="selectedRole"
multiple multiple
:loading="userLoading" :loading="roleLoading"
placeholder="请选择角色" placeholder="请选择角色"
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
v-for="item in users" v-for="item in roles"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
@@ -62,51 +77,17 @@
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" /> <el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
</el-tooltip> </el-tooltip>
</el-form-item> </el-form-item>
<el-form-item>
<el-button
type="primary"
style="margin-right: 5px"
:loading="submitLoading"
@click="submitEdit"
>
修改并保存
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</el-card>
<el-dialog
v-model="modalVisible"
:title="modalTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formAddRef" :model="formAdd" label-width="85px" :rules="formValidate">
<div v-if="showParent">
<el-form-item label="上级部门:">{{ form.title }}</el-form-item>
</div>
<el-form-item label="部门名称" prop="title">
<el-input v-model="formAdd.title" />
</el-form-item>
<el-form-item label="排序值" prop="sortOrder">
<el-tooltip placement="right" content="值越小越靠前,支持小数">
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
</el-tooltip>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="cancelAdd">取消</el-button> <el-button @click="modalVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button> <el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { import {
initDepartment, initDepartment,
addDepartment, addDepartment,
@@ -120,36 +101,24 @@ import { Message, Modal } from "@/utils/message";
defineOptions({ name: "store-department-manage" }); defineOptions({ name: "store-department-manage" });
const treeRef = ref(null);
const formRef = ref(null); const formRef = ref(null);
const formAddRef = ref(null);
const loading = ref(true); const loading = ref(true);
const maxHeight = ref("500px");
const strict = ref(true);
const userLoading = ref(false);
const modalVisible = ref(false); const modalVisible = ref(false);
const selectList = ref([]); const modalType = ref(0); // 0 添加 1 编辑
const selectCount = ref(0);
const showParent = ref(false);
const modalTitle = ref(""); const modalTitle = ref("");
const editTitle = ref(""); const submitLoading = ref(false);
const roleLoading = ref(false);
const data = ref([]);
const roles = ref([]);
const selectedRole = ref([]); const selectedRole = ref([]);
const currentNodeKey = ref(null); const selectedRows = ref([]);
const treeProps = {
label: "title",
children: "children",
isLeaf: "isLeaf",
};
const form = ref({ const form = ref({
id: "", id: "",
parentId: "0",
title: "", title: "",
parentId: "",
parentTitle: "",
sortOrder: 0, sortOrder: 0,
status: 0, status: 0,
}); });
const formAdd = ref({});
const formValidate = { const formValidate = {
title: [{ required: true, message: "名称不能为空", trigger: "blur" }], title: [{ required: true, message: "名称不能为空", trigger: "blur" }],
sortOrder: [ sortOrder: [
@@ -161,23 +130,57 @@ const formValidate = {
}, },
], ],
}; };
const submitLoading = ref(false);
const data = ref([]);
const users = ref([]);
function init() { // id -> title 映射,用于展示上级部门名称
getParentList(); const titleMap = computed(() => {
const map = {};
const walk = (list) => {
list.forEach((item) => {
map[item.id] = item.title;
if (item.children?.length) walk(item.children);
});
};
walk(data.value);
return map;
});
// 上级部门下拉树:根节点为“一级部门”;编辑时剔除自身及子级,防止循环挂级
const parentOptions = computed(() => {
const tree =
modalType.value === 1 ? pruneTree(data.value, form.value.id) : data.value;
return [{ id: "0", title: "一级部门", children: tree }];
});
function pruneTree(list, excludeId) {
return list
.filter((item) => item.id !== excludeId)
.map((item) => ({
...item,
children: item.children?.length
? pruneTree(item.children, excludeId)
: undefined,
}));
} }
function getParentList() {
function parentTitle(row) {
if (row.parentId === "0" || row.parentId == null) return "-";
return titleMap.value[row.parentId] || "-";
}
function getList() {
loading.value = true; loading.value = true;
initDepartment().then((res) => { initDepartment()
loading.value = false; .then((res) => {
if (res.success) { if (res.success) {
const list = Array.isArray(res.result) ? res.result : []; const list = Array.isArray(res.result) ? res.result : [];
data.value = bubbleSort(normalizeDepartmentTree(list)); data.value = sortTree(normalizeDepartmentTree(list));
} }
})
.finally(() => {
loading.value = false;
}); });
} }
function normalizeDepartmentTree(list) { function normalizeDepartmentTree(list) {
return list.map((item) => { return list.map((item) => {
const node = { ...item }; const node = { ...item };
@@ -189,90 +192,71 @@ function normalizeDepartmentTree(list) {
return node; return node;
}); });
} }
function onNodeClick(data) {
if (!data) {
cancelSelect();
return;
}
const row = JSON.parse(JSON.stringify(data));
for (const attr in row) {
if (row[attr] == null) {
row[attr] = "";
}
}
editTitle.value = row.title;
currentNodeKey.value = row.id;
userLoading.value = true;
getUserByDepartmentId(row.id).then((res) => { function sortTree(list) {
const way = []; list.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
res.result && list.forEach((item) => {
res.result.forEach((item) => { if (item.children?.length) sortTree(item.children);
way.push(item.roleId);
});
selectedRole.value = way;
}); });
return list;
}
getRoleList({ function onSelectionChange(rows) {
pageNumber: 1, selectedRows.value = rows;
pageSize: 10000,
}).then((res) => {
userLoading.value = false;
if (res.success) {
users.value = res.result.records;
form.value = row;
}
});
} }
function onCheckChange(_data, { checkedNodes }) {
selectList.value = checkedNodes; function openAdd(parentId) {
selectCount.value = checkedNodes.length; modalType.value = 0;
modalTitle.value = parentId === "0" ? "添加部门" : "添加子部门";
form.value = { id: "", parentId, title: "", sortOrder: 0, status: 0 };
modalVisible.value = true;
} }
function bubbleSort(array) {
if (!Array.isArray(array)) return []; function openEdit(row) {
const len = array.length; modalType.value = 1;
if (len < 2) return array; modalTitle.value = "编辑部门";
for (let i = 0; i < len; i++) { form.value = {
for (let j = 0; j < i; j++) { id: row.id,
if (array[j].sortOrder > array[i].sortOrder) { parentId: row.parentId ?? "0",
const temp = array[j]; title: row.title,
array[j] = array[i]; sortOrder: row.sortOrder ?? 0,
array[i] = temp; status: 0,
} };
}
}
return array;
}
function cancelSelect() {
treeRef.value?.setCurrentKey(null);
currentNodeKey.value = null;
formRef.value.resetFields();
delete form.value.id;
editTitle.value = "";
selectedRole.value = []; selectedRole.value = [];
} roleLoading.value = true;
function cancelAdd() { modalVisible.value = true;
modalVisible.value = false; Promise.all([
} getUserByDepartmentId(row.id),
function handleReset() { getRoleList({ pageNumber: 1, pageSize: 10000 }),
formRef.value.resetFields(); ]).then(([bindRes, roleRes]) => {
form.value.status = 0; roleLoading.value = false;
} if (bindRes.result) {
function submitEdit() { selectedRole.value = bindRes.result.map((item) => item.roleId);
formRef.value.validate((valid) => { }
if (valid) { if (roleRes.success) {
if (!form.value.id) { roles.value = roleRes.result.records;
Message.warning("请先点击选择要修改的部门");
return;
} }
const roleWay = [];
selectedRole.value.forEach((item) => {
roleWay.push({
departmentId: form.value.id,
roleId: item,
});
}); });
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
if (modalType.value === 0) {
addDepartment(form.value).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("添加成功");
modalVisible.value = false;
getList();
}
});
} else {
const roleWay = selectedRole.value.map((roleId) => ({
departmentId: form.value.id,
roleId,
}));
Promise.all([ Promise.all([
editDepartment(form.value.id, form.value), editDepartment(form.value.id, form.value),
updateDepartmentRole(form.value.id, roleWay), updateDepartmentRole(form.value.id, roleWay),
@@ -280,76 +264,48 @@ function submitEdit() {
submitLoading.value = false; submitLoading.value = false;
if (res[0].success) { if (res[0].success) {
Message.success("编辑成功"); Message.success("编辑成功");
init();
modalVisible.value = false; modalVisible.value = false;
getList();
} }
}); });
} }
}); });
} }
function submitAdd() {
formAddRef.value.validate((valid) => { function delOne(row) {
if (valid) { Modal.confirm({
submitLoading.value = true; title: "确认删除",
addDepartment(formAdd.value).then((res) => { content: "删除该部门将同时删除其下级所有部门,确认删除?",
submitLoading.value = false; loading: true,
onOk: () => {
deleteDepartment(row.id).then((res) => {
Modal.remove();
if (res.success) { if (res.success) {
Message.success("添加成功"); Message.success("删除成功");
init(); getList();
modalVisible.value = false;
} }
}); });
} },
}); });
} }
function add() {
if (form.value.id == "" || form.value.id == null) {
Message.warning("请先点击选择一个部门");
return;
}
modalTitle.value = "添加子部门";
showParent.value = true;
formAdd.value = {
parentId: form.value.id,
sortOrder: 0,
status: 0,
};
modalVisible.value = true;
}
function addRoot() {
modalTitle.value = "添加一级部门";
showParent.value = false;
formAdd.value = {
parentId: 0,
sortOrder: 0,
status: 0,
};
modalVisible.value = true;
}
function delAll() { function delAll() {
if (selectCount.value <= 0) { if (selectedRows.value.length === 0) {
Message.warning("您还未勾选要删除的数据"); Message.warning("您还未勾选要删除的数据");
return; return;
} }
Modal.confirm({ Modal.confirm({
title: "确认删除", title: "确认删除",
content: content: `您确认要删除所选的 ${selectedRows.value.length} 条数据及其下级所有数据?`,
"您确认要删除所选的 " + selectCount.value + " 条数据及其下级所有数据?",
loading: true, loading: true,
onOk: () => { onOk: () => {
let ids = ""; const ids = selectedRows.value.map((e) => e.id).join(",");
selectList.value.forEach(function (e) {
ids += e.id + ",";
});
ids = ids.substring(0, ids.length - 1);
deleteDepartment(ids).then((res) => { deleteDepartment(ids).then((res) => {
Modal.remove(); Modal.remove();
if (res.success) { if (res.success) {
Message.success("删除成功"); Message.success("删除成功");
selectList.value = []; selectedRows.value = [];
selectCount.value = 0; getList();
cancelSelect();
init();
} }
}); });
}, },
@@ -357,12 +313,21 @@ function delAll() {
} }
onMounted(() => { onMounted(() => {
const height = document.documentElement.clientHeight; getList();
maxHeight.value = Number(height - 287) + "px";
init();
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/tree-common.scss"; .operation {
margin-bottom: 12px;
}
.link-text {
color: #2d8cf0;
cursor: pointer;
text-decoration: none;
}
.op-split {
margin: 0 8px;
color: #dcdee2;
}
</style> </style>

View File

@@ -19,6 +19,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center" /> <el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column prop="updateTime" label="修改时间" width="180" align="center" />
<el-table-column label="状态" width="120" align="center"> <el-table-column label="状态" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-switch <el-switch

View File

@@ -35,7 +35,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="goodsName" label="销售商品" min-width="160" /> <el-table-column prop="goodsName" label="销售商品" min-width="160" />
<el-table-column prop="goodsId" label="商品ID" width="140" /> <el-table-column prop="goodsId" label="SPU编码" width="140" />
<el-table-column prop="salesNum" label="商品销售数量" width="110" /> <el-table-column prop="salesNum" label="商品销售数量" width="110" />
<el-table-column prop="salesAmount" label="商品销售金额" width="120"> <el-table-column prop="salesAmount" label="商品销售金额" width="120">
<template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template> <template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template>

View File

@@ -1,4 +1,6 @@
// 商家素材库(OSS 管理)样式:全部规则挂在 .oss-manage 根类下,避免全局污染 // 商家素材库(OSS 管理)样式。
// 本文件经 @import 全局引入且未加 scoped,页面内规则全部挂在 .oss-manage 根类下避免污染;
// 全局样式里不要写 :deep(),浏览器会把含未知伪类的整条规则丢弃。
.oss-manage { .oss-manage {
// 单面板容器 // 单面板容器
.oss-panel { .oss-panel {
@@ -91,11 +93,11 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-content: flex-start; align-content: flex-start;
gap: 16px 14px;
margin-top: 14px; margin-top: 14px;
.oss-grid-item { .oss-grid-item {
width: 120px; width: 120px;
margin: 0 14px 14px 0;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -104,12 +106,15 @@
.oss-card { .oss-card {
width: 120px; width: 120px;
height: 120px; height: 120px;
border-radius: 6px; border-radius: 8px;
border: 2px solid transparent; border: 1px solid #ebeef5;
background: #f7f8fa;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
transition: border-color 0.2s; transition:
border-color 0.2s,
box-shadow 0.2s;
img, img,
.card-media { .card-media {
@@ -150,7 +155,7 @@
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
:deep(.el-checkbox__label) { .el-checkbox__label {
display: none; display: none;
} }
} }
@@ -186,6 +191,7 @@
&:hover { &:hover {
border-color: var(--el-color-primary); border-color: var(--el-color-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
.oss-card-check { .oss-card-check {
opacity: 1; opacity: 1;
@@ -236,6 +242,92 @@
} }
} }
// 左侧分组树
.oss-group-tree {
flex: 1;
padding: 4px 4px 0;
.el-tree-node__expand-icon {
display: none;
}
.el-tree-node {
margin-bottom: 4px;
}
.el-tree-node__content {
padding: 0 10px !important;
height: 38px;
border-radius: 6px;
font-size: 14px;
line-height: 38px;
}
.el-tree-node__content:hover {
background: #f5f7fa;
}
.el-tree-node.is-current > .el-tree-node__content {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
.el-tree-node.is-current .oss-tree-icon {
color: var(--el-color-primary);
}
}
.oss-tree-node {
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 4px;
font-size: 14px;
&:hover .oss-tree-more {
opacity: 1;
}
}
.oss-tree-label {
display: inline-flex;
align-items: center;
min-width: 0;
overflow: hidden;
}
.oss-tree-icon {
margin-right: 6px;
flex-shrink: 0;
color: #c0c4cc;
}
.oss-tree-more {
cursor: pointer;
color: #909399;
padding: 0 4px;
font-size: 16px;
opacity: 0;
transition: opacity 0.2s;
flex-shrink: 0;
&:hover {
color: var(--el-color-primary);
}
}
// picker 模式的底部确认栏
.oss-select-footer {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #ededed;
display: flex;
justify-content: flex-end;
gap: 10px;
}
// 弹窗选择器模式:去掉面板阴影,固定内容区最小高度,避免空分组时布局塌陷 // 弹窗选择器模式:去掉面板阴影,固定内容区最小高度,避免空分组时布局塌陷
&.oss-manage-picker { &.oss-manage-picker {
.oss-panel { .oss-panel {
@@ -272,3 +364,34 @@
} }
} }
} }
// 以下两条用于 el-dialog 内部;el-dialog 默认挂载到 body 下,必须保持全局
.modal-footer {
text-align: center;
> * {
margin: 0 10px;
}
}
.file-preview-fallback {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
color: #606266;
.file-preview-name {
margin: 16px 0 8px;
font-size: 16px;
font-weight: 500;
word-break: break-all;
text-align: center;
}
.file-preview-tip {
margin: 0 0 20px;
color: #909399;
}
}

View File

@@ -1,110 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "./ossManage.scss"; @import "./ossManage.scss";
.table {
flex: 11;
}
.el-card {
width: 100%;
}
.modal-footer {
text-align: center;
> * {
margin: 0 10px;
}
}
.oss-select-footer {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #ededed;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.oss-group-tree {
flex: 1;
padding: 4px 4px 0;
:deep(.el-tree-node__expand-icon) {
display: none;
}
:deep(.el-tree-node) {
margin-bottom: 4px;
}
:deep(.el-tree-node__content) {
padding: 0 10px !important;
height: 38px;
border-radius: 6px;
font-size: 14px;
line-height: 38px;
}
:deep(.el-tree-node__content:hover) {
background: #f5f7fa;
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
}
.oss-tree-node {
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 4px;
font-size: 14px;
&:hover .oss-tree-more {
opacity: 1;
}
}
.oss-tree-more {
cursor: pointer;
color: #909399;
padding: 0 4px;
font-size: 16px;
opacity: 0;
transition: opacity 0.2s;
&:hover {
color: var(--el-color-primary);
}
}
.file-preview-fallback {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
color: #606266;
.file-preview-name {
margin: 16px 0 8px;
font-size: 16px;
font-weight: 500;
word-break: break-all;
text-align: center;
}
.file-preview-tip {
margin: 0 0 20px;
color: #909399;
}
}
</style> </style>
<template> <template>
<div class="search oss-manage" :class="{ 'oss-manage-picker': isComponent }"> <div class="search oss-manage" :class="{ 'oss-manage-picker': isComponent }">
@@ -167,7 +62,10 @@
> >
<template #default="{ data }"> <template #default="{ data }">
<span v-if="data" class="oss-tree-node"> <span v-if="data" class="oss-tree-node">
<span>{{ data.title }}</span> <span class="oss-tree-label">
<el-icon class="oss-tree-icon"><Folder /></el-icon>
{{ data.title }}
</span>
<el-dropdown <el-dropdown
v-if="data.value !== '0' && !isComponent" v-if="data.value !== '0' && !isComponent"
trigger="click" trigger="click"
@@ -197,8 +95,6 @@
'is-checked': selectedOss.includes(item.id + ',' + item.url), 'is-checked': selectedOss.includes(item.id + ',' + item.url),
'oss-card-selectable': isComponent && selectImage, 'oss-card-selectable': isComponent && selectImage,
}" }"
@mouseenter="onMouseOver(item, index)"
@mouseleave="onMouseOut(item, index)"
@click="handleCardClick(item, $event)" @click="handleCardClick(item, $event)"
> >
<el-checkbox <el-checkbox
@@ -206,7 +102,15 @@
class="oss-card-check" class="oss-card-check"
@click.stop @click.stop
/> />
<img v-if="isImageFile(item)" :src="item.url" alt="" /> <img
v-if="isImageFile(item) && !imgFailIds.has(item.id)"
:src="item.url"
alt=""
@error="onImgError(item)"
/>
<div v-else-if="isImageFile(item)" class="card-file">
<el-icon :size="28"><Picture /></el-icon>
</div>
<video <video
v-else-if="isVideoFile(item)" v-else-if="isVideoFile(item)"
class="card-media" class="card-media"
@@ -216,7 +120,7 @@
<div v-else class="card-file"> <div v-else class="card-file">
<el-icon :size="28"><Document /></el-icon> <el-icon :size="28"><Document /></el-icon>
</div> </div>
<div v-if="item.isShowPreview" class="oss-card-ops"> <div class="oss-card-ops">
<div @click.prevent="download(item)"> <div @click.prevent="download(item)">
<el-tooltip content="下载" placement="top"> <el-tooltip content="下载" placement="top">
<el-icon :size="18"><Download /></el-icon> <el-icon :size="18"><Download /></el-icon>
@@ -365,14 +269,13 @@ let dp;
<script setup> <script setup>
import { import {
computed, computed,
getCurrentInstance,
onMounted, onMounted,
reactive, reactive,
ref, ref,
watch, watch,
} from "vue"; } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { Delete, Download, View, Document } from "@element-plus/icons-vue"; import { Delete, Download, View, Document, Folder, Picture } from "@element-plus/icons-vue";
import { import {
addFileDirectory, addFileDirectory,
deleteFile, deleteFile,
@@ -411,8 +314,11 @@ const props = defineProps({
}); });
const emit = defineEmits(["callback", "selected"]); const emit = defineEmits(["callback", "selected"]);
// $forceUpdate 依赖组件实例 // 加载失败的图片 id 集合:裂图时用图标占位代替浏览器默认裂图样式
const { proxy } = getCurrentInstance(); const imgFailIds = ref(new Set());
function onImgError(item) {
imgFailIds.value.add(item.id);
}
const groupFormValidate = ref({ const groupFormValidate = ref({
id: "", id: "",
@@ -601,13 +507,6 @@ function onTreeDropdownCommand(cmd, data) {
handleContextMenuDelete(); handleContextMenuDelete();
} }
} }
function onMouseOver(item, index) {
data.value[index].isShowPreview = true;
proxy.$forceUpdate();
}
function onMouseOut(item, index) {
data.value[index].isShowPreview = false;
}
function handleCategoryTabChange(name) { function handleCategoryTabChange(name) {
activeCategoryTab.value = name; activeCategoryTab.value = name;
searchForm.fileCategory = name; searchForm.fileCategory = name;
@@ -884,6 +783,7 @@ function getDataList(type = null) {
getFileListData(searchForm) getFileListData(searchForm)
.then((res) => { .then((res) => {
if (res && res.success && res.result) { if (res && res.success && res.result) {
imgFailIds.value = new Set();
data.value = res.result.records || []; data.value = res.result.records || [];
total.value = res.result.total || 0; total.value = res.result.total || 0;
} else { } else {