|
|
|
|
@ -2,7 +2,7 @@
|
|
|
|
|
<div class="quick-submit">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span class="card-title">{{ $t('modal.submit') }}</span>
|
|
|
|
|
<span class="card-title">{{ isEdit ? '编辑工单' : $t('modal.submit') }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<el-form
|
|
|
|
|
@ -10,90 +10,557 @@
|
|
|
|
|
:model="form"
|
|
|
|
|
:rules="rules"
|
|
|
|
|
label-width="120px"
|
|
|
|
|
style="max-width: 720px"
|
|
|
|
|
>
|
|
|
|
|
<el-form-item :label="$t('label.title')" prop="title">
|
|
|
|
|
<el-input v-model="form.title" :placeholder="$t('placeholder.title')" maxlength="100" show-word-limit />
|
|
|
|
|
<div class="title-row">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="form.title"
|
|
|
|
|
:placeholder="$t('placeholder.title')"
|
|
|
|
|
maxlength="100"
|
|
|
|
|
show-word-limit
|
|
|
|
|
/>
|
|
|
|
|
<div class="color-tag-picker" ref="colorPickerRef">
|
|
|
|
|
<div
|
|
|
|
|
class="color-tag-preview"
|
|
|
|
|
:style="{ background: form.colorTag || '#dcdfe6' }"
|
|
|
|
|
:class="{ 'is-empty': !form.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: form.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="form.serviceType" :placeholder="$t('placeholder.type')" style="width: 240px">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="form.serviceType"
|
|
|
|
|
:placeholder="$t('placeholder.type')"
|
|
|
|
|
clearable
|
|
|
|
|
style="width: 240px"
|
|
|
|
|
>
|
|
|
|
|
<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('label.priority')" prop="priority">
|
|
|
|
|
<el-radio-group v-model="form.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 label-width="0">
|
|
|
|
|
<div class="inline-row">
|
|
|
|
|
<el-form-item :label="$t('label.priority')" prop="priority" class="inline-item">
|
|
|
|
|
<el-radio-group v-model="form.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="form.feedbackchannel"
|
|
|
|
|
:placeholder="$t('placeholder.feedbackchannel')"
|
|
|
|
|
clearable
|
|
|
|
|
style="width: 200px"
|
|
|
|
|
>
|
|
|
|
|
<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')">
|
|
|
|
|
<el-input v-model="form.department" :placeholder="$t('placeholder.dept')" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item :label="$t('label.colorTag')">
|
|
|
|
|
<el-radio-group v-model="form.colorTag">
|
|
|
|
|
<el-radio-button label="">无</el-radio-button>
|
|
|
|
|
<el-radio-button label="red"><span style="color:#F4664A">红</span></el-radio-button>
|
|
|
|
|
<el-radio-button label="yellow"><span style="color:#F6BD16">黄</span></el-radio-button>
|
|
|
|
|
<el-radio-button label="blue"><span style="color:#5B8FF9">蓝</span></el-radio-button>
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
<el-form-item :label="$t('label.dept')" prop="department">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="form.department"
|
|
|
|
|
:placeholder="$t('placeholder.dept')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item :label="$t('label.description')" prop="description">
|
|
|
|
|
<el-form-item :label="$t('label.name')" prop="submitter">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="form.description"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="5"
|
|
|
|
|
:placeholder="$t('placeholder.description')"
|
|
|
|
|
v-model="form.submitter"
|
|
|
|
|
:placeholder="$t('placeholder.name')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item :label="$t('label.createat')" prop="createdat">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="form.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="form.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 class="fujian" :label="$t('label.attachment')">
|
|
|
|
|
<el-upload
|
|
|
|
|
v-model:file-list="form.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-item>
|
|
|
|
|
<el-button type="primary" :loading="submitting" @click="submit">{{ $t('common.submit') }}</el-button>
|
|
|
|
|
<el-button @click="onCancel">{{ $t('common.cancel') }}</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<!-- 提交工单后 +1 积分奖励弹窗 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="rewardVisible"
|
|
|
|
|
width="360px"
|
|
|
|
|
:show-close="false"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
:close-on-press-escape="false"
|
|
|
|
|
append-to-body
|
|
|
|
|
class="reward-dialog"
|
|
|
|
|
>
|
|
|
|
|
<div class="reward-content">
|
|
|
|
|
<div class="reward-badge">✦ 奖励已发放</div>
|
|
|
|
|
<div class="coin-wrapper">
|
|
|
|
|
<span class="reward-coin">🪙</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="points-number">
|
|
|
|
|
+1 <small>积分</small>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="reward-title">🎉 到账通知</div>
|
|
|
|
|
<div class="reward-sub">1 积分已放入您的钱包</div>
|
|
|
|
|
|
|
|
|
|
<div class="reward-progress">
|
|
|
|
|
<div class="reward-progress-label">
|
|
|
|
|
<span>🏆 距兑换「康小虎 AI 吉祥物」</span>
|
|
|
|
|
<span class="reward-distance">
|
|
|
|
|
<template v-if="rewardDistance > 0">
|
|
|
|
|
还差 {{ rewardDistance }} 分
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>已达标 🎉</template>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="reward-bar-bg">
|
|
|
|
|
<div
|
|
|
|
|
class="reward-bar-fill"
|
|
|
|
|
:style="{ width: rewardProgress + '%' }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="reward-current">
|
|
|
|
|
当前 {{ rewardTotalPoints }} 分
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="reward-actions">
|
|
|
|
|
<el-button class="reward-btn reward-btn-received" @click="onRewardClose">
|
|
|
|
|
开心收下
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button class="reward-btn reward-btn-assets" type="warning" @click="goMyAssets">
|
|
|
|
|
查看我的资产 →
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { reactive, ref } from "vue";
|
|
|
|
|
import { useRouter } from "vue-router";
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
import {
|
|
|
|
|
nextTick,
|
|
|
|
|
onBeforeUnmount,
|
|
|
|
|
reactive,
|
|
|
|
|
ref,
|
|
|
|
|
shallowRef,
|
|
|
|
|
computed,
|
|
|
|
|
watch,
|
|
|
|
|
onMounted,
|
|
|
|
|
} from "vue";
|
|
|
|
|
import { useRouter,useRoute } 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 { createHospitalOrder } from "@/service/modular/hospital";
|
|
|
|
|
import {
|
|
|
|
|
createHospitalOrder,
|
|
|
|
|
getHospitalOrderDetail,
|
|
|
|
|
updateHospitalOrder,
|
|
|
|
|
getPoints,
|
|
|
|
|
} from "@/service/modular/hospital";
|
|
|
|
|
|
|
|
|
|
// 兑换目标(与 points 页保持一致)
|
|
|
|
|
const EXCHANGE_GOAL = 500;
|
|
|
|
|
|
|
|
|
|
const { t: $t } = useI18n();
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const formRef = ref();
|
|
|
|
|
const submitting = ref(false);
|
|
|
|
|
|
|
|
|
|
// 编辑模式:url 带 :id 参数
|
|
|
|
|
const isEdit = computed(() => !!route.params.id);
|
|
|
|
|
const editId = computed(() => route.params.id);
|
|
|
|
|
|
|
|
|
|
// 从带路径分隔符的地址中截取最后的文件名
|
|
|
|
|
const getAttachmentFileName = (path) => {
|
|
|
|
|
if (!path) return "";
|
|
|
|
|
return String(path).replace(/\\/g, "/").split("/").pop() || "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 已加载的原始附件 filePath 集合(用于判断用户是否需要保留原附件)
|
|
|
|
|
const originalAttachments = ref([]);
|
|
|
|
|
|
|
|
|
|
// 根据后端返回填充表单(编辑模式)
|
|
|
|
|
const fillFormFromDetail = (detail) => {
|
|
|
|
|
if (!detail) return;
|
|
|
|
|
form.title = detail.title || "";
|
|
|
|
|
form.serviceType = detail.serviceType || "";
|
|
|
|
|
form.priority = detail.priority || "中";
|
|
|
|
|
form.feedbackchannel = detail.feedbackChannel || "";
|
|
|
|
|
form.department = detail.department || "";
|
|
|
|
|
form.colorTag = detail.colorTag || "#667eea";
|
|
|
|
|
form.description = detail.description || "";
|
|
|
|
|
form.submitter = detail.submitter || "";
|
|
|
|
|
form.createdat = detail.createdAt
|
|
|
|
|
? dayjs(detail.createdAt).format("YYYY-MM-DD HH:mm:ss")
|
|
|
|
|
: "";
|
|
|
|
|
// 附件回显:把详情里的第一个附件作为 el-upload 的“已上传”项展示
|
|
|
|
|
const atts = detail.attachments || [];
|
|
|
|
|
originalAttachments.value = atts.map((a) => a.filePath).filter(Boolean);
|
|
|
|
|
if (atts.length) {
|
|
|
|
|
const first = atts[0];
|
|
|
|
|
const fileName = first.fileName || getAttachmentFileName(first.filePath);
|
|
|
|
|
if (fileName) {
|
|
|
|
|
form.files = [
|
|
|
|
|
{
|
|
|
|
|
uid: `attachment-${editId.value}-0`,
|
|
|
|
|
name: fileName,
|
|
|
|
|
status: "success",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
form.files = [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadDetail = async (id) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getHospitalOrderDetail(id);
|
|
|
|
|
fillFormFromDetail(res);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
ElMessage.error("加载工单详情失败");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// id 变化时重新加载(同一组件跨路由复用场景)
|
|
|
|
|
watch(
|
|
|
|
|
() => route.params.id,
|
|
|
|
|
(id) => {
|
|
|
|
|
if (id) {
|
|
|
|
|
loadDetail(id);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
title: "",
|
|
|
|
|
serviceType: "",
|
|
|
|
|
priority: "中",
|
|
|
|
|
feedbackchannel: "",
|
|
|
|
|
department: "",
|
|
|
|
|
colorTag: "",
|
|
|
|
|
colorTag: "#667eea",
|
|
|
|
|
description: "",
|
|
|
|
|
submitter: "",
|
|
|
|
|
createdat: "",
|
|
|
|
|
files: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rules = {
|
|
|
|
|
title: [{ required: true, message: () => $t('msg.pleaseInputTitle'), trigger: "blur" }],
|
|
|
|
|
serviceType: [{ required: true, message: () => $t('msg.pleaseSelectType'), trigger: "change" }],
|
|
|
|
|
priority: [{ required: true, message: () => $t('msg.pleaseSelectPriority'), trigger: "change" }],
|
|
|
|
|
description: [{ required: true, message: () => $t('msg.pleaseInputDescription'), trigger: "blur" }],
|
|
|
|
|
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 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) => {
|
|
|
|
|
form.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 MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
const handleAttachmentChange = (file) => {
|
|
|
|
|
if (file?.size > MAX_FILE_SIZE) {
|
|
|
|
|
ElMessage.warning("文件不能超过 50MB");
|
|
|
|
|
form.files = form.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 nowDatetime = () => dayjs().format("YYYY-MM-DD HH:mm:ss");
|
|
|
|
|
|
|
|
|
|
// 富文本编辑器配置
|
|
|
|
|
const editorRef = shallowRef();
|
|
|
|
|
const toolbarConfig = {
|
|
|
|
|
excludeKeys: [
|
|
|
|
|
"uploadImage",
|
|
|
|
|
"insertVideo",
|
|
|
|
|
"uploadVideo",
|
|
|
|
|
"group-video",
|
|
|
|
|
"emotion",
|
|
|
|
|
"group-emoji",
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ============== 提交后奖励弹窗 ==============
|
|
|
|
|
// 后端在创建工单时已自动 +1 积分,这里仅刷新最新积分并提示用户
|
|
|
|
|
const rewardVisible = ref(false);
|
|
|
|
|
const rewardTotalPoints = ref(0);
|
|
|
|
|
const rewardProgress = ref(0); // 进度条宽度 (%)
|
|
|
|
|
const rewardDistance = ref(0); // 还差多少分
|
|
|
|
|
|
|
|
|
|
const playCoinAnimation = () => {
|
|
|
|
|
// 重播动画:先移除 class 再加回去(仅做 minimal 动画)
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
const coin = document.querySelector(".reward-coin");
|
|
|
|
|
if (!coin) return;
|
|
|
|
|
coin.classList.remove("spin");
|
|
|
|
|
void coin.offsetWidth;
|
|
|
|
|
coin.classList.add("spin");
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const showRewardDialog = async () => {
|
|
|
|
|
let total = 0;
|
|
|
|
|
try {
|
|
|
|
|
const res = await getPoints();
|
|
|
|
|
const data = res?.data ?? res ?? {};
|
|
|
|
|
total = Number(data.totalPoints) || 0;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// 刷新失败不阻塞
|
|
|
|
|
}
|
|
|
|
|
rewardTotalPoints.value = total;
|
|
|
|
|
rewardProgress.value = Math.min(100, Math.round((total / EXCHANGE_GOAL) * 100));
|
|
|
|
|
rewardDistance.value = Math.max(0, EXCHANGE_GOAL - total);
|
|
|
|
|
rewardVisible.value = true;
|
|
|
|
|
playCoinAnimation();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onRewardClose = () => {
|
|
|
|
|
rewardVisible.value = false;
|
|
|
|
|
router.replace("/hospital/orders");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const goMyAssets = () => {
|
|
|
|
|
rewardVisible.value = false;
|
|
|
|
|
router.replace("/hospital/points");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
form.department = (form.department || "").trim();
|
|
|
|
|
form.submitter = (form.submitter || "").trim();
|
|
|
|
|
await formRef.value.validate(async (valid) => {
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
submitting.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await createHospitalOrder(form);
|
|
|
|
|
ElMessage.success($t('msg.submitSuccess'));
|
|
|
|
|
router.replace("/hospital/orders");
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
const { files, ...rest } = form;
|
|
|
|
|
for (const [k, v] of Object.entries(rest)) {
|
|
|
|
|
if (v !== undefined && v !== null && v !== "") {
|
|
|
|
|
formData.append(k, v);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const f of files || []) {
|
|
|
|
|
if (f.raw) formData.append("files", f.raw, f.name);
|
|
|
|
|
}
|
|
|
|
|
if (isEdit.value) {
|
|
|
|
|
await updateHospitalOrder(editId.value, formData);
|
|
|
|
|
ElMessage.success("编辑成功");
|
|
|
|
|
router.replace("/hospital/orders");
|
|
|
|
|
} else {
|
|
|
|
|
await createHospitalOrder(formData);
|
|
|
|
|
ElMessage.success($t('msg.submitSuccess'));
|
|
|
|
|
// 新建成功 → 弹出 +1 积分奖励提示,后端已自动加分
|
|
|
|
|
showRewardDialog();
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
submitting.value = false;
|
|
|
|
|
}
|
|
|
|
|
@ -101,11 +568,306 @@ const submit = async () => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onCancel = () => router.back();
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
document.addEventListener("click", onDocClick);
|
|
|
|
|
if (isEdit.value) {
|
|
|
|
|
// 编辑模式:加载详情填充表单
|
|
|
|
|
loadDetail(editId.value);
|
|
|
|
|
} else {
|
|
|
|
|
// 新增模式:默认提交时间为当前时间
|
|
|
|
|
form.createdat = nowDatetime();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
.quick-submit {
|
|
|
|
|
:deep(.el-card) {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.el-form {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.card-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 限制上传文件预览盒子宽度溢出
|
|
|
|
|
:deep(.fujian .el-form-item__content > div) {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============== 提交后奖励弹窗 ==============
|
|
|
|
|
:deep(.reward-dialog) {
|
|
|
|
|
.el-dialog__header {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
.el-dialog__body {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.reward-content {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 24px 24px 22px;
|
|
|
|
|
}
|
|
|
|
|
.reward-badge {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
background: #fef3e6;
|
|
|
|
|
color: #c47a2e;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
padding: 4px 16px;
|
|
|
|
|
border-radius: 40px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
.coin-wrapper {
|
|
|
|
|
height: 80px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
.reward-coin {
|
|
|
|
|
font-size: 64px;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
filter: drop-shadow(0 8px 16px rgba(255, 193, 7, 0.3));
|
|
|
|
|
}
|
|
|
|
|
.reward-coin.spin {
|
|
|
|
|
animation: rewardCoinSpin 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
|
|
|
}
|
|
|
|
|
@keyframes rewardCoinSpin {
|
|
|
|
|
0% {
|
|
|
|
|
transform: rotateY(0deg) scale(0.2);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
60% {
|
|
|
|
|
transform: rotateY(720deg) scale(1.2);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
100% {
|
|
|
|
|
transform: rotateY(720deg) scale(1);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.points-number {
|
|
|
|
|
font-size: 48px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: #f5a623;
|
|
|
|
|
line-height: 1.1;
|
|
|
|
|
margin-top: -4px;
|
|
|
|
|
}
|
|
|
|
|
.points-number small {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #b8863a;
|
|
|
|
|
}
|
|
|
|
|
.reward-title {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #1f140e;
|
|
|
|
|
margin: 4px 0 6px;
|
|
|
|
|
}
|
|
|
|
|
.reward-sub {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #7a6a5c;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.reward-progress {
|
|
|
|
|
background: #f8f4f0;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
.reward-progress-label {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #4d3e32;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.reward-distance {
|
|
|
|
|
color: #f5a623;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.reward-bar-bg {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 6px;
|
|
|
|
|
background: #e6ddd5;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.reward-bar-fill {
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: linear-gradient(90deg, #f9b84a, #f5a623);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
transition: width 0.6s ease;
|
|
|
|
|
}
|
|
|
|
|
.reward-current {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #8a7a6a;
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
.reward-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
.reward-btn {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 12px 0 !important;
|
|
|
|
|
font-size: 14px !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
border-radius: 60px !important;
|
|
|
|
|
}
|
|
|
|
|
.reward-btn-received {
|
|
|
|
|
background: #f0ebe6 !important;
|
|
|
|
|
border-color: #f0ebe6 !important;
|
|
|
|
|
color: #4d3e32 !important;
|
|
|
|
|
}
|
|
|
|
|
.reward-btn-received:hover {
|
|
|
|
|
background: #e6ddd5 !important;
|
|
|
|
|
border-color: #e6ddd5 !important;
|
|
|
|
|
}
|
|
|
|
|
.reward-btn-assets {
|
|
|
|
|
background: #f9b84a !important;
|
|
|
|
|
border-color: #f9b84a !important;
|
|
|
|
|
color: #1f140e !important;
|
|
|
|
|
box-shadow: 0 4px 14px rgba(249, 184, 74, 0.25);
|
|
|
|
|
}
|
|
|
|
|
.reward-btn-assets:hover {
|
|
|
|
|
background: #f5ac2e !important;
|
|
|
|
|
border-color: #f5ac2e !important;
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|