|
|
<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(/ /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> |