style(member): 优化申诉详情、会员等级、优惠券、礼品卡页面样式

This commit is contained in:
pikachu1995@126.com
2026-10-26 17:01:08 +08:00
parent 99e7f6ed47
commit 1f872254cc
4 changed files with 550 additions and 312 deletions

View File

@@ -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=""> &nbsp;{{ 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>

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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;