项目中心
This commit is contained in:
@@ -2,27 +2,32 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type {
|
||||
AfterSalesAttachment,
|
||||
AfterSalesPayload,
|
||||
Project,
|
||||
ProjectQuery,
|
||||
ProjectStatistics,
|
||||
ProjectTypeCount,
|
||||
RdProjectVo,
|
||||
RequirementDetailBundle,
|
||||
} from '@/types/business/project'
|
||||
import {
|
||||
cancelAfterSales as cancelAfterSalesApi,
|
||||
deleteProjectFile,
|
||||
getProjectDetail,
|
||||
getCompletedProjects,
|
||||
getProjectDetailBundle,
|
||||
getProjectDetailTab,
|
||||
getProjectFilterConditions,
|
||||
getProjectList,
|
||||
submitAfterSales as submitAfterSalesApi,
|
||||
uploadProjectFile,
|
||||
uploadAfterSalesFile as uploadAfterSalesFileApi,
|
||||
} from '@/api/project'
|
||||
import type { ProjectDetailTab } from '@/api/project'
|
||||
import { toProjectDetail } from '@/utils/projectVo'
|
||||
|
||||
function createDefaultQuery(): ProjectQuery {
|
||||
return {
|
||||
keyword: '',
|
||||
businessType: '',
|
||||
businessTypes: [],
|
||||
status: '',
|
||||
timeShortcut: 'all',
|
||||
startDate: '',
|
||||
@@ -65,6 +70,31 @@ export const useProjectStore = defineStore('project', () => {
|
||||
/** 业务类型筛选条件是否已拉过(值域在运行期不变,进页面拉一次即可) */
|
||||
let filterConditionsLoaded = false
|
||||
|
||||
/**
|
||||
* 已完成项目(**申请售后页「项目名称」下拉的候选**)。
|
||||
*
|
||||
* <p>口径见 `api/project.ts#getCompletedProjects`:自己名下已立项且状态为 COMPLETED 的项目,
|
||||
* 与后端「只有已完成的项目可以申请售后」的准入校验一致。行的 `id` 就是申请接口要传的需求 id。</p>
|
||||
*
|
||||
* <p>与业务类型的筛选条件不同,这个值域**会随项目状态变化**,所以不缓存、
|
||||
* 每次进申请页都重拉(见 {@link fetchCompletedProjects})。</p>
|
||||
*/
|
||||
const completedProjects = ref<RdProjectVo[]>([])
|
||||
|
||||
/** 详情原始 bundle(4 支接口的响应);切页签时按页签替换其中一支后重新映射 */
|
||||
const detailBundle = ref<RequirementDetailBundle | null>(null)
|
||||
/** 详情请求序号:需求 id 一变就自增,用来丢弃在途的「上一个需求」的响应 */
|
||||
let detailSeq = 0
|
||||
/** 各页签自己的请求序号:同一页签连点只应用最后一次响应 */
|
||||
const tabSeq: Record<ProjectDetailTab, number> = {
|
||||
progress: 0,
|
||||
files: 0,
|
||||
finance: 0,
|
||||
afterSales: 0,
|
||||
}
|
||||
/** 正在刷新的页签('' = 无);页签按钮据此显示加载态 */
|
||||
const loadingTab = ref<ProjectDetailTab | ''>('')
|
||||
|
||||
/**
|
||||
* 取业务类型筛选条件(左侧筛选栏「业务类型」的值域 + 件数)。
|
||||
*
|
||||
@@ -97,21 +127,98 @@ export const useProjectStore = defineStore('project', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 取详情(真实接口)。
|
||||
* 取「已完成项目」(申请售后页「项目名称」下拉的值域)。
|
||||
*
|
||||
* <p>后端在「需求不存在」或「不属于当前用户」时返回 `data: null` ⇒ 页面显示空态;
|
||||
* 请求本身失败(如 500)也归为 null,不让异常冒到组件里。</p>
|
||||
* <p>失败只告警不清空:保留上一次的结果,页面下拉至少还有数据;首次即失败则为空数组,
|
||||
* 申请页会退化成只显示当前项目的单项下拉(见 `after-sales.vue` 的兜底合并)。</p>
|
||||
*/
|
||||
async function fetchDetail(id: string): Promise<void> {
|
||||
async function fetchCompletedProjects(): Promise<void> {
|
||||
try {
|
||||
current.value = await getProjectDetail(id)
|
||||
completedProjects.value = await getCompletedProjects()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('[project] 已完成项目列表加载失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取详情(**首屏**:4 支并行,一次到位)。
|
||||
*
|
||||
* <p>后端在「需求不存在」或「不属于当前用户」时 4 支都返回 `data: null` ⇒ 页面显示空态;
|
||||
* 请求本身失败(如 500)也归为 null,不让异常冒到组件里。</p>
|
||||
*
|
||||
* <p>原始响应同时缓存进 `detailBundle`,之后切页签只替换其中一支(见 {@link refreshTab}),
|
||||
* 不必重拉另外三支。</p>
|
||||
*/
|
||||
async function fetchDetail(id: string): Promise<void> {
|
||||
const seq = ++detailSeq
|
||||
loadingTab.value = ''
|
||||
try {
|
||||
const bundle = await getProjectDetailBundle(id)
|
||||
// 期间已切到别的需求 ⇒ 丢弃这次响应,别用旧项目覆盖新项目
|
||||
if (seq !== detailSeq) return
|
||||
if (!bundle) {
|
||||
detailBundle.value = null
|
||||
current.value = null
|
||||
return
|
||||
}
|
||||
detailBundle.value = bundle
|
||||
current.value = toProjectDetail(bundle)
|
||||
}
|
||||
catch (error) {
|
||||
if (seq !== detailSeq) return
|
||||
detailBundle.value = null
|
||||
current.value = null
|
||||
console.warn('[project] 项目详情加载失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切页签刷新:**只调该页签对应的那一个接口**,拿到最新数据后落回 bundle 再重新映射。
|
||||
*
|
||||
* <p>后端 4 个页签各一个端点 ⇒ 「点一次页签 = 一次请求」,不需要重拉另外三支。
|
||||
* 页签内容(里程碑 / 付款计划 / 附件 / 售后…)可能被别的入口改动,所以每次点击都重新取。</p>
|
||||
*
|
||||
* <p>并发保护两层:① 需求 id 变了(切项目)⇒ 丢弃在途响应;② 同一页签连点 ⇒ 只应用
|
||||
* 最后一次响应(先到的慢响应被后到的覆盖)。</p>
|
||||
*
|
||||
* <p>后端返回 `null`(需求不存在 / 非本人)时**保留现有数据**,只告警、不清空页面。</p>
|
||||
*/
|
||||
async function refreshTab(tab: ProjectDetailTab): Promise<void> {
|
||||
const requestId = current.value?.id
|
||||
if (!requestId) return
|
||||
const seq = ++tabSeq[tab]
|
||||
loadingTab.value = tab
|
||||
try {
|
||||
const vo = await getProjectDetailTab(requestId, tab)
|
||||
if (seq !== tabSeq[tab] || current.value?.id !== requestId) return
|
||||
if (!vo || !detailBundle.value) return
|
||||
const next: RequirementDetailBundle = { ...detailBundle.value }
|
||||
switch (tab) {
|
||||
case 'progress':
|
||||
next.progress = vo as RequirementDetailBundle['progress']
|
||||
break
|
||||
case 'files':
|
||||
next.files = vo as RequirementDetailBundle['files']
|
||||
break
|
||||
case 'finance':
|
||||
next.finance = vo as RequirementDetailBundle['finance']
|
||||
break
|
||||
case 'afterSales':
|
||||
next.afterSales = vo as RequirementDetailBundle['afterSales']
|
||||
break
|
||||
}
|
||||
detailBundle.value = next
|
||||
current.value = toProjectDetail(next)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`[project] 页签「${tab}」数据刷新失败`, error)
|
||||
}
|
||||
finally {
|
||||
if (seq === tabSeq[tab]) loadingTab.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function setQuery(next: ProjectQuery): void {
|
||||
query.value = next
|
||||
}
|
||||
@@ -143,30 +250,35 @@ export const useProjectStore = defineStore('project', () => {
|
||||
await load()
|
||||
}
|
||||
|
||||
/** 上传项目文件(追加到当前详情) */
|
||||
async function uploadFile(file: File): Promise<void> {
|
||||
const info = await uploadProjectFile(file)
|
||||
current.value?.files.push(info)
|
||||
/**
|
||||
* 上传售后附件(提交前的暂存步骤)。
|
||||
*
|
||||
* <p>只传 OSS、不落业务表;返回项带 `ossId`,由申请页收集后在提交时下发
|
||||
* `attachmentOssIds`,后端再转存成售后附件。</p>
|
||||
*/
|
||||
async function uploadAfterSalesFile(file: File): Promise<AfterSalesAttachment> {
|
||||
return uploadAfterSalesFileApi(file)
|
||||
}
|
||||
|
||||
/** 删除当前详情中的项目文件 */
|
||||
async function deleteFile(index: number): Promise<void> {
|
||||
await deleteProjectFile(index)
|
||||
current.value?.files.splice(index, 1)
|
||||
/**
|
||||
* 申请售后(成功后就地重拉「售后信息」页签)。
|
||||
*
|
||||
* <p>不拿返回值本地拼状态:售后状态与工单附件都由后端决定,本地拼容易和真实数据漂移。
|
||||
* 接口失败(如「只有已完成的项目可以申请售后」)会抛出,由统一 request 层弹提示,
|
||||
* 这里不做兜底吞异常。</p>
|
||||
*/
|
||||
async function submitAfterSales(id: string, payload: AfterSalesPayload): Promise<void> {
|
||||
await submitAfterSalesApi(id, payload)
|
||||
ElMessage.success('售后申请已提交')
|
||||
// 详情页在场时同步刷新该页签;从申请页提交时会立刻跳回详情,那边 onMounted 会重新取数
|
||||
if (current.value?.id === id) await refreshTab('afterSales')
|
||||
}
|
||||
|
||||
/** 提交售后工单 */
|
||||
async function submitAfterSales(payload: AfterSalesPayload): Promise<void> {
|
||||
if (!current.value) return
|
||||
current.value.afterSales = await submitAfterSalesApi(current.value.id, payload)
|
||||
ElMessage.success('已提交售后工单')
|
||||
}
|
||||
|
||||
/** 取消售后工单 */
|
||||
async function cancelAfterSales(): Promise<void> {
|
||||
if (!current.value) return
|
||||
current.value.afterSales = await cancelAfterSalesApi(current.value.id)
|
||||
ElMessage.success('已取消售后工单')
|
||||
/** 撤回售后申请(成功后就地重拉「售后信息」页签) */
|
||||
async function cancelAfterSales(id: string): Promise<void> {
|
||||
await cancelAfterSalesApi(id)
|
||||
ElMessage.success('售后申请已撤回')
|
||||
if (current.value?.id === id) await refreshTab('afterSales')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -177,15 +289,24 @@ export const useProjectStore = defineStore('project', () => {
|
||||
typeCounts,
|
||||
ensureFilterConditions,
|
||||
fetchList,
|
||||
/** 已完成项目(申请售后页「项目名称」下拉候选) */
|
||||
completedProjects,
|
||||
fetchCompletedProjects,
|
||||
fetchDetail,
|
||||
/** 切页签刷新(每次点击都调用,只拉该页签那一支) */
|
||||
refreshTab,
|
||||
/** 正在刷新的页签,供按钮显示加载态 */
|
||||
loadingTab,
|
||||
setQuery,
|
||||
load,
|
||||
search,
|
||||
resetQuery,
|
||||
changePage,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
/** 上传售后附件(申请页用) */
|
||||
uploadAfterSalesFile,
|
||||
/** 申请售后(成功后自动刷新售后页签) */
|
||||
submitAfterSales,
|
||||
/** 撤回售后申请(成功后自动刷新售后页签) */
|
||||
cancelAfterSales,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user