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> <template>
<div class="add-eval"> <div class="complain-detail">
<UserCenterLayout> <UserCenterLayout>
<div class="title"> <div class="detail-head">
<p> <el-tag :type="statusTagType" effect="dark">{{ statusText }}</el-tag>
<span class="fontsize_16 global_color">{{ statusLabel[detail.complainStatus] }}</span> <span class="create-time">创建时间:{{ detail.createTime || "-" }}</span>
<span class="color999 ml_20">创建时间:</span><span>{{ detail.createTime }}</span>
<span class="color999 ml_20">{{ detail.createTime }}</span>
</p>
</div> </div>
<el-alert class="l_title" show-icon type="warning">我的申诉信息</el-alert> <section class="panel">
<table cellspacing="0" cellpadding='0' border="1"> <div class="panel-title">我的申诉信息</div>
<tbody> <div class="info-row">
<tr> <span class="info-label">投诉商品</span>
<td>投诉商品</td> <div class="info-value">
<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> <button
</tr> v-if="detail.goodsId"
<tr> type="button"
<td>投诉主题</td> class="goods-info"
<td>{{ detail.complainTopic }}</td> @click="goGoodsDetail(detail.skuId, detail.goodsId)"
</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">
<div class="demo-upload-list-cover">
<el-icon @click="handleView(img)"><View /></el-icon>
</div>
</div>
</template>
<div v-else>暂无</div>
</div>
</td>
</tr>
</tbody>
</table>
<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">
<div class="demo-upload-list-cover">
<el-icon @click="handleView(img)"><View /></el-icon>
</div>
</div>
</template>
<div v-else>暂无</div>
</div>
</td>
</tr>
</tbody>
</table>
<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>
<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"
> >
{{ <img :src="detail.goodsImage" loading="lazy" decoding="async" alt="" />
item.owner == "PLATFORM" <span class="goods-name">{{ detail.goodsName }}</span>
? "平台" </button>
: item.owner == "BUYER" <span v-else class="empty-text">暂无</span>
? "买家"
: "卖家"
}}[{{ item.createTime }}]:
<span>{{ item.content }}</span>
</div> </div>
</div> </div>
<div v-else>暂无对话</div> <div class="info-row">
<table cellspacing="0" cellpadding='0' border="1" v-if="detail.complainStatus!='COMPLETE'"> <span class="info-label">投诉主题</span>
<tbody> <span class="info-value">{{ detail.complainTopic || "暂无" }}</span>
<tr> </div>
<td>回复:</td> <div class="info-row">
<td><label> <span class="info-label">投诉内容</span>
<input type="textarea" maxlength="200" :rows="4" clearable <span class="info-value">{{ detail.content || "暂无" }}</span>
style="width:260px" v-model="params.content" /> </div>
</label></td> <div class="info-row">
</tr> <span class="info-label">补充内容</span>
<tr> <div class="info-value">
<td></td> <div v-if="images.length" class="img-list">
<td> <div
<el-button type="primary" :loading="submitLoading" @click="handleSubmit" style="margin-left: 5px"> 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><View /></el-icon>
</div>
</div>
</div>
<span v-else class="empty-text">暂无</span>
</div>
</div>
</section>
<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><View /></el-icon>
</div>
</div>
</div>
<span v-else class="empty-text">暂无</span>
</div>
</div>
</section>
<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>
<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> </el-button>
</td> </div>
</tr> </section>
</tbody>
</table>
<el-dialog title="图片预览" v-model="visible"> <el-dialog title="图片预览" v-model="visible">
<img :src="previewImage" loading="lazy" decoding="async" v-if="visible" style="width: 100%"> <img :src="previewImage" loading="lazy" decoding="async" v-if="visible" style="width: 100%" />
</el-dialog> </el-dialog>
</UserCenterLayout> </UserCenterLayout>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { View } from '@element-plus/icons-vue'; import { View } from "@element-plus/icons-vue";
import {getComplainDetail} from '@/api/member.js'; import { getComplainDetail } from "@/api/member.js";
import {communication} from '@/api/order'; import { communication } from "@/api/order";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const detail = ref({}); // 评价详情 const detail = ref({}); // 投诉详情
const visible = ref(false); // 图片预览 const visible = ref(false); // 图片预览
const previewImage = ref(''); // 预览图片地址 const previewImage = ref(""); // 预览图片地址
const loading = ref(false); // 加载状态
const submitLoading = ref(false); // 回复消息loading const submitLoading = ref(false); // 回复消息loading
// 状态 // 状态
const statusLabel = { const statusLabel = {
NO_APPLY: '未申请', NO_APPLY: "未申请",
APPLYING: '申请中', APPLYING: "申请中",
COMPLETE: '已完成', COMPLETE: "已完成",
EXPIRED: '已失效', EXPIRED: "已失效",
CANCEL: '已取消', CANCEL: "已取消",
NEW: '待审核' NEW: "待审核",
}; };
// 商家回复内容 // 商家回复内容
const params = reactive({ const params = reactive({
content: '', content: "",
complainId: '' complainId: "",
}); });
function getDetail () { // 获取投诉详情 const statusText = computed(
getComplainDetail(route.query.id).then(res => { () => statusLabel[detail.value.complainStatus] || detail.value.complainStatus || ""
if (res.success) detail.value = res.result );
}) 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({ let routerUrl = router.resolve({
path: '/goodsDetail', path: "/goodsDetail",
query: {skuId, goodsId} query: { skuId, goodsId },
}) });
window.open(routerUrl.href, '_blank') window.open(routerUrl.href, "_blank");
} }
function handleView (name) { // 预览图片 function handleView(name) {
// 预览图片
previewImage.value = name; previewImage.value = name;
visible.value = true; visible.value = true;
} }
// 回复消息 // 回复消息
function handleSubmit () { function handleSubmit() {
if (params.content === '') { if (params.content === "") {
Message.error('请填写对话内容'); Message.error("请填写对话内容");
return; return;
} }
submitLoading.value = true; submitLoading.value = true;
@@ -187,64 +226,129 @@ function handleSubmit () {
communication(params).then((res) => { communication(params).then((res) => {
submitLoading.value = false; submitLoading.value = false;
if (res.success) { if (res.success) {
Message.success('对话成功'); Message.success("对话成功");
params.content = ''; params.content = "";
getDetail(); getDetail();
} }
}); });
} }
onMounted(() => { onMounted(() => {
getDetail() getDetail();
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.speak-way { .detail-head {
background-color: #fff; display: flex;
width: 100%; align-items: center;
border: 1px solid #999; gap: 12px;
padding-bottom: 4px;
}
.speak-seller { .create-time {
background-color: #fff2c6; color: #999;
} font-size: 14px;
}
.speak-platform { .panel {
background-color: #ffe2d1; margin-top: 16px;
} border: 1px solid #eee;
border-radius: 8px;
background: #fff;
padding: 16px 20px;
}
.speak-buyer { .panel-title {
background-color: #c1d6d5; 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 { .info-label {
height: 40px; flex: 0 0 80px;
line-height: 40px; color: #999;
padding: 0 5px; font-size: 14px;
border-radius: 5px; line-height: 22px;
margin: 5px; }
background-color: #eee;
.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 { .demo-upload-list {
display: inline-block;
width: 100px; width: 100px;
height: 100px; height: 100px;
text-align: center; border-radius: 6px;
line-height: 100px;
border: 1px solid transparent;
border-radius: 4px;
overflow: hidden; overflow: hidden;
background: #fff; background: #f5f5f5;
position: relative; position: relative;
box-shadow: 0 1px 1px rgba(0, 0, 0, .2); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
margin-right: 4px; cursor: pointer;
margin-top: 10px;
} }
.demo-upload-list img { .demo-upload-list img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover;
} }
.demo-upload-list-cover { .demo-upload-list-cover {
@@ -254,51 +358,70 @@ onMounted(() => {
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 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 { .demo-upload-list:hover .demo-upload-list-cover {
display: block; display: flex;
} }
.demo-upload-list-cover i { .chat-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 4px;
}
.chat-item {
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 80%;
&.is-buyer {
align-self: flex-end;
align-items: flex-end;
.chat-bubble {
background: $theme_color;
color: #fff; color: #fff;
font-size: 30px; }
cursor: pointer; }
margin: 0 2px;
}
.icon-upload { &.is-platform .chat-bubble {
width: 58px; background: #fdf3e4;
height: 58px; color: #8a5a00;
line-height: 58px;
text-align: center;
display: inline-block;
border: 1px dashed #999;
border-radius: 4px;
margin-top: 10px;
&:hover {
cursor: pointer;
border-color: $theme_color;
} }
} }
.l_title { .chat-meta {
margin: 10px 0; margin-bottom: 4px;
}
table {
width: 100%;
border-color: #eee;
td {
padding: 10px;
&:nth-child(1) {
width: 120px;
color: #999; 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> </style>

View File

@@ -9,13 +9,23 @@
<span v-for="b in currentGradeBenefits" :key="b.id" class="benefit-tag">{{ b.benefitName || '-' }}</span> <span v-for="b in currentGradeBenefits" :key="b.id" class="benefit-tag">{{ b.benefitName || '-' }}</span>
</div> </div>
<div class="grade-exp">当前经验值:<span class="num">{{ currentExperience || 0 }}</span></div> <div class="grade-exp">当前经验值:<span class="num">{{ currentExperience || 0 }}</span></div>
<div class="grade-next" v-if="nextGrade"> <template v-if="nextGrade">
<div class="grade-next">
距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值 距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值
</div> </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 class="grade-next" v-else>已达最高等级</div>
</div> </div>
</div> </div>
<div class="grade-panel">
<div class="grade-tabs"> <div class="grade-tabs">
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div> <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 class="grade-tab" :class="{ active: activeGradeTab === 'logs' }" @click="activeGradeTab = 'logs'">经验值记录</div>
@@ -68,6 +78,7 @@
/> />
</div> </div>
</div> </div>
</div>
</UserCenterLayout> </UserCenterLayout>
</div> </div>
@@ -92,7 +103,6 @@ const currentGradeId = ref('')
const selectedRuleKey = ref('') const selectedRuleKey = ref('')
const activeGradeTab = ref('rules') const activeGradeTab = ref('rules')
const logLoading = ref(false) const logLoading = ref(false)
const gradeLoading = ref(false)
const params = reactive({ const params = reactive({
pageNumber: 1, pageNumber: 1,
pageSize: 20 pageSize: 20
@@ -137,6 +147,15 @@ const diffExperience = computed(() => {
return diff > 0 ? diff : 0 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 一致) */ /** 当前等级在列表详情中对应的启用权益(顺序与后台 benefits 一致) */
const currentGradeBenefits = computed(() => { const currentGradeBenefits = computed(() => {
const id = currentGradeId.value const id = currentGradeId.value
@@ -174,7 +193,6 @@ function loadRules () {
} }
function loadGradeList () { function loadGradeList () {
gradeLoading.value = true
memberGradeList() memberGradeList()
.then(res => { .then(res => {
if (res && res.success) { if (res && res.success) {
@@ -197,9 +215,6 @@ function loadGradeList () {
gradeList.value = [] gradeList.value = []
} }
}) })
.finally(() => {
gradeLoading.value = false
})
} }
function loadCurrent () { function loadCurrent () {
@@ -223,12 +238,6 @@ function loadCurrent () {
}) })
} }
function selectRule (ruleKey) {
selectedRuleKey.value = ruleKey || ''
params.pageNumber = 1
loadCurrent()
}
function changePage (val) { function changePage (val) {
params.pageNumber = val params.pageNumber = val
loadCurrent() loadCurrent()
@@ -283,30 +292,31 @@ function getLogDeltaAbsValue (row) {
const num = getLogDeltaRaw(row) const num = getLogDeltaRaw(row)
return Math.abs(num) return Math.abs(num)
} }
function getLogDeltaColor (row) {
return getLogDeltaRaw(row) >= 0 ? 'green' : 'red'
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.grade-card { .grade-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 20px;
margin-bottom: 8px; padding: 24px 28px;
padding: 16px; border-radius: 12px;
background: #fafafa; background: linear-gradient(135deg, #3a4157 0%, #23262f 100%);
border-radius: 8px; box-shadow: 0 6px 20px rgba(35, 38, 47, 0.25);
} }
.grade-icon { .grade-icon {
width: 56px; width: 64px;
height: 56px; height: 64px;
object-fit: contain; object-fit: contain;
flex-shrink: 0; flex-shrink: 0;
} }
.grade-info {
flex: 1;
min-width: 0;
}
.grade-name-row { .grade-name-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -315,39 +325,57 @@ function getLogDeltaColor (row) {
} }
.grade-name { .grade-name {
color: #171A26; color: #f0c46c;
font-size: 18px; font-size: 20px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
letter-spacing: 1px;
} }
.benefit-tag { .benefit-tag {
padding: 0 8px; padding: 0 10px;
border-radius: 4px; border: 1px solid rgba(240, 196, 108, 0.4);
background: #fff; border-radius: 10px;
color: $theme_color; background: rgba(240, 196, 108, 0.12);
font-size: 14px; color: #f0d9a8;
line-height: 22px; font-size: 12px;
line-height: 20px;
} }
.grade-exp, .grade-exp,
.grade-next { .grade-next {
margin-top: 6px; margin-top: 8px;
color: #666; color: rgba(255, 255, 255, 0.75);
font-size: 14px; font-size: 14px;
line-height: 22px; line-height: 22px;
} }
.num { .num {
color: $theme_color; color: #f0c46c;
font-size: 16px; font-size: 16px;
font-weight: 600; 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 { .grade-tabs {
display: flex; display: flex;
gap: 24px; gap: 24px;
margin: 16px 0; margin-bottom: 16px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
} }

View File

@@ -7,7 +7,12 @@
> >
<empty v-if="list.length == 0" /> <empty v-if="list.length == 0" />
<ul class="coupon-list" v-else> <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 class="c-left">
<div> <div>
<span <span
@@ -176,34 +181,61 @@ onMounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../../assets/styles/coupon.scss"; @import "../../../assets/styles/coupon.scss";
.coupon-list {
gap: 16px;
}
.coupon-item { .coupon-item {
overflow: hidden; overflow: hidden;
height: auto; height: auto;
min-height: 168px; min-height: 140px;
margin-left: 0; margin: 0;
border-radius: 8px;
transition: box-shadow 0.2s ease, transform 0.2s ease;
$claim-width: 55px; $claim-width: 55px;
&:not(.is-disabled):hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.c-left { .c-left {
width: calc(100% - #{$claim-width}); width: calc(100% - #{$claim-width});
box-sizing: border-box; box-sizing: border-box;
padding: 16px 16px 14px; padding: 16px 16px 14px;
background: #fff8f5;
.coupon-amount { .coupon-amount {
font-size: 14px; font-size: 14px;
.price {
font-size: 28px;
font-weight: 700;
}
} }
.describe { .describe {
font-size: 14px; font-size: 12px;
} }
p { p {
font-size: 14px; font-size: 13px;
overflow: visible; overflow: visible;
text-overflow: unset; text-overflow: unset;
white-space: normal; white-space: normal;
word-break: break-all; word-break: normal;
line-height: 20px; line-height: 20px;
} }
p:nth-of-type(1) {
font-weight: 500;
color: #333;
}
.coupon-expire {
font-size: 12px;
}
} }
b { b {
@@ -248,6 +280,7 @@ onMounted(() => {
right: calc(#{$claim-width} - 9px); right: calc(#{$claim-width} - 9px);
width: 18px; width: 18px;
height: 18px; height: 18px;
border-color: #eee;
} }
i.circle-top { i.circle-top {
@@ -257,6 +290,26 @@ onMounted(() => {
i.circle-bottom { i.circle-bottom {
bottom: -9px; bottom: -9px;
} }
&.is-disabled {
.c-left {
background: #fafafa;
.coupon-amount,
.price {
color: #999;
}
.describe {
background: #f0f0f0;
color: #999;
}
p {
color: #999 !important;
}
}
}
} }
.pageration { .pageration {

View File

@@ -33,13 +33,16 @@
<template v-if="!loading"> <template v-if="!loading">
<empty v-if="list.length === 0" /> <empty v-if="list.length === 0" />
<div v-else class="gift-card-grid"> <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-card-header">
<div class="gcc-header-pattern" aria-hidden="true" /> <div class="gcc-header-pattern" aria-hidden="true" />
<div class="gcc-header-inner"> <div class="gcc-header-inner">
<div class="gcc-header-left"> <div class="gcc-header-left">
<div class="gcc-name-row">
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div> <div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
<div class="gcc-face">面值{{ formatYuan(item.faceValue) }}元</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 class="gcc-cardno">{{ item.cardNo }}</div>
</div> </div>
<div class="gcc-header-right"> <div class="gcc-header-right">
@@ -146,6 +149,10 @@ const primaryActionDisabled = computed(() => {
const pendingActivateLocked = computed(() => { const pendingActivateLocked = computed(() => {
return statusTab.value === "PENDING_ACTIVATION" && !!activatingId.value; return statusTab.value === "PENDING_ACTIVATION" && !!activatingId.value;
}); });
const statusChipLabel = computed(() => {
const map = { AVAILABLE: "可用", UNAVAILABLE: "不可用", PENDING_ACTIVATION: "待激活" };
return map[statusTab.value] || "";
});
onMounted(() => { onMounted(() => {
getList(); getList();
@@ -303,8 +310,8 @@ function isActivating(item) {
.my-gift-cards { .my-gift-cards {
.bind-section { .bind-section {
padding: 16px 18px; padding: 16px 18px;
border-radius: 4px; border-radius: 8px;
border: 1px solid #e8eaec; border: 1px solid #eee;
@include white_background_color(); @include white_background_color();
} }
.section-title { .section-title {
@@ -331,8 +338,8 @@ function isActivating(item) {
} }
.gift-card-grid { .gift-card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px 18px; gap: 16px;
align-items: start; align-items: start;
} }
.gcc-card-item { .gcc-card-item {
@@ -375,19 +382,38 @@ function isActivating(item) {
padding-right: 8px; padding-right: 8px;
} }
.gcc-title { .gcc-title {
font-size: 17px; font-size: 14px;
font-weight: 700; font-weight: 600;
line-height: 1.3; line-height: 1.3;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); 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 { .gcc-face {
margin-top: 8px; margin-top: 10px;
font-size: 14px; font-size: 26px;
opacity: 0.95; font-weight: 700;
line-height: 1.2;
}
.gcc-face-symbol {
margin-right: 2px;
font-size: 15px;
font-weight: 500;
} }
.gcc-cardno { .gcc-cardno {
margin-top: 22px; margin-top: 14px;
font-size: 14px; font-size: 12px;
letter-spacing: 0.02em; letter-spacing: 0.02em;
opacity: 0.75; opacity: 0.75;
word-break: break-all; word-break: break-all;
@@ -433,24 +459,32 @@ function isActivating(item) {
display: block; display: block;
width: 100%; width: 100%;
margin-top: 14px; margin-top: 14px;
padding: 10px 16px; padding: 9px 16px;
border: none; border: none;
border-radius: 999px; border-radius: 999px;
font-size: 15px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
color: #e95b6c; color: #fff;
background: #faf4f5; background: $theme_color;
transition: opacity 0.2s, background 0.2s; transition: opacity 0.2s, background 0.2s;
} }
.gcc-action:hover:not(.is-disabled) { .gcc-action:hover:not(.is-disabled) {
background: #f5e8ea; opacity: 0.9;
} }
.gcc-action.is-disabled { .gcc-action.is-disabled {
color: #c5c8ce; color: #c5c8ce;
cursor: not-allowed; cursor: not-allowed;
background: #f7f7f7; background: #f7f7f7;
} }
.gcc-card-item.is-unavailable {
.gcc-card-header {
background: linear-gradient(125deg, #c9c9c9 0%, #a8a8a8 100%);
}
.gcc-strong {
color: #999;
}
}
.page-size { .page-size {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;