csm修改

master
2358328281@qq.com 5 days ago
parent 5d9626f55b
commit c9efdb0e65

@ -97,6 +97,7 @@ export default {
close: "关闭",
detail: "详情",
process: "处理",
modify: "修改",
submit: "提交工单",
save: "保存修改",
cancelModal: "取消",
@ -142,11 +143,12 @@ export default {
category: "客户分类",
signDate: "签约时间",
acceptDate: "验收时间",
years: "年限",
years: "免费维保年限",
maintenanceEndDate: "维保截止时间",
manager: "客户经理",
acceptanceReport: "请上传验收报告",
acceptanceReportTip: "支持上传PDF格式文件必填",
attachmentTip: "不超过50M",
account: "账号",
accountType: "账号类型",
nameAccount: "姓名/账号",
@ -252,7 +254,7 @@ export default {
phone: "联系电话",
manager: "客户经理",
signDate: "签约日期",
usageYears: "客户使用年限",
usageYears: "累计使用年限",
maintenanceEnd: "维保截止时间",
acceptanceReport: "验收报告",
account: "账号",

@ -142,7 +142,7 @@ export default {
category: '客戶分類',
signDate: '簽約時間',
acceptDate: '驗收時間',
years: '年限',
years: '免费维保年限',
maintenanceEndDate: '維保截止時間',
manager: '客戶經理',
acceptanceReport: '驗收報告',

@ -24,7 +24,7 @@
<el-descriptions-item :label="$t('label.contactPhone')">{{ detail.contactPhone }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.signDate')">{{ formatDate(detail.signDate) }}</el-descriptions-item>
<el-descriptions-item :label="$t('label.acceptDate')">{{ formatDate(detail.acceptDate) }}</el-descriptions-item>
<el-descriptions-item :label="$t('label.years')">{{ formatUsageYears(detail.signDate) }}</el-descriptions-item>
<el-descriptions-item :label="$t('label.years')">{{ detail.contractYears }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.maintenanceEnd')">{{ formatDate(detail.maintenanceEnd) }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.manager')">{{ detail.managerName }}</el-descriptions-item>
</el-descriptions>

@ -39,7 +39,7 @@
</div>
<div class="section">
<div class="section-title">{{ $t('label.attachment') }} ({{ (detail.attachments || []).length }})</div>
<div class="section-title">{{ $t('label.attachment') }}</div>
<el-empty v-if="!detail.attachments?.length" :description="$t('orderDetail.noAttachment')" :image-size="80" />
<div v-else class="files">
<el-button

@ -85,22 +85,38 @@
</el-form>
<!-- 工单总览 -->
<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-value">{{ stats.total }}</div>
<div class="stat-label">{{ $t("stat.totalOrders") }}</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-value">{{ stats.pending }}</div>
<div class="stat-label">{{ $t("stat.pendingOrders") }}</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-value">{{ stats.completed }}</div>
<div class="stat-label">{{ $t("stat.completedOrders") }}</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-value">{{ stats.highPriority }}</div>
<div class="stat-label">{{ $t("stat.highPriorityOrders") }}</div>
@ -114,10 +130,17 @@
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="200"
min-width="300"
show-overflow-tooltip
/>
<el-table-column
@ -155,14 +178,7 @@
width="170"
>
<template #default="{ row }">
{{ 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>
{{ row.completedAt || "-" }}
</template>
</el-table-column>
<el-table-column :label="$t('table.status')" width="100">
@ -184,10 +200,11 @@
}}</el-button>
<el-button
link
type="success"
v-if="row.status !== '已完成'"
:type="row.status === '已完成' ? 'warning' : 'success'"
@click="goProcess(row.id)"
>{{ $t("btn.process") }}</el-button
>{{
row.status === "已完成" ? $t("btn.modify") : $t("btn.process")
}}</el-button
>
</template>
</el-table-column>
@ -290,18 +307,18 @@
<el-radio-button label="低"></el-radio-button>
</el-radio-group>
</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-input
v-model="quickForm.department"
:placeholder="$t('placeholder.dept')"
/>
</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">
<div class="rich-editor">
<Toolbar
@ -321,28 +338,21 @@
</div>
</el-form-item>
<el-form-item :label="$t('label.attachment')">
<div class="attachment-pick">
<el-input
:model-value="quickForm.files[0]?.name || ''"
:placeholder="$t('upload.text')"
readonly
clearable
@click="triggerFilePicker"
@clear="clearFile"
>
<template #append>
<el-button @click.stop="triggerFilePicker">{{
$t("btn.upload")
}}</el-button>
</template>
</el-input>
<input
ref="fileInputRef"
type="file"
class="attachment-hidden-input"
@change="onFileChange"
/>
</div>
<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>
@ -361,6 +371,7 @@
</template>
<script setup>
import dayjs from "dayjs";
import {
nextTick,
onBeforeUnmount,
@ -422,6 +433,8 @@ const stats = reactive({
completed: 0,
highPriority: 0,
});
// total | pending | completed | high
const activeStat = ref("total");
const parseStats = (res) => {
stats.total = res?.total || 0;
let payload = res?.data;
@ -449,10 +462,12 @@ const loadList = async () => {
list.value = res?.list || [];
list.value.forEach((item) => {
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) {
item.completedAt = item.completedAt.split("T").join(" ");
item.completedAt = dayjs(item.completedAt).format(
"YYYY-MM-DD HH:mm:ss",
);
}
});
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 = () => {
page.value = 1;
loadList();
@ -475,6 +510,7 @@ const onReset = () => {
keyword: "",
});
dateRange.value = [];
activeStat.value = "total";
onSearch();
};
@ -522,22 +558,27 @@ const quickForm = reactive({
submitter: "",
files: [],
});
const fileInputRef = ref();
const MAX_FILE_SIZE = 50 * 1024 * 1024;
// base64 HTML
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) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
if (file.size > MAX_FILE_SIZE) return;
quickForm.files = [{ name: file.name, size: file.size, raw: file }];
// el-upload limit
const handleAttachmentExceed = () => {
ElMessage.warning("只能上传 1 个文件");
};
const clearFile = () => {
quickForm.files = [];
// el-upload
const handleAttachmentRemove = () => {
// el-upload v-model:file-list
};
const quickRules = {
hospitalId: [
@ -620,7 +661,7 @@ const collectPastedImages = (clipboardData) => {
return files;
};
// customPaste
//
const handleEditorPasteImage = (editor, event) => {
if (event.__wpsImageHandled) return;
const imageFiles = collectPastedImages(event.clipboardData);
@ -630,15 +671,13 @@ const handleEditorPasteImage = (editor, event) => {
if (typeof event.stopPropagation === "function") event.stopPropagation();
if (typeof event.stopImmediatePropagation === "function")
event.stopImmediatePropagation();
// ElMessage.info(` ${imageFiles.length} ...`);
// selection FileReader
if (typeof editor.focus === "function") editor.focus();
const savedSelection = editor.selection;
imageFiles.forEach((file) => {
if (file.size > MAX_PASTE_IMAGE_SIZE) {
ElMessage.warning("粘贴的图片不能超过 5MB,请使用下方附件上传");
ElMessage.warning("粘贴的图片不能超过 5MB");
return;
}
const reader = new FileReader();
@ -649,7 +688,6 @@ const handleEditorPasteImage = (editor, event) => {
return;
}
try {
//
if (savedSelection) editor.select(savedSelection);
} catch (_) {
if (typeof editor.focus === "function") editor.focus();
@ -658,32 +696,26 @@ const handleEditorPasteImage = (editor, event) => {
editor.dangerouslyInsertHtml(
`<img src="${dataUrl}" style="max-width:100%;height:auto;" />`,
);
// v-model
if (typeof editor.updateView === "function") editor.updateView();
// ElMessage.success("");
};
reader.onerror = () => {
ElMessage.error("图片处理失败");
};
reader.readAsDataURL(file);
});
return false; // wangEditor customPaste
return false;
};
// customPaste default-config wangEditor for Vue
// <Editor> @custom-paste + contentEditable
const editorConfig = {
placeholder: $t("placeholder.description"),
};
let pasteHandlerRef = null;
const handleEditorCreated = (editor) => {
editorRef.value = editor;
// contentEditable capture customPaste
nextTick(() => {
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
if (!root) return;
const editable =
root.querySelector?.('[contenteditable="true"]') || root;
const editable = root.querySelector?.('[contenteditable="true"]') || root;
if (pasteHandlerRef) {
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
}
@ -728,8 +760,9 @@ const submitQuick = async () => {
}
}
formData.append("registrarName", userStore.userInfo?.userName || "");
// el-upload file-list raw File
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);
ElMessage.success($t("msg.submitSuccess"));
@ -888,14 +921,6 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%) rotate(-45deg);
}
//
.attachment-pick {
width: 100%;
}
.attachment-hidden-input {
display: none;
}
//
.order-stats {
display: grid;
@ -915,13 +940,32 @@ onBeforeUnmount(() => {
border-left: 4px solid #d9d9d9;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
cursor: default;
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;
}

@ -1,14 +1,19 @@
<template>
<div class="process" v-loading="loading">
<el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<el-page-header
:icon="ArrowLeft"
:content="$t('common.back')"
@back="goBack"
class="page-header"
>
<template #content>
<span class="page-title">{{ $t('btn.process') }}</span>
<span class="page-title">{{ $t("btn.process") }}</span>
</template>
</el-page-header>
<el-card shadow="never" v-if="detail.id" class="form-card">
<template #header>
<span class="card-title">{{ $t('btn.process') }}</span>
<span class="card-title">{{ $t("btn.process") }}</span>
</template>
<el-form
@ -61,8 +66,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting" @click="submit">{{ $t('btn.confirm') }}</el-button>
<el-button @click="goBack">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="submitting" @click="submit">{{
$t("btn.confirm")
}}</el-button>
<el-button @click="goBack">{{ $t("common.cancel") }}</el-button>
</el-form-item>
</el-form>
</el-card>
@ -70,14 +77,25 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from "vue";
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
reactive,
ref,
shallowRef,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { ArrowLeft } from "@element-plus/icons-vue";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { useI18n } from "vue-i18n";
import { getAdminOrderDetail, processAdminOrder } from "@/service/modular/admin";
import {
getAdminOrderDetail,
processAdminOrder,
} from "@/service/modular/admin";
import { useUserStore } from "@/stores/api/user";
const { t: $t } = useI18n();
@ -92,7 +110,11 @@ const formRef = ref();
//
const handlerName = computed(
() => userStore.userInfo?.name || userStore.userInfo?.userName || userStore.userInfo?.account || '管理员',
() =>
userStore.userInfo?.name ||
userStore.userInfo?.userName ||
userStore.userInfo?.account ||
"管理员",
);
// ""
@ -102,7 +124,9 @@ const form = reactive({
});
const rules = {
status: [{ required: true, message: () => $t('table.status'), trigger: "change" }],
status: [
{ required: true, message: () => $t("table.status"), trigger: "change" },
],
processRemark: [
{
required: true,
@ -112,7 +136,8 @@ const rules = {
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, "")
.trim();
if (!text && !hasImage) return callback(new Error($t('placeholder.remark')));
if (!text && !hasImage)
return callback(new Error($t("placeholder.remark")));
callback();
},
trigger: "change",
@ -133,7 +158,7 @@ const toolbarConfig = {
],
};
const editorConfig = {
placeholder: $t('placeholder.remark'),
placeholder: $t("placeholder.remark"),
};
// base64 HTML
@ -215,8 +240,7 @@ const handleEditorCreated = (editor) => {
nextTick(() => {
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
if (!root) return;
const editable =
root.querySelector?.('[contenteditable="true"]') || root;
const editable = root.querySelector?.('[contenteditable="true"]') || root;
if (pasteHandlerRef) {
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
}
@ -229,6 +253,7 @@ const loadDetail = async () => {
loading.value = true;
try {
detail.value = await getAdminOrderDetail(route.params.id);
form.processRemark = detail.value.processRemark || "";
} finally {
loading.value = false;
}
@ -244,7 +269,7 @@ const submit = async () => {
status: form.status,
processRemark: form.processRemark,
});
ElMessage.success($t('msg.editSuccess'));
ElMessage.success($t("msg.editSuccess"));
router.replace("/admin/orders");
} finally {
submitting.value = false;

Loading…
Cancel
Save