first
This commit is contained in:
237
user-app/src/pages/support/create.vue
Normal file
237
user-app/src/pages/support/create.vue
Normal 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>
|
||||
Reference in New Issue
Block a user