286 lines
6.9 KiB
Vue
286 lines
6.9 KiB
Vue
<script setup lang="ts">
|
||
import { ref, watch } from 'vue';
|
||
import { fetchGetProductActivityTimelinePage } from '@/service/api';
|
||
import {
|
||
DEFAULT_PRODUCT_ACTIVITY_PAGE_SIZE,
|
||
buildProductActivityDisplayItems,
|
||
buildProductActivityRange,
|
||
formatProductActivityTime
|
||
} from '../product-activity';
|
||
import ProductActivityTimelineDialog from './product-activity-timeline-dialog.vue';
|
||
|
||
defineOptions({ name: 'ProductActivityTimelinePanel' });
|
||
|
||
interface Props {
|
||
productId: string;
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'latest-time-change', value: string): void;
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
const emit = defineEmits<Emits>();
|
||
|
||
const dialogVisible = ref(false);
|
||
const loading = ref(false);
|
||
const loadError = ref(false);
|
||
const items = ref<ReturnType<typeof buildProductActivityDisplayItems>>([]);
|
||
|
||
async function loadRecentActivities() {
|
||
if (!props.productId) {
|
||
items.value = [];
|
||
loadError.value = false;
|
||
emit('latest-time-change', '');
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
loadError.value = false;
|
||
|
||
try {
|
||
const [startTime, endTime] = buildProductActivityRange(30);
|
||
const result = await fetchGetProductActivityTimelinePage(props.productId, {
|
||
pageNo: 1,
|
||
pageSize: DEFAULT_PRODUCT_ACTIVITY_PAGE_SIZE,
|
||
startTime,
|
||
endTime
|
||
});
|
||
|
||
if (result.error || !result.data) {
|
||
loadError.value = true;
|
||
items.value = [];
|
||
emit('latest-time-change', '');
|
||
return;
|
||
}
|
||
|
||
items.value = buildProductActivityDisplayItems(result.data.list);
|
||
emit('latest-time-change', formatProductActivityTime(result.data.list[0]?.occurredAt) || '');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function openDialog() {
|
||
if (!props.productId) {
|
||
return;
|
||
}
|
||
|
||
dialogVisible.value = true;
|
||
}
|
||
|
||
watch(
|
||
() => props.productId,
|
||
async () => {
|
||
await loadRecentActivities();
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<ElCard class="product-activity-panel card-wrapper">
|
||
<template #header>
|
||
<div class="product-activity-panel__header">
|
||
<div>
|
||
<h3 class="product-activity-panel__title">产品动态时间线</h3>
|
||
</div>
|
||
|
||
<ElButton text type="primary" :disabled="!productId" @click="openDialog">更多</ElButton>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-loading="loading" class="product-activity-panel__body">
|
||
<div v-if="loadError" class="product-activity-panel__state">
|
||
<ElEmpty description="产品动态加载失败" :image-size="88" />
|
||
<ElButton type="primary" plain @click="loadRecentActivities">重新加载</ElButton>
|
||
</div>
|
||
|
||
<div v-else-if="items.length" class="product-activity-panel__timeline">
|
||
<article v-for="item in items" :key="item.id" class="product-activity-panel__item">
|
||
<div class="product-activity-panel__rail">
|
||
<span class="product-activity-panel__dot" :class="`product-activity-panel__dot--${item.tone}`" />
|
||
<span class="product-activity-panel__line" />
|
||
</div>
|
||
|
||
<div class="product-activity-panel__content">
|
||
<div class="product-activity-panel__meta">
|
||
<div class="product-activity-panel__meta-main">
|
||
<ElTag effect="plain" size="small">{{ item.tagLabel }}</ElTag>
|
||
</div>
|
||
<span class="product-activity-panel__time">{{ item.timeText }}</span>
|
||
</div>
|
||
|
||
<p class="product-activity-panel__sentence">
|
||
<span class="product-activity-panel__sentence-main">
|
||
<template v-for="(part, index) in item.compactTextParts" :key="`${item.id}-${index}`">
|
||
<strong v-if="part.strong" class="product-activity-panel__subject">{{ part.text }}</strong>
|
||
<span v-else>{{ part.text }}</span>
|
||
</template>
|
||
</span>
|
||
<span v-if="item.statusTransition">,状态:{{ item.statusTransition }}</span>
|
||
<span v-if="item.reasonText">,原因:{{ item.reasonText }}</span>
|
||
</p>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<ElEmpty v-else description="当前最近30天暂无可展示的产品动态" :image-size="88" />
|
||
</div>
|
||
</ElCard>
|
||
|
||
<ProductActivityTimelineDialog v-model:visible="dialogVisible" :product-id="productId" />
|
||
</template>
|
||
|
||
<style scoped>
|
||
.product-activity-panel {
|
||
overflow: hidden;
|
||
}
|
||
|
||
.product-activity-panel__header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.product-activity-panel__title {
|
||
margin: 0;
|
||
color: rgb(15 23 42 / 98%);
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.product-activity-panel__desc {
|
||
margin: 4px 0 0;
|
||
color: rgb(100 116 139 / 92%);
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.product-activity-panel__body {
|
||
min-height: 520px;
|
||
}
|
||
|
||
.product-activity-panel__state {
|
||
display: flex;
|
||
min-height: 420px;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.product-activity-panel__timeline {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.product-activity-panel__item {
|
||
display: grid;
|
||
grid-template-columns: 20px minmax(0, 1fr);
|
||
gap: 12px;
|
||
}
|
||
|
||
.product-activity-panel__rail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.product-activity-panel__dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
margin-top: 6px;
|
||
border-radius: 999px;
|
||
box-shadow: 0 0 0 4px rgb(255 255 255 / 96%);
|
||
}
|
||
|
||
.product-activity-panel__dot--sky {
|
||
background-color: rgb(14 165 233 / 88%);
|
||
}
|
||
|
||
.product-activity-panel__dot--emerald {
|
||
background-color: rgb(5 150 105 / 88%);
|
||
}
|
||
|
||
.product-activity-panel__dot--amber {
|
||
background-color: rgb(217 119 6 / 88%);
|
||
}
|
||
|
||
.product-activity-panel__dot--rose {
|
||
background-color: rgb(225 29 72 / 88%);
|
||
}
|
||
|
||
.product-activity-panel__dot--slate {
|
||
background-color: rgb(100 116 139 / 88%);
|
||
}
|
||
|
||
.product-activity-panel__line {
|
||
flex: 1;
|
||
width: 2px;
|
||
min-height: 24px;
|
||
margin-top: 4px;
|
||
background: linear-gradient(180deg, rgb(203 213 225 / 96%), rgb(226 232 240 / 28%));
|
||
}
|
||
|
||
.product-activity-panel__item:last-child .product-activity-panel__line {
|
||
opacity: 0;
|
||
}
|
||
|
||
.product-activity-panel__content {
|
||
padding: 10px 12px;
|
||
border: 1px solid rgb(226 232 240 / 92%);
|
||
border-radius: 16px;
|
||
background-color: rgb(255 255 255 / 98%);
|
||
}
|
||
|
||
.product-activity-panel__meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.product-activity-panel__meta-main {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.product-activity-panel__time {
|
||
color: rgb(100 116 139 / 90%);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.product-activity-panel__sentence {
|
||
margin: 6px 0 0;
|
||
color: rgb(71 85 105 / 94%);
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.product-activity-panel__sentence-main {
|
||
color: rgb(15 23 42 / 98%);
|
||
}
|
||
|
||
.product-activity-panel__subject {
|
||
color: rgb(15 23 42 / 98%);
|
||
font-weight: 700;
|
||
}
|
||
|
||
@media (width <= 768px) {
|
||
.product-activity-panel__body {
|
||
min-height: auto;
|
||
}
|
||
|
||
.product-activity-panel__header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
</style>
|