238 lines
8.5 KiB
Vue
238 lines
8.5 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref } from "vue";
|
||
import { onLoad } from "@dcloudio/uni-app";
|
||
import { appApi, type OrderDetailData, type TicketAttachmentAsset, type TicketTypeOption } from "../../api/app";
|
||
import { resolveErrorMessage, showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||
|
||
const orderDetail = ref<OrderDetailData | null>(null);
|
||
const orderId = ref(0);
|
||
const reportId = ref(0);
|
||
const ticketType = ref("order_issue");
|
||
const title = ref("");
|
||
const content = ref("");
|
||
const submitting = ref(false);
|
||
const uploading = ref(false);
|
||
const attachments = ref<TicketAttachmentAsset[]>([]);
|
||
const orderLoading = ref(false);
|
||
const orderLoadError = ref("");
|
||
const ticketTypes = ref<TicketTypeOption[]>([]);
|
||
|
||
const selectedTypeHint = computed(
|
||
() => ticketTypes.value.find((item) => item.code === ticketType.value)?.hint || "",
|
||
);
|
||
|
||
function goBack() {
|
||
uni.navigateBack();
|
||
}
|
||
|
||
function previewAttachments(current: string) {
|
||
if (!attachments.value.length) return;
|
||
uni.previewImage({
|
||
urls: attachments.value.map((item) => item.file_url),
|
||
current,
|
||
});
|
||
}
|
||
|
||
async function chooseAttachments() {
|
||
try {
|
||
const result = await uni.chooseImage({
|
||
count: 3,
|
||
sizeType: ["compressed"],
|
||
sourceType: ["album", "camera"],
|
||
});
|
||
if (!result.tempFilePaths?.length) {
|
||
return;
|
||
}
|
||
|
||
uploading.value = true;
|
||
for (const filePath of result.tempFilePaths) {
|
||
const asset = await appApi.uploadTicketFile(filePath);
|
||
attachments.value.push(asset);
|
||
}
|
||
showInfoToast("附件上传成功");
|
||
} catch (error) {
|
||
showErrorToast(error, "附件上传失败");
|
||
} finally {
|
||
uploading.value = false;
|
||
}
|
||
}
|
||
|
||
async function removeAttachment(fileUrl: string) {
|
||
try {
|
||
await appApi.deleteTicketFile(fileUrl);
|
||
attachments.value = attachments.value.filter((item) => item.file_url !== fileUrl);
|
||
showInfoToast("附件已删除");
|
||
} catch (error) {
|
||
showErrorToast(error, "附件删除失败");
|
||
}
|
||
}
|
||
|
||
async function submitTicket() {
|
||
if (!title.value.trim()) {
|
||
showInfoToast("请先填写工单标题");
|
||
return;
|
||
}
|
||
if (!content.value.trim() && !attachments.value.length) {
|
||
showInfoToast("请先填写问题描述或上传附件");
|
||
return;
|
||
}
|
||
|
||
submitting.value = true;
|
||
try {
|
||
const response = await withLoading("正在提交工单", async () =>
|
||
appApi.createTicket({
|
||
ticket_type: ticketType.value,
|
||
title: title.value.trim(),
|
||
content: content.value.trim(),
|
||
order_id: orderId.value || undefined,
|
||
report_id: reportId.value || undefined,
|
||
attachments: attachments.value,
|
||
}),
|
||
);
|
||
showInfoToast("工单已提交");
|
||
uni.redirectTo({ url: `/pages/support/detail?id=${response.ticket_id}` });
|
||
} catch (error) {
|
||
showErrorToast(error, "工单提交失败");
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
onLoad(async (options) => {
|
||
try {
|
||
const meta = await appApi.getTicketMeta();
|
||
ticketTypes.value = meta.ticket_types;
|
||
} catch (error) {
|
||
console.warn("ticket meta load failed", error);
|
||
}
|
||
|
||
ticketType.value = String(options?.ticket_type || ticketType.value);
|
||
title.value = String(options?.prefill_title || "");
|
||
orderId.value = Number(options?.order_id || 0);
|
||
reportId.value = Number(options?.report_id || 0);
|
||
|
||
if (!orderId.value) {
|
||
return;
|
||
}
|
||
|
||
orderLoading.value = true;
|
||
orderLoadError.value = "";
|
||
try {
|
||
orderDetail.value = await appApi.getOrderDetail(orderId.value);
|
||
} catch (error) {
|
||
console.warn("order detail fallback", error);
|
||
orderDetail.value = null;
|
||
orderLoadError.value = resolveErrorMessage(error, "关联订单读取失败,当前仍可继续提交工单。");
|
||
} finally {
|
||
orderLoading.value = false;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<view class="app-page app-page--tight">
|
||
<view class="section-card">
|
||
<view class="tag tag--accent">发起工单</view>
|
||
<view class="page-title" style="margin-top: 20rpx; font-size: var(--font-size-2xl); color: var(--color-heading);">
|
||
提交客服问题
|
||
</view>
|
||
<view class="section__desc">请尽量描述清楚当前问题、出现时间和您的预期,我们会按工单类型尽快分流处理。</view>
|
||
</view>
|
||
|
||
<view v-if="ticketType" class="section section-card section-card--soft">
|
||
<view class="section__title">当前选择的问题类型</view>
|
||
<view class="section__desc">{{ ticketTypes.find((item) => item.code === ticketType)?.title || "未选择类型" }}</view>
|
||
<view class="form-group__hint" style="margin-top: 12rpx;">{{ selectedTypeHint }}</view>
|
||
</view>
|
||
|
||
<view v-if="orderId && orderLoading" class="section notice-card">
|
||
<view class="notice-card__title">正在读取关联订单</view>
|
||
<view class="notice-card__desc">请稍候,我们正在同步当前工单对应的订单信息。</view>
|
||
</view>
|
||
|
||
<view v-else-if="orderId && orderLoadError" class="section notice-card">
|
||
<view class="notice-card__title">关联订单读取失败</view>
|
||
<view class="notice-card__desc">{{ orderLoadError }}</view>
|
||
</view>
|
||
|
||
<view v-if="orderId && orderDetail" class="section section-card section-card--soft">
|
||
<view class="section__title">当前关联订单</view>
|
||
<view class="report-meta__row">
|
||
<text class="report-meta__label">订单号</text>
|
||
<text class="report-meta__value">{{ orderDetail.order_info.order_no }}</text>
|
||
</view>
|
||
<view class="report-meta__row">
|
||
<text class="report-meta__label">当前状态</text>
|
||
<text class="report-meta__value">{{ orderDetail.order_info.display_status }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section form-panel">
|
||
<view class="form-panel__title">问题类型</view>
|
||
<view class="form-panel__desc">先选择问题类别,客服会按对应队列更快跟进。</view>
|
||
<view class="chip-list">
|
||
<view
|
||
v-for="item in ticketTypes"
|
||
:key="item.code"
|
||
:class="['choice-chip', ticketType === item.code ? 'choice-chip--selected' : '']"
|
||
@click="ticketType = item.code"
|
||
>
|
||
{{ item.title }}
|
||
</view>
|
||
</view>
|
||
<view class="form-group__hint">{{ selectedTypeHint }}</view>
|
||
</view>
|
||
|
||
<view class="section form-panel">
|
||
<view class="form-group">
|
||
<view class="form-group__label">工单标题</view>
|
||
<view class="field-box">
|
||
<input v-model="title" class="field-input" maxlength="40" placeholder="例如:补资料上传后仍显示待处理" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<view class="form-group__label">问题描述</view>
|
||
<view class="textarea-box">
|
||
<textarea
|
||
v-model="content"
|
||
class="textarea-box__input"
|
||
maxlength="500"
|
||
placeholder="请描述问题现象、出现时间、您已尝试的操作,以及希望客服协助的内容。"
|
||
/>
|
||
</view>
|
||
<view class="form-group__hint">建议尽量写清楚页面位置、操作步骤和报错现象,能帮助客服更快定位。</view>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<view class="form-group__label">补充截图</view>
|
||
<view class="task-card__desc">可上传订单截图、报错提示或页面截图,帮助客服更快定位。</view>
|
||
<view v-if="attachments.length" class="task-files">
|
||
<view v-for="item in attachments" :key="item.file_id" class="task-file">
|
||
<image class="task-file__img" :src="item.thumbnail_url" mode="aspectFill" @click="previewAttachments(item.file_url)" />
|
||
<view class="task-file__remove" @click="removeAttachment(item.file_url)">删除</view>
|
||
</view>
|
||
</view>
|
||
<view class="task-card__row" style="margin-top: 20rpx">
|
||
<text class="info-list__label">已上传 {{ attachments.length }} 张</text>
|
||
<text class="btn btn--ghost" @click="chooseAttachments">
|
||
{{ uploading ? "上传中..." : "上传附件" }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section section-note">
|
||
<view class="support-banner__title">处理说明</view>
|
||
<view class="support-banner__desc">工单提交后,您可以在工单详情里查看客服回复。涉及订单问题时,建议尽量关联具体订单。</view>
|
||
</view>
|
||
|
||
<view class="fixed-action-bar">
|
||
<view class="btn btn--secondary" @click="goBack">取消</view>
|
||
<view :class="['btn', 'btn--primary', submitting ? 'btn--disabled' : '']" @click="submitTicket">
|
||
{{ submitting ? "提交中..." : "提交工单" }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|