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请求基础路径
*/
API_DEV: {
seller: "https://admin-api.pickmall.cn",
seller: "http://127.0.0.1:8887",
},
API_PROD: {
seller: "https://admin-api.pickmall.cn",

View File

@@ -295,7 +295,7 @@ export const deleteDepartment = (ids, params) => {
};
// 编辑部门
export const editDepartment = (ids, params) => {
return putRequest(`/department/${ids} `, params);
return putRequest(`/department/${ids}`, params);
};
// 加载部门子级数据
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) {
title = title || `${config.title} 商家后台`;
const siteName = util.getSiteName();
title = title ? `${title} - ${siteName}商家后台` : `${siteName} 商家后台`;
window.document.title = title;
};
@@ -508,9 +514,8 @@ util.initRouterNode = function (routers, data) {
}
const meta = {};
meta.title = menu.title
? menu.title + " - " + config.title + "商家后台"
: null;
// 站点后缀统一由 util.title 在路由跳转时动态拼接,避免初始化时定死
meta.title = menu.title ? menu.title : null;
meta.firstRouterName = item.firstRouterName;
meta.keepAlive = menu.keepAlive ? true : false;
menu.meta = meta;

View File

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

View File

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

View File

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

View File

@@ -37,7 +37,12 @@
<el-tabs v-model="activeTab" @tab-click="switchTabs">
<el-tab-pane label="预警商品" name="warnList">
<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">
<template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column>
@@ -54,7 +59,12 @@
<el-tab-pane label="设置预警" name="warnSetting">
<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">
<template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column>

View File

