This commit is contained in:
2026-07-27 17:19:36 +08:00
parent e50d1b8fda
commit 1c7532e0de
132 changed files with 7066 additions and 1241 deletions

View File

@@ -1,9 +1,11 @@
# AGENTS.md
本文件用于指导 Codex (Codex.ai/code) 在 Saber3 代码仓库中工作时的行为规范。
> 本规范适用于 Saber3 前端工程的所有开发任务,为强制性条款。除非用户显式豁免,任何条目都不得忽视或删减。
>
> 作为 AI 助手参与本项目开发时,你必须:
>
> - 每次输出前深度理解 BladeX 微服务架构体系、Saber3 前端工程结构和 Vue 3 技术栈特征
> - 当回答依赖外部知识时,先查询 Vue 3、Element Plus、Avue、Vite 等官方文档
> - 若需求含糊,先复述已知信息并列出关键澄清问题
@@ -21,7 +23,7 @@
### 1.1 技术栈
| 技术 | 版本 | 用途 |
| ------------------ | -------- | ------------------ |
| --------------------- | ------- | ------------------ |
| Vue | ^3.5.13 | 核心框架 |
| Element Plus | ^2.10.1 | UI 组件库 |
| @smallwei/avue | ^3.7.2 | 增强型 CRUD 组件库 |
@@ -91,27 +93,36 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
- 命名约定:列表 `getList(current, size, params)`、详情 `getDetail(id)`、新增 `add(row)`、更新 `update(row)`、删除 `remove(ids)`、树形 `getXxxTree()`
- 后端微服务前缀:`/blade-system/``/blade-resource/``/blade-flow/``/blade-desk/``/blade-log/``/blade-develop/`
### 3.4 认证机制
### 3.4 批量导入失败文件下载规范
- 所有批量导入功能如果存在部分数据无法导入、校验失败或后端处理异常,前端必须支持自动下载后端返回的导入失败 Excel。
- 失败 Excel 的字段顺序必须与原导入模板保持一致,并在最后一列追加“导入失败原因”。
- 前端上传接口需支持 `blob` 响应Avue 默认上传无法可靠处理文件流时,应使用 `httpRequest` 或独立 API 方法自定义上传。
- 前端需根据响应 `content-type` 或 Blob 类型区分 JSON 成功响应与 Excel 文件流:成功时提示并刷新列表,失败文件流时下载失败明细并提示“部分数据导入失败,已下载失败明细”。
- 批量导入失败明细文件命名统一包含业务名称、`导入失败明细` 和时间戳,例如 `货物类型导入失败明细YYYY-MM-DD HH:mm:ss.xlsx`
- 批量导入的前端预校验与后端导入校验必须与新增、编辑表单校验保持一致,包括必填、长度、格式、枚举范围、父子级联关系和金额/日期等业务规则;新增、编辑校验调整时,必须同步更新导入校验。
### 3.5 认证机制
- OAuth2`Basic` 头传递 `clientId:clientSecret`Base64 编码)
- Token请求头 `Blade-Auth: bearer {token}`,支持 AES 加密模式
- 存储:`saber3-access-token` / `saber3-refresh-token`(通过 `utils/auth.js` 管理)
- 401 自动刷新 Token并发请求排队等待登录密码使用 SM2 国密加密
### 3.5 路由系统
### 3.6 路由系统
- 静态路由:`router/page/` + `router/views/`
- 动态路由:`avue-router.js` 将后端菜单数据转换为 Vue Router 路由
- 多级路由自动扁平化为二级,支持 keep-alive 跨层级缓存
- 外部链接自动转换为 iframe 路由,支持 Token 透传
### 3.6 权限控制
### 3.7 权限控制
- 路由守卫:`permission.js` 控制登录态、锁屏、标签页
- 按钮权限:`store.getters.permission`,格式 `{module}_{action}`(如 `dict_add`
- 管理员判断:`userInfo.authority.includes('admin')`
### 3.7 多租户
### 3.8 多租户
通过 `website.tenantMode` 控制开关,管理组租户编号 `000000`,后端自动通过请求头传递租户信息。
@@ -124,6 +135,7 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
项目同时支持 **Options API**(主流,绝大多数现有页面)和 **Composition API**(新组件可选)。
**选择原则**
- 修改现有页面:保持该页面原有风格,不混用
- 新建 CRUD 页面:调用 `avue-design` Skill 生成,或参考现有页面手动编写
- 新建复杂交互页面:可使用 Composition API + `<script setup>`
@@ -132,7 +144,7 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
### 4.2 命名规范
| 类型 | 命名方式 | 示例 |
| -------------- | -------------------------- | ----------------------------- |
| ----------------------- | -------------------------- | --------------------------- |
| 页面文件 | kebab-case | `notice.vue` |
| 组件文件 | kebab-case 目录 + main.vue | `basic-container/main.vue` |
| API / Option / 工具文件 | camelCase | `dict.js``dictbiz.js` |
@@ -148,6 +160,21 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
- 全局 SCSS 变量通过 `styles/variables.scss` 定义Vite 自动注入所有组件
- 编写样式优先使用已有变量和 mixin`styles/mixin.scss`),而非硬编码值
- 系统主色调统一使用 `#409eff`涉及主题变量、Element Plus 主题覆盖、按钮/链接/选中态等品牌色场景均应保持一致
- 各页面搜索栏多行展示时,行与行之间的垂直间距统一为 `8px`
- 搜索组件 label 宽度统一不小于 `160px`label 文本不得换行。
- 搜索组件内部 padding 统一为上、左右 `12px`,底部 `4px`
- 搜索组件需展示轻量阴影,统一使用 `0 2px 8px rgba(0, 0, 0, 0.06)`,不得被局部卡片去样式规则覆盖。
- 表格线条颜色需统一使用 `#EFF1F7`,包括表格外边框、单元格分割线和固定列边线。
- 表格偶数行需统一使用 `#FAFAFA` 背景色,固定列单元格必须与对应行背景保持一致。
- 表格操作列同时展示“查看、编辑、删除”等三个按钮时,操作列宽度统一不小于 `220px`,禁止出现按钮裁切或显示不全。
- `.avue-crud__header` 顶部间距统一为 `12px`
- 分页组件整体靠右展示,必须展示接口返回的数据总条数,`X条/页` 的页容量选择器必须放在总条数右侧。
- `.basic-container__card` 的直接子级 `.el-card__body` 内边距统一去除,保持 `padding: 0`
- 所有 CRUD 页面搜索栏需与下方表格拆分为独立区域,两者垂直间距统一为 `8px`
- 所有 CRUD 页面搜索栏操作按钮(搜索、清空等)需在搜索表单下方独占一行并放置在最右侧;搜索条件超过两行时必须显示“展开/折叠”,默认折叠且仅展示两行搜索条件。
- 所有 CRUD 页面中新增、批量导入等顶部按钮组行不需要背景色,应保持透明背景。
- 所有 `.basic-container__card` 内的 Avue 内层 `el-card` 需去卡片化,仅保留外层容器卡片样式,禁止出现二级卡片边框、阴影或额外内边距。
### 4.5 全局注册组件
@@ -165,8 +192,15 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
### 4.8 表单布局规范
- 新增/编辑弹窗中的表单项,若 `label` 文案超过 4 个字,`label` 必须单独占一行展示,表单控件另起一行,避免长标签挤压输入控件或换行错位
- 使用 Avue 配置、Element Plus 表单或自定义弹窗表单时均需遵守该规则;可通过 `label-position="top"`、自定义 `formslot`、局部样式或单项布局调整实现
- 新增编辑弹窗中的表单 `label` 默认必须位于控件左侧Avue 配置统一使用 `labelPosition: 'right'``labelWidth: 'auto'`Element Plus 表单统一使用 `label-position="right"``label-width="auto"`
- 新增、编辑弹窗中的表单 `label` 宽度必须以当前视觉列内最长 `label` 为准;多列表单需按列分别计算 label 宽度,禁止用整张表单的最长 label 统一挤压所有列
- 所有新增、编辑弹窗中的表单 `label` 文本必须右对齐,确保同一列内控件起始位置一致。
- 新增、编辑弹窗标题左侧必须展示 `4px` 宽的主色竖条,竖条与标题文本间距统一为 `8px`
- 复杂布局弹窗中的自定义分区标题、明细表格标题、步骤区标题等承担弹窗内容标题作用时,也必须沿用 `4px` 主色竖条与 `8px` 标题间距;可使用全局 `.dialog-section-title` 或等效局部样式实现。
- 新增、编辑弹窗中的表单项上下间距统一为 `16px`,弹窗内表单布局不得叠加 `.el-col``.el-form-item` 的额外下边距。
- 所有新增、编辑弹窗中的备注字段如果有必须单独占一整行展示禁止与其他字段并列Avue 配置可使用 `span: 24` 或等效布局实现。
- 新增、编辑弹窗底部按钮顺序统一为“取消、提交”,取消按钮在左,提交按钮在右。
- 使用 Avue 配置、Element Plus 表单或自定义弹窗表单时均需遵守该规则;若局部组件因特殊结构无法自动对齐,应通过 `src/utils/dialog-form-label.js` 的全局增强、局部样式或 `formslot` 保证 `label` 左侧展示、按列统一宽度、文本右对齐。
### 4.9 审计字段展示规范
@@ -175,6 +209,10 @@ Avue 表格/表单配置独立存放于 `src/option/` 目录,与 `views` 和 `
- “更新人”列统一使用 `prop: 'updateUserName'`,并通过 `src/utils/audit.js` 中的 `formatUpdateUserName(row)` 做兼容兜底。
- 表单提交仍保留后端审计字段机制,前端不得手动提交或覆盖 `createUser``updateUser`
### 4.10 表格排序规范
- 表格的排序默认按照创建时间降序;分页列表应由后端默认按 `create_time DESC` 返回,前端不得在无明确业务要求时覆盖为其它默认排序。
---
## 5. 新功能开发流程
@@ -226,7 +264,7 @@ pnpm run serve # 预览构建产物
项目采用 **Gitmoji** 风格,中文描述:
| Emoji | 代码 | 场景 |
| ---------- | ------------ | ---------------- |
| ---------- | ------------ | ------------------ |
| :sparkles: | `:sparkles:` | 新增功能、优化增强 |
| :bug: | `:bug:` | 修复 Bug |
| :zap: | `:zap:` | 性能优化、问题修复 |

View File

@@ -13,14 +13,13 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection" content="telephone=no" />
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/iconfont/index.css" />
<link rel="stylesheet" href="/iconfont/avue/iconfont.css" />
<link rel="stylesheet" href="/iconfont/saber/iconfont.css" />
<link rel="stylesheet" href="/iconfont/common/iconfont.css" />
<link rel="stylesheet" href="/css/loading.css" />
<link rel="stylesheet" href="/css/saber.css" />
<title>TMS物流运输管理系统</title>
<title>运输板块数字化管理平台</title>
</head>
<body>

View File

@@ -11,6 +11,8 @@
"dependencies": {
"element-plus": "^2.14.2",
"@element-plus/icons-vue": "^2.3.1",
"@open-file-viewer/core": "^0.1.27",
"@open-file-viewer/vue": "^0.1.27",
"@smallwei/avue": "~3.9.2",
"avue-plugin-ueditor": "^1.0.4",
"@saber/nf-design-base-elp": "^1.3.0",
@@ -27,6 +29,7 @@
"js-cookie": "^3.0.0",
"js-md5": "^0.7.3",
"nprogress": "^0.2.0",
"pdfjs-dist": "^6.1.200",
"sm-crypto": "^0.4.0",
"vue": "^3.5.13",
"vue-i18n": "^11.4.2",

701
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -0,0 +1,73 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/cargo-type/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/cargo-type/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/cargo-type/remove',
method: 'post',
params: {
ids,
},
});
};
export const submit = row => {
return request({
url: '/blade-system/cargo-type/submit',
method: 'post',
data: row,
});
};
export const importCargoType = file => {
const formData = new FormData();
formData.append('file', file);
return request({
url: '/blade-system/cargo-type/import-cargo-type',
method: 'post',
data: formData,
responseType: 'blob',
timeout: 60000,
});
};
export const getParentOptions = keyword => {
return request({
url: '/blade-system/cargo-type/parent-options',
method: 'get',
params: {
keyword,
},
});
};
export const getNextCode = parentCargoCode => {
return request({
url: '/blade-system/cargo-type/next-code',
method: 'get',
params: {
parentCargoCode,
},
});
};

View File

@@ -13,3 +13,15 @@ export const exportBlob = (url, params) => {
responseType: 'blob',
});
};
export const importBlob = (url, file) => {
const formData = new FormData();
formData.append('file', file);
return request({
url,
method: 'post',
data: formData,
responseType: 'blob',
timeout: 60000,
});
};

View File

@@ -48,6 +48,11 @@ export default {
&__card {
width: 100%;
background: none;
> .el-card__body {
padding: 0;
}
}
&:first-child {

View File

@@ -13,7 +13,7 @@
<div class="cron-config-area">
<!-- 每天模式 -->
<div v-if="cronMode === 'daily'" class="mode-content">
<el-form :model="dailyConfig" label-width="100px" size="default">
<el-form :model="dailyConfig" label-width="auto" size="default">
<el-form-item label="执行时间">
<el-time-picker
v-model="dailyConfig.time"
@@ -30,7 +30,7 @@
<!-- 每周模式 -->
<div v-if="cronMode === 'weekly'" class="mode-content">
<el-form :model="weeklyConfig" label-width="100px" size="default">
<el-form :model="weeklyConfig" label-width="auto" size="default">
<el-form-item label="星期选择">
<el-checkbox-group v-model="weeklyConfig.weekDays" @change="generateCron" :disabled="disabled">
<el-checkbox label="1">周一</el-checkbox>
@@ -58,7 +58,7 @@
<!-- 每月模式 -->
<div v-if="cronMode === 'monthly'" class="mode-content">
<el-form :model="monthlyConfig" label-width="100px" size="default">
<el-form :model="monthlyConfig" label-width="auto" size="default">
<el-form-item label="日期选择">
<el-select
v-model="monthlyConfig.days"
@@ -93,7 +93,7 @@
<!-- 自定义模式 -->
<div v-if="cronMode === 'custom'" class="mode-content">
<el-form :model="customConfig" label-width="100px" size="default">
<el-form :model="customConfig" label-width="auto" size="default">
<el-form-item label="秒">
<el-input
v-model="customConfig.second"
@@ -161,7 +161,7 @@
</div>
<div class="cron-result">
<el-form label-width="120px">
<el-form label-width="auto">
<el-form-item label="Cron 表达式">
<el-input
v-model="cronExpression"
@@ -567,7 +567,7 @@ export default {
font-family: 'Courier New', monospace;
font-size: 14px;
font-weight: 600;
color: #409eff;
color: var(--el-color-primary);
}
:deep(.el-checkbox) {

View File

@@ -3,10 +3,10 @@
<el-pagination
background
:current-page="page.currentPage"
:page-size="page.pageSize"
:page-size="pageSize"
:page-sizes="pageSizes"
layout="total, sizes, prev, pager, next, jumper"
:total="0"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
@@ -27,9 +27,15 @@ export default {
isEmptyTotal() {
return Number(this.page.total || 0) === 0;
},
pageSize() {
return this.page.pageSize || 10;
},
pageSizes() {
return this.page.pageSizes || [10, 20, 50, 100];
},
total() {
return Number(this.page.total || 0);
},
},
methods: {
handleSizeChange(pageSize) {
@@ -51,7 +57,6 @@ export default {
.empty-pagination {
display: flex;
justify-content: flex-end;
padding: 20px 0 0;
background: #fff;
}
</style>

View File

@@ -8,7 +8,7 @@
:show-close="false"
width="20%"
>
<el-form :model="form" ref="form" label-width="80px">
<el-form :model="form" ref="form" label-width="auto">
<el-form-item v-if="tenantMode" label="租户编号">
<el-input v-model="form.tenantId" placeholder="请输入租户编号"></el-input>
</el-form-item>

View File

@@ -0,0 +1,364 @@
<template>
<div class="vehicle-attachment-upload">
<el-upload
:action="uploadAction"
name="file"
:headers="uploadHeaders"
:file-list="fileList"
:accept="acceptText"
:multiple="multiple"
:limit="limit"
:disabled="readonly"
:show-file-list="true"
:on-success="handleSuccess"
:on-error="handleError"
:on-remove="handleRemove"
:on-preview="handlePreview"
:before-upload="beforeUpload"
>
<el-button v-if="!readonly" type="primary" plain :icon="buttonIcon">
{{ buttonText }}
</el-button>
<template #tip>
<div v-if="showTip && tipText && !readonly" class="el-upload__tip">
{{ tipText }}
</div>
</template>
</el-upload>
<span v-if="readonly && fileList.length === 0" class="vehicle-attachment-upload__empty">
-
</span>
<el-dialog
v-model="previewVisible"
:title="previewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
class="vehicle-attachment-upload__viewer-dialog"
>
<open-file-viewer
v-if="previewVisible && previewSource"
:file="previewSource"
:file-name="previewFile.name"
:mime-type="previewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="viewerToolbar"
:plugins="viewerPlugins"
@unsupported="handleUnsupported"
@error="handleViewerError"
/>
</el-dialog>
</div>
</template>
<script>
import { OpenFileViewer } from '@open-file-viewer/vue';
import {
fallbackPlugin,
imagePlugin,
officePlugin,
pdfPlugin,
textPlugin,
} from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { baseUrl } from '@/config/env';
import website from '@/config/website';
import { getToken } from '@/utils/auth';
const MIME_MAP = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
bmp: 'image/bmp',
webp: 'image/webp',
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
txt: 'text/plain',
};
const viewerPlugins = [
imagePlugin(),
pdfPlugin({
workerSrc: pdfWorkerSrc,
useFetchData: true,
}),
officePlugin({
pdf: {
workerSrc: pdfWorkerSrc,
useFetchData: true,
},
}),
textPlugin(),
fallbackPlugin(),
];
export default {
name: 'VehicleAttachmentUpload',
components: {
OpenFileViewer,
},
props: {
modelValue: {
type: [Array, String, Object],
default: () => [],
},
action: {
type: String,
default: '',
},
accept: {
type: [String, Array],
default: '',
},
fileTypes: {
type: Array,
default: () => [],
},
multiple: {
type: Boolean,
default: true,
},
limit: {
type: Number,
default: 20,
},
maxSize: {
type: Number,
default: 0,
},
readonly: {
type: Boolean,
default: false,
},
buttonText: {
type: String,
default: '上传附件',
},
buttonIcon: {
type: String,
default: 'el-icon-upload',
},
tip: {
type: String,
default: '',
},
showTip: {
type: Boolean,
default: true,
},
},
emits: ['update:modelValue', 'change', 'success'],
data() {
return {
fileList: [],
previewVisible: false,
previewFile: {},
previewSource: '',
viewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
viewerPlugins,
};
},
computed: {
uploadAction() {
return this.action || `${baseUrl}/blade-resource/oss/endpoint/put-file`;
},
uploadHeaders() {
return {
[website.tokenHeader]: getToken(),
};
},
acceptedList() {
const types = this.fileTypes.length ? this.fileTypes : this.accept;
if (Array.isArray(types)) {
return types.map(item => String(item).trim()).filter(Boolean);
}
return String(types || '')
.split(',')
.map(item => item.trim())
.filter(Boolean);
},
acceptText() {
return this.acceptedList.join(',');
},
tipText() {
if (this.tip) return this.tip;
const typeText = this.acceptText || '所有类型';
const sizeText = this.maxSize > 0 ? `,单文件不超过 ${this.maxSize}MB` : '';
return `支持上传 ${typeText}${sizeText}`;
},
},
watch: {
modelValue: {
handler(value) {
this.fileList = this.toUploadList(value);
},
immediate: true,
deep: true,
},
},
methods: {
toUploadList(value) {
const list = this.parseValue(value);
return list.map((item, index) => {
const url = this.getFileUrl(item);
return {
...item,
uid: item.uid || `${url || item.name || 'file'}-${index}`,
name: item.name || this.getFileName(url) || '附件',
url,
status: 'success',
};
});
},
parseValue(value) {
if (!value) return [];
if (Array.isArray(value)) return value.filter(Boolean);
if (typeof value === 'object') return [value];
try {
const list = JSON.parse(value);
return Array.isArray(list) ? list : [];
} catch (error) {
return String(value)
.split(',')
.map(item => item.trim())
.filter(Boolean)
.map(item => ({
name: this.getFileName(item),
url: item,
}));
}
},
getFileUrl(file) {
return file.url || file.link || file.src || file.domain || '';
},
getFileName(url) {
if (!url) return '';
const path = String(url).split('?')[0];
return decodeURIComponent(path.substring(path.lastIndexOf('/') + 1));
},
getExtension(file) {
const name = String(file.name || this.getFileUrl(file) || '').split('?')[0];
const index = name.lastIndexOf('.');
return index >= 0 ? name.substring(index + 1).toLowerCase() : '';
},
beforeUpload(file) {
if (!this.validFileType(file)) {
this.$message.warning('文件类型不符合上传要求');
return false;
}
if (this.maxSize > 0 && file.size / 1024 / 1024 > this.maxSize) {
this.$message.warning(`单文件大小不能超过 ${this.maxSize}MB`);
return false;
}
return true;
},
validFileType(file) {
if (!this.acceptedList.length) return true;
const fileName = String(file.name || '').toLowerCase();
const mimeType = String(file.type || '').toLowerCase();
return this.acceptedList.some(type => {
const value = String(type).toLowerCase();
if (value === '*/*') return true;
if (value.startsWith('.')) return fileName.endsWith(value);
if (value.endsWith('/*')) return mimeType.startsWith(value.slice(0, -1));
if (value.includes('/')) return mimeType === value;
return fileName.endsWith(`.${value}`);
});
},
handleSuccess(response, file, files) {
if (!response || response.success === false || (response.code && response.code !== 200)) {
this.$message.error((response && response.msg) || '上传失败');
return;
}
this.emitUploadFiles(this.multiple ? files : [file]);
this.$emit('success', this.normalizeUploadFile(file));
this.$message.success('上传成功');
},
handleError() {
this.$message.error('上传失败');
},
handleRemove(file, files) {
if (this.readonly) return false;
this.emitUploadFiles(files);
return true;
},
emitUploadFiles(files) {
const list = files
.filter(file => file.status === 'success' || this.getFileUrl(file))
.map(this.normalizeUploadFile)
.filter(item => item.url);
this.$emit('update:modelValue', list);
this.$emit('change', list);
},
normalizeUploadFile(file) {
const data = (file.response && file.response.data) || file.data || file;
const url = data.link || data.url || data.domain || file.url || '';
const name = data.name || data.originalName || file.name || this.getFileName(url) || '附件';
const extension = this.getExtension({ name, url });
return {
...data,
uid: file.uid || data.uid,
name,
url,
link: data.link || url,
size: data.size || data.attachSize || file.size || '',
extension,
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
};
},
handlePreview(file) {
const previewFile = this.normalizeUploadFile(file);
if (!previewFile.url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
this.previewFile = previewFile;
this.previewSource = previewFile.url;
this.previewVisible = true;
},
handleUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handleViewerError() {
this.$message.error('附件预览失败');
},
},
};
</script>
<style lang="scss" scoped>
.vehicle-attachment-upload {
width: 100%;
:deep(.el-upload-list__item-name) {
cursor: pointer;
}
&__empty {
color: #909399;
}
}
:global(.vehicle-attachment-upload__viewer-dialog .el-dialog__body) {
padding: 12px 16px 16px;
}
</style>

View File

@@ -5,7 +5,7 @@ export default {
title: '运输板块数字化管理平台',
logo: 'X',
key: 'saber', //配置主键,目前用于存储
indexTitle: 'TMS物流运输管理系统',
indexTitle: '运输板块数字化管理平台',
clientId: 'saber3', // 客户端id
clientSecret: 'saber3_secret', // 客户端密钥
tenantMode: true, // 是否开启租户模式
@@ -53,7 +53,7 @@ export default {
//水印配置
watermark: {
mode: false,
text: 'TMS物流运输管理系统',
text: '运输板块数字化管理平台',
},
//oauth2配置
oauth2: {

View File

@@ -1,5 +1,5 @@
export default {
title: 'TMS物流运输管理系统',
title: '运输板块数字化管理平台',
confirmTip: '提示',
logoutTip: '退出系统, 是否继续?',
submitText: '确定',
@@ -79,7 +79,7 @@ export default {
},
login: {
title: '登录 ',
info: 'TMS物流运输管理系统',
info: '运输板块数字化管理平台',
tenantId: '请输入租户ID',
name: '请输入姓名',
username: '请输入账号',

View File

@@ -13,6 +13,8 @@ import '@smallwei/avue/lib/index.css';
import avueUeditor from 'avue-plugin-ueditor';
import crudCommon from '@/mixins/crud.js';
import { getScreen, findColumn } from './utils/util';
import { setupPaginationEnhancer } from './utils/pagination';
import { setupDialogFormLabelAlign } from './utils/dialog-form-label';
import './permission';
import error from './error';
import App from './App.vue';
@@ -28,6 +30,7 @@ import cronEditor from './components/cron-editor/main.vue';
import basicBlock from './components/basic-block/main.vue';
import basicContainer from './components/basic-container/main.vue';
import emptyPagination from './components/empty-pagination/main.vue';
import vehicleAttachmentUpload from './components/vehicle-attachment-upload/main.vue';
import thirdRegister from './components/third-register/main.vue';
import flowDesign from './components/flow-design/main.vue';
import flowDesignStep from './components/flow-design-step/main.vue';
@@ -47,6 +50,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component('basicContainer', basicContainer);
app.component('basicBlock', basicBlock);
app.component('emptyPagination', emptyPagination);
app.component('vehicleAttachmentUpload', vehicleAttachmentUpload);
app.component('highlight', highlight);
app.component('codeEditor', codeEditor);
app.component('cronEditor', cronEditor);
@@ -72,6 +76,16 @@ app.use(ElementPlus, {
app.use(Avue, {
axios,
calcHeight: 10,
crudOption: {
searchIcon: true,
searchIndex: 8,
searchLabelWidth: 160,
searchMenuSpan: 24,
searchMenuPosition: 'right',
menuWidth: 220,
},
labelPosition: 'right',
labelWidth: 'auto',
locale: language,
i18n: (key, options) => i18n.global.t(key, options),
});
@@ -81,3 +95,5 @@ app.use(VueClipboard, {
appendToBody: true, // 这可以帮助解决一些更复杂的使用场景下的问题
});
app.mount('#app');
setupPaginationEnhancer();
setupDialogFormLabelAlign();

View File

@@ -11,7 +11,10 @@ export const createOption = ({ validateLongitude, validateLatitude }) => ({
dialogWidth: 980,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
@@ -83,16 +86,14 @@ export const createOption = ({ validateLongitude, validateLatitude }) => ({
{
label: '经度',
prop: 'longitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 120,
rules: [{ validator: validateLongitude, trigger: 'blur' }],
},
{
label: '纬度',
prop: 'latitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 120,
rules: [{ validator: validateLatitude, trigger: 'blur' }],
},

View File

@@ -120,13 +120,16 @@ export default {
calcHeight: 30,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
labelWidth: 180,
labelWidth: 'auto',
menuWidth: 350,
dialogWidth: 1200,
dialogClickModal: false,

View File

@@ -3,13 +3,16 @@ export default {
calcHeight: 30,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
labelWidth: 100,
labelWidth: 'auto',
menuWidth: 350,
dialogClickModal: false,
column: [

View File

@@ -12,7 +12,10 @@ export const LOCK_STATUS_DIC = [
export const authLockOption = {
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
addBtn: false,

View File

@@ -10,7 +10,10 @@ export const GRANT_TYPE_DIC = [
export const authLogOption = {
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
addBtn: false,

View File

@@ -3,7 +3,10 @@ export const optionParent = {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 10,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
@@ -92,7 +95,10 @@ export const optionChild = {
calcHeight: 95,
tip: false,
searchShow: true,
searchMenuSpan: 10,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,
@@ -105,8 +111,8 @@ export const optionChild = {
{
label: '字典编号',
prop: 'code',
addDisabled: true,
editDisabled: true,
addDisplay: false,
editDisplay: false,
search: true,
span: 24,
rules: [

View File

@@ -3,7 +3,10 @@ export const optionParent = {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 10,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
@@ -93,7 +96,10 @@ export const optionChild = {
calcHeight: 95,
tip: false,
searchShow: true,
searchMenuSpan: 10,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,
@@ -106,8 +112,8 @@ export const optionChild = {
{
label: '字典编号',
prop: 'code',
addDisabled: true,
editDisabled: true,
addDisplay: false,
editDisplay: false,
search: true,
span: 24,
rules: [

View File

@@ -23,7 +23,10 @@ export const userOption = safe => {
calcHeight: 150,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
@@ -353,13 +356,16 @@ export const userOption = safe => {
export const platformOption = {
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
viewBtn: true,
dialogClickModal: false,
menuWidth: 120,
menuWidth: 220,
editBtnText: '配置',
column: [
{
@@ -475,7 +481,7 @@ export const excelOption = {
export const platformFormOption = {
submitBtn: false,
emptyBtn: false,
labelWidth: 100,
labelWidth: 'auto',
column: [
{
label: '用户账号',

View File

@@ -6,11 +6,14 @@ export const codeOption = {
dialogWidth: 900,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
labelWidth: 120,
labelWidth: 'auto',
menuWidth: 350,
viewBtn: true,
dialogClickModal: false,
@@ -376,7 +379,7 @@ export const codeOption = {
};
export const genOption = {
labelWidth: 120,
labelWidth: 'auto',
column: [
{
label: '上级菜单',

View File

@@ -3,14 +3,17 @@ export default {
calcHeight: 50,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 100,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 320,
dialogClickModal: false,
column: [

View File

@@ -3,14 +3,17 @@ export default {
calcHeight: 50,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 100,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 320,
dialogWidth: 500,
dialogClickModal: false,

View File

@@ -3,7 +3,10 @@ export const option = {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,
@@ -71,7 +74,17 @@ export const option = {
{
label: '所属组织',
prop: 'organizationName',
type: 'cascader',
search: true,
emitPath: false,
checkStrictly: true,
filterable: true,
dicData: [],
props: {
label: 'label',
value: 'value',
children: 'children',
},
width: 140,
},
{

View File

@@ -3,7 +3,10 @@ export const option = {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -3,7 +3,10 @@ export const option = {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -33,21 +33,26 @@ export const accidentResponsibilityDic = [
export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 1050,
labelPosition: 'top',
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
column: [
{
label: '车船类型',
@@ -57,6 +62,7 @@ export const option = {
dicData: vehicleTypeDic,
value: '车辆',
search: true,
span: 24,
rules: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
},
{
@@ -66,6 +72,8 @@ export const option = {
formslot: true,
search: true,
minWidth: 140,
span: 12,
placeholder: '输入车牌号/船号查询选择',
rules: [
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
@@ -78,6 +86,8 @@ export const option = {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 140,
span: 12,
placeholder: '请选择,日期选择器',
rules: [{ required: true, message: '请选择事故发生日期', trigger: 'click' }],
},
{
@@ -85,6 +95,8 @@ export const option = {
prop: 'accidentLocation',
minWidth: 200,
overHidden: true,
span: 12,
placeholder: '请输入100字内',
rules: [{ max: 100, message: '最多 100 个字符', trigger: 'blur' }],
},
{
@@ -94,6 +106,8 @@ export const option = {
dicData: accidentNatureDic,
search: true,
minWidth: 120,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择事故性质', trigger: 'change' }],
},
{
@@ -103,6 +117,8 @@ export const option = {
dicData: accidentResponsibilityDic,
search: true,
minWidth: 120,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择事故责任', trigger: 'change' }],
},
{
@@ -112,6 +128,8 @@ export const option = {
precision: 2,
append: '元',
minWidth: 140,
span: 12,
placeholder: '请输入',
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -121,17 +139,20 @@ export const option = {
precision: 2,
append: '元',
minWidth: 140,
span: 12,
placeholder: '请输入',
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '事故原因',
label: '事故原因及损坏情况',
prop: 'accidentReasonDamage',
type: 'textarea',
minRows: 3,
minRows: 4,
span: 24,
search: true,
minWidth: 220,
overHidden: true,
placeholder: '请输入500字以内',
rules: [{ max: 500, message: '最多 500 个字符', trigger: 'blur' }],
},
{
@@ -142,24 +163,16 @@ export const option = {
span: 24,
minWidth: 180,
overHidden: true,
placeholder: '请输入200字以内',
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '事故评定开始日期',
@@ -203,7 +216,6 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD 00:00:00',
valueFormat: 'YYYY-MM-DD 00:00:00',
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -215,7 +227,6 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD 23:59:59',
valueFormat: 'YYYY-MM-DD 23:59:59',
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -261,6 +272,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -32,21 +32,26 @@ export const shipInspectionTypeDic = [
export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 1050,
labelPosition: 'top',
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
column: [
{
label: '车船类型',
@@ -56,6 +61,7 @@ export const option = {
dicData: vehicleTypeDic,
value: '车辆',
search: true,
span: 24,
rules: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
},
{
@@ -65,6 +71,8 @@ export const option = {
formslot: true,
search: true,
minWidth: 140,
span: 12,
placeholder: '输入车牌号/船号查询选择',
rules: [
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
@@ -77,6 +85,8 @@ export const option = {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 140,
span: 12,
placeholder: '请选择,日期选择器',
rules: [{ required: true, message: '请选择检测评定日期', trigger: 'click' }],
},
{
@@ -86,6 +96,8 @@ export const option = {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 140,
span: 12,
placeholder: '请输入',
rules: [{ required: true, message: '请选择有效期截止日', trigger: 'click' }],
},
{
@@ -94,6 +106,8 @@ export const option = {
type: 'select',
dicData: vehicleTechnicalLevelDic,
minWidth: 130,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择车辆技术等级', trigger: 'change' }],
},
{
@@ -103,18 +117,24 @@ export const option = {
dicData: shipInspectionTypeDic,
display: false,
minWidth: 130,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择船舶检验类型', trigger: 'change' }],
},
{
label: '客车类型及等级',
prop: 'passengerTypeLevel',
minWidth: 150,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
label: '检测评定单位',
prop: 'inspectionUnit',
minWidth: 160,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
@@ -124,6 +144,8 @@ export const option = {
precision: 2,
append: '元',
minWidth: 100,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入费用', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -133,6 +155,8 @@ export const option = {
label: '评定(复核)单位',
prop: 'assessmentUnit',
minWidth: 160,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
@@ -143,24 +167,16 @@ export const option = {
span: 24,
minWidth: 180,
overHidden: true,
placeholder: '请输入200字以内',
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '检测评定开始日期',
@@ -204,7 +220,6 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD 00:00:00',
valueFormat: 'YYYY-MM-DD 00:00:00',
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -216,7 +231,6 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD 23:59:59',
valueFormat: 'YYYY-MM-DD 23:59:59',
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -262,6 +276,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -22,17 +22,26 @@ const maskEtcCardNo = value => {
export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'top',
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
menuFixed: 'right',
column: [
{
@@ -42,22 +51,13 @@ export const option = {
formslot: true,
search: true,
minWidth: 140,
span: 12,
placeholder: '输入车牌号模糊查询选择',
rules: [
{ required: true, message: '请输入车牌号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
],
},
{
label: 'ETC卡号',
prop: 'etcCardNo',
formatter: row => maskEtcCardNo(row.etcCardNo),
search: true,
minWidth: 220,
rules: [
{ required: true, message: '请输入ETC卡号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
],
},
{
label: '入口时间',
prop: 'entryTime',
@@ -65,6 +65,21 @@ export const option = {
format: 'YYYY-MM-DD HH:mm',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
span: 12,
placeholder: '请选择,日期选择器',
},
{
label: 'ETC卡号',
prop: 'etcCardNo',
formatter: row => maskEtcCardNo(row.etcCardNo),
search: true,
minWidth: 220,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入ETC卡号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
],
},
{
label: '出口时间',
@@ -73,18 +88,38 @@ export const option = {
format: 'YYYY-MM-DD HH:mm',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
span: 12,
placeholder: '请选择,日期选择器',
rules: [{ required: true, message: '请选择出口时间', trigger: 'click' }],
},
{
label: '入口站',
prop: 'entryStation',
minWidth: 150,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
label: '交易金额',
prop: 'transactionAmount',
type: 'number',
precision: 2,
append: '元',
minWidth: 130,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入交易金额', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
],
},
{
label: '出口站',
prop: 'exitStation',
minWidth: 150,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
@@ -94,18 +129,7 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '交易金额',
prop: 'transactionAmount',
type: 'number',
precision: 2,
append: '元',
minWidth: 130,
rules: [
{ required: true, message: '请输入交易金额', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
],
viewDisplay: false,
},
{
label: '备注',
@@ -115,24 +139,16 @@ export const option = {
span: 24,
minWidth: 180,
overHidden: true,
placeholder: '请输入200字以内',
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '组织',
@@ -185,6 +201,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -30,24 +30,26 @@ export const ocrTemplateDic = [
];
export const option = {
title: '保险记录(OCR)',
height: 'auto',
calcHeight: 32,
dialogWidth: 1100,
labelPosition: 'top',
labelPosition: 'right',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
column: [
{
label: '车船类型',
@@ -243,6 +245,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -12,71 +12,109 @@ const validateNonNegative = (rule, value, callback) => {
}
};
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
];
export const option = {
title: '里程记录(无船)',
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'top',
labelPosition: 'right',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
menuFixed: 'right',
column: [
{
label: '车牌号',
label: '车船类型',
prop: 'vehicleType',
type: 'radio',
dicData: vehicleTypeDic,
value: '车辆',
search: true,
minWidth: 110,
rules: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
},
{
label: '车牌号/船号',
prop: 'vehicleNo',
slot: true,
formslot: true,
search: true,
minWidth: 130,
rules: [
{ required: true, message: '请输入车牌号', trigger: 'blur' },
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
],
},
{
label: '上月统计里程表示值(km)',
label: '上月统计里程',
prop: 'previousMonthMileage',
type: 'number',
precision: 2,
append: '公里',
minWidth: 210,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '本月统计里程表示值(km)',
label: '本月统计里程',
prop: 'currentMonthMileage',
type: 'number',
precision: 2,
append: '公里',
minWidth: 210,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '本月行驶里程(km)',
label: '本月行驶里程',
prop: 'monthlyMileage',
type: 'number',
precision: 2,
append: '公里',
minWidth: 170,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '累计行驶里程(km)',
label: '累计行驶里程',
prop: 'totalMileage',
type: 'number',
precision: 2,
append: '公里',
minWidth: 170,
slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
label: '里程单位',
prop: 'mileageUnit',
type: 'select',
dicData: [
{ label: '公里', value: '公里' },
{ label: '海里', value: '海里' },
],
value: '公里',
hide: true,
display: false,
search: false,
},
{
label: '备注',
prop: 'remark',
@@ -90,37 +128,28 @@ export const option = {
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '累计行驶里程开始值',
label: '累计行驶里程',
prop: 'totalMileageStart',
type: 'number',
precision: 2,
search: true,
searchSpan: 8,
hide: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '累计行驶里程结束值',
label: '累计行驶里程结束值',
prop: 'totalMileageEnd',
type: 'number',
precision: 2,
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -202,6 +231,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -26,16 +26,19 @@ export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'top',
labelPosition: 'right',
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
menuFixed: 'right',
column: [
{
@@ -85,6 +88,7 @@ export const option = {
formslot: true,
search: true,
minWidth: 150,
placeholder: '输入车牌号/船号查询选择',
rules: [
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
@@ -159,19 +163,10 @@ export const option = {
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '交易开始时间',
@@ -237,7 +232,6 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD 00:00:00',
valueFormat: 'YYYY-MM-DD 00:00:00',
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -249,7 +243,6 @@ export const option = {
type: 'date',
format: 'YYYY-MM-DD 23:59:59',
valueFormat: 'YYYY-MM-DD 23:59:59',
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -295,6 +288,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -30,37 +30,28 @@ export const expenseTypeDic = [
export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'top',
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
menuFixed: 'right',
column: [
{
label: '费用日期',
prop: 'expenseDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 140,
rules: [{ required: true, message: '请选择费用日期', trigger: 'click' }],
},
{
label: '其他费用类型',
prop: 'expenseType',
type: 'select',
dicData: expenseTypeDic,
search: true,
minWidth: 150,
rules: [{ required: true, message: '请选择其他费用类型', trigger: 'change' }],
},
{
label: '车船类型',
prop: 'vehicleType',
@@ -69,6 +60,7 @@ export const option = {
value: '车辆',
search: true,
minWidth: 110,
span: 24,
rules: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
},
{
@@ -78,11 +70,49 @@ export const option = {
formslot: true,
search: true,
minWidth: 150,
span: 12,
placeholder: '输入车牌号/船号查询选择',
rules: [
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
],
},
{
label: '费用日期',
prop: 'expenseDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 140,
span: 12,
placeholder: '请选择,日期选择器',
rules: [{ required: true, message: '请选择费用日期', trigger: 'click' }],
},
{
label: '费用类型',
prop: 'expenseType',
type: 'select',
dicData: expenseTypeDic,
search: true,
minWidth: 150,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择费用类型', trigger: 'change' }],
},
{
label: '金额',
prop: 'amount',
type: 'number',
precision: 2,
append: '元',
minWidth: 120,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
],
},
{
label: '数据来源',
prop: 'dataSource',
@@ -90,18 +120,7 @@ export const option = {
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '金额',
prop: 'amount',
type: 'number',
precision: 2,
append: '元',
minWidth: 120,
rules: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
],
viewDisplay: false,
},
{
label: '备注',
@@ -111,24 +130,16 @@ export const option = {
span: 24,
minWidth: 180,
overHidden: true,
placeholder: '请输入200字以内',
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '费用日期',
@@ -194,6 +205,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -21,24 +21,28 @@ const validatePositiveInteger = (rule, value, callback) => {
};
export const option = {
title: '换胎记录(无船)',
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'top',
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
column: [
{
label: '车牌号',
@@ -47,6 +51,8 @@ export const option = {
formslot: true,
search: true,
minWidth: 130,
span: 12,
placeholder: '输入车牌号查询选择',
rules: [
{ required: true, message: '请输入车牌号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
@@ -56,6 +62,8 @@ export const option = {
label: '处理人',
prop: 'handler',
minWidth: 120,
span: 12,
placeholder: '请输入',
rules: [{ max: 20, message: '最多 20 个字符', trigger: 'blur' }],
},
{
@@ -65,12 +73,16 @@ export const option = {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 130,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择换胎时间', trigger: 'click' }],
},
{
label: '轮胎品牌',
prop: 'tireBrand',
minWidth: 140,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
@@ -79,6 +91,8 @@ export const option = {
type: 'number',
precision: 0,
minWidth: 110,
span: 12,
placeholder: '请输入',
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
},
{
@@ -88,6 +102,8 @@ export const option = {
precision: 2,
append: '元',
minWidth: 120,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -98,6 +114,8 @@ export const option = {
prop: 'replacementDescription',
minWidth: 180,
overHidden: true,
span: 24,
placeholder: '请输入',
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
@@ -108,24 +126,16 @@ export const option = {
span: 24,
minWidth: 180,
overHidden: true,
placeholder: '请输入',
rules: [{ max: 500, message: '最多 500 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '创建时间',
@@ -203,6 +213,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -17,16 +17,19 @@ export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'top',
labelPosition: 'right',
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
menuFixed: 'right',
column: [
{
@@ -46,6 +49,7 @@ export const option = {
formslot: true,
search: true,
minWidth: 140,
placeholder: '输入车牌号/船号模糊查询选择',
rules: [{ max: 30, message: '最多 30 个字符', trigger: 'blur' }],
},
{
@@ -83,19 +87,10 @@ export const option = {
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '创建时间',
@@ -173,6 +168,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -41,21 +41,26 @@ export const processStatusDic = [
export const option = {
height: 'auto',
calcHeight: 32,
dialogWidth: 1100,
labelPosition: 'top',
dialogWidth: 960,
labelPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
menuWidth: 180,
menuWidth: 220,
column: [
{
label: '车船类型',
@@ -65,6 +70,7 @@ export const option = {
dicData: vehicleTypeDic,
value: '车辆',
search: true,
span: 24,
rules: [{ required: true, message: '请选择车船类型', trigger: 'change' }],
},
{
@@ -74,6 +80,8 @@ export const option = {
formslot: true,
search: true,
minWidth: 140,
span: 12,
placeholder: '输入车牌号/船号查询选择',
rules: [
{ required: true, message: '请输入车牌号/船号', trigger: 'blur' },
{ max: 30, message: '最多 30 个字符', trigger: 'blur' },
@@ -85,6 +93,8 @@ export const option = {
formslot: true,
search: true,
minWidth: 120,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入驾驶人/船长', trigger: 'blur' },
{ max: 20, message: '最多 20 个字符', trigger: 'blur' },
@@ -96,6 +106,8 @@ export const option = {
type: 'select',
dicData: violationTypeDic,
minWidth: 120,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择类型', trigger: 'change' }],
},
{
@@ -104,6 +116,8 @@ export const option = {
minWidth: 180,
overHidden: true,
display: false,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入事项', trigger: 'blur' },
{ max: 100, message: '最多 100 个字符', trigger: 'blur' },
@@ -116,6 +130,8 @@ export const option = {
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
span: 12,
placeholder: '请选择',
rules: [{ required: true, message: '请选择时间', trigger: 'click' }],
},
{
@@ -123,6 +139,8 @@ export const option = {
prop: 'location',
minWidth: 180,
overHidden: true,
span: 12,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入地点', trigger: 'blur' },
{ max: 100, message: '最多 100 个字符', trigger: 'blur' },
@@ -135,6 +153,8 @@ export const option = {
precision: 2,
append: '元',
minWidth: 100,
span: 12,
placeholder: '请输入',
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -143,12 +163,16 @@ export const option = {
type: 'number',
precision: 0,
minWidth: 110,
span: 12,
placeholder: '请输入',
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
},
{
label: '被罚单位',
prop: 'penaltyUnit',
minWidth: 150,
span: 12,
placeholder: '请输入',
rules: [{ max: 50, message: '最多 50 个字符', trigger: 'blur' }],
},
{
@@ -160,6 +184,7 @@ export const option = {
search: true,
clearable: true,
minWidth: 120,
span: 12,
rules: [{ required: true, message: '请选择状态', trigger: 'change' }],
},
{
@@ -170,6 +195,7 @@ export const option = {
span: 24,
minWidth: 220,
overHidden: true,
placeholder: '请输入',
rules: [
{ required: true, message: '请输入过程描述', trigger: 'blur' },
{ max: 500, message: '最多 500 个字符', trigger: 'blur' },
@@ -183,24 +209,16 @@ export const option = {
span: 24,
minWidth: 220,
overHidden: true,
placeholder: '请输入',
rules: [{ max: 500, message: '最多 500 个字符', trigger: 'blur' }],
},
{
label: '附件',
prop: 'attachments',
type: 'upload',
listType: 'text',
multiple: true,
accept: '.jpg,.jpeg,.png,.pdf',
propsHttp: {
res: 'data',
url: 'link',
name: 'name',
},
action: '/blade-resource/oss/endpoint/put-file',
span: 24,
minWidth: 100,
slot: true,
formslot: true,
},
{
label: '创建时间',
@@ -278,6 +296,7 @@ export const option = {
};
export const excelOption = {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [

View File

@@ -30,12 +30,10 @@ export default {
</script>
<style scoped>
.logo-title {
font-size: 16px;
background-image: linear-gradient(120deg, #54b6d0 16%, #3f8bdb, #2c77f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 20px;
color: #fff;
font-weight: 700;
padding-left: 30px;
padding-left: 4px;
}
.logo-img {
width: 40px;

View File

@@ -20,7 +20,7 @@
:key="index"
@click="handleSelect(item)"
>
<i :class="[item[iconKey], 'avue-searchs__item-icon']"></i>
<i v-if="item[iconKey]" :class="[item[iconKey], 'avue-searchs__item-icon']"></i>
<span class="avue-searchs__item-title">{{ item[labelKey] }}</span>
<div class="avue-searchs__item-path">
{{ item[pathKey] }}

View File

@@ -6,7 +6,7 @@
@click="open(item)"
:key="item[labelKey]"
>
<i :class="item[iconKey]"></i>
<i v-if="item[iconKey]" :class="item[iconKey]"></i>
<template #title>
<span :alt="item[pathKey]">{{ getTitle(item) }}</span>
</template>
@@ -17,7 +17,7 @@
:key="item[labelKey]"
>
<template #title>
<i :class="item[iconKey]"></i>
<i v-if="item[iconKey]" :class="item[iconKey]"></i>
<span>{{ getTitle(item) }}</span>
</template>
<template v-for="(child, cindex) in item[childrenKey]" :key="child[labelKey]">
@@ -26,7 +26,7 @@
@click="open(child)"
v-if="validatenull(child[childrenKey])"
>
<i :class="child[iconKey]"></i>
<i v-if="child[iconKey]" :class="child[iconKey]"></i>
<template #title>
<span>{{ getTitle(child) }}</span>
</template>

View File

@@ -161,7 +161,7 @@ export default {
roleId: '',
},
userOption: {
labelWidth: 70,
labelWidth: 'auto',
submitBtn: true,
emptyBtn: false,
submitText: '切 换',

View File

@@ -9,6 +9,8 @@
<script>
import { mapGetters } from 'vuex'; // default color
const DEFAULT_PRIMARY_COLOR = '#409eff';
export default {
name: 'topColor',
data() {
@@ -17,7 +19,7 @@ export default {
};
},
created() {
this.themeVal = this.colorName || '#2C77F1';
this.themeVal = this.colorName || DEFAULT_PRIMARY_COLOR;
},
watch: {
themeVal(val, oldVal) {
@@ -69,6 +71,7 @@ export default {
`${this.getLightColor(e, i / 10)}`
);
}
document.documentElement.style.setProperty(`${pre}-dark-2`, `${this.getDarkColor(e, 0.2)}`);
},
},
};

View File

@@ -2,7 +2,7 @@
<span v-if="text" @click="handleLock">{{ text }}</span>
<i v-else class="icon-suoping" @click="handleLock"></i>
<el-dialog title="设置锁屏密码" v-model="box" width="30%" append-to-body>
<el-form :model="form" ref="form" label-width="80px">
<el-form :model="form" ref="form" label-width="auto">
<el-form-item
label="锁屏密码"
prop="passwd"

View File

@@ -2,7 +2,7 @@
<el-menu class="top-menu" :default-active="activeIndex" mode="horizontal" text-color="#333">
<el-menu-item index="0" @click="openHome">
<template #title>
<i :class="itemHome.source" style="padding-right: 5px"></i>
<i v-if="itemHome.source" :class="itemHome.source" style="padding-right: 5px"></i>
<span>{{ itemHome.name }}</span>
</template>
</el-menu-item>
@@ -10,7 +10,7 @@
<template v-for="(item, index) in items" :key="index">
<el-menu-item :index="item.id + ''" @click="openMenu(item)">
<template #title>
<i :class="item.source" style="padding-right: 5px"></i>
<i v-if="item.source" :class="item.source" style="padding-right: 5px"></i>
<span>{{ item.name }}</span>
</template>
</el-menu-item>

View File

@@ -8,7 +8,7 @@
@select="handleSelect"
>
<template #="{ item }">
<i :class="[item[iconKey], 'icon']"></i>
<i v-if="item[iconKey]" :class="[item[iconKey], 'icon']"></i>
<div class="name">{{ item[labelKey] }}</div>
<div class="addr">{{ item[pathKey] }}</div>
</template>

View File

@@ -171,7 +171,7 @@ RouterPlugin.install = function (option = {}) {
export const formatPath = (ele, first) => {
const propsDefault = website.menu;
const icon = ele[propsDefault.icon];
ele[propsDefault.icon] = !icon ? propsDefault.iconDefault : icon;
ele[propsDefault.icon] = icon || '';
ele.meta = {
keepAlive: ele.isOpen === 2,
};

View File

@@ -1,6 +1,10 @@
import { setStore, getStore, removeStore } from 'utils/store';
import website from '@/config/website';
const DEFAULT_PRIMARY_COLOR = '#409eff';
const LEGACY_PRIMARY_COLORS = ['#2C77F1', '#5C9DF8'];
const storedColorName = getStore({ name: 'colorName' });
const common = {
state: {
language: getStore({ name: 'language' }) || 'zh-cn',
@@ -10,7 +14,10 @@ const common = {
isSearch: false,
isRefresh: true,
isLock: getStore({ name: 'isLock' }),
colorName: getStore({ name: 'colorName' }) || '#2C77F1',
colorName:
!storedColorName || LEGACY_PRIMARY_COLORS.includes(String(storedColorName).toUpperCase())
? DEFAULT_PRIMARY_COLOR
: storedColorName,
themeName: getStore({ name: 'themeName' }) || 'theme-go',
lockPasswd: getStore({ name: 'lockPasswd' }) || '',
website: website,

View File

@@ -19,7 +19,7 @@
//图标配置
@use './iconfont.scss';
//登录样式
@use "./login.scss";
@use './login.scss';
//适配
@use './media.scss';
//滚动条样式
@@ -30,6 +30,13 @@ a {
color: #333;
}
.el-menu {
.el-menu-item span,
.el-sub-menu__title span {
font-size: 16px;
}
}
* {
outline: none;
}
@@ -40,7 +47,7 @@ a {
.avue-layout,
.login-logo,
.avue-main {
transition: all .3s;
transition: all 0.3s;
}
.avue-layout {
@@ -50,7 +57,6 @@ a {
&:not(.avue-layout--horizontal) {
.avue-sidebar {
background-image: url('/img/左侧背景放底部.png') !important;
background-repeat: no-repeat !important;
background-position: left bottom !important;
background-size: 100% auto !important;
@@ -65,7 +71,9 @@ a {
height: variables.$top_height;
display: flex;
.avue-menu, .el-menu-item, .el-sub-menu__title {
.avue-menu,
.el-menu-item,
.el-sub-menu__title {
height: variables.$top_height;
line-height: variables.$top_height;
}
@@ -76,7 +84,7 @@ a {
}
.avue-logo {
width: variables.$sidebar_width
width: variables.$sidebar_width;
}
}
}
@@ -104,7 +112,7 @@ a {
flex: 1;
box-sizing: border-box;
overflow: hidden;
background: #f0f2f5;
background: #f4f6fa;
}
#avue-view {
@@ -132,7 +140,7 @@ a {
}
.mac_bg {
background-image: url("/img/bg.jpg");
background-image: url('/img/bg.jpg');
background-color: #000;
position: fixed;
left: 0;

View File

@@ -1,3 +1,17 @@
:root {
--el-color-primary: #409eff;
--el-color-primary-light-1: #53a8ff;
--el-color-primary-light-2: #66b1ff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-4: #8cc5ff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-6: #b3d8ff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-8: #d9ecff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
}
.el-card.is-always-shadow {
box-shadow: none;
border: none !important;
@@ -22,7 +36,6 @@
z-index: 2048;
}
.el-col {
margin-bottom: 8px;
}
@@ -31,12 +44,19 @@
padding: 0 !important;
}
.el-dropdown-menu__item--divided:before, .el-menu, .el-menu--horizontal > .el-menu-item:not(.is-disabled):focus, .el-menu--horizontal > .el-menu-item:not(.is-disabled):hover, .el-menu--horizontal > .el-sub-menu .el-sub-menu__title:hover {
.el-dropdown-menu__item--divided:before,
.el-menu,
.el-menu--horizontal > .el-menu-item:not(.is-disabled):focus,
.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover,
.el-menu--horizontal > .el-sub-menu .el-sub-menu__title:hover {
background-color: transparent;
}
.el-dropdown-menu__item--divided:before, .el-menu, .el-menu--horizontal > .el-menu-item:not(.is-disabled):focus, .el-menu--horizontal > .el-menu-item:not(.is-disabled):hover, .el-menu--horizontal > .el-sub-menu .el-sub-menu__title:hover {
.el-dropdown-menu__item--divided:before,
.el-menu,
.el-menu--horizontal > .el-menu-item:not(.is-disabled):focus,
.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover,
.el-menu--horizontal > .el-sub-menu .el-sub-menu__title:hover {
background-color: transparent !important;
}
@@ -49,7 +69,8 @@
background-color: transparent;
}
.el-button.is-text:not(.is-disabled):focus, .el-button.is-text:not(.is-disabled):hover {
.el-button.is-text:not(.is-disabled):focus,
.el-button.is-text:not(.is-disabled):hover {
background-color: transparent;
}
.el-dropdown {
@@ -60,7 +81,8 @@
}
}
.avue-icon i, .avue-icon svg {
.avue-icon i,
.avue-icon svg {
line-height: 20px;
}
@@ -68,6 +90,144 @@
background-color: #fafafa;
}
.basic-container__card .avue-crud > .el-card,
.basic-container__card .avue-crud__pagination.el-card {
border: none;
background: transparent;
box-shadow: none;
}
.basic-container__card .avue-crud__search.el-card {
border: none;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.basic-container__card .avue-crud > .el-card > .el-card__body,
.basic-container__card .avue-crud__pagination.el-card > .el-card__body {
padding: 0;
}
.basic-container__card .avue-crud__search.el-card > .el-card__body {
padding: 12px 12px 4px;
}
.basic-container__card .avue-crud__header {
background-color: transparent;
}
.avue-crud__header,
.avue-crud .el-table th .cell,
.avue-crud .el-table td .cell {
white-space: nowrap;
}
.avue-crud__search .el-form-item,
.avue-crud__search .el-form-item--default.el-form-item,
.avue-crud__search .el-form-item--small.el-form-item,
.avue-crud__search .el-form-item--large.el-form-item,
.avue-crud__search .el-form-item--medium.el-form-item {
margin-bottom: 8px;
}
.avue-crud__search .el-form-item__label {
min-width: 160px;
white-space: nowrap;
}
.avue-crud__search.el-card {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.avue-crud__search.el-card > .el-card__body {
padding: 12px 12px 4px;
}
.avue-crud__search {
margin-bottom: 8px;
}
.avue-crud__search .avue-form__menu--right {
display: flex;
justify-content: flex-end;
width: 100%;
}
.el-dialog .el-form-item__label {
justify-content: flex-end;
text-align: right;
white-space: nowrap;
}
.el-dialog .el-col {
margin-bottom: 0;
}
.el-dialog .el-form-item,
.el-dialog .el-form-item--default.el-form-item,
.el-dialog .el-form-item--small.el-form-item,
.el-dialog .el-form-item--large.el-form-item,
.el-dialog .el-form-item--medium.el-form-item {
margin-bottom: 16px;
}
.el-dialog__title,
.dialog-section-title {
display: inline-flex;
align-items: center;
gap: 8px;
}
.el-dialog__title:not(:empty)::before,
.dialog-section-title::before {
content: '';
flex: none;
width: 4px;
height: 16px;
border-radius: 2px;
background-color: var(--el-color-primary);
}
.avue-crud__dialog .avue-dialog__footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
.avue-crud__dialog .avue-dialog__footer--left {
justify-content: flex-start;
}
.avue-crud__dialog .avue-dialog__footer--center {
justify-content: center;
}
.avue-crud__dialog .avue-dialog__footer > .el-button {
margin-left: 12px;
}
.avue-crud__dialog .avue-dialog__footer > .el-button:last-of-type {
order: 1;
margin-left: 0;
}
.avue-crud__dialog .avue-dialog__footer > .el-button.el-button--primary {
order: 2;
}
// 统一表格线条颜色
.el-table {
--el-table-border-color: #eff1f7;
--el-table-border: 1px solid var(--el-table-border-color);
}
.el-table,
.el-table th.el-table__cell,
.el-table td.el-table__cell {
border-color: #eff1f7;
}
// 统一 avue-crud 表头背景色
.avue-crud .el-table th.el-table__cell,
.avue-crud .el-table__header-wrapper tr th.el-table-fixed-column--left,
@@ -75,8 +235,212 @@
background-color: #fafafa;
}
// 统一 avue-crud 偶数行背景色
.avue-crud .el-table__body-wrapper tr.el-table__row:nth-child(even) td.el-table__cell,
.avue-crud .el-table__body-wrapper tr.el-table__row:nth-child(even) td.el-table-fixed-column--left,
.avue-crud
.el-table__body-wrapper
tr.el-table__row:nth-child(even)
td.el-table-fixed-column--right {
background-color: #fafafa;
}
// 统一 avue-crud 行 hover 背景色
.avue-crud .el-table__body-wrapper tr.hover-row td.el-table__cell,
.avue-crud .el-table__body-wrapper tr.hover-row td.el-table-fixed-column--left,
.avue-crud .el-table__body-wrapper tr.hover-row td.el-table-fixed-column--right {
background-color: var(--el-table-row-hover-bg-color);
}
.avue-crud__header {
margin-top: 12px;
}
// 统一分页为按钮样式并靠右展示
.avue-crud__pagination,
.empty-pagination {
display: flex;
justify-content: flex-end;
width: 100%;
box-sizing: border-box;
}
.avue-crud__pagination .el-pagination,
.empty-pagination .el-pagination,
.el-pagination {
--el-pagination-button-width: 36px;
--el-pagination-button-height: 32px;
--el-pagination-border-radius: 4px;
--el-pagination-button-bg-color: #fff;
--el-pagination-bg-color: #fff;
--el-pagination-hover-color: var(--el-color-primary);
justify-content: flex-end;
width: 100%;
gap: 6px;
padding: 18px 0 0;
color: #303133;
font-size: 14px;
font-weight: 400;
}
.el-pagination__total {
display: inline-flex !important;
align-items: center;
margin-right: 6px;
}
.el-pagination__sizes {
display: inline-flex !important;
margin: 0 6px 0 0;
align-items: center;
}
.el-pagination__sizes .el-select {
width: 112px;
}
.el-pagination__sizes .el-select__wrapper {
min-height: 32px;
border-radius: 4px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
.el-pagination.is-background .btn-prev,
.el-pagination.is-background .btn-next,
.el-pagination.is-background .el-pager li,
.el-pagination .btn-prev,
.el-pagination .btn-next,
.el-pagination .el-pager li,
.erp-pagination-button {
min-width: 36px;
height: 32px;
margin: 0 !important;
padding: 0 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
color: #303133;
line-height: 30px;
font-size: 14px;
font-weight: 400;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.el-pagination .btn-prev,
.el-pagination .btn-next,
.erp-pagination-first,
.erp-pagination-last {
min-width: 54px;
}
.el-pagination .el-pager {
gap: 6px;
margin: 0;
padding: 0;
}
.el-pagination .btn-prev .el-icon,
.el-pagination .btn-next .el-icon {
display: none;
}
.el-pagination .btn-prev::before {
content: '上一页';
}
.el-pagination .btn-next::before {
content: '下一页';
}
.el-pagination.is-background .el-pager li.is-active,
.el-pagination .el-pager li.is-active {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary);
color: #fff;
font-weight: 500;
}
.el-pagination.is-background .btn-prev:hover,
.el-pagination.is-background .btn-next:hover,
.el-pagination.is-background .el-pager li:hover,
.el-pagination .btn-prev:hover,
.el-pagination .btn-next:hover,
.el-pagination .el-pager li:hover,
.erp-pagination-button:hover {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.el-pagination.is-background .el-pager li.is-active:hover,
.el-pagination .el-pager li.is-active:hover,
.erp-pagination-jump:hover {
color: #fff;
}
.el-pagination .btn-prev:disabled,
.el-pagination .btn-prev.is-disabled,
.el-pagination .btn-next:disabled,
.el-pagination .btn-next.is-disabled,
.erp-pagination-button:disabled {
border-color: #ebeef5;
background: #fff;
color: #c0c4cc;
box-shadow: none;
cursor: not-allowed;
}
.el-pagination__jump {
margin-left: 6px;
color: #606266;
gap: 6px;
}
.el-pagination__goto {
margin-right: 0;
font-size: 0;
}
.el-pagination__goto::before {
content: '';
font-size: 14px;
}
.el-pagination__editor.el-input {
width: 48px;
}
.el-pagination__editor .el-input__wrapper {
height: 32px;
padding: 0 6px;
border-radius: 4px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
.el-pagination__editor .el-input__inner {
height: 30px;
line-height: 30px;
}
.el-pagination__classifier {
margin-left: 0;
}
.erp-pagination-button {
cursor: pointer;
}
.erp-pagination-jump {
min-width: 60px;
border-color: var(--el-color-primary);
background: var(--el-color-primary);
color: #fff;
}
@media (max-width: 768px) {
.avue-crud__pagination .el-pagination,
.empty-pagination .el-pagination,
.el-pagination {
flex-wrap: wrap;
gap: 6px;
}
}

View File

@@ -46,16 +46,16 @@
.avue-logo {
width:100%;
height:50px;
line-height: 50px;
height:60px;
line-height: 60px;
background:#fff;
color:#333;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) !important;
}
.avue-top{
height:50px;
line-height: 50px;
height:60px;
line-height: 60px;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) !important;
.top-bar__left i, .top-bar__right i{
line-height: 50px;
@@ -74,7 +74,7 @@
}
}
.avue-main{
background-color: rgb(249,250,251);
background-color: #f4f6fa;
}
.avue-tags {
margin: 5px 6px 8px 6px;

View File

@@ -33,3 +33,50 @@
//beautiful
@use './beautiful.scss';
.theme-white,
.theme-star,
.theme-vip,
.theme-d2,
.theme-bule,
.theme-iview,
.theme-go,
.theme-hey,
.theme-dark,
.theme-lte,
.theme-cool,
.theme-beautiful {
.avue-logo {
background: var(--el-color-primary) !important;
background-color: var(--el-color-primary) !important;
color: #fff;
.avue-logo_title,
.avue-logo_subtitle,
.logo-title {
color: #fff;
background-image: none;
-webkit-text-fill-color: #fff;
}
}
}
.avue-layout:not(.avue-layout--horizontal) {
.avue-sidebar {
.el-menu-item,
.el-sub-menu__title {
&.is-active {
background-color: #E6EAED !important;
&::before {
display: none !important;
}
i,
span {
color: #333 !important;
}
}
}
}
}

View File

@@ -20,8 +20,10 @@
.avue-breadcrumb {
height: 100%;
color: #fff;
i {
color: #fff;
font-size: 30px !important;
}
@@ -130,7 +132,7 @@
height: 100%;
padding-left: 50px;
box-sizing: border-box;
background: url('/img/系统首页顶部背景图.png') center center / cover no-repeat;
background: var(--el-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -141,7 +143,7 @@
.avue-logo {
height: variables.$top_height;
line-height: variables.$top_height;
background-color: #031527;
background-color: var(--el-color-primary);
font-size: 20px;
overflow: hidden;
box-sizing: border-box;
@@ -153,6 +155,7 @@
text-align: center;
font-weight: bold;
font-size: 20px;
color: #fff;
}
&_subtitle {

54
src/utils/coordinate.js Normal file
View File

@@ -0,0 +1,54 @@
export const coordinateInputPattern = /^-?\d+(\.\d{0,6})?$/;
const coordinateRanges = {
longitude: {
label: '经度',
min: -180,
max: 180,
},
latitude: {
label: '纬度',
min: -90,
max: 90,
},
};
export const normalizeCoordinateInput = value => {
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
const isNegative = rawValue.startsWith('-');
const unsignedValue = rawValue.replace(/-/g, '');
if (!unsignedValue) {
return isNegative ? '-' : '';
}
const [integerPart, ...decimalParts] = unsignedValue.split('.');
const sign = isNegative ? '-' : '';
if (!decimalParts.length) {
return `${sign}${integerPart}`;
}
return `${sign}${integerPart || '0'}.${decimalParts.join('').slice(0, 6)}`;
};
export const isCoordinateInput = value => {
if (value === undefined || value === null || value === '') {
return true;
}
return coordinateInputPattern.test(String(value));
};
export const getCoordinateValidationMessage = (value, type, required = false) => {
const range = coordinateRanges[type];
if (!range) {
return '';
}
if (value === undefined || value === null || value === '') {
return required ? `${range.label}不能为空` : '';
}
if (!isCoordinateInput(value)) {
return `${range.label}最多保留6位小数`;
}
const numberValue = Number(value);
if (numberValue < range.min || numberValue > range.max) {
return `${range.label}范围为 ${range.min}${range.max}`;
}
return '';
};

View File

@@ -0,0 +1,136 @@
const FORM_SELECTOR = '.el-dialog .el-form';
const ITEM_SELECTOR = '.el-form-item';
const COLUMN_TOLERANCE = 8;
const REQUIRED_MARK_WIDTH = 12;
let textMeasureEl;
const isVisible = element => {
if (!element) return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const getDirectLabel = item => {
return Array.from(item.children).find(child => child.classList.contains('el-form-item__label'));
};
const getColumnLeft = item => {
const column = item.closest('.el-col');
const target = column || item;
return Math.round(target.getBoundingClientRect().left);
};
const findColumnGroup = (groups, left) => {
return groups.find(group => Math.abs(group.left - left) <= COLUMN_TOLERANCE);
};
const getTextMeasureEl = () => {
if (textMeasureEl) return textMeasureEl;
textMeasureEl = document.createElement('span');
textMeasureEl.style.position = 'fixed';
textMeasureEl.style.left = '-9999px';
textMeasureEl.style.top = '-9999px';
textMeasureEl.style.visibility = 'hidden';
textMeasureEl.style.whiteSpace = 'nowrap';
textMeasureEl.style.pointerEvents = 'none';
document.body.appendChild(textMeasureEl);
return textMeasureEl;
};
const measureLabelWidth = (label, item) => {
const text = String(label.textContent || '').trim();
if (!text) return 0;
const style = window.getComputedStyle(label);
const measurer = getTextMeasureEl();
measurer.style.font = style.font;
measurer.style.letterSpacing = style.letterSpacing;
measurer.textContent = text;
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
const requiredWidth = item.classList.contains('is-required') ? REQUIRED_MARK_WIDTH : 0;
return Math.ceil(measurer.getBoundingClientRect().width + paddingLeft + paddingRight + requiredWidth);
};
const collectLabelGroups = form => {
const groups = [];
const items = Array.from(form.querySelectorAll(ITEM_SELECTOR));
items.forEach(item => {
if (!isVisible(item) || item.closest('.avue-form__menu')) return;
const label = getDirectLabel(item);
if (!label || !isVisible(label)) return;
const width = measureLabelWidth(label, item);
if (!width) return;
const left = getColumnLeft(item);
let group = findColumnGroup(groups, left);
if (!group) {
group = {
left,
labels: [],
width: 0,
};
groups.push(group);
}
group.labels.push(label);
group.width = Math.max(group.width, width);
});
return groups;
};
const alignFormLabelsByColumn = form => {
const groups = collectLabelGroups(form);
groups.forEach(group => {
group.labels.forEach(label => {
const width = `${group.width}px`;
if (label.style.width !== width) {
label.style.width = width;
}
if (label.style.flexBasis !== width) {
label.style.flexBasis = width;
}
if (label.style.justifyContent !== 'flex-end') {
label.style.justifyContent = 'flex-end';
}
if (label.style.textAlign !== 'right') {
label.style.textAlign = 'right';
}
});
});
};
const alignAllDialogFormLabels = () => {
document.querySelectorAll(FORM_SELECTOR).forEach(alignFormLabelsByColumn);
};
export const setupDialogFormLabelAlign = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
let frameId = 0;
const scheduleAlign = () => {
if (frameId) return;
frameId = window.requestAnimationFrame(() => {
frameId = 0;
alignAllDialogFormLabels();
});
};
scheduleAlign();
window.addEventListener('resize', scheduleAlign);
const observer = new MutationObserver(scheduleAlign);
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'style'],
childList: true,
subtree: true,
});
};

76
src/utils/import-excel.js Normal file
View File

@@ -0,0 +1,76 @@
import { importBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const excelPattern = /\.(xls|xlsx)$/;
const isExcelResponse = res => {
const contentType = res.headers['content-type'] || res.data.type || '';
return contentType.includes('application/vnd.ms-excel');
};
const parseBlobJson = async blob => {
const text = await blob.text();
return text ? JSON.parse(text) : {};
};
export const openImportDialog = (vm, businessName, onSuccess) => {
const column = vm.findColumn(vm.excelOption.column, 'excelFile');
column.httpRequest = (option, uploadColumn) =>
handleImportExcel(vm, option, uploadColumn, businessName, onSuccess);
vm.excelBox = true;
};
export const handleImportExcel = async (vm, option, column, businessName, onSuccess) => {
const file = option.file;
const fileName = file && file.name ? file.name.toLowerCase() : '';
if (!excelPattern.test(fileName)) {
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
if (typeof option.onError === 'function') {
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
}
return;
}
NProgress.start();
try {
const res = await importBlob(column.action, file);
if (isExcelResponse(res)) {
downloadXls(
res.data,
`${businessName}导入失败明细${vm.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
vm.$message.warning('部分数据导入失败,已下载失败明细');
} else {
const result = await parseBlobJson(res.data);
if (result.code !== 200) {
vm.$message.error(result.msg || '导入失败');
if (typeof option.onError === 'function') {
option.onError(new Error(result.msg || '导入失败'));
}
return;
}
vm.$message.success(result.msg || '导入数据成功');
}
vm.excelBox = false;
vm.excelForm = {};
if (typeof onSuccess === 'function') {
onSuccess();
} else if (typeof vm.onLoad === 'function') {
vm.onLoad(vm.page, vm.query);
} else if (typeof vm.initTree === 'function') {
vm.initTree();
}
if (typeof option.onSuccess === 'function') {
option.onSuccess(res);
}
} catch (error) {
vm.$message.error(error.message || '导入失败');
if (typeof option.onError === 'function') {
option.onError(error);
}
} finally {
NProgress.done();
}
};

