Merge branch 'feature/comment-0615-ch' into msb_test

msb_test
ch 2 years ago
commit 15bd961f5d

@ -1,4 +1,4 @@
VITE_BASE_URL=https://you-gateway.mashibing.com
VITE_SOCKET_URL=wss://you-gateway.mashibing.com/ws
VITE_REQUEST_TIMEOUT=20000
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn
VITE_BROWSER_URL = https://you.mashibing.com

@ -0,0 +1,36 @@
/*
* @Author: ch
* @Date: 2022-06-15 17:55:43
* @LastEditors: ch
* @LastEditTime: 2022-06-17 18:13:27
* @Description: file content
*/
import request from '@/utils/request.js';
//获取评论列表
export const commentList = (params) =>
request({
url: '/mall/comment/admin/comment',
method: 'get',
params,
});
// 获取评价详情
export const commentDetail = ({ id }) =>
request({
url: `/mall/comment/admin/comment/getCommentDetail/${id}`,
method: 'get',
});
//获取评论列表
export const commentAdd = (data) =>
request({
url: '/mall/comment/admin/comment',
method: 'post',
data,
});
// 更新评价显示状态
export const updateCommentShow = (data) =>
request({
url: '/mall/comment/admin/comment',
method: 'put',
data,
});

@ -0,0 +1,204 @@
import * as api from '@/api/goods/comment.js';
import { ElMessage } from 'element-plus/es';
const state = {
list: [],
detail: {
id: 18,
originId: 0,
parentId: 0,
userId: 97,
userName: 'ocean',
commentContent: '用户超时未做出评价,系统默认好评',
isShow: false,
createTime: '2022-06-17 15:27:09',
commentType: 1,
userAvatar: 'https://msb-edu-dev.oss-cn-beijing.aliyuncs.com/default-headimg.png',
skuName: '测试SKU',
productName: '测试虚拟111',
phone: '151****5744',
pictureUrl: 'https://msb-edu-dev.oss-cn-beijing.aliyuncs.com/uc/account-avatar/120x120.png',
commentScore: 5,
followComment: {
id: 19,
originId: 18,
parentId: 18,
userId: 97,
userName: 'ocean',
commentContent: '新增一条追评',
isShow: true,
createTime: '2022-06-17 17:13:32',
commentType: 2,
},
answerCommentList: [
{
id: 20,
originId: 18,
parentId: 18,
userId: 8,
userName: '随机昵称',
commentContent: '新增一条回复',
isShow: true,
createTime: '2022-06-17 17:14:02',
commentType: 3,
parentUserName: 'ocean',
},
{
id: 21,
originId: 18,
parentId: 18,
userId: 8,
userName: '随机昵称',
commentContent: '1232321',
isShow: true,
createTime: '2022-06-17 18:31:48',
commentType: 3,
parentUserName: 'ocean',
},
{
id: 22,
originId: 18,
parentId: 18,
userId: 8,
userName: '随机昵称',
commentContent: '我回复我回复我回复我回复我回复',
isShow: true,
createTime: '2022-06-17 18:32:39',
commentType: 3,
parentUserName: 'ocean',
},
{
id: 23,
originId: 18,
parentId: 22,
userId: 8,
userName: '随机昵称',
commentContent: '222222222222',
isShow: true,
createTime: '2022-06-17 18:34:30',
commentType: 3,
parentUserName: '随机昵称',
},
{
id: 24,
originId: 18,
parentId: 18,
userId: 8,
userName: '随机昵称',
commentContent: '直接评论直接评论',
isShow: true,
createTime: '2022-06-17 18:36:03',
commentType: 3,
parentUserName: 'ocean',
},
],
},
total: 0,
opts: {
isShow: [
{
value: true,
label: '显示',
},
{
value: false,
label: '隐藏',
},
],
score: [
{
value: 1,
label: '1星',
},
{
value: 2,
label: '2星',
},
{
value: 3,
label: '3星',
},
{
value: 4,
label: '4星',
},
{
value: 5,
label: '5星',
},
],
commentType: {
//  评论
comment: 1,
// 追评
addComment: 2,
// 回复
reply: 3,
},
},
};
const getters = {};
const mutations = {
setList: (state, data) => (state.list = data),
setTotal: (state, data) => (state.total = data),
setDetail: (state, data) => (state.detail = data),
};
const actions = {
async search({ state, commit, rootGetters }, params) {
let data = { ...params };
let pagingCode = params.pagingCode;
if (data.dateRange?.length) {
data.commentTimeBegin = data.dateRange[0];
data.commentTimeEnd = data.dateRange[1];
}
delete data.dateRange;
delete data.pagingCode;
const res = await api.commentList({
...rootGetters['local/page'](pagingCode),
...data,
});
if (!res) {
ElMessage.error('评价列表查询失败');
}
res;
commit(
'setList',
res?.records.map((i) => ({
...i,
scoreName: state.opts.score.find((item) => item.value == i.commentScore)?.label,
})) || []
);
commit('setTotal', res?.total || 0);
},
async updateShow({ state }, params) {
const res = await api.updateCommentShow(params);
if (!res) {
ElMessage.error('状态更新失败!');
return Promise.reject('更新失败');
}
return Promise.resolve(res);
},
async getDetail({ state, commit }, id) {
const res = await api.commentDetail({ id });
if (!res) {
ElMessage.error('获取详情失败');
return Promise.reject('获取详情失败');
}
commit('setDetail', res);
return Promise.resolve(res);
},
async add({ state, commit }, data) {
const res = await api.commentAdd(data);
if (!res) {
ElMessage.error('回复失败');
return Promise.reject(false);
}
return Promise.resolve(res);
},
};
export default {
state,
getters,
mutations,
actions,
};

