|
|
|
@ -14,12 +14,12 @@
|
|
|
|
<el-tag :type="statusType(detail.status)" size="small">{{ detail.status }}</el-tag>
|
|
|
|
<el-tag :type="statusType(detail.status)" size="small">{{ detail.status }}</el-tag>
|
|
|
|
<el-button
|
|
|
|
<el-button
|
|
|
|
v-if="detail.status !== '已完成'"
|
|
|
|
v-if="detail.status !== '已完成'"
|
|
|
|
type="success"
|
|
|
|
type="primary"
|
|
|
|
size="small"
|
|
|
|
size="small"
|
|
|
|
style="margin-left:auto"
|
|
|
|
style="margin-left:auto"
|
|
|
|
@click="goProcess"
|
|
|
|
@click="openEdit"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{{ $t('btn.process') }}
|
|
|
|
{{ $t('btn.edit') }}
|
|
|
|
</el-button>
|
|
|
|
</el-button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
@ -71,6 +71,179 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</el-card>
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 编辑工单弹窗 -->
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
|
|
v-model="editVisible"
|
|
|
|
|
|
|
|
:title="$t('modal.edit')"
|
|
|
|
|
|
|
|
width="640px"
|
|
|
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
|
|
|
@closed="onEditClosed"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
|
|
ref="editFormRef"
|
|
|
|
|
|
|
|
:model="editForm"
|
|
|
|
|
|
|
|
:rules="editRules"
|
|
|
|
|
|
|
|
label-width="100px"
|
|
|
|
|
|
|
|
v-loading="editLoading"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.hospitalName')" prop="hospitalId">
|
|
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
|
|
v-model="editForm.hospitalId"
|
|
|
|
|
|
|
|
:placeholder="$t('msg.pleaseSelectHospital')"
|
|
|
|
|
|
|
|
filterable
|
|
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
|
|
v-for="h in hospitals"
|
|
|
|
|
|
|
|
:key="h.id"
|
|
|
|
|
|
|
|
:label="h.name"
|
|
|
|
|
|
|
|
:value="h.id"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.title')" prop="title">
|
|
|
|
|
|
|
|
<div class="title-row">
|
|
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
|
|
v-model="editForm.title"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.title')"
|
|
|
|
|
|
|
|
maxlength="100"
|
|
|
|
|
|
|
|
show-word-limit
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div class="color-tag-picker" ref="editColorPickerRef">
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="color-tag-preview"
|
|
|
|
|
|
|
|
:style="{ background: editForm.colorTag || '#dcdfe6' }"
|
|
|
|
|
|
|
|
:class="{ 'is-empty': !editForm.colorTag }"
|
|
|
|
|
|
|
|
@click.stop="toggleEditColorPicker"
|
|
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
v-show="editColorPickerVisible"
|
|
|
|
|
|
|
|
class="color-tag-dropdown"
|
|
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div class="color-tag-title">{{ $t("label.colorTag") }}</div>
|
|
|
|
|
|
|
|
<div class="color-tag-options">
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
v-for="c in colorOptions"
|
|
|
|
|
|
|
|
:key="c.value"
|
|
|
|
|
|
|
|
class="color-tag-option"
|
|
|
|
|
|
|
|
:class="{ active: editForm.colorTag === c.value }"
|
|
|
|
|
|
|
|
:style="{ background: c.value }"
|
|
|
|
|
|
|
|
:title="c.label"
|
|
|
|
|
|
|
|
@click="selectEditColor(c.value)"
|
|
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.type')" prop="serviceType">
|
|
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
|
|
v-model="editForm.serviceType"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.type')"
|
|
|
|
|
|
|
|
clearable
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<el-option label="故障问题" value="故障问题" />
|
|
|
|
|
|
|
|
<el-option label="使用咨询" value="使用咨询" />
|
|
|
|
|
|
|
|
<el-option label="功能需求" value="功能需求" />
|
|
|
|
|
|
|
|
<el-option label="其他" value="其他" />
|
|
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item label-width="0">
|
|
|
|
|
|
|
|
<div class="inline-row">
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.priority')" prop="priority" class="inline-item">
|
|
|
|
|
|
|
|
<el-radio-group v-model="editForm.priority">
|
|
|
|
|
|
|
|
<el-radio-button label="高">高</el-radio-button>
|
|
|
|
|
|
|
|
<el-radio-button label="中">中</el-radio-button>
|
|
|
|
|
|
|
|
<el-radio-button label="低">低</el-radio-button>
|
|
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.feedbackchannel')" prop="feedbackchannel" class="inline-item">
|
|
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
|
|
v-model="editForm.feedbackchannel"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.feedbackchannel')"
|
|
|
|
|
|
|
|
clearable
|
|
|
|
|
|
|
|
style="width: 160px"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<el-option label="微信群" value="微信群" />
|
|
|
|
|
|
|
|
<el-option label="微信" value="微信" />
|
|
|
|
|
|
|
|
<el-option label="电话" value="电话" />
|
|
|
|
|
|
|
|
<el-option label="现场" value="现场" />
|
|
|
|
|
|
|
|
<el-option label="钉钉群" value="钉钉群" />
|
|
|
|
|
|
|
|
<el-option label="钉钉" value="钉钉" />
|
|
|
|
|
|
|
|
<el-option label="其他" value="其他" />
|
|
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.dept')" prop="department">
|
|
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
|
|
v-model="editForm.department"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.dept')"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.name')" prop="submitter">
|
|
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
|
|
v-model="editForm.submitter"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.name')"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.createat')" prop="createdat">
|
|
|
|
|
|
|
|
<el-date-picker
|
|
|
|
|
|
|
|
v-model="editForm.createdat"
|
|
|
|
|
|
|
|
type="datetime"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.createat')"
|
|
|
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
|
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.description')" prop="description">
|
|
|
|
|
|
|
|
<div class="rich-editor">
|
|
|
|
|
|
|
|
<Toolbar
|
|
|
|
|
|
|
|
:editor="editorRef"
|
|
|
|
|
|
|
|
:default-config="toolbarConfig"
|
|
|
|
|
|
|
|
mode="default"
|
|
|
|
|
|
|
|
style="border-bottom: 1px solid #dcdfe6"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Editor
|
|
|
|
|
|
|
|
v-model="editForm.description"
|
|
|
|
|
|
|
|
:default-config="editorConfig"
|
|
|
|
|
|
|
|
mode="default"
|
|
|
|
|
|
|
|
style="height: 200px; overflow-y: auto"
|
|
|
|
|
|
|
|
@on-created="handleEditorCreated"
|
|
|
|
|
|
|
|
@custom-paste="handleEditorPasteImage"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.attachment')">
|
|
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
|
|
v-model:file-list="editForm.files"
|
|
|
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
|
|
|
:limit="1"
|
|
|
|
|
|
|
|
:on-exceed="handleAttachmentExceed"
|
|
|
|
|
|
|
|
:on-change="handleAttachmentChange"
|
|
|
|
|
|
|
|
:on-remove="handleAttachmentRemove"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<el-button type="primary" plain>{{ $t("btn.upload") }}</el-button>
|
|
|
|
|
|
|
|
<template #tip>
|
|
|
|
|
|
|
|
<div class="el-upload__tip">
|
|
|
|
|
|
|
|
{{ $t("label.attachmentTip") }}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
|
|
<el-button @click="editVisible = false">{{ $t("common.cancel") }}</el-button>
|
|
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
|
|
:loading="editSubmitting"
|
|
|
|
|
|
|
|
@click="submitEdit"
|
|
|
|
|
|
|
|
>{{ $t("common.save") }}</el-button>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
<el-dialog
|
|
|
|
v-model="previewVisible"
|
|
|
|
v-model="previewVisible"
|
|
|
|
:title="$t('btn.preview')"
|
|
|
|
:title="$t('btn.preview')"
|
|
|
|
@ -94,12 +267,29 @@
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
<script setup>
|
|
|
|
import { onMounted, ref } from "vue";
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
|
|
onBeforeUnmount,
|
|
|
|
|
|
|
|
onMounted,
|
|
|
|
|
|
|
|
reactive,
|
|
|
|
|
|
|
|
ref,
|
|
|
|
|
|
|
|
shallowRef,
|
|
|
|
|
|
|
|
} from "vue";
|
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
import { ArrowLeft, Document } from "@element-plus/icons-vue";
|
|
|
|
import { ArrowLeft, Document } from "@element-plus/icons-vue";
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
import { getAdminOrderDetail, getUploadFile } from "@/service/modular/admin";
|
|
|
|
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
|
|
|
|
|
|
|
import "@wangeditor/editor/dist/css/style.css";
|
|
|
|
|
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
|
|
getAdminOrderDetail,
|
|
|
|
|
|
|
|
getAdminHospitals,
|
|
|
|
|
|
|
|
getUploadFile,
|
|
|
|
|
|
|
|
updateAdminOrder,
|
|
|
|
|
|
|
|
} from "@/service/modular/admin";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { t: $t } = useI18n();
|
|
|
|
const route = useRoute();
|
|
|
|
const route = useRoute();
|
|
|
|
const router = useRouter();
|
|
|
|
const router = useRouter();
|
|
|
|
const loading = ref(false);
|
|
|
|
const loading = ref(false);
|
|
|
|
@ -119,7 +309,6 @@ const loadDetail = async () => {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const goBack = () => router.back();
|
|
|
|
const goBack = () => router.back();
|
|
|
|
const goProcess = () => router.push(`/admin/orders/process/${route.params.id}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const previewVisible = ref(false);
|
|
|
|
const previewVisible = ref(false);
|
|
|
|
const previewUrl = ref("");
|
|
|
|
const previewUrl = ref("");
|
|
|
|
@ -168,7 +357,6 @@ const downloadBlob = (blob, fileName) => {
|
|
|
|
const viewReport = async (attachmentPath, originalName) => {
|
|
|
|
const viewReport = async (attachmentPath, originalName) => {
|
|
|
|
if (!attachmentPath) return;
|
|
|
|
if (!attachmentPath) return;
|
|
|
|
const path = String(attachmentPath);
|
|
|
|
const path = String(attachmentPath);
|
|
|
|
// 优先用原始文件名作为下载名,没有则回退到路径
|
|
|
|
|
|
|
|
const downloadName = originalName || path;
|
|
|
|
const downloadName = originalName || path;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const blob = await getUploadFile(path);
|
|
|
|
const blob = await getUploadFile(path);
|
|
|
|
@ -186,7 +374,372 @@ const viewReport = async (attachmentPath, originalName) => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(loadDetail);
|
|
|
|
// ============== 编辑工单 ==============
|
|
|
|
|
|
|
|
const hospitals = ref([]);
|
|
|
|
|
|
|
|
// 颜色标签可选值(颜色 Hex 取自设计原型)
|
|
|
|
|
|
|
|
const colorOptions = [
|
|
|
|
|
|
|
|
{ label: "蓝", value: "#667eea" },
|
|
|
|
|
|
|
|
{ label: "绿", value: "#52c41a" },
|
|
|
|
|
|
|
|
{ label: "青", value: "#13c2c2" },
|
|
|
|
|
|
|
|
{ label: "紫", value: "#722ed1" },
|
|
|
|
|
|
|
|
{ label: "橙", value: "#fa8c16" },
|
|
|
|
|
|
|
|
{ label: "红", value: "#ff4d4f" },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const loadHospitals = async () => {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const res = await getAdminHospitals({ page: 1, pageSize: 1000 });
|
|
|
|
|
|
|
|
hospitals.value = res?.list || [];
|
|
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
|
|
hospitals.value = [];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const editVisible = ref(false);
|
|
|
|
|
|
|
|
const editLoading = ref(false);
|
|
|
|
|
|
|
|
const editSubmitting = ref(false);
|
|
|
|
|
|
|
|
const editFormRef = ref();
|
|
|
|
|
|
|
|
// 已加载的原始附件 filePath 集合(用于判断用户是否需要保留原附件)
|
|
|
|
|
|
|
|
const editOriginalAttachments = ref([]);
|
|
|
|
|
|
|
|
// 颜色选择器
|
|
|
|
|
|
|
|
const editColorPickerVisible = ref(false);
|
|
|
|
|
|
|
|
const editColorPickerRef = ref();
|
|
|
|
|
|
|
|
const toggleEditColorPicker = () => {
|
|
|
|
|
|
|
|
editColorPickerVisible.value = !editColorPickerVisible.value;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const selectEditColor = (val) => {
|
|
|
|
|
|
|
|
editForm.colorTag = val;
|
|
|
|
|
|
|
|
editColorPickerVisible.value = false;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDocClick = (e) => {
|
|
|
|
|
|
|
|
if (!editColorPickerVisible.value) return;
|
|
|
|
|
|
|
|
const el = editColorPickerRef.value;
|
|
|
|
|
|
|
|
if (el && !el.contains(e.target)) {
|
|
|
|
|
|
|
|
editColorPickerVisible.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const editForm = reactive({
|
|
|
|
|
|
|
|
hospitalId: undefined,
|
|
|
|
|
|
|
|
title: "",
|
|
|
|
|
|
|
|
serviceType: "",
|
|
|
|
|
|
|
|
priority: "中",
|
|
|
|
|
|
|
|
feedbackchannel: "",
|
|
|
|
|
|
|
|
department: "",
|
|
|
|
|
|
|
|
colorTag: "#667eea",
|
|
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
|
|
submitter: "",
|
|
|
|
|
|
|
|
createdat: "",
|
|
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
|
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// el-upload 事件:选文件后立即校验大小
|
|
|
|
|
|
|
|
const handleAttachmentChange = (file) => {
|
|
|
|
|
|
|
|
if (file?.size > MAX_FILE_SIZE) {
|
|
|
|
|
|
|
|
ElMessage.warning("文件不能超过 50MB");
|
|
|
|
|
|
|
|
editForm.files = editForm.files.filter(
|
|
|
|
|
|
|
|
(f) => f.uid !== file.uid || f.size <= MAX_FILE_SIZE,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleAttachmentExceed = () => {
|
|
|
|
|
|
|
|
ElMessage.warning("只能上传 1 个文件");
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleAttachmentRemove = () => {
|
|
|
|
|
|
|
|
// el-upload 会自动从 v-model:file-list 移除
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const editRules = {
|
|
|
|
|
|
|
|
hospitalId: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseSelectHospital"),
|
|
|
|
|
|
|
|
trigger: "change",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
title: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseInputTitle"),
|
|
|
|
|
|
|
|
trigger: "blur",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
serviceType: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseSelectType"),
|
|
|
|
|
|
|
|
trigger: "blur",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
priority: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseSelectPriority"),
|
|
|
|
|
|
|
|
trigger: "change",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
feedbackchannel: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseSelectFeedbackChannel"),
|
|
|
|
|
|
|
|
trigger: "change",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
department: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
whitespace: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseInputDept"),
|
|
|
|
|
|
|
|
trigger: "blur",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
submitter: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
whitespace: true,
|
|
|
|
|
|
|
|
message: () => $t("msg.pleaseInputName"),
|
|
|
|
|
|
|
|
trigger: "blur",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
description: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
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("msg.pleaseInputDescription")));
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
trigger: "change",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 富文本编辑器配置
|
|
|
|
|
|
|
|
const editorRef = shallowRef();
|
|
|
|
|
|
|
|
const toolbarConfig = {
|
|
|
|
|
|
|
|
excludeKeys: [
|
|
|
|
|
|
|
|
"uploadImage",
|
|
|
|
|
|
|
|
"insertVideo",
|
|
|
|
|
|
|
|
"uploadVideo",
|
|
|
|
|
|
|
|
"group-video",
|
|
|
|
|
|
|
|
"emotion",
|
|
|
|
|
|
|
|
"group-emoji",
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// 从剪贴板中捞出图片文件(同时兼容 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;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.onerror = () => {
|
|
|
|
|
|
|
|
ElMessage.error("图片处理失败");
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const editorConfig = {
|
|
|
|
|
|
|
|
placeholder: $t("placeholder.description"),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
let pasteHandlerRef = null;
|
|
|
|
|
|
|
|
const handleEditorCreated = (editor) => {
|
|
|
|
|
|
|
|
editorRef.value = editor;
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
|
|
|
|
|
|
|
|
if (!root) return;
|
|
|
|
|
|
|
|
const editable = root.querySelector?.('[contenteditable="true"]') || root;
|
|
|
|
|
|
|
|
if (pasteHandlerRef) {
|
|
|
|
|
|
|
|
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
pasteHandlerRef = (event) => handleEditorPasteImage(editor, event);
|
|
|
|
|
|
|
|
editable.addEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 从带路径分隔符的地址中截取最后的文件名
|
|
|
|
|
|
|
|
const getAttachmentFileName = (path) => {
|
|
|
|
|
|
|
|
if (!path) return "";
|
|
|
|
|
|
|
|
return String(path).replace(/\\/g, "/").split("/").pop() || "";
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const resetEditForm = () => {
|
|
|
|
|
|
|
|
Object.assign(editForm, {
|
|
|
|
|
|
|
|
hospitalId: undefined,
|
|
|
|
|
|
|
|
title: "",
|
|
|
|
|
|
|
|
serviceType: "",
|
|
|
|
|
|
|
|
priority: "中",
|
|
|
|
|
|
|
|
feedbackchannel: "",
|
|
|
|
|
|
|
|
department: "",
|
|
|
|
|
|
|
|
colorTag: "#667eea",
|
|
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
|
|
submitter: "",
|
|
|
|
|
|
|
|
createdat: "",
|
|
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
editOriginalAttachments.value = [];
|
|
|
|
|
|
|
|
editFormRef.value?.clearValidate();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const openEdit = () => {
|
|
|
|
|
|
|
|
resetEditForm();
|
|
|
|
|
|
|
|
editLoading.value = true;
|
|
|
|
|
|
|
|
editVisible.value = true;
|
|
|
|
|
|
|
|
// 延迟一帧等弹窗挂载,避免富文本在未挂载时设置 v-model 报错
|
|
|
|
|
|
|
|
nextTick(async () => {
|
|
|
|
|
|
|
|
const d = detail.value || {};
|
|
|
|
|
|
|
|
editForm.hospitalId = hospitals.value.find((h) => h.name === d.hospitalName)?.id || undefined;
|
|
|
|
|
|
|
|
editForm.title = d.title || "";
|
|
|
|
|
|
|
|
editForm.serviceType = d.serviceType || "";
|
|
|
|
|
|
|
|
editForm.priority = d.priority || "中";
|
|
|
|
|
|
|
|
editForm.feedbackchannel = d.feedbackChannel || d.feedbackchannel || "";
|
|
|
|
|
|
|
|
editForm.department = d.department || "";
|
|
|
|
|
|
|
|
editForm.colorTag = d.colorTag || "#667eea";
|
|
|
|
|
|
|
|
editForm.description = d.description || "";
|
|
|
|
|
|
|
|
editForm.submitter = d.submitter || "";
|
|
|
|
|
|
|
|
// 后端返回时间可能含 T,统一格式化为 YYYY-MM-DD HH:mm:ss 供 date-picker 回显
|
|
|
|
|
|
|
|
editForm.createdat = d.createdAt
|
|
|
|
|
|
|
|
? dayjs(d.createdAt).format("YYYY-MM-DD HH:mm:ss")
|
|
|
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
// 附件回显:把详情里的第一个附件作为 el-upload 的"已上传"项展示
|
|
|
|
|
|
|
|
const atts = d.attachments || [];
|
|
|
|
|
|
|
|
editOriginalAttachments.value = atts.map((a) => a.filePath).filter(Boolean);
|
|
|
|
|
|
|
|
if (atts.length) {
|
|
|
|
|
|
|
|
const first = atts[0];
|
|
|
|
|
|
|
|
const fileName = first.fileName || getAttachmentFileName(first.filePath);
|
|
|
|
|
|
|
|
if (fileName) {
|
|
|
|
|
|
|
|
editForm.files = [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
uid: `attachment-${route.params.id}-0`,
|
|
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
|
|
status: "success",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
editLoading.value = false;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onEditClosed = () => {
|
|
|
|
|
|
|
|
resetEditForm();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const submitEdit = async () => {
|
|
|
|
|
|
|
|
editForm.department = (editForm.department || "").trim();
|
|
|
|
|
|
|
|
editForm.submitter = (editForm.submitter || "").trim();
|
|
|
|
|
|
|
|
await editFormRef.value.validate(async (valid) => {
|
|
|
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
|
|
|
editSubmitting.value = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
|
|
const { files, ...rest } = editForm;
|
|
|
|
|
|
|
|
for (const [k, v] of Object.entries(rest)) {
|
|
|
|
|
|
|
|
if (v !== undefined && v !== null && v !== "") {
|
|
|
|
|
|
|
|
formData.append(k, v);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// 仅在用户上传了新文件时才把 files 追加到 formData,
|
|
|
|
|
|
|
|
// 这样后端看到 files 就视为替换附件,未带 files 则保留原附件
|
|
|
|
|
|
|
|
const item = (files || [])[0];
|
|
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
|
|
const file = item.raw || (typeof item.size === "number" ? item : null);
|
|
|
|
|
|
|
|
if (file) formData.append("files", file, item.name);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
await updateAdminOrder(route.params.id, formData);
|
|
|
|
|
|
|
|
ElMessage.success($t("msg.editSuccess"));
|
|
|
|
|
|
|
|
editVisible.value = false;
|
|
|
|
|
|
|
|
loadDetail();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
editSubmitting.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const removePasteListener = () => {
|
|
|
|
|
|
|
|
const editor = editorRef.value;
|
|
|
|
|
|
|
|
if (!editor || !pasteHandlerRef) return;
|
|
|
|
|
|
|
|
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
|
|
|
|
|
|
|
|
if (!root) return;
|
|
|
|
|
|
|
|
const editable = root.querySelector?.('[contenteditable="true"]') || root;
|
|
|
|
|
|
|
|
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
|
|
loadDetail();
|
|
|
|
|
|
|
|
loadHospitals();
|
|
|
|
|
|
|
|
document.addEventListener("click", onDocClick);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
document.removeEventListener("click", onDocClick);
|
|
|
|
|
|
|
|
removePasteListener();
|
|
|
|
|
|
|
|
pasteHandlerRef = null;
|
|
|
|
|
|
|
|
const editor = editorRef.value;
|
|
|
|
|
|
|
|
if (editor == null) return;
|
|
|
|
|
|
|
|
editor.destroy();
|
|
|
|
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
<style scoped lang="scss">
|
|
|
|
@ -254,4 +807,119 @@ onMounted(loadDetail);
|
|
|
|
margin: 0 auto;
|
|
|
|
margin: 0 auto;
|
|
|
|
object-fit: contain;
|
|
|
|
object-fit: contain;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor {
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
// 用 box-shadow 模拟边框,避免 wangEditor 内部元素覆盖 border
|
|
|
|
|
|
|
|
// box-shadow: 0 0 0 1px #dcdfe6 inset;
|
|
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.rich-editor :deep(.w-e-toolbar) {
|
|
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
|
|
border-bottom: 1px solid #dcdfe6;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.rich-editor :deep(.w-e-text-container) {
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// 标题 + 颜色标签
|
|
|
|
|
|
|
|
.title-row {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.title-row .el-input {
|
|
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// 优先级 + 反馈渠道同一行展示
|
|
|
|
|
|
|
|
.inline-row {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 0 24px;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.inline-row .inline-item {
|
|
|
|
|
|
|
|
flex: 1 1 0;
|
|
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
margin-bottom: 0 !important;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.inline-row .inline-item :deep(.el-form-item__content) {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-picker {
|
|
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-preview {
|
|
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
border: 2px solid #fff;
|
|
|
|
|
|
|
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
|
transition:
|
|
|
|
|
|
|
|
transform 0.2s ease,
|
|
|
|
|
|
|
|
box-shadow 0.2s ease;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-preview:hover {
|
|
|
|
|
|
|
|
transform: scale(1.08);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-preview.is-empty {
|
|
|
|
|
|
|
|
background-image: linear-gradient(
|
|
|
|
|
|
|
|
135deg,
|
|
|
|
|
|
|
|
#f5f5f5 25%,
|
|
|
|
|
|
|
|
transparent 25%,
|
|
|
|
|
|
|
|
transparent 50%,
|
|
|
|
|
|
|
|
#f5f5f5 50%,
|
|
|
|
|
|
|
|
#f5f5f5 75%,
|
|
|
|
|
|
|
|
transparent 75%
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
background-size: 8px 8px;
|
|
|
|
|
|
|
|
box-shadow: 0 0 0 1px #dcdfe6;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-dropdown {
|
|
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
|
|
top: calc(100% + 8px);
|
|
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
|
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-title {
|
|
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-options {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-option {
|
|
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
|
|
transition:
|
|
|
|
|
|
|
|
transform 0.2s ease,
|
|
|
|
|
|
|
|
border-color 0.2s ease;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-option:hover {
|
|
|
|
|
|
|
|
transform: scale(1.12);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.color-tag-option.active {
|
|
|
|
|
|
|
|
border-color: #1890ff;
|
|
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|
|
|
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</style>
|
|
|
|
|