167
src/utils/pagination.js Normal file
View File

@@ -0,0 +1,167 @@
const FIRST_BUTTON_CLASS = 'erp-pagination-first';
const LAST_BUTTON_CLASS = 'erp-pagination-last';
const JUMP_BUTTON_CLASS = 'erp-pagination-jump';
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_PAGE_SIZES = [10, 20, 50, 100];
const PAGE_KEY_PATTERN = /(^page$|Page$|^page[A-Z])/;
const toNumber = value => {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
};
const getNativeInputValueSetter = input => {
const prototype = Object.getPrototypeOf(input);
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
return descriptor && descriptor.set;
};
const setInputValue = (input, value) => {
const setter = getNativeInputValueSetter(input);
if (setter) {
setter.call(input, value);
} else {
input.value = value;
}
input.dispatchEvent(new Event('input', { bubbles: true }));
};
const getPaginationState = pagination => {
const active = pagination.querySelector('.el-pager li.is-active');
const jumpInput = pagination.querySelector('.el-pagination__editor input');
const currentPage = toNumber(
active && active.textContent ? active.textContent.trim() : jumpInput?.value
);
const pageCount = toNumber(jumpInput?.getAttribute('max'));
return {
currentPage: currentPage || 1,
pageCount: pageCount || 1,
jumpInput,
};
};
const changePage = (pagination, page) => {
const { currentPage, pageCount, jumpInput } = getPaginationState(pagination);
const targetPage = Math.min(Math.max(toNumber(page), 1), pageCount);
if (!targetPage || targetPage === currentPage) return;
const targetPager = Array.from(pagination.querySelectorAll('.el-pager li.number')).find(
item => toNumber(item.textContent && item.textContent.trim()) === targetPage
);
if (targetPager) {
targetPager.click();
return;
}
if (jumpInput) {
setInputValue(jumpInput, targetPage);
jumpInput.dispatchEvent(new Event('change', { bubbles: true }));
jumpInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
}
};
const createPaginationButton = (className, text, onClick) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `erp-pagination-button ${className}`;
button.textContent = text;
button.addEventListener('click', onClick);
return button;
};
const setButtonDisabled = (button, disabled) => {
if (!button || button.disabled === disabled) return;
button.disabled = disabled;
};
const isPageConfig = value =>
value &&
typeof value === 'object' &&
('pageSize' in value || 'currentPage' in value || 'total' in value);
const normalizePageConfig = page => {
if (!isPageConfig(page)) return;
page.pageSize = DEFAULT_PAGE_SIZES.includes(toNumber(page.pageSize))
? toNumber(page.pageSize)
: DEFAULT_PAGE_SIZE;
page.pageSizes = DEFAULT_PAGE_SIZES;
};
const normalizeComponentPageConfigs = pagination => {
let component = pagination.__vueParentComponent;
while (component) {
[component.data, component.setupState].forEach(state => {
if (!state) return;
Object.keys(state)
.filter(key => PAGE_KEY_PATTERN.test(key))
.forEach(key => normalizePageConfig(state[key]));
});
component = component.parent;
}
};
const updatePaginationButtons = pagination => {
const { currentPage, pageCount, jumpInput } = getPaginationState(pagination);
const firstButton = pagination.querySelector(`.${FIRST_BUTTON_CLASS}`);
const lastButton = pagination.querySelector(`.${LAST_BUTTON_CLASS}`);
const jumpButton = pagination.querySelector(`.${JUMP_BUTTON_CLASS}`);
setButtonDisabled(firstButton, currentPage <= 1);
setButtonDisabled(lastButton, currentPage >= pageCount);
setButtonDisabled(jumpButton, !jumpInput || jumpInput.disabled);
};
const enhancePagination = pagination => {
const prevButton = pagination.querySelector('.btn-prev');
const nextButton = pagination.querySelector('.btn-next');
const jumpWrapper = pagination.querySelector('.el-pagination__jump');
normalizeComponentPageConfigs(pagination);
if (prevButton && !pagination.querySelector(`.${FIRST_BUTTON_CLASS}`)) {
const firstButton = createPaginationButton(FIRST_BUTTON_CLASS, '首页', () => {
changePage(pagination, 1);
});
prevButton.before(firstButton);
}
if (nextButton && !pagination.querySelector(`.${LAST_BUTTON_CLASS}`)) {
const lastButton = createPaginationButton(LAST_BUTTON_CLASS, '尾页', () => {
const { pageCount } = getPaginationState(pagination);
changePage(pagination, pageCount);
});
nextButton.after(lastButton);
}
if (jumpWrapper && !pagination.querySelector(`.${JUMP_BUTTON_CLASS}`)) {
const jumpButton = createPaginationButton(JUMP_BUTTON_CLASS, '跳转', () => {
const { jumpInput } = getPaginationState(pagination);
if (!jumpInput) return;
changePage(pagination, jumpInput.value);
});
jumpWrapper.after(jumpButton);
}
updatePaginationButtons(pagination);
};
const enhanceAllPaginations = () => {
document.querySelectorAll('.el-pagination').forEach(enhancePagination);
};
export const setupPaginationEnhancer = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
const scheduleEnhance = () => {
window.requestAnimationFrame(enhanceAllPaginations);
};
scheduleEnhance();
const observer = new MutationObserver(scheduleEnhance);
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'disabled'],
childList: true,
subtree: true,
});
};

