664 lines
17 KiB
Vue
664 lines
17 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
|||
|
|
|
|||
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
|||
|
|
|
|||
|
|
import { Button, Card, Input, message, Select, Tabs, Upload } from 'ant-design-vue';
|
|||
|
|
|
|||
|
|
import { JsonViewer } from '@vben/common-ui';
|
|||
|
|
|
|||
|
|
import { useVbenForm } from '#/adapter/form';
|
|||
|
|
import { codeGenerationForm } from '#/views/code-generation/config/form';
|
|||
|
|
|
|||
|
|
import type { CodeGenData, FieldType } from '../utils/data-transformer';
|
|||
|
|
import { transformJsonToStandard, validateData } from '../utils/data-transformer';
|
|||
|
|
import {
|
|||
|
|
deleteDraft,
|
|||
|
|
listDrafts,
|
|||
|
|
loadDraft,
|
|||
|
|
saveDraft,
|
|||
|
|
type GenerationMode,
|
|||
|
|
} from '../utils/draft-manager';
|
|||
|
|
|
|||
|
|
const mode: GenerationMode = 'json';
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
generate: [data: CodeGenData];
|
|||
|
|
back: [];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
// 表单相关
|
|||
|
|
const [Form, formApi] = useVbenForm(codeGenerationForm);
|
|||
|
|
const formData = ref<Record<string, any>>({});
|
|||
|
|
|
|||
|
|
// JSON输入
|
|||
|
|
const jsonText = ref('');
|
|||
|
|
const jsonError = ref('');
|
|||
|
|
const parsedData = ref<CodeGenData | null>(null);
|
|||
|
|
const activeTab = ref('text');
|
|||
|
|
|
|||
|
|
// 文件上传相关
|
|||
|
|
const fileList = ref<UploadFile[]>([]);
|
|||
|
|
const uploadError = ref('');
|
|||
|
|
|
|||
|
|
// 草稿相关
|
|||
|
|
const draftId = ref<string | null>(null);
|
|||
|
|
const drafts = ref<ReturnType<typeof listDrafts>>([]);
|
|||
|
|
const selectedDraft = ref<string>('');
|
|||
|
|
|
|||
|
|
// 预览数据
|
|||
|
|
const previewData = computed(() => {
|
|||
|
|
if (parsedData.value) {
|
|||
|
|
return {
|
|||
|
|
...parsedData.value,
|
|||
|
|
field: parsedData.value.field || [],
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载草稿列表
|
|||
|
|
const loadDraftList = () => {
|
|||
|
|
drafts.value = listDrafts(mode);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 加载草稿
|
|||
|
|
const handleLoadDraft = (id: string) => {
|
|||
|
|
const draft = loadDraft(mode, id);
|
|||
|
|
if (draft) {
|
|||
|
|
draftId.value = id;
|
|||
|
|
jsonText.value = JSON.stringify(draft.data, null, 2);
|
|||
|
|
selectedDraft.value = id;
|
|||
|
|
parseJson();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 手动保存草稿
|
|||
|
|
const handleSaveDraft = () => {
|
|||
|
|
if (!parsedData.value) {
|
|||
|
|
message.warning('请先导入或填写JSON数据');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const id = saveDraft(mode, parsedData.value, undefined, draftId.value || undefined);
|
|||
|
|
draftId.value = id;
|
|||
|
|
loadDraftList();
|
|||
|
|
message.success('草稿已保存');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('保存草稿失败:', error);
|
|||
|
|
message.error('保存草稿失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 解析JSON
|
|||
|
|
const parseJson = () => {
|
|||
|
|
jsonError.value = '';
|
|||
|
|
if (!jsonText.value.trim()) {
|
|||
|
|
parsedData.value = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const json = JSON.parse(jsonText.value);
|
|||
|
|
const transformed = transformJsonToStandard(json);
|
|||
|
|
parsedData.value = transformed;
|
|||
|
|
|
|||
|
|
// 更新表单数据
|
|||
|
|
formData.value = {
|
|||
|
|
class_name: transformed.class_name,
|
|||
|
|
class_comment: transformed.class_comment,
|
|||
|
|
icon: transformed.icon,
|
|||
|
|
sort: transformed.sort,
|
|||
|
|
pid: transformed.pid,
|
|||
|
|
};
|
|||
|
|
formApi.setValues(formData.value);
|
|||
|
|
} catch (error: any) {
|
|||
|
|
jsonError.value = error.message || 'JSON格式错误';
|
|||
|
|
parsedData.value = null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 监听JSON文本变化
|
|||
|
|
watch(jsonText, () => {
|
|||
|
|
parseJson();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 文件上传前验证和处理
|
|||
|
|
const handleBeforeUpload = (file: File): boolean => {
|
|||
|
|
uploadError.value = '';
|
|||
|
|
|
|||
|
|
// 验证文件类型
|
|||
|
|
const isValidType = file.type === 'application/json' || file.name.endsWith('.json');
|
|||
|
|
if (!isValidType) {
|
|||
|
|
uploadError.value = '只能上传 JSON 格式文件!';
|
|||
|
|
message.error('只能上传 JSON 格式文件!');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证文件大小(限制 10MB)
|
|||
|
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
|||
|
|
if (!isLt10M) {
|
|||
|
|
uploadError.value = '文件大小不能超过 10MB!';
|
|||
|
|
message.error('文件大小不能超过 10MB!');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加到文件列表
|
|||
|
|
const uploadFile: UploadFile = {
|
|||
|
|
uid: `${Date.now()}-${file.name}`,
|
|||
|
|
name: file.name,
|
|||
|
|
status: 'uploading',
|
|||
|
|
size: file.size,
|
|||
|
|
type: file.type,
|
|||
|
|
originFileObj: file,
|
|||
|
|
};
|
|||
|
|
fileList.value = [uploadFile];
|
|||
|
|
|
|||
|
|
// 读取文件内容
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
try {
|
|||
|
|
const text = e.target?.result as string;
|
|||
|
|
|
|||
|
|
// 验证是否为有效的 JSON
|
|||
|
|
const json = JSON.parse(text);
|
|||
|
|
|
|||
|
|
// 更新 JSON 文本(格式化为美观的 JSON)
|
|||
|
|
jsonText.value = JSON.stringify(json, null, 2);
|
|||
|
|
|
|||
|
|
// 更新文件状态为成功
|
|||
|
|
uploadFile.status = 'done';
|
|||
|
|
fileList.value = [uploadFile];
|
|||
|
|
|
|||
|
|
// 自动切换到文本输入标签页
|
|||
|
|
activeTab.value = 'text';
|
|||
|
|
|
|||
|
|
// 清空错误信息
|
|||
|
|
uploadError.value = '';
|
|||
|
|
|
|||
|
|
message.success('文件上传并解析成功!');
|
|||
|
|
} catch (error: any) {
|
|||
|
|
uploadError.value = `JSON 解析失败:${error.message || '文件格式不正确'}`;
|
|||
|
|
message.error('JSON 解析失败,请检查文件格式');
|
|||
|
|
uploadFile.status = 'error';
|
|||
|
|
fileList.value = [uploadFile];
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
reader.onerror = () => {
|
|||
|
|
uploadError.value = '文件读取失败,请重试';
|
|||
|
|
message.error('文件读取失败');
|
|||
|
|
uploadFile.status = 'error';
|
|||
|
|
fileList.value = [uploadFile];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
reader.readAsText(file, 'UTF-8');
|
|||
|
|
|
|||
|
|
// 返回 false 阻止自动上传(因为我们手动处理)
|
|||
|
|
return false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 移除文件
|
|||
|
|
const handleRemoveFile = () => {
|
|||
|
|
fileList.value = [];
|
|||
|
|
uploadError.value = '';
|
|||
|
|
if (!jsonText.value) {
|
|||
|
|
parsedData.value = null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 格式化JSON
|
|||
|
|
const formatJson = () => {
|
|||
|
|
try {
|
|||
|
|
const json = JSON.parse(jsonText.value);
|
|||
|
|
jsonText.value = JSON.stringify(json, null, 2);
|
|||
|
|
message.success('格式化成功');
|
|||
|
|
} catch (error) {
|
|||
|
|
message.error('JSON格式错误,无法格式化');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 清空
|
|||
|
|
const clearJson = () => {
|
|||
|
|
jsonText.value = '';
|
|||
|
|
parsedData.value = null;
|
|||
|
|
formData.value = {};
|
|||
|
|
formApi.resetFields();
|
|||
|
|
draftId.value = null;
|
|||
|
|
selectedDraft.value = '';
|
|||
|
|
fileList.value = [];
|
|||
|
|
uploadError.value = '';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 生成代码
|
|||
|
|
const handleGenerate = async () => {
|
|||
|
|
if (!parsedData.value) {
|
|||
|
|
message.warning('请先导入或填写JSON数据');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证数据
|
|||
|
|
const validation = validateData(parsedData.value);
|
|||
|
|
if (!validation.valid) {
|
|||
|
|
message.error(validation.message);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并表单数据(用户可能修改了基础信息)
|
|||
|
|
const formValues = await formApi.getValues();
|
|||
|
|
const finalData: CodeGenData = {
|
|||
|
|
...parsedData.value,
|
|||
|
|
class_name: formValues.class_name || parsedData.value.class_name,
|
|||
|
|
class_comment: formValues.class_comment || parsedData.value.class_comment,
|
|||
|
|
icon: formValues.icon || parsedData.value.icon,
|
|||
|
|
sort: formValues.sort ?? parsedData.value.sort,
|
|||
|
|
pid: formValues.pid ?? parsedData.value.pid,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
emit('generate', finalData);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 删除草稿
|
|||
|
|
const handleDeleteDraft = (id: string) => {
|
|||
|
|
if (deleteDraft(mode, id)) {
|
|||
|
|
message.success('草稿已删除');
|
|||
|
|
loadDraftList();
|
|||
|
|
if (draftId.value === id) {
|
|||
|
|
clearJson();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadDraftList();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="json-import">
|
|||
|
|
<!-- 顶部操作栏 -->
|
|||
|
|
<Card class="mb-4">
|
|||
|
|
<template #title>
|
|||
|
|
<div class="flex items-center justify-between">
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<Button type="text" @click="emit('back')">
|
|||
|
|
<i class="fa-solid fa-arrow-left mr-2"></i>返回
|
|||
|
|
</Button>
|
|||
|
|
<span class="text-lg font-semibold">JSON导入模式</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<Select
|
|||
|
|
v-model:value="selectedDraft"
|
|||
|
|
placeholder="选择草稿"
|
|||
|
|
style="width: 200px"
|
|||
|
|
allow-clear
|
|||
|
|
@change="handleLoadDraft"
|
|||
|
|
>
|
|||
|
|
<Select.Option
|
|||
|
|
v-for="draft in drafts"
|
|||
|
|
:key="draft.id"
|
|||
|
|
:value="draft.id"
|
|||
|
|
>
|
|||
|
|
<div class="flex items-center justify-between">
|
|||
|
|
<span>{{ draft.name }}</span>
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
danger
|
|||
|
|
size="small"
|
|||
|
|
@click.stop="handleDeleteDraft(draft.id)"
|
|||
|
|
>
|
|||
|
|
<i class="fa-solid fa-trash"></i>
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</Select.Option>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<Tabs v-model:activeKey="activeTab">
|
|||
|
|
<Tabs.TabPane key="text" tab="文本输入">
|
|||
|
|
<div class="mb-4">
|
|||
|
|
<Input.TextArea
|
|||
|
|
v-model:value="jsonText"
|
|||
|
|
:rows="12"
|
|||
|
|
placeholder="请粘贴JSON数据..."
|
|||
|
|
:class="{ 'border-red-500': jsonError }"
|
|||
|
|
class="font-mono text-sm"
|
|||
|
|
/>
|
|||
|
|
<div v-if="jsonError" class="mt-2 text-red-500 text-sm">
|
|||
|
|
<i class="fa-solid fa-circle-exclamation mr-1"></i>{{ jsonError }}
|
|||
|
|
</div>
|
|||
|
|
<div class="mt-3 flex gap-2">
|
|||
|
|
<Button @click="formatJson">
|
|||
|
|
<i class="fa-solid fa-code mr-2"></i>格式化
|
|||
|
|
</Button>
|
|||
|
|
<Button @click="clearJson">
|
|||
|
|
<i class="fa-solid fa-trash mr-2"></i>清空
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Tabs.TabPane>
|
|||
|
|
<Tabs.TabPane key="file" tab="文件上传">
|
|||
|
|
<div class="upload-container">
|
|||
|
|
<Upload.Dragger
|
|||
|
|
:file-list="fileList"
|
|||
|
|
:before-upload="handleBeforeUpload"
|
|||
|
|
accept=".json,application/json"
|
|||
|
|
:show-upload-list="true"
|
|||
|
|
@remove="handleRemoveFile"
|
|||
|
|
class="upload-dragger"
|
|||
|
|
>
|
|||
|
|
<div class="upload-content">
|
|||
|
|
<div class="upload-icon">
|
|||
|
|
<i class="fa-solid fa-cloud-arrow-up text-4xl text-primary"></i>
|
|||
|
|
</div>
|
|||
|
|
<p class="upload-text">
|
|||
|
|
点击或拖拽文件到此区域上传
|
|||
|
|
</p>
|
|||
|
|
<p class="upload-hint">
|
|||
|
|
支持上传 JSON 格式文件
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</Upload.Dragger>
|
|||
|
|
<div v-if="uploadError" class="mt-4 rounded-lg bg-red-50 p-3 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
|
|||
|
|
<i class="fa-solid fa-circle-exclamation mr-2"></i>{{ uploadError }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Tabs.TabPane>
|
|||
|
|
</Tabs>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<!-- 内容区域 -->
|
|||
|
|
<div v-if="parsedData" class="content-area mb-24 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|||
|
|
<Card title="基础信息" class="shadow-sm">
|
|||
|
|
<Form />
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card title="字段预览" class="shadow-sm">
|
|||
|
|
<div class="max-h-[500px] overflow-auto">
|
|||
|
|
<JsonViewer
|
|||
|
|
:value="previewData"
|
|||
|
|
:expand-depth="2"
|
|||
|
|
copyable
|
|||
|
|
boxed
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="mt-4 flex items-center gap-2 text-sm text-gray-500">
|
|||
|
|
<i class="fa-solid fa-list mr-1"></i>
|
|||
|
|
<span>共 {{ parsedData.field?.length || 0 }} 个字段</span>
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 固定底部操作栏 -->
|
|||
|
|
<div class="fixed-bottom-bar">
|
|||
|
|
<div class="fixed-bottom-bar-wrapper">
|
|||
|
|
<div class="fixed-bottom-bar-content">
|
|||
|
|
<!-- 左侧状态信息 -->
|
|||
|
|
<div class="flex items-center gap-3">
|
|||
|
|
<div
|
|||
|
|
class="flex h-10 w-10 items-center justify-center rounded-full transition-all"
|
|||
|
|
:class="
|
|||
|
|
parsedData
|
|||
|
|
? 'bg-primary/10 text-primary'
|
|||
|
|
: 'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-600'
|
|||
|
|
"
|
|||
|
|
>
|
|||
|
|
<i
|
|||
|
|
:class="parsedData ? 'fa-solid fa-check-circle' : 'fa-solid fa-circle-info'"
|
|||
|
|
></i>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div
|
|||
|
|
class="text-sm font-medium"
|
|||
|
|
:class="
|
|||
|
|
parsedData
|
|||
|
|
? 'text-gray-900 dark:text-gray-100'
|
|||
|
|
: 'text-gray-500 dark:text-gray-400'
|
|||
|
|
"
|
|||
|
|
>
|
|||
|
|
<span v-if="parsedData">
|
|||
|
|
已解析 <span class="text-primary font-semibold">{{ parsedData.field?.length || 0 }}</span> 个字段
|
|||
|
|
</span>
|
|||
|
|
<span v-else>请先导入JSON数据</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|||
|
|
{{ parsedData ? '数据已就绪,可以生成代码' : '等待数据导入...' }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧操作按钮 -->
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<Button
|
|||
|
|
size="large"
|
|||
|
|
class="action-btn-secondary"
|
|||
|
|
@click="clearJson"
|
|||
|
|
>
|
|||
|
|
<i class="fa-solid fa-trash mr-2"></i>清空
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
size="large"
|
|||
|
|
class="action-btn-secondary"
|
|||
|
|
:disabled="!parsedData"
|
|||
|
|
@click="handleSaveDraft"
|
|||
|
|
>
|
|||
|
|
<i class="fa-solid fa-save mr-2"></i>保存草稿
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
size="large"
|
|||
|
|
class="action-btn-primary"
|
|||
|
|
:disabled="!parsedData"
|
|||
|
|
@click="handleGenerate"
|
|||
|
|
>
|
|||
|
|
<i class="fa-solid fa-code mr-2"></i>生成代码
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.json-import {
|
|||
|
|
animation: fade-slide-enter 0.3s ease-out;
|
|||
|
|
padding-bottom: 120px; /* 为固定底部栏留出空间 */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content-area {
|
|||
|
|
min-height: 400px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 优化卡片样式 */
|
|||
|
|
:deep(.ant-card) {
|
|||
|
|
border-radius: 12px;
|
|||
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-card:hover) {
|
|||
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark :deep(.ant-card) {
|
|||
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 固定底部操作栏 - 现代化设计 */
|
|||
|
|
.fixed-bottom-bar {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
z-index: 100;
|
|||
|
|
padding: 0 24px 24px;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-wrapper {
|
|||
|
|
max-width: 1400px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
background: rgba(255, 255, 255, 0.98);
|
|||
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|||
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|||
|
|
border-radius: 16px;
|
|||
|
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
|||
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
pointer-events: all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .fixed-bottom-bar-wrapper {
|
|||
|
|
background: rgba(31, 41, 55, 0.98);
|
|||
|
|
border-color: rgba(255, 255, 255, 0.12);
|
|||
|
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-content {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 20px 24px;
|
|||
|
|
gap: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-secondary {
|
|||
|
|
border-radius: 8px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-secondary:hover:not(:disabled) {
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-primary {
|
|||
|
|
border-radius: 8px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
padding: 0 24px;
|
|||
|
|
height: 44px;
|
|||
|
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-primary:hover:not(:disabled) {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-primary:active:not(:disabled) {
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fade-slide-enter {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateX(-20px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateX(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 文件上传样式优化 */
|
|||
|
|
.upload-container {
|
|||
|
|
padding: 8px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-dragger {
|
|||
|
|
border-radius: 12px;
|
|||
|
|
border: 2px dashed #d9d9d9;
|
|||
|
|
background: #fafafa;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-dragger:hover {
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
background: #f0f7ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .upload-dragger {
|
|||
|
|
background: rgba(255, 255, 255, 0.02);
|
|||
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .upload-dragger:hover {
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
background: rgba(24, 144, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-content {
|
|||
|
|
padding: 40px 20px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-icon {
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
opacity: 0.8;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-dragger:hover .upload-icon {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(-4px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-text {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #262626;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .upload-text {
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-hint {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .upload-hint {
|
|||
|
|
color: #8c8c8c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 响应式优化 */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.fixed-bottom-bar {
|
|||
|
|
padding: 0 16px 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-wrapper {
|
|||
|
|
border-radius: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-content {
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: stretch;
|
|||
|
|
padding: 16px;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-content > div:first-child {
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-content > div:last-child {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-content > div:last-child .action-btn-primary {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|