mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
style(member): 优化申诉详情、会员等级、优惠券、礼品卡页面样式
This commit is contained in:
@@ -1,185 +1,224 @@
|
||||
<template>
|
||||
<div class="add-eval">
|
||||
<div class="complain-detail">
|
||||
<UserCenterLayout>
|
||||
<div class="title">
|
||||
<p>
|
||||
<span class="fontsize_16 global_color">{{ statusLabel[detail.complainStatus] }}</span>
|
||||
<span class="color999 ml_20">创建时间:</span><span>{{ detail.createTime }}</span>
|
||||
<span class="color999 ml_20">{{ detail.createTime }}</span>
|
||||
</p>
|
||||
<div class="detail-head">
|
||||
<el-tag :type="statusTagType" effect="dark">{{ statusText }}</el-tag>
|
||||
<span class="create-time">创建时间:{{ detail.createTime || "-" }}</span>
|
||||
</div>
|
||||
|
||||
<el-alert class="l_title" show-icon type="warning">我的申诉信息</el-alert>
|
||||
<table cellspacing="0" cellpadding='0' border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>投诉商品</td>
|
||||
<td class="hover-color" @click="linkTo(`/goodsDetail?goodsId=${detail.goodsId}&skuId=${detail.skuId}`)"><img :src="detail.goodsImage" loading="lazy" decoding="async" width="60" alt=""> {{ detail.goodsName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>投诉主题</td>
|
||||
<td>{{ detail.complainTopic }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>投诉内容</td>
|
||||
<td>{{ detail.content }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>补充内容</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;">
|
||||
<template v-if="detail.images">
|
||||
<div class="demo-upload-list" v-for="(img, index) in detail.images.split(',')" :key="index">
|
||||
<img :src="img" loading="lazy" decoding="async">
|
||||
<section class="panel">
|
||||
<div class="panel-title">我的申诉信息</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">投诉商品</span>
|
||||
<div class="info-value">
|
||||
<button
|
||||
v-if="detail.goodsId"
|
||||
type="button"
|
||||
class="goods-info"
|
||||
@click="goGoodsDetail(detail.skuId, detail.goodsId)"
|
||||
>
|
||||
<img :src="detail.goodsImage" loading="lazy" decoding="async" alt="" />
|
||||
<span class="goods-name">{{ detail.goodsName }}</span>
|
||||
</button>
|
||||
<span v-else class="empty-text">暂无</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">投诉主题</span>
|
||||
<span class="info-value">{{ detail.complainTopic || "暂无" }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">投诉内容</span>
|
||||
<span class="info-value">{{ detail.content || "暂无" }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">补充内容</span>
|
||||
<div class="info-value">
|
||||
<div v-if="images.length" class="img-list">
|
||||
<div
|
||||
v-for="(img, index) in images"
|
||||
:key="index"
|
||||
class="demo-upload-list"
|
||||
@click="handleView(img)"
|
||||
>
|
||||
<img :src="img" loading="lazy" decoding="async" />
|
||||
<div class="demo-upload-list-cover">
|
||||
<el-icon @click="handleView(img)"><View /></el-icon>
|
||||
<el-icon><View /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>暂无</div>
|
||||
</div>
|
||||
<span v-else class="empty-text">暂无</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-alert class="l_title" show-icon type="warning">商家申诉信息</el-alert>
|
||||
<table cellspacing="0" cellpadding='0' border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>申诉时间</td>
|
||||
<td>{{ detail.appealTime || '暂无' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>申诉内容</td>
|
||||
<td>{{ detail.appealContent || '暂无' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>申诉凭证</td>
|
||||
<td>
|
||||
|
||||
<div style="display:flex;align-items:center;">
|
||||
<template v-if="detail.appealImages">
|
||||
<div class="demo-upload-list" v-for="(img, index) in detail.appealImages.split(',')" :key="index">
|
||||
<img :src="img" loading="lazy" decoding="async">
|
||||
<section class="panel">
|
||||
<div class="panel-title">商家申诉信息</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">申诉时间</span>
|
||||
<span class="info-value">{{ detail.appealTime || "暂无" }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">申诉内容</span>
|
||||
<span class="info-value">{{ detail.appealContent || "暂无" }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">申诉凭证</span>
|
||||
<div class="info-value">
|
||||
<div v-if="appealImages.length" class="img-list">
|
||||
<div
|
||||
v-for="(img, index) in appealImages"
|
||||
:key="index"
|
||||
class="demo-upload-list"
|
||||
@click="handleView(img)"
|
||||
>
|
||||
<img :src="img" loading="lazy" decoding="async" />
|
||||
<div class="demo-upload-list-cover">
|
||||
<el-icon @click="handleView(img)"><View /></el-icon>
|
||||
<el-icon><View /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>暂无</div>
|
||||
</div>
|
||||
<span v-else class="empty-text">暂无</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-alert class="l_title" show-icon type="warning">平台仲裁</el-alert>
|
||||
<table cellspacing="0" cellpadding='0' border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>仲裁意见</td>
|
||||
<td>{{ detail.arbitrationResult || '暂无' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="panel">
|
||||
<div class="panel-title">平台仲裁</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">仲裁意见</span>
|
||||
<span class="info-value">{{ detail.arbitrationResult || "暂无" }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-alert class="l_title" show-icon type="warning">对话详情</el-alert>
|
||||
<div class="speak-way" v-if="detail.orderComplaintCommunications && detail.orderComplaintCommunications.length">
|
||||
<div
|
||||
class="speak-msg seller"
|
||||
:class="{'speak-buyer': item.owner == 'BUYER', 'speak-platform': item.owner == 'PLATFORM', 'speak-seller': item.owner == 'STORE',}"
|
||||
:key="i"
|
||||
v-for="(item, i) in detail.orderComplaintCommunications"
|
||||
>
|
||||
{{
|
||||
item.owner == "PLATFORM"
|
||||
? "平台"
|
||||
: item.owner == "BUYER"
|
||||
? "买家"
|
||||
: "卖家"
|
||||
}}[{{ item.createTime }}]:
|
||||
<span>{{ item.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>暂无对话</div>
|
||||
<table cellspacing="0" cellpadding='0' border="1" v-if="detail.complainStatus!='COMPLETE'">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>回复:</td>
|
||||
<td><label>
|
||||
<input type="textarea" maxlength="200" :rows="4" clearable
|
||||
style="width:260px" v-model="params.content" />
|
||||
</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit" style="margin-left: 5px">
|
||||
<section class="panel">
|
||||
<div class="panel-title">对话详情</div>
|
||||
<div v-if="communications.length" class="chat-list">
|
||||
<div
|
||||
v-for="(item, i) in communications"
|
||||
:key="i"
|
||||
class="chat-item"
|
||||
:class="{
|
||||
'is-buyer': item.owner === 'BUYER',
|
||||
'is-platform': item.owner === 'PLATFORM',
|
||||
}"
|
||||
>
|
||||
<div class="chat-meta">{{ ownerName(item.owner) }} · {{ item.createTime }}</div>
|
||||
<div class="chat-bubble">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-text">暂无对话</div>
|
||||
|
||||
<div v-if="detail.complainStatus !== 'COMPLETE'" class="reply-box">
|
||||
<el-input
|
||||
v-model="params.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入回复内容"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
class="reply-btn"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
回复
|
||||
</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog title="图片预览" v-model="visible">
|
||||
<img :src="previewImage" loading="lazy" decoding="async" v-if="visible" style="width: 100%">
|
||||
</el-dialog>
|
||||
<el-dialog title="图片预览" v-model="visible">
|
||||
<img :src="previewImage" loading="lazy" decoding="async" v-if="visible" style="width: 100%" />
|
||||
</el-dialog>
|
||||
</UserCenterLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "@/utils/message";
|
||||
import { View } from '@element-plus/icons-vue';
|
||||
import {getComplainDetail} from '@/api/member.js';
|
||||
import {communication} from '@/api/order';
|
||||
import { View } from "@element-plus/icons-vue";
|
||||
import { getComplainDetail } from "@/api/member.js";
|
||||
import { communication } from "@/api/order";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const detail = ref({}); // 评价详情
|
||||
const detail = ref({}); // 投诉详情
|
||||
const visible = ref(false); // 图片预览
|
||||
const previewImage = ref(''); // 预览图片地址
|
||||
const loading = ref(false); // 加载状态
|
||||
const previewImage = ref(""); // 预览图片地址
|
||||
const submitLoading = ref(false); // 回复消息loading
|
||||
// 状态
|
||||
const statusLabel = {
|
||||
NO_APPLY: '未申请',
|
||||
APPLYING: '申请中',
|
||||
COMPLETE: '已完成',
|
||||
EXPIRED: '已失效',
|
||||
CANCEL: '已取消',
|
||||
NEW: '待审核'
|
||||
NO_APPLY: "未申请",
|
||||
APPLYING: "申请中",
|
||||
COMPLETE: "已完成",
|
||||
EXPIRED: "已失效",
|
||||
CANCEL: "已取消",
|
||||
NEW: "待审核",
|
||||
};
|
||||
// 商家回复内容
|
||||
const params = reactive({
|
||||
content: '',
|
||||
complainId: ''
|
||||
content: "",
|
||||
complainId: "",
|
||||
});
|
||||
|
||||
function getDetail () { // 获取投诉详情
|
||||
getComplainDetail(route.query.id).then(res => {
|
||||
if (res.success) detail.value = res.result
|
||||
})
|
||||
const statusText = computed(
|
||||
() => statusLabel[detail.value.complainStatus] || detail.value.complainStatus || ""
|
||||
);
|
||||
const statusTagType = computed(() => {
|
||||
const map = {
|
||||
COMPLETE: "success",
|
||||
APPLYING: "warning",
|
||||
NEW: "warning",
|
||||
CANCEL: "info",
|
||||
EXPIRED: "info",
|
||||
NO_APPLY: "info",
|
||||
};
|
||||
return map[detail.value.complainStatus] || "info";
|
||||
});
|
||||
const images = computed(() =>
|
||||
detail.value.images ? detail.value.images.split(",").filter(Boolean) : []
|
||||
);
|
||||
const appealImages = computed(() =>
|
||||
detail.value.appealImages ? detail.value.appealImages.split(",").filter(Boolean) : []
|
||||
);
|
||||
const communications = computed(() =>
|
||||
Array.isArray(detail.value.orderComplaintCommunications)
|
||||
? detail.value.orderComplaintCommunications
|
||||
: []
|
||||
);
|
||||
|
||||
function ownerName(owner) {
|
||||
return owner === "PLATFORM" ? "平台" : owner === "BUYER" ? "买家" : "卖家";
|
||||
}
|
||||
function goGoodsDetail (skuId, goodsId) { // 跳转商品详情
|
||||
|
||||
function getDetail() {
|
||||
// 获取投诉详情
|
||||
getComplainDetail(route.query.id).then((res) => {
|
||||
if (res.success) detail.value = res.result;
|
||||
});
|
||||
}
|
||||
function goGoodsDetail(skuId, goodsId) {
|
||||
// 跳转商品详情
|
||||
let routerUrl = router.resolve({
|
||||
path: '/goodsDetail',
|
||||
query: {skuId, goodsId}
|
||||
})
|
||||
window.open(routerUrl.href, '_blank')
|
||||
path: "/goodsDetail",
|
||||
query: { skuId, goodsId },
|
||||
});
|
||||
window.open(routerUrl.href, "_blank");
|
||||
}
|
||||
function handleView (name) { // 预览图片
|
||||
function handleView(name) {
|
||||
// 预览图片
|
||||
previewImage.value = name;
|
||||
visible.value = true;
|
||||
}
|
||||
// 回复消息
|
||||
function handleSubmit () {
|
||||
if (params.content === '') {
|
||||
Message.error('请填写对话内容');
|
||||
function handleSubmit() {
|
||||
if (params.content === "") {
|
||||
Message.error("请填写对话内容");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
@@ -187,64 +226,129 @@ function handleSubmit () {
|
||||
communication(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success('对话成功');
|
||||
params.content = '';
|
||||
Message.success("对话成功");
|
||||
params.content = "";
|
||||
getDetail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
getDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.speak-way {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
border: 1px solid #999;
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.speak-seller {
|
||||
background-color: #fff2c6;
|
||||
}
|
||||
.create-time {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.speak-platform {
|
||||
background-color: #ffe2d1;
|
||||
}
|
||||
.panel {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.speak-buyer {
|
||||
background-color: #c1d6d5;
|
||||
.panel-title {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.speak-msg {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
background-color: #eee;
|
||||
.info-label {
|
||||
flex: 0 0 80px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
background: #f5f5f5;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover .goods-name {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.img-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.demo-upload-list {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
background: #f5f5f5;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
|
||||
margin-right: 4px;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-upload-list img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.demo-upload-list-cover {
|
||||
@@ -254,51 +358,70 @@ onMounted(() => {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, .6);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.demo-upload-list:hover .demo-upload-list-cover {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.demo-upload-list-cover i {
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
.chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 4px;
|
||||
}
|
||||
|
||||
.icon-upload {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
line-height: 58px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
border: 1px dashed #999;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
.chat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 80%;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-color: $theme_color;
|
||||
}
|
||||
}
|
||||
&.is-buyer {
|
||||
align-self: flex-end;
|
||||
align-items: flex-end;
|
||||
|
||||
.l_title {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-color: #eee;
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 120px;
|
||||
color: #999;
|
||||
.chat-bubble {
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-platform .chat-bubble {
|
||||
background: #fdf3e4;
|
||||
color: #8a5a00;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-meta {
|
||||
margin-bottom: 4px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.reply-box {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.reply-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,63 +9,74 @@
|
||||
<span v-for="b in currentGradeBenefits" :key="b.id" class="benefit-tag">{{ b.benefitName || '-' }}</span>
|
||||
</div>
|
||||
<div class="grade-exp">当前经验值:<span class="num">{{ currentExperience || 0 }}</span></div>
|
||||
<div class="grade-next" v-if="nextGrade">
|
||||
距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值
|
||||
</div>
|
||||
<template v-if="nextGrade">
|
||||
<div class="grade-next">
|
||||
距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值
|
||||
</div>
|
||||
<el-progress
|
||||
class="grade-progress"
|
||||
:percentage="progressPercent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
color="#f0c46c"
|
||||
/>
|
||||
</template>
|
||||
<div class="grade-next" v-else>已达最高等级</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grade-tabs">
|
||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div>
|
||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'logs' }" @click="activeGradeTab = 'logs'">经验值记录</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeGradeTab === 'rules'" v-loading="rulesLoading">
|
||||
<empty v-if="!rulesLoading && !ruleList.length" />
|
||||
<div v-else class="rule-list">
|
||||
<div class="rule-item" v-for="row in ruleList" :key="row.ruleKey || row.id">
|
||||
<div class="rule-top">
|
||||
<div class="rule-name">{{ row.ruleName || row.ruleKey || '-' }}</div>
|
||||
<span class="rule-status" :class="{ done: row.completed === true }">{{ row.completed === true ? '已完成' : '未完成' }}</span>
|
||||
</div>
|
||||
<div class="rule-desc">{{ getRuleDesc(row.ruleKey) }}</div>
|
||||
<div class="rule-meta">
|
||||
<span>单次获得 <b>+{{ row.value == null ? 0 : row.value }}</b></span>
|
||||
<span v-if="row.maxValue != null && Number(row.maxValue) > 0">限额 <b>{{ row.maxValue }}</b></span>
|
||||
<span>已获得 <b>{{ row.gainedExperience == null ? 0 : row.gainedExperience }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grade-panel">
|
||||
<div class="grade-tabs">
|
||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div>
|
||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'logs' }" @click="activeGradeTab = 'logs'">经验值记录</div>
|
||||
</div>
|
||||
<div class="rules-desc" v-if="rulesDescription">{{ rulesDescription }}</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeGradeTab === 'logs'" v-loading="logLoading">
|
||||
<empty v-if="!(experienceLogs.records || []).length" />
|
||||
<div v-else class="log-list">
|
||||
<div class="log-head">
|
||||
<span>时间</span>
|
||||
<span>规则</span>
|
||||
<span>变动值</span>
|
||||
</div>
|
||||
<div class="log-row" v-for="(row, index) in experienceLogs.records" :key="row.id || index">
|
||||
<div class="log-time">{{ row.createTime }}</div>
|
||||
<div class="log-rule">{{ getRuleName(row.ruleKey) }}</div>
|
||||
<div class="log-point" :class="getLogDeltaSign(row) === '+' ? 'is-plus' : 'is-minus'">
|
||||
{{ getLogDeltaSign(row) }}{{ getLogDeltaAbsValue(row) }}
|
||||
<div v-show="activeGradeTab === 'rules'" v-loading="rulesLoading">
|
||||
<empty v-if="!rulesLoading && !ruleList.length" />
|
||||
<div v-else class="rule-list">
|
||||
<div class="rule-item" v-for="row in ruleList" :key="row.ruleKey || row.id">
|
||||
<div class="rule-top">
|
||||
<div class="rule-name">{{ row.ruleName || row.ruleKey || '-' }}</div>
|
||||
<span class="rule-status" :class="{ done: row.completed === true }">{{ row.completed === true ? '已完成' : '未完成' }}</span>
|
||||
</div>
|
||||
<div class="rule-desc">{{ getRuleDesc(row.ruleKey) }}</div>
|
||||
<div class="rule-meta">
|
||||
<span>单次获得 <b>+{{ row.value == null ? 0 : row.value }}</b></span>
|
||||
<span v-if="row.maxValue != null && Number(row.maxValue) > 0">限额 <b>{{ row.maxValue }}</b></span>
|
||||
<span>已获得 <b>{{ row.gainedExperience == null ? 0 : row.gainedExperience }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rules-desc" v-if="rulesDescription">{{ rulesDescription }}</div>
|
||||
</div>
|
||||
<div class="grade-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
v-model:page-size="params.pageSize"
|
||||
:total="experienceLogs.total || 0"
|
||||
@current-change="changePage"
|
||||
@size-change="changePageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
|
||||
<div v-show="activeGradeTab === 'logs'" v-loading="logLoading">
|
||||
<empty v-if="!(experienceLogs.records || []).length" />
|
||||
<div v-else class="log-list">
|
||||
<div class="log-head">
|
||||
<span>时间</span>
|
||||
<span>规则</span>
|
||||
<span>变动值</span>
|
||||
</div>
|
||||
<div class="log-row" v-for="(row, index) in experienceLogs.records" :key="row.id || index">
|
||||
<div class="log-time">{{ row.createTime }}</div>
|
||||
<div class="log-rule">{{ getRuleName(row.ruleKey) }}</div>
|
||||
<div class="log-point" :class="getLogDeltaSign(row) === '+' ? 'is-plus' : 'is-minus'">
|
||||
{{ getLogDeltaSign(row) }}{{ getLogDeltaAbsValue(row) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grade-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
v-model:page-size="params.pageSize"
|
||||
:total="experienceLogs.total || 0"
|
||||
@current-change="changePage"
|
||||
@size-change="changePageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UserCenterLayout>
|
||||
@@ -92,7 +103,6 @@ const currentGradeId = ref('')
|
||||
const selectedRuleKey = ref('')
|
||||
const activeGradeTab = ref('rules')
|
||||
const logLoading = ref(false)
|
||||
const gradeLoading = ref(false)
|
||||
const params = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
@@ -137,6 +147,15 @@ const diffExperience = computed(() => {
|
||||
return diff > 0 ? diff : 0
|
||||
})
|
||||
|
||||
/** 当前经验占下一级所需经验的百分比(无下一级时视为 100%) */
|
||||
const progressPercent = computed(() => {
|
||||
if (!nextGrade.value || !nextGrade.value.requiredExperience) return 100
|
||||
const required = Number(nextGrade.value.requiredExperience)
|
||||
if (!required || required <= 0) return 100
|
||||
const percent = Math.round((Number(currentExperience.value || 0) / required) * 100)
|
||||
return Math.min(100, Math.max(0, percent))
|
||||
})
|
||||
|
||||
/** 当前等级在列表详情中对应的启用权益(顺序与后台 benefits 一致) */
|
||||
const currentGradeBenefits = computed(() => {
|
||||
const id = currentGradeId.value
|
||||
@@ -174,7 +193,6 @@ function loadRules () {
|
||||
}
|
||||
|
||||
function loadGradeList () {
|
||||
gradeLoading.value = true
|
||||
memberGradeList()
|
||||
.then(res => {
|
||||
if (res && res.success) {
|
||||
@@ -197,9 +215,6 @@ function loadGradeList () {
|
||||
gradeList.value = []
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
gradeLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadCurrent () {
|
||||
@@ -223,12 +238,6 @@ function loadCurrent () {
|
||||
})
|
||||
}
|
||||
|
||||
function selectRule (ruleKey) {
|
||||
selectedRuleKey.value = ruleKey || ''
|
||||
params.pageNumber = 1
|
||||
loadCurrent()
|
||||
}
|
||||
|
||||
function changePage (val) {
|
||||
params.pageNumber = val
|
||||
loadCurrent()
|
||||
@@ -283,30 +292,31 @@ function getLogDeltaAbsValue (row) {
|
||||
const num = getLogDeltaRaw(row)
|
||||
return Math.abs(num)
|
||||
}
|
||||
|
||||
function getLogDeltaColor (row) {
|
||||
return getLogDeltaRaw(row) >= 0 ? 'green' : 'red'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.grade-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
gap: 20px;
|
||||
padding: 24px 28px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #3a4157 0%, #23262f 100%);
|
||||
box-shadow: 0 6px 20px rgba(35, 38, 47, 0.25);
|
||||
}
|
||||
|
||||
.grade-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.grade-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.grade-name-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -315,39 +325,57 @@ function getLogDeltaColor (row) {
|
||||
}
|
||||
|
||||
.grade-name {
|
||||
color: #171A26;
|
||||
font-size: 18px;
|
||||
color: #f0c46c;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.benefit-tag {
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: $theme_color;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(240, 196, 108, 0.4);
|
||||
border-radius: 10px;
|
||||
background: rgba(240, 196, 108, 0.12);
|
||||
color: #f0d9a8;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.grade-exp,
|
||||
.grade-next {
|
||||
margin-top: 6px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.num {
|
||||
color: $theme_color;
|
||||
color: #f0c46c;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.grade-progress {
|
||||
margin-top: 10px;
|
||||
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.grade-panel {
|
||||
margin-top: 16px;
|
||||
padding: 4px 20px 20px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.grade-tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin: 16px 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
>
|
||||
<empty v-if="list.length == 0" />
|
||||
<ul class="coupon-list" v-else>
|
||||
<li v-for="(item, index) in list" class="coupon-item" :key="index">
|
||||
<li
|
||||
v-for="(item, index) in list"
|
||||
class="coupon-item"
|
||||
:class="{ 'is-disabled': params.memberCouponStatus !== 'NEW' }"
|
||||
:key="index"
|
||||
>
|
||||
<div class="c-left">
|
||||
<div>
|
||||
<span
|
||||
@@ -176,34 +181,61 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../../assets/styles/coupon.scss";
|
||||
|
||||
.coupon-list {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.coupon-item {
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
min-height: 168px;
|
||||
margin-left: 0;
|
||||
min-height: 140px;
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||
$claim-width: 55px;
|
||||
|
||||
&:not(.is-disabled):hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.c-left {
|
||||
width: calc(100% - #{$claim-width});
|
||||
box-sizing: border-box;
|
||||
padding: 16px 16px 14px;
|
||||
background: #fff8f5;
|
||||
|
||||
.coupon-amount {
|
||||
font-size: 14px;
|
||||
|
||||
.price {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.describe {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
word-break: normal;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
p:nth-of-type(1) {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.coupon-expire {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
b {
|
||||
@@ -248,6 +280,7 @@ onMounted(() => {
|
||||
right: calc(#{$claim-width} - 9px);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-color: #eee;
|
||||
}
|
||||
|
||||
i.circle-top {
|
||||
@@ -257,6 +290,26 @@ onMounted(() => {
|
||||
i.circle-bottom {
|
||||
bottom: -9px;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
.c-left {
|
||||
background: #fafafa;
|
||||
|
||||
.coupon-amount,
|
||||
.price {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.describe {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #999 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pageration {
|
||||
|
||||
@@ -33,13 +33,16 @@
|
||||
<template v-if="!loading">
|
||||
<empty v-if="list.length === 0" />
|
||||
<div v-else class="gift-card-grid">
|
||||
<div v-for="item in list" :key="item.id || item.cardNo" class="gcc-card-item">
|
||||
<div v-for="item in list" :key="item.id || item.cardNo" class="gcc-card-item" :class="{ 'is-unavailable': statusTab === 'UNAVAILABLE' }">
|
||||
<div class="gcc-card-header">
|
||||
<div class="gcc-header-pattern" aria-hidden="true" />
|
||||
<div class="gcc-header-inner">
|
||||
<div class="gcc-header-left">
|
||||
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
|
||||
<div class="gcc-face">面值{{ formatYuan(item.faceValue) }}元</div>
|
||||
<div class="gcc-name-row">
|
||||
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
|
||||
<span class="gcc-status-chip">{{ statusChipLabel }}</span>
|
||||
</div>
|
||||
<div class="gcc-face"><span class="gcc-face-symbol">¥</span>{{ formatYuan(item.faceValue) }}</div>
|
||||
<div class="gcc-cardno">{{ item.cardNo }}</div>
|
||||
</div>
|
||||
<div class="gcc-header-right">
|
||||
@@ -146,6 +149,10 @@ const primaryActionDisabled = computed(() => {
|
||||
const pendingActivateLocked = computed(() => {
|
||||
return statusTab.value === "PENDING_ACTIVATION" && !!activatingId.value;
|
||||
});
|
||||
const statusChipLabel = computed(() => {
|
||||
const map = { AVAILABLE: "可用", UNAVAILABLE: "不可用", PENDING_ACTIVATION: "待激活" };
|
||||
return map[statusTab.value] || "";
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
@@ -303,8 +310,8 @@ function isActivating(item) {
|
||||
.my-gift-cards {
|
||||
.bind-section {
|
||||
padding: 16px 18px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
@include white_background_color();
|
||||
}
|
||||
.section-title {
|
||||
@@ -331,8 +338,8 @@ function isActivating(item) {
|
||||
}
|
||||
.gift-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px 18px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
.gcc-card-item {
|
||||
@@ -375,19 +382,38 @@ function isActivating(item) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.gcc-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.gcc-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.gcc-status-chip {
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.gcc-face {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0.95;
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.gcc-face-symbol {
|
||||
margin-right: 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.gcc-cardno {
|
||||
margin-top: 22px;
|
||||
font-size: 14px;
|
||||
margin-top: 14px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
opacity: 0.75;
|
||||
word-break: break-all;
|
||||
@@ -433,24 +459,32 @@ function isActivating(item) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
padding: 10px 16px;
|
||||
padding: 9px 16px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
color: #e95b6c;
|
||||
background: #faf4f5;
|
||||
color: #fff;
|
||||
background: $theme_color;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
}
|
||||
.gcc-action:hover:not(.is-disabled) {
|
||||
background: #f5e8ea;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.gcc-action.is-disabled {
|
||||
color: #c5c8ce;
|
||||
cursor: not-allowed;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.gcc-card-item.is-unavailable {
|
||||
.gcc-card-header {
|
||||
background: linear-gradient(125deg, #c9c9c9 0%, #a8a8a8 100%);
|
||||
}
|
||||
.gcc-strong {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.page-size {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
Reference in New Issue
Block a user