You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
12 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<el-dialog :model-value="modelValue" class="admin-order-process-dialog" width="800px" :close-on-click-modal="false"
top="5vh" @update:model-value="(v) => $emit('update:modelValue', v)" @open="onOpen">
<div class="body-wrap" v-loading="loading">
<div class="body-header">
<div class="body-title">处理工单</div>
<img src="@/assets/image/close-btn.svg" alt="" class="close-btn" @click="closeDialog" />
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" v-if="detail.id">
<!-- 工单号(只读) -->
<el-form-item :label="$t('table.id')">
<el-input :model-value="detail.orderNo" readonly />
</el-form-item>
<!-- 科室 / 提交人(可编辑,回显原值) -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item :label="$t('table.dept')" prop="department">
<el-input v-model="form.department" :placeholder="'请输入科室'" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('table.submitter')" prop="submitter">
<el-input v-model="form.submitter" :placeholder="'请输入提交人'" readonly />
</el-form-item>
</el-col>
</el-row>
<!-- 问题描述(可编辑富文本,默认回显原值) -->
<el-form-item :label="$t('label.description')" prop="description">
<!-- <div class="rich-editor">
<Toolbar :editor="descEditorRef" :default-config="toolbarConfig" mode="default"
style="border-bottom: 1px solid #dcdfe6" />
<Editor v-model="form.description" :default-config="descEditorConfig" mode="default"
style="height: 200px; overflow-y: auto" @on-created="handleDescEditorCreated"
@custom-paste="handleEditorPasteImage" />
</div> -->
<div class="html-wrap" style="max-height: 300px; overflow: auto;">
<div class="html-content" v-html="detail.description || '—'"></div>
</div>
</el-form-item>
<!-- 处理说明wangEditor 富文本编辑器,与提交工单页面一致) -->
<el-form-item :label="$t('common.remark')" prop="processRemark">
<div class="rich-editor">
<Toolbar :editor="editorRef" :default-config="toolbarConfig" mode="default"
style="border-bottom: 1px solid #dcdfe6" />
<Editor v-model="form.processRemark" :default-config="editorConfig" mode="default"
style="height: 200px; overflow-y: auto" @on-created="handleEditorCreated"
@custom-paste="handleEditorPasteImage" />
</div>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<!-- 新状态(只允许"已完成" -->
<el-form-item :label="'新状态'" prop="status">
<el-select v-model="form.status">
<el-option :label="$t('status.completed')" value="已完成" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- 处理人(取当前登录用户名) -->
<el-form-item :label="$t('table.handler')">
<el-input :model-value="handlerName" readonly />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div class="body-footer">
<el-button @click="closeDialog">{{ $t("common.cancel") }}</el-button>
<el-button type="primary" :loading="submitting" @click="submit">{{
$t("btn.confirm")
}}</el-button>
</div>
</el-dialog>
</template>
<script setup>
import {
computed,
nextTick,
onBeforeUnmount,
reactive,
ref,
shallowRef,
watch,
} from "vue";
import { ElMessage } from "element-plus";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { useI18n } from "vue-i18n";
import {
getAdminOrderDetail,
processAdminOrder,
} from "@/service/modular/admin";
import { useUserStore } from "@/stores/api/user";
const { t: $t } = useI18n();
const userStore = useUserStore();
const props = defineProps({
modelValue: { type: Boolean, default: false },
orderId: { type: [String, Number], default: "" },
});
const emit = defineEmits(["update:modelValue", "processed"]);
const loading = ref(false);
const submitting = ref(false);
const detail = ref({});
const formRef = ref();
// 处理人显示当前登录用户名
const handlerName = computed(
() =>
userStore.userInfo?.name ||
userStore.userInfo?.userName ||
userStore.userInfo?.account ||
"管理员",
);
// 新状态只能为"已完成"
const form = reactive({
status: "已完成",
processRemark: "",
description: "",
department: "",
submitter: "",
});
const rules = {
status: [
{ required: true, message: () => $t("table.status"), trigger: "change" },
],
processRemark: [
{
required: true,
validator: (_, value, callback) => {
const hasImage = /<img\s/i.test(value || "");
const text = (value || "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, "")
.trim();
if (!text && !hasImage)
return callback(new Error($t("placeholder.remark")));
callback();
},
trigger: "change",
},
],
};
// wangEditor
const editorRef = shallowRef();
const descEditorRef = shallowRef();
const toolbarConfig = {
excludeKeys: [
"uploadImage",
"insertVideo",
"uploadVideo",
"group-video",
"emotion",
"group-emoji",
],
};
const editorConfig = {
placeholder: $t("placeholder.remark"),
};
const descEditorConfig = {
placeholder: $t("placeholder.description") || "请输入问题描述",
};
// 粘贴到富文本的图片大小上限base64 内联存储,避免 HTML 过大)
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
// 从剪贴板中捞出图片文件(同时兼容 items 和 files 两种来源)
const collectPastedImages = (clipboardData) => {
const files = [];
const items = clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === "file" && item.type?.startsWith("image/")) {
const file = item.getAsFile();
if (file) files.push(file);
}
}
}
if (files.length === 0 && clipboardData?.files) {
for (let i = 0; i < clipboardData.files.length; i++) {
const file = clipboardData.files[i];
if (file.type?.startsWith("image/")) files.push(file);
}
}
return files;
};
// 共用的图片粘贴处理逻辑customPaste 和原生监听都走这里,用事件标记防重复)
const handleEditorPasteImage = (editor, event) => {
if (event.__wpsImageHandled) return;
const imageFiles = collectPastedImages(event.clipboardData);
if (imageFiles.length === 0) return;
event.__wpsImageHandled = true;
event.preventDefault();
if (typeof event.stopPropagation === "function") event.stopPropagation();
if (typeof event.stopImmediatePropagation === "function")
event.stopImmediatePropagation();
// 同步先拿到当前 selection等 FileReader 异步回调里恢复
if (typeof editor.focus === "function") editor.focus();
const savedSelection = editor.selection;
imageFiles.forEach((file) => {
if (file.size > MAX_PASTE_IMAGE_SIZE) {
ElMessage.warning("粘贴的图片不能超过 5MB");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target?.result;
if (!dataUrl) {
ElMessage.error("图片读取失败");
return;
}
try {
if (savedSelection) editor.select(savedSelection);
} catch (_) {
if (typeof editor.focus === "function") editor.focus();
}
if (typeof editor.focus === "function") editor.focus();
editor.dangerouslyInsertHtml(
`<img src="${dataUrl}" style="max-width:100%;height:auto;" />`,
);
if (typeof editor.updateView === "function") editor.updateView();
// ElMessage.success("图片插入成功");
};
reader.onerror = () => {
ElMessage.error("图片处理失败");
};
reader.readAsDataURL(file);
});
return false; // 告诉 wangEditor 跳过默认处理
};
// 给单个编辑器绑定 paste 监听capture 阶段,防 customPaste 未生效的兜底)
// 用一个对象持有可变的 handler 引用,避免在 setup 顶层用 let 变量被闭包丢失更新
const pasteHandlerRef = { current: null };
const descPasteHandlerRef = { current: null };
const attachPasteListener = (editor, refHolder) => {
nextTick(() => {
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
if (!root) return;
const editable = root.querySelector?.('[contenteditable="true"]') || root;
if (refHolder.current) {
editable.removeEventListener("paste", refHolder.current, { capture: true });
}
refHolder.current = (event) => handleEditorPasteImage(editor, event);
editable.addEventListener("paste", refHolder.current, { capture: true });
});
};
const handleEditorCreated = (editor) => {
editorRef.value = editor;
attachPasteListener(editor, pasteHandlerRef);
};
const handleDescEditorCreated = (editor) => {
descEditorRef.value = editor;
attachPasteListener(editor, descPasteHandlerRef);
};
const loadDetail = async () => {
if (!props.orderId) return;
loading.value = true;
try {
detail.value = await getAdminOrderDetail(props.orderId);
form.processRemark = detail.value.processRemark || "";
form.description = detail.value.description || "";
form.department = detail.value.department || "";
form.submitter = detail.value.submitter || "";
} finally {
loading.value = false;
}
};
const onOpen = () => {
loadDetail();
};
watch(
() => props.orderId,
() => {
if (props.modelValue) loadDetail();
},
);
const submit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) return;
submitting.value = true;
try {
await processAdminOrder(props.orderId, {
status: form.status,
processRemark: form.processRemark,
description: form.description,
department: form.department,
submitter: form.submitter,
});
ElMessage.success($t("msg.editSuccess"));
emit("update:modelValue", false);
emit("processed");
} finally {
submitting.value = false;
}
});
};
const closeDialog = () => emit("update:modelValue", false);
const removePasteListener = (editor, refHolder) => {
if (!editor || !refHolder?.current) return;
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
if (!root) return;
const editable = root.querySelector?.('[contenteditable="true"]') || root;
editable.removeEventListener("paste", refHolder.current, { capture: true });
};
onBeforeUnmount(() => {
removePasteListener(editorRef.value, pasteHandlerRef);
removePasteListener(descEditorRef.value, descPasteHandlerRef);
pasteHandlerRef.current = null;
descPasteHandlerRef.current = null;
if (editorRef.value) editorRef.value.destroy();
if (descEditorRef.value) descEditorRef.value.destroy();
});
</script>
<style lang="scss">
.admin-order-process-dialog {
max-width: 90vw;
min-width: 70vw;
--el-component-size: 40px;
.el-select__wrapper {
height: 40px;
}
.body-header {
margin-bottom: 16px;
}
// wangEditor 富文本容器(与提交工单页面一致)
.rich-editor {
border: 1px solid #dcdfe6;
border-radius: 4px;
width: 100%;
overflow: hidden;
background: #fff;
}
.html-wrap {
border: 1px solid #ebeef5;
border-radius: 4px;
width: 100%;
overflow: hidden;
background: #fff;
padding: 1px 11px;
.html-content {
min-height: 40px;
img {
max-width: 100%;
}
}
}
.rich-editor :deep(.w-e-toolbar) {
background: #fafafa;
}
}
</style>