Files
cn-rdms-web/src/views/product/dashboard/modules/product-activity-timeline-panel.vue

286 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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