feat(@vben/web-antd): 手动创建工单分步弹窗 + code review 修复
新增手动创建工单功能,分3步:选类型→选区域(TreeSelect全路径)→填表单。 优先级使用字典值,按工单类型动态显示保洁/安保专属字段。 - 新增 create-order-form.vue 分步创建弹窗组件 - work-order/index.vue 挂载弹窗替换占位 - card-view.vue 补充 OpsOrderCenterApi 类型导入 - 还原 auth.ts 误删的 visitTenantId 逻辑 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
import AssignForm from './modules/assign-form.vue';
|
||||
import CancelForm from './modules/cancel-form.vue';
|
||||
import CardView from './modules/card-view.vue';
|
||||
import CreateOrderForm from './modules/create-order-form.vue';
|
||||
import SecurityAssignForm from './modules/security-assign-form.vue';
|
||||
import StatsBar from './modules/stats-bar.vue';
|
||||
import UpgradePriorityForm from './modules/upgrade-priority-form.vue';
|
||||
@@ -74,6 +75,11 @@ const [CancelFormModal, cancelFormModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [CreateOrderFormModal, createOrderFormModalApi] = useVbenModal({
|
||||
connectedComponent: CreateOrderForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
orderType: undefined as OpsOrderCenterApi.OrderType | undefined,
|
||||
@@ -164,6 +170,7 @@ function handleUpgrade(row: OpsOrderCenterApi.OrderItem) {
|
||||
.setData({
|
||||
orderId: row.id,
|
||||
orderCode: row.orderCode,
|
||||
orderType: row.orderType,
|
||||
currentPriority: row.priority,
|
||||
})
|
||||
.open();
|
||||
@@ -175,6 +182,7 @@ function handleCancel(row: OpsOrderCenterApi.OrderItem) {
|
||||
.setData({
|
||||
orderId: row.id,
|
||||
orderCode: row.orderCode,
|
||||
orderType: row.orderType,
|
||||
title: row.title,
|
||||
})
|
||||
.open();
|
||||
@@ -313,6 +321,7 @@ onActivated(() => {
|
||||
<SecurityAssignFormModal @success="handleRefresh" />
|
||||
<UpgradePriorityFormModal @success="handleRefresh" />
|
||||
<CancelFormModal @success="handleRefresh" />
|
||||
<CreateOrderFormModal @success="handleRefresh" />
|
||||
|
||||
<!-- 区域筛选抽屉 -->
|
||||
<AreaFilterDrawer
|
||||
@@ -402,7 +411,7 @@ onActivated(() => {
|
||||
<Button
|
||||
type="primary"
|
||||
class="create-btn"
|
||||
@click="() => message.info('手动创建工单功能开发中')"
|
||||
@click="createOrderFormModalApi.open()"
|
||||
>
|
||||
<IconifyIcon icon="solar:add-circle-bold" />
|
||||
<span>创建工单</span>
|
||||
|
||||
@@ -20,10 +20,11 @@ import {
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getOrderPage } from '#/api/ops/order-center';
|
||||
import { type OpsOrderCenterApi, getOrderPage } from '#/api/ops/order-center';
|
||||
|
||||
import {
|
||||
ORDER_TYPE_COLOR_MAP,
|
||||
ORDER_TYPE_ICON_MAP,
|
||||
ORDER_TYPE_TEXT_MAP,
|
||||
STATUS_COLOR_MAP,
|
||||
STATUS_ICON_MAP,
|
||||
@@ -292,24 +293,49 @@ defineExpose({
|
||||
'order-card--urgent': isUrgent(item.priority),
|
||||
'order-card--terminal': isTerminal(item.status),
|
||||
}"
|
||||
:style="{
|
||||
'--type-color': ORDER_TYPE_COLOR_MAP[item.orderType]?.border || '#d9d9d9',
|
||||
'--type-color-bg': ORDER_TYPE_COLOR_MAP[item.orderType]?.bg || 'rgb(0 0 0 / 3%)',
|
||||
}"
|
||||
@click="emit('detail', item.id)"
|
||||
>
|
||||
<!-- 头部:状态 + 优先级 -->
|
||||
<!-- 头部:类型标识 + 状态 + 优先级 -->
|
||||
<div class="card-header">
|
||||
<div
|
||||
class="status-info"
|
||||
:style="{
|
||||
'--status-color': STATUS_COLOR_MAP[item.status]?.text,
|
||||
'--status-bg': STATUS_COLOR_MAP[item.status]?.bg,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="STATUS_ICON_MAP[item.status] || 'solar:circle-bold'"
|
||||
class="status-icon"
|
||||
/>
|
||||
<span class="status-text">{{
|
||||
STATUS_TEXT_MAP[item.status]
|
||||
}}</span>
|
||||
<div class="card-header-left">
|
||||
<div
|
||||
class="type-badge"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
ORDER_TYPE_COLOR_MAP[item.orderType]?.bg,
|
||||
color: ORDER_TYPE_COLOR_MAP[item.orderType]?.text,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
ORDER_TYPE_ICON_MAP[item.orderType] ||
|
||||
'solar:widget-bold-duotone'
|
||||
"
|
||||
class="type-badge-icon"
|
||||
/>
|
||||
<span>{{ ORDER_TYPE_TEXT_MAP[item.orderType] }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="status-info"
|
||||
:style="{
|
||||
'--status-color': STATUS_COLOR_MAP[item.status]?.text,
|
||||
'--status-bg': STATUS_COLOR_MAP[item.status]?.bg,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
STATUS_ICON_MAP[item.status] || 'solar:circle-bold'
|
||||
"
|
||||
class="status-icon"
|
||||
/>
|
||||
<span class="status-text">{{
|
||||
STATUS_TEXT_MAP[item.status]
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="priority-tag"
|
||||
@@ -400,17 +426,8 @@ defineExpose({
|
||||
|
||||
<!-- 底部:元信息 + 操作按钮 -->
|
||||
<div class="card-footer">
|
||||
<!-- 左侧:类型 + 时间 -->
|
||||
<!-- 左侧:时间 -->
|
||||
<div class="card-meta">
|
||||
<span
|
||||
class="type-tag"
|
||||
:style="{
|
||||
backgroundColor: ORDER_TYPE_COLOR_MAP[item.orderType]?.bg,
|
||||
color: ORDER_TYPE_COLOR_MAP[item.orderType]?.text,
|
||||
}"
|
||||
>
|
||||
{{ ORDER_TYPE_TEXT_MAP[item.orderType] }}
|
||||
</span>
|
||||
<span class="create-time">{{
|
||||
formatTime(item.createTime)
|
||||
}}</span>
|
||||
@@ -534,18 +551,42 @@ defineExpose({
|
||||
|
||||
// 卡片
|
||||
.order-card {
|
||||
--type-color: #d9d9d9;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
// 底层渐变背景 —— 按工单类型着色
|
||||
&::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
var(--type-color-bg, rgb(0 0 0 / 3%)) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// 所有直接子元素提升到渐变层之上
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #d9d9d9;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 6%);
|
||||
@@ -587,6 +628,27 @@ defineExpose({
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-header-left {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 工单类型徽章
|
||||
.type-badge {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
|
||||
.type-badge-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-info {
|
||||
--status-color: #6b5e52;
|
||||
--status-bg: #f0ede8;
|
||||
@@ -731,16 +793,6 @@ defineExpose({
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
.type-tag {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 6px;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.order-code {
|
||||
flex-shrink: 0;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
<script setup lang="ts">
|
||||
import type { OpsAreaApi } from '#/api/ops/area';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useDictStore } from '@vben/stores';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { Button, message, Steps, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
import { getAreaTree } from '#/api/ops/area';
|
||||
import { manualCreateOrder } from '#/api/ops/cleaning';
|
||||
import { manualCreateSecurityOrder } from '#/api/ops/security';
|
||||
import { CLEANING_TYPE_OPTIONS } from '../data';
|
||||
|
||||
defineOptions({ name: 'CreateOrderForm' });
|
||||
|
||||
const emit = defineEmits<{ success: [] }>();
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
const dictStore = useDictStore();
|
||||
const priorityOptions = computed(() => {
|
||||
const opts = dictStore.getDictOptions(DICT_TYPE.OPS_ORDER_PRIORITY);
|
||||
return opts.map((item) => ({
|
||||
label: item.label,
|
||||
value: Number(item.value),
|
||||
}));
|
||||
});
|
||||
|
||||
const currentStep = ref(0);
|
||||
const orderType = ref<'CLEAN' | 'SECURITY'>();
|
||||
const selectedAreaId = ref<number>();
|
||||
const selectedAreaPath = ref<string>('');
|
||||
const loading = ref(false);
|
||||
|
||||
// 区域树 + 平铺列表(用于计算全路径)
|
||||
const areaTreeData = ref<any[]>([]);
|
||||
const areaFlatList = ref<OpsAreaApi.BusArea[]>([]);
|
||||
const areaTreeLoading = ref(false);
|
||||
|
||||
// ==================== 表单 ====================
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
// 隐藏字段:用于驱动保洁/安保条件字段的显隐
|
||||
{
|
||||
fieldName: '_orderType',
|
||||
label: '',
|
||||
component: 'Input',
|
||||
dependencies: { triggerFields: [''], show: () => false },
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '工单标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入工单标题',
|
||||
maxLength: 100,
|
||||
},
|
||||
rules: z.string().min(2, '标题至少2个字符').max(100, '标题不能超过100字符'),
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '工单描述',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入工单描述(选填)',
|
||||
rows: 3,
|
||||
maxLength: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'priority',
|
||||
label: '优先级',
|
||||
component: 'Select',
|
||||
componentProps: () => ({
|
||||
placeholder: '请选择优先级',
|
||||
options: priorityOptions.value,
|
||||
}),
|
||||
defaultValue: 2,
|
||||
},
|
||||
// 保洁专属
|
||||
{
|
||||
fieldName: 'cleaningType',
|
||||
label: '保洁类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择保洁类型',
|
||||
options: CLEANING_TYPE_OPTIONS,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['_orderType'],
|
||||
show: (values) => values._orderType === 'CLEAN',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'expectedDuration',
|
||||
label: '预计时长(分钟)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入预计作业时长',
|
||||
min: 1,
|
||||
max: 480,
|
||||
style: { width: '100%' },
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['_orderType'],
|
||||
show: (values) => values._orderType === 'CLEAN',
|
||||
},
|
||||
},
|
||||
// 安保专属:相关图片(选填)
|
||||
{
|
||||
fieldName: 'imageUrl',
|
||||
label: '相关图片',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxNumber: 1,
|
||||
maxSize: 5,
|
||||
showDescription: false,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['_orderType'],
|
||||
show: (values) => values._orderType === 'SECURITY',
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// ==================== 弹窗 ====================
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (isOpen) {
|
||||
resetForm();
|
||||
await loadAreaTree();
|
||||
}
|
||||
},
|
||||
onConfirm: handleSubmit,
|
||||
});
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
const canNext = computed(() => {
|
||||
if (currentStep.value === 0) return !!orderType.value;
|
||||
if (currentStep.value === 1) return !!selectedAreaId.value;
|
||||
return true;
|
||||
});
|
||||
|
||||
const stepItems = computed(() => [
|
||||
{ title: '工单类型' },
|
||||
{ title: '选择区域' },
|
||||
{ title: '填写信息' },
|
||||
]);
|
||||
|
||||
// ==================== 方法 ====================
|
||||
|
||||
function resetForm() {
|
||||
currentStep.value = 0;
|
||||
orderType.value = undefined;
|
||||
selectedAreaId.value = undefined;
|
||||
selectedAreaPath.value = '';
|
||||
formApi.resetForm();
|
||||
}
|
||||
|
||||
async function loadAreaTree() {
|
||||
areaTreeLoading.value = true;
|
||||
try {
|
||||
const data = await getAreaTree({ isActive: true });
|
||||
const list = (
|
||||
Array.isArray(data) ? data : ((data as any)?.list ?? [])
|
||||
) as OpsAreaApi.BusArea[];
|
||||
areaFlatList.value = list;
|
||||
areaTreeData.value = handleTree(list, 'id', 'parentId', 'children');
|
||||
} catch {
|
||||
areaFlatList.value = [];
|
||||
areaTreeData.value = [];
|
||||
} finally {
|
||||
areaTreeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectType(type: 'CLEAN' | 'SECURITY') {
|
||||
orderType.value = type;
|
||||
// 同步到表单隐藏字段,驱动 dependencies 重新计算显隐
|
||||
formApi.setValues({ _orderType: type });
|
||||
}
|
||||
|
||||
/** 根据 id 向上查找,拼接 "父级 / 子级" 全路径 */
|
||||
function getAreaPath(id: number): string {
|
||||
const map = new Map(areaFlatList.value.map((a) => [a.id, a]));
|
||||
const parts: string[] = [];
|
||||
let current = map.get(id);
|
||||
while (current) {
|
||||
parts.unshift(current.areaName);
|
||||
current = current.parentId ? map.get(current.parentId) : undefined;
|
||||
}
|
||||
return parts.join(' / ');
|
||||
}
|
||||
|
||||
function handleAreaChange(value: number | undefined) {
|
||||
selectedAreaId.value = value;
|
||||
selectedAreaPath.value = value ? getAreaPath(value) : '';
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (canNext.value && currentStep.value < 2) {
|
||||
currentStep.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (currentStep.value < 2) {
|
||||
handleNext();
|
||||
return;
|
||||
}
|
||||
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
|
||||
if (!orderType.value || !selectedAreaId.value) {
|
||||
message.warning('请先完成前面的步骤');
|
||||
currentStep.value = orderType.value ? 1 : 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 getValues 获取完整表单值(含 defaultValue)
|
||||
const values = await formApi.getValues();
|
||||
|
||||
loading.value = true;
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
if (orderType.value === 'CLEAN') {
|
||||
await manualCreateOrder({
|
||||
title: values.title,
|
||||
description: values.description || undefined,
|
||||
priority: values.priority ?? 2,
|
||||
areaId: selectedAreaId.value,
|
||||
cleaningType: values.cleaningType || undefined,
|
||||
expectedDuration: values.expectedDuration || undefined,
|
||||
});
|
||||
} else {
|
||||
await manualCreateSecurityOrder({
|
||||
title: values.title,
|
||||
description: values.description || undefined,
|
||||
priority: values.priority ?? 2,
|
||||
areaId: selectedAreaId.value,
|
||||
imageUrl: values.imageUrl || undefined,
|
||||
sourceType: 'MANUAL',
|
||||
});
|
||||
}
|
||||
message.success('工单创建成功');
|
||||
modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 step 变化,动态设置确认按钮文本
|
||||
watch(currentStep, (step) => {
|
||||
modalApi.setState({
|
||||
confirmText: step === 2 ? '提交' : '下一步',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="创建工单" class="w-[580px]">
|
||||
<!-- 步骤条 -->
|
||||
<Steps :current="currentStep" :items="stepItems" size="small" class="co-steps" />
|
||||
|
||||
<!-- Step 1: 选择工单类型 -->
|
||||
<div v-show="currentStep === 0" class="co-step-content">
|
||||
<div class="co-type-grid">
|
||||
<div
|
||||
class="co-type-card"
|
||||
:class="{ 'co-type-card--active': orderType === 'CLEAN' }"
|
||||
@click="selectType('CLEAN')"
|
||||
>
|
||||
<div class="co-type-icon" style="background: #e5faf2; color: #047857">
|
||||
<IconifyIcon icon="solar:bath-bold-duotone" class="size-6" />
|
||||
</div>
|
||||
<div class="co-type-info">
|
||||
<div class="co-type-title">保洁工单</div>
|
||||
<div class="co-type-desc">日常/深度/应急保洁任务</div>
|
||||
</div>
|
||||
<IconifyIcon
|
||||
v-if="orderType === 'CLEAN'"
|
||||
icon="lucide:check-circle-2"
|
||||
class="co-type-check"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="co-type-card"
|
||||
:class="{ 'co-type-card--active': orderType === 'SECURITY' }"
|
||||
@click="selectType('SECURITY')"
|
||||
>
|
||||
<div class="co-type-icon" style="background: #eef0ff; color: #3730a3">
|
||||
<IconifyIcon icon="solar:shield-bold-duotone" class="size-6" />
|
||||
</div>
|
||||
<div class="co-type-info">
|
||||
<div class="co-type-title">安保工单</div>
|
||||
<div class="co-type-desc">入侵/离岗/火焰等告警处理</div>
|
||||
</div>
|
||||
<IconifyIcon
|
||||
v-if="orderType === 'SECURITY'"
|
||||
icon="lucide:check-circle-2"
|
||||
class="co-type-check"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: 选择区域 -->
|
||||
<div v-show="currentStep === 1" class="co-step-content">
|
||||
<div class="co-area-section">
|
||||
<div class="co-section-label">选择工单所属区域</div>
|
||||
<TreeSelect
|
||||
v-model:value="selectedAreaId"
|
||||
:tree-data="areaTreeData"
|
||||
:field-names="{ label: 'areaName', value: 'id', children: 'children' }"
|
||||
:loading="areaTreeLoading"
|
||||
tree-default-expand-all
|
||||
show-search
|
||||
:tree-node-filter-prop="'areaName'"
|
||||
placeholder="搜索并选择区域"
|
||||
allow-clear
|
||||
style="width: 100%"
|
||||
size="large"
|
||||
@change="handleAreaChange"
|
||||
/>
|
||||
<div v-if="selectedAreaId" class="co-area-selected">
|
||||
<IconifyIcon icon="solar:map-point-bold-duotone" class="co-area-icon" />
|
||||
<span>已选择: {{ selectedAreaPath }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 填写工单信息 -->
|
||||
<div v-show="currentStep === 2" class="co-step-content">
|
||||
<!-- 已选信息概要 -->
|
||||
<div class="co-summary">
|
||||
<span class="co-summary-tag" :style="orderType === 'CLEAN'
|
||||
? 'background:#e5faf2;color:#047857'
|
||||
: 'background:#eef0ff;color:#3730a3'
|
||||
">
|
||||
{{ orderType === 'CLEAN' ? '保洁' : '安保' }}
|
||||
</span>
|
||||
<span class="co-summary-area">
|
||||
<IconifyIcon icon="solar:map-point-bold-duotone" class="co-summary-icon" />
|
||||
{{ selectedAreaPath || '未选择区域' }}
|
||||
</span>
|
||||
</div>
|
||||
<Form />
|
||||
</div>
|
||||
|
||||
<!-- 底部导航按钮 -->
|
||||
<template #prepend-footer>
|
||||
<Button v-if="currentStep > 0" @click="handlePrev">上一步</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.co-steps {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.co-step-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 类型选择卡片 */
|
||||
.co-type-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.co-type-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.co-type-card:hover {
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.co-type-card--active {
|
||||
background: #f0f7ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.co-type-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.co-type-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.co-type-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.co-type-desc {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.co-type-check {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
font-size: 20px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
/* 区域选择 */
|
||||
.co-area-section {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.co-section-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.co-area-selected {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #047857;
|
||||
background: #f0fdf4;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.co-area-icon {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* 已选概要 */
|
||||
.co-summary {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.co-summary-tag {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.co-summary-area {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.co-summary-icon {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* dark mode */
|
||||
html.dark .co-type-card {
|
||||
border-color: rgb(255 255 255 / 12%);
|
||||
}
|
||||
|
||||
html.dark .co-type-card:hover {
|
||||
border-color: rgb(255 255 255 / 25%);
|
||||
}
|
||||
|
||||
html.dark .co-type-card--active {
|
||||
background: rgb(22 119 255 / 10%);
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
html.dark .co-type-title {
|
||||
color: rgb(255 255 255 / 88%);
|
||||
}
|
||||
|
||||
html.dark .co-type-desc {
|
||||
color: rgb(255 255 255 / 45%);
|
||||
}
|
||||
|
||||
html.dark .co-section-label {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
html.dark .co-area-selected {
|
||||
color: #34d399;
|
||||
background: rgb(16 185 129 / 10%);
|
||||
}
|
||||
|
||||
html.dark .co-summary {
|
||||
background: rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
html.dark .co-summary-area {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user