290 lines
8.0 KiB
Vue
290 lines
8.0 KiB
Vue
<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>
|