|
|
<template>
|
|
|
<div class="order-list">
|
|
|
<el-card shadow="never">
|
|
|
<el-form :inline="true" :model="filters" class="filter-form">
|
|
|
<el-form-item :label="$t('label.hospitalName')">
|
|
|
<el-select
|
|
|
v-model="filters.hospitalId"
|
|
|
:placeholder="$t('common.all')"
|
|
|
clearable
|
|
|
filterable
|
|
|
style="width: 200px"
|
|
|
>
|
|
|
<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('table.status')">
|
|
|
<el-select
|
|
|
v-model="filters.status"
|
|
|
:placeholder="$t('common.all')"
|
|
|
clearable
|
|
|
style="width: 140px"
|
|
|
>
|
|
|
<el-option label="待处理" value="待处理" />
|
|
|
<el-option label="已完成" value="已完成" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item :label="$t('table.priority')">
|
|
|
<el-select
|
|
|
v-model="filters.priority"
|
|
|
:placeholder="$t('common.all')"
|
|
|
clearable
|
|
|
style="width: 120px"
|
|
|
>
|
|
|
<el-option label="高" value="高" />
|
|
|
<el-option label="中" value="中" />
|
|
|
<el-option label="低" value="低" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item :label="$t('table.type')">
|
|
|
<el-select
|
|
|
v-model="filters.type"
|
|
|
:placeholder="$t('common.all')"
|
|
|
clearable
|
|
|
style="width: 120px"
|
|
|
>
|
|
|
<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="$t('placeholder.keyword')">
|
|
|
<el-input
|
|
|
v-model="filters.keyword"
|
|
|
:placeholder="$t('placeholder.keyword')"
|
|
|
clearable
|
|
|
style="width: 200px"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item :label="$t('table.time')">
|
|
|
<el-date-picker
|
|
|
v-model="dateRange"
|
|
|
type="daterange"
|
|
|
range-separator="-"
|
|
|
:start-placeholder="$t('common.startDate')"
|
|
|
:end-placeholder="$t('common.endDate')"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
style="width: 240px"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button type="primary" @click="onSearch">{{
|
|
|
$t("btn.query")
|
|
|
}}</el-button>
|
|
|
<el-button @click="onReset">{{ $t("btn.reset") }}</el-button>
|
|
|
<el-button type="success" @click="openQuick">{{
|
|
|
$t("btn.quickAddOrder")
|
|
|
}}</el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<!-- 工单总览 -->
|
|
|
<div class="order-stats">
|
|
|
<div
|
|
|
class="stat-card stat-total"
|
|
|
:class="{ active: activeStat === 'total' }"
|
|
|
@click="onStatClick('total')"
|
|
|
>
|
|
|
<div class="stat-icon">📋</div>
|
|
|
<div class="stat-value">{{ stats.total }}</div>
|
|
|
<div class="stat-label">{{ $t("stat.totalOrders") }}</div>
|
|
|
</div>
|
|
|
<div
|
|
|
class="stat-card stat-pending"
|
|
|
:class="{ active: activeStat === 'pending' }"
|
|
|
@click="onStatClick('pending')"
|
|
|
>
|
|
|
<div class="stat-icon">⏳</div>
|
|
|
<div class="stat-value">{{ stats.pending }}</div>
|
|
|
<div class="stat-label">{{ $t("stat.pendingOrders") }}</div>
|
|
|
</div>
|
|
|
<div
|
|
|
class="stat-card stat-completed"
|
|
|
:class="{ active: activeStat === 'completed' }"
|
|
|
@click="onStatClick('completed')"
|
|
|
>
|
|
|
<div class="stat-icon">✅</div>
|
|
|
<div class="stat-value">{{ stats.completed }}</div>
|
|
|
<div class="stat-label">{{ $t("stat.completedOrders") }}</div>
|
|
|
</div>
|
|
|
<div
|
|
|
class="stat-card stat-high"
|
|
|
:class="{ active: activeStat === 'high' }"
|
|
|
@click="onStatClick('high')"
|
|
|
>
|
|
|
<div class="stat-icon">🔴</div>
|
|
|
<div class="stat-value">{{ stats.highPriority }}</div>
|
|
|
<div class="stat-label">{{ $t("stat.highPriorityOrders") }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<el-table :data="list" v-loading="loading" border stripe>
|
|
|
<el-table-column :label="$t('table.id')" prop="orderNo" width="160" />
|
|
|
<el-table-column
|
|
|
:label="$t('table.hospital')"
|
|
|
prop="hospitalName"
|
|
|
min-width="160"
|
|
|
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
|
|
|
:label="$t('table.title')"
|
|
|
prop="title"
|
|
|
min-width="300"
|
|
|
show-overflow-tooltip
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.type')"
|
|
|
prop="serviceType"
|
|
|
width="120"
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.dept')"
|
|
|
prop="department"
|
|
|
min-width="120"
|
|
|
show-overflow-tooltip
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.submitter')"
|
|
|
prop="submitter"
|
|
|
min-width="100"
|
|
|
show-overflow-tooltip
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.feedbackchannel')"
|
|
|
prop="feedbackChannel"
|
|
|
min-width="100"
|
|
|
show-overflow-tooltip
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.registrar')"
|
|
|
prop="registrarName"
|
|
|
min-width="100"
|
|
|
show-overflow-tooltip
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.handler')"
|
|
|
prop="processName"
|
|
|
min-width="100"
|
|
|
show-overflow-tooltip
|
|
|
/>
|
|
|
<el-table-column
|
|
|
:label="$t('table.handleTime')"
|
|
|
prop="completedAt"
|
|
|
width="170"
|
|
|
>
|
|
|
<template #default="{ row }">
|
|
|
{{ row.completedAt || "-" }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column :label="$t('table.status')" width="100">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="statusType(row.status)" size="small">{{
|
|
|
row.status
|
|
|
}}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column
|
|
|
:label="$t('table.time')"
|
|
|
prop="createdAt"
|
|
|
width="170"
|
|
|
/>
|
|
|
<el-table-column :label="$t('table.action')" width="120" fixed="right">
|
|
|
<template #default="{ row }">
|
|
|
<el-button link type="primary" @click="goDetail(row.id)">{{
|
|
|
$t("btn.detail")
|
|
|
}}</el-button>
|
|
|
<el-button
|
|
|
link
|
|
|
:type="row.status === '已完成' ? 'warning' : 'success'"
|
|
|
@click="goProcess(row.id)"
|
|
|
>{{
|
|
|
row.status === "已完成" ? $t("btn.modify") : $t("btn.process")
|
|
|
}}</el-button
|
|
|
>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
<div class="pagination">
|
|
|
<el-pagination
|
|
|
v-model:current-page="page"
|
|
|
v-model:page-size="pageSize"
|
|
|
:page-sizes="[10, 20, 50]"
|
|
|
:total="total"
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
@size-change="loadList"
|
|
|
@current-change="loadList"
|
|
|
/>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
|
|
|
<!-- 快速提交工单 弹窗 -->
|
|
|
<el-dialog
|
|
|
v-model="quickVisible"
|
|
|
:title="$t('modal.submit')"
|
|
|
width="640px"
|
|
|
:close-on-click-modal="false"
|
|
|
@closed="onQuickClosed"
|
|
|
>
|
|
|
<el-form
|
|
|
ref="quickFormRef"
|
|
|
:model="quickForm"
|
|
|
:rules="quickRules"
|
|
|
label-width="100px"
|
|
|
>
|
|
|
<el-form-item :label="$t('label.hospitalName')" prop="hospitalId">
|
|
|
<el-select
|
|
|
v-model="quickForm.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="quickForm.title"
|
|
|
:placeholder="$t('placeholder.title')"
|
|
|
maxlength="100"
|
|
|
show-word-limit
|
|
|
/>
|
|
|
<div class="color-tag-picker" ref="colorPickerRef">
|
|
|
<div
|
|
|
class="color-tag-preview"
|
|
|
:style="{ background: quickForm.colorTag || '#dcdfe6' }"
|
|
|
:class="{ 'is-empty': !quickForm.colorTag }"
|
|
|
@click.stop="toggleColorPicker"
|
|
|
></div>
|
|
|
<div
|
|
|
v-show="colorPickerVisible"
|
|
|
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: quickForm.colorTag === c.value }"
|
|
|
:style="{ background: c.value }"
|
|
|
:title="c.label"
|
|
|
@click="selectColor(c.value)"
|
|
|
></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
<el-form-item :label="$t('label.type')" prop="serviceType">
|
|
|
<el-select
|
|
|
v-model="quickForm.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="quickForm.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="quickForm.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="quickForm.department"
|
|
|
:placeholder="$t('placeholder.dept')"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item :label="$t('label.name')" prop="submitter">
|
|
|
<el-input
|
|
|
v-model="quickForm.submitter"
|
|
|
:placeholder="$t('placeholder.name')"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item :label="$t('label.createat')" prop="createdat">
|
|
|
<el-date-picker
|
|
|
v-model="quickForm.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="quickForm.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="quickForm.files"
|
|
|
:auto-upload="false"
|
|
|
:limit="1"
|
|
|
:on-exceed="handleAttachmentExceed"
|
|
|
:on-remove="handleAttachmentRemove"
|
|
|
:on-change="handleAttachmentChange"
|
|
|
>
|
|
|
<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="quickVisible = false">{{
|
|
|
$t("common.cancel")
|
|
|
}}</el-button>
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
:loading="quickSubmitting"
|
|
|
@click="submitQuick"
|
|
|
>{{ $t("common.submit") }}</el-button
|
|
|
>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import dayjs from "dayjs";
|
|
|
import {
|
|
|
nextTick,
|
|
|
onBeforeUnmount,
|
|
|
onMounted,
|
|
|
reactive,
|
|
|
ref,
|
|
|
shallowRef,
|
|
|
} from "vue";
|
|
|
import { useRouter } from "vue-router";
|
|
|
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 {
|
|
|
createAdminOrder,
|
|
|
getAdminHospitals,
|
|
|
getAdminOrders,
|
|
|
} from "@/service/modular/admin";
|
|
|
import { useUserStore } from "@/stores/api/user";
|
|
|
|
|
|
const { t: $t } = useI18n();
|
|
|
const router = useRouter();
|
|
|
const userStore = useUserStore();
|
|
|
const list = ref([]);
|
|
|
const hospitals = ref([]);
|
|
|
const total = ref(0);
|
|
|
const page = ref(1);
|
|
|
const pageSize = ref(10);
|
|
|
const loading = ref(false);
|
|
|
const dateRange = ref([]);
|
|
|
const filters = reactive({
|
|
|
hospitalId: undefined,
|
|
|
status: "",
|
|
|
priority: "",
|
|
|
type: "",
|
|
|
keyword: "",
|
|
|
});
|
|
|
|
|
|
const loadHospitals = async () => {
|
|
|
try {
|
|
|
const res = await getAdminHospitals({ page: 1, pageSize: 1000 });
|
|
|
hospitals.value = res?.list || [];
|
|
|
} catch (_) {
|
|
|
hospitals.value = [];
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const priorityType = (p) =>
|
|
|
p === "高" ? "danger" : p === "中" ? "warning" : "info";
|
|
|
const statusType = (s) =>
|
|
|
({ 待处理: "warning", 处理中: "primary", 已完成: "success", 已取消: "info" })[
|
|
|
s
|
|
|
] || "info";
|
|
|
|
|
|
// 工单总览统计
|
|
|
const stats = reactive({
|
|
|
total: 0,
|
|
|
pending: 0,
|
|
|
completed: 0,
|
|
|
highPriority: 0,
|
|
|
});
|
|
|
// 当前选中的总览卡片:total | pending | completed | high
|
|
|
const activeStat = ref("total");
|
|
|
const parseStats = (res) => {
|
|
|
let payload = res?.data;
|
|
|
if (typeof payload === "string") {
|
|
|
try {
|
|
|
payload = JSON.parse(payload);
|
|
|
} catch (_) {
|
|
|
payload = {};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
stats.completed = Number(payload?.TotalStatus1 ?? 0);
|
|
|
stats.pending = Number(payload?.TotalStatus2 ?? 0);
|
|
|
stats.total = stats.completed + stats.pending;
|
|
|
stats.highPriority = Number(payload?.TotalPriority ?? 0);
|
|
|
};
|
|
|
|
|
|
const loadList = async () => {
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
const params = { page: page.value, pageSize: pageSize.value, ...filters };
|
|
|
if (dateRange.value?.length === 2) {
|
|
|
params.startDate = dateRange.value[0];
|
|
|
params.endDate = dateRange.value[1];
|
|
|
}
|
|
|
const res = await getAdminOrders(params);
|
|
|
list.value = res?.list || [];
|
|
|
list.value.forEach((item) => {
|
|
|
if (item.createdAt) {
|
|
|
item.createdAt = dayjs(item.createdAt).format("YYYY-MM-DD HH:mm:ss");
|
|
|
}
|
|
|
if (item.completedAt) {
|
|
|
item.completedAt = dayjs(item.completedAt).format(
|
|
|
"YYYY-MM-DD HH:mm:ss",
|
|
|
);
|
|
|
}
|
|
|
});
|
|
|
total.value = res?.total || 0;
|
|
|
parseStats(res);
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 总览卡片点击:根据卡片类型设置筛选条件并重新加载
|
|
|
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 = () => {
|
|
|
page.value = 1;
|
|
|
loadList();
|
|
|
};
|
|
|
const onReset = () => {
|
|
|
Object.assign(filters, {
|
|
|
hospitalId: undefined,
|
|
|
status: "",
|
|
|
priority: "",
|
|
|
type: "",
|
|
|
keyword: "",
|
|
|
});
|
|
|
dateRange.value = [];
|
|
|
activeStat.value = "total";
|
|
|
onSearch();
|
|
|
};
|
|
|
|
|
|
const goDetail = (id) => router.push(`/admin/orders/detail/${id}`);
|
|
|
const goProcess = (id) => router.push(`/admin/orders/process/${id}`);
|
|
|
|
|
|
// 颜色标签可选值(颜色 Hex 取自设计原型)
|
|
|
const colorOptions = [
|
|
|
{ label: "蓝", value: "#667eea" },
|
|
|
{ label: "绿", value: "#52c41a" },
|
|
|
{ label: "青", value: "#13c2c2" },
|
|
|
{ label: "紫", value: "#722ed1" },
|
|
|
{ label: "橙", value: "#fa8c16" },
|
|
|
{ label: "红", value: "#ff4d4f" },
|
|
|
];
|
|
|
const colorPickerVisible = ref(false);
|
|
|
const colorPickerRef = ref();
|
|
|
const toggleColorPicker = () => {
|
|
|
colorPickerVisible.value = !colorPickerVisible.value;
|
|
|
};
|
|
|
const selectColor = (val) => {
|
|
|
quickForm.colorTag = val;
|
|
|
colorPickerVisible.value = false;
|
|
|
};
|
|
|
const onDocClick = (e) => {
|
|
|
if (!colorPickerVisible.value) return;
|
|
|
const el = colorPickerRef.value;
|
|
|
if (el && !el.contains(e.target)) {
|
|
|
colorPickerVisible.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 快速提交工单(弹窗)
|
|
|
const quickVisible = ref(false);
|
|
|
const quickSubmitting = ref(false);
|
|
|
const quickFormRef = ref();
|
|
|
const quickForm = reactive({
|
|
|
hospitalId: undefined,
|
|
|
title: "",
|
|
|
serviceType: "",
|
|
|
priority: "中",
|
|
|
feedbackchannel: "",
|
|
|
department: "",
|
|
|
colorTag: "#667eea",
|
|
|
description: "",
|
|
|
submitter: "",
|
|
|
createdat: "",
|
|
|
files: [],
|
|
|
});
|
|
|
// 生成当前时间(带时分秒)作为提交时间默认值
|
|
|
const nowDatetime = () => dayjs().format("YYYY-MM-DD HH:mm:ss");
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
// 粘贴到富文本的图片大小上限(base64 内联存储,避免 HTML 过大)
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
|
|
// 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,
|
|
|
);
|
|
|
}
|
|
|
};
|
|
|
// el-upload 事件:超过 limit 时的回调
|
|
|
const handleAttachmentExceed = () => {
|
|
|
ElMessage.warning("只能上传 1 个文件");
|
|
|
};
|
|
|
// el-upload 事件:删除文件
|
|
|
const handleAttachmentRemove = () => {
|
|
|
// el-upload 会自动从 v-model:file-list 移除,这里无需额外处理
|
|
|
};
|
|
|
const quickRules = {
|
|
|
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 resetQuickForm = () => {
|
|
|
Object.assign(quickForm, {
|
|
|
hospitalId: undefined,
|
|
|
title: "",
|
|
|
serviceType: "",
|
|
|
priority: "中",
|
|
|
feedbackchannel: "",
|
|
|
department: "",
|
|
|
colorTag: "#667eea",
|
|
|
description: "",
|
|
|
submitter: "",
|
|
|
createdat: "",
|
|
|
files: [],
|
|
|
});
|
|
|
quickFormRef.value?.clearValidate();
|
|
|
};
|
|
|
|
|
|
const openQuick = () => {
|
|
|
// 打开弹窗时自动填入当前时间作为提交时间默认值
|
|
|
quickForm.createdat = nowDatetime();
|
|
|
quickVisible.value = true;
|
|
|
};
|
|
|
|
|
|
const onQuickClosed = () => {
|
|
|
resetQuickForm();
|
|
|
};
|
|
|
|
|
|
const submitQuick = async () => {
|
|
|
// dept / submitter 去掉两端空格后再校验,避免仅含空格的“伪必填”绕过
|
|
|
quickForm.department = (quickForm.department || "").trim();
|
|
|
quickForm.submitter = (quickForm.submitter || "").trim();
|
|
|
await quickFormRef.value.validate(async (valid) => {
|
|
|
if (!valid) return;
|
|
|
quickSubmitting.value = true;
|
|
|
try {
|
|
|
const formData = new FormData();
|
|
|
const { files, ...rest } = quickForm;
|
|
|
for (const [k, v] of Object.entries(rest)) {
|
|
|
if (v !== undefined && v !== null && v !== "") {
|
|
|
formData.append(k, v);
|
|
|
}
|
|
|
}
|
|
|
formData.append("registrarName", userStore.userInfo?.userName || "");
|
|
|
// el-upload 的 file-list 元素 raw 字段就是 File 对象
|
|
|
for (const f of files || []) {
|
|
|
if (f.raw) formData.append("files", f.raw, f.name);
|
|
|
}
|
|
|
await createAdminOrder(formData);
|
|
|
ElMessage.success($t("msg.submitSuccess"));
|
|
|
quickVisible.value = false;
|
|
|
loadList();
|
|
|
} finally {
|
|
|
quickSubmitting.value = false;
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
loadHospitals();
|
|
|
loadList();
|
|
|
document.addEventListener("click", onDocClick);
|
|
|
});
|
|
|
|
|
|
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 });
|
|
|
};
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
document.removeEventListener("click", onDocClick);
|
|
|
removePasteListener();
|
|
|
pasteHandlerRef = null;
|
|
|
const editor = editorRef.value;
|
|
|
if (editor == null) return;
|
|
|
editor.destroy();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.filter-form {
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
.pagination {
|
|
|
margin-top: 16px;
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
.rich-editor {
|
|
|
border: 1px solid #dcdfe6;
|
|
|
border-radius: 4px;
|
|
|
width: 100%;
|
|
|
overflow: hidden;
|
|
|
background: #fff;
|
|
|
}
|
|
|
.rich-editor :deep(.w-e-toolbar) {
|
|
|
background: #fafafa;
|
|
|
}
|
|
|
|
|
|
// 标题 + 颜色标签
|
|
|
.title-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.title-row .el-input {
|
|
|
flex: 1;
|
|
|
}
|
|
|
// 优先级 + 反馈渠道同一行展示
|
|
|
.inline-row {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
gap: 0 24px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.inline-row .inline-item {
|
|
|
margin-bottom: 0 !important;
|
|
|
}
|
|
|
.inline-row .inline-item :deep(.el-form-item__content) {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
.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);
|
|
|
}
|
|
|
.color-tag-clear {
|
|
|
background: #f5f5f5 !important;
|
|
|
position: relative;
|
|
|
}
|
|
|
.color-tag-clear::before,
|
|
|
.color-tag-clear::after {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
left: 50%;
|
|
|
width: 14px;
|
|
|
height: 2px;
|
|
|
background: #999;
|
|
|
transform-origin: center;
|
|
|
}
|
|
|
.color-tag-clear::before {
|
|
|
transform: translate(-50%, -50%) rotate(45deg);
|
|
|
}
|
|
|
.color-tag-clear::after {
|
|
|
transform: translate(-50%, -50%) rotate(-45deg);
|
|
|
}
|
|
|
|
|
|
// 工单总览统计
|
|
|
.order-stats {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
gap: 16px;
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
.stat-card {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 14px;
|
|
|
padding: 18px 20px;
|
|
|
background: #fff;
|
|
|
border-radius: 10px;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
|
border-left: 4px solid #d9d9d9;
|
|
|
transition:
|
|
|
transform 0.2s ease,
|
|
|
box-shadow 0.2s ease,
|
|
|
background 0.2s ease;
|
|
|
cursor: pointer;
|
|
|
user-select: none;
|
|
|
}
|
|
|
.stat-card:hover {
|
|
|
transform: translateY(-2px);
|
|
|
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 {
|
|
|
border-left-color: #1890ff;
|
|
|
}
|
|
|
.stat-pending {
|
|
|
border-left-color: #fa8c16;
|
|
|
}
|
|
|
.stat-completed {
|
|
|
border-left-color: #52c41a;
|
|
|
}
|
|
|
.stat-high {
|
|
|
border-left-color: #ff4d4f;
|
|
|
}
|
|
|
.stat-icon {
|
|
|
font-size: 28px;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
.stat-value {
|
|
|
font-size: 26px;
|
|
|
font-weight: 700;
|
|
|
color: #333;
|
|
|
line-height: 1.1;
|
|
|
}
|
|
|
.stat-label {
|
|
|
font-size: 13px;
|
|
|
color: #999;
|
|
|
margin-top: 4px;
|
|
|
}
|
|
|
</style> |