@ -0,0 +1,350 @@
<template>
<div class="container">
<div class="sidebar">
<div class="sidebar--header">
<el-avatar :size="50" :src="detailData.userAvatar" />
<p>{{ detailData.userName }}</p>
<span>{{ detailData.phone }}</span>
</div>
<div class="sidebar--ctx">
<p class="goods">
<label>商品</label>
<span>{{ detailData.productName }}</span>
</p>
<p>
<label>规格</label>
<span>{{ detailData.skuName }}</span>
</p>
<p class="rate">
<label>评分</label>
<el-rate v-model="detailData.commentScore" disabled />
</p>
<p>
<label>日期</label>
<span>{{ detailData.createTime }}</span>
</p>
</div>
</div>
<div class="main">
<div class="title">
<b>评价内容</b>
<el-switch
v-if="detailData.id"
inactive-text="是否显示"
:value="!detailData.isShow"
@click="handleShowHide(detailData)"
/>
</div>
<div class="comment">
<p class="comment--ctx">{{ detailData.commentContent }}</p>
<div class="comment--imgs" v-if="detailData.pictureUrl">
<el-image
v-for="(item, idx) in detailData.pictureUrl"
class="img"
alt="评论"
:key="idx"
:src="item"
/>
</div>
<template v-if="detailData.followComment">
<b class="comment--title">
用户追评
<small>{{ detailData.followComment.createTime }}</small>
</b>
<p class="comment--ctx">{{ detailData.followComment.commentContent }}</p>
</template>
</div>
<div class="title">
<b>全部回复{{ detailData.answerCommentList?.length || 0 }}</b>
</div>
<ul class="reply" v-if="detailData.answerCommentList?.length">
<li class="reply--item" v-for="item in detailData.answerCommentList" :key="item.id">
<div class="reply--title">
{{ item.userName }}
<span v-if="item.parentId !== detailData.id"> {{ item.parentUserName }}</span>
</div>
<p class="reply--ctx">{{ item.commentContent }}</p>
<div class="reply--footer">
<i>{{ item.createTime }}</i>
<span>
<em @click="handleShowHide(item)" class="show-btn">
<template v-if="item.isShow">
<el-icon name="Open" />
隐藏
</template>
<template v-else>
<el-icon name="TurnOff" />
显示
</template>
</em>
<em @click="handleReply(item)" class="reply-btn">
<el-icon name="ChatDotSquare" />
回复
</em>
</span>
</div>
</li>
</ul>
<div class="reply__emtpy" v-else></div>
<div class="footer">
<textarea
class="footer--textarea"
ref="$textarea"
maxlength="500"
v-model="state.commentContent"
:placeholder="commentPlaceholder"
@keydown="handleClearReplay"
></textarea>
<el-button type="primary" class="footer--confirm" :disabled="sbumitDisabled" @click="handleSubmit">
提交
</el-button>
</div>
</div>
</div>
</template>
<script lang="jsx" setup>
import dayjs from 'dayjs';
import { ElMessage } from 'element-plus/es';
const store = useStore();
const router = useRouter();
const route = useRoute();
let detailData = ref({});
const state = reactive({
commentContent: '',
//
reply: {},
isSubmit: false,
});
//
const sbumitDisabled = computed(() => {
return !state.isSubmit && !state.commentContent;
});
const commentPlaceholder = computed(() =>
state.reply.userName ? `回复 ${state.reply.userName}` : '请输入回复内容'
);
const $textarea = ref(null);
const getDetail = async () => {
const res = await store.dispatch('comment/getDetail', route.params.id);
if (res) {
detailData.value = _.cloneDeep(store.state.comment.detail);
if (detailData.value.pictureUrl) {
detailData.value.pictureUrl = detailData.value.pictureUrl.split(',');
}
}
};
getDetail();
/**
* 点击回复设置当前回复对象以及输入框提示
*/
const handleReply = (item) => {
state.reply = item;
$textarea.value.focus();
};
/**
* 清空回复人
* 输入框没内容时再按一次退格清空回复对象
*/
const handleClearReplay = (target) => {
if (target.key === 'Backspace' && !state.commentContent) {
state.reply = {};
}
};
/**
* 评价/回复记录的显示隐藏
*/
const handleShowHide = async (item) => {
let val = !item.isShow;
try {
await store.dispatch('comment/updateShow', {
idList: [item.id],
isShow: val,
});
} catch (e) {
val = !val;
}
item.isShow = val;
};
/**
* 提交回复
*/
const handleSubmit = async () => {
state.isSubmit = true;
let data = {
commentContent: state.commentContent,
commentType: 3,
parentId: state.reply.id || detailData.value.id,
originId: detailData.value.id,
userId: store.state.auth.userInfo.userId,
userType: 1,
};
// ID userName
const res = await store.dispatch('comment/add', data);
ElMessage.success('回复成功!');
state.isSubmit = false;
state.commentContent = '';
detailData.value.answerCommentList.push({
...res,
isShow: true,
parentUserName: state.reply.userName,
userName: store.state.auth.userInfo.userName,
});
};
</script>
<style lang="less" scoped>
.container {
padding: 0 !important;
display: flex;
}
.sidebar {
width: 240px;
height: 100%;
border-right: 1px solid #ebeef5;
&--header {
text-align: center;
margin: 20px 0;
p {
margin: 10px 0;
}
span {
color: #999;
// font-size: 14px;
}
}
&--ctx {
margin: 20px 30px;
p {
// margin: 10px 0;
min-height: 32px;
line-height: 32px;
font-size: 14px;
color: #999;
label {
margin-right: 5px;
}
span {
line-height: 24px;
}
}
}
.goods {
line-height: 24px;
span {
color: #333;
}
}
.rate {
display: flex;
align-items: center;
:deep(.el-rate__item) {
display: flex;
}
}
}
.main {
flex: 1;
// position: relative;
padding: 20px 30px;
height: 100%;
overflow: auto;
.title {
border-bottom: 1px solid #eee;
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.comment {
margin: 30px 0;
&--ctx {
margin: 10px 0;
// text-indent: 28px;
}
&--title {
font-weight: normal;
color: var(--el-color-danger);
display: block;
margin: 30px 0 20px;
}
&--imgs {
margin-top: 20px;
.img {
border: 1px solid #eee;
margin-right: 10px;
}
}
}
.reply {
border-bottom: 1px solid #eee;
padding-bottom: 130px;
color: #666;
&__emtpy {
color: #999;
text-align: center;
height: 100px;
line-height: 100px;
}
&--item {
border-bottom: 1px solid #eee;
margin-top: 20px;
}
&--title {
color: #999;
margin: 5px 0;
}
&--ctx {
line-height: 24px;
margin: 10px 0;
}
&--footer {
margin: 5px 0;
display: flex;
justify-content: space-between;
span {
display: flex;
align-items: center;
}
em,
i {
color: #999;
font-style: normal;
font-size: 14px;
}
em {
cursor: pointer;
display: flex;
align-items: center;
margin-left: 20px;
}
}
}
.footer {
position: absolute;
bottom: 0;
left: 240px;
right: 0;
padding: 20px 30px;
text-align: right;
background: #fff;
border-top: 1px solid #eee;
display: flex;
align-items: center;
&--textarea {
flex: 1;
padding: 10px;
height: 60px;
resize: none;
border: 1px solid #eee;
&:focus {
border: 1px solid var(--el-color-primary);
outline: none !important;
}
}
&--confirm {
height: 60px;
border-radius: 0;
}
}
}
</style>

@ -0,0 +1,258 @@
<!--
* @Author: ch
* @Date: 2022-06-15 17:29:32
* @LastEditors: ch
* @LastEditTime: 2022-06-16 21:24:49
* @Description: file content
-->
<template>
<table-list
v-loading="loading"
:operation="['search']"
:code="pagingCode"
:config="config"
:data="list"
:total="total"
@search="handleSearch"
:reset="handleReset"
>
<template #search>
<el-form inline>
<el-form-item label="商品名称" prop="name">
<el-input v-model="state.condition.goodsName" />
</el-form-item>
<el-form-item label="用户昵称" prop="name">
<el-input v-model="state.condition.nickName" />
</el-form-item>
<el-form-item label="手机号" prop="name">
<el-input v-model="state.condition.phone" />
</el-form-item>
<el-form-item label="评分" prop="name">
<el-select v-model="state.condition.scoreList" multiple collapse-tags collapse-tags-tooltip>
<el-option
v-for="(item, idx) in opts.score"
:key="idx"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否显示" prop="name">
<el-select v-model="state.condition.isShow">
<el-option
v-for="(item, idx) in opts.isShow"
:key="idx"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="评价时间" prop="dateRange">
<el-date-picker
v-model="state.condition.dateRange"
:default-time="[new Date(0, 0, 0, 0, 0, 0), new Date(0, 0, 0, 23, 59, 59)]"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-form>
</template>
<template #operation="{ selection }">
<div class="batch-show-hide" v-if="selection.length">
<el-select v-model="allShowHideVal">
<el-option
v-for="(item, idx) in opts.isShow"
:key="idx"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-button type="primary" @click="handleAllShowHide(selection)"></el-button>
</div>
</template>
</table-list>
</template>
<script setup lang="jsx">
import ElButton from '@/components/extra/ElButton.vue';
import ElSwitch from '@/components/extra/ElSwitch.vue';
const router = useRouter();
const store = useStore();
const loading = ref(false);
//
const opts = computed(() => store.state.comment.opts);
//
const list = computed(() => _.cloneDeep(store.state.comment.list));
const total = computed(() => store.state.comment.total);
// code使codesotre
const _pagingCode = 'CommentManagement';
//
const _condition = {
productName: '',
nickName: '',
phone: '',
scoreList: [],
isShow: '',
dateRange: '',
};
const state = reactive({
condition: { ..._condition },
});
//
const allShowHideVal = ref(true);
store.dispatch('comment/search', { pagingCode: _pagingCode });
onActivated(() => handleSearch);
/**
* 搜索
*/
const handleSearch = async () => {
loading.value = true;
await store.dispatch('comment/search', { ...state.condition, pagingCode: _pagingCode });
loading.value = false;
};
/**
* 重置
*/
const handleReset = () => {
state.condition = { ..._condition };
};
/**
* 单行操作显示隐藏
*
*/
const handleShowHide = async (row) => {
loading.value = true;
try {
await store.dispatch('comment/updateShow', {
idList: [row.id],
isShow: row.isShow,
});
} catch (e) {
row.isShow = !row.isShow;
}
loading.value = false;
};
/**
* 批量操作显示隐藏
*/
const handleAllShowHide = async (selection) => {
loading.value = true;
let val = allShowHideVal.value;
try {
await store.dispatch('comment/updateShow', {
idList: selection.map((i) => i.id),
isShow: val,
});
} catch (e) {
val = !val;
}
loading.value = false;
selection.forEach((item) => {
item.isShow = val;
});
};
const handleDetail = (id) => {
router.push({
name: 'CommentDetail',
params: {
id: id,
},
});
};
const config = reactive({
columns: [
{
type: 'selection',
fixed: 'left',
width: 60,
},
{
label: '商品名称',
width: 180,
align: 'left',
slots: {
default: ({ row }) => <div class="row-ellipsis">{row.productName}</div>,
},
},
{
label: '评价内容',
align: 'left',
slots: {
default: ({ row }) => (
<div class="row-ellipsis ctx-link" onClick={() => handleDetail(row.id)}>
{row.commentContent}
</div>
),
},
},
{
label: '评分',
prop: 'scoreName',
width: 70,
},
{
label: '用户',
width: 120,
slots: {
default: ({ row }) => (
<div>
<p class="row-ellipsis">{row.userName}</p>
<p>{row.phone}</p>
</div>
),
},
},
{
label: '评价时间',
prop: 'time',
width: 120,
slots: {
default: ({ row }) => (
<div>
<p>{row.createTime.split(' ')[0]}</p>
<p>{row.createTime.split(' ')[1]}</p>
</div>
),
},
},
{
label: '显示',
width: 80,
slots: {
default: ({ row }) => <ElSwitch v-model={row.isShow} onChange={() => handleShowHide(row)} />,
},
},
{
label: '操作',
width: 70,
slots: {
default: ({ row }) => (
<ElButton type="text" onClick={() => handleDetail(row.id)}>
查看
</ElButton>
),
},
},
],
});
</script>
<style lang="less" scoped>
.batch-show-hide {
margin-left: 20px;
}
:deep(.row-ellipsis) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
:deep(.ctx-link) {
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
</style>
Loading…
Cancel
Save