chore: fork from IRIS track 供 LICA 部门独立运行
- 复制来源: /home/yueli/track @ 192c8ee (feature/ai-audit-update) - 组织隔离目标: LICA - 端口规划: 前端 8030 / 后端 8031 / 数据库 8032 - 已排除 deploy.sh、deploy_full.sh、docker-compose.prod.yml(IRIS 生产发布脚本) - 已排除工作区未提交改动,取干净的 192c8ee 状态
This commit is contained in:
233
track-uniapp/src/pages/profile/feedback.vue
Normal file
233
track-uniapp/src/pages/profile/feedback.vue
Normal file
@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 问题类型 -->
|
||||
<view class="section">
|
||||
<text class="section-title">问题类型</text>
|
||||
<view class="type-row">
|
||||
<view
|
||||
v-for="t in FEEDBACK_TYPES"
|
||||
:key="t.value"
|
||||
:class="['type-chip', form.type === t.value ? 'type-chip-active' : '']"
|
||||
@tap="form.type = t.value"
|
||||
>{{ t.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<view class="section">
|
||||
<text class="section-title">详细描述</text>
|
||||
<view class="textarea-wrap">
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
class="textarea"
|
||||
placeholder="请描述遇到的问题:在哪个页面、做了什么操作、出现什么结果。描述越清楚,我们修得越快。"
|
||||
placeholder-class="ta-placeholder"
|
||||
:maxlength="MAX_LEN"
|
||||
:show-confirm-bar="false"
|
||||
auto-height
|
||||
/>
|
||||
<text class="counter">{{ form.content.length }}/{{ MAX_LEN }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片上传(复用 utils/upload.js 的有界并发上传;上限 4 张) -->
|
||||
<view class="section">
|
||||
<text class="section-title">问题截图<text class="section-sub">(选填,最多 {{ MAX_IMAGES }} 张)</text></text>
|
||||
<view class="img-grid">
|
||||
<view v-for="(img, i) in form.images" :key="img + '-' + i" class="img-cell">
|
||||
<view class="success-badge-wrapper img-frame">
|
||||
<image :src="imageUrl(img)" mode="aspectFill" class="img" @tap="previewImage(i)" />
|
||||
<view v-if="isUploaded(img)" class="success-badge" />
|
||||
</view>
|
||||
<text class="img-del" @tap.stop="removeImage(i)">✕</text>
|
||||
</view>
|
||||
<!-- ⏳ 占位符:每张图成功/失败都会恰好回收一个,避免"以为传好了其实没有" -->
|
||||
<view v-for="n in pendingCount" :key="'p' + n" class="img-cell img-loading">
|
||||
<text class="img-loading-text">⏳</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="form.images.length + pendingCount < MAX_IMAGES"
|
||||
class="img-cell img-add"
|
||||
@tap="chooseImages"
|
||||
>
|
||||
<text class="img-add-icon">📷</text>
|
||||
<text class="img-add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="submit-btn"
|
||||
:disabled="!canSubmit"
|
||||
@tap="handleSubmit"
|
||||
>{{ submitting ? '提交中...' : '提交反馈' }}</button>
|
||||
|
||||
<view class="hint">提交后我们会尽快处理。若设备故障影响生产,请同时通知当班主管。</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from "vue";
|
||||
import { FEEDBACK_TYPES, submitFeedback } from "../../api/feedback";
|
||||
import { uploadImages, isUploadedUrl } from "../../utils/upload";
|
||||
import { getBaseUrl } from "../../utils/request";
|
||||
|
||||
const MAX_IMAGES = 4;
|
||||
const MAX_LEN = 500;
|
||||
|
||||
const form = reactive({ type: "system", content: "", images: [] });
|
||||
const pendingCount = ref(0);
|
||||
const uploading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const canSubmit = computed(
|
||||
() => form.content.trim().length > 0 && !uploading.value && !submitting.value
|
||||
);
|
||||
|
||||
function imageUrl(url) {
|
||||
if (!url) return "";
|
||||
if (url.startsWith("http")) return url;
|
||||
const domain = getBaseUrl().replace(/\/api.*$/, "");
|
||||
return domain + (url.startsWith("/") ? url : "/" + url);
|
||||
}
|
||||
|
||||
/** 绿勾角标只认「已上传成功」的项(判定逻辑统一收口在 utils/upload.js) */
|
||||
function isUploaded(img) {
|
||||
return isUploadedUrl(img);
|
||||
}
|
||||
|
||||
function previewImage(index) {
|
||||
uni.previewImage({ urls: form.images.map(imageUrl), current: index });
|
||||
}
|
||||
|
||||
function removeImage(index) {
|
||||
form.images.splice(index, 1);
|
||||
}
|
||||
|
||||
async function chooseImages() {
|
||||
const maxSlots = MAX_IMAGES - (form.images.length + pendingCount.value);
|
||||
if (maxSlots <= 0) return;
|
||||
|
||||
const chooseRes = await new Promise((resolve, reject) => {
|
||||
uni.chooseImage({
|
||||
count: maxSlots,
|
||||
sizeType: ["compressed"],
|
||||
sourceType: ["camera", "album"],
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
}).catch(() => null);
|
||||
if (!chooseRes || !chooseRes.tempFilePaths || !chooseRes.tempFilePaths.length) return;
|
||||
|
||||
const compressedPaths = [];
|
||||
for (const p of chooseRes.tempFilePaths) {
|
||||
try {
|
||||
const compressed = await new Promise((resolve, reject) => {
|
||||
uni.compressImage({ src: p, quality: 60, success: resolve, fail: reject });
|
||||
});
|
||||
compressedPaths.push(compressed.tempFilePath);
|
||||
} catch {}
|
||||
}
|
||||
if (!compressedPaths.length) {
|
||||
uni.showToast({ title: "图片处理失败,请重试", icon: "none", duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true;
|
||||
pendingCount.value += compressedPaths.length;
|
||||
// 🚀 复用 utils/upload.js 的 3 并发上传池(与追加记录 / 驳回弹窗同一份实现)
|
||||
const { failed, total } = await uploadImages(compressedPaths, (url) => {
|
||||
if (url) form.images.push(url);
|
||||
pendingCount.value--;
|
||||
});
|
||||
uploading.value = false;
|
||||
if (failed > 0) {
|
||||
uni.showToast({
|
||||
title: failed === total ? "图片上传失败,请重试" : "部分图片上传失败,请重试",
|
||||
icon: "none",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function readUserId() {
|
||||
try {
|
||||
let user = uni.getStorageSync("user");
|
||||
if (typeof user === "string" && user) user = JSON.parse(user);
|
||||
return (user && (user.username || user.id)) || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!canSubmit.value) return;
|
||||
// 上传还没结束就提交,会把没传完的图片漏掉 —— 拦住并说明原因
|
||||
if (pendingCount.value > 0) {
|
||||
uni.showToast({ title: "图片还在上传,请稍候", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await submitFeedback({
|
||||
type: form.type,
|
||||
content: form.content.trim(),
|
||||
images: form.images,
|
||||
user_id: readUserId(),
|
||||
});
|
||||
uni.showToast({ title: "感谢反馈", icon: "success" });
|
||||
// 让 toast 露个面再返回;被直接打开(无上级页)时退回个人中心,避免卡死
|
||||
setTimeout(() => {
|
||||
if (getCurrentPages().length > 1) uni.navigateBack();
|
||||
else uni.reLaunch({ url: "/pages/profile/index" });
|
||||
}, 1200);
|
||||
} catch (e) {
|
||||
// 校验失败 / 断网等真实错误照常提示;request.js 已弹过通用 toast 就不再重复弹
|
||||
console.error("[feedback] 提交失败:", e);
|
||||
if (e && e.isNetworkError) {
|
||||
uni.showToast({ title: "网络连接失败,请稍后重试", icon: "none", duration: 3000 });
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; padding: 16px; background: #f3f4f6; }
|
||||
|
||||
.section { background: #fff; border-radius: 14px; padding: 16px; margin-bottom: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
.section-title { font-size: 14px; font-weight: 700; color: #1f2937; display: block; margin-bottom: 12px; }
|
||||
.section-sub { font-size: 12px; font-weight: 400; color: #9ca3af; }
|
||||
|
||||
/* 问题类型 */
|
||||
.type-row { display: flex; gap: 10px; }
|
||||
.type-chip { flex: 1; height: 40px; line-height: 40px; text-align: center; border-radius: 10px; background: #f3f4f6; color: #6b7280; font-size: 13px; font-weight: 600; }
|
||||
.type-chip-active { background: #2563eb; color: #fff; }
|
||||
|
||||
/* 描述 */
|
||||
.textarea-wrap { position: relative; }
|
||||
.textarea { width: 100%; min-height: 110px; border: 1px solid #e5e7eb; border-radius: 10px; padding: 10px 10px 26px; font-size: 14px; line-height: 1.5; box-sizing: border-box; background: #fafbfc; }
|
||||
.ta-placeholder { color: #b0b6bf; font-size: 13px; }
|
||||
.counter { position: absolute; right: 12px; bottom: 8px; font-size: 11px; color: #b0b6bf; }
|
||||
|
||||
/* 图片 */
|
||||
.img-grid { display: flex; flex-wrap: wrap; }
|
||||
.img-cell { position: relative; width: 160rpx; height: 160rpx; margin: 0 16rpx 16rpx 0; }
|
||||
/* 圆角与裁剪交给 .success-badge-wrapper,图片只负责填满 */
|
||||
.img-frame { width: 160rpx; height: 160rpx; }
|
||||
.img { width: 100%; height: 100%; display: block; border: 1px solid #e5e7eb; box-sizing: border-box; background: #e5e7eb; }
|
||||
.img-del { position: absolute; top: -12rpx; right: -12rpx; width: 40rpx; height: 40rpx; background: #ef4444; color: #fff; border-radius: 20rpx; font-size: 24rpx; text-align: center; line-height: 40rpx; z-index: 2; }
|
||||
.img-loading { display: flex; align-items: center; justify-content: center; background: #f3f4f6; border-radius: 12rpx; border: 1px dashed #d1d5db; }
|
||||
.img-loading-text { font-size: 36rpx; }
|
||||
.img-add { display: flex; flex-direction: column; align-items: center; justify-content: center; background: #f9fafb; border-radius: 12rpx; border: 1px dashed #d1d5db; }
|
||||
.img-add-icon { font-size: 36rpx; }
|
||||
.img-add-text { font-size: 20rpx; color: #9ca3af; margin-top: 4rpx; }
|
||||
|
||||
/* 提交 */
|
||||
.submit-btn { width: 100%; height: 46px; line-height: 46px; background: #2563eb; color: #fff; border-radius: 12px; font-size: 15px; font-weight: 600; margin-top: 6px; padding: 0; }
|
||||
.submit-btn::after { border: none; }
|
||||
.submit-btn[disabled] { opacity: 0.45; }
|
||||
.hint { margin-top: 14px; padding: 0 4px; font-size: 11px; color: #b0b6bf; line-height: 1.6; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user