View File

@@ -72,7 +72,7 @@
v-for="(item, index) in apiPathList"
:key="index"
:gutter="8"
style="margin-bottom: 8px"
style="margin-bottom: 16px"
>
<el-col :span="22">
<el-input
@@ -106,7 +106,7 @@
v-for="(item, index) in extParamsList"
:key="index"
:gutter="8"
style="margin-bottom: 8px"
style="margin-bottom: 16px"
>
<el-col :span="8">
<el-input v-model="item.key" placeholder="参数名" :disabled="type === 'view'" />
@@ -330,7 +330,10 @@ export default {
menuWidth: 350,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
@@ -493,7 +496,10 @@ export default {
tip: false,
header: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -157,13 +157,16 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 120,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,
@@ -263,7 +266,10 @@ export default {
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,
@@ -324,12 +330,15 @@ export default {
optionScope: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
labelWidth: 'auto',
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,

View File

@@ -158,13 +158,16 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 120,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,
@@ -300,7 +303,10 @@ export default {
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,
@@ -363,12 +369,15 @@ export default {
optionScope: {
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
labelWidth: 120,
labelWidth: 'auto',
menuWidth: 250,
dialogWidth: 900,
dialogClickModal: false,

View File

@@ -83,7 +83,7 @@
v-for="(item, index) in limitPatternList"
:key="index"
:gutter="8"
style="margin-bottom: 8px"
style="margin-bottom: 16px"
>
<el-col :span="22">
<el-input
@@ -208,9 +208,12 @@ export default {
dialogWidth: 700,
tip: false,
searchShow: true,
searchMenuSpan: 6,
labelWidth: 110,
searchLabelWidth: 90,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
labelWidth: 'auto',
searchLabelWidth: 160,
border: true,
index: true,
viewBtn: true,
@@ -360,7 +363,10 @@ export default {
tip: false,
header: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -242,14 +242,17 @@ export default {
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
labelWidth: 120,
searchLabelWidth: 120,
labelWidth: 'auto',
searchLabelWidth: 160,
menuWidth: 350,
dialogWidth: 600,
dialogClickModal: false,

View File

@@ -79,15 +79,45 @@
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
<template #districtCode-form>
<el-select
v-model="form.districtCode"
clearable
filterable
:loading="districtLoading"
:disabled="!form.cityCode"
:placeholder="form.cityCode ? '请选择区县' : '请先选择城市'"
style="width: 100%"
@change="handleDistrictChange"
>
<el-option
v-for="item in districtOptions"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<template #longitude-form>
<el-input
v-model="form.longitude"
clearable
placeholder="请输入经度"
@input="handleCoordinateInput('longitude', $event)"
/>
</template>
<template #latitude-form>
<el-input
v-model="form.latitude"
clearable
placeholder="请输入纬度"
@input="handleCoordinateInput('latitude', $event)"
/>
</template>
</avue-crud>
<el-dialog title="空港机场主数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form
:option="excelOption"
v-model="excelForm"
:upload-before="uploadBefore"
:upload-after="uploadAfter"
>
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
@@ -104,8 +134,10 @@ import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -128,19 +160,17 @@ export default {
}
};
const validateLongitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < -180 || Number(value) > 180) {
callback(new Error('经度范围为 -180 到 180'));
const message = getCoordinateValidationMessage(value, 'longitude');
if (message) {
callback(new Error(message));
} else {
callback();
}
};
const validateLatitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < -90 || Number(value) > 90) {
callback(new Error('纬度范围为 -90 到 90'));
const message = getCoordinateValidationMessage(value, 'latitude');
if (message) {
callback(new Error(message));
} else {
callback();
}
@@ -154,6 +184,9 @@ export default {
excelForm: {},
provinceOptions: [],
cityOptions: [],
districtOptions: [],
districtLoading: false,
regionInitializing: false,
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
@@ -165,11 +198,15 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'right',
tip: false,
searchShow: true,
labelWidth: 120,
searchLabelWidth: 120,
searchMenuSpan: 6,
labelWidth: 'auto',
searchLabelWidth: 160,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
@@ -185,8 +222,8 @@ export default {
label: '编码',
prop: 'code',
search: true,
addDisabled: true,
editDisabled: true,
addDisplay: false,
editDisplay: false,
placeholder: '系统自动生成',
searchPlaceholder: '请输入编码',
},
@@ -271,14 +308,35 @@ export default {
editDisplay: false,
},
{
label: '行政区划',
prop: 'regionName',
search: true,
label: '所属区县',
prop: 'districtCode',
type: 'select',
formslot: true,
hide: true,
props: {
label: 'title',
value: 'id',
},
dicData: [],
filterable: true,
rules: [{ required: true, message: '请选择区县', trigger: 'change' }],
change: ({ value }) => this.handleDistrictChange(value),
},
{
label: '所属区县',
prop: 'districtName',
minWidth: 140,
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划编号',
prop: 'regionCode',
minWidth: 160,
overHidden: false,
maxlength: 128,
showWordLimit: true,
rules: [{ max: 128, message: '行政区划不能超过128字', trigger: 'blur' }],
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '详细地址',
@@ -295,20 +353,24 @@ export default {
{
label: '经度',
prop: 'longitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 130,
overHidden: false,
rules: [{ validator: validateLongitude, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入经度', trigger: 'blur' },
{ validator: validateLongitude, trigger: 'blur' },
],
},
{
label: '纬度',
prop: 'latitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 130,
overHidden: false,
rules: [{ validator: validateLatitude, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入纬度', trigger: 'blur' },
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '数据来源',
@@ -317,6 +379,8 @@ export default {
search: true,
minWidth: 120,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '初始导入', value: '初始导入' },
@@ -333,6 +397,8 @@ export default {
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
@@ -383,6 +449,7 @@ export default {
],
},
excelOption: {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [
@@ -419,7 +486,9 @@ export default {
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority) ? authority.includes('admin') : String(authority || '').includes('admin');
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
ids() {
let ids = [];
@@ -441,7 +510,7 @@ export default {
},
loadProvinceOptions() {
getLazyTree(DEFAULT_COUNTRY_CODE).then(res => {
this.provinceOptions = res.data.data;
this.provinceOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('provinceCode').dicData = this.provinceOptions;
});
},
@@ -452,10 +521,60 @@ export default {
return Promise.resolve();
}
return getLazyTree(provinceCode).then(res => {
this.cityOptions = res.data.data;
this.cityOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('cityCode').dicData = this.cityOptions;
});
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.districtOptions = [];
this.findColumn('districtCode').dicData = [];
return Promise.resolve();
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('districtCode').dicData = this.districtOptions;
})
.finally(() => {
this.districtLoading = false;
});
},
normalizeRegionOption(region) {
return {
...region,
id: region.id === undefined || region.id === null ? region.id : String(region.id),
};
},
normalizeRegionForm(row) {
return {
...row,
provinceCode:
row.provinceCode === undefined || row.provinceCode === null
? row.provinceCode
: String(row.provinceCode),
cityCode:
row.cityCode === undefined || row.cityCode === null ? row.cityCode : String(row.cityCode),
districtCode:
row.districtCode === undefined || row.districtCode === null
? row.districtCode
: String(row.districtCode),
};
},
ensureDistrictOption(row) {
if (!row.districtCode || !row.districtName) return;
const exists = this.districtOptions.some(item => item.id === row.districtCode);
if (exists) return;
this.districtOptions = [
...this.districtOptions,
{
id: row.districtCode,
title: row.districtName,
},
];
this.findColumn('districtCode').dicData = this.districtOptions;
},
handleIataChange(value) {
this.form.iataCode = String(value || '')
.toUpperCase()
@@ -472,22 +591,67 @@ export default {
handleProvinceChange(value) {
const province = this.provinceOptions.find(item => item.id === value);
this.form.provinceName = province ? province.title : '';
if (this.regionInitializing) {
this.loadCityOptions(value);
return;
}
this.form.cityCode = undefined;
this.form.cityName = '';
this.form.districtCode = undefined;
this.form.districtName = '';
this.form.regionCode = '';
this.form.regionName = '';
this.loadCityOptions(value);
this.loadDistrictOptions();
},
handleCityChange(value) {
const city = this.cityOptions.find(item => item.id === value);
this.form.cityName = city ? city.title : '';
if (this.regionInitializing) {
this.loadDistrictOptions(value);
return;
}
this.form.districtCode = undefined;
this.form.districtName = '';
this.form.regionCode = '';
this.form.regionName = '';
this.loadDistrictOptions(value);
},
handleDistrictChange(value) {
const district = this.districtOptions.find(item => item.id === value);
this.form.districtName = district ? district.title : '';
this.form.regionCode = value || '';
this.form.regionName = district ? district.title : '';
},
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
normalizeRow(row) {
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim();
row.regionName = String(row.regionName || '').trim();
row.districtCode = String(row.districtCode || '').trim();
row.districtName = String(row.districtName || '').trim();
row.detailAddress = String(row.detailAddress || '').trim();
row.longitude = normalizeCoordinateInput(row.longitude);
row.latitude = normalizeCoordinateInput(row.latitude);
if (!row.dataSource) row.dataSource = '手动录入';
if (!row.status) row.status = 1;
return row;
},
validateCoordinateFields(row) {
const longitudeMessage = getCoordinateValidationMessage(row.longitude, 'longitude', true);
if (longitudeMessage) {
this.$message.warning(longitudeMessage);
return false;
}
const latitudeMessage = getCoordinateValidationMessage(row.latitude, 'latitude', true);
if (latitudeMessage) {
this.$message.warning(latitudeMessage);
return false;
}
return true;
},
validateUnique(row) {
const rowId = String(row.id || '');
const hasDuplicate = (params, prop) => {
@@ -519,6 +683,10 @@ export default {
},
rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateCoordinateFields(submitRow)) {
loading();
return;
}
this.validateUnique(submitRow)
.then(() => submit(submitRow))
.then(() => {
@@ -530,6 +698,10 @@ export default {
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateCoordinateFields(submitRow)) {
loading();
return;
}
this.validateUnique(submitRow)
.then(() => submit(submitRow))
.then(() => {
@@ -584,16 +756,30 @@ export default {
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
this.regionInitializing = true;
getDetail(this.form.id).then(res => {
this.form = res.data.data;
this.loadCityOptions(this.form.provinceCode).then(() => done());
const detail = this.normalizeRegionForm(res.data.data || {});
this.loadCityOptions(detail.provinceCode)
.then(() => this.loadDistrictOptions(detail.cityCode))
.then(() => {
this.ensureDistrictOption(detail);
this.form = detail;
done();
this.$nextTick(() => {
window.setTimeout(() => {
this.regionInitializing = false;
}, 0);
});
});
});
return;
}
this.form.code = '';
this.form.dataSource = '手动录入';
this.form.status = 1;
this.loadCityOptions(this.form.provinceCode).then(() => done());
this.loadCityOptions(this.form.provinceCode)
.then(() => this.loadDistrictOptions(this.form.cityCode))
.then(() => done());
},
searchReset() {
this.query = {};
@@ -635,23 +821,7 @@ export default {
});
},
handleImport() {
this.excelBox = true;
},
uploadBefore(file, done) {
const fileName = file && file.name ? file.name.toLowerCase() : '';
if (!/\.(xls|xlsx)$/.test(fileName)) {
this.$message.error('请上传 .xls,.xlsx 标准格式文件');
return false;
}
if (typeof done === 'function') {
done();
}
return true;
},
uploadAfter(res, done) {
this.excelBox = false;
this.onLoad(this.page);
done();
openImportDialog(this, '空港机场主数据');
},
handleExport() {
this.$confirm('是否导出空港机场主数据?', '提示', {

View File

@@ -0,0 +1,705 @@
<template>
<basic-container class="cargo-type-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="permission.cargo_type_import"
@click="handleImport"
>批量导入
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="permission.cargo_type_template"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="permission.cargo_type_export"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="permission.cargo_type_delete"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #parentCargoName="{ row }">
{{ row.typeLevel === 1 ? '/' : row.parentCargoName || '/' }}
</template>
<template #parentCargoCode="{ row }">
{{ row.typeLevel === 1 ? '/' : row.parentCargoCode || '/' }}
</template>
<template #createUserName="{ row }">
{{ row.createUserName || row.createUser || '' }}
</template>
<template #parentId-form>
<el-select
v-model="form.parentId"
filterable
remote
clearable
:remote-method="loadParentOptions"
:loading="parentLoading"
:disabled="form.typeLevel !== 2"
placeholder="请输入名称或编码搜索"
style="width: 100%"
@change="handleParentChange"
>
<el-option
v-for="item in parentOptions"
:key="item.id"
:label="`${item.cargoName}${item.cargoCode}`"
:value="item.id"
/>
</el-select>
</template>
<template #menu="{ row, index }">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="permission.cargo_type_view"
@click="$refs.crud.rowView(row, index)"
>
查看
</el-button>
<el-button
type="primary"
text
icon="el-icon-edit"
v-if="permission.cargo_type_edit"
@click="$refs.crud.rowEdit(row, index)"
>
编辑
</el-button>
<el-button
type="primary"
text
icon="el-icon-delete"
v-if="permission.cargo_type_delete"
@click="rowDel(row)"
>
删除
</el-button>
</template>
</avue-crud>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog title="货物类型导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
</basic-container>
</template>
<script>
import {
getDetail,
getList,
getNextCode,
getParentOptions,
remove,
submit,
} from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const typeLevelDic = [
{ label: '一级货物类型', value: 1 },
{ label: '二级货物类型', value: 2 },
];
export default {
data() {
const validateCargoName = (rule, value, callback) => {
const cargoName = String(value || '').trim();
if (!cargoName) {
callback(new Error('请输入货物类型名称'));
} else if (cargoName.length > 50) {
callback(new Error('货物类型名称不能超过50字符'));
} else {
callback();
}
};
const validateCargoCode = (rule, value, callback) => {
const cargoCode = String(value || '').trim();
const pattern = this.form.typeLevel === 1 ? /^\d{2}$/ : /^\d{4}$/;
if (!pattern.test(cargoCode)) {
callback(new Error('货物类型编码格式不正确'));
} else if (
this.form.typeLevel === 2 &&
this.form.parentCargoCode &&
!cargoCode.startsWith(this.form.parentCargoCode)
) {
callback(new Error('二级编码前2位必须与上级货物类型编码一致'));
} else {
callback();
}
};
const validateRemark = (rule, value, callback) => {
if (String(value || '').length > 200) {
callback(new Error('备注不能超过200字'));
} else {
callback();
}
};
return {
form: {},
query: {},
loading: true,
data: [],
excelBox: false,
excelForm: {},
parentOptions: [],
parentLoading: false,
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 760,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
viewBtn: false,
delBtn: false,
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 220,
column: [
{
label: '类型',
prop: 'typeLevel',
type: 'radio',
dataType: 'number',
dicData: typeLevelDic,
search: true,
value: 1,
span: 24,
minWidth: 120,
rules: [{ required: true, message: '请选择货物类型级别', trigger: 'change' }],
change: ({ value }) => this.handleTypeLevelChange(value),
},
{
label: '上级货物类型',
prop: 'parentId',
formslot: true,
hide: true,
display: false,
span: 24,
rules: [{ required: true, message: '请选择上级货物类型', trigger: 'change' }],
},
{
label: '上级货物类型',
prop: 'parentCargoName',
slot: true,
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '上级货物类型编码',
prop: 'parentCargoCode',
slot: true,
readonly: true,
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
rules: [{ required: true, message: '请选择上级货物类型', trigger: 'change' }],
},
{
label: '货物类型',
prop: 'cargoName',
minWidth: 150,
maxlength: 50,
showWordLimit: true,
rules: [{ validator: validateCargoName, trigger: 'blur' }],
},
{
label: '货物类型编码',
prop: 'cargoCode',
minWidth: 140,
search: true,
maxlength: 4,
rules: [{ validator: validateCargoCode, trigger: 'blur' }],
change: ({ value }) => this.handleCargoCodeChange(value),
},
{
label: '创建人',
prop: 'createUserName',
slot: true,
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
minWidth: 180,
maxlength: 200,
showWordLimit: true,
overHidden: true,
rules: [{ validator: validateRemark, trigger: 'blur' }],
},
{
label: '更新时间',
prop: 'updateTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
],
},
excelOption: {
submitBtn: false,
emptyBtn: false,
column: [
{
label: '模板上传',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
headers: {
'Blade-Auth': `bearer ${getToken()}`,
},
tip: '请上传 .xls,.xlsx 标准格式文件',
action: '/blade-system/cargo-type/import-cargo-type',
},
{
label: '模板下载',
prop: 'excelTemplate',
formslot: true,
span: 24,
},
],
},
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.cargo_type_add, false),
};
},
ids() {
return this.selectionList.map(ele => ele.id).join(',');
},
},
methods: {
findCargoColumn(prop) {
return this.option.column.find(item => item.prop === prop) || {};
},
syncColumnDisplay() {
const isChild = this.form.typeLevel === 2;
this.findCargoColumn('parentId').display = isChild;
this.findCargoColumn('parentCargoCode').display = isChild;
this.findCargoColumn('parentCargoCode').addDisplay = isChild;
this.findCargoColumn('parentCargoCode').editDisplay = isChild;
},
handleTypeLevelChange(value) {
this.form.typeLevel = value;
if (value === 1) {
this.form.parentId = undefined;
this.form.parentCargoName = '';
this.form.parentCargoCode = '';
this.form.cargoCode = String(this.form.cargoCode || '')
.replace(/\D/g, '')
.slice(0, 2);
} else {
this.form.cargoCode = String(this.form.cargoCode || '')
.replace(/\D/g, '')
.slice(0, 4);
}
this.syncColumnDisplay();
},
handleCargoCodeChange(value) {
const maxLength = this.form.typeLevel === 1 ? 2 : 4;
this.form.cargoCode = String(value || '')
.replace(/\D/g, '')
.slice(0, maxLength);
},
loadParentOptions(keyword = '') {
this.parentLoading = true;
return getParentOptions(keyword)
.then(res => {
this.parentOptions = res.data.data || [];
})
.finally(() => {
this.parentLoading = false;
});
},
ensureParentOption(row) {
if (!row.parentId || !row.parentCargoName) return;
const exists = this.parentOptions.some(item => String(item.id) === String(row.parentId));
if (exists) return;
this.parentOptions = [
...this.parentOptions,
{
id: row.parentId,
cargoName: row.parentCargoName,
cargoCode: row.parentCargoCode,
},
];
},
handleParentChange(value) {
const parent = this.parentOptions.find(item => String(item.id) === String(value));
this.form.parentCargoName = parent ? parent.cargoName : '';
this.form.parentCargoCode = parent ? parent.cargoCode : '';
if (!parent) {
this.form.cargoCode = '';
return;
}
getNextCode(parent.cargoCode).then(res => {
this.form.cargoCode = res.data.data;
});
},
normalizeRow(row) {
row.typeLevel = Number(row.typeLevel || 1);
row.cargoName = String(row.cargoName || '').trim();
row.cargoCode = String(row.cargoCode || '').trim();
row.remark = String(row.remark || '').trim();
if (row.typeLevel === 1) {
row.parentId = undefined;
row.parentCargoName = '';
row.parentCargoCode = '';
}
return row;
},
validateRow(row) {
if (![1, 2].includes(row.typeLevel)) {
this.$message.warning('请选择货物类型级别');
return false;
}
if (row.typeLevel === 2 && !row.parentId) {
this.$message.warning('请选择上级货物类型');
return false;
}
if (!row.cargoName) {
this.$message.warning('请输入货物类型名称');
return false;
}
if (row.cargoName.length > 50) {
this.$message.warning('货物类型名称不能超过50字符');
return false;
}
const pattern = row.typeLevel === 1 ? /^\d{2}$/ : /^\d{4}$/;
if (!pattern.test(row.cargoCode)) {
this.$message.warning('货物类型编码格式不正确');
return false;
}
if (row.typeLevel === 2 && !row.cargoCode.startsWith(row.parentCargoCode)) {
this.$message.warning('二级编码前2位必须与上级货物类型编码一致');
return false;
}
if (row.remark.length > 200) {
this.$message.warning('备注不能超过200字');
return false;
}
return true;
},
rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateRow(submitRow)) {
loading();
return;
}
submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateRow(submitRow)) {
loading();
return;
}
submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定要删除该货物类型吗?删除后不可恢复。', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(() => {
this.afterDelete(1);
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定删除所选数据,删除后不可恢复?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(this.ids))
.then(() => {
this.afterDelete(this.selectionList.length);
this.$message({ type: 'success', message: '操作成功!' });
this.$refs.crud.toggleSelection();
});
},
afterDelete(deleteCount) {
if (this.page.currentPage > 1 && this.data.length <= deleteCount) {
this.page.currentPage -= 1;
}
this.onLoad(this.page, this.query);
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
this.ensureParentOption(this.form);
this.syncColumnDisplay();
done();
});
return;
}
this.form = {
typeLevel: 1,
cargoName: '',
cargoCode: '',
parentId: undefined,
parentCargoName: '',
parentCargoCode: '',
remark: '',
};
this.syncColumnDisplay();
done();
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query })
.then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records || [];
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
handleImport() {
openImportDialog(this, '货物类型');
},
buildExportParams() {
const params = { ...this.query };
if (this.ids) {
params.ids = this.ids;
}
return params;
},
handleExport() {
this.$confirm('是否导出货物类型数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob('/blade-system/cargo-type/export-cargo-type', this.buildExportParams())
.then(res => {
downloadXls(res.data, `货物类型${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
handleTemplate() {
NProgress.start();
exportBlob('/blade-system/cargo-type/export-template', {})
.then(res => {
downloadXls(res.data, '货物类型导入模板.xlsx');
})
.finally(() => {
NProgress.done();
});
},
},
};
</script>
<style lang="scss" scoped>
.cargo-type-page {
:deep(.avue-crud__header) {
margin-top: 12px;
background: transparent;
}
:deep(.el-card) {
box-shadow: none;
}
:deep(.avue-crud__search) {
padding: 12px 12px 4px;
margin-bottom: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
:deep(.avue-crud__search .el-form-item__label) {
min-width: 160px;
white-space: nowrap;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
--el-table-row-hover-bg-color: #f5f7fa;
}
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
:deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
background: #fafafa;
}
}
</style>

View File

@@ -133,6 +133,26 @@
</el-button>
</div>
</template>
<template #longitude-form>
<el-input
v-model="form.longitude"
clearable
placeholder="请输入经度"
:disabled="dialogReadonly || form.addressType !== '常规地址'"
@input="handleCoordinateInput('longitude', $event)"
/>
</template>
<template #latitude-form>
<el-input
v-model="form.latitude"
clearable
placeholder="请输入纬度"
:disabled="dialogReadonly || form.addressType !== '常规地址'"
@input="handleCoordinateInput('latitude', $event)"
/>
</template>
</avue-crud>
<el-dialog
@@ -179,6 +199,7 @@ import { createOption } from '@/option/base/common-address';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import { isCoordinateInput, normalizeCoordinateInput } from '@/utils/coordinate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -190,6 +211,8 @@ export default {
const validateLongitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('经度不能为空'));
} else if (!isCoordinateInput(value)) {
callback(new Error('经度最多保留6位小数'));
} else if (Number(value) < -180 || Number(value) > 180) {
callback(new Error('经度范围为 -180 到 180'));
} else {
@@ -199,6 +222,8 @@ export default {
const validateLatitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('纬度不能为空'));
} else if (!isCoordinateInput(value)) {
callback(new Error('纬度最多保留6位小数'));
} else if (Number(value) < -90 || Number(value) > 90) {
callback(new Error('纬度范围为 -90 到 90'));
} else {
@@ -319,6 +344,9 @@ export default {
this.form.longitude = source.longitude;
this.form.latitude = source.latitude;
},
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
clearTypeRelatedFields(siteCode = '') {
this.form.sourceId = undefined;
this.form.siteCode = siteCode;
@@ -356,6 +384,8 @@ export default {
detailAddress: String(row.detailAddress || '').trim(),
regionCode: String(row.regionCode || '').trim(),
regionName: String(row.regionName || '').trim(),
longitude: normalizeCoordinateInput(row.longitude),
latitude: normalizeCoordinateInput(row.latitude),
contactName: String(row.contactName || '').trim(),
contactPhone: String(row.contactPhone || '').trim(),
remark: String(row.remark || '').trim(),
@@ -400,6 +430,10 @@ export default {
return false;
}
if (row.longitude !== undefined && row.longitude !== null && row.longitude !== '') {
if (!isCoordinateInput(row.longitude)) {
this.$message.warning('经度最多保留6位小数');
return false;
}
if (Number(row.longitude) < -180 || Number(row.longitude) > 180) {
this.$message.warning('经度范围为 -180 到 180');
return false;
@@ -407,6 +441,10 @@ export default {
row.longitude = Number(row.longitude).toFixed(6);
}
if (row.latitude !== undefined && row.latitude !== null && row.latitude !== '') {
if (!isCoordinateInput(row.latitude)) {
this.$message.warning('纬度最多保留6位小数');
return false;
}
if (Number(row.latitude) < -90 || Number(row.latitude) > 90) {
this.$message.warning('纬度范围为 -90 到 90');
return false;

View File

@@ -90,7 +90,7 @@
</avue-crud>
<el-dialog title="币种汇率导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm" :upload-after="uploadAfter">
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
@@ -106,6 +106,8 @@ import { changeStatus, getDetail, getList, submit } from '@/api/base/currency';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -131,7 +133,7 @@ export default {
};
const validateExchangeRate = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入汇率'));
callback();
} else if (Number(value) <= 0) {
callback(new Error('汇率必须大于0'));
} else if (!/^\d+(\.\d{1,6})?$/.test(String(value))) {
@@ -157,10 +159,13 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 860,
labelPosition: 'top',
labelPosition: 'right',
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
@@ -195,7 +200,6 @@ export default {
label: '汇率',
prop: 'exchangeRate',
type: 'number',
search: true,
minWidth: 120,
precision: 6,
rules: [{ validator: validateExchangeRate, trigger: 'blur' }],
@@ -216,7 +220,6 @@ export default {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
searchRange: true,
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -239,7 +242,6 @@ export default {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
searchRange: true,
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -278,6 +280,14 @@ export default {
showWordLimit: true,
rules: [{ max: 200, message: '最多 200 个字符', trigger: 'blur' }],
},
{
label: '更新人',
prop: 'updateUserName',
formatter: formatUpdateUserName,
addDisplay: false,
editDisplay: false,
minWidth: 120,
},
{
label: '更新时间',
prop: 'updateTime',
@@ -296,7 +306,6 @@ export default {
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
searchRange: true,
search: true,
hide: true,
addDisplay: false,
editDisplay: false,
@@ -344,7 +353,10 @@ export default {
},
methods: {
handleCodeChange(value) {
this.form.code = String(value || '').toUpperCase().replace(/[^A-Z]/g, '').slice(0, 3);
this.form.code = String(value || '')
.toUpperCase()
.replace(/[^A-Z]/g, '')
.slice(0, 3);
const matched = this.data.find(item => item.code === this.form.code);
if (matched) {
this.form.name = matched.name;
@@ -375,6 +387,7 @@ export default {
normalizeRow(row) {
const values = { ...row };
values.code = String(values.code || '').toUpperCase();
values.exchangeRate = values.exchangeRate === '' ? null : values.exchangeRate;
values.dataSource = values.dataSource || '手工录入';
values.status = values.status || 1;
return values;
@@ -384,11 +397,21 @@ export default {
this.$message.warning('备注不能超过 200 字');
return false;
}
if (Number(row.exchangeRate) <= 0) {
if (
row.exchangeRate !== undefined &&
row.exchangeRate !== null &&
row.exchangeRate !== '' &&
Number(row.exchangeRate) <= 0
) {
this.$message.warning('汇率必须大于0');
return false;
}
if (!/^\d+(\.\d{1,6})?$/.test(String(row.exchangeRate))) {
if (
row.exchangeRate !== undefined &&
row.exchangeRate !== null &&
row.exchangeRate !== '' &&
!/^\d+(\.\d{1,6})?$/.test(String(row.exchangeRate))
) {
this.$message.warning('汇率最多保留6位小数');
return false;
}
@@ -519,12 +542,7 @@ export default {
});
},
handleImport() {
this.excelBox = true;
},
uploadAfter(res, done) {
this.excelBox = false;
this.onLoad(this.page);
done();
openImportDialog(this, '币种汇率');
},
handleExport() {
this.$confirm('是否导出币种汇率数据?', '提示', {
@@ -550,7 +568,9 @@ export default {
};
},
handleTemplate() {
exportBlob(`/blade-system/currency/export-template?${this.website.tokenHeader}=${getToken()}`).then(res => {
exportBlob(
`/blade-system/currency/export-template?${this.website.tokenHeader}=${getToken()}`
).then(res => {
downloadXls(res.data, '币种汇率模板.xlsx');
});
},

View File

@@ -93,7 +93,10 @@ export default {
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',

View File

@@ -80,9 +80,13 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 680,
labelPosition: 'right',
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
@@ -94,15 +98,29 @@ export default {
menuWidth: 160,
column: [
{
label: '中文名称',
label: '费用类型',
prop: 'feeCategory',
type: 'select',
search: true,
filterable: true,
dicUrl: '/blade-system/dict/dictionary?code=fee_category',
props: {
label: 'dictValue',
value: 'dictKey',
},
minWidth: 120,
rules: [{ required: true, message: '请选择费用类型', trigger: 'change' }],
},
{
label: '费用项',
prop: 'name',
minWidth: 140,
search: true,
maxlength: 50,
rules: [{ required: true, message: '请输入中文名称', trigger: 'blur' }],
rules: [{ required: true, message: '请输入费用项', trigger: 'blur' }],
},
{
label: '英文名称',
label: '费用项代码',
prop: 'englishName',
minWidth: 160,
search: true,
@@ -119,6 +137,8 @@ export default {
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
addDisplay: false,
editDisplay: false,
value: 1,
},
{
@@ -164,6 +184,7 @@ export default {
},
methods: {
normalizeRow(row) {
row.feeCategory = String(row.feeCategory || '').trim();
row.name = String(row.name || '').trim();
row.englishName = String(row.englishName || '').trim();
if (!row.status) row.status = 1;

View File

@@ -60,6 +60,9 @@
<template #parentCode="{ row }">
<span>{{ row.category === '港口' ? '-' : row.parentCode }}</span>
</template>
<template #name-header>
<span>港口/码头</span>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
{{ row.status === 1 ? '启用' : '停用' }}
@@ -71,7 +74,7 @@
clearable
filterable
:disabled="isParentRegionLocked"
placeholder="请选择国家"
:placeholder="isParentRegionLocked ? '自动带出' : '请选择国家'"
style="width: 100%"
@change="handleCountryChange"
>
@@ -83,6 +86,32 @@
/>
</el-select>
</template>
<template #category-form>
<el-radio-group v-model="form.category" @change="handleCategoryChange">
<el-radio label="港口">港口</el-radio>
<el-radio label="码头">码头</el-radio>
</el-radio-group>
</template>
<template #code-form>
<el-input
v-if="form.category !== '码头'"
v-model="form.code"
clearable
maxlength="5"
placeholder="请输入5位大写字母"
@input="handleCodeChange"
@blur="handleCodeChange(form.code)"
/>
<div v-else>
<el-input
v-model="form.terminalCode"
clearable
maxlength="24"
placeholder="请输入码头标识"
@input="handleTerminalCodeChange"
/>
</div>
</template>
<template #menu="{ row, index }">
<el-button
type="primary"
@@ -103,27 +132,72 @@
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
<template #regionCode-form>
<el-cascader
:key="regionCascaderKey"
v-model="form.regionCode"
:props="regionProps"
<template #cityCode-form>
<el-select
v-model="form.cityCode"
clearable
filterable
:disabled="isRegionDisabled"
:loading="cityLoading"
:disabled="isCityDisabled"
:placeholder="isParentRegionLocked ? '自动带出' : '请先选择国家'"
style="width: 100%"
@change="handleRegionChange"
@change="handleCityChange"
>
<el-option
v-for="item in cityOptions"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<template #districtCode-form>
<el-select
v-model="form.districtCode"
clearable
filterable
:loading="districtLoading"
:disabled="isDistrictDisabled"
:placeholder="
isParentRegionLocked ? '自动带出' : form.cityCode ? '请选择区县' : '请先选择城市'
"
style="width: 100%"
@change="handleDistrictChange"
>
<el-option
v-for="item in districtOptions"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<template #longitude-form>
<el-input
v-model="form.longitude"
clearable
placeholder="请输入经度"
@input="handleCoordinateInput('longitude', $event)"
/>
</template>
<template #latitude-form>
<el-input
v-model="form.latitude"
clearable
placeholder="请输入纬度"
@input="handleCoordinateInput('latitude', $event)"
/>
</template>
</avue-crud>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog title="港口码头主数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form
:option="excelOption"
v-model="excelForm"
:upload-before="uploadBefore"
:upload-after="uploadAfter"
>
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
@@ -147,8 +221,10 @@ import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -179,19 +255,17 @@ export default {
callback();
};
const validateLongitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < -180 || Number(value) > 180) {
callback(new Error('经度范围为 -180 到 180'));
const message = getCoordinateValidationMessage(value, 'longitude');
if (message) {
callback(new Error(message));
} else {
callback();
}
};
const validateLatitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < -90 || Number(value) > 90) {
callback(new Error('纬度范围为 -90 到 90'));
const message = getCoordinateValidationMessage(value, 'latitude');
if (message) {
callback(new Error(message));
} else {
callback();
}
@@ -205,17 +279,12 @@ export default {
excelForm: {},
portOptions: [],
countryOptions: [],
regionCascaderKey: 0,
regionNodeMap: {},
regionNameMap: {},
regionProps: {
lazy: true,
lazyLoad: this.loadRegionNode,
label: 'title',
value: 'id',
checkStrictly: false,
emitPath: true,
},
cityOptions: [],
cityLoading: false,
cityOptionCache: {},
districtOptions: [],
districtLoading: false,
districtOptionCache: {},
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
@@ -227,10 +296,15 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchShow: true,
searchLabelWidth: 120,
searchMenuSpan: 6,
searchLabelWidth: 160,
searchIcon: true,
searchIndex: 8,
searchMenuSpan: 24,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
@@ -242,31 +316,13 @@ export default {
dialogClickModal: false,
menuWidth: 160,
column: [
{
label: '编码',
prop: 'code',
search: true,
width: 140,
placeholder: '港口如 CNSHG码头如 CNSHG-CT1',
rules: [
{ required: true, message: '请输入编码', trigger: 'blur' },
{ validator: validateCode, trigger: 'blur' },
],
change: ({ value }) => this.handleCodeChange(value),
blur: ({ value }) => this.handleCodeChange(value),
},
{
label: '港口/码头名称',
prop: 'name',
minWidth: 140,
search: true,
rules: [{ required: true, message: '请输入港口/码头名称', trigger: 'blur' }],
},
{
label: '类型',
prop: 'category',
type: 'select',
search: true,
formslot: true,
span: 24,
dicData: [
{ label: '全部', value: '' },
{ label: '港口', value: '港口' },
@@ -285,6 +341,8 @@ export default {
},
dicData: [],
hide: true,
span: 12,
placeholder: '请选择',
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
change: ({ value }) => this.handleParentChange(value),
},
@@ -301,50 +359,147 @@ export default {
prop: 'parentCode',
minWidth: 120,
slot: true,
disabled: true,
span: 12,
addDisplay: false,
editDisplay: false,
placeholder: '自动带出',
rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }],
},
{
label: '港口/码头名称',
prop: 'name',
minWidth: 140,
search: true,
span: 12,
placeholder: '请输入',
rules: [{ required: true, message: '请输入港口/码头名称', trigger: 'blur' }],
},
{
label: '编码',
prop: 'code',
search: true,
formslot: true,
width: 140,
span: 12,
placeholder: '请输入编码',
rules: [
{ required: true, message: '请输入编码', trigger: 'blur' },
{ validator: validateCode, trigger: 'blur' },
],
change: ({ value }) => this.handleCodeChange(value),
blur: ({ value }) => this.handleCodeChange(value),
},
{
label: '国家',
prop: 'country',
type: 'select',
search: true,
formslot: true,
filterable: true,
dicData: [],
span: 12,
minWidth: 100,
minWidth: 140,
overHidden: false,
placeholder: '自动带出',
searchPlaceholder: '请选择 国家',
change: ({ value }) => this.handleCountryNameChange(value),
rules: [{ required: true, message: '请选择国家', trigger: 'change' }],
},
{
label: '城市',
prop: 'regionCode',
prop: 'cityCode',
formslot: true,
hide: true,
span: 12,
placeholder: '自动带出',
rules: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
},
{
label: '城市',
prop: 'city',
type: 'select',
search: true,
filterable: true,
dicData: [],
addDisplay: false,
editDisplay: false,
minWidth: 100,
change: ({ value }) => this.handleCityNameChange(value),
},
{
label: '区县',
prop: 'districtCode',
type: 'select',
formslot: true,
hide: true,
props: {
label: 'title',
value: 'id',
},
dicData: [],
span: 12,
placeholder: '选择城市后再选择区县',
rules: [{ required: true, message: '请选择区县', trigger: 'change' }],
},
{
label: '区县',
prop: 'districtName',
type: 'select',
search: true,
filterable: true,
dicData: [],
minWidth: 100,
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划编号',
prop: 'regionCode',
minWidth: 140,
span: 12,
placeholder: '选择区县后自动填写',
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '详细地址',
prop: 'detailAddress',
type: 'textarea',
minRows: 3,
span: 24,
minWidth: 220,
overHidden: false,
placeholder: '请输入',
maxlength: 255,
showWordLimit: true,
rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }],
},
{
label: '经度',
prop: 'longitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 130,
span: 12,
placeholder: '请输入',
overHidden: false,
rules: [{ validator: validateLongitude, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入经度', trigger: 'blur' },
{ validator: validateLongitude, trigger: 'blur' },
],
},
{
label: '纬度',
prop: 'latitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 130,
span: 12,
placeholder: '请输入',
overHidden: false,
rules: [{ validator: validateLatitude, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入纬度', trigger: 'blur' },
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '数据来源',
@@ -353,6 +508,8 @@ export default {
search: true,
minWidth: 120,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '初始化导入', value: '初始化导入' },
@@ -369,6 +526,8 @@ export default {
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
@@ -418,6 +577,7 @@ export default {
],
},
excelOption: {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [
@@ -459,11 +619,14 @@ export default {
: String(authority || '').includes('admin');
},
isParentRegionLocked() {
return this.form.category === '码头' && !!this.form.parentId;
return this.form.category === '码头';
},
isRegionDisabled() {
isCityDisabled() {
return this.isParentRegionLocked || !this.form.countryCode;
},
isDistrictDisabled() {
return this.isParentRegionLocked || !this.form.cityCode;
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
@@ -487,36 +650,52 @@ export default {
return;
}
if (this.form.category === '码头') {
const terminalCode = this.getTerminalCodePart(code).replace(/[^A-Z0-9]/g, '');
if (this.form.parentCode) {
this.handleTerminalCodeChange(terminalCode);
} else {
this.form.terminalCode = terminalCode;
this.form.code = code.replace(/[^A-Z0-9-]/g, '');
}
},
loadRegionNode(node, resolve) {
const parentCode = node.level === 0 ? this.form.countryCode : node.value;
if (!parentCode) {
resolve([]);
return;
}
getLazyTree(parentCode).then(res => {
const regionList = res.data.data || [];
const nodes = regionList.map(region => {
this.regionNodeMap[region.id] = region;
this.regionNameMap[region.title] = region.id;
return {
id: region.id,
title: region.title,
leaf: node.level >= 1,
};
});
resolve(nodes);
});
},
handleTerminalCodeChange(value) {
const terminalCode = String(value || '')
.toUpperCase()
.replace(/[^A-Z0-9]/g, '');
this.form.terminalCode = terminalCode;
this.form.code =
this.form.parentCode && terminalCode ? `${this.form.parentCode}-${terminalCode}` : '';
},
getTerminalCodePart(code) {
const value = String(code || '').toUpperCase();
if (this.form.parentCode && value.startsWith(`${this.form.parentCode}-`)) {
return value.slice(this.form.parentCode.length + 1);
}
return value.split('-').slice(1).join('-');
},
syncTerminalCode() {
if (this.form.category !== '码头') return;
this.form.terminalCode = this.getTerminalCodePart(this.form.code);
this.handleTerminalCodeChange(this.form.terminalCode);
},
setColumnDisplay(prop, display) {
const column = this.findColumn(this.option.column, prop);
if (!column) return;
column.display = display;
column.addDisplay = display;
column.editDisplay = display;
column.viewDisplay = display;
},
loadCountryOptions() {
return getLazyTree('0').then(res => {
this.countryOptions = res.data.data || [];
this.countryOptions.forEach(country => {
this.regionNodeMap[country.id] = country;
this.regionNameMap[country.title] = country.id;
});
this.countryOptions = (res.data.data || []).map(this.normalizeRegionOption);
const countryColumn = this.findColumn(this.option.column, 'country');
countryColumn.dicData = this.countryOptions.map(country => ({
label: country.title,
value: country.title,
id: country.id,
}));
});
},
loadPortOptions() {
@@ -525,22 +704,39 @@ export default {
this.findColumn(this.option.column, 'parentId').dicData = this.portOptions;
});
},
handleCategoryChange(value) {
clearParentPortFields() {
this.form.parentId = undefined;
this.form.parentCode = '';
this.form.parentName = '';
this.form.countryCode = '';
this.form.country = '';
this.clearCityValue();
},
handleCategoryChange(value, reset = true) {
const parentColumn = this.findColumn(this.option.column, 'parentId');
parentColumn.rules[0].required = value === '码头';
this.setColumnDisplay('parentId', value === '码头');
if (value === '港口') {
this.form.parentId = undefined;
this.form.parentCode = '';
this.form.parentName = '';
this.form.terminalCode = '';
this.form.code = reset ? '' : String(this.form.code || '').split('-')[0];
} else if (reset) {
this.clearParentPortFields();
}
this.handleCodeChange(this.form.code);
},
handleCountryChange(value) {
const country = this.countryOptions.find(item => item.id === value);
this.form.country = country ? country.title : '';
this.form.regionCode = [];
this.form.city = '';
this.regionCascaderKey += 1;
this.clearCityValue();
this.loadCityOptions(value);
},
handleCountryNameChange(value) {
const country = this.countryOptions.find(item => item.title === value);
this.setDistrictOptions([]);
this.loadCityOptions(country ? country.id : '');
},
syncCountryName(countryCode) {
const country = this.countryOptions.find(item => item.id === countryCode);
@@ -548,37 +744,177 @@ export default {
this.form.country = country.title;
}
},
normalizeRegionOption(region) {
return {
...region,
id: region.id === undefined || region.id === null ? region.id : String(region.id),
parentCode:
region.parentCode === undefined || region.parentCode === null
? region.parentCode
: String(region.parentCode),
};
},
setCityOptions(cityOptions) {
this.cityOptions = cityOptions;
const cityCodeColumn = this.findColumn(this.option.column, 'cityCode');
if (cityCodeColumn) {
cityCodeColumn.dicData = cityOptions;
}
const cityColumn = this.findColumn(this.option.column, 'city');
if (cityColumn) {
cityColumn.dicData = cityOptions.map(city => ({
label: city.title,
value: city.title,
id: city.id,
}));
}
},
clearCityValue() {
this.form.cityCode = undefined;
this.form.city = '';
this.clearDistrictValue();
},
clearDistrictValue() {
this.form.districtCode = undefined;
this.form.districtName = '';
this.form.regionCode = '';
},
loadCityOptions(countryCode) {
if (!countryCode) {
this.setCityOptions([]);
return Promise.resolve([]);
}
if (this.cityOptionCache[countryCode]) {
this.setCityOptions(this.cityOptionCache[countryCode]);
return Promise.resolve(this.cityOptionCache[countryCode]);
}
this.cityLoading = true;
return getLazyTree(countryCode)
.then(res => {
const firstLevelList = (res.data.data || []).map(this.normalizeRegionOption);
return Promise.all(
firstLevelList.map(parent => {
return getLazyTree(parent.id).then(childRes => {
const childList = (childRes.data.data || []).map(this.normalizeRegionOption);
return childList.map(child => ({
...child,
parentCode: child.parentCode || parent.id,
}));
});
})
).then(cityGroups => {
const cityList = cityGroups.flat();
const options = cityList.length > 0 ? cityList : firstLevelList;
this.cityOptionCache[countryCode] = options;
this.setCityOptions(options);
return options;
});
})
.finally(() => {
this.cityLoading = false;
});
},
resolveCityCode(cityName, regionCode) {
const city = this.cityOptions.find(item => item.title === cityName || item.id === regionCode);
return city ? city.id : '';
},
setDistrictOptions(districtOptions) {
this.districtOptions = districtOptions;
const districtCodeColumn = this.findColumn(this.option.column, 'districtCode');
if (districtCodeColumn) {
districtCodeColumn.dicData = districtOptions;
}
const districtNameColumn = this.findColumn(this.option.column, 'districtName');
if (districtNameColumn) {
districtNameColumn.dicData = districtOptions.map(district => ({
label: district.title,
value: district.title,
id: district.id,
}));
}
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.setDistrictOptions([]);
return Promise.resolve([]);
}
if (this.districtOptionCache[cityCode]) {
this.setDistrictOptions(this.districtOptionCache[cityCode]);
return Promise.resolve(this.districtOptionCache[cityCode]);
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
const options = (res.data.data || []).map(this.normalizeRegionOption);
this.districtOptionCache[cityCode] = options;
this.setDistrictOptions(options);
return options;
})
.finally(() => {
this.districtLoading = false;
});
},
resolveDistrictCode(districtName, regionCode) {
const district = this.districtOptions.find(
item => item.id === regionCode || item.title === districtName
);
return district ? district.id : regionCode || '';
},
handleParentChange(value) {
const parent = this.portOptions.find(item => item.id === value);
if (!parent) return;
const terminalCode = String(this.form.code || '')
if (!parent) {
this.clearParentPortFields();
this.syncTerminalCode();
return;
}
const terminalCode =
this.form.terminalCode ||
String(this.form.code || '')
.split('-')
.slice(1)
.join('-');
this.form.parentCode = parent.code;
this.form.parentName = parent.name;
if (this.form.category === '码头' && terminalCode) {
this.form.code = `${parent.code}-${terminalCode}`;
}
this.form.terminalCode = terminalCode;
this.handleTerminalCodeChange(terminalCode);
this.form.country = parent.country;
this.form.city = parent.city;
this.form.districtCode = parent.districtCode;
this.form.districtName = parent.districtName;
this.form.regionCode = parent.regionCode;
this.resolveCountryCode(parent.country)
.then(countryCode => {
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
this.regionCascaderKey += 1;
return this.resolveRegionCode(parent.country, parent.city);
return this.loadCityOptions(countryCode);
})
.then(regionCode => {
this.form.regionCode = regionCode;
.then(() => {
this.form.cityCode = this.resolveCityCode(parent.city, parent.regionCode);
return this.loadDistrictOptions(this.form.cityCode);
})
.then(() => {
this.form.districtCode = this.resolveDistrictCode(parent.districtName, parent.regionCode);
this.handleDistrictChange(this.form.districtCode);
});
},
handleRegionChange(value) {
const regionCode = Array.isArray(value) ? value : [];
const provinceCode = regionCode[0];
const cityCode = regionCode[1];
const city = this.regionNodeMap[cityCode];
handleCityChange(value) {
const city = this.cityOptions.find(item => item.id === value);
this.form.city = city ? city.title : '';
this.clearDistrictValue();
this.loadDistrictOptions(value);
},
handleCityNameChange(value) {
const city = this.cityOptions.find(item => item.title === value);
this.setDistrictOptions([]);
this.loadDistrictOptions(city ? city.id : '');
},
handleDistrictChange(value) {
const district = this.districtOptions.find(item => item.id === value);
this.form.districtName = district ? district.title : '';
this.form.regionCode = value || '';
},
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
resolveCountryCode(country) {
if (!country) {
@@ -597,55 +933,26 @@ export default {
return option ? option.id : DEFAULT_COUNTRY_CODE;
});
},
resolveRegionCode(country, city) {
if (!country || !city) {
return Promise.resolve([]);
}
const countryCode = this.form.countryCode || this.regionNameMap[country];
if (!countryCode) {
return Promise.resolve([]);
}
return this.loadRegionList(countryCode).then(provinceList => {
const province = provinceList.find(item => item.title === country || item.title === city);
if (province) {
return this.loadRegionList(province.id).then(cityList => {
const cityNode = cityList.find(item => item.title === city);
return cityNode ? [province.id, cityNode.id] : [province.id];
});
}
return provinceList.reduce((promise, item) => {
return promise.then(result => {
if (result.length > 0) {
return result;
}
return this.loadRegionList(item.id).then(cityList => {
const cityNode = cityList.find(child => child.title === city);
return cityNode ? [item.id, cityNode.id] : [];
});
});
}, Promise.resolve([]));
});
},
loadRegionList(parentCode) {
return getLazyTree(parentCode).then(res => {
const regionList = res.data.data || [];
regionList.forEach(region => {
this.regionNodeMap[region.id] = region;
this.regionNameMap[region.title] = region.id;
});
return regionList;
});
},
normalizeRow(row) {
row.code = String(row.code || '').toUpperCase();
if (row.category === '港口') {
row.parentId = undefined;
row.parentCode = undefined;
row.parentName = undefined;
const submitRow = { ...row };
submitRow.code = String(submitRow.code || '').toUpperCase();
submitRow.regionCode = String(submitRow.regionCode || '').trim();
submitRow.districtCode = String(submitRow.districtCode || '').trim();
submitRow.districtName = String(submitRow.districtName || '').trim();
submitRow.detailAddress = String(submitRow.detailAddress || '').trim();
submitRow.longitude = normalizeCoordinateInput(submitRow.longitude);
submitRow.latitude = normalizeCoordinateInput(submitRow.latitude);
if (submitRow.category === '港口') {
submitRow.parentId = undefined;
submitRow.parentCode = undefined;
submitRow.parentName = undefined;
}
if (!row.dataSource) row.dataSource = '手工导入';
if (!row.status) row.status = 1;
return row;
if (!submitRow.dataSource) submitRow.dataSource = '手工导入';
if (!submitRow.status) submitRow.status = 1;
delete submitRow.terminalCode;
delete submitRow.cityCode;
delete submitRow.countryCode;
return submitRow;
},
validateRequiredFields(row) {
const isBlank = value => value === undefined || value === null || String(value).trim() === '';
@@ -665,8 +972,26 @@ export default {
this.$message.warning('请选择上级港口');
return false;
}
if (!Array.isArray(row.regionCode) || row.regionCode.length < 2) {
this.$message.warning('请选择所属城市');
if (isBlank(row.regionCode)) {
this.$message.warning('请选择区县');
return false;
}
if (isBlank(row.longitude)) {
this.$message.warning('请输入经度');
return false;
}
const longitudeMessage = getCoordinateValidationMessage(row.longitude, 'longitude');
if (longitudeMessage) {
this.$message.warning(longitudeMessage);
return false;
}
if (isBlank(row.latitude)) {
this.$message.warning('请输入纬度');
return false;
}
const latitudeMessage = getCoordinateValidationMessage(row.latitude, 'latitude');
if (latitudeMessage) {
this.$message.warning(latitudeMessage);
return false;
}
return true;
@@ -770,37 +1095,57 @@ export default {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
this.form.terminalCode = this.getTerminalCodePart(this.form.code);
this.resolveCountryCode(this.form.country)
.then(countryCode => {
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
this.regionCascaderKey += 1;
return this.resolveRegionCode(this.form.country, this.form.city);
return this.loadCityOptions(countryCode);
})
.then(regionCode => {
this.form.regionCode = regionCode;
this.handleCategoryChange(this.form.category);
.then(() => {
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
return this.loadDistrictOptions(this.form.cityCode);
})
.then(() => {
this.form.districtCode = this.resolveDistrictCode(
this.form.districtName,
this.form.regionCode
);
if (!this.form.regionCode) {
this.handleDistrictChange(this.form.districtCode);
}
this.handleCategoryChange(this.form.category, false);
this.syncTerminalCode();
done();
});
});
return;
} else {
this.resolveCountryCode(this.form.country)
.then(countryCode => {
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
this.regionCascaderKey += 1;
return this.resolveRegionCode(this.form.country, this.form.city);
})
.then(regionCode => {
this.form.regionCode = regionCode;
done();
});
return;
}
this.resolveCountryCode(this.form.country)
.then(countryCode => {
this.form.countryCode = countryCode;
this.syncCountryName(countryCode);
return this.loadCityOptions(countryCode);
})
.then(() => {
this.form.cityCode = this.resolveCityCode(this.form.city, this.form.regionCode);
return this.loadDistrictOptions(this.form.cityCode);
})
.then(() => {
this.form.districtCode = this.resolveDistrictCode(
this.form.districtName,
this.form.regionCode
);
if (!this.form.regionCode) {
this.handleDistrictChange(this.form.districtCode);
}
done();
});
},
searchReset() {
this.query = {};
this.setCityOptions([]);
this.setDistrictOptions([]);
this.onLoad(this.page);
},
searchChange(params, done) {
@@ -839,24 +1184,9 @@ export default {
});
},
handleImport() {
this.excelBox = true;
},
uploadBefore(file, done) {
const fileName = file && file.name ? file.name.toLowerCase() : '';
if (!/\.(xls|xlsx)$/.test(fileName)) {
this.$message.error('请上传 .xls,.xlsx 标准格式文件');
return false;
}
if (typeof done === 'function') {
done();
}
return true;
},
uploadAfter(res, done) {
this.excelBox = false;
this.onLoad(this.page);
openImportDialog(this, '港口码头主数据', () => {
this.loadPortOptions();
done();
});
},
handleExport() {
this.$confirm('是否导出港口码头主数据?', '提示', {
@@ -894,13 +1224,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.port-terminal-page {
:deep(.avue-crud__header),
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
</style>

View File

@@ -79,15 +79,45 @@
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
</template>
<template #districtCode-form>
<el-select
v-model="form.districtCode"
clearable
filterable
:loading="districtLoading"
:disabled="!form.cityCode"
:placeholder="form.cityCode ? '请选择区县' : '请先选择城市'"
style="width: 100%"
@change="handleDistrictChange"
>
<el-option
v-for="item in districtOptions"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</el-select>
</template>
<template #longitude-form>
<el-input
v-model="form.longitude"
clearable
placeholder="请输入经度"
@input="handleCoordinateInput('longitude', $event)"
/>
</template>
<template #latitude-form>
<el-input
v-model="form.latitude"
clearable
placeholder="请输入纬度"
@input="handleCoordinateInput('latitude', $event)"
/>
</template>
</avue-crud>
<el-dialog title="铁路车站主数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form
:option="excelOption"
v-model="excelForm"
:upload-before="uploadBefore"
:upload-after="uploadAfter"
>
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
@@ -104,8 +134,10 @@ import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -128,19 +160,17 @@ export default {
}
};
const validateLongitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < -180 || Number(value) > 180) {
callback(new Error('经度范围为 -180 到 180'));
const message = getCoordinateValidationMessage(value, 'longitude');
if (message) {
callback(new Error(message));
} else {
callback();
}
};
const validateLatitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < -90 || Number(value) > 90) {
callback(new Error('纬度范围为 -90 到 90'));
const message = getCoordinateValidationMessage(value, 'latitude');
if (message) {
callback(new Error(message));
} else {
callback();
}
@@ -154,6 +184,8 @@ export default {
excelForm: {},
provinceOptions: [],
cityOptions: [],
districtOptions: [],
districtLoading: false,
regionInitializing: false,
page: {
pageSize: 10,
@@ -166,9 +198,14 @@ export default {
height: 'auto',
calcHeight: 32,
dialogWidth: 980,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
@@ -186,8 +223,8 @@ export default {
minWidth: 120,
overHidden: false,
search: true,
addDisabled: true,
editDisabled: true,
addDisplay: false,
editDisplay: false,
placeholder: '系统自动生成',
searchPlaceholder: '请输入编码',
},
@@ -271,14 +308,36 @@ export default {
editDisplay: false,
},
{
label: '行政区划',
prop: 'regionName',
search: true,
label: '所属区县',
prop: 'districtCode',
type: 'select',
formslot: true,
hide: true,
props: {
label: 'title',
value: 'id',
},
dicData: [],
filterable: true,
span: 12,
rules: [{ required: true, message: '请选择所属区县', trigger: 'change' }],
change: ({ value }) => this.handleDistrictChange(value),
},
{
label: '所属区县',
prop: 'districtName',
minWidth: 140,
addDisplay: false,
editDisplay: false,
},
{
label: '行政区划编号',
prop: 'regionCode',
minWidth: 160,
overHidden: false,
maxlength: 128,
showWordLimit: true,
rules: [{ max: 128, message: '行政区划不能超过128字', trigger: 'blur' }],
addDisplay: false,
editDisplay: false,
rules: [{ max: 32, message: '行政区划编码不能超过32字', trigger: 'blur' }],
},
{
label: '详细地址',
@@ -295,20 +354,24 @@ export default {
{
label: '经度',
prop: 'longitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 130,
overHidden: false,
rules: [{ validator: validateLongitude, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入经度', trigger: 'blur' },
{ validator: validateLongitude, trigger: 'blur' },
],
},
{
label: '纬度',
prop: 'latitude',
type: 'number',
precision: 6,
formslot: true,
minWidth: 130,
overHidden: false,
rules: [{ validator: validateLatitude, trigger: 'blur' }],
rules: [
{ required: true, message: '请输入纬度', trigger: 'blur' },
{ validator: validateLatitude, trigger: 'blur' },
],
},
{
label: '数据来源',
@@ -317,6 +380,8 @@ export default {
search: true,
minWidth: 120,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '初始化导入', value: '初始化导入' },
@@ -333,6 +398,8 @@ export default {
dataType: 'number',
minWidth: 110,
overHidden: false,
addDisplay: false,
editDisplay: false,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
@@ -383,6 +450,7 @@ export default {
],
},
excelOption: {
labelPosition: 'right',
submitBtn: false,
emptyBtn: false,
column: [
@@ -419,7 +487,9 @@ export default {
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority) ? authority.includes('admin') : String(authority || '').includes('admin');
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
ids() {
let ids = [];
@@ -456,6 +526,22 @@ export default {
this.findColumn('cityCode').dicData = this.cityOptions;
});
},
loadDistrictOptions(cityCode) {
if (!cityCode) {
this.districtOptions = [];
this.findColumn('districtCode').dicData = [];
return Promise.resolve();
}
this.districtLoading = true;
return getLazyTree(cityCode)
.then(res => {
this.districtOptions = (res.data.data || []).map(this.normalizeRegionOption);
this.findColumn('districtCode').dicData = this.districtOptions;
})
.finally(() => {
this.districtLoading = false;
});
},
normalizeRegionOption(region) {
return {
...region,
@@ -471,6 +557,10 @@ export default {
: String(row.provinceCode),
cityCode:
row.cityCode === undefined || row.cityCode === null ? row.cityCode : String(row.cityCode),
districtCode:
row.districtCode === undefined || row.districtCode === null
? row.districtCode
: String(row.districtCode),
};
},
ensureCityOption(row) {
@@ -486,6 +576,19 @@ export default {
];
this.findColumn('cityCode').dicData = this.cityOptions;
},
ensureDistrictOption(row) {
if (!row.districtCode || !row.districtName) return;
const exists = this.districtOptions.some(item => item.id === row.districtCode);
if (exists) return;
this.districtOptions = [
...this.districtOptions,
{
id: row.districtCode,
title: row.districtName,
},
];
this.findColumn('districtCode').dicData = this.districtOptions;
},
handleTmisChange(value) {
this.form.tmisCode = String(value || '')
.replace(/\D/g, '')
@@ -507,20 +610,61 @@ export default {
}
this.form.cityCode = undefined;
this.form.cityName = '';
this.form.districtCode = undefined;
this.form.districtName = '';
this.form.regionCode = '';
this.form.regionName = '';
this.loadCityOptions(value);
this.loadDistrictOptions();
},
handleCityChange(value) {
const city = this.cityOptions.find(item => item.id === value);
this.form.cityName = city ? city.title : '';
if (this.regionInitializing) {
this.loadDistrictOptions(value).then(() => this.ensureDistrictOption(this.form));
return;
}
this.form.districtCode = undefined;
this.form.districtName = '';
this.form.regionCode = '';
this.form.regionName = '';
this.loadDistrictOptions(value);
},
handleDistrictChange(value) {
const district = this.districtOptions.find(item => item.id === value);
this.form.districtName = district ? district.title : '';
this.form.regionCode = value || '';
this.form.regionName = district ? district.title : '';
},
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
normalizeRow(row) {
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim();
row.regionName = String(row.regionName || '').trim();
row.districtCode = String(row.districtCode || '').trim();
row.districtName = String(row.districtName || '').trim();
row.detailAddress = String(row.detailAddress || '').trim();
row.longitude = normalizeCoordinateInput(row.longitude);
row.latitude = normalizeCoordinateInput(row.latitude);
if (!row.dataSource) row.dataSource = '手动';
if (!row.status) row.status = 1;
return row;
},
validateCoordinateFields(row) {
const longitudeMessage = getCoordinateValidationMessage(row.longitude, 'longitude', true);
if (longitudeMessage) {
this.$message.warning(longitudeMessage);
return false;
}
const latitudeMessage = getCoordinateValidationMessage(row.latitude, 'latitude', true);
if (latitudeMessage) {
this.$message.warning(latitudeMessage);
return false;
}
return true;
},
validateUnique(row) {
const rowId = String(row.id || '');
const hasDuplicate = (params, prop) => {
@@ -552,6 +696,10 @@ export default {
},
rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateCoordinateFields(submitRow)) {
loading();
return;
}
this.validateUnique(submitRow)
.then(() => submit(submitRow))
.then(() => {
@@ -563,6 +711,10 @@ export default {
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateCoordinateFields(submitRow)) {
loading();
return;
}
this.validateUnique(submitRow)
.then(() => submit(submitRow))
.then(() => {
@@ -620,8 +772,13 @@ export default {
this.regionInitializing = true;
getDetail(this.form.id).then(res => {
const detail = this.normalizeRegionForm(res.data.data || {});
this.loadCityOptions(detail.provinceCode).then(() => {
this.loadCityOptions(detail.provinceCode)
.then(() => {
this.ensureCityOption(detail);
return this.loadDistrictOptions(detail.cityCode);
})
.then(() => {
this.ensureDistrictOption(detail);
this.form = detail;
done();
this.$nextTick(() => {
@@ -636,7 +793,9 @@ export default {
this.form.code = '';
this.form.dataSource = '手动';
this.form.status = 1;
this.loadCityOptions(this.form.provinceCode).then(() => done());
this.loadCityOptions(this.form.provinceCode)
.then(() => this.loadDistrictOptions(this.form.cityCode))
.then(() => done());
},
searchReset() {
this.query = {};
@@ -678,23 +837,7 @@ export default {
});
},
handleImport() {
this.excelBox = true;
},
uploadBefore(file, done) {
const fileName = file && file.name ? file.name.toLowerCase() : '';
if (!/\.(xls|xlsx)$/.test(fileName)) {
this.$message.error('请上传 .xls,.xlsx 标准格式文件');
return false;
}
if (typeof done === 'function') {
done();
}
return true;
},
uploadAfter(res, done) {
this.excelBox = false;
this.onLoad(this.page);
done();
openImportDialog(this, '铁路车站主数据');
},
handleExport() {
this.$confirm('是否导出铁路车站主数据?', '提示', {

View File

@@ -1,6 +1,6 @@
<template>
<el-row>
<el-col :span="9">
<el-row class="region-page">
<el-col class="region-page__tree-panel" :span="6">
<div class="box">
<el-scrollbar>
<basic-container>
@@ -9,7 +9,7 @@
</el-scrollbar>
</div>
</el-col>
<el-col :span="15">
<el-col class="region-page__form-panel" :span="18">
<basic-container>
<el-button-group>
<el-button
@@ -66,6 +66,23 @@
</basic-container>
<basic-container>
<avue-form ref="form" :option="regionOption" v-model="regionForm" @submit="handleSubmit">
<template #parentName="{}">
<el-select
class="region-parent-select"
v-model="regionForm.parentCode"
filterable
:disabled="isCountryRegion"
placeholder="请选择父区划"
@change="handleParentChange"
>
<el-option
v-for="item in parentOptions"
:key="item.code"
:label="item.name"
:value="item.code"
/>
</el-select>
</template>
<template #code="{}">
<el-input
v-if="isCountryRegion"
@@ -83,7 +100,7 @@
</template>
</avue-form>
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm" :upload-after="uploadAfter">
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
@@ -105,6 +122,7 @@ import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -118,6 +136,7 @@ export default {
treeCode: '',
treeParentCode: '',
treeData: [],
parentOptions: [],
syncLoading: false,
treeOption: {
nodeKey: 'id',
@@ -137,7 +156,7 @@ export default {
},
addBtn: false,
menu: false,
size: 'small',
size: 'default',
props: {
labelText: '标题',
label: 'title',
@@ -147,13 +166,13 @@ export default {
},
regionForm: {},
regionOption: {
labelWidth: 100,
labelWidth: 'auto',
column: [
{
label: '父区划编号',
prop: 'parentCode',
span: 24,
disabled: true,
display: false,
rules: [
{
required: true,
@@ -166,7 +185,7 @@ export default {
label: '父区划名称',
prop: 'parentName',
span: 24,
disabled: true,
formslot: true,
},
{
label: '区划编号',
@@ -295,7 +314,7 @@ export default {
debugBox: false,
debugForm: {},
debugOption: {
labelWidth: 50,
labelWidth: 'auto',
submitBtn: false,
emptyBtn: false,
column: [
@@ -345,24 +364,25 @@ export default {
if (this.isCountryRegion) {
return;
}
const parentCode = this.isProvinceRegion ? '' : this.regionForm.parentCode;
this.regionForm.code = parentCode + this.regionForm.subCode;
this.syncRegionCode();
},
'excelForm.isCovered'() {
if (this.excelForm.isCovered !== '') {
const column = this.findColumn(this.excelOption.column, 'excelFile');
column.action = `/blade-system/region/import-region?isCovered=${this.excelForm.isCovered}`;
}
this.syncImportAction();
},
},
computed: {
...mapGetters(['permission']),
isCountryRegion() {
return Number(this.regionForm.regionLevel) === 0 || this.regionForm.parentCode === this.topCode;
return (
Number(this.regionForm.regionLevel) === 0 || this.regionForm.parentCode === this.topCode
);
},
isProvinceRegion() {
return Number(this.regionForm.regionLevel) === 1;
},
isEditRegion() {
return !validatenull(this.regionForm.originalCode);
},
permissionList() {
return {
addBtn: this.validData(this.permission.region_add, false),
@@ -392,28 +412,30 @@ export default {
});
},
nodeClick(data) {
const column = this.findColumn(this.regionOption.column, 'parentCode');
column.disabled = true;
this.setRegionColumnDisplay('code', true);
this.treeCode = data.id;
this.treeParentCode = data.parentId;
getDetail(this.treeCode).then(res => {
this.regionForm = res.data.data;
this.regionForm.originalCode = this.regionForm.code;
this.regionForm.subCode =
this.regionForm.parentCode === this.topCode
? this.regionForm.code
: this.regionForm.code.replace(this.regionForm.parentCode, '');
this.loadParentOptions(this.regionForm.parentCode, this.regionForm.parentName);
});
},
addCountry() {
const column = this.findColumn(this.regionOption.column, 'parentCode');
column.disabled = true;
this.setRegionColumnDisplay('code', true);
this.treeCode = '';
this.treeParentCode = this.topCode;
this.parentOptions = this.getRootParentOptions();
this.regionForm = {
parentCode: this.topCode,
parentName: '根节点',
code: '',
subCode: '',
originalCode: '',
name: '',
regionLevel: 0,
sort: 1,
@@ -424,13 +446,84 @@ export default {
this.$message.warning('请先选择一项区划');
return;
}
this.regionForm.parentCode = this.regionForm.code;
this.regionForm.parentName = this.regionForm.name;
const parentCode = this.regionForm.code;
const parentName = this.regionForm.name;
const parentGroupCode = this.regionForm.parentCode || this.topCode;
this.setRegionColumnDisplay('code', true);
this.regionForm.parentCode = parentCode;
this.regionForm.parentName = parentName;
this.regionForm.code = '';
this.regionForm.subCode = '';
this.regionForm.originalCode = '';
this.regionForm.name = '';
this.regionForm.regionLevel =
this.regionForm.regionLevel === 5 ? 5 : this.regionForm.regionLevel + 1;
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
},
setRegionColumnDisplay(prop, display) {
const column = this.findColumn(this.regionOption.column, prop);
if (column) {
column.display = display;
}
},
handleParentChange(value) {
const parent = this.parentOptions.find(item => item.code === value);
this.regionForm.parentName = parent ? parent.name : '';
this.syncRegionCode();
},
syncRegionCode() {
if (this.isCountryRegion) {
return;
}
const parentCode = this.isProvinceRegion ? '' : this.regionForm.parentCode || '';
this.regionForm.code = parentCode + (this.regionForm.subCode || '');
},
getRootParentOptions() {
return [
{
code: this.topCode,
name: '根节点',
},
];
},
normalizeParentOption(region) {
return {
code: String(region.code || region.id || ''),
name: region.name || region.title || '',
};
},
ensureSelectedParent(options, selectedCode, selectedName) {
if (validatenull(selectedCode) || options.some(item => item.code === selectedCode)) {
return options;
}
return [
...options,
{
code: String(selectedCode),
name: selectedName || selectedCode,
},
];
},
loadParentOptions(selectedCode, selectedName) {
if (validatenull(selectedCode) || selectedCode === this.topCode) {
this.parentOptions = this.getRootParentOptions();
return;
}
getDetail(selectedCode).then(res => {
const parent = res.data.data || {};
const groupCode = parent.parentCode || this.topCode;
this.loadParentOptionsByGroup(groupCode, selectedCode, selectedName);
});
},
loadParentOptionsByGroup(groupCode, selectedCode, selectedName) {
if (validatenull(groupCode)) {
this.parentOptions = this.ensureSelectedParent([], selectedCode, selectedName);
return;
}
getLazyTree(groupCode).then(res => {
const options = res.data.data.map(this.normalizeParentOption).filter(item => item.code);
this.parentOptions = this.ensureSelectedParent(options, selectedCode, selectedName);
});
},
handleSubmit(form, done) {
if (Number(form.regionLevel) === 0) {
@@ -481,18 +574,27 @@ export default {
this.$refs.form.resetForm();
});
},
uploadAfter(res, done, loading, column) {
window.console.log(column);
this.excelBox = false;
handleImport() {
this.syncImportAction();
openImportDialog(this, '行政区划', () => {
this.initTree();
done();
});
},
syncImportAction() {
const column = this.findColumn(this.excelOption.column, 'excelFile');
if (!column) {
return;
}
const covered = this.excelForm.isCovered;
if (covered === '' || covered === undefined || covered === null) {
column.action = '/blade-system/region/import-region';
} else {
column.action = `/blade-system/region/import-region?isCovered=${covered}`;
}
},
handleDebug() {
this.debugBox = true;
},
handleImport() {
this.excelBox = true;
},
handleSync() {
this.$confirm('确定同步最新行政区划数据?', {
confirmButtonText: '确定',
@@ -553,4 +655,46 @@ export default {
.box .el-scrollbar__wrap {
overflow: scroll;
}
.region-parent-select {
width: 100%;
}
.region-page .region-page__tree-panel {
flex: 0 0 17.5%;
max-width: 17.5%;
}
.region-page .region-page__form-panel {
flex: 0 0 82.5%;
max-width: 82.5%;
}
.region-page__tree-panel .avue-tree__filter {
padding: 0 8px 8px;
}
.region-page__tree-panel .avue-tree__filter .el-input,
.region-page__tree-panel .avue-tree__filter .el-input__wrapper {
width: 100%;
min-height: 32px;
}
.region-page__tree-panel .el-tree.el-tree--highlight-current {
padding: 8px;
box-sizing: border-box;
}
.region-page__form-panel .avue-form__group.avue-form__group--flex {
padding: 12px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.region-page .region-page__tree-panel,
.region-page .region-page__form-panel {
flex: 0 0 100%;
max-width: 100%;
}
}
</style>

View File

@@ -89,8 +89,9 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchIndex: 3,
searchMenuSpan: 24,
searchMenuPosition: 'right',
searchIndex: 8,
searchIcon: true,
border: true,
index: true,
@@ -99,7 +100,7 @@ export default {
addBtn: false,
delBtn: false,
menuWidth: 120,
labelWidth: 120,
labelWidth: 'auto',
dialogType: 'drawer',
column: [
{

View File

@@ -59,7 +59,10 @@ const option = reactive({
dialogWidth: 950,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -60,7 +60,10 @@ export default {
dialogWidth: 950,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -24,7 +24,7 @@ export default {
file: {},
},
option: {
labelWidth: 120,
labelWidth: 'auto',
menuBtn: false,
column: [
{

View File

@@ -22,7 +22,7 @@
</template>
</avue-crud>
<el-dialog title="流程删除" append-to-body v-model="followBox" width="20%">
<el-form :model="form" ref="form" label-width="80px">
<el-form :model="form" ref="form" label-width="auto">
<el-form-item label="删除理由">
<el-input v-model="deleteReason" placeholder="请输入删除理由" />
</el-form-item>
@@ -62,7 +62,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -88,7 +88,7 @@
</template>
</el-dialog>
<el-dialog title="流程变更" append-to-body v-model="stateBox" width="20%">
<el-form :model="form" ref="form" label-width="80px">
<el-form :model="form" ref="form" label-width="auto">
<el-form-item label="流程状态">
<el-select v-model="flowState" placeholder="请选择" value="">
<el-option
@@ -150,7 +150,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -207,7 +207,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -41,7 +41,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -41,7 +41,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -41,7 +41,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -69,7 +69,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -108,7 +108,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -86,14 +86,17 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
menuWidth: 350,
labelWidth: 100,
labelWidth: 'auto',
dialogWidth: 880,
dialogClickModal: false,
column: [
@@ -103,7 +106,7 @@ export default {
value: 1,
span: 24,
width: 120,
searchLabelWidth: 50,
searchLabelWidth: 160,
row: true,
dicUrl: '/blade-system/dict/dictionary?code=oss',
props: {

View File

@@ -86,14 +86,17 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
menuWidth: 350,
labelWidth: 100,
labelWidth: 'auto',
dialogWidth: 880,
dialogClickModal: false,
column: [
@@ -103,7 +106,7 @@ export default {
value: 1,
span: 24,
width: 100,
searchLabelWidth: 50,
searchLabelWidth: 160,
row: true,
dicUrl: '/blade-system/dict/dictionary?code=sms',
props: {

View File

@@ -67,7 +67,7 @@
<!-- 手动锁定用户 -->
<el-dialog title="锁定用户" append-to-body v-model="lockUserBox" width="500px">
<el-form :model="lockUserForm" label-width="100px" :rules="lockUserRules" ref="lockUserFormRef">
<el-form :model="lockUserForm" label-width="auto" :rules="lockUserRules" ref="lockUserFormRef">
<el-form-item label="用户账号">
<el-input :model-value="lockUserForm.account" disabled />
</el-form-item>
@@ -108,7 +108,7 @@
<!-- 解锁原因确认 -->
<el-dialog title="解锁确认" append-to-body v-model="unlockReasonBox" width="450px">
<el-form :model="unlockReasonForm" label-width="80px">
<el-form :model="unlockReasonForm" label-width="auto">
<el-form-item label="解锁原因">
<el-input
v-model="unlockReasonForm.unlockReason"

View File

@@ -55,7 +55,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -73,7 +73,10 @@ export default {
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
tree: true,
border: true,
index: true,

View File

@@ -79,7 +79,10 @@ export default {
tip: false,
simplePage: true,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
dialogWidth: '60%',
tree: true,
border: true,
@@ -137,13 +140,6 @@ export default {
type: 'icon',
slot: true,
iconList: iconList,
rules: [
{
required: true,
message: '请输入菜单图标',
trigger: 'click',
},
],
},
{
label: '菜单编号',

View File

@@ -136,7 +136,7 @@
:before-close="beforeClose"
append-to-body
>
<el-form :disabled="view" ref="form" :model="form" label-width="80px">
<el-form :disabled="view" ref="form" :model="form" label-width="auto">
<!-- 表单字段 -->
<el-form-item label="参数名:" prop="paramName">
<el-input v-model="form.paramName" placeholder="请输入参数名" />

View File

@@ -59,7 +59,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -231,7 +231,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
selection: true,

View File

@@ -55,7 +55,10 @@ export default {
dialogWidth: 900,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
@@ -84,7 +87,7 @@ export default {
value: 1,
span: 24,
width: 120,
searchLabelWidth: 100,
searchLabelWidth: 160,
row: true,
gridRow: true,
dicUrl: '/blade-system/dict/dictionary?code=datasource_category',

View File

@@ -90,7 +90,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -165,7 +165,10 @@ export default {
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,

View File

@@ -275,7 +275,7 @@
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-width="90px"
label-width="auto"
>
<el-form-item label="登录账号">
<el-input v-model="passwordForm.account" disabled />

View File

@@ -4,7 +4,7 @@
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- 个人信息 Tab -->
<el-tab-pane label="个人信息" name="info">
<el-form ref="infoForm" :model="infoForm" label-width="100px">
<el-form ref="infoForm" :model="infoForm" label-width="auto">
<el-form-item label="头像">
<div class="avatar-upload-container">
<div class="avatar-wrapper" v-if="infoForm.avatar">
@@ -74,7 +74,7 @@
<!-- 修改密码 Tab -->
<el-tab-pane label="修改密码" name="password">
<el-form ref="passwordForm" :model="passwordForm" label-width="100px">
<el-form ref="passwordForm" :model="passwordForm" label-width="auto">
<el-form-item label="原密码">
<el-input
v-model="passwordForm.oldPassword"

View File

@@ -57,7 +57,10 @@ export default {
dialogWidth: 900,
tip: false,
searchShow: true,
searchMenuSpan: 6,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
viewBtn: true,
@@ -86,7 +89,7 @@ export default {
value: 1,
span: 24,
width: 120,
searchLabelWidth: 100,
searchLabelWidth: 160,
row: true,
gridRow: true,
display: false,

Some files were not shown because too many files have changed in this diff Show More