This commit is contained in:
wushumin
2026-05-11 15:28:27 +08:00
commit 9aac78b8da
289 changed files with 67193 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
<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>