|
|
|
@ -85,22 +85,38 @@
|
|
|
|
</el-form>
|
|
|
|
</el-form>
|
|
|
|
<!-- 工单总览 -->
|
|
|
|
<!-- 工单总览 -->
|
|
|
|
<div class="order-stats">
|
|
|
|
<div class="order-stats">
|
|
|
|
<div class="stat-card stat-total">
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="stat-card stat-total"
|
|
|
|
|
|
|
|
:class="{ active: activeStat === 'total' }"
|
|
|
|
|
|
|
|
@click="onStatClick('total')"
|
|
|
|
|
|
|
|
>
|
|
|
|
<div class="stat-icon">📋</div>
|
|
|
|
<div class="stat-icon">📋</div>
|
|
|
|
<div class="stat-value">{{ stats.total }}</div>
|
|
|
|
<div class="stat-value">{{ stats.total }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.totalOrders") }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.totalOrders") }}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="stat-card stat-pending">
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="stat-card stat-pending"
|
|
|
|
|
|
|
|
:class="{ active: activeStat === 'pending' }"
|
|
|
|
|
|
|
|
@click="onStatClick('pending')"
|
|
|
|
|
|
|
|
>
|
|
|
|
<div class="stat-icon">⏳</div>
|
|
|
|
<div class="stat-icon">⏳</div>
|
|
|
|
<div class="stat-value">{{ stats.pending }}</div>
|
|
|
|
<div class="stat-value">{{ stats.pending }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.pendingOrders") }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.pendingOrders") }}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="stat-card stat-completed">
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="stat-card stat-completed"
|
|
|
|
|
|
|
|
:class="{ active: activeStat === 'completed' }"
|
|
|
|
|
|
|
|
@click="onStatClick('completed')"
|
|
|
|
|
|
|
|
>
|
|
|
|
<div class="stat-icon">✅</div>
|
|
|
|
<div class="stat-icon">✅</div>
|
|
|
|
<div class="stat-value">{{ stats.completed }}</div>
|
|
|
|
<div class="stat-value">{{ stats.completed }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.completedOrders") }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.completedOrders") }}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="stat-card stat-high">
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="stat-card stat-high"
|
|
|
|
|
|
|
|
:class="{ active: activeStat === 'high' }"
|
|
|
|
|
|
|
|
@click="onStatClick('high')"
|
|
|
|
|
|
|
|
>
|
|
|
|
<div class="stat-icon">🔴</div>
|
|
|
|
<div class="stat-icon">🔴</div>
|
|
|
|
<div class="stat-value">{{ stats.highPriority }}</div>
|
|
|
|
<div class="stat-value">{{ stats.highPriority }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.highPriorityOrders") }}</div>
|
|
|
|
<div class="stat-label">{{ $t("stat.highPriorityOrders") }}</div>
|
|
|
|
@ -114,10 +130,17 @@
|
|
|
|
min-width="160"
|
|
|
|
min-width="160"
|
|
|
|
show-overflow-tooltip
|
|
|
|
show-overflow-tooltip
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
<el-table-column :label="$t('table.priority')" width="90">
|
|
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
|
|
<el-tag :type="priorityType(row.priority)" size="small">{{
|
|
|
|
|
|
|
|
row.priority
|
|
|
|
|
|
|
|
}}</el-tag>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</el-table-column>
|
|
|
|
<el-table-column
|
|
|
|
<el-table-column
|
|
|
|
:label="$t('table.title')"
|
|
|
|
:label="$t('table.title')"
|
|
|
|
prop="title"
|
|
|
|
prop="title"
|
|
|
|
min-width="200"
|
|
|
|
min-width="300"
|
|
|
|
show-overflow-tooltip
|
|
|
|
show-overflow-tooltip
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
<el-table-column
|
|
|
|
<el-table-column
|
|
|
|
@ -155,14 +178,7 @@
|
|
|
|
width="170"
|
|
|
|
width="170"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<template #default="{ row }">
|
|
|
|
<template #default="{ row }">
|
|
|
|
{{ row.completedAt || '-' }}
|
|
|
|
{{ row.completedAt || "-" }}
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
<el-table-column :label="$t('table.priority')" width="90">
|
|
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
|
|
<el-tag :type="priorityType(row.priority)" size="small">{{
|
|
|
|
|
|
|
|
row.priority
|
|
|
|
|
|
|
|
}}</el-tag>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</el-table-column>
|
|
|
|
</el-table-column>
|
|
|
|
<el-table-column :label="$t('table.status')" width="100">
|
|
|
|
<el-table-column :label="$t('table.status')" width="100">
|
|
|
|
@ -184,10 +200,11 @@
|
|
|
|
}}</el-button>
|
|
|
|
}}</el-button>
|
|
|
|
<el-button
|
|
|
|
<el-button
|
|
|
|
link
|
|
|
|
link
|
|
|
|
type="success"
|
|
|
|
:type="row.status === '已完成' ? 'warning' : 'success'"
|
|
|
|
v-if="row.status !== '已完成'"
|
|
|
|
|
|
|
|
@click="goProcess(row.id)"
|
|
|
|
@click="goProcess(row.id)"
|
|
|
|
>{{ $t("btn.process") }}</el-button
|
|
|
|
>{{
|
|
|
|
|
|
|
|
row.status === "已完成" ? $t("btn.modify") : $t("btn.process")
|
|
|
|
|
|
|
|
}}</el-button
|
|
|
|
>
|
|
|
|
>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</el-table-column>
|
|
|
|
</el-table-column>
|
|
|
|
@ -290,18 +307,18 @@
|
|
|
|
<el-radio-button label="低">低</el-radio-button>
|
|
|
|
<el-radio-button label="低">低</el-radio-button>
|
|
|
|
</el-radio-group>
|
|
|
|
</el-radio-group>
|
|
|
|
</el-form-item>
|
|
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="$t('label.name')">
|
|
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
|
|
v-model="quickForm.submitter"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.name')"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.dept')">
|
|
|
|
<el-form-item :label="$t('label.dept')">
|
|
|
|
<el-input
|
|
|
|
<el-input
|
|
|
|
v-model="quickForm.department"
|
|
|
|
v-model="quickForm.department"
|
|
|
|
:placeholder="$t('placeholder.dept')"
|
|
|
|
:placeholder="$t('placeholder.dept')"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</el-form-item>
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
<el-form-item :label="$t('label.name')">
|
|
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
|
|
v-model="quickForm.submitter"
|
|
|
|
|
|
|
|
:placeholder="$t('placeholder.name')"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="$t('label.description')" prop="description">
|
|
|
|
<el-form-item :label="$t('label.description')" prop="description">
|
|
|
|
<div class="rich-editor">
|
|
|
|
<div class="rich-editor">
|
|
|
|
<Toolbar
|
|
|
|
<Toolbar
|
|
|
|
@ -321,28 +338,21 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</el-form-item>
|
|
|
|
</el-form-item>
|
|
|
|
<el-form-item :label="$t('label.attachment')">
|
|
|
|
<el-form-item :label="$t('label.attachment')">
|
|
|
|
<div class="attachment-pick">
|
|
|
|
<el-upload
|
|
|
|
<el-input
|
|
|
|
v-model:file-list="quickForm.files"
|
|
|
|
:model-value="quickForm.files[0]?.name || ''"
|
|
|
|
:auto-upload="false"
|
|
|
|
:placeholder="$t('upload.text')"
|
|
|
|
:limit="1"
|
|
|
|
readonly
|
|
|
|
:on-exceed="handleAttachmentExceed"
|
|
|
|
clearable
|
|
|
|
:on-remove="handleAttachmentRemove"
|
|
|
|
@click="triggerFilePicker"
|
|
|
|
:on-change="handleAttachmentChange"
|
|
|
|
@clear="clearFile"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<el-button type="primary" plain>{{ $t("btn.upload") }}</el-button>
|
|
|
|
<template #append>
|
|
|
|
<template #tip>
|
|
|
|
<el-button @click.stop="triggerFilePicker">{{
|
|
|
|
<div class="el-upload__tip">
|
|
|
|
$t("btn.upload")
|
|
|
|
{{ $t("label.attachmentTip") }}
|
|
|
|
}}</el-button>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</el-input>
|
|
|
|
</el-upload>
|
|
|
|
<input
|
|
|
|
|
|
|
|
ref="fileInputRef"
|
|
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
|
|
class="attachment-hidden-input"
|
|
|
|
|
|
|
|
@change="onFileChange"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</el-form-item>
|
|
|
|
</el-form-item>
|
|
|
|
</el-form>
|
|
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
|
|
<template #footer>
|
|
|
|
@ -361,10 +371,11 @@
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
<script setup>
|
|
|
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
nextTick,
|
|
|
|
nextTick,
|
|
|
|
onBeforeUnmount,
|
|
|
|
onBeforeUnmount,
|
|
|
|
onMounted,
|
|
|
|
onMounted,
|
|
|
|
reactive,
|
|
|
|
reactive,
|
|
|
|
ref,
|
|
|
|
ref,
|
|
|
|
shallowRef,
|
|
|
|
shallowRef,
|
|
|
|
@ -422,6 +433,8 @@ const stats = reactive({
|
|
|
|
completed: 0,
|
|
|
|
completed: 0,
|
|
|
|
highPriority: 0,
|
|
|
|
highPriority: 0,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// 当前选中的总览卡片:total | pending | completed | high
|
|
|
|
|
|
|
|
const activeStat = ref("total");
|
|
|
|
const parseStats = (res) => {
|
|
|
|
const parseStats = (res) => {
|
|
|
|
stats.total = res?.total || 0;
|
|
|
|
stats.total = res?.total || 0;
|
|
|
|
let payload = res?.data;
|
|
|
|
let payload = res?.data;
|
|
|
|
@ -449,10 +462,12 @@ const loadList = async () => {
|
|
|
|
list.value = res?.list || [];
|
|
|
|
list.value = res?.list || [];
|
|
|
|
list.value.forEach((item) => {
|
|
|
|
list.value.forEach((item) => {
|
|
|
|
if (item.createdAt) {
|
|
|
|
if (item.createdAt) {
|
|
|
|
item.createdAt = item.createdAt.split("T").join(" ");
|
|
|
|
item.createdAt = dayjs(item.createdAt).format("YYYY-MM-DD HH:mm:ss");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (item.completedAt) {
|
|
|
|
if (item.completedAt) {
|
|
|
|
item.completedAt = item.completedAt.split("T").join(" ");
|
|
|
|
item.completedAt = dayjs(item.completedAt).format(
|
|
|
|
|
|
|
|
"YYYY-MM-DD HH:mm:ss",
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
total.value = res?.total || 0;
|
|
|
|
total.value = res?.total || 0;
|
|
|
|
@ -462,6 +477,26 @@ const loadList = async () => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 总览卡片点击:根据卡片类型设置筛选条件并重新加载
|
|
|
|
|
|
|
|
const onStatClick = (type) => {
|
|
|
|
|
|
|
|
activeStat.value = type;
|
|
|
|
|
|
|
|
if (type === "total") {
|
|
|
|
|
|
|
|
filters.status = "";
|
|
|
|
|
|
|
|
filters.priority = "";
|
|
|
|
|
|
|
|
} else if (type === "pending") {
|
|
|
|
|
|
|
|
filters.status = "待处理";
|
|
|
|
|
|
|
|
filters.priority = "";
|
|
|
|
|
|
|
|
} else if (type === "completed") {
|
|
|
|
|
|
|
|
filters.status = "已完成";
|
|
|
|
|
|
|
|
filters.priority = "";
|
|
|
|
|
|
|
|
} else if (type === "high") {
|
|
|
|
|
|
|
|
filters.status = "";
|
|
|
|
|
|
|
|
filters.priority = "高";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
|
|
loadList();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onSearch = () => {
|
|
|
|
const onSearch = () => {
|
|
|
|
page.value = 1;
|
|
|
|
page.value = 1;
|
|
|
|
loadList();
|
|
|
|
loadList();
|
|
|
|
@ -475,6 +510,7 @@ const onReset = () => {
|
|
|
|
keyword: "",
|
|
|
|
keyword: "",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
dateRange.value = [];
|
|
|
|
dateRange.value = [];
|
|
|
|
|
|
|
|
activeStat.value = "total";
|
|
|
|
onSearch();
|
|
|
|
onSearch();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@ -522,22 +558,27 @@ const quickForm = reactive({
|
|
|
|
submitter: "",
|
|
|
|
submitter: "",
|
|
|
|
files: [],
|
|
|
|
files: [],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const fileInputRef = ref();
|
|
|
|
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
// 粘贴到富文本的图片大小上限(base64 内联存储,避免 HTML 过大)
|
|
|
|
// 粘贴到富文本的图片大小上限(base64 内联存储,避免 HTML 过大)
|
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
const triggerFilePicker = () => {
|
|
|
|
|
|
|
|
fileInputRef.value?.click();
|
|
|
|
// el-upload 事件:选文件后立即校验大小
|
|
|
|
|
|
|
|
const handleAttachmentChange = (file) => {
|
|
|
|
|
|
|
|
if (file?.size > MAX_FILE_SIZE) {
|
|
|
|
|
|
|
|
ElMessage.warning("文件不能超过 50MB");
|
|
|
|
|
|
|
|
// 把超限文件从列表里移除
|
|
|
|
|
|
|
|
quickForm.files = quickForm.files.filter(
|
|
|
|
|
|
|
|
(f) => f.uid !== file.uid || f.size <= MAX_FILE_SIZE,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const onFileChange = (e) => {
|
|
|
|
// el-upload 事件:超过 limit 时的回调
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
const handleAttachmentExceed = () => {
|
|
|
|
e.target.value = "";
|
|
|
|
ElMessage.warning("只能上传 1 个文件");
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
if (file.size > MAX_FILE_SIZE) return;
|
|
|
|
|
|
|
|
quickForm.files = [{ name: file.name, size: file.size, raw: file }];
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const clearFile = () => {
|
|
|
|
// el-upload 事件:删除文件
|
|
|
|
quickForm.files = [];
|
|
|
|
const handleAttachmentRemove = () => {
|
|
|
|
|
|
|
|
// el-upload 会自动从 v-model:file-list 移除,这里无需额外处理
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const quickRules = {
|
|
|
|
const quickRules = {
|
|
|
|
hospitalId: [
|
|
|
|
hospitalId: [
|
|
|
|
@ -620,7 +661,7 @@ const collectPastedImages = (clipboardData) => {
|
|
|
|
return files;
|
|
|
|
return files;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 共用的图片粘贴处理逻辑(customPaste 和原生监听都走这里,用事件标记防重复)
|
|
|
|
// 共用的图片粘贴处理逻辑
|
|
|
|
const handleEditorPasteImage = (editor, event) => {
|
|
|
|
const handleEditorPasteImage = (editor, event) => {
|
|
|
|
if (event.__wpsImageHandled) return;
|
|
|
|
if (event.__wpsImageHandled) return;
|
|
|
|
const imageFiles = collectPastedImages(event.clipboardData);
|
|
|
|
const imageFiles = collectPastedImages(event.clipboardData);
|
|
|
|
@ -630,15 +671,13 @@ const handleEditorPasteImage = (editor, event) => {
|
|
|
|
if (typeof event.stopPropagation === "function") event.stopPropagation();
|
|
|
|
if (typeof event.stopPropagation === "function") event.stopPropagation();
|
|
|
|
if (typeof event.stopImmediatePropagation === "function")
|
|
|
|
if (typeof event.stopImmediatePropagation === "function")
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
// ElMessage.info(`检测到 ${imageFiles.length} 张粘贴图片,正在处理...`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 同步先拿到当前 selection,等 FileReader 异步回调里恢复
|
|
|
|
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
const savedSelection = editor.selection;
|
|
|
|
const savedSelection = editor.selection;
|
|
|
|
|
|
|
|
|
|
|
|
imageFiles.forEach((file) => {
|
|
|
|
imageFiles.forEach((file) => {
|
|
|
|
if (file.size > MAX_PASTE_IMAGE_SIZE) {
|
|
|
|
if (file.size > MAX_PASTE_IMAGE_SIZE) {
|
|
|
|
ElMessage.warning("粘贴的图片不能超过 5MB,请使用下方附件上传");
|
|
|
|
ElMessage.warning("粘贴的图片不能超过 5MB");
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
|
|
const reader = new FileReader();
|
|
|
|
@ -649,7 +688,6 @@ const handleEditorPasteImage = (editor, event) => {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 恢复保存的选区
|
|
|
|
|
|
|
|
if (savedSelection) editor.select(savedSelection);
|
|
|
|
if (savedSelection) editor.select(savedSelection);
|
|
|
|
} catch (_) {
|
|
|
|
} catch (_) {
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
@ -658,32 +696,26 @@ const handleEditorPasteImage = (editor, event) => {
|
|
|
|
editor.dangerouslyInsertHtml(
|
|
|
|
editor.dangerouslyInsertHtml(
|
|
|
|
`<img src="${dataUrl}" style="max-width:100%;height:auto;" />`,
|
|
|
|
`<img src="${dataUrl}" style="max-width:100%;height:auto;" />`,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
// 手动触发一次更新保证 v-model 同步
|
|
|
|
|
|
|
|
if (typeof editor.updateView === "function") editor.updateView();
|
|
|
|
if (typeof editor.updateView === "function") editor.updateView();
|
|
|
|
// ElMessage.success("图片插入成功");
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
reader.onerror = () => {
|
|
|
|
reader.onerror = () => {
|
|
|
|
ElMessage.error("图片处理失败");
|
|
|
|
ElMessage.error("图片处理失败");
|
|
|
|
};
|
|
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return false; // 告诉 wangEditor 跳过默认处理(customPaste 调用时生效)
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:customPaste 不能放在 default-config 里(会被 wangEditor for Vue 检测到抛错)
|
|
|
|
|
|
|
|
// 已改用 <Editor> 上的 @custom-paste 事件 + contentEditable 原生监听双重保障
|
|
|
|
|
|
|
|
const editorConfig = {
|
|
|
|
const editorConfig = {
|
|
|
|
placeholder: $t("placeholder.description"),
|
|
|
|
placeholder: $t("placeholder.description"),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
let pasteHandlerRef = null;
|
|
|
|
let pasteHandlerRef = null;
|
|
|
|
const handleEditorCreated = (editor) => {
|
|
|
|
const handleEditorCreated = (editor) => {
|
|
|
|
editorRef.value = editor;
|
|
|
|
editorRef.value = editor;
|
|
|
|
// 额外在 contentEditable 上绑一份 capture 监听(防 customPaste 未生效的兜底)
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
nextTick(() => {
|
|
|
|
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
|
|
|
|
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
|
|
|
|
if (!root) return;
|
|
|
|
if (!root) return;
|
|
|
|
const editable =
|
|
|
|
const editable = root.querySelector?.('[contenteditable="true"]') || root;
|
|
|
|
root.querySelector?.('[contenteditable="true"]') || root;
|
|
|
|
|
|
|
|
if (pasteHandlerRef) {
|
|
|
|
if (pasteHandlerRef) {
|
|
|
|
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -728,8 +760,9 @@ const submitQuick = async () => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
formData.append("registrarName", userStore.userInfo?.userName || "");
|
|
|
|
formData.append("registrarName", userStore.userInfo?.userName || "");
|
|
|
|
|
|
|
|
// el-upload 的 file-list 元素 raw 字段就是 File 对象
|
|
|
|
for (const f of files || []) {
|
|
|
|
for (const f of files || []) {
|
|
|
|
formData.append("files", f.raw, f.name);
|
|
|
|
if (f.raw) formData.append("files", f.raw, f.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await createAdminOrder(formData);
|
|
|
|
await createAdminOrder(formData);
|
|
|
|
ElMessage.success($t("msg.submitSuccess"));
|
|
|
|
ElMessage.success($t("msg.submitSuccess"));
|
|
|
|
@ -888,14 +921,6 @@ onBeforeUnmount(() => {
|
|
|
|
transform: translate(-50%, -50%) rotate(-45deg);
|
|
|
|
transform: translate(-50%, -50%) rotate(-45deg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 附件上传
|
|
|
|
|
|
|
|
.attachment-pick {
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.attachment-hidden-input {
|
|
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 工单总览统计
|
|
|
|
// 工单总览统计
|
|
|
|
.order-stats {
|
|
|
|
.order-stats {
|
|
|
|
display: grid;
|
|
|
|
display: grid;
|
|
|
|
@ -915,13 +940,32 @@ onBeforeUnmount(() => {
|
|
|
|
border-left: 4px solid #d9d9d9;
|
|
|
|
border-left: 4px solid #d9d9d9;
|
|
|
|
transition:
|
|
|
|
transition:
|
|
|
|
transform 0.2s ease,
|
|
|
|
transform 0.2s ease,
|
|
|
|
box-shadow 0.2s ease;
|
|
|
|
box-shadow 0.2s ease,
|
|
|
|
cursor: default;
|
|
|
|
background 0.2s ease;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
user-select: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.stat-card:hover {
|
|
|
|
.stat-card:hover {
|
|
|
|
transform: translateY(-2px);
|
|
|
|
transform: translateY(-2px);
|
|
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
|
|
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.stat-card.active {
|
|
|
|
|
|
|
|
background: #f0f7ff;
|
|
|
|
|
|
|
|
box-shadow: 0 4px 14px rgba(24, 144, 255, 0.18);
|
|
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stat-card.active.stat-pending {
|
|
|
|
|
|
|
|
background: #fff7e6;
|
|
|
|
|
|
|
|
box-shadow: 0 4px 14px rgba(250, 140, 22, 0.18);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stat-card.active.stat-completed {
|
|
|
|
|
|
|
|
background: #f6ffed;
|
|
|
|
|
|
|
|
box-shadow: 0 4px 14px rgba(82, 196, 26, 0.18);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stat-card.active.stat-high {
|
|
|
|
|
|
|
|
background: #fff1f0;
|
|
|
|
|
|
|
|
box-shadow: 0 4px 14px rgba(255, 77, 79, 0.18);
|
|
|
|
|
|
|
|
}
|
|
|
|
.stat-total {
|
|
|
|
.stat-total {
|
|
|
|
border-left-color: #1890ff;
|
|
|
|
border-left-color: #1890ff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|