@@ -6,74 +6,70 @@
:model="searchForm"
inline
label-width="70px"
class="search-form"
class="search-form goods-search-form"
@keyup.enter="handleSearch"
>
<div class="search-form-row">
<el-form-item label="商品名称" prop="goodsName">
<el-input
v-model="searchForm.goodsName"
placeholder="请输入商品名称"
clearable
/>
</el-form-item>
<el-form-item label="SKU名称" prop="skuName">
<el-input
v-model="searchForm.skuName"
placeholder="请输入SKU名称"
clearable
/>
</el-form-item>
<el-form-item label="SKU编码" prop="skuSn">
<el-input
v-model="searchForm.skuSn"
placeholder="请输入SKU编码"
clearable
/>
</el-form-item>
<el-form-item label="商品ID" prop="id">
<el-input
v-model="searchForm.id"
placeholder="请输入商品ID"
clearable
/>
</el-form-item>
</div>
<div class="search-form-row">
<el-form-item label="店铺分类" prop="storeCategoryPath">
<el-cascader
v-model="searchForm.storeCategoryPath"
:options="shopCategoryList"
:props="shopCategoryProps"
clearable
filterable
placeholder="请选择店铺分类"
/>
</el-form-item>
<el-form-item label="平台分类" prop="categoryPath">
<el-cascader
v-model="searchForm.categoryPath"
:options="platformCategoryList"
:props="platformCategoryProps"
clearable
filterable
placeholder="请选择平台商品分类"
/>
</el-form-item>
<el-form-item label="商品类型" prop="goodsType">
<el-select
v-model="searchForm.goodsType"
placeholder="请选择"
clearable
>
<el-option label="实物商品" value="PHYSICAL_GOODS" />
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
<!-- E_COUPON:卡密商品,可跳转卡池管理 -->
<el-option label="电子卡券" value="E_COUPON" />
<el-option label="预约商品" value="APPOINTMENT_GOODS" />
</el-select>
</el-form-item>
</div>
<el-form-item label="商品名称" prop="goodsName">
<el-input
v-model="searchForm.goodsName"
placeholder="请输入商品名称"
clearable
/>
</el-form-item>
<el-form-item label="SKU名称" prop="skuName">
<el-input
v-model="searchForm.skuName"
placeholder="请输入SKU名称"
clearable
/>
</el-form-item>
<el-form-item label="SKU编码" prop="skuSn">
<el-input
v-model="searchForm.skuSn"
placeholder="请输入SKU编码"
clearable
/>
</el-form-item>
<el-form-item label="SPU编码" prop="id">
<el-input
v-model="searchForm.id"
placeholder="请输入SPU编码"
clearable
/>
</el-form-item>
<el-form-item label="店铺分类" prop="storeCategoryPath">
<el-cascader
v-model="searchForm.storeCategoryPath"
:options="shopCategoryList"
:props="shopCategoryProps"
clearable
filterable
placeholder="请选择店铺分类"
/>
</el-form-item>
<el-form-item label="平台分类" prop="categoryPath">
<el-cascader
v-model="searchForm.categoryPath"
:options="platformCategoryList"
:props="platformCategoryProps"
clearable
filterable
placeholder="请选择平台商品分类"
/>
</el-form-item>
<el-form-item label="商品类型" prop="goodsType">
<el-select
v-model="searchForm.goodsType"
placeholder="请选择"
clearable
>
<el-option label="实物商品" value="PHYSICAL_GOODS" />
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
<!-- E_COUPON:卡密商品,可跳转卡池管理 -->
<el-option label="电子卡券" value="E_COUPON" />
<el-option label="预约商品" value="APPOINTMENT_GOODS" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createRange"
@@ -81,7 +77,6 @@
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始"
end-placeholder="结束"
style="width: 360px"
/>
</el-form-item>
<el-form-item label="库存状态" prop="stockStatus">
@@ -89,7 +84,6 @@
v-model="searchForm.stockStatus"
placeholder="全部"
clearable
style="width: 240px"
@change="onStockStatusChange"
>
<el-option label="有货" value="IN_STOCK" />
@@ -97,34 +91,18 @@
</el-select>
</el-form-item>
<el-form-item v-if="searchForm.stockStatus === 'IN_STOCK'" label="库存区间">
<el-input
v-model="searchForm.stockMin"
placeholder="≥"
clearable
style="width: 110px"
/>
<span style="margin: 0 6px">~</span>
<el-input
v-model="searchForm.stockMax"
placeholder="≤"
clearable
style="width: 110px"
/>
<div class="range-inputs">
<el-input v-model="searchForm.stockMin" placeholder="≥" clearable />
<span class="range-sep">~</span>
<el-input v-model="searchForm.stockMax" placeholder="≤" clearable />
</div>
</el-form-item>
<el-form-item label="售价区间">
<el-input
v-model="searchForm.priceMin"
placeholder="≥"
clearable
style="width: 110px"
/>
<span style="margin: 0 6px">~</span>
<el-input
v-model="searchForm.priceMax"
placeholder="≤"
clearable
style="width: 110px"
/>
<div class="range-inputs">
<el-input v-model="searchForm.priceMin" placeholder="≥" clearable />
<span class="range-sep">~</span>
<el-input v-model="searchForm.priceMax" placeholder="≤" clearable />
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
@@ -172,7 +150,7 @@
@selection-change="onSelectionChange"
>
<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">
<template #default="{ row }">
<img alt="商品图片"
@@ -219,7 +197,7 @@
</template>
</el-table-column>
<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 v-if="row.marketEnable === 'DOWN'">
<a class="link-text" @click="upper(row)">上架</a>
@@ -693,28 +671,48 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.search-form-row {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
width: 100%;
margin-bottom: 18px;
.goods-search-form {
display: grid !important;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 16px;
align-items: start;
:deep(.el-form-item) {
flex: 1;
> :deep(.el-form-item) {
display: flex;
width: auto;
min-width: 0;
margin-right: 12px;
margin-bottom: 0;
margin: 8px 0 !important;
}
:deep(.el-form-item:last-child) {
margin-right: 0;
:deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
:deep(.el-input),
: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%;
:deep(.el-input) {
flex: 1;
min-width: 0;
}
.range-sep {
margin: 0 6px;
flex-shrink: 0;
}
}
}

View File

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

View File

@@ -20,23 +20,28 @@
</a>
</li>
</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="" />
{{ config.company.name }}
{{ companyName }}
</a>
</div>
<div class="copyright">
<p>Copyright © {{ year }} {{ config.title }}</p>
<p>Copyright © {{ year }} {{ siteName }}</p>
</div>
</div>
</template>
<script setup>
import config from "@/config/index";
import util from "@/libs/util.js";
import zhizhaoImg from "@/assets/images/zhizhao.jpg";
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>
<style lang="scss" scoped>

View File

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

View File

@@ -10,16 +10,27 @@
@keyup.enter="handleSearch"
>
<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 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 label="评价" prop="grade">
<el-select v-model="searchForm.grade" placeholder="请选择" clearable style="width: 240px">
<el-option label="好评" value="GOOD" />
<el-option label="中评" value="MODERATE" />
<el-option label="差评" value="WORSE" />
<el-form-item label="回复状态" prop="replyStatus">
<el-select v-model="searchForm.replyStatus" placeholder="请选择" clearable style="width: 140px">
<el-option label="已回复" :value="true" />
<el-option label="未回复" :value="false" />
</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-form-item>
<el-form-item label="评论日期">
@@ -42,24 +53,54 @@
</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-column prop="memberName" label="会员名称" min-width="150" show-overflow-tooltip />
<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">
<el-table-column label="会员" min-width="160">
<template #default="{ row }">
<el-tag v-if="row.grade === 'GOOD'" type="success">好评</el-tag>
<el-tag v-else-if="row.grade === 'MODERATE'" type="warning">中评</el-tag>
<el-tag v-else type="danger">差评</el-tag>
<div class="member-cell">
<el-avatar :size="32" :src="row.memberProfile" />
<span class="cell-name" :title="row.memberName">{{ row.memberName }}</span>
</div>
</template>
</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 }">
<el-tag v-if="row.status === 'OPEN'" type="success">展示</el-tag>
<el-tag v-else type="danger">隐藏</el-tag>
</template>
</el-table-column>
<el-table-column label="回复状态" width="110">
<el-table-column label="回复状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.replyStatus" type="success">已回复</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 label="操作" align="center" fixed="right" width="100">
<template #default="{ row }">
<a class="link-text" @click="detail(row)">详细</a>
<a class="link-text" @click="detail(row)">{{ row.replyStatus ? "查看" : "回复" }}</a>
</template>
</el-table-column>
</el-table>
@@ -87,42 +128,116 @@
</div>
</el-card>
<el-dialog v-model="modalVisible" :title="modalTitle" width="500px" :close-on-click-modal="false">
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
<el-form-item label="评价内容">
<span v-if="!content">暂无评价</span>
<el-input v-else v-model="content" type="textarea" maxlength="200" disabled :rows="4" style="width: 90%" />
</el-form-item>
<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" />
</el-form-item>
<el-form-item label="回复内容" prop="reply">
<el-input
v-if="!replyStatus"
v-model="form.reply"
type="textarea"
maxlength="200"
:rows="4"
clearable
style="width: 90%"
/>
<el-input v-else v-model="form.reply" type="textarea" maxlength="200" disabled :rows="4" style="width: 90%" />
</el-form-item>
<el-form-item
v-if="detailInfo.haveReplyImage == 1 || !replyStatus"
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>
<el-drawer
v-model="modalVisible"
title="评价详情"
size="600px"
append-to-body
:close-on-click-modal="false"
>
<div v-loading="detailLoading" class="comment-detail">
<template v-if="detailInfo.id">
<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
v-model="form.reply"
type="textarea"
maxlength="200"
:rows="4"
resize="none"
show-word-limit
placeholder="请输入回复内容,回复后将展示在商品评价中"
/>
</el-form-item>
<el-form-item label="回复图片" class="reply-upload-item">
<upload-pic-thumb v-model="form.replyImage" :limit="5" />
</el-form-item>
</el-form>
</div>
</template>
</div>
<template #footer>
<el-button @click="modalVisible = false">取消</el-button>
<el-button v-if="!replyStatus" type="primary" :loading="submitLoading" @click="handleSubmit">回复</el-button>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
@@ -138,10 +253,11 @@ const detailInfo = ref({});
const image = ref([]);
const replyStatus = ref(false);
const modalVisible = ref(false);
const modalTitle = ref("");
const loading = ref(true);
const detailLoading = ref(false);
const content = ref("");
const searchForm = ref({
const defaultSearchForm = () => ({
pageNumber: 1,
pageSize: 10,
sort: "createTime",
@@ -149,8 +265,17 @@ const searchForm = ref({
startTime: "",
endTime: "",
});
const searchForm = ref(defaultSearchForm());
const selectDate = ref(null);
const gradeTabs = [
{ title: "全部评价", value: "" },
{ title: "好评", value: "GOOD" },
{ title: "中评", value: "MODERATE" },
{ title: "差评", value: "WORSE" },
];
const currentGrade = ref("");
const form = reactive({
id: "",
replyImage: [],
reply: "",
});
@@ -187,15 +312,15 @@ function handleSearch() {
getDataList();
}
function handleReset() {
searchForm.value = {
pageNumber: 1,
pageSize: 10,
sort: "createTime",
order: "desc",
startTime: "",
endTime: "",
};
searchForm.value = defaultSearchForm();
selectDate.value = null;
currentGrade.value = "";
getDataList();
}
function onGradeTabClick(tab) {
currentGrade.value = tab.paneName;
searchForm.value.grade = tab.paneName || undefined;
searchForm.value.pageNumber = 1;
getDataList();
}
function selectDateRange(v) {
@@ -207,55 +332,233 @@ function selectDateRange(v) {
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() {
loading.value = true;
API_Member.getMemberReview(searchForm.value).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
API_Member.getMemberReview(searchForm.value)
.then((res) => {
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.finally(() => {
loading.value = false;
});
}
function handleSubmit() {
formRef.value.validate((valid) => {
if (valid) {
API_Member.replyMemberReview(form.id, form).then((res) => {
submitLoading.value = false;
if (!valid) return;
submitLoading.value = true;
API_Member.replyMemberReview(form.id, form)
.then((res) => {
if (res.success) {
Message.success("回复成功");
getDataList();
modalVisible.value = false;
}
})
.finally(() => {
submitLoading.value = false;
});
}
});
}
function detail(v) {
modalVisible.value = true;
detailInfo.value = {};
form.reply = "";
form.replyImage = [];
loading.value = true;
API_Member.getMemberInfoReview(v.id).then((res) => {
loading.value = false;
if (res.success) {
form.id = res.result.id;
content.value = res.result.content;
form.reply = res.result.reply;
replyStatus.value = res.result.replyStatus;
image.value = res.result.images ? (res.result.images || "").split(",") : [];
form.replyImage = res.result.replyImage ? (res.result.replyImage || "").split(",") : [];
detailInfo.value = res.result;
modalVisible.value = true;
modalTitle.value = "详细";
}
});
image.value = [];
detailLoading.value = true;
API_Member.getMemberInfoReview(v.id)
.then((res) => {
if (res.success) {
form.id = res.result.id;
content.value = res.result.content;
form.reply = res.result.reply;
replyStatus.value = res.result.replyStatus;
image.value = res.result.images ? (res.result.images || "").split(",") : [];
form.replyImage = res.result.replyImage ? (res.result.replyImage || "").split(",") : [];
detailInfo.value = res.result;
}
})
.finally(() => {
detailLoading.value = false;
});
}
</script>
<style lang="scss">
<style lang="scss" scoped>
@import "@/styles/table-common.scss";
.link-text {
color: #409eff;
cursor: pointer;
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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@
<el-table-column prop="addressName" label="自提点名称" min-width="120" />
<el-table-column prop="address" label="详细地址" min-width="280" />
<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">
<template #default="{ row }">
<a class="link-text" @click="edit(row)">修改</a>

View File

@@ -1,81 +1,37 @@
<template>
<div class="search">
<el-card>
<el-row class="operation">
<el-switch
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>
<div class="operation">
<el-button type="primary" @click="openAdd('0')">添加部门</el-button>
<el-button @click="delAll">批量删除</el-button>
<el-button @click="getParentList">刷新</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>
</el-col>
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
<el-form-item label="部门名称" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="选择角色">
<el-select
v-model="selectedRole"
multiple
:loading="userLoading"
placeholder="请选择角色"
style="width: 100%"
>
<el-option
v-for="item in users"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="排序值" prop="sortOrder">
<el-tooltip placement="right" content="值越小越靠前,支持小数">
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
</el-tooltip>
</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-button @click="getList">刷新</el-button>
</div>
<el-table
v-loading="loading"
:data="data"
row-key="id"
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
@@ -83,30 +39,55 @@
:title="modalTitle"
width="500px"
:close-on-click-modal="false"
destroy-on-close
>
<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 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 label="部门名称" prop="title">
<el-input v-model="formAdd.title" />
<el-input v-model="form.title" placeholder="请输入部门名称" />
</el-form-item>
<el-form-item v-if="modalType === 1" label="选择角色">
<el-select
v-model="selectedRole"
multiple
:loading="roleLoading"
placeholder="请选择角色"
style="width: 100%"
>
<el-option
v-for="item in roles"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</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-input-number v-model="form.sortOrder" :max="1000" :min="0" />
</el-tooltip>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelAdd">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
<el-button @click="modalVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import {
initDepartment,
addDepartment,
@@ -120,36 +101,24 @@ import { Message, Modal } from "@/utils/message";
defineOptions({ name: "store-department-manage" });
const treeRef = ref(null);
const formRef = ref(null);
const formAddRef = ref(null);
const loading = ref(true);
const maxHeight = ref("500px");
const strict = ref(true);
const userLoading = ref(false);
const modalVisible = ref(false);
const selectList = ref([]);
const selectCount = ref(0);
const showParent = ref(false);
const modalType = ref(0); // 0 添加 1 编辑
const modalTitle = ref("");
const editTitle = ref("");
const submitLoading = ref(false);
const roleLoading = ref(false);
const data = ref([]);
const roles = ref([]);
const selectedRole = ref([]);
const currentNodeKey = ref(null);
const treeProps = {
label: "title",
children: "children",
isLeaf: "isLeaf",
};
const selectedRows = ref([]);
const form = ref({
id: "",
parentId: "0",
title: "",
parentId: "",
parentTitle: "",
sortOrder: 0,
status: 0,
});
const formAdd = ref({});
const formValidate = {
title: [{ required: true, message: "名称不能为空", trigger: "blur" }],
sortOrder: [
@@ -161,23 +130,57 @@ const formValidate = {
},
],
};
const submitLoading = ref(false);
const data = ref([]);
const users = ref([]);
function init() {
getParentList();
// id -> title 映射,用于展示上级部门名称
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;
initDepartment().then((res) => {
loading.value = false;
if (res.success) {
const list = Array.isArray(res.result) ? res.result : [];
data.value = bubbleSort(normalizeDepartmentTree(list));
}
});
initDepartment()
.then((res) => {
if (res.success) {
const list = Array.isArray(res.result) ? res.result : [];
data.value = sortTree(normalizeDepartmentTree(list));
}
})
.finally(() => {
loading.value = false;
});
}
function normalizeDepartmentTree(list) {
return list.map((item) => {
const node = { ...item };
@@ -189,90 +192,71 @@ function normalizeDepartmentTree(list) {
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) => {
const way = [];
res.result &&
res.result.forEach((item) => {
way.push(item.roleId);
});
selectedRole.value = way;
function sortTree(list) {
list.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
list.forEach((item) => {
if (item.children?.length) sortTree(item.children);
});
return list;
}
getRoleList({
pageNumber: 1,
pageSize: 10000,
}).then((res) => {
userLoading.value = false;
if (res.success) {
users.value = res.result.records;
form.value = row;
}
});
function onSelectionChange(rows) {
selectedRows.value = rows;
}
function onCheckChange(_data, { checkedNodes }) {
selectList.value = checkedNodes;
selectCount.value = checkedNodes.length;
function openAdd(parentId) {
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 [];
const len = array.length;
if (len < 2) return array;
for (let i = 0; i < len; i++) {
for (let j = 0; j < i; j++) {
if (array[j].sortOrder > array[i].sortOrder) {
const temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}
}
return array;
}
function cancelSelect() {
treeRef.value?.setCurrentKey(null);
currentNodeKey.value = null;
formRef.value.resetFields();
delete form.value.id;
editTitle.value = "";
function openEdit(row) {
modalType.value = 1;
modalTitle.value = "编辑部门";
form.value = {
id: row.id,
parentId: row.parentId ?? "0",
title: row.title,
sortOrder: row.sortOrder ?? 0,
status: 0,
};
selectedRole.value = [];
roleLoading.value = true;
modalVisible.value = true;
Promise.all([
getUserByDepartmentId(row.id),
getRoleList({ pageNumber: 1, pageSize: 10000 }),
]).then(([bindRes, roleRes]) => {
roleLoading.value = false;
if (bindRes.result) {
selectedRole.value = bindRes.result.map((item) => item.roleId);
}
if (roleRes.success) {
roles.value = roleRes.result.records;
}
});
}
function cancelAdd() {
modalVisible.value = false;
}
function handleReset() {
formRef.value.resetFields();
form.value.status = 0;
}
function submitEdit() {
formRef.value.validate((valid) => {
if (valid) {
if (!form.value.id) {
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([
editDepartment(form.value.id, form.value),
updateDepartmentRole(form.value.id, roleWay),
@@ -280,76 +264,48 @@ function submitEdit() {
submitLoading.value = false;
if (res[0].success) {
Message.success("编辑成功");
init();
modalVisible.value = false;
getList();
}
});
}
});
}
function submitAdd() {
formAddRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true;
addDepartment(formAdd.value).then((res) => {
submitLoading.value = false;
function delOne(row) {
Modal.confirm({
title: "确认删除",
content: "删除该部门将同时删除其下级所有部门,确认删除?",
loading: true,
onOk: () => {
deleteDepartment(row.id).then((res) => {
Modal.remove();
if (res.success) {
Message.success("添加成功");
init();
modalVisible.value = false;
Message.success("删除成功");
getList();
}
});
}
},
});
}
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() {
if (selectCount.value <= 0) {
if (selectedRows.value.length === 0) {
Message.warning("您还未勾选要删除的数据");
return;
}
Modal.confirm({
title: "确认删除",
content:
"您确认要删除所选的 " + selectCount.value + " 条数据及其下级所有数据?",
content: `您确认要删除所选的 ${selectedRows.value.length} 条数据及其下级所有数据?`,
loading: true,
onOk: () => {
let ids = "";
selectList.value.forEach(function (e) {
ids += e.id + ",";
});
ids = ids.substring(0, ids.length - 1);
const ids = selectedRows.value.map((e) => e.id).join(",");
deleteDepartment(ids).then((res) => {
Modal.remove();
if (res.success) {
Message.success("删除成功");
selectList.value = [];
selectCount.value = 0;
cancelSelect();
init();
selectedRows.value = [];
getList();
}
});
},
@@ -357,12 +313,21 @@ function delAll() {
}
onMounted(() => {
const height = document.documentElement.clientHeight;
maxHeight.value = Number(height - 287) + "px";
init();
getList();
});
</script>
<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>

View File

@@ -19,6 +19,7 @@
</template>
</el-table-column>
<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">
<template #default="{ row }">
<el-switch

View File

@@ -35,7 +35,7 @@
</template>
</el-table-column>
<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="salesAmount" label="商品销售金额" width="120">
<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-panel {
@@ -91,11 +93,11 @@
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 16px 14px;
margin-top: 14px;
.oss-grid-item {
width: 120px;
margin: 0 14px 14px 0;
position: relative;
display: flex;
flex-direction: column;
@@ -104,12 +106,15 @@
.oss-card {
width: 120px;
height: 120px;
border-radius: 6px;
border: 2px solid transparent;
border-radius: 8px;
border: 1px solid #ebeef5;
background: #f7f8fa;
overflow: hidden;
box-sizing: border-box;
position: relative;
transition: border-color 0.2s;
transition:
border-color 0.2s,
box-shadow 0.2s;
img,
.card-media {
@@ -150,7 +155,7 @@
opacity: 0;
transition: opacity 0.2s;
:deep(.el-checkbox__label) {
.el-checkbox__label {
display: none;
}
}
@@ -186,6 +191,7 @@
&:hover {
border-color: var(--el-color-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
.oss-card-check {
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-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">
@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>
<template>
<div class="search oss-manage" :class="{ 'oss-manage-picker': isComponent }">
@@ -167,7 +62,10 @@
>
<template #default="{ data }">
<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
v-if="data.value !== '0' && !isComponent"
trigger="click"
@@ -197,8 +95,6 @@
'is-checked': selectedOss.includes(item.id + ',' + item.url),
'oss-card-selectable': isComponent && selectImage,
}"
@mouseenter="onMouseOver(item, index)"
@mouseleave="onMouseOut(item, index)"
@click="handleCardClick(item, $event)"
>
<el-checkbox
@@ -206,7 +102,15 @@
class="oss-card-check"
@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
v-else-if="isVideoFile(item)"
class="card-media"
@@ -216,7 +120,7 @@
<div v-else class="card-file">
<el-icon :size="28"><Document /></el-icon>
</div>
<div v-if="item.isShowPreview" class="oss-card-ops">
<div class="oss-card-ops">
<div @click.prevent="download(item)">
<el-tooltip content="下载" placement="top">
<el-icon :size="18"><Download /></el-icon>
@@ -365,14 +269,13 @@ let dp;
<script setup>
import {
computed,
getCurrentInstance,
onMounted,
reactive,
ref,
watch,
} from "vue";
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 {
addFileDirectory,
deleteFile,
@@ -411,8 +314,11 @@ const props = defineProps({
});
const emit = defineEmits(["callback", "selected"]);
// $forceUpdate 依赖组件实例
const { proxy } = getCurrentInstance();
// 加载失败的图片 id 集合:裂图时用图标占位代替浏览器默认裂图样式
const imgFailIds = ref(new Set());
function onImgError(item) {
imgFailIds.value.add(item.id);
}
const groupFormValidate = ref({
id: "",
@@ -601,13 +507,6 @@ function onTreeDropdownCommand(cmd, data) {
handleContextMenuDelete();
}
}
function onMouseOver(item, index) {
data.value[index].isShowPreview = true;
proxy.$forceUpdate();
}
function onMouseOut(item, index) {
data.value[index].isShowPreview = false;
}
function handleCategoryTabChange(name) {
activeCategoryTab.value = name;
searchForm.fileCategory = name;
@@ -884,6 +783,7 @@ function getDataList(type = null) {
getFileListData(searchForm)
.then((res) => {
if (res && res.success && res.result) {
imgFailIds.value = new Set();
data.value = res.result.records || [];
total.value = res.result.total || 0;
} else {