refactor(@vben/web-antd): 重写升级优先级和取消工单弹窗,统一布局风格

- 移除 useVbenForm 避免弹窗内布局冲突,改用原生 Input.TextArea
- 统一为图标+标签+值的信息行布局,与安保派单弹窗风格一致
- 提交时增加 confirmLoading 状态反馈及 maxLength 保护校验
- 支持暗色模式样式适配

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-18 15:21:58 +08:00
parent ed50dc3f7e
commit bdb06e761a
2 changed files with 286 additions and 147 deletions

View File

@@ -4,120 +4,183 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Alert, message } from 'ant-design-vue';
import { Alert, Input, message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
import { cancelOrder } from '#/api/ops/order-center';
defineOptions({ name: 'CancelOrderForm' });
const emit = defineEmits<{ success: [] }>();
interface ModalData {
orderId: number;
orderCode: string;
title: string;
}
const modalData = ref<ModalData>({
orderId: 0,
orderCode: '',
title: '',
});
const reason = ref('');
const loading = ref(false);
const [Modal, modalApi] = useVbenModal({
onOpenChange: (isOpen) => {
if (isOpen) {
const data = modalApi.getData<{
orderCode: string;
orderId: number;
title: string;
}>();
const data = modalApi.getData<ModalData>();
if (data) {
orderId.value = data.orderId;
orderCode.value = data.orderCode;
orderTitle.value = data.title;
modalData.value = data;
}
reason.value = '';
}
},
onConfirm: handleSubmit,
});
const orderId = ref<number>();
const orderCode = ref<string>('');
const orderTitle = ref<string>('');
const loading = ref(false);
const [Form, formApi] = useVbenForm({
schema: [
{
fieldName: 'reason',
label: '取消原因',
component: 'Textarea',
componentProps: {
placeholder: '请输入取消工单的原因',
rows: 4,
maxLength: 200,
showCount: true,
},
rules: z
.string()
.min(2, '原因至少2个字符')
.max(200, '原因不能超过200字符'),
},
],
showDefaultActions: false,
});
/** 提交表单 */
async function handleSubmit() {
const { valid, values } = await formApi.validate();
if (!valid) return;
const val = reason.value.trim();
if (val.length < 2) {
message.warning('取消原因至少2个字符');
return;
}
if (val.length > 200) {
message.warning('取消原因不能超过200字符');
return;
}
loading.value = true;
modalApi.setState({ confirmLoading: true });
try {
await cancelOrder({
id: orderId.value!,
reason: values.reason,
id: modalData.value.orderId,
reason: val,
});
message.success('工单已取消');
modalApi.close();
emit('success');
} finally {
loading.value = false;
modalApi.setState({ confirmLoading: false });
}
}
</script>
<template>
<Modal title="取消工单" class="w-[480px]">
<div class="cancel-form">
<!-- 警告提示 -->
<Alert
type="error"
show-icon
class="mb-4"
message="取消后工单将无法恢复,请确认操作"
>
<template #icon>
<IconifyIcon icon="lucide:alert-circle" class="text-red-500" />
</template>
</Alert>
<!-- 警告提示 -->
<Alert
type="error"
show-icon
class="cf-alert"
message="取消后工单将无法恢复,请确认操作"
>
<template #icon>
<IconifyIcon icon="lucide:alert-circle" class="text-red-500" />
</template>
</Alert>
<!-- 工单信息 -->
<div class="mb-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
<div class="mb-2 text-sm text-gray-500">
工单编号<span
class="font-medium text-gray-700 dark:text-gray-300"
>{{ orderCode }}</span
>
</div>
<div class="text-sm text-gray-500">
工单标题<span
class="font-medium text-gray-700 dark:text-gray-300"
>{{ orderTitle }}</span
>
</div>
<!-- 工单信息 -->
<div class="cf-order-info">
<div class="cf-info-row">
<IconifyIcon
icon="solar:document-text-bold-duotone"
class="cf-info-icon"
/>
<span class="cf-info-label">工单编号</span>
<span class="cf-info-value">{{ modalData.orderCode }}</span>
</div>
<div v-if="modalData.title" class="cf-info-row">
<IconifyIcon
icon="solar:notes-bold-duotone"
class="cf-info-icon"
/>
<span class="cf-info-label">工单标题</span>
<span class="cf-info-value cf-info-desc">{{ modalData.title }}</span>
</div>
</div>
<!-- 表单 -->
<Form />
<!-- 取消原因 -->
<div class="cf-section">
<div class="cf-section-title">取消原因</div>
<Input.TextArea
v-model:value="reason"
placeholder="请输入取消工单的原因"
:rows="4"
:maxlength="200"
show-count
/>
</div>
</Modal>
</template>
<style scoped>
.cancel-form {
:deep(.ant-form-item) {
margin-bottom: 0;
}
.cf-alert {
margin-bottom: 12px;
}
.cf-order-info {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
margin-bottom: 12px;
background: #fafafa;
border-radius: 8px;
}
.cf-info-row {
display: flex;
gap: 6px;
align-items: flex-start;
font-size: 13px;
line-height: 20px;
}
.cf-info-icon {
flex-shrink: 0;
margin-top: 2px;
color: #8c8c8c;
}
.cf-info-label {
flex-shrink: 0;
color: #8c8c8c;
}
.cf-info-value {
flex: 1;
min-width: 0;
color: #333;
word-break: break-all;
}
.cf-info-desc {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.cf-section {
margin-bottom: 12px;
}
.cf-section-title {
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
}
/* dark mode */
html.dark .cf-order-info {
background: rgb(255 255 255 / 6%);
}
html.dark .cf-info-value {
color: rgb(255 255 255 / 85%);
}
</style>

View File

@@ -4,60 +4,41 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Alert, message } from 'ant-design-vue';
import { Alert, Input, message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
import { upgradePriority } from '#/api/ops/cleaning';
defineOptions({ name: 'UpgradePriorityForm' });
const emit = defineEmits<{ success: [] }>();
interface ModalData {
orderId: number;
orderCode: string;
currentPriority: number;
}
const modalData = ref<ModalData>({
orderId: 0,
orderCode: '',
currentPriority: 2,
});
const reason = ref('');
const loading = ref(false);
const [Modal, modalApi] = useVbenModal({
onOpenChange: (isOpen) => {
if (isOpen) {
const data = modalApi.getData<{
currentPriority: number;
orderCode: string;
orderId: number;
}>();
const data = modalApi.getData<ModalData>();
if (data) {
orderId.value = data.orderId;
orderCode.value = data.orderCode;
currentPriority.value = data.currentPriority;
modalData.value = data;
}
reason.value = '';
}
},
onConfirm: handleSubmit,
});
const orderId = ref<number>();
const orderCode = ref<string>('');
const currentPriority = ref<number>(2);
const loading = ref(false);
const [Form, formApi] = useVbenForm({
schema: [
{
fieldName: 'reason',
label: '升级原因',
component: 'Textarea',
componentProps: {
placeholder:
'请输入升级为P0紧急工单的原因例如领导临时检查、VIP客户投诉等',
rows: 4,
maxLength: 200,
showCount: true,
},
rules: z
.string()
.min(5, '原因至少5个字符')
.max(200, '原因不能超过200字符'),
},
],
showDefaultActions: false,
});
/** 获取优先级文本 */
function getPriorityText(priority: number) {
const map: Record<number, string> = {
@@ -70,67 +51,162 @@ function getPriorityText(priority: number) {
/** 提交表单 */
async function handleSubmit() {
const { valid, values } = await formApi.validate();
if (!valid || !values) return;
const val = reason.value.trim();
if (val.length < 5) {
message.warning('升级原因至少5个字符');
return;
}
if (val.length > 200) {
message.warning('升级原因不能超过200字符');
return;
}
loading.value = true;
modalApi.setState({ confirmLoading: true });
try {
await upgradePriority({
orderId: orderId.value!,
reason: values.reason,
orderId: modalData.value.orderId,
reason: val,
});
message.success('已升级为P0紧急工单');
modalApi.close();
emit('success');
} finally {
loading.value = false;
modalApi.setState({ confirmLoading: false });
}
}
</script>
<template>
<Modal title="升级优先级" class="w-[500px]">
<div class="upgrade-form">
<!-- 警告提示 -->
<Alert
type="warning"
show-icon
class="mb-4"
message="升级为P0紧急工单后该工单将插队优先处理"
>
<template #icon>
<IconifyIcon icon="lucide:zap" class="text-orange-500" />
</template>
</Alert>
<!-- 警告提示 -->
<Alert
type="warning"
show-icon
class="uf-alert"
message="升级为P0紧急工单后该工单将优先处理"
>
<template #icon>
<IconifyIcon icon="lucide:zap" class="text-orange-500" />
</template>
</Alert>
<!-- 工单信息 -->
<div class="mb-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm text-gray-500">工单编号</span>
<span class="font-medium">{{ orderCode }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500">当前优先级</span>
<div class="flex items-center gap-2">
<span class="text-gray-600 dark:text-gray-300">
{{ getPriorityText(currentPriority) }}
</span>
<IconifyIcon icon="lucide:arrow-right" class="text-gray-400" />
<span class="font-bold text-red-500">P0 (紧急)</span>
</div>
</div>
<!-- 工单信息 -->
<div class="uf-order-info">
<div class="uf-info-row">
<IconifyIcon
icon="solar:document-text-bold-duotone"
class="uf-info-icon"
/>
<span class="uf-info-label">工单编号</span>
<span class="uf-info-value">{{ modalData.orderCode }}</span>
</div>
<div class="uf-info-row">
<IconifyIcon
icon="solar:ranking-bold-duotone"
class="uf-info-icon"
/>
<span class="uf-info-label">优先级变更</span>
<span class="uf-info-value uf-priority">
<span class="uf-priority-from">
{{ getPriorityText(modalData.currentPriority) }}
</span>
<IconifyIcon icon="lucide:arrow-right" class="uf-arrow" />
<span class="uf-priority-to">P0 (紧急)</span>
</span>
</div>
</div>
<!-- 表单 -->
<Form />
<!-- 升级原因 -->
<div class="uf-section">
<div class="uf-section-title">升级原因</div>
<Input.TextArea
v-model:value="reason"
placeholder="请输入升级为P0紧急工单的原因例如领导临时检查、VIP客户投诉等"
:rows="4"
:maxlength="200"
show-count
/>
</div>
</Modal>
</template>
<style scoped>
.upgrade-form {
:deep(.ant-form-item) {
margin-bottom: 0;
}
.uf-alert {
margin-bottom: 12px;
}
.uf-order-info {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
margin-bottom: 12px;
background: #fafafa;
border-radius: 8px;
}
.uf-info-row {
display: flex;
gap: 6px;
align-items: center;
font-size: 13px;
line-height: 20px;
}
.uf-info-icon {
flex-shrink: 0;
color: #8c8c8c;
}
.uf-info-label {
flex-shrink: 0;
color: #8c8c8c;
}
.uf-info-value {
flex: 1;
min-width: 0;
color: #333;
word-break: break-all;
}
.uf-priority {
display: flex;
gap: 6px;
align-items: center;
}
.uf-priority-from {
color: #8c8c8c;
}
.uf-arrow {
color: #bfbfbf;
}
.uf-priority-to {
font-weight: 600;
color: #ff4d4f;
}
.uf-section {
margin-bottom: 12px;
}
.uf-section-title {
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
}
/* dark mode */
html.dark .uf-order-info {
background: rgb(255 255 255 / 6%);
}
html.dark .uf-info-value {
color: rgb(255 255 255 / 85%);
}
</style>