docs(api): 添加产品动态时间线前端API文档
- 新增产品动态时间线接口文档,明确前端调用规范 - 定义接口请求参数、响应结构和字段语义说明 - 提供请求示例和错误码说明 - 添加左侧筛选项映射规则和时间格式说明 feat(product): 实现产品首页动态时间线功能 - 重构产品首页布局结构,采用档案横幅型设计 - 新增对象基础概述横幅模块 - 实现产品动态时间线面板组件 - 集成需求池管理概览和最近变化区域 - 添加扩展信息区预留模块位 chore(docs): 更新代理工作说明和前端测试策略 - 添加前端任务测试策略说明 - 更新代理工作流程规范 - 明确git操作执行边界 - 优化组件类型声明更新
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
<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">{{ item.compactText }}</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%);
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.product-activity-panel__body {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.product-activity-panel__header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user