Files
cz-web/src/views/project/cpns/ProjectAfterSales.vue
2026-09-28 17:47:07 +08:00

290 lines
8.0 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 { computed } from 'vue'
import { ElMessage } from 'element-plus'
import { FileList } from '@/components'
import type { FileInfo } from '@/types/api'
import type { AfterSales, ProjectStatus } from '@/types/business/project'
interface Props {
/** 售后工单;`undefined` 或 `status === '无'` 表示没有进行中的工单 */
afterSales?: AfterSales
/** 项目状态 —— 决定售后区的三态展示(见下方模板注释) */
projectStatus: ProjectStatus
}
const props = defineProps<Props>()
const emit = defineEmits<{
apply: []
cancel: []
}>()
/**
* 售后页签的三态(设计稿便签原文):
* 1. 项目**未完成** → 不展示售后信息;
* 2. 已完成 + **未提交**售后 → 内容为空,只给「申请售后」;
* 3. 已完成 + **已提交**售后 → 展示售后信息 + 「取消售后」。
*
* <p>「已完成」按详情页状态口径判定(后端 `COMPLETED` 经 projectVo 映射成「已完成」)。
* 后端 `requestAfterSale` 也会校验这一点,前端只是提前把入口藏掉。</p>
*/
const isCompleted = computed(() => props.projectStatus === '已完成')
/** 存在进行中的售后工单(撤回后的记录在映射层已归为「无」) */
const hasTicket = computed(() => props.afterSales?.status === '处理中')
/**
* 预览 / 下载直接走附件自身的 OSS 地址(后端下发的 `url` 就是可访问地址);
* 删除没有接口 —— 售后附件只在「申请 / 完成售后」时整体写入,不支持单独删。
*/
function previewFile(file: FileInfo): void {
if (!file.url) {
ElMessage.info('该附件没有可访问的地址')
return
}
window.open(file.url, '_blank', 'noopener')
}
function downloadFile(file: FileInfo): void {
if (!file.url) {
ElMessage.info('该附件没有可访问的地址')
return
}
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
link.rel = 'noopener'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function onDeleteFile(): void {
// TODO(api): 售后工单附件无单独删除接口(后端只在申请 / 完成售后时整体写入)
ElMessage.info('售后附件暂不支持单独删除')
}
</script>
<template>
<section class="project-after-sales">
<!-- 规则 1:项目未完成 → 不展示售后信息 -->
<div v-if="!isCompleted" class="project-after-sales__blank">
<p class="project-after-sales__blank-text text-secondary">
项目完成后可申请售后
</p>
</div>
<!-- 规则 3:已完成 + 已提交售后 → 工单横幅 + 售后信息 + 取消售后 -->
<template v-else-if="hasTicket && afterSales">
<div class="project-after-sales__banner">
<span class="project-after-sales__banner-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor">
<path d="M6 2h7l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm7 1.6V8h4.4L13 3.6ZM8 12.2h8v1.6H8v-1.6Zm0 3.6h8v1.6H8v-1.6Z" />
</svg>
</span>
<div class="project-after-sales__banner-text">
<h3 class="project-after-sales__banner-title">
售后工单处理中
</h3>
<p class="project-after-sales__banner-desc">
工程师正在处理您的修改需求,请耐心等待
</p>
</div>
</div>
<h2 class="project-after-sales__title">
售后信息
</h2>
<div class="project-after-sales__field">
<span class="project-after-sales__label">售后类型</span>
<div class="project-after-sales__readonly">
{{ afterSales.type || '—' }}
</div>
</div>
<div class="project-after-sales__field">
<span class="project-after-sales__label">问题描述</span>
<div class="project-after-sales__readonly project-after-sales__readonly--block">
{{ afterSales.description || '—' }}
</div>
</div>
<div class="project-after-sales__field">
<div class="project-after-sales__field-head">
<span class="project-after-sales__label">附件上传</span>
<span class="project-after-sales__field-hint text-secondary">支持图片/文档/压缩包,大小不超过1G</span>
</div>
<FileList
v-if="afterSales.attachments.length > 0"
:files="afterSales.attachments"
@preview="previewFile"
@download="downloadFile"
@delete="onDeleteFile"
/>
<p v-else class="project-after-sales__none text-secondary">
暂无附件
</p>
</div>
<div class="project-after-sales__actions">
<button
class="project-after-sales__btn project-after-sales__btn--primary"
@click="emit('cancel')"
>
取消售后
</button>
</div>
</template>
<!-- 规则 2:已完成 + 未提交售后 → 内容为空 + 申请售后 -->
<template v-else>
<div class="project-after-sales__blank">
<p class="project-after-sales__blank-text text-secondary">
暂无售后工单
</p>
</div>
<div class="project-after-sales__actions">
<button
class="project-after-sales__btn project-after-sales__btn--primary"
@click="emit('apply')"
>
申请售后
</button>
</div>
</template>
</section>
</template>
<style lang="less" scoped>
.project-after-sales {
.flex(column, flex-start, stretch);
gap: @spacing-lg;
padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
// ── 顶部工单横幅:浅红底 + 红色图标块(设计稿) ──
&__banner {
.flex(row, flex-start, center);
gap: @spacing-base;
padding: @spacing-base @spacing-lg;
border-radius: @radius-base;
background: fade(@danger-color, 8%);
}
&__banner-icon {
.flex(row, center);
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: @radius-base;
background: @danger-color;
color: @white;
}
&__banner-text {
.flex(column, flex-start, stretch);
gap: 6px;
min-width: 0;
}
&__banner-title {
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
line-height: 1.2;
}
&__banner-desc {
font-size: @font-size-sm;
color: @text-color-secondary;
}
// ── 「售后信息」区块标题:竖条 + 15px 缩进(与其它页签一致) ──
&__title {
.section-title();
margin-top: @spacing-xs;
}
&__field {
.flex(column, flex-start, stretch);
gap: @spacing-sm;
}
&__field-head {
.flex(row, flex-start, baseline);
gap: @spacing-sm;
}
&__field-hint {
font-size: @font-size-sm;
}
&__label {
font-size: @font-size-base;
font-weight: 600;
color: @heading-color;
}
// 只读展示(设计稿里是灰底带边框的框,不是输入控件)
&__readonly {
min-height: 40px;
padding: 0 @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
color: @text-color;
font-size: @font-size-base;
line-height: 40px;
&--block {
min-height: 96px;
padding: @spacing-sm @spacing-base;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
}
}
&__none {
padding: @spacing-sm 0;
font-size: @font-size-sm;
}
&__blank {
.flex(row, center);
padding: @spacing-xl 0;
}
&__blank-text {
font-size: @font-size-base;
}
&__actions {
.flex(row, flex-end, center);
gap: @spacing-sm;
}
&__btn {
height: 36px;
padding: 0 @spacing-lg;
border-radius: @radius-base;
font-size: @font-size-base;
transition: all 0.2s;
&--primary {
border: none;
background: @primary-color;
color: @white;
&:hover {
background: @primary-hover;
}
}
}
}
</style>