打包不成功修改严格模式
This commit is contained in:
parent
7d95fd9b27
commit
d8f743ef4e
@ -3,6 +3,6 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV='development'
|
||||
|
||||
VITE_APP_TITLE = '公司开发平台框架'
|
||||
VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
|
||||
VITE_APP_PORT = 3000
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
## 生产环境
|
||||
NODE_ENV='production'
|
||||
|
||||
VITE_APP_TITLE = 'NewFrameWork2023-WEB'
|
||||
VITE_APP_TITLE = 'qgc-buji-web'
|
||||
VITE_APP_PORT = 3000
|
||||
VITE_APP_BASE_API = '/prod-api'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
## 模拟环境
|
||||
NODE_ENV='staging'
|
||||
|
||||
VITE_APP_TITLE = 'NewFrameWork2023-WEB'
|
||||
VITE_APP_TITLE = 'qgc-buji-web'
|
||||
VITE_APP_PORT = 3000
|
||||
VITE_APP_BASE_API = '/prod--api'
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="NewFrameWork2023-WEB" />
|
||||
<meta name="keywords" content="NewFrameWork2023-WEB" />
|
||||
<title>公司开发平台框架</title>
|
||||
<meta name="description" content="qgc-buji-web" />
|
||||
<meta name="keywords" content="qgc-buji-web" />
|
||||
<title>水电水利建设项目全过程环境管理信息平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "NewFrameWork2023-WEB",
|
||||
"name": "qgc-buji-web",
|
||||
"version": "1.2.0",
|
||||
"scripts": {
|
||||
"dev": "vite serve --mode development",
|
||||
"build:prod": "vue-tsc --noEmit && vite build --mode production",
|
||||
"build": "vue-tsc --noEmit && vite build --mode production",
|
||||
"build:mvn": "vite build --mode production",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint src/**/*.{ts,js,vue} --fix",
|
||||
@ -16,7 +16,7 @@
|
||||
"@vueuse/core": "^9.1.1",
|
||||
"@wangeditor/editor": "^5.0.0",
|
||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"ant-design-vue": "latest",
|
||||
"axios": "^1.2.0",
|
||||
"better-scroll": "^2.4.2",
|
||||
"dayjs": "^1.11.20",
|
||||
@ -50,6 +50,8 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
@ -67,10 +69,10 @@
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.53.0",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.7.4",
|
||||
"typescript": "latest",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^0.35.0"
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
"repository": "https://gitee.com/youlaiorg/vue3-element-admin.git",
|
||||
"author": "有来开源组织",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -80,7 +80,7 @@ export function listDepartments(queryParams?: DeptQuery): AxiosPromise<Dept[]> {
|
||||
/**
|
||||
* 部门下拉列表
|
||||
*/
|
||||
export function listDeptOptions(): AxiosPromise<OptionType[]> {
|
||||
export function listDeptOptions(): AxiosPromise<Dept[]> {
|
||||
return request({
|
||||
url: '/api/v1/dept/options',
|
||||
method: 'get'
|
||||
|
||||
@ -170,7 +170,7 @@ export function deleteDictTypes(ids: string) {
|
||||
*
|
||||
* @param typeCode 字典类型编码
|
||||
*/
|
||||
export function getDictionaries(typeCode: string): AxiosPromise<OptionType[]> {
|
||||
export function getDictionaries(typeCode: string): AxiosPromise<any[]> {
|
||||
return request({
|
||||
url: '/api/v1/dict/types/' + typeCode + '/items',
|
||||
method: 'get'
|
||||
|
||||
@ -7,6 +7,10 @@ export interface DictQuery extends PageQuery {
|
||||
*/
|
||||
name?: string;
|
||||
}
|
||||
interface PageResult<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
@ -34,6 +38,10 @@ export interface DictTypeForm {
|
||||
status: number;
|
||||
remark: string;
|
||||
}
|
||||
interface PageQuery {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典项查询参数类型声明
|
||||
|
||||
34
frontend/src/api/guoYuSheShiShuJuTianBao/index.ts
Normal file
34
frontend/src/api/guoYuSheShiShuJuTianBao/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 分页查询过鱼数据
|
||||
export function getFishDraftPage(data:any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/page',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
//新增目录
|
||||
export function addFishDraft(queryParams:any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/add',
|
||||
method: 'post',
|
||||
data: queryParams
|
||||
});
|
||||
}
|
||||
//修改目录
|
||||
export function editFishDraft(queryParams:any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/update',
|
||||
method: 'post',
|
||||
data: queryParams
|
||||
});
|
||||
}
|
||||
//删除
|
||||
export function delFishDraft(data:any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/batchDelete',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
@ -91,7 +91,7 @@ export function listMenus(queryParams: MenuQuery): AxiosPromise<Menu[]> {
|
||||
/**
|
||||
* 获取菜单下拉树形列表
|
||||
*/
|
||||
export function listMenuOptions(): AxiosPromise<OptionType[]> {
|
||||
export function listMenuOptions(): AxiosPromise<Menu[]> {
|
||||
return request({
|
||||
url: '/api/v1/menus/options',
|
||||
method: 'get'
|
||||
|
||||
@ -108,7 +108,7 @@ export function postOrgscope (queryParams:any){
|
||||
*/
|
||||
export function listRoleOptions(
|
||||
queryParams?: RoleQuery
|
||||
): AxiosPromise<OptionType[]> {
|
||||
): AxiosPromise<any[]> {
|
||||
return request({
|
||||
url: '/api/v1/roles/options',
|
||||
method: 'get',
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
/**
|
||||
* 角色查询参数类型
|
||||
*/
|
||||
interface PageQuery {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
}
|
||||
export interface RoleQuery extends PageQuery {
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
interface PageResult<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
/**
|
||||
* 角色分页列表项
|
||||
*/
|
||||
|
||||
@ -9,10 +9,14 @@ export interface UserInfo {
|
||||
roles: string[];
|
||||
perms: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户查询参数
|
||||
*/
|
||||
export interface PageQuery {
|
||||
keywords: string;
|
||||
status: number;
|
||||
deptId: number;
|
||||
}
|
||||
export interface UserQuery extends PageQuery {
|
||||
keywords: string;
|
||||
status: number;
|
||||
@ -36,10 +40,14 @@ export interface UserType {
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
interface PageResult<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
/**
|
||||
* 用户分页项类型声明
|
||||
*/
|
||||
export type UserPageResult = PageResult<UserType[]>;
|
||||
export type UserPageResult = PageResult<any[]>;
|
||||
|
||||
/**
|
||||
* 用户表单类型声明
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useUiStore } from "@/store/modules/ui";
|
||||
import shiliangImg from "@/assets/images/map-shiliangtu.png";
|
||||
import dixingImg from "@/assets/images/map-dixingtu.png";
|
||||
@ -52,10 +52,6 @@ const handleSwitch = (key: string) => {
|
||||
activeKey.value = key;
|
||||
props.map.baseLayerSwitcher(key);
|
||||
}
|
||||
watch(activeKey, (val) => {
|
||||
// nineSectionsImg.value =
|
||||
// nineSectionsData.value.find((item) => item.name === val)?.img || "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -18,9 +18,17 @@
|
||||
:name="item.name"
|
||||
style="width: 100%; margin-bottom: 0"
|
||||
>
|
||||
<!-- 1. 优先检查是否有具名插槽,或者 type 为 custom -->
|
||||
<slot
|
||||
v-if="$slots[item.name] || item.type === 'custom'"
|
||||
:name="item.name"
|
||||
:value="formData[item.name]"
|
||||
:onChange="(val:any) => { formData[item.name] = val }"
|
||||
:formModel="formData"
|
||||
/>
|
||||
<!-- 普通日期选择器 -->
|
||||
<a-date-picker
|
||||
v-if="item.type === 'DataPicker'"
|
||||
v-else-if="item.type === 'DataPicker'"
|
||||
v-model:value="formData[item.name]"
|
||||
:picker="item.picker"
|
||||
:format="item.fieldProps?.format"
|
||||
@ -70,20 +78,6 @@
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
:value="formData.stcd?.hbrvcd"
|
||||
placeholder="请选择河流"
|
||||
@change="hbrvcdChange"
|
||||
style="width: 135px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in item.options"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
:value="formData.stcd?.stcdId"
|
||||
placeholder="请选择电站"
|
||||
@ -117,6 +111,21 @@
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<!-- 单选 框 -->
|
||||
<a-radio-group
|
||||
v-else-if="item.type === 'Radio'"
|
||||
v-model:value="formData[item.name]"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
>
|
||||
<a-radio
|
||||
v-for="opt in item.options"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
@ -141,7 +150,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, reactive, watch, onMounted } from "vue";
|
||||
import type { FormInstance } from "ant-design-vue";
|
||||
|
||||
// --- 类型定义 ---
|
||||
export interface SearchItem {
|
||||
@ -178,7 +186,7 @@ const emit = defineEmits<{
|
||||
(e: "reset"): void;
|
||||
}>();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const formRef = ref<any>();
|
||||
const formData = reactive<any>({});
|
||||
const rules = reactive<Record<string, any>>({});
|
||||
|
||||
@ -209,9 +217,9 @@ const initForm = () => {
|
||||
const dataDimensionDataChange = (value: any) => {
|
||||
formData.stcd.dataDimensionData = value;
|
||||
};
|
||||
const hbrvcdChange = (value: any) => {
|
||||
formData.stcd.hbrvcd = value;
|
||||
};
|
||||
// const hbrvcdChange = (value: any) => {
|
||||
// formData.stcd.hbrvcd = value;
|
||||
// };
|
||||
const stcdIdChange = (value: any) => {
|
||||
formData.stcd.stcdId = value;
|
||||
};
|
||||
|
||||
186
frontend/src/components/BasicTable/index.vue
Normal file
186
frontend/src/components/BasicTable/index.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:row-selection="enableRowSelection ? rowSelection : undefined"
|
||||
:data-source="tableData"
|
||||
:columns="columns"
|
||||
:pagination="paginationConfig"
|
||||
:scroll="{ x: '100%' }"
|
||||
:row-key="rowKey"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<!-- 透传插槽,支持自定义列内容 -->
|
||||
<template v-for="slot in Object.keys($slots)" #[slot]="scope" :key="slot">
|
||||
<slot :name="slot" v-bind="scope"></slot>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
|
||||
// --- Types ---
|
||||
interface Props {
|
||||
columns: any[];
|
||||
// 请求数据的函数,由父组件传入
|
||||
listUrl: (params: any) => Promise<any>;
|
||||
// 是否开启行选择
|
||||
enableRowSelection?: boolean;
|
||||
// 行数据的 Key,用来优化 Table 的渲染
|
||||
rowKey?: string;
|
||||
// 外部传入的搜索/过滤参数,变化时会自动触发刷新
|
||||
searchParams?: Record<string, any>;
|
||||
// 默认每页显示数量
|
||||
defaultPageSize?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
enableRowSelection: false,
|
||||
rowKey: "id",
|
||||
searchParams: () => ({}),
|
||||
defaultPageSize: 20,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
// 每次数据请求后,向父组件返回当前的查询参数和数据结果
|
||||
(e: "data-loaded", params: any, data: any): void;
|
||||
// 选中项变化
|
||||
(e: "selection-change", selectedRowKeys: string[], selectedRows: any[]): void;
|
||||
}>();
|
||||
|
||||
// --- State ---
|
||||
const loading = ref(false);
|
||||
const tableData = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const size = ref(props.defaultPageSize);
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
|
||||
// --- Row Selection ---
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
emit("selection-change", keys, rows);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: record.disabled, // 可根据业务逻辑禁用某些行的勾选
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Pagination Config ---
|
||||
const paginationConfig = computed(() => ({
|
||||
total: total.value,
|
||||
current: page.value,
|
||||
pageSize: size.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
pageSizeOptions: ["20", "50", "100"],
|
||||
}));
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
/**
|
||||
* 获取列表数据
|
||||
* @param extraParams 额外的临时参数(可选)
|
||||
*/
|
||||
const getList = async (extraParams?: Record<string, any>) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 合并基础分页参数、外部搜索参数和临时参数
|
||||
const params = {
|
||||
...props.searchParams,
|
||||
...extraParams,
|
||||
skip: page.value,
|
||||
take: size.value,
|
||||
filter: {}
|
||||
// 如果后端需要 skip/take 格式,可以在此转换
|
||||
// skip: (page.value - 1) * size.value,
|
||||
// take: size.value,
|
||||
};
|
||||
|
||||
const res = await props.listUrl(params);
|
||||
|
||||
// 假设后端返回结构为 { data: { records: [], total: 0 } }
|
||||
// 请根据实际后端接口调整以下取值逻辑
|
||||
const records = res?.data?.records || res?.data?.list || res?.data || [];
|
||||
const totalCount = res?.data?.total || res?.total || 0;
|
||||
|
||||
tableData.value = records;
|
||||
total.value = totalCount;
|
||||
|
||||
// 向父组件暴露当前请求参数和结果
|
||||
emit("data-loaded", params, { records, total: totalCount });
|
||||
} catch (error) {
|
||||
console.error("Fetch table data error:", error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理分页、排序、筛选变化
|
||||
*/
|
||||
const handleTableChange = (pag: any) => {
|
||||
page.value = pag.current;
|
||||
size.value = pag.pageSize;
|
||||
getList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置表格状态(回到第一页,清空选中)
|
||||
*/
|
||||
const reset = () => {
|
||||
page.value = 1;
|
||||
size.value = props.defaultPageSize;
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
getList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新当前页
|
||||
*/
|
||||
const refresh = () => {
|
||||
getList();
|
||||
};
|
||||
|
||||
// --- Expose Methods to Parent ---
|
||||
defineExpose({
|
||||
getList,
|
||||
reset,
|
||||
refresh,
|
||||
// 也可以暴露选中的数据供父组件获取
|
||||
getSelected: () => ({
|
||||
keys: selectedRowKeys.value,
|
||||
rows: selectedRows.value,
|
||||
}),
|
||||
});
|
||||
|
||||
// --- Watchers ---
|
||||
|
||||
// 监听外部搜索参数变化,自动重置页码并刷新
|
||||
watch(
|
||||
() => props.searchParams,
|
||||
() => {
|
||||
page.value = 1; // 搜索时通常重置到第一页
|
||||
getList();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// --- Lifecycle ---
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 如有必要,添加少量样式 */
|
||||
</style>
|
||||
@ -22,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, PropType, defineEmits, computed ,onMounted} from 'vue'
|
||||
import { ref, PropType, computed ,onMounted} from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
const appStore = useAppStore();
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div style="border: 1px solid #ccc">
|
||||
<!-- 工具栏 -->
|
||||
<Toolbar
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
style="border-bottom: 1px solid #ccc"
|
||||
:mode="mode"
|
||||
/>
|
||||
<!-- 编辑器 -->
|
||||
<Editor
|
||||
:defaultConfig="editorConfig"
|
||||
v-model="defaultHtml"
|
||||
@onChange="handleChange"
|
||||
style="height: 500px; overflow-y: hidden"
|
||||
:mode="mode"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, shallowRef, reactive, toRefs } from 'vue';
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
|
||||
// API 引用
|
||||
import { uploadFileApi } from '@/api/file';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String],
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef();
|
||||
|
||||
const state = reactive({
|
||||
toolbarConfig: {},
|
||||
editorConfig: {
|
||||
placeholder: '请输入内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
// 自定义图片上传
|
||||
async customUpload(file: any, insertFn: any) {
|
||||
uploadFileApi(file).then(response => {
|
||||
const url = response.data.url;
|
||||
insertFn(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultHtml: props.modelValue,
|
||||
mode: 'default'
|
||||
});
|
||||
|
||||
const { toolbarConfig, editorConfig, defaultHtml, mode } = toRefs(state);
|
||||
|
||||
const handleCreated = (editor: any) => {
|
||||
editorRef.value = editor; // 记录 editor 实例,重要!
|
||||
};
|
||||
|
||||
function handleChange(editor: any) {
|
||||
emit('update:modelValue', editor.getHtml());
|
||||
}
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value;
|
||||
if (editor == null) return;
|
||||
editor.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style src="@wangeditor/editor/dist/css/style.css"></style>
|
||||
@ -44,11 +44,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
// 状态定义
|
||||
const dataLoading = ref(false);
|
||||
const data = ref<Record<string, number>>({ one: 0, two: 0, three: 0 });
|
||||
|
||||
|
||||
|
||||
271
frontend/src/components/fishSearch/index.vue
Normal file
271
frontend/src/components/fishSearch/index.vue
Normal file
@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<a-select
|
||||
:style="{ width: width }"
|
||||
:value="modelValue"
|
||||
@change="handleChange"
|
||||
placeholder="请选择鱼名称"
|
||||
mode="multiple"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
class="custom-fish-select"
|
||||
:dropdownMatchSelectWidth="false"
|
||||
:getPopupContainer="(triggerNode: HTMLElement) => triggerNode.parentNode"
|
||||
@dropdownVisibleChange="handleDropdownVisibleChange"
|
||||
>
|
||||
<!-- 自定义 Tag 显示名称 -->
|
||||
<template #tagRender="{ value: tagId, onClose }">
|
||||
<a-tag
|
||||
closable
|
||||
@close="onClose"
|
||||
style="margin-right: 3px; max-width: 120px"
|
||||
>
|
||||
{{ getFishNameById(tagId) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #dropdownRender>
|
||||
<div class="custom-dropdown-container">
|
||||
<!-- 左侧:可滚动的选项列表 -->
|
||||
<div class="dropdown-left-list">
|
||||
<div
|
||||
v-for="opt in filteredOptions"
|
||||
:key="opt.id"
|
||||
class="dropdown-item"
|
||||
:class="{
|
||||
'is-active': Array.isArray(modelValue) && modelValue.includes(opt.id),
|
||||
'is-hovered': opt.id === hoveredId,
|
||||
}"
|
||||
@click.stop="handleSelectOption(opt)"
|
||||
@mouseenter="hoveredId = opt.id"
|
||||
>
|
||||
<span class="item-name">{{ opt.name }}</span>
|
||||
<!-- 选中对勾 -->
|
||||
<span v-if="Array.isArray(modelValue) && modelValue.includes(opt.id)" class="check-icon">✓</span>
|
||||
</div>
|
||||
<div v-if="filteredOptions.length === 0" class="empty-tip">
|
||||
无匹配数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间分割线 -->
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<!-- 右侧:固定显示的别名/详情 -->
|
||||
<div class="dropdown-right-detail">
|
||||
<div v-if="currentDetailData" class="detail-content">
|
||||
<div class="detail-title">{{ currentDetailData.name }}</div>
|
||||
<div class="detail-alias" :title="currentDetailData.alias">
|
||||
{{ currentDetailData.alias || "暂无别名" }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="detail-placeholder">请选择或悬停查看</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// --- Props & Emits ---
|
||||
interface Props {
|
||||
modelValue: string[]; // 接收选中的 ID 数组
|
||||
options: any[];
|
||||
width: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string[]): void;
|
||||
}>();
|
||||
|
||||
// --- State ---
|
||||
// 这里可以改为从 API 获取,目前先保留静态数据作为示例
|
||||
const options = ref<any>(props.options || []);
|
||||
|
||||
const hoveredId = ref<string | null>(null);
|
||||
|
||||
// --- Computed ---
|
||||
const filteredOptions = computed(() => {
|
||||
return options.value;
|
||||
});
|
||||
|
||||
const currentDetailData = computed(() => {
|
||||
if (hoveredId.value) {
|
||||
return options.value.find((item: any) => item.id === hoveredId.value);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// --- Methods ---
|
||||
const handleDropdownVisibleChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
hoveredId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
if (!input) return true;
|
||||
const targetOpt = options.value.find((item: any) => item.id === option.value);
|
||||
if (!targetOpt) return false;
|
||||
|
||||
const lowerInput = input.toLowerCase();
|
||||
const nameMatch = targetOpt.name?.toLowerCase().includes(lowerInput);
|
||||
const aliasMatch = targetOpt.alias?.toLowerCase().includes(lowerInput);
|
||||
return nameMatch || aliasMatch;
|
||||
};
|
||||
|
||||
const handleSelectOption = (opt: any) => {
|
||||
let newValues: string[] = Array.isArray(props.modelValue) ? [...props.modelValue] : [];
|
||||
const index = newValues.indexOf(opt.id);
|
||||
|
||||
if (index > -1) {
|
||||
newValues.splice(index, 1);
|
||||
} else {
|
||||
newValues.push(opt.id);
|
||||
}
|
||||
|
||||
// 触发更新
|
||||
emit("update:modelValue", newValues);
|
||||
};
|
||||
|
||||
const handleChange = (val: any) => {
|
||||
// 处理直接通过 a-select 内部机制触发的变化(如删除 tag)
|
||||
emit("update:modelValue", val);
|
||||
};
|
||||
|
||||
const getFishNameById = (id: string) => {
|
||||
if (!id) return "";
|
||||
const fish = options.value.find((item: any) => item.id === id);
|
||||
return fish ? fish.name : id;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-fish-select {
|
||||
:deep(.ant-select-dropdown) {
|
||||
padding: 0 !important;
|
||||
min-width: 400px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dropdown-container {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
height: 300px; /* 固定高度 */
|
||||
}
|
||||
|
||||
.dropdown-left-list {
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
transition: background-color 0.3s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.is-hovered {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
width: 1px;
|
||||
background-color: #e8e8e8;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropdown-right-detail {
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detail-alias {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-placeholder {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 10px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,115 +0,0 @@
|
||||
type EventCallback = (...args: any[]) => void;
|
||||
|
||||
export class EventEmitter {
|
||||
private events: Map<string, EventCallback[]> = new Map();
|
||||
|
||||
on(event: string, callback: EventCallback): this {
|
||||
if (!this.events.has(event)) {
|
||||
this.events.set(event, []);
|
||||
}
|
||||
this.events.get(event)!.push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event: string, callback?: EventCallback): this {
|
||||
if (!this.events.has(event)) return this;
|
||||
|
||||
if (callback) {
|
||||
const callbacks = this.events.get(event)!;
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.events.delete(event);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
emit(event: string, ...args: any[]): this {
|
||||
if (this.events.has(event)) {
|
||||
this.events.get(event)!.forEach(callback => {
|
||||
try {
|
||||
callback(...args);
|
||||
} catch (error) {
|
||||
console.error(`Event ${event} handler error:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
once(event: string, callback: EventCallback): this {
|
||||
const onceCallback: EventCallback = (...args: any[]) => {
|
||||
callback(...args);
|
||||
this.off(event, onceCallback);
|
||||
};
|
||||
return this.on(event, onceCallback);
|
||||
}
|
||||
|
||||
removeAllListeners(): this {
|
||||
this.events.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
listenerCount(event: string): number {
|
||||
return this.events.get(event)?.length || 0;
|
||||
}
|
||||
}type EventCallback = (...args: any[]) => void;
|
||||
|
||||
export class EventEmitter {
|
||||
private events: Map<string, EventCallback[]> = new Map();
|
||||
|
||||
on(event: string, callback: EventCallback): this {
|
||||
if (!this.events.has(event)) {
|
||||
this.events.set(event, []);
|
||||
}
|
||||
this.events.get(event)!.push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event: string, callback?: EventCallback): this {
|
||||
if (!this.events.has(event)) return this;
|
||||
|
||||
if (callback) {
|
||||
const callbacks = this.events.get(event)!;
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.events.delete(event);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
emit(event: string, ...args: any[]): this {
|
||||
if (this.events.has(event)) {
|
||||
this.events.get(event)!.forEach(callback => {
|
||||
try {
|
||||
callback(...args);
|
||||
} catch (error) {
|
||||
console.error(`Event ${event} handler error:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
once(event: string, callback: EventCallback): this {
|
||||
const onceCallback: EventCallback = (...args: any[]) => {
|
||||
callback(...args);
|
||||
this.off(event, onceCallback);
|
||||
};
|
||||
return this.on(event, onceCallback);
|
||||
}
|
||||
|
||||
removeAllListeners(): this {
|
||||
this.events.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
listenerCount(event: string): number {
|
||||
return this.events.get(event)?.length || 0;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,15 @@
|
||||
// import { Session } from '@zebras/qgc-share/service/Session'
|
||||
import domtoimage from 'dom-to-image'
|
||||
import { offset2, drawDotImg2, drawDotImg1, offset1, drawDotImg3, offset3, drawDotImg5, offset5 } from '@/utils/GisUrlList'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__lyConfigs?: {
|
||||
theme?: string;
|
||||
[key: string]: any; // 根据实际配置结构补充具体字段,或使用索引签名兼容其他属性
|
||||
};
|
||||
__mapMode?: string; // 建议同时声明代码中用到的其他全局变量
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 根据镜头高度获取地图级别
|
||||
* @param {Number} height
|
||||
@ -17,8 +25,8 @@ const D = -40467.74
|
||||
* @returns 以nameEn为下标的图例数据
|
||||
*/
|
||||
export const legendData2Obj = (data: any[]) => {
|
||||
const _tempData = {}
|
||||
const f = (_data) => {
|
||||
const _tempData: any = {}
|
||||
const f = (_data: any[]) => {
|
||||
_data.forEach((item) => {
|
||||
// childrenList有值表示这是一个分组
|
||||
if (item?.childrenList && item.childrenList?.length > 0) {
|
||||
@ -171,7 +179,7 @@ export const layerConfig2Flat = (data: any): any[] => {
|
||||
* @param config - 接口获取的配置数据
|
||||
* @returns 基础图层配置
|
||||
*/
|
||||
export const getMapConfig = (config) => {
|
||||
export const getMapConfig = () => {
|
||||
// const mapBaseUrls = MemoryCache.get('mapBaseUrls') || {}
|
||||
// const r = { ...config }
|
||||
// const { urlType } = r
|
||||
@ -193,7 +201,7 @@ export const getMapConfig = (config) => {
|
||||
*/
|
||||
export const resetMapElPos = () => {
|
||||
const legend = document.querySelector('#qgc-legendtl') as HTMLElement // 图例
|
||||
const filter = document.querySelector('#map-filter-container') as HTMLElement // 全局表单
|
||||
// const filter = document.querySelector('#map-filter-container') as HTMLElement // 全局表单
|
||||
const controller = document.querySelector('#map-controller') as HTMLElement // 地图工具栏
|
||||
const baselayer = document.querySelector('#map-baselayer') as HTMLElement // 底图模式切换
|
||||
|
||||
@ -217,7 +225,7 @@ export const resetMapElPos = () => {
|
||||
* @param position - 位置
|
||||
* @returns 布局数据列表
|
||||
*/
|
||||
const getListByPosition = (data, position: string) => data?.data?.filter((el) => el.position === position && el.code)
|
||||
const getListByPosition = (data: any, position: string) => data?.data?.filter((el: any) => el.position === position && el.code)
|
||||
|
||||
/**
|
||||
* 设置地图组件位置
|
||||
@ -225,7 +233,7 @@ const getListByPosition = (data, position: string) => data?.data?.filter((el) =>
|
||||
* @param data - 布局数据
|
||||
* @param offset - 偏移量
|
||||
*/
|
||||
export const setMapLegendPos = (layoutType: string, data, offset: number = 456) => {
|
||||
export const setMapLegendPos = (layoutType: string, data: any, offset: number = 456) => {
|
||||
const menuStateString = localStorage.getItem('menuState'); //处理澜沧江左侧菜单状态
|
||||
const menuState = menuStateString !== null ? JSON.parse(menuStateString) : true;
|
||||
const _theme = localStorage.getItem("ly-theme") || window.__lyConfigs?.theme
|
||||
@ -238,7 +246,7 @@ export const setMapLegendPos = (layoutType: string, data, offset: number = 456)
|
||||
const controller = document.querySelector('#map-controller') as HTMLElement // 地图工具栏
|
||||
const monitor = document.querySelector('#map-monitor') as HTMLElement // 地图工具栏
|
||||
const baselayer = document.querySelector('#map-baselayer') as HTMLElement // 底图模式切换
|
||||
const vd = document.querySelector('#vd_operate') as HTMLElement // 底部视频
|
||||
// const vd = document.querySelector('#vd_operate') as HTMLElement // 底部视频
|
||||
const left = ['layout1', 'layout2', 'layout3', 'layout4', 'layout6', 'layout7', 'layout8', 'layout9', 'layout10', 'layout11', 'layout14', 'layout15', 'layout16', 'layout17'] // 左侧布局
|
||||
const right = ['layout1', 'layout2', 'layout3', 'layout4', 'layout5', 'layout6', 'layout8', 'layout10', 'layout11', 'layout15', 'layout16', 'layout17'] // 右侧布局
|
||||
const bottom1 = ['layout1', 'layout6', 'layout8', 'layout9', 'layout10', 'layout16'] // 三行底部布局
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { layer, MapInterface } from "./map.d";
|
||||
import { MapLeaflet } from "./map.leaflet";
|
||||
// import { MapLeaflet } from "./map.leaflet";
|
||||
import { MapOl } from "./map.ol";
|
||||
|
||||
interface MapClassInterface extends MapInterface {
|
||||
@ -47,8 +47,9 @@ export class MapClass implements MapClassInterface {
|
||||
jdPanelControlShowAndHidden(baseid: String, isAll: boolean): void {
|
||||
this.service.jdPanelControlShowAndHidden(baseid, isAll)
|
||||
}
|
||||
mdLayerShowOrHidden(layerType: String, key?: String, baseid: String, checked: boolean, isAll: boolean): void {
|
||||
this.service.mdLayerShowOrHidden(layerType, key, baseid, checked, isAll)
|
||||
mdLayerShowOrHidden(): void {
|
||||
// layerType: String, key?: String, baseid: String, checked: boolean, isAll: boolean
|
||||
// this.service.mdLayerShowOrHidden(layerType, key, baseid, checked, isAll)
|
||||
}
|
||||
// 添加基础数据图层
|
||||
addBaseDataLayer(layer: any): void {
|
||||
@ -63,8 +64,8 @@ export class MapClass implements MapClassInterface {
|
||||
this.service.mdLayerTreeShowOrHidden(layerType, checked)
|
||||
}
|
||||
// 初始化加载描点数据
|
||||
addInitDataLayer = (pointData: any[], layerType: any, mdoptions?: MDOptions, legendArray?: any) => {
|
||||
return this.service.addInitDataLayer(pointData, layerType, mdoptions, legendArray)
|
||||
addInitDataLayer = (pointData: any[], layerType: any, mdoptions?: any) => {
|
||||
return this.service.addInitDataLayer(pointData, layerType, mdoptions)
|
||||
}
|
||||
//切换底图
|
||||
baseLayerSwitcher(key: string): void {
|
||||
@ -110,4 +111,19 @@ export class MapClass implements MapClassInterface {
|
||||
this.service.destroy()
|
||||
}
|
||||
|
||||
// 切换地图视图
|
||||
switchView(): void {
|
||||
// this.service.switchView()
|
||||
}
|
||||
|
||||
// 飞行到指定的点
|
||||
fitBounds(): void {
|
||||
// this.service.fitBounds(bounds)
|
||||
}
|
||||
|
||||
// 飞行到指定的点
|
||||
flyTopanto(): void {
|
||||
// this.service.flyTopanto(point)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
64
frontend/src/components/gis/map.d.ts
vendored
64
frontend/src/components/gis/map.d.ts
vendored
@ -8,7 +8,8 @@ export type layerType =
|
||||
| "arcgisFeature"
|
||||
| "dynamicMapLayer"
|
||||
| "arcgisMap"
|
||||
| "label";
|
||||
| "label"
|
||||
| "vector";
|
||||
export type layerOption = {
|
||||
opacity?: number;
|
||||
data?: Array<any>;
|
||||
@ -44,6 +45,7 @@ export interface layer {
|
||||
layerType?: string
|
||||
matrixIds_index?: string[]
|
||||
tileMatrixSetID?: string
|
||||
urlType: string
|
||||
}
|
||||
|
||||
export interface MapInterface {
|
||||
@ -94,35 +96,35 @@ export interface MapInterface {
|
||||
//飞行到指定的点
|
||||
fitBounds(bbox,bearing):void
|
||||
flyTopanto(positon,zoom,stcd):void
|
||||
/**
|
||||
* 加载倾斜摄影数据
|
||||
* @param HH3DUrlArray
|
||||
*/
|
||||
addQxsyLayer(HH3DUrlArray: siteItem): void
|
||||
// /**
|
||||
// * 加载倾斜摄影数据
|
||||
// * @param HH3DUrlArray
|
||||
// */
|
||||
// addQxsyLayer(HH3DUrlArray: siteItem): void
|
||||
|
||||
/**
|
||||
* 移除倾斜摄影数据
|
||||
* @param HH3DUrlArray
|
||||
*/
|
||||
removeQxsyLayer(HH3DUrlArray: siteItem): void
|
||||
// /**
|
||||
// * 移除倾斜摄影数据
|
||||
// * @param HH3DUrlArray
|
||||
// */
|
||||
// removeQxsyLayer(HH3DUrlArray: siteItem): void
|
||||
|
||||
/**
|
||||
* 倾斜摄影定位
|
||||
* @param HH3DUrlArray
|
||||
*/
|
||||
qxsyToPosition(HH3DUrlArray: siteItem): void
|
||||
// /**
|
||||
// * 倾斜摄影定位
|
||||
// * @param HH3DUrlArray
|
||||
// */
|
||||
// qxsyToPosition(HH3DUrlArray: siteItem): void
|
||||
|
||||
/**
|
||||
* 倾斜摄影裁剪
|
||||
* @param HH3DUrlArray
|
||||
*/
|
||||
qxsyClipBoundary(HH3DUrlArray: siteItem): void
|
||||
// /**
|
||||
// * 倾斜摄影裁剪
|
||||
// * @param HH3DUrlArray
|
||||
// */
|
||||
// qxsyClipBoundary(HH3DUrlArray: siteItem): void
|
||||
|
||||
/**
|
||||
* 移除倾斜摄影裁剪
|
||||
* @param HH3DUrlArray
|
||||
*/
|
||||
removeQxsyClipBoundary(HH3DUrlArray: siteItem): void
|
||||
// /**
|
||||
// * 移除倾斜摄影裁剪
|
||||
// * @param HH3DUrlArray
|
||||
// */
|
||||
// removeQxsyClipBoundary(HH3DUrlArray: siteItem): void
|
||||
|
||||
/**
|
||||
* 基地面板控制
|
||||
@ -137,8 +139,8 @@ export interface MapInterface {
|
||||
* @param checked
|
||||
* @param isAll
|
||||
*/
|
||||
mdLayerShowOrHidden(layerType: String, key?: String, baseid: String, checked: boolean, isAll: boolean): void
|
||||
|
||||
mdLayerShowOrHidden(): void
|
||||
// layerType: String, key?: String, baseid: String, checked: boolean, isAll: boolean
|
||||
/**
|
||||
* 基础图层显示影隐藏方法
|
||||
* @param layerType
|
||||
@ -182,10 +184,4 @@ export interface MapInterface {
|
||||
* 移除地图对象
|
||||
*/
|
||||
destroy(): void
|
||||
|
||||
setVisibleDistanceOfLable(): void
|
||||
|
||||
recoverVisibleDistanceOfLable(): viod
|
||||
|
||||
changeMaskStyle(options?: any)
|
||||
}
|
||||
|
||||
@ -1,373 +1,440 @@
|
||||
import { layer, MapInterface } from "./map";
|
||||
import * as L from "leaflet";
|
||||
import * as esriLeaflet from "esri-leaflet";
|
||||
import { mapServerBaseUrl } from "./map.class";
|
||||
// @ts-ignore
|
||||
import axios from "axios";
|
||||
// import "@/components/thematicMap/leaflet/leaflet.inflatable-markers-group.js"
|
||||
// import { MapInterface } from './map';
|
||||
// import * as L from 'leaflet';
|
||||
// import * as esriLeaflet from 'esri-leaflet';
|
||||
// import '@/utils/leaflet/leaflet-tilelayer-wmts-src.js';
|
||||
// import { mapServerBaseUrl } from './map.class';
|
||||
// import '@/utils/leaflet/leaflet.inflatable-markers-group.js';
|
||||
// import {MDOptions} from './map.class';
|
||||
// import { getIconPath } from "@/utils/index";
|
||||
// // @ts-ignore
|
||||
// import axios from 'axios';
|
||||
// // import "@/components/thematicMap/leaflet/leaflet.inflatable-markers-group.js"
|
||||
|
||||
const tiledMapGroup = L.layerGroup();
|
||||
const chartMapGroup = L.layerGroup();
|
||||
const overlayGroup = L.layerGroup();
|
||||
const CENTER_positionCN: any = [37.072654, 86.171125]; // [26.072654, 107.171125]; //中心纬经度 中国
|
||||
// const tiledMapGroup = L.layerGroup();
|
||||
// const chartMapGroup = L.layerGroup();
|
||||
// const overlayGroup = L.layerGroup();
|
||||
// const CENTER_positionCN: any = [38, 114.17112499999996]; //中心纬经度 中国
|
||||
// const basinCenter = {
|
||||
// DA_HHGLSX: [103.343357, 35.931812],
|
||||
// FA_CJGLSX: [111.001911, 30.821327]
|
||||
// };
|
||||
// let boundCavansLayer: any = null;
|
||||
// let ganliulist: any = [];
|
||||
// export class MapLeaflet implements MapInterface {
|
||||
// map: any = null;
|
||||
// htmlMakerLayer: any = [];
|
||||
// defaultScale = 10;
|
||||
// minimumZoom = 7;
|
||||
// setDrawPlug: any = null;
|
||||
// layermarkers: any = [];
|
||||
// rainlayerslist: any = [];
|
||||
// private layerRegistry: Map<string, any> = new Map(); // ✅ 新增:存储 key -> layer 实例
|
||||
|
||||
let boundCavansLayer: any = null;
|
||||
let ganliulist:any = []
|
||||
export class MapLeaflet implements MapInterface {
|
||||
map: any = null;
|
||||
htmlMakerLayer: any = [];
|
||||
minimumZoom = 7;
|
||||
// private currentBaseLayerKey: string | null = null; // ✅ 新增:记录当前激活的底图 Key
|
||||
|
||||
temperatureMapObj: any = [];
|
||||
// temperatureMapObj: any = [];
|
||||
// //地图初始化
|
||||
// init(container: HTMLElement, rectangle?: any): Promise<any> {
|
||||
// try {
|
||||
// console.log('init初始化container', container);
|
||||
// var corner1 = L.latLng(55.35715491537772, 140.7821677051657);
|
||||
// var corner2 = L.latLng(0.975580441812298, 67.56008229483018);
|
||||
// var bounds = L.latLngBounds(corner2, corner1);
|
||||
// const map = L.map(container as any, {
|
||||
// preferCanvas: true,
|
||||
// zoom: 4.5,
|
||||
// minZoom: 4.23,
|
||||
// maxZoom: 22, // 【修改点1】增大最大缩放级别,允许用户继续滚轮放大
|
||||
// maxNativeZoom: 12,
|
||||
// zoomSnap: 0.1, // 【修改点2】让缩放更平滑,不强制对齐整数
|
||||
// zoomDelta: 0.5,
|
||||
// center: CENTER_positionCN,
|
||||
// attributionControl: false,
|
||||
// zoomControl: false,
|
||||
// trackResize: true,
|
||||
// maxBounds: bounds,
|
||||
// maxBoundsViscosity: 1.0,
|
||||
// wheelPxPerZoomLevel: 180,
|
||||
// });
|
||||
// // ✅ 新增:可视化显示 maxBounds 范围
|
||||
// // L.rectangle(bounds, {color: "#ff7800", weight: 3, opacity: 0.8, fillOpacity: 0.1})
|
||||
// // .addTo(map)
|
||||
// // .bindPopup("这是 maxBounds 的范围");
|
||||
// this.map = map;
|
||||
// this.map.on('zoomend', (e: any) => {
|
||||
// console.log('当前缩放级别', e.target.getZoom());
|
||||
// });
|
||||
// return Promise.resolve(map);
|
||||
// } catch (e) {
|
||||
// console.log('测试', e);
|
||||
// return Promise.reject({});
|
||||
// }
|
||||
// }
|
||||
// addBaseDataLayer(layer: any): any {
|
||||
// // The WMTS URL
|
||||
// console.log(layer);
|
||||
// switch (layer.type) {
|
||||
// case 'wmts':
|
||||
// if (layer.url) {
|
||||
// console.log('https://211.99.26.225:18085' + layer.url);
|
||||
// if (layer) {
|
||||
// var ignLayer = L.tileLayer
|
||||
// .wmts('https://211.99.26.225:18085/geoserver/gwc/service/wmts', {
|
||||
// tileMatrixSet: 'EPSG:3857_qgc_qsj_arcgistiles_l13',
|
||||
// tileSize: 256, //切片大小
|
||||
// maxZoom: 13,
|
||||
// noWrap: true,
|
||||
// opacity: 0.99,
|
||||
// minZoom: 4,
|
||||
// styles:{
|
||||
// abc:123
|
||||
// },
|
||||
// layer: 'qgc_qsj_arcgistiles_l13'
|
||||
// })
|
||||
// .addTo(this.map)
|
||||
// .bringToBack();
|
||||
// const registryKey = layer.key || layer.title;
|
||||
// if (registryKey) {
|
||||
// this.layerRegistry.set(registryKey, ignLayer);
|
||||
// }
|
||||
// layer._layer = ignLayer;
|
||||
// layer._layer.layerGroup = overlayGroup;
|
||||
// }
|
||||
// }
|
||||
// return layer;
|
||||
|
||||
addLayer(layer: any): any {
|
||||
// The WMTS URL
|
||||
console.warn(layer)
|
||||
switch (layer.type) {
|
||||
case "tiledMap":
|
||||
if (layer.url) {
|
||||
let setzIndex = 1;
|
||||
if (layer.zIndex && layer.zIndex != "" && layer.zIndex != "0") {
|
||||
setzIndex = Number(layer.zIndex);
|
||||
}
|
||||
if (layer.title !== '干流河流') {
|
||||
layer._layer = esriLeaflet
|
||||
.dynamicMapLayer({
|
||||
url: layer.url,
|
||||
opacity: layer.opacity,
|
||||
zIndex: 999999999999
|
||||
})
|
||||
?.addTo(this.map)
|
||||
.bringToBack()
|
||||
} else {
|
||||
axios.get('http://210.72.227.199:18084/geoserver/cite/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=cite:glhl_epsg4326&maxFeatures=50&outputFormat=application%2Fjson')
|
||||
.then((res) => {
|
||||
res.data?.features.map((item: any, index: number) => {
|
||||
const a = L.geoJSON(item, {
|
||||
style: {
|
||||
},
|
||||
}).addTo(this.map);
|
||||
ganliulist.push(a)
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
break
|
||||
case "geoserver-wmts":
|
||||
if (layer.url) {
|
||||
if (layer.title == "地图瓦片图层") {
|
||||
console.log('地图瓦片')
|
||||
//增加遮罩
|
||||
axios.get('/zfile/qgcbuji/overlayLand3.json')
|
||||
.then((res) => {
|
||||
var landLayer = L.geoJson(res.data, {
|
||||
style: {
|
||||
opacity: 0,
|
||||
//填充透明度
|
||||
fillOpacity: 1,
|
||||
//填充颜色
|
||||
fillColor: '#ececec'
|
||||
}
|
||||
})?.addTo(overlayGroup).bringToBack();
|
||||
})
|
||||
axios.get('/zfile/qgcbuji/overlayOcean.json')
|
||||
.then((res) => {
|
||||
var oceanLayer = L.geoJson(res.data, {
|
||||
style: {
|
||||
opacity: 0,
|
||||
//填充透明度
|
||||
fillOpacity: 1,
|
||||
//填充颜色
|
||||
fillColor: '#8eccf1'
|
||||
}
|
||||
})?.addTo(overlayGroup).bringToFront();
|
||||
})
|
||||
overlayGroup?.addTo(this.map);
|
||||
}
|
||||
var optionobj = JSON.parse(layer.options);
|
||||
if (optionobj) {
|
||||
var ignLayer = L.tileLayer
|
||||
.wmts(layer.url, {
|
||||
tileMatrixSet: optionobj.tileMatrixSet, //"EPSG:3857_qgc_arcgistiles_l13_google",
|
||||
tileSize: 256, //切片大小
|
||||
maxZoom: 13,
|
||||
noWrap: true,
|
||||
opacity: 1.01,
|
||||
minZoom: 4,
|
||||
layer: optionobj.layer, //"qgc_arcgistiles_l13_google",
|
||||
})
|
||||
.addTo(this.map)
|
||||
.bringToBack();
|
||||
layer._layer = ignLayer;
|
||||
layer._layer.layerGroup = overlayGroup;
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
case "arcgisMap":
|
||||
if (layer.url) {
|
||||
if (this.map) {
|
||||
const mapLayer = esriLeaflet
|
||||
.tiledMapLayer({
|
||||
url: layer.url,
|
||||
format: "image/png",
|
||||
maxNativeZoom: 17,
|
||||
})
|
||||
.addTo(this.map)
|
||||
.bringToBack();
|
||||
layer._layer = mapLayer;
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
case "tiledMapQuery":
|
||||
if (layer.url && layer.id) {
|
||||
if (layer.title == "岛屿") {
|
||||
layer.layers = "cite:nhqddt_epsg3857";
|
||||
}
|
||||
layer._layer = L.tileLayer
|
||||
.wms(layer.url, {
|
||||
id: layer.id,
|
||||
format: "image/png",
|
||||
transparent: true,
|
||||
zIndex: 9999,
|
||||
layers: layer.layers,
|
||||
// maxZoom:10,
|
||||
})
|
||||
.addTo(this.map);
|
||||
// .bringToBack();
|
||||
return layer;
|
||||
}
|
||||
return layer;
|
||||
case "label":
|
||||
if (layer.url) {
|
||||
const tileLayer: any = L.tileLayer(layer.url, {
|
||||
subdomains: ["0", "1", "2", "3", "4", "5", "6", "7"],
|
||||
crossOrigin: "anonymous",
|
||||
});
|
||||
const boundary = (providerName: any) => {
|
||||
// return BoundaryCanvas(tileLayer, {});
|
||||
};
|
||||
let boundaryLayers;
|
||||
if (layer.title == "行政标注") {
|
||||
boundaryLayers = boundary(tileLayer._leaflet_id)
|
||||
.addTo(this.map)
|
||||
.bringToBack()
|
||||
.setZIndex(10);
|
||||
} else {
|
||||
boundaryLayers = boundary(tileLayer._leaflet_id).addTo(this.map);
|
||||
}
|
||||
tileLayer.boundLayer = boundaryLayers;
|
||||
layer._layer = boundaryLayers;
|
||||
return layer;
|
||||
}
|
||||
break;
|
||||
// case 'markers':
|
||||
// this.getRain(layer);
|
||||
// break;
|
||||
// default: {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
case "markers":
|
||||
this.getRain(layer);
|
||||
break;
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// baseLayerSwitcher(key: string) {
|
||||
// if (!this.map) return;
|
||||
|
||||
// console.log('切换底图 key:', key);
|
||||
|
||||
/**
|
||||
* 用于消除瓦片之间的缝隙(leaflet加载wmts在非整数缩放时瓦片间会有缝隙)
|
||||
*/
|
||||
inittiles() {
|
||||
var originalInitTile = L.GridLayer.prototype._initTile;
|
||||
L.GridLayer.include({
|
||||
_initTile: function (tile: any) {
|
||||
originalInitTile.call(this, tile);
|
||||
// // 1. 如果当前已有底图且不是同一个,先移除
|
||||
// if (this.currentBaseLayerKey && this.currentBaseLayerKey !== key) {
|
||||
// const oldLayer = this.layerRegistry.get(this.currentBaseLayerKey);
|
||||
// if (oldLayer && this.map.hasLayer(oldLayer)) {
|
||||
// this.map.removeLayer(oldLayer);
|
||||
// console.log(`已移除旧底图: ${this.currentBaseLayerKey}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
var tileSize = this.getTileSize();
|
||||
// // 2. 检查新底图是否已存在 registry 中
|
||||
// let newLayer = this.layerRegistry.get(key);
|
||||
|
||||
tile.style.width = tileSize.x + 2 + "px";
|
||||
tile.style.height = tileSize.y + 2 + "px";
|
||||
},
|
||||
});
|
||||
}
|
||||
init(container: React.ReactNode, rectangle?: any): Promise<any> {
|
||||
try {
|
||||
console.log("init初始化container", container);
|
||||
this.inittiles();
|
||||
// var corner1 = L.latLng(0.712, -10.227),
|
||||
// corner2 = L.latLng(70.774, 204.125),
|
||||
// // 3. 如果不存在,则创建并添加到地图和 registry
|
||||
// if (!newLayer) {
|
||||
// newLayer = this.createBaseLayerByKey(key);
|
||||
// if (newLayer) {
|
||||
// this.layerRegistry.set(key, newLayer);
|
||||
// newLayer.addTo(this.map);
|
||||
// // 确保底图在最底层
|
||||
// newLayer.bringToBack();
|
||||
// }
|
||||
// } else {
|
||||
// // 4. 如果已存在,直接添加(如果未添加)
|
||||
// if (!this.map.hasLayer(newLayer)) {
|
||||
// newLayer.addTo(this.map);
|
||||
// newLayer.bringToBack();
|
||||
// }
|
||||
// }
|
||||
|
||||
// var corner1 = L.latLng(-50.712, -10.227),
|
||||
// corner2 = L.latLng(100.774, 204.125),
|
||||
// var corner1 = L.latLng(20.712, 50.227),
|
||||
// corner2 = L.latLng(51.774, 160.125),
|
||||
var corner1 = L.latLng(20.712, 0),
|
||||
corner2 = L.latLng(51.774, 179.125),
|
||||
bounds = L.latLngBounds(corner1, corner2);
|
||||
const map = L.map(container as any, {
|
||||
preferCanvas: true,
|
||||
minZoom: 4.4,
|
||||
center: CENTER_positionCN,
|
||||
zoom: 4.4,
|
||||
maxZoom: 18,
|
||||
zoomControl: false,
|
||||
maxBounds: bounds,
|
||||
zoomSnap: 0,
|
||||
crs: L.CRS.EPSG3857,
|
||||
});
|
||||
this.map = map;
|
||||
// // 5. 更新当前激活的 Key
|
||||
// if (newLayer) {
|
||||
// this.currentBaseLayerKey = key;
|
||||
// }
|
||||
// }
|
||||
|
||||
//遮罩
|
||||
var pNW = { lat: 85.0, lng: -90.0 };
|
||||
var pNE = { lat: 85.0, lng: 270.0 };
|
||||
var pSE = { lat: -45.0, lng: 270.0 };
|
||||
var pSW = { lat: -45.0, lng: -90.0 };
|
||||
var pArray = [];
|
||||
pArray.push(pNW);
|
||||
pArray.push(pSW);
|
||||
pArray.push(pSE);
|
||||
pArray.push(pNE);
|
||||
pArray.push(pNW);
|
||||
// /**
|
||||
// * 根据 Key 创建具体的 Leaflet 图层实例
|
||||
// */
|
||||
// createBaseLayerByKey(key: string): L.Layer | null {
|
||||
// console.log(key)
|
||||
// const tdtToken = 'e90d56e5a09d1767899ad45846b0cefd' //企业版密钥,勿换e650f138c4481cca888cd13094bb9026
|
||||
// const mapType = 'w' //c:天地图经纬度底图;w:天地图墨卡托底图
|
||||
// const URL_TerTDT = `https://t0.tianditu.gov.cn/ter_${mapType}/wmts?tk=${tdtToken}` //terMap
|
||||
// switch (key) {
|
||||
// case 's_province_boundaries':
|
||||
// // 假设这是之前的 WMTS 矢量图层
|
||||
// // 注意:原代码中硬编码了 URL 和 layer 名称,这里需要确保与实际服务对应
|
||||
// return L.tileLayer.wmts('https://211.99.26.225:18085/geoserver/gwc/service/wmts', {
|
||||
// tileMatrixSet: 'EPSG:3857_qgc_qsj_arcgistiles_l13',
|
||||
// tileSize: 256,
|
||||
// // maxZoom: 13,
|
||||
// noWrap: true,
|
||||
// opacity: 1,
|
||||
// minZoom: 4,
|
||||
// zIndex: 1,
|
||||
// layer: 'qgc_qsj_arcgistiles_l13', // 请确认此 layer 名称是否对应“矢量”
|
||||
// format: 'image/png'
|
||||
// });
|
||||
|
||||
let mapURL =
|
||||
mapServerBaseUrl + "arcgis/rest/services/zh_boundary/MapServer/0/query";
|
||||
esriLeaflet
|
||||
.query({
|
||||
url: mapURL,
|
||||
})
|
||||
// .where(mapWhere)
|
||||
.run((error: any, result: any, response: any) => {
|
||||
if (response) {
|
||||
// let test = arcgisToGeoJSON(response);
|
||||
// let chinaList = test.features[0].geometry.coordinates;
|
||||
// var boundsCounty = [
|
||||
// [0.0, -20.0],
|
||||
// [276.0, -20.0],
|
||||
// [276.0, 75.0],
|
||||
// [0.0, 75.0],
|
||||
// [0.0, -20.0],
|
||||
// ];
|
||||
// console.log("chinaList", chinaList);
|
||||
// case 'BASEMAP-white':
|
||||
// // 地形图示例 (使用 OpenStreetMap 或其他地形服务作为占位,请替换为实际地址)
|
||||
// return L.tileLayer(`${URL_TerTDT}&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=${mapType}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles`, {
|
||||
// // maxZoom: 17,
|
||||
// format: "image/png",
|
||||
// zIndex: 12,
|
||||
// attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
|
||||
// });
|
||||
|
||||
// var geojsonRelt: any = {
|
||||
// type: "Feature",
|
||||
// properties: {},
|
||||
// geometry: {
|
||||
// type: "MultiPolygon",
|
||||
// coordinates: [[boundsCounty, ...chinaList]],
|
||||
// },
|
||||
// };
|
||||
// case 'BASEMAP-img':
|
||||
// // 影像图示例 (使用 Esri World Imagery 作为占位,请替换为实际地址)
|
||||
// return L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
// attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
// });
|
||||
|
||||
// var chinaCavansLayer = L.geoJSON(geojsonRelt, {
|
||||
// style: {
|
||||
// color: "#8EA5BA",
|
||||
// weight: 1,
|
||||
// fillColor: "#ffffff",
|
||||
// fillOpacity: 1,
|
||||
// },
|
||||
// });
|
||||
//chinaCavansLayer.addTo(this.map).bringToBack().setZIndex(0);
|
||||
}
|
||||
});
|
||||
// default:
|
||||
// console.warn(`未知的底图 Key: ${key}`);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
let params = {
|
||||
startTime: "2022-01-01 00:00:00",
|
||||
};
|
||||
// controlBaseLayerTreeShowAndHidden(layerType: String, key: String, checked: boolean) {
|
||||
// // 优先使用 key 查找,如果没有 key 则尝试用 layerType
|
||||
// // console.log(this.getAllLayers())
|
||||
// console.log(key)
|
||||
// console.log(this.layerRegistry)
|
||||
// const registryKey:any = key || layerType;
|
||||
// const layerInstance = this.layerRegistry.get(registryKey);
|
||||
// if (layerInstance) {
|
||||
// if (checked) {
|
||||
// // 显示:如果不在地图上,则添加
|
||||
// if (!this.map.hasLayer(layerInstance)) {
|
||||
// layerInstance.addTo(this.map);
|
||||
// }
|
||||
// } else {
|
||||
// // 隐藏:如果在地图上,则移除
|
||||
// if (this.map.hasLayer(layerInstance)) {
|
||||
// this.map.removeLayer(layerInstance);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// console.warn(`未找到标识为 ${registryKey} 的图层实例`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 添加初始化数据图层
|
||||
// * @param pointLayerObj 图层配置对象 (包含 data, key, checked 等属性)
|
||||
// * @param layerType 图层类型/Key (如果 pointLayerObj 中有 key,则优先使用 obj.key)
|
||||
// * @param mdoptions 描点选项配置
|
||||
// * @param legendArray 图例映射数据 (用于根据 anchoPointState 匹配图标)
|
||||
// */
|
||||
// addInitDataLayer = (pointLayerObj: any, layerType?: any, mdoptions?: MDOptions, legendArray?: any) => {
|
||||
// // 1. 参数校验与数据提取
|
||||
// if (!this.map || !pointLayerObj) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
L.control
|
||||
.scale({ maxWidth: 150, metric: true, imperial: false })
|
||||
.addTo(this.map);
|
||||
this.map.on("zoomend", (e: any) => {
|
||||
var scale = document.getElementsByClassName(
|
||||
"leaflet-control-scale-line"
|
||||
)[0].innerHTML;
|
||||
var num = parseInt(scale);
|
||||
// let dataArray: any[] = [];
|
||||
// let targetLayerKey: string = layerType;
|
||||
|
||||
if (scale.includes("km")) {
|
||||
console.log("比例尺:1:" + num * 1000 + "m");
|
||||
} else {
|
||||
console.log("比例尺1:" + num + "m");
|
||||
}
|
||||
console.log("scale", scale);
|
||||
// if (Array.isArray(pointLayerObj)) {
|
||||
// dataArray = pointLayerObj;
|
||||
// } else {
|
||||
// dataArray = pointLayerObj.data || [];
|
||||
// targetLayerKey = pointLayerObj.key || layerType;
|
||||
// }
|
||||
|
||||
console.log("当前缩放级别", e.target.getZoom());
|
||||
});
|
||||
console.log("rectangle", rectangle);
|
||||
if (rectangle) {
|
||||
if (rectangle[0] && rectangle[0][0] == 90) {
|
||||
//this.map.setView([43.719771, 126.687641,], 9);
|
||||
} else {
|
||||
map.fitBounds(rectangle);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(map);
|
||||
} catch (e) {
|
||||
console.log("测试", e);
|
||||
return Promise.reject({});
|
||||
}
|
||||
}
|
||||
// if (!targetLayerKey) {
|
||||
// console.warn('缺少图层 Key,无法加载描点');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (dataArray.length === 0) {
|
||||
// const existingGroup = this.layerRegistry.get(targetLayerKey);
|
||||
// if (existingGroup && existingGroup instanceof L.LayerGroup) {
|
||||
// existingGroup.clearLayers();
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
removeLayer(layer: layer | any): void {
|
||||
switch (layer.type) {
|
||||
case "geoJson":
|
||||
break;
|
||||
default:
|
||||
if (layer._layer) {
|
||||
if (layer._layer.layerGroup) {
|
||||
layer._layer.layerGroup.clearLayers();
|
||||
}
|
||||
this.map?.off("zoom", layer._layer.func);
|
||||
this.map && this.map.removeLayer(layer._layer);
|
||||
} else if (layer) {
|
||||
if (layer.layerGroup) {
|
||||
layer.layerGroup.clearLayers();
|
||||
}
|
||||
this.map?.off("zoom", layer.func);
|
||||
this.map && this.map.removeLayer(layer);
|
||||
let mapvalue = tiledMapGroup.getLayers();
|
||||
// console.log(`开始加载图层 [${targetLayerKey}] 的描点,数量: ${dataArray.length}`);
|
||||
|
||||
for (let i = 0; i < mapvalue.length; i++) {
|
||||
if (mapvalue[i].options.id == layer.id) {
|
||||
tiledMapGroup.removeLayer(mapvalue[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// // 2. 获取或创建该图层的 LayerGroup
|
||||
// let layerGroup = this.layerRegistry.get(targetLayerKey);
|
||||
|
||||
addboundCavansLayer = (list: any, bgStyle?: any) => {
|
||||
if (boundCavansLayer && this.map.hasLayer(boundCavansLayer)) {
|
||||
this.map.removeLayer(boundCavansLayer);
|
||||
}
|
||||
var boundsCounty = [
|
||||
[0.0, -20.0],
|
||||
[176.0, -20.0],
|
||||
[176.0, 75.0],
|
||||
[0.0, 75.0],
|
||||
[0.0, -20.0],
|
||||
];
|
||||
var geojsonRelt: any = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "MultiPolygon",
|
||||
coordinates: [[boundsCounty, ...list]],
|
||||
},
|
||||
};
|
||||
var boundCavansLayerRelt = L.geoJSON(
|
||||
geojsonRelt,
|
||||
bgStyle
|
||||
? { ...bgStyle }
|
||||
: {
|
||||
color: "transparent",
|
||||
fillColor: "#0a2b4b",
|
||||
fillOpacity: 0.9,
|
||||
}
|
||||
);
|
||||
boundCavansLayer = boundCavansLayerRelt;
|
||||
console.warn(boundCavansLayerRelt.addTo)
|
||||
boundCavansLayerRelt.addTo(this.map).bringToFront().setZIndex(99999999999);
|
||||
};
|
||||
// const shouldClear = mdoptions?.isRemove !== false;
|
||||
|
||||
}
|
||||
// if (!layerGroup || !(layerGroup instanceof L.LayerGroup)) {
|
||||
// if (layerGroup && this.map.hasLayer(layerGroup)) {
|
||||
// this.map.removeLayer(layerGroup);
|
||||
// }
|
||||
// layerGroup = L.layerGroup();
|
||||
// this.layerRegistry.set(targetLayerKey, layerGroup);
|
||||
// layerGroup.addTo(this.map);
|
||||
// } else {
|
||||
// if (shouldClear) {
|
||||
// layerGroup.clearLayers();
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 3. 遍历数据生成 Marker
|
||||
// dataArray.forEach((item: any) => {
|
||||
// const { lgtd, lttd, stcd, stnm, iconCode, anchoPointState, _id, titleName, ennm } = item;
|
||||
|
||||
// if (lgtd == null || lttd == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 4. 确定图标样式和文字
|
||||
// let iconUrl = '';
|
||||
// let iconSize = [15, 15];
|
||||
|
||||
// // 获取图标 URL
|
||||
// if (iconCode) {
|
||||
// iconUrl = getIconPath(iconCode);
|
||||
// } else if (anchoPointState && legendArray) {
|
||||
// const legendItem = legendArray[anchoPointState];
|
||||
// if (legendItem && legendItem.icon) {
|
||||
// iconUrl = getIconPath(legendItem.icon);
|
||||
// if (legendItem.width && legendItem.height) {
|
||||
// iconSize = [legendItem.width, legendItem.height];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 如果没有找到图标,使用默认或跳过
|
||||
// if (!iconUrl) {
|
||||
// // 可以选择使用默认图标或者继续
|
||||
// iconUrl = getIconPath('default'); // 假设有一个默认图标
|
||||
// if(!iconUrl) return;
|
||||
// }
|
||||
|
||||
// // 5. 创建带文字的 DivIcon
|
||||
// // 显示的文字优先使用 titleName,其次 ennm,最后 stnm
|
||||
// const labelText = titleName || ennm || stnm || '';
|
||||
|
||||
// // 构建 HTML 结构:一个容器包含图片和文字
|
||||
// // 注意:这里使用内联样式简单演示,建议在实际项目中提取到 CSS 类中
|
||||
// const iconHtml = `
|
||||
// <div style="position: relative; display: flex; flex-direction: column; align-items: center; width: max-content;">
|
||||
// <div style="
|
||||
// font: 10px sans-serif;
|
||||
// font-weight: bold;
|
||||
// color: #fff;
|
||||
// white-space: nowrap;
|
||||
// margin-bottom: 2px;
|
||||
// -webkit-text-stroke: 2px #2e2d2d;
|
||||
// paint-order: stroke fill; /* 确保描边在填充外部,防止文字变细 */
|
||||
|
||||
// ">
|
||||
// ${labelText}
|
||||
// </div>
|
||||
// <img src="${iconUrl}" style="width: ${iconSize[0]}px; height: ${iconSize[1]}px; display: block;" />
|
||||
// </div>`;
|
||||
|
||||
// const customIcon = L.divIcon({
|
||||
// html: iconHtml,
|
||||
// className: '', // 重要:设置为空字符串以避免 Leaflet 默认样式干扰
|
||||
// iconSize: [iconSize[0], iconSize[1] + 20], // 高度增加以容纳文字 (假设文字高约20px)
|
||||
// iconAnchor: [iconSize[0] / 2, iconSize[1] + 10], // 锚点设在图片底部中心
|
||||
// popupAnchor: [0, -(iconSize[1] + 20)] // 弹窗锚点设在整体顶部
|
||||
// });
|
||||
|
||||
// // 6. 创建 Marker
|
||||
// const marker = L.marker([lttd, lgtd], {
|
||||
// icon: customIcon,
|
||||
// // @ts-ignore
|
||||
// options: {
|
||||
// ...item,
|
||||
// layerKey: targetLayerKey,
|
||||
// originalEvent: item
|
||||
// }
|
||||
// });
|
||||
|
||||
// // 7. 绑定点击事件 (如果需要)
|
||||
// marker.on('click', (e: any) => {
|
||||
// // 触发全局事件,例如:
|
||||
// // GlobalEvents.get('map_dataLayer_click').set(item);
|
||||
// console.log('Marker clicked:', item);
|
||||
// });
|
||||
|
||||
// // 8. 将 Marker 添加到 LayerGroup
|
||||
// layerGroup.addLayer(marker);
|
||||
// });
|
||||
|
||||
// console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
||||
// }
|
||||
// getRain(data: any) {
|
||||
// if (data?.geojson) {
|
||||
// this.removeRainLayer();
|
||||
// this.removeLayermarkers();
|
||||
// const renderColor = (item: any, colRules: any) => {
|
||||
// let color = 'rgba(0,0,0,0)';
|
||||
// colRules.map((rule: any, index: number) => {
|
||||
// if (
|
||||
// item.properties.hvalue >= rule?.sv &&
|
||||
// item.properties.hvalue < rule?.ev
|
||||
// ) {
|
||||
// color = rule.colors;
|
||||
// }
|
||||
// if (index === colRules.length - 1) {
|
||||
// if (item.properties.hvalue >= rule?.ev) {
|
||||
// color = rule.colors;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// return color;
|
||||
// };
|
||||
|
||||
// data.geojson.features.map((item: any, index: number) => {
|
||||
// let pushlist = L.geoJSON(item, {
|
||||
// style: {
|
||||
// color: renderColor(item, data.colRules),
|
||||
// weight: 2,
|
||||
// opacity: 0.8,
|
||||
// fillOpacity: 0.7
|
||||
// // fillColor: "#1D91C0",
|
||||
// }
|
||||
// }).addTo(this.map);
|
||||
// this.layermarkers.push(pushlist);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// removeLayermarkers = () => {
|
||||
// // console.log('layermarkers',layermarkers);
|
||||
|
||||
// try {
|
||||
// if (this.layermarkers.length > 0) {
|
||||
// if (this.map.hasLayer(this.layermarkers)) {
|
||||
// this.map.removeLayer(this.layermarkers);
|
||||
// } else {
|
||||
// console.log('移除图层失败,应已经移除');
|
||||
// }
|
||||
// }
|
||||
// // removeAllGeojson(map);
|
||||
// } catch (e) {
|
||||
// // message.info('移除图层失败')
|
||||
// console.log('移除图层失败,是否应移除');
|
||||
// }
|
||||
// };
|
||||
|
||||
// removeRainLayer(): void {
|
||||
// console.log('test!删除Layer');
|
||||
// const _this = this;
|
||||
// if (this.rainlayerslist.length > 0) {
|
||||
// this.rainlayerslist.map((item: any) => {
|
||||
// _this.map.removeLayer(item);
|
||||
// });
|
||||
// this.rainlayerslist = [];
|
||||
// }
|
||||
// } // 缩放
|
||||
// zoomToggle(type: 'out' | 'in') {
|
||||
// if (this.map) {
|
||||
// if (type === 'out') {
|
||||
// this.map && this.map.zoomOut();
|
||||
// } else {
|
||||
// this.map && this.map.zoomIn();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -12,11 +12,11 @@ import Stroke from 'ol/style/Stroke';
|
||||
import Icon from 'ol/style/Icon';
|
||||
import Text from 'ol/style/Text';
|
||||
|
||||
import LayerGroup from 'ol/layer/Group';
|
||||
import OSM from 'ol/source/OSM';
|
||||
// import LayerGroup from 'ol/layer/Group';
|
||||
// import OSM from 'ol/source/OSM';
|
||||
import WMTS from 'ol/source/WMTS';
|
||||
import WMTSTileGrid from 'ol/tilegrid/WMTS';
|
||||
import { get as getProjection, fromLonLat, toLonLat } from 'ol/proj';
|
||||
import { get as getProjection, fromLonLat } from 'ol/proj';
|
||||
import {
|
||||
defaults as defaultInteractions,
|
||||
Draw,
|
||||
@ -25,13 +25,11 @@ import {
|
||||
import { getTopLeft, getWidth } from 'ol/extent';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
import { servers } from './mapurlManage';
|
||||
import geoJsonData from '@/assets/geoJson.json';
|
||||
import geoJsonData1 from '@/assets/geoJson1.json';
|
||||
import { XYZ } from 'ol/source';
|
||||
import Feature from 'ol/Feature';
|
||||
import Point from 'ol/geom/Point'; // ✅ 新增导入
|
||||
import LineString from 'ol/geom/LineString';
|
||||
import Polygon from 'ol/geom/Polygon';
|
||||
// import Polygon from 'ol/geom/Polygon';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import {
|
||||
getLength as getSphericalLength,
|
||||
@ -58,20 +56,41 @@ export class MapOl implements MapInterface {
|
||||
private layerRegistry: Map<string, any> = new Map();
|
||||
// ✅ 新增:记录当前正在显示的“切换 Key”,用于判断是否需要重新加载或只是切换显隐
|
||||
private baseLayerConfig: TileLayer | null = null;
|
||||
private hydropBaseConfig: TileLayer | null = null;
|
||||
// private hydropBaseConfig: TileLayer | null = null;
|
||||
private REGISTRY_KEY = 'customBaseLayer';
|
||||
|
||||
// ✅ 新增:量算相关属性
|
||||
private drawInteraction: any | null = null;
|
||||
private drawInteraction: any = null;
|
||||
private measureLayer: VectorLayer | null = null;
|
||||
private measureSource: VectorSource | null = null;
|
||||
// ✅ 新增:存储点图层的 Registry,key 为 layerType,value 为 VectorLayer
|
||||
private pointLayerRegistry: Map<string, VectorLayer> = new Map();
|
||||
geoJsonData: any = null;
|
||||
geoJsonData1: any = null;
|
||||
|
||||
private BASEID: string = '01';
|
||||
|
||||
// private BASEID: string = '01';
|
||||
constructor() {
|
||||
this.loadGeoJsonData();
|
||||
this.loadGeoJsonData1();
|
||||
}
|
||||
private async loadGeoJsonData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/data/geoJson.json')
|
||||
this.geoJsonData = await response.json()
|
||||
} catch (error) {
|
||||
console.error('配置加载失败:', error)
|
||||
}
|
||||
}
|
||||
private async loadGeoJsonData1(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/data/geoJson1.json')
|
||||
this.geoJsonData1 = await response.json()
|
||||
} catch (error) {
|
||||
console.error('配置加载失败:', error)
|
||||
}
|
||||
}
|
||||
//地图初始化
|
||||
init(container: HTMLElement, rectangle?: any): Promise<any> {
|
||||
init(container: HTMLElement): Promise<any> {
|
||||
try {
|
||||
console.log('OL init初始化container', container);
|
||||
|
||||
@ -222,10 +241,8 @@ export class MapOl implements MapInterface {
|
||||
const {
|
||||
lgtd,
|
||||
lttd,
|
||||
stcd,
|
||||
stnm,
|
||||
iconCode,
|
||||
anchoPointState,
|
||||
titleName,
|
||||
ennm
|
||||
} = item;
|
||||
@ -287,20 +304,20 @@ export class MapOl implements MapInterface {
|
||||
|
||||
// ✅ 1. 获取当前地图分辨率
|
||||
// 注意:如果样式函数在地图未完全初始化时调用,view 可能为 null
|
||||
const currentResolution = this.view ? this.view.getResolution() : 1000;
|
||||
// const currentResolution = this.view ? this.view.getResolution() : 1000;
|
||||
|
||||
// ✅ 2. 定义基准分辨率和基准缩放比例
|
||||
// INITIAL_ZOOM (4.5) 对应的分辨率大约是 3000-4000 左右 (取决于投影)
|
||||
// 这里我们用一个经验公式:分辨率越小(zoom越大),scale 越大
|
||||
// 假设在初始分辨率下 scale 为 0.7
|
||||
const baseResolution = 3000; // 这是一个估算值,你可以根据实际效果微调
|
||||
const baseScale = 0.7;
|
||||
// const baseResolution = 3000; // 这是一个估算值,你可以根据实际效果微调
|
||||
// const baseScale = 0.7;
|
||||
|
||||
// 计算动态缩放比例:
|
||||
// 如果 currentResolution 变小 (放大地图),ratio 变大 -> icon 变大
|
||||
// 限制最大和最小缩放,防止过大或过小
|
||||
// let dynamicScale = baseScale * (baseResolution / (currentResolution || 1));
|
||||
const currentZoom = this.view ? this.view.getZoom() : 4.5;
|
||||
const currentZoom:any = this.view ? this.view.getZoom() : 4.5;
|
||||
let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08;
|
||||
|
||||
// 限制范围:最小 0.5,最大 3.0 (可根据需求调整)
|
||||
@ -420,11 +437,12 @@ export class MapOl implements MapInterface {
|
||||
this.map.addLayer(tileLayer);
|
||||
} else if (layer.type === 'vector') {
|
||||
if (layer.key === 'hydropBase') {
|
||||
this.hydropBaseConfig = layer;
|
||||
// this.hydropBaseConfig = layer;
|
||||
}
|
||||
console.log(this.geoJsonData1)
|
||||
// ✅ 1. 创建矢量源,关键是要配置投影转换
|
||||
const vectorSource = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(geoJsonData1, {
|
||||
features: new GeoJSON().readFeatures(this.geoJsonData1, {
|
||||
dataProjection: 'EPSG:4326',
|
||||
featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影
|
||||
})
|
||||
@ -473,95 +491,95 @@ export class MapOl implements MapInterface {
|
||||
* @param layer 栅格图层(TileLayer)
|
||||
* @param clipGeometry 裁切几何(Polygon 或 MultiPolygon,EPSG:3857 投影)
|
||||
*/
|
||||
private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): void {
|
||||
if (!layer || !clipGeometry) return;
|
||||
|
||||
// 处理几何类型,统一为多边形数组
|
||||
let polygons: Polygon[] = [];
|
||||
const type = clipGeometry.getType();
|
||||
|
||||
if (type === 'Polygon') {
|
||||
polygons = [clipGeometry as Polygon];
|
||||
} else if (type === 'MultiPolygon') {
|
||||
const multiPolygon = clipGeometry as MultiPolygon;
|
||||
const coords = multiPolygon.getCoordinates();
|
||||
polygons = coords.map(coord => new Polygon(coord));
|
||||
} else {
|
||||
console.error('不支持的几何类型:', type);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预存储所有外环坐标
|
||||
const allRings: number[][][] = [];
|
||||
for (const polygon of polygons) {
|
||||
const coords = polygon.getCoordinates();
|
||||
if (coords && coords[0] && coords[0].length > 0) {
|
||||
allRings.push(coords[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除旧事件
|
||||
layer.removeEventListener('prerender');
|
||||
layer.removeEventListener('postrender');
|
||||
|
||||
// 渲染前:设置裁切区域
|
||||
layer.on('prerender', (event) => {
|
||||
const context = event.context;
|
||||
const frameState = event.frameState;
|
||||
// private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): void {
|
||||
// if (!layer || !clipGeometry) return;
|
||||
|
||||
if (!context || !frameState) return;
|
||||
// // 处理几何类型,统一为多边形数组
|
||||
// let polygons: Polygon[] = [];
|
||||
// const type = clipGeometry.getType();
|
||||
|
||||
// 获取坐标转像素函数
|
||||
const toPixel = frameState.coordinateToPixel;
|
||||
if (!toPixel) return;
|
||||
// if (type === 'Polygon') {
|
||||
// polygons = [clipGeometry as Polygon];
|
||||
// } else if (type === 'MultiPolygon') {
|
||||
// const multiPolygon = clipGeometry as MultiPolygon;
|
||||
// const coords = multiPolygon.getCoordinates();
|
||||
// polygons = coords.map(coord => new Polygon(coord));
|
||||
// } else {
|
||||
// console.error('不支持的几何类型:', type);
|
||||
// return;
|
||||
// }
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
// // 预存储所有外环坐标
|
||||
// const allRings: number[][][] = [];
|
||||
// for (const polygon of polygons) {
|
||||
// const coords = polygon.getCoordinates();
|
||||
// if (coords && coords[0] && coords[0].length > 0) {
|
||||
// allRings.push(coords[0]);
|
||||
// }
|
||||
// }
|
||||
|
||||
let hasPath = false;
|
||||
// // 移除旧事件
|
||||
// layer.removeEventListener('prerender');
|
||||
// layer.removeEventListener('postrender');
|
||||
|
||||
for (const ring of allRings) {
|
||||
if (!ring || ring.length === 0) continue;
|
||||
// // 渲染前:设置裁切区域
|
||||
// layer.on('prerender', (event) => {
|
||||
// const context = event.context;
|
||||
// const frameState = event.frameState;
|
||||
|
||||
for (let i = 0; i < ring.length; i++) {
|
||||
const coord = ring[i];
|
||||
const pixel = toPixel(coord);
|
||||
// if (!context || !frameState) return;
|
||||
|
||||
// // 获取坐标转像素函数
|
||||
// const toPixel = frameState.coordinateToPixel;
|
||||
// if (!toPixel) return;
|
||||
|
||||
// context.save();
|
||||
// context.beginPath();
|
||||
|
||||
// let hasPath = false;
|
||||
|
||||
// for (const ring of allRings) {
|
||||
// if (!ring || ring.length === 0) continue;
|
||||
|
||||
if (!pixel || pixel.length < 2) continue;
|
||||
|
||||
if (i === 0) {
|
||||
context.moveTo(pixel[0], pixel[1]);
|
||||
} else {
|
||||
context.lineTo(pixel[0], pixel[1]);
|
||||
}
|
||||
hasPath = true;
|
||||
}
|
||||
context.closePath();
|
||||
}
|
||||
// for (let i = 0; i < ring.length; i++) {
|
||||
// const coord = ring[i];
|
||||
// const pixel = toPixel(coord);
|
||||
|
||||
// if (!pixel || pixel.length < 2) continue;
|
||||
|
||||
// if (i === 0) {
|
||||
// context.moveTo(pixel[0], pixel[1]);
|
||||
// } else {
|
||||
// context.lineTo(pixel[0], pixel[1]);
|
||||
// }
|
||||
// hasPath = true;
|
||||
// }
|
||||
// context.closePath();
|
||||
// }
|
||||
|
||||
// if (hasPath) {
|
||||
// context.clip();
|
||||
// } else {
|
||||
// context.restore();
|
||||
// }
|
||||
// });
|
||||
|
||||
if (hasPath) {
|
||||
context.clip();
|
||||
} else {
|
||||
context.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染后:恢复状态
|
||||
layer.on('postrender', (event) => {
|
||||
if (event.context) {
|
||||
event.context.restore();
|
||||
}
|
||||
});
|
||||
}
|
||||
// // 渲染后:恢复状态
|
||||
// layer.on('postrender', (event) => {
|
||||
// if (event.context) {
|
||||
// event.context.restore();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
/**
|
||||
* 控制特定区域图层的显示(互斥显示)
|
||||
* @param regionId 区域ID (例如 "hebei", "13", "01" 等,需与图层 Key 或属性对应)
|
||||
* @param isAll 是否显示所有 (true: 显示所有图层; false: 仅显示匹配 regionId 的图层,隐藏其他)
|
||||
*/
|
||||
jdPanelControlShowAndHidden(regionId: string, isAll: boolean): void {
|
||||
this.BASEID = regionId;
|
||||
console.log(this.layerRegistry);
|
||||
console.log(this.layerRegistry.keys());
|
||||
jdPanelControlShowAndHidden(_regionId: string): void {
|
||||
// this.BASEID = regionId;
|
||||
// console.log(this.layerRegistry);
|
||||
// console.log(this.layerRegistry.keys());
|
||||
// this.addBaseDataLayer(this.hydropBaseConfig);
|
||||
// this.controlBaseLayerTreeShowAndHidden(this.REGISTRY_KEY,this.REGISTRY_KEY,false)
|
||||
}
|
||||
@ -607,7 +625,7 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
}
|
||||
// 图层树控制描点数据显示隐藏方法
|
||||
mdLayerTreeShowOrHidden(layerType: String, checked?: boolean): void {
|
||||
const layerInstance = this.pointLayerRegistry.get(layerType as string);
|
||||
const layerInstance: any = this.pointLayerRegistry.get(layerType as string);
|
||||
layerInstance.setVisible(checked);
|
||||
// this.service.mdLayerTreeShowOrHidden(layerType, checked)
|
||||
}
|
||||
@ -707,7 +725,7 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
|
||||
// 3. 创建矢量源 (VectorSource) 并直接加载数据
|
||||
const vectorSource = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(geoJsonData, {
|
||||
features: new GeoJSON().readFeatures(this.geoJsonData, {
|
||||
featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影
|
||||
})
|
||||
});
|
||||
@ -901,7 +919,7 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
|
||||
// 3. 创建绘制交互
|
||||
this.drawInteraction = new Draw({
|
||||
source: this.measureSource,
|
||||
source: this.measureSource!,
|
||||
type: geomType,
|
||||
style: drawStyle
|
||||
});
|
||||
@ -1188,6 +1206,11 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
const areaSqKm = areaSqMeters / 1000000;
|
||||
return '面积:' + areaSqKm.toFixed(3) + 'km²';
|
||||
}
|
||||
|
||||
mdLayerShowOrHidden(): void { }
|
||||
switchView(): void { }
|
||||
fitBounds(): void { }
|
||||
flyTopanto(): void { }
|
||||
/**
|
||||
* 移除地图对象,释放资源
|
||||
*/
|
||||
@ -1199,7 +1222,7 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
|
||||
// 2. 清除所有通过 layerRegistry 管理的图层
|
||||
if (this.layerRegistry) {
|
||||
this.layerRegistry.forEach((layer, key) => {
|
||||
this.layerRegistry.forEach((layer) => {
|
||||
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
@ -1209,7 +1232,7 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
|
||||
// ✅ 3. 清除点图层 Registry
|
||||
if (this.pointLayerRegistry) {
|
||||
this.pointLayerRegistry.forEach((layer, key) => {
|
||||
this.pointLayerRegistry.forEach((layer) => {
|
||||
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
@ -1221,7 +1244,7 @@ private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): voi
|
||||
if (this.map) {
|
||||
this.map.getInteractions().clear();
|
||||
this.map.getOverlays().clear();
|
||||
this.map.setTarget(null);
|
||||
this.map.setTarget(undefined);
|
||||
this.map.dispose();
|
||||
this.map = null;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ const URL_TerTDT = `https://t0.tianditu.gov.cn/ter_${mapType}/wmts?tk=${tdtToken
|
||||
// const URL_TerLabelTDT = `http://t0.tianditu.gov.cn/cta_${mapType}/wmts?tk=${tdtToken}` //terLabel
|
||||
// const URL_VecTDT = `http://t0.tianditu.com/vec_${mapType}/wmts?tk=${tdtToken}` //vecMap
|
||||
// const URL_VecLableTDT = `http://t0.tianditu.com/cva_${mapType}/wmts?tk=${tdtToken}` //vecLabel
|
||||
const URL_ImgTDT = `http://t0.tianditu.com/img_${mapType}/wmts?tk=${tdtToken}` //imgMap
|
||||
// const URL_ImgTDT = `http://t0.tianditu.com/img_${mapType}/wmts?tk=${tdtToken}` //imgMap
|
||||
// const URL_ImgLableTDT = `http://t0.tianditu.com/cia_${mapType}/wmts?tk=${tdtToken}`
|
||||
|
||||
// const znyMapUrl = "http://10.219.26.6:8050/" //中南院的测试地图环境
|
||||
|
||||
@ -33,7 +33,7 @@ const mapStore: any = useMapStore();
|
||||
const layerConfigs = computed(() => mapStore.layerData);
|
||||
|
||||
// 选中的图层
|
||||
const checkedKeys = ref([]);
|
||||
const checkedKeys: any = ref([]);
|
||||
const onCheck = (v: any, e: any) => {
|
||||
// 环保设施和环保设施在建数据获取,两者只能同时存在勾选一项
|
||||
const fData = layerConfigs.value?.filter((e: any) => e.key === "facilities")?.[0];
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
v-for="child in item.children"
|
||||
:key="child.key"
|
||||
>
|
||||
<component v-if="child.component" :is="child.component" />
|
||||
<component v-if="child.component" :is="child.component" :map="map" />
|
||||
<div v-else class="map-controller-item" @click="handleControllerClick(child)">
|
||||
<i class="icon iconfont" :class="'icon-' + child.icon"></i>
|
||||
</div>
|
||||
@ -20,6 +20,11 @@ import { ref, watch, computed } from "vue";
|
||||
import { useUiStore } from "@/store/modules/ui";
|
||||
import Calculate from "./Calculate.vue";
|
||||
import LayerController from "./LayerController.vue";
|
||||
const props = defineProps<{
|
||||
map: any;
|
||||
onClick:(key: any) => void
|
||||
}>();
|
||||
const map = props.map;
|
||||
|
||||
// 使用 Pinia store
|
||||
const uiStore = useUiStore();
|
||||
@ -126,27 +131,48 @@ const controllers: any = computed(() => [
|
||||
]);
|
||||
|
||||
// 添加全屏切换功能
|
||||
const toggleFullScreen = () => {
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
// 这里可以添加实际的浏览器全屏切换逻辑
|
||||
if (isFullScreen.value) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
// const toggleFullScreen = () => {
|
||||
// isFullScreen.value = !isFullScreen.value;
|
||||
// // 这里可以添加实际的浏览器全屏切换逻辑
|
||||
// if (isFullScreen.value) {
|
||||
// document.documentElement.requestFullscreen();
|
||||
// } else {
|
||||
// document.exitFullscreen();
|
||||
// }
|
||||
// };
|
||||
|
||||
// 控制器点击事件处理
|
||||
const handleControllerClick = (item: any) => {
|
||||
switch (item.key) {
|
||||
case "fullScreen":
|
||||
toggleFullScreen();
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
map.jdPanelControlShowAndHidden('01',false)
|
||||
// map.mdLayerTreeShowOrHidden('fp_point',!isFullScreen.value);
|
||||
// map.controlBaseLayerTreeShowAndHidden('customBaseLayer','customBaseLayer',!isFullScreen.value);
|
||||
// map.controlBaseLayerTreeShowAndHidden('lcj_bhq','lcj_bhq',!isFullScreen.value);
|
||||
// toggleFullScreen();
|
||||
break;
|
||||
case "zoomIn":
|
||||
map.zoomToggle('in');
|
||||
break;
|
||||
case "zoomOut":
|
||||
map.zoomToggle('out');
|
||||
break;
|
||||
case "dim":
|
||||
mapType.value = mapType.value === "2D" ? "3D" : "2D";
|
||||
break;
|
||||
case "rightDrawer":
|
||||
// 使用全局事件切换右侧面板
|
||||
// GlobalEvents.get('rightDrawerState').set(!drawerOpen);
|
||||
break;
|
||||
// 可以在这里添加其他控制器的处理逻辑
|
||||
case "screenShot":
|
||||
map.mapOutPut()
|
||||
break;
|
||||
case "TJ":
|
||||
props.onClick(4)
|
||||
// map.addTertiarybasinLayer(servers.Tertiarybasin, '#4DFFDD', '#92A0A5')
|
||||
break;
|
||||
default:
|
||||
console.log(`点击了控制器: ${item.name}`);
|
||||
break;
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const siteRangePicker = [
|
||||
{ label: "0-10", value: "0-10" },
|
||||
@ -47,7 +47,6 @@ const formModel = ref({
|
||||
const rules = ref({
|
||||
siteRangePicker: [{ required: true, message: "请选择装机容量" }],
|
||||
});
|
||||
const formRef = ref<any>(null);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed, watch } from "vue";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { useMapStore } from "@/store/modules/map";
|
||||
import LegendItem from "@/components/mapLegend/LegendItem.vue";
|
||||
const mapStore: any = useMapStore();
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { UserOutlined, LogoutOutlined } from "@ant-design/icons-vue";
|
||||
// 国际化
|
||||
import { useI18n } from "vue-i18n";
|
||||
@ -13,10 +12,9 @@ import Sidebar from "./Sidebar/index.vue";
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import Cookies from "js-cookie";
|
||||
import { storeToRefs } from "pinia";
|
||||
import {getPath,removePath } from '@/utils/auth';
|
||||
|
||||
const url = import.meta.env.VITE_APP_BASE_API;
|
||||
// const url = import.meta.env.VITE_APP_BASE_API;
|
||||
const username = Cookies.get("username");
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
@ -13,7 +13,7 @@ import 'ant-design-vue/dist/reset.css' // Ant Design 全局样式重置
|
||||
import dayjs from 'dayjs'; // ant 中文语言
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
// 引入svg注册脚本
|
||||
// @ts-ignore
|
||||
import 'virtual:svg-icons-register';
|
||||
|
||||
// 国际化
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<SidePanelItem title="设施类型及接入情况">
|
||||
<div class="card-container">
|
||||
<div v-for="(item, index) in dataJson" class="facility-card">
|
||||
<div v-for="(item) in dataJson" class="facility-card">
|
||||
<div class="img_icon">
|
||||
<i class="icon iconfont" :class="item?.icon" />
|
||||
</div>
|
||||
@ -91,21 +91,15 @@ const dataJson = ref<DataString[]>([
|
||||
}
|
||||
]);
|
||||
|
||||
const clickList = ref<DataString | null>(null);
|
||||
const res = ref({
|
||||
bldstt: '',
|
||||
hydrodtin: ''
|
||||
});
|
||||
|
||||
// 卡片点击事件处理
|
||||
const handleCardClick = (item: DataString) => {
|
||||
clickList.value = item;
|
||||
// dialog.current?.open() - 需要根据实际的对话框实现调整
|
||||
res.value = {
|
||||
bldstt: '',
|
||||
hydrodtin: ''
|
||||
};
|
||||
};
|
||||
// const handleCardClick = (item: DataString) => {
|
||||
// clickList.value = item;
|
||||
// // dialog.current?.open() - 需要根据实际的对话框实现调整
|
||||
// res.value = {
|
||||
// bldstt: '',
|
||||
// hydrodtin: ''
|
||||
// };
|
||||
// };
|
||||
|
||||
// 页面加载时执行的逻辑
|
||||
onMounted(() => {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="legend-container">
|
||||
<div class="legend-items">
|
||||
<div
|
||||
v-for="(item, index) in currentLegendItems"
|
||||
v-for="(item) in currentLegendItems"
|
||||
:key="item.name"
|
||||
class="legend-item"
|
||||
:class="{ 'inactive': legendInactiveSet.has(item.name) }"
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
const data = [
|
||||
@ -23,18 +23,18 @@ const data = [
|
||||
const chartRef = ref<HTMLElement | null>(null)
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
const transUnit = (value: number | null, code: string, type: string) => {
|
||||
const transUnit = (value: number | null) => {
|
||||
if (value === null) return null
|
||||
return value
|
||||
}
|
||||
|
||||
const getUnitConfigByCode = (code: string, type: string) => {
|
||||
const getUnitConfigByCode = (_code: string, _type: string) => {
|
||||
return {
|
||||
unit: '℃'
|
||||
}
|
||||
}
|
||||
|
||||
const getColorByCodeAndType = (code: string[], typeKey: string[]) => {
|
||||
const getColorByCodeAndType = (_code: string[], _typeKey: string[]) => {
|
||||
return ['#4b79ab', '#78c300']
|
||||
}
|
||||
|
||||
@ -47,8 +47,8 @@ const getChartOption = () => {
|
||||
|
||||
data.forEach((item: any) => {
|
||||
xData.push(`${item.monthInt}月`)
|
||||
actualData.push(item.actualTemp === null ? null : transUnit(item.actualTemp, 'Other', 'ACTUALTEMP'))
|
||||
naturalData.push(item.naturalTemp === null ? null : transUnit(item.naturalTemp, 'Other', 'NATURALTEMP'))
|
||||
actualData.push(item.actualTemp === null ? null : transUnit(item.actualTemp))
|
||||
naturalData.push(item.naturalTemp === null ? null : transUnit(item.naturalTemp))
|
||||
})
|
||||
|
||||
const code = ['Other']
|
||||
@ -65,10 +65,10 @@ const getChartOption = () => {
|
||||
color: '#ffffff',
|
||||
fontSize: 14
|
||||
},
|
||||
formatter: function(params) {
|
||||
formatter: function(params: any) {
|
||||
if (!params || params.length === 0) return '';
|
||||
let result = `<div style="font-weight: bold; margin-bottom: 8px;">${params[0].axisValue}</div>`;
|
||||
params.forEach(item => {
|
||||
params.forEach((item: any) => {
|
||||
result += `<div style="display: flex; align-items: center; margin: 4px 0;">`;
|
||||
result += `<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background-color: ${item.color}; margin-right: 8px;"></span>`;
|
||||
result += `<span>${item.seriesName} ${item.value}${unit}</span>`;
|
||||
|
||||
@ -188,7 +188,7 @@ const tableData = ref([
|
||||
]);
|
||||
|
||||
// 自定义行样式
|
||||
const customRow = (record: any, index: number) => {
|
||||
const customRow = (_record: any, index: number) => {
|
||||
return {
|
||||
style: {
|
||||
backgroundColor: index % 2 === 1 ? '#fafafa' : '#ffffff'
|
||||
|
||||
@ -12,7 +12,7 @@ const permissionStore = usePermissionStoreHook();
|
||||
const whiteList = ['/login', '/login-sjtb']; //login
|
||||
|
||||
// 查找第一个可用路由
|
||||
function findFirstAvailableRoute(routes: RouteRecordRaw[]): string | undefined {
|
||||
function findFirstAvailableRoute(routes: any[]): string | undefined {
|
||||
for (const route of routes) {
|
||||
if (route.meta?.hidden) continue;
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
setSidebarStatus,
|
||||
getSize,
|
||||
setSize,
|
||||
setLanguage
|
||||
|
||||
@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useJidiSelectEventStore = defineStore('jidiSelectEvent', () => {
|
||||
const jidiData = ref([
|
||||
const jidiData:any = ref([
|
||||
{
|
||||
"_tls": {},
|
||||
"id": null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,6 @@
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding: 16px 24px !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
.ant-modal-footer {
|
||||
margin-top: 0 !important;
|
||||
@ -38,3 +37,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
:where(.css-dev-only-do-not-override-ekaqbe).ant-btn >span {
|
||||
display: inline-flex;
|
||||
}
|
||||
@ -142,11 +142,6 @@ namespace DateSetting {
|
||||
return dayjs().startOf('day').startOf('hour');
|
||||
}
|
||||
|
||||
//获取当前时间
|
||||
function getEndTime(): Dayjs {
|
||||
return dayjs().endOf('day');
|
||||
}
|
||||
|
||||
function getStartYear(): Dayjs {
|
||||
return dayjs().startOf('year');
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ export function parseTime(time :any, cFormat :any) {
|
||||
}
|
||||
date = new Date(time)
|
||||
}
|
||||
const formatObj = {
|
||||
const formatObj:any = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
|
||||
99
frontend/src/utils/leaflet/leaflet-tilelayer-wmts-src.js
Normal file
99
frontend/src/utils/leaflet/leaflet-tilelayer-wmts-src.js
Normal file
@ -0,0 +1,99 @@
|
||||
L.TileLayer.WMTS = L.TileLayer.extend({
|
||||
defaultWmtsParams: {
|
||||
service: "WMTS",
|
||||
request: "GetTile",
|
||||
version: "1.0.0",
|
||||
layer: "",
|
||||
style: "",
|
||||
tilematrixset: "",
|
||||
format: "image/jpeg",
|
||||
},
|
||||
|
||||
initialize: function (url, options) {
|
||||
// (String, Object)
|
||||
this._url = url;
|
||||
var lOptions = {};
|
||||
var cOptions = Object.keys(options);
|
||||
cOptions.forEach((element) => {
|
||||
lOptions[element.toLowerCase()] = options[element];
|
||||
});
|
||||
var wmtsParams = L.extend({}, this.defaultWmtsParams);
|
||||
var tileSize = lOptions.tileSize || this.options.tileSize;
|
||||
if (lOptions.detectRetina && L.Browser.retina) {
|
||||
wmtsParams.width = wmtsParams.height = tileSize * 2;
|
||||
} else {
|
||||
wmtsParams.width = wmtsParams.height = tileSize;
|
||||
}
|
||||
for (var i in lOptions) {
|
||||
// all keys that are in defaultWmtsParams options go to WMTS params
|
||||
if (wmtsParams.hasOwnProperty(i) && i != "matrixIds") {
|
||||
wmtsParams[i] = lOptions[i];
|
||||
}
|
||||
}
|
||||
this.wmtsParams = wmtsParams;
|
||||
this.matrixIds = options.matrixIds || this.getDefaultMatrix();
|
||||
L.setOptions(this, options);
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this._crs = this.options.crs || map.options.crs;
|
||||
L.TileLayer.prototype.onAdd.call(this, map);
|
||||
},
|
||||
|
||||
getTileUrl: function (coords) {
|
||||
// (Point, Number) -> String
|
||||
var tileSize = this.options.tileSize;
|
||||
var nwPoint = coords.multiplyBy(tileSize);
|
||||
nwPoint.x += 1;
|
||||
nwPoint.y -= 1;
|
||||
var sePoint = nwPoint.add(new L.Point(tileSize, tileSize));
|
||||
var zoom = this._tileZoom;
|
||||
var nw = this._crs.project(this._map.unproject(nwPoint, zoom));
|
||||
var se = this._crs.project(this._map.unproject(sePoint, zoom));
|
||||
var tilewidth = se.x - nw.x;
|
||||
var ident = this.matrixIds[zoom].identifier;
|
||||
var tilematrix = this.wmtsParams.tilematrixset + ":" + ident;
|
||||
var X0 = this.matrixIds[zoom].topLeftCorner.lng;
|
||||
var Y0 = this.matrixIds[zoom].topLeftCorner.lat;
|
||||
var tilecol = Math.floor((nw.x - X0) / tilewidth);
|
||||
var tilerow = -Math.floor((nw.y - Y0) / tilewidth);
|
||||
var url = L.Util.template(this._url, { s: this._getSubdomain(coords) });
|
||||
return (
|
||||
url +
|
||||
L.Util.getParamString(this.wmtsParams, url) +
|
||||
"&tilematrix=" +
|
||||
tilematrix +
|
||||
"&tilerow=" +
|
||||
tilerow +
|
||||
"&tilecol=" +
|
||||
tilecol
|
||||
);
|
||||
},
|
||||
|
||||
setParams: function (params, noRedraw) {
|
||||
L.extend(this.wmtsParams, params);
|
||||
if (!noRedraw) {
|
||||
this.redraw();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
getDefaultMatrix: function () {
|
||||
/**
|
||||
* the matrix3857 represents the projection
|
||||
* for in the IGN WMTS for the google coordinates.
|
||||
*/
|
||||
var matrixIds3857 = new Array(22);
|
||||
for (var i = 0; i < 22; i++) {
|
||||
matrixIds3857[i] = {
|
||||
identifier: "" + i,
|
||||
topLeftCorner: new L.LatLng(20037508.3428, -20037508.3428),
|
||||
};
|
||||
}
|
||||
return matrixIds3857;
|
||||
},
|
||||
});
|
||||
|
||||
L.tileLayer.wmts = function (url, options) {
|
||||
return new L.TileLayer.WMTS(url, options);
|
||||
};
|
||||
877
frontend/src/utils/leaflet/leaflet.inflatable-markers-group.js
Normal file
877
frontend/src/utils/leaflet/leaflet.inflatable-markers-group.js
Normal file
@ -0,0 +1,877 @@
|
||||
(function (factory, window) {
|
||||
if (typeof window !== 'undefined' && window.L) {
|
||||
factory(window.L);
|
||||
}
|
||||
}(function leafletInflatableMarkersGroupFactory(L) {
|
||||
|
||||
/**
|
||||
* The Z-index offset to apply to inflated markers to make them show on top
|
||||
* of deflated markers
|
||||
*/
|
||||
const INFLATED_MARKERS_ZINDEX_OFFSET = 10000;
|
||||
|
||||
/**
|
||||
* The Z-index offset to apply to deflated markers to make them show on top
|
||||
* of all markers when the special 'show hidden markers' action is used
|
||||
*/
|
||||
const DEFLATED_MARKERS_ZINDEX_OFFSET = 20000;
|
||||
|
||||
/**
|
||||
* A two-state marker that can be added to a InflatableMarkerGroup
|
||||
*
|
||||
* An inflatable marker can be either inflated (it's displayed normally)
|
||||
* or deflated (it's displayed as a smaller different icon to declutter the
|
||||
* map).
|
||||
*
|
||||
* This class is not normally used by end-users. Normal markers should be
|
||||
* added to L.InflatableMarkerGroup instead and the group will manage its
|
||||
* own L.InflatableMarker-s.
|
||||
*
|
||||
* To handle the two inflated/deflated states and the associated changes in
|
||||
* shape and size, instances of this class are also their own icons.
|
||||
* Toggling between the inflated and deflated state boils down to toggle the
|
||||
* display of the base marker's icon (shown when inflated) and this class'
|
||||
* icon (shown when deflated).
|
||||
* @extends L.Marker
|
||||
*/
|
||||
const InflatableMarker = L.InflatableMarker = L.Marker.extend({
|
||||
/**
|
||||
* The options applicable to the marker, same as the Icon options but
|
||||
* an explicit size in mandatory
|
||||
* @public
|
||||
* @type {L.IconOptions}
|
||||
*/
|
||||
options: L.Icon.prototype.options,
|
||||
|
||||
/**
|
||||
* Constructs the marker.
|
||||
* @constructs L.InflatableMarker
|
||||
* @public
|
||||
* @param {L.LatLng} latlng - The position of the marker on the map
|
||||
* @param {L.InflatableMarkerGroup} group - The group this marker belongs to,
|
||||
* marker collisions are only computed inside a single group
|
||||
* @param {L.Layer} baseMarker - The inflated version of the marker, added to the
|
||||
* group via addLayer(...)
|
||||
*/
|
||||
initialize: function (latlng, group, baseMarker) {
|
||||
L.Util.setOptions(this, baseMarker.options);
|
||||
this.options.pane = group.options.pane;
|
||||
// hijack the icon drawing process
|
||||
this.options._inflatedIcon = this.options.icon;
|
||||
this.options.icon = this;
|
||||
|
||||
/**
|
||||
* The underlying marker added to the group, used as the inflated
|
||||
* version of the current inflatable marker
|
||||
* @public
|
||||
* @type {L.Marker}
|
||||
*/
|
||||
this.baseMarker = baseMarker;
|
||||
|
||||
/**
|
||||
* Where the marker is displayed
|
||||
* @private
|
||||
* @type {L.LatLng}
|
||||
*/
|
||||
this._latlng = latlng;
|
||||
|
||||
/**
|
||||
* The clearance box around the marker in the order (north, east, south, west)
|
||||
* @type {[number, number, number, number]}
|
||||
* @private
|
||||
*/
|
||||
this._borders = [null, null, null, null];
|
||||
|
||||
/**
|
||||
* The L.InflatableMarkerGroup this marker belongs to
|
||||
* @private
|
||||
* @type {L.InflatableMarkersGroup}
|
||||
*/
|
||||
this._group = group;
|
||||
|
||||
/**
|
||||
* The set of all markers which collision with the current marker if
|
||||
* they're both inflated
|
||||
* @private
|
||||
* @type {Set<L.InflatableMarker>}
|
||||
*/
|
||||
this._obstructiveMarkers = new Set();
|
||||
|
||||
/**
|
||||
* Whether the marker is currently inflated
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._inflated = false;
|
||||
|
||||
/**
|
||||
* Whether the marker's icon needs to be redrawn, typically after a
|
||||
* change from inflated to deflated or vice-versa
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._iconNeedsUpdate = true;
|
||||
|
||||
/**
|
||||
* The original Z-index attributed to this marker
|
||||
* @private
|
||||
* @type {integer}
|
||||
*/
|
||||
this._savedZIndexOffset = this.options.zIndexOffset;
|
||||
|
||||
this.addEventParent(this.baseMarker);
|
||||
|
||||
this.on("contextmenu", this.toggle, this);
|
||||
},
|
||||
|
||||
_computeBorders: function(map, margin) {
|
||||
const pos = map.latLngToContainerPoint(this._latlng);
|
||||
const halfSize = L.point(this.options._inflatedIcon.options.iconSize).divideBy(2);
|
||||
const br = pos.add(halfSize).add(margin);
|
||||
const ul = pos.subtract(halfSize).subtract(margin);
|
||||
this._borders = [ul.y, br.x, br.y, ul.x];
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the size of the marker when inflated.
|
||||
* @public
|
||||
* @return {L.Point} The inflated size of the marker
|
||||
*/
|
||||
getInflatedSize: function () {
|
||||
const iconOptions = this.options._inflatedIcon.options;
|
||||
if (!(iconOptions.iconSize instanceof L.Point))
|
||||
iconOptions.iconSize = L.point(iconOptions.iconSize);
|
||||
return iconOptions.iconSize;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the icon to be displayed on the map, depending on its
|
||||
* inflated/deflated state.
|
||||
* @public
|
||||
* @return {HTMLElement} The icon to draw
|
||||
*/
|
||||
createIcon: function () {
|
||||
this._iconObj = this._inflated ?
|
||||
this.options._inflatedIcon :
|
||||
this._group.options.iconCreateFunction(this);
|
||||
return this._iconObj.createIcon();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the shadow to be displayed on the map, alongside with the
|
||||
* icon.
|
||||
* @public
|
||||
* @todo This is not implemented for now and returns null (no shadow)
|
||||
* @return {HTMLElement} The shadow to add to the icon
|
||||
*/
|
||||
createShadow: function () {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Forces the icon to be redrawn, for example if its inflated/deflated
|
||||
* state has been modified externally.
|
||||
* @public
|
||||
*/
|
||||
redraw: function () {
|
||||
// Wierd but convenient; remember that InflatableMarker-s are their
|
||||
// own icons, this is why this works.
|
||||
this.setIcon(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets another marker as conflicting/collisioning with the current one.
|
||||
* @private
|
||||
* @param {L.InflatableMarker} otherMarker - Another marker in the group
|
||||
*/
|
||||
_addObstructiveMarker: function (otherMarker) {
|
||||
this._obstructiveMarkers.add(otherMarker);
|
||||
otherMarker._obstructiveMarkers.add(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets another marker as NOT conflicting/collisioning with the current
|
||||
* one (for example, if it's getting removed altogether or reduced in
|
||||
* size).
|
||||
* @private
|
||||
* @param {L.InflatableMarker} otherMarker - Another marker in the group
|
||||
*/
|
||||
_removeObstructiveMarker: function (otherMarker) {
|
||||
this._obstructiveMarkers.delete(otherMarker);
|
||||
otherMarker._obstructiveMarkers.delete(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all markers from the conflicting/collisioning set of the
|
||||
* current one.
|
||||
* @private
|
||||
*/
|
||||
_clearObstructiveMarkers: function() {
|
||||
for (const other of this._obstructiveMarkers) {
|
||||
this._removeObstructiveMarker(other);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the set of all markers conflicting/collisioning to the
|
||||
* current one.
|
||||
* @private
|
||||
* @return {Set<L.InflatableMarker>} The conflicting/collisioning
|
||||
* markers.
|
||||
*/
|
||||
_getObstructiveMarkers: function() {
|
||||
return this._obstructiveMarkers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Puts a marker on top, this is used to ensure for instance that
|
||||
* inflated markers are shown on top by default
|
||||
* @private
|
||||
* @param {int} offset - The offset by which to increase the Z-index of
|
||||
* the marker
|
||||
*/
|
||||
_bringToFront: function(offset=INFLATED_MARKERS_ZINDEX_OFFSET) {
|
||||
this.setZIndexOffset(offset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Puts a marker back where it was after it has been brought onto top.
|
||||
* @private
|
||||
*/
|
||||
_bringBackFromFront: function() {
|
||||
this.setZIndexOffset(this._savedZIndexOffset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Switches the marker to the inflated state (or no-op if it was already
|
||||
* inflated).
|
||||
* @private
|
||||
*/
|
||||
_inflate: function() {
|
||||
if (this._inflated)
|
||||
return;
|
||||
this._bringToFront();
|
||||
this._inflated = true;
|
||||
this._iconNeedsUpdate = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Switches the marker to the deflated state (or no-op if it was already
|
||||
* inflated).
|
||||
* @private
|
||||
*/
|
||||
_deflate: function() {
|
||||
if (!this._inflated)
|
||||
return;
|
||||
if (this._group._inflatedMarkersAbove)
|
||||
this._bringBackFromFront();
|
||||
else
|
||||
this._bringToFront(DEFLATED_MARKERS_ZINDEX_OFFSET);
|
||||
this._inflated = false;
|
||||
this._iconNeedsUpdate = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the inflated/deflated state of the marker
|
||||
* @public
|
||||
*/
|
||||
toggle: function() {
|
||||
if (this._inflated) {
|
||||
this._deflate();
|
||||
} else {
|
||||
this._inflate();
|
||||
for (const other of this._obstructiveMarkers) {
|
||||
other._deflate();
|
||||
}
|
||||
}
|
||||
|
||||
// call daddy to refresh everybody
|
||||
this._group._refreshIcons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells whether the marker is currently inflated
|
||||
* @public
|
||||
* @return {boolean} Whether the marker is inflated
|
||||
*/
|
||||
isInflated: function() {
|
||||
return this._inflated;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A group of inflatable markers that can be added onto a leaflet map.
|
||||
*
|
||||
* This class should be used as a regular L.FeatureGroup but for now, users
|
||||
* should stick to L.Marker objects. Other features are not explicitly
|
||||
* supported (but have never been tested so who knows?). In any case, this
|
||||
* class handled its own L.InflatableMarker, so end-users should not
|
||||
* construct them manually but instead add the normal markers to this group.
|
||||
*
|
||||
* The markers group make sure that two conflicting markers (i.e. markers
|
||||
* that would collision if they are both inflated) are never inflated at the
|
||||
* same time. This makes the map much more readable while keeping the
|
||||
* markers at their place on the map, as long as the deflated icon of each
|
||||
* marker is carefully chosen not to clutter the map.
|
||||
* @extends L.FeatureGroup
|
||||
*/
|
||||
const InflatableMarkersGroup = L.InflatableMarkersGroup = L.FeatureGroup.extend({
|
||||
/**
|
||||
* How to configure the markers group, additionally from the
|
||||
* L.FeatureGroup options
|
||||
* @public
|
||||
*/
|
||||
options: {
|
||||
/**
|
||||
* The margin that must be kept clear around an inflated marker.
|
||||
*
|
||||
* Any marker closer to the current marker than this clearance will
|
||||
* be marked as collisioning. The first element is the horizontal
|
||||
* margin, the second one the vertical margin. It's set by default
|
||||
* to [2, 2] (pixels). You can set it to [0, 0] or even to a
|
||||
* negative value to tolerate some amount of overlapping between
|
||||
* inflated markers.
|
||||
*/
|
||||
obstructionSize: L.point(2, 2),
|
||||
/**
|
||||
* The function that will be used to create deflated markers' icons.
|
||||
*/
|
||||
iconCreateFunction: null,
|
||||
/**
|
||||
* The map pane this group has to be added to, by default the
|
||||
* markers pane
|
||||
*/
|
||||
groupPane: L.Marker.prototype.options.pane,
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs an InflatableMarkersGroup
|
||||
* @constructs {L.InflatableMarkersGroup}
|
||||
* @param {L.InflatableMarkersGroup.options} options - The configuration options
|
||||
*/
|
||||
initialize: function (options) {
|
||||
L.Util.setOptions(this, options);
|
||||
if (!this.options.obstructionSize instanceof L.Point)
|
||||
this.options.obstructionSize = L.point(this.options.obstructionSize);
|
||||
|
||||
/**
|
||||
* The underlying layer group used to handle the map layer adding and
|
||||
* removal operations
|
||||
* @private
|
||||
* @type {L.FeatureGroup}
|
||||
*/
|
||||
this._featureGroup = L.featureGroup();
|
||||
|
||||
/**
|
||||
* A associative array between base Leaflet layers (the one added to
|
||||
* this group) and the corresponding inflatable markers this group
|
||||
* constructs.
|
||||
*
|
||||
* Note: this attribute is a Map but a Javascript one (i.e. an
|
||||
* associative array), not a Leaflet Map!
|
||||
* @private
|
||||
* @type {Map<L.Layer, L.InflatableMarker>}
|
||||
*/
|
||||
this._markers = new Map();
|
||||
|
||||
/**
|
||||
* The boundaries of this layer group
|
||||
* @private
|
||||
* @type {L.LatLngBounds}
|
||||
*/
|
||||
this._bounds = null;
|
||||
|
||||
/**
|
||||
* Whether this group has already been initialized and added to a map,
|
||||
* setting it to false will force recompute all the InflatableMarker-s
|
||||
* collisions.
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._alreadyDisplayed = false;
|
||||
|
||||
/**
|
||||
* Whether the inflated markers should be displayed on top of deflated
|
||||
* markers (the default) or the opposite (to make masked deflated
|
||||
* markers prominent).
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._inflatedMarkersAbove = true;
|
||||
|
||||
/**
|
||||
* Other InflatableMarkersGroup that could be added to the same map and
|
||||
* whose member markers may collision with the current group.
|
||||
* @type {Set<L.InflatableMarkersGroup>}
|
||||
*/
|
||||
this._otherGroups = new Set();
|
||||
|
||||
this._featureGroup.addEventParent(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* The events we handle.
|
||||
*
|
||||
* The class reacts to zooming/dezooming to inflate as many markers as
|
||||
* possible with the new zoom without causing collisions.
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
getEvents: function() {
|
||||
return {
|
||||
'zoomend': this._zoomend,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether two markers collision when both are inflated
|
||||
*
|
||||
* @param {L.Point} distance - The distance between the center of both
|
||||
* markers
|
||||
* @param {L.InflatableMarker} marker1 - The first marker
|
||||
* @param {L.InflatableMarker} marker2 - The second marker
|
||||
* @return {boolean} Whether the markers are closer to each other than
|
||||
* the obstruction size
|
||||
*/
|
||||
_mayObstruct: function(distance, marker1, marker2) {
|
||||
const marker1HalfSize = marker1.getInflatedSize().divideBy(2);
|
||||
const marker2HalfSize = marker2.getInflatedSize().divideBy(2);
|
||||
return Math.abs(distance.x) <= marker1HalfSize.x + marker2HalfSize.x + this.options.obstructionSize.x &&
|
||||
Math.abs(distance.y) <= marker1HalfSize.y + marker2HalfSize.y + this.options.obstructionSize.y
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a marker to the group and compute the collisions with already
|
||||
* existing markers
|
||||
* @param {L.Marker} layer - A marker to add (technically, it should be
|
||||
* any layer, but we kind of break the Liskov principle to make things
|
||||
* simpler for now...)
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
addLayer: function (layer) {
|
||||
if (this.hasLayer(layer)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const marker = new InflatableMarker(layer._latlng, this, layer);
|
||||
let inhibited = false;
|
||||
if (this._map) {
|
||||
const target = this._map.latLngToContainerPoint(layer._latlng);
|
||||
for (const [l, m] of this._markers) {
|
||||
const other = this._map.latLngToContainerPoint(l._latlng);
|
||||
if (this._mayObstruct(target.subtract(other), marker, m)) {
|
||||
marker._addObstructiveMarker(m);
|
||||
inhibited = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._markers.set(layer, marker);
|
||||
this._featureGroup.addLayer(marker);
|
||||
|
||||
if (!inhibited) {
|
||||
marker._inflate();
|
||||
marker.redraw();
|
||||
}
|
||||
|
||||
if (!this._inflatedMarkersAbove && !marker._inflated) {
|
||||
marker._bringToFront(DEFLATED_MARKERS_ZINDEX_OFFSET);
|
||||
}
|
||||
|
||||
if (this._bounds == null) {
|
||||
this._bounds = L.latLngBounds(layer._latlng, layer._latlng);
|
||||
} else {
|
||||
this._bounds.extend(layer._latlng);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a marker from the group and recompute the collisions of
|
||||
* previously collisioning markers
|
||||
* @param {L.Layer} layer - The layer to remove
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
removeLayer: function (layer) {
|
||||
if (this._markers.has(layer)) {
|
||||
const marker = this._markers.get(layer);
|
||||
marker._clearObstructiveMarkers();
|
||||
this._featureGroup.removeLayer(marker);
|
||||
this._markers.delete(layer);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all layers from this group and reset it completely
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
clearLayers: function () {
|
||||
this._markers.clear();
|
||||
this._featureGroup.clearLayers();
|
||||
this._alreadyDisplayed = false; // reset the layer as if it had never
|
||||
// been displayed
|
||||
this._bounds = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the bounds of this group on the map
|
||||
* @return L.LatLngBounds
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
getBounds: function () {
|
||||
return this._bounds;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a marker has been added to the group
|
||||
* @param {L.Marker} layer - The marker looked for
|
||||
* @return {boolean} whether the layer belongs to this group
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
hasLayer: function(layer) {
|
||||
return this._markers.has(layer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Recompute all collision sets for all markers
|
||||
* @private
|
||||
*/
|
||||
_recomputeObstructions: async function() {
|
||||
for (const [l,m] of this._markers) {
|
||||
m._clearObstructiveMarkers();
|
||||
}
|
||||
|
||||
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
const iterator = this._markers.entries();
|
||||
let result = iterator.next();
|
||||
|
||||
const allMarkers = [...this._iterateOnOwnAndOtherGroupsMarkers()];
|
||||
allMarkers.forEach(m => m[1]._computeBorders(this._map, this.options.obstructionSize));
|
||||
allMarkers.sort((m1, m2) => {
|
||||
return m1[1]._borders[0] - m2[1]._borders[0];
|
||||
});
|
||||
|
||||
const process = L.bind(async function () {
|
||||
const start = new Date();
|
||||
|
||||
while (!result.done) {
|
||||
const currentDate = new Date();
|
||||
if (currentDate - start > 200) {
|
||||
break;
|
||||
}
|
||||
const marker = result.value[1];
|
||||
|
||||
const posNorth = this._bisectNorth(allMarkers, marker._borders[0]);
|
||||
const posSouth = this._bisectSouth(allMarkers, marker._borders[2]);
|
||||
|
||||
const sortedByWest = allMarkers.slice(posNorth, posSouth)
|
||||
.sort((m1, m2) => {
|
||||
return m1[1]._borders[3] - m2[1]._borders[3];
|
||||
});
|
||||
const posWest = this._bisectWest(sortedByWest, marker._borders[3]);
|
||||
const posEast = this._bisectEast(sortedByWest, marker._borders[1]);
|
||||
|
||||
sortedByWest.slice(posWest, posEast).forEach(m => {
|
||||
if (marker !== m[1])
|
||||
marker._addObstructiveMarker(m[1]);
|
||||
})
|
||||
|
||||
result = iterator.next();
|
||||
}
|
||||
|
||||
if (!result.done) {
|
||||
process();
|
||||
return waitFor(50).then(process);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}, this);
|
||||
|
||||
return process();
|
||||
},
|
||||
|
||||
_iterateOnOwnAndOtherGroupsMarkers: function* () {
|
||||
let iteratorOnGroups = this._otherGroups.values();
|
||||
let iteratorOnMarkers = this._markers.entries();
|
||||
let result = iteratorOnMarkers.next();
|
||||
while (!result.done) {
|
||||
yield result.value;
|
||||
result = iteratorOnMarkers.next();
|
||||
}
|
||||
|
||||
let gr = iteratorOnGroups.next();
|
||||
while (!gr.done) {
|
||||
iteratorOnMarkers = gr.value._markers.entries();
|
||||
result = iteratorOnMarkers.next();
|
||||
while (!result.done) {
|
||||
yield result.value;
|
||||
result = iteratorOnMarkers.next();
|
||||
}
|
||||
gr = iteratorOnGroups.next();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* React to this group being added onto a map
|
||||
*
|
||||
* Computation of the collision sets is delayed until this method is
|
||||
* called.
|
||||
* @param {L.Map} map - The leaflet map
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
onAdd: function (map) {
|
||||
this._map = map;
|
||||
this._recomputeObstructions().then(() => {
|
||||
this._featureGroup.addTo(map);
|
||||
if (!this._alreadyDisplayed) {
|
||||
this._alreadyDisplayed = true;
|
||||
this.inflateAsManyAsPossible(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* React to this group being removed from a map
|
||||
*
|
||||
* The collision sets are not cleared at this point in case someone
|
||||
* wants to have a look at them, but they will be recomputed if the
|
||||
* group is added again onto a map anyway.
|
||||
* @param {L.Map} map - The leaflet map
|
||||
* @public
|
||||
* @inheritdoc
|
||||
*/
|
||||
onRemove: function (map) {
|
||||
this._featureGroup.removeFrom(map);
|
||||
this._map = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Redraw all icons that are marked for update (after zooming in for
|
||||
* instance)
|
||||
* @private
|
||||
* @inheritdoc
|
||||
*/
|
||||
_refreshIcons: function() {
|
||||
for (const [baseMarker,marker] of this._iterateOnOwnAndOtherGroupsMarkers()) {
|
||||
if (marker._iconNeedsUpdate)
|
||||
marker.redraw();
|
||||
marker._iconNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inflate as many deflated markers as possible without causing
|
||||
* collisions
|
||||
*
|
||||
* We don't actually go clever and guarantee that we display the
|
||||
* theoretical maximum number of markers. We just go through them in
|
||||
* order and avoid inflating a marker if it would collision with already
|
||||
* inflated markers.
|
||||
*
|
||||
* @param {boolean} reset - Start by deflating all markers before
|
||||
* inflating as many as possible
|
||||
* @public
|
||||
*/
|
||||
inflateAsManyAsPossible: function(reset = false) {
|
||||
const inhibited = new Set();
|
||||
if (!reset) {
|
||||
// add all the markers that could obstruct the already inflated
|
||||
// markers
|
||||
for (const [layer, marker] of this._markers) {
|
||||
if (marker.inflated) {
|
||||
for (const other of marker._obstructiveMarkers) {
|
||||
inhibited.add(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [layer, marker] of this._markers) {
|
||||
if (!inhibited.has(marker)) {
|
||||
marker._inflate();
|
||||
for (const other of marker._obstructiveMarkers) {
|
||||
inhibited.add(other);
|
||||
other._deflate();
|
||||
}
|
||||
}
|
||||
}
|
||||
this._refreshIcons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Deflate all markers
|
||||
* @public
|
||||
*/
|
||||
deflateAll: function() {
|
||||
for (const [layer, marker] of this._markers) {
|
||||
marker._deflate();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* React to zoom/dezoom by recomputing the collision between markers and
|
||||
* inflate as many as possible
|
||||
* @private
|
||||
*/
|
||||
_zoomend: function() {
|
||||
this._recomputeObstructions().then(() =>
|
||||
this.inflateAsManyAsPossible()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles between showing the inflating markers on top of all markers
|
||||
* (the normal state) and showing the deflating markers (so that we can
|
||||
* locate deflated markers previously hidden).
|
||||
* @public
|
||||
*/
|
||||
toggleInflatedMarkersAbove() {
|
||||
if (this._inflatedMarkersAbove) {
|
||||
for (const [layer, marker] of this._markers) {
|
||||
if (!marker._inflated) {
|
||||
marker._bringToFront(DEFLATED_MARKERS_ZINDEX_OFFSET);
|
||||
}
|
||||
}
|
||||
this._inflatedMarkersAbove = false;
|
||||
} else {
|
||||
for (const [layer, marker] of this._markers) {
|
||||
if (!marker._inflated) {
|
||||
marker._bringBackFromFront();
|
||||
}
|
||||
}
|
||||
this._inflatedMarkersAbove = true;
|
||||
}
|
||||
},
|
||||
|
||||
makeAwareOfOtherGroup(other) {
|
||||
this._otherGroups.add(other);
|
||||
other._otherGroups.add(this);
|
||||
if (this._map) {
|
||||
this._recomputeObstructions().then(() =>
|
||||
this.inflateAsManyAsPossible()
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
removeOtherGroup(other) {
|
||||
this._otherGroups.delete(other);
|
||||
other._otherGroups.delete(this);
|
||||
if (this._map) {
|
||||
this._recomputeObstructions().then(() =>
|
||||
this.inflateAsManyAsPossible()
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Bisect a map of markers to find the last one before the west boundary in parameter
|
||||
* The array has to be sorted.
|
||||
* @param {[L.Marker, L.InflatableMarker][]} markers An array of pairs [Marker, InflatableMarker]
|
||||
* @param {number} x The minimum X boundary
|
||||
* @private
|
||||
*/
|
||||
_bisectWest(markers, x) {
|
||||
if (!markers.length)
|
||||
return markers.length;
|
||||
let left = 0;
|
||||
let right = markers.length - 1;
|
||||
let pos = Math.trunc((left + right) / 2);
|
||||
while (pos > left) {
|
||||
if (x > markers[pos][1]._borders[1]) {
|
||||
left = pos;
|
||||
} else {
|
||||
right = pos;
|
||||
}
|
||||
pos = Math.trunc((left + right) / 2);
|
||||
}
|
||||
return markers.length > left && x > markers[left][1]._borders[1] ? left + 1 : left;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bisect a map of markers to find the first one past the east boundary in parameter
|
||||
* The array has to be sorted.
|
||||
* @param {[L.Marker, L.InflatableMarker][]} markers An array of pairs [Marker, InflatableMarker]
|
||||
* @param {number} x The minimum X boundary
|
||||
* @private
|
||||
*/
|
||||
_bisectEast(markers, x) {
|
||||
if (!markers.length)
|
||||
return -1;
|
||||
let left = 0;
|
||||
let right = markers.length - 1;
|
||||
let pos = Math.ceil((left + right) / 2);
|
||||
while (pos < right) {
|
||||
if (x > markers[pos][1]._borders[3]) {
|
||||
left = pos;
|
||||
} else {
|
||||
right = pos;
|
||||
}
|
||||
pos = Math.ceil((left + right) / 2);
|
||||
}
|
||||
return markers.length > right && x < markers[right][1]._borders[3] ? right : right - 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bisect a map of markers to find the first one past the east boundary in parameter
|
||||
* The array has to be sorted.
|
||||
* @param {[L.Marker, L.InflatableMarker][]} markers An array of pairs [Marker, InflatableMarker]
|
||||
* @param {number} y The minimum Y boundary
|
||||
* @private
|
||||
*/
|
||||
_bisectNorth(markers, y) {
|
||||
if (!markers.length)
|
||||
return markers.length;
|
||||
let left = 0;
|
||||
let right = markers.length - 1;
|
||||
let pos = Math.trunc((left + right) / 2);
|
||||
while (pos > left) {
|
||||
if (y > markers[pos][1]._borders[2]) {
|
||||
left = pos;
|
||||
} else {
|
||||
right = pos;
|
||||
}
|
||||
pos = Math.trunc((left + right) / 2);
|
||||
}
|
||||
return y > markers[left][1]._borders[2] ? left : left + 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bisect a map of markers to find the first one past the east boundary in parameter
|
||||
* The array has to be sorted.
|
||||
* @param {[L.Marker, L.InflatableMarker][]} markers An array of pairs [Marker, InflatableMarker]
|
||||
* @param {number} y The minimum Y boundary
|
||||
* @private
|
||||
*/
|
||||
_bisectSouth(markers, y) {
|
||||
if (!markers.length)
|
||||
return -1;
|
||||
let left = 0;
|
||||
let right = markers.length - 1;
|
||||
let pos = Math.ceil((left + right) / 2);
|
||||
while (pos < right) {
|
||||
if (y > markers[pos][1]._borders[0]) {
|
||||
left = pos;
|
||||
} else {
|
||||
right = pos;
|
||||
}
|
||||
pos = Math.ceil((left + right) / 2);
|
||||
}
|
||||
return markers.length > right && y < markers[right][1]._borders[0] ? right : right - 1;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Constructs an inflatable markers group
|
||||
* @constructs {L.InflatableMarkersGroup}
|
||||
* @param {L.InflatableMarkersGroup.options} options - the configuration
|
||||
* @return {L.InflatableMarkersGroup} The newly constructed group
|
||||
*/
|
||||
L.inflatableMarkersGroup = function (options) {
|
||||
return new L.InflatableMarkersGroup(options);
|
||||
};
|
||||
}, window));
|
||||
@ -21,9 +21,6 @@ export const localStorage = {
|
||||
}
|
||||
};
|
||||
|
||||
export function setSidebarStatus(sidebarStatus: string) {
|
||||
localStorage.set(SidebarStatusKey, sidebarStatus);
|
||||
}
|
||||
// 布局大小
|
||||
const SizeKey = 'size';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { useUserStoreHook } from '@/store/modules/user';
|
||||
@ -12,7 +12,7 @@ const service = axios.create({
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
(config: any) => {
|
||||
if (!config.headers) {
|
||||
throw new Error(
|
||||
`Expected 'config' and 'config.headers' not to be undefined`
|
||||
@ -31,10 +31,8 @@ service.interceptors.request.use(
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
(response: any) => {
|
||||
const { status, msg } = response;
|
||||
// console.log(msg)
|
||||
// console.log(response);
|
||||
if (status === 200) {
|
||||
if (response.data.code == 401) {
|
||||
message.error(response.data.msg||'请求失败');
|
||||
@ -62,7 +60,7 @@ service.interceptors.response.use(
|
||||
title: "提示",
|
||||
content: "当前页面已失效,请重新登录",
|
||||
okText: "确定",
|
||||
cancelButtonProps: { style: { display: "none" } },
|
||||
cancelButtonProps: { disabled: false },
|
||||
onOk: () => {
|
||||
localStorage.clear();
|
||||
window.location.href = '/';
|
||||
|
||||
@ -4,22 +4,22 @@ import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
|
||||
import RightDrawer from "@/components/RightDrawer/index.vue";
|
||||
import jidiInfoMod from "@/modules/jidiInfoMod/index.vue";
|
||||
import shuidianhuangjingjieruMod from "@/modules/shuidianhuangjingjieruMod/index.vue";
|
||||
import { getQgcStaticData } from "@/api/ecoFlow";
|
||||
// import { getQgcStaticData } from "@/api/ecoFlow";
|
||||
onMounted(() => {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: "and",
|
||||
filters: [
|
||||
{ field: "dtin", operator: "eq", dataType: "string", value: "1" },
|
||||
{ field: "type", operator: "eq", dataType: "string", value: "hour" },
|
||||
{ field: "tm", operator: "gte", dataType: "date", value: "2026-03-02 00:00:00" },
|
||||
{ field: "tm", operator: "lte", dataType: "date", value: "2026-04-02 23:59:59" },
|
||||
],
|
||||
},
|
||||
};
|
||||
getQgcStaticData(params).then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
// const params = {
|
||||
// filter: {
|
||||
// logic: "and",
|
||||
// filters: [
|
||||
// { field: "dtin", operator: "eq", dataType: "string", value: "1" },
|
||||
// { field: "type", operator: "eq", dataType: "string", value: "hour" },
|
||||
// { field: "tm", operator: "gte", dataType: "date", value: "2026-03-02 00:00:00" },
|
||||
// { field: "tm", operator: "lte", dataType: "date", value: "2026-04-02 23:59:59" },
|
||||
// ],
|
||||
// },
|
||||
// };
|
||||
// getQgcStaticData(params).then((res) => {
|
||||
// console.log(res);
|
||||
// });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -240,7 +240,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, toRefs, watch, nextTick } from "vue";
|
||||
import { onMounted, reactive, ref, toRefs, watch } from "vue";
|
||||
import loginImg from "@/assets/images/logo.png";
|
||||
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
|
||||
import { getCaptcha } from "@/api/auth";
|
||||
@ -322,25 +322,27 @@ const forgotPasswordRules = ref({
|
||||
],
|
||||
});
|
||||
|
||||
const { loginData, loginRules, loading, passwordType, capslockTooltipDisabled } = toRefs(
|
||||
const { loginData, loginRules, loading,
|
||||
// passwordType, capslockTooltipDisabled
|
||||
} = toRefs(
|
||||
state
|
||||
);
|
||||
|
||||
function checkCapslock(e: any) {
|
||||
const { key } = e;
|
||||
state.capslockTooltipDisabled = key && key.length === 1 && key >= "A" && key <= "Z";
|
||||
}
|
||||
// function checkCapslock(e: any) {
|
||||
// const { key } = e;
|
||||
// state.capslockTooltipDisabled = key && key.length === 1 && key >= "A" && key <= "Z";
|
||||
// }
|
||||
|
||||
function showPwd() {
|
||||
if (passwordType.value === "password") {
|
||||
passwordType.value = "";
|
||||
} else {
|
||||
passwordType.value = "password";
|
||||
}
|
||||
nextTick(() => {
|
||||
passwordRef.value.focus();
|
||||
});
|
||||
}
|
||||
// function showPwd() {
|
||||
// if (passwordType.value === "password") {
|
||||
// passwordType.value = "";
|
||||
// } else {
|
||||
// passwordType.value = "password";
|
||||
// }
|
||||
// nextTick(() => {
|
||||
// passwordRef.value.focus();
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* 登录
|
||||
@ -432,9 +434,9 @@ const startCountdown = () => {
|
||||
};
|
||||
|
||||
// 显示忘记密码页面
|
||||
const showForgotPasswordPage = () => {
|
||||
showForgotPassword.value = true;
|
||||
};
|
||||
// const showForgotPasswordPage = () => {
|
||||
// showForgotPassword.value = true;
|
||||
// };
|
||||
|
||||
// 返回登录页面
|
||||
const backToLogin = () => {
|
||||
@ -448,45 +450,45 @@ const backToLogin = () => {
|
||||
};
|
||||
|
||||
// 发送短信验证码
|
||||
const sendSms = async () => {
|
||||
// 检查手机号是否为空
|
||||
if (!loginData.value.username) {
|
||||
message.error("请输入手机号");
|
||||
return;
|
||||
}
|
||||
// const sendSms = async () => {
|
||||
// // 检查手机号是否为空
|
||||
// if (!loginData.value.username) {
|
||||
// message.error("请输入手机号");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 检查手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(loginData.value.username)) {
|
||||
message.error("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
// // 检查手机号格式
|
||||
// const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
// if (!phoneRegex.test(loginData.value.username)) {
|
||||
// message.error("请输入正确的手机号");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 如果正在倒计时,不允许重复发送
|
||||
if (smsCountdown.value > 0) {
|
||||
return;
|
||||
}
|
||||
// // 如果正在倒计时,不允许重复发送
|
||||
// if (smsCountdown.value > 0) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
loading.value = true;
|
||||
smsButtonDisabled.value = true;
|
||||
// loading.value = true;
|
||||
// smsButtonDisabled.value = true;
|
||||
|
||||
try {
|
||||
// 模拟发送短信验证码接口
|
||||
// await axios.post('/sms/send', { phone: loginForm.value.username })
|
||||
// try {
|
||||
// // 模拟发送短信验证码接口
|
||||
// // await axios.post('/sms/send', { phone: loginForm.value.username })
|
||||
|
||||
// 模拟发送成功
|
||||
message.success("验证码发送成功");
|
||||
// // 模拟发送成功
|
||||
// message.success("验证码发送成功");
|
||||
|
||||
// 开始倒计时
|
||||
startCountdown();
|
||||
} catch (error) {
|
||||
console.error("发送验证码失败", error);
|
||||
message.error("验证码发送失败,请重试");
|
||||
smsButtonDisabled.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// // 开始倒计时
|
||||
// startCountdown();
|
||||
// } catch (error) {
|
||||
// console.error("发送验证码失败", error);
|
||||
// message.error("验证码发送失败,请重试");
|
||||
// smsButtonDisabled.value = false;
|
||||
// } finally {
|
||||
// loading.value = false;
|
||||
// }
|
||||
// };
|
||||
// 发送忘记密码短信验证码
|
||||
const sendForgotPasswordSms = async () => {
|
||||
// 检查手机号是否为空
|
||||
|
||||
@ -241,7 +241,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { setPath } from '@/utils/auth';
|
||||
import { onMounted, reactive, ref, toRefs, watch, nextTick } from "vue";
|
||||
import { onMounted, reactive, ref, toRefs, watch } from "vue";
|
||||
import loginImg from "@/assets/images/logo.png";
|
||||
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
|
||||
import { getCaptcha } from "@/api/auth";
|
||||
@ -322,25 +322,27 @@ const forgotPasswordRules = ref({
|
||||
],
|
||||
});
|
||||
|
||||
const { loginData, loginRules, loading, passwordType, capslockTooltipDisabled } = toRefs(
|
||||
const { loginData, loginRules, loading,
|
||||
// passwordType, capslockTooltipDisabled
|
||||
} = toRefs(
|
||||
state
|
||||
);
|
||||
|
||||
function checkCapslock(e: any) {
|
||||
const { key } = e;
|
||||
state.capslockTooltipDisabled = key && key.length === 1 && key >= "A" && key <= "Z";
|
||||
}
|
||||
// function checkCapslock(e: any) {
|
||||
// const { key } = e;
|
||||
// state.capslockTooltipDisabled = key && key.length === 1 && key >= "A" && key <= "Z";
|
||||
// }
|
||||
|
||||
function showPwd() {
|
||||
if (passwordType.value === "password") {
|
||||
passwordType.value = "";
|
||||
} else {
|
||||
passwordType.value = "password";
|
||||
}
|
||||
nextTick(() => {
|
||||
passwordRef.value.focus();
|
||||
});
|
||||
}
|
||||
// function showPwd() {
|
||||
// if (passwordType.value === "password") {
|
||||
// passwordType.value = "";
|
||||
// } else {
|
||||
// passwordType.value = "password";
|
||||
// }
|
||||
// nextTick(() => {
|
||||
// passwordRef.value.focus();
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* 登录
|
||||
|
||||
@ -5,23 +5,33 @@
|
||||
:import-btn="importBtn"
|
||||
:save-btn="saveBtn"
|
||||
:handle-add="handleAdd"
|
||||
:batchData="batchData"
|
||||
:batchDel="batchDel"
|
||||
@search-finish="handleSearchFinish"
|
||||
/>
|
||||
|
||||
<!-- 主表格 -->
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:row-selection="rowSelection"
|
||||
:data-source="tableData"
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:pagination="paginationConfig"
|
||||
:scroll="{ x: '100%' }"
|
||||
row-key="key"
|
||||
:list-url="getFishDraftPage"
|
||||
:search-params="{}"
|
||||
:enable-row-selection="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<!-- 自定义插槽渲染可根据需要扩展,这里主要依赖 columns 中的 render 函数逻辑,但在 Vue中通常使用 slot 或 h 函数 -->
|
||||
<!-- 注意:Antdv 的 columns render 支持返回 VNode 或字符串 -->
|
||||
</a-table>
|
||||
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
|
||||
<div class="flex">
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" danger size="small" @click="handleDelete([record.id])"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<!-- <BasicTable :columns="columns" :listUrl="getFishDraftPage" /> -->
|
||||
|
||||
<!-- 导入预览 Modal -->
|
||||
<a-modal
|
||||
@ -29,7 +39,7 @@
|
||||
ok-text="提交导入"
|
||||
cancel-text="取消"
|
||||
:width="1500"
|
||||
:open="visible"
|
||||
v-model:open="visible"
|
||||
:confirm-loading="fileLoading"
|
||||
@cancel="handleModalCancel"
|
||||
@ok="handleModalOk"
|
||||
@ -39,7 +49,6 @@
|
||||
:loading="fileLoading"
|
||||
:data-source="fileTableData"
|
||||
:columns="modalColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 500, x: '100%' }"
|
||||
row-key="index"
|
||||
>
|
||||
@ -50,7 +59,7 @@
|
||||
<!-- 新增/编辑 Modal (对应 React 的 EditModal) -->
|
||||
<!-- 假设已创建对应的 Vue 组件 GuoYuSheShiShuJuTianBaoForm -->
|
||||
<EditModal
|
||||
v-model:visible="editModalVisible"
|
||||
v-model:visible="editModalVisible"
|
||||
:initial-values="currentRecord"
|
||||
:loading="submitLoading"
|
||||
@cancel="editModalCancel"
|
||||
@ -60,12 +69,18 @@
|
||||
<!-- 视频预览 Modal -->
|
||||
<a-modal
|
||||
title="视频预览"
|
||||
:open="videoPreviewVisible"
|
||||
v-model:open="videoPreviewVisible"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
@cancel="closeVideoPreview"
|
||||
>
|
||||
<video v-if="currentVideoUrl" controls autoplay style="width: 100%" :src="currentVideoUrl">
|
||||
<video
|
||||
v-if="currentVideoUrl"
|
||||
controls
|
||||
autoplay
|
||||
style="width: 100%"
|
||||
:src="currentVideoUrl"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</a-modal>
|
||||
@ -73,97 +88,120 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue' // 假设使用 ant-design-vue
|
||||
import JSZip from 'jszip'
|
||||
import * as XLSX from 'xlsx'
|
||||
import GuoYuSheShiShuJuTianBaoSearch from './guoYuSheShiShuJuTianBaoSearch.vue'
|
||||
import EditModal from './guoYuSheShiShuJuTianBaoForm.vue'
|
||||
import { ref, computed, onMounted, h } from "vue";
|
||||
import { message, Modal } from "ant-design-vue"; // 假设使用 ant-design-vue
|
||||
import JSZip from "jszip";
|
||||
import * as XLSX from "xlsx";
|
||||
import BasicTable from "@/components/BasicTable/index.vue";
|
||||
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
|
||||
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
|
||||
import {
|
||||
getFishDraftPage,
|
||||
addFishDraft,
|
||||
editFishDraft,
|
||||
delFishDraft,
|
||||
} from "@/api/guoYuSheShiShuJuTianBao";
|
||||
import dayjs from "dayjs";
|
||||
import { Tag } from 'ant-design-vue'; // 确保导入 Tag
|
||||
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface FormData {
|
||||
[key: string]: any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface ColumnConfig {
|
||||
dataIndex: string
|
||||
key: string
|
||||
title: string
|
||||
width?: number
|
||||
dataIndex: string;
|
||||
key: string;
|
||||
title: string;
|
||||
width?: number;
|
||||
customRender?: (text: any, record: any) => any;
|
||||
}
|
||||
|
||||
const tableRef = ref<any>(null);
|
||||
// --- 基础配置 ---
|
||||
const baseColumnsConfig: ColumnConfig[] = [
|
||||
{ dataIndex: 'stcd', key: 'stcd', title: '水电基地', width: 100 },
|
||||
{ dataIndex: 'title', key: 'title', title: '电站名称', width: 120 },
|
||||
{ dataIndex: 'office', key: 'office', title: '过鱼设施名称', width: 150 },
|
||||
{ dataIndex: 'regionName', key: 'regionName', title: '过鱼时间', width: 120 },
|
||||
{ dataIndex: 'location', key: 'location', title: '鱼种类', width: 120 },
|
||||
{ dataIndex: 'location11', key: 'location', title: '是否鱼苗', width: 120 },
|
||||
{ dataIndex: 'DIRECTION', key: 'DIRECTION', title: '游向', width: 120 },
|
||||
{ dataIndex: 'level1', key: 'level1', title: '过鱼数量(尾)', width: 160 },
|
||||
{ dataIndex: 'level2', key: 'level2', title: '体长', width: 120 },
|
||||
{ dataIndex: 'level3', key: 'level3', title: '平均体重', width: 120 },
|
||||
{ dataIndex: 'level4', key: 'level4', title: '水温', width: 120 },
|
||||
{ dataIndex: 'level5', key: 'level5', title: '图片', width: 100 },
|
||||
{ dataIndex: 'level6', key: 'level6', title: '视频', width: 100 },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 100 }
|
||||
]
|
||||
{ dataIndex: "engName", key: "engName", title: "水电基地", width: 100 },
|
||||
{ dataIndex: "baseName", key: "baseName", title: "电站名称", width: 120 },
|
||||
{ dataIndex: "fpname", key: "fpname", title: "过鱼设施名称", width: 150 },
|
||||
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 150 },
|
||||
{ dataIndex: "ftp", key: "ftp", title: "鱼种类", width: 120 },
|
||||
{
|
||||
dataIndex: "isfs",
|
||||
key: "isfs",
|
||||
title: "是否鱼苗",
|
||||
width: 74,
|
||||
customRender: ({ text }: any) => {
|
||||
const isYes = text === 1 || text === '1';
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
color: isYes ? 'success' : 'error', // Antdv Tag 的颜色预设
|
||||
style: { margin: 0 } // 去除默认 margin,使其在表格中对齐更好
|
||||
},
|
||||
() => isYes ? '是' : '否'
|
||||
);
|
||||
},
|
||||
},
|
||||
{ dataIndex: "direction", key: "direction", title: "游向", width: 80 },
|
||||
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
|
||||
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 },
|
||||
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 },
|
||||
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
|
||||
{ dataIndex: "picpth", key: "level5", title: "图片", width: 100 },
|
||||
{ dataIndex: "vdpth", key: "level6", title: "视频", width: 100 },
|
||||
{ dataIndex: "tm", key: "tm", title: "填报时间", width: 150 },
|
||||
{ dataIndex: "status", key: "status", title: "状态", width: 100 },
|
||||
];
|
||||
|
||||
// --- 状态定义 ---
|
||||
const searchData = ref<any>(null)
|
||||
const visible = ref(false) // 导入预览 Modal
|
||||
const visible = ref(false); // 导入预览 Modal
|
||||
|
||||
// 编辑相关状态
|
||||
const editModalVisible = ref(false)
|
||||
const currentRecord = ref<FormData | null>(null)
|
||||
const submitLoading = ref(false)
|
||||
const editModalVisible = ref(false);
|
||||
const currentRecord = ref<FormData | null>(null);
|
||||
const submitLoading = ref(false);
|
||||
|
||||
// 视频预览相关状态
|
||||
const videoPreviewVisible = ref(false)
|
||||
const currentVideoUrl = ref<string>('')
|
||||
const videoPreviewVisible = ref(false);
|
||||
const currentVideoUrl = ref<string>("");
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const fileTableData = ref<any[]>([])
|
||||
const tableData = ref<any[]>([]);
|
||||
const fileTableData = ref<any[]>([]);
|
||||
const batchData = ref<any[]>([]);
|
||||
|
||||
const loading = ref(false)
|
||||
const fileLoading = ref(false)
|
||||
const fileLoading = ref(false);
|
||||
|
||||
// 行内编辑 Key (用于导入预览表格)
|
||||
const editingKey = ref<string | number>('')
|
||||
const editingKey = ref<string | number>("");
|
||||
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(10)
|
||||
|
||||
// --- 辅助函数 ---
|
||||
|
||||
// 从 Zip 获取 Blob URL
|
||||
const getBlobUrlFromZip = async (zip: JSZip, fileName: string): Promise<string> => {
|
||||
try {
|
||||
const file = zip.file(fileName)
|
||||
if (!file) return ''
|
||||
const blob = await file.async('blob')
|
||||
return URL.createObjectURL(blob)
|
||||
} catch (e) {
|
||||
console.error('Extract file failed', e)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
// const getBlobUrlFromZip = async (zip: JSZip, fileName: string): Promise<string> => {
|
||||
// try {
|
||||
// const file = zip.file(fileName);
|
||||
// if (!file) return "";
|
||||
// const blob = await file.async("blob");
|
||||
// return URL.createObjectURL(blob);
|
||||
// } catch (e) {
|
||||
// console.error("Extract file failed", e);
|
||||
// return "";
|
||||
// }
|
||||
// };
|
||||
|
||||
// 渲染媒体单元格 (返回 VNode 或简单结构,实际在 Antdv columns render 中处理)
|
||||
// 在 Vue Antdv 中,render 函数接收 (text, record, index)
|
||||
const createMediaRender = (type: 'image' | 'video') => {
|
||||
return (text: string) => {
|
||||
if (!text) return '-'
|
||||
// 这里简化处理,实际项目中可能需要使用 h 函数渲染图标和点击事件
|
||||
// 由于无法直接在这里绑定 click 事件到简单的字符串返回,建议在 columns 定义中使用 slots 或 h 函数
|
||||
// 为了保持逻辑清晰,这里仅返回文本提示,实际 UI 需结合 Antdv 的 customRender
|
||||
return type === 'image' ? '查看图片' : '播放视频'
|
||||
}
|
||||
}
|
||||
// const createMediaRender = (type: "image" | "video") => {
|
||||
// return (text: string) => {
|
||||
// if (!text) return "-";
|
||||
// // 这里简化处理,实际项目中可能需要使用 h 函数渲染图标和点击事件
|
||||
// // 由于无法直接在这里绑定 click 事件到简单的字符串返回,建议在 columns 定义中使用 slots 或 h 函数
|
||||
// // 为了保持逻辑清晰,这里仅返回文本提示,实际 UI 需结合 Antdv 的 customRender
|
||||
// return type === "image" ? "查看图片" : "播放视频";
|
||||
// };
|
||||
// };
|
||||
|
||||
// --- Columns 定义 ---
|
||||
|
||||
@ -171,337 +209,338 @@ const createMediaRender = (type: 'image' | 'video') => {
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
...baseColumnsConfig.map((col) => {
|
||||
if (col.dataIndex === 'level5') {
|
||||
if (col.dataIndex === "level5") {
|
||||
return {
|
||||
...col,
|
||||
customRender: ({ text }: any) => {
|
||||
if(!text) return '-'
|
||||
// 实际应渲染 Icon 和点击事件,此处简化
|
||||
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`
|
||||
}
|
||||
}
|
||||
if (!text) return "-";
|
||||
// 实际应渲染 Icon 和点击事件,此处简化
|
||||
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (col.dataIndex === 'level6') {
|
||||
if (col.dataIndex === "level6") {
|
||||
return {
|
||||
...col,
|
||||
customRender: ({ text }: any) => {
|
||||
if(!text) return '-'
|
||||
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`
|
||||
}
|
||||
}
|
||||
if (!text) return "-";
|
||||
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
return { ...col, visible: true }
|
||||
return { ...col, visible: true };
|
||||
}),
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
customRender: ({ record, index }: any) => {
|
||||
// 在 Vue 模板中,通常通过 slot #bodyCell 来处理复杂操作列
|
||||
// 这里仅做逻辑占位,实际需在 template 中定义 <template #bodyCell="{ column, record, index }">
|
||||
return '操作列'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
title: "操作",
|
||||
key: "action",
|
||||
dataIndex: "action",
|
||||
fixed: "right",
|
||||
width: 100,
|
||||
align: "center",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// 导入预览表格 Columns (包含行内编辑逻辑)
|
||||
const modalColumns = computed(() => {
|
||||
const isEditing = (record: any, index: number) => index === editingKey.value
|
||||
const isEditing = (_record: any, index: number) => index === editingKey.value;
|
||||
|
||||
const save = async (index: number) => {
|
||||
// 保存逻辑:实际上 fileTableData 是响应式的,input change 时已经更新
|
||||
editingKey.value = ''
|
||||
message.success('行数据已更新')
|
||||
}
|
||||
// const save = async (index: number) => {
|
||||
// // 保存逻辑:实际上 fileTableData 是响应式的,input change 时已经更新
|
||||
// editingKey.value = "";
|
||||
// message.success("行数据已更新");
|
||||
// };
|
||||
|
||||
const deleteRow = (index: number) => {
|
||||
fileTableData.value = fileTableData.value.filter((_, i) => i !== index)
|
||||
message.success('行数据已删除')
|
||||
}
|
||||
// const deleteRow = (index: number) => {
|
||||
// fileTableData.value = fileTableData.value.filter((_, i) => i !== index);
|
||||
// message.success("行数据已删除");
|
||||
// };
|
||||
|
||||
return baseColumnsConfig.map((col) => ({
|
||||
...col,
|
||||
customRender: ({ text, record, index }: any) => {
|
||||
const editing = isEditing(record, index)
|
||||
return baseColumnsConfig
|
||||
.map((col) => ({
|
||||
...col,
|
||||
customRender: ({ text, record, index }: any) => {
|
||||
const editing = isEditing(record, index);
|
||||
|
||||
// 如果是媒体列
|
||||
if (col.dataIndex === 'level5' || col.dataIndex === 'level6') {
|
||||
if (editing) {
|
||||
// 返回 Input 组件的 VNode 或标识,实际需用 slot 或 h 函数
|
||||
return 'Input编辑中'
|
||||
// 如果是媒体列
|
||||
if (col.dataIndex === "level5" || col.dataIndex === "level6") {
|
||||
if (editing) {
|
||||
// 返回 Input 组件的 VNode 或标识,实际需用 slot 或 h 函数
|
||||
return "Input编辑中";
|
||||
}
|
||||
return col.dataIndex === "level5" ? "查看图片" : "播放视频";
|
||||
}
|
||||
return col.dataIndex === 'level5' ? '查看图片' : '播放视频'
|
||||
}
|
||||
|
||||
// 普通列
|
||||
if (editing) {
|
||||
// 返回 Input 组件标识
|
||||
return 'Input编辑中'
|
||||
}
|
||||
return text
|
||||
},
|
||||
// Antdv 支持通过 slots 自定义单元格内容以实现交互
|
||||
slots: { customRender: `cell-${col.dataIndex}` }
|
||||
})).concat({
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: 140,
|
||||
align: 'center',
|
||||
customRender: ({ record, index }: any) => {
|
||||
const editable = isEditing(record, index)
|
||||
return editable ? '保存/取消' : '修改/删除'
|
||||
},
|
||||
slots: { customRender: 'cell-operation' }
|
||||
})
|
||||
})
|
||||
// 普通列
|
||||
if (editing) {
|
||||
// 返回 Input 组件标识
|
||||
return "Input编辑中";
|
||||
}
|
||||
return text;
|
||||
},
|
||||
// Antdv 支持通过 slots 自定义单元格内容以实现交互
|
||||
slots: { customRender: `cell-${col.dataIndex}` },
|
||||
}))
|
||||
// .concat({
|
||||
// title: "操作",
|
||||
// dataIndex: "operation",
|
||||
// fixed: "right",
|
||||
// width: 140,
|
||||
// align: "center",
|
||||
// customRender: ({ record, index }: any) => {
|
||||
// const editable = isEditing(record, index);
|
||||
// return editable ? "保存/取消" : "修改/删除";
|
||||
// },
|
||||
// slots: { customRender: "cell-operation" },
|
||||
// });
|
||||
});
|
||||
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: any[]) => {
|
||||
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: record.name === 'Disabled User', // Column configuration not to be checked
|
||||
name: record.name,
|
||||
}),
|
||||
};
|
||||
// --- 业务逻辑方法 ---
|
||||
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
editModalVisible.value = true
|
||||
}
|
||||
currentRecord.value = null;
|
||||
editModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
currentRecord.value = { ...record }
|
||||
editModalVisible.value = true
|
||||
}
|
||||
currentRecord.value = { ...record };
|
||||
editModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteMain = (index: number) => {
|
||||
// 删除过鱼数据
|
||||
const handleDelete = (ids: any[]) => {
|
||||
console.log(ids)
|
||||
Modal.confirm({
|
||||
title: '确定删除这条数据吗?',
|
||||
onOk: () => {
|
||||
tableData.value = tableData.value.filter((_, i) => i !== index)
|
||||
message.success('删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
title: "是否确认删除选中数据吗?",
|
||||
onOk: async () => {
|
||||
let res: any = await delFishDraft(ids);
|
||||
if (res && res?.code == 0) {
|
||||
message.success("删除成功");
|
||||
tableRef.value?.getList();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
// 批量删除
|
||||
const batchDel = () => {
|
||||
handleDelete(batchData.value);
|
||||
};
|
||||
// 多选
|
||||
const handleSelectionChange = (keys: any) => {
|
||||
batchData.value = keys;
|
||||
};
|
||||
const editModalCancel = () => {
|
||||
editModalVisible.value = false
|
||||
}
|
||||
editModalVisible.value = false;
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (values: FormData) => {
|
||||
submitLoading.value = true
|
||||
submitLoading.value = true;
|
||||
console.log(values);
|
||||
// 模拟异步请求
|
||||
setTimeout(() => {
|
||||
if (currentRecord.value) {
|
||||
// 编辑逻辑
|
||||
const newData = tableData.value.map(item => {
|
||||
// 简单比较,实际建议用 ID
|
||||
if (JSON.stringify(item) === JSON.stringify(currentRecord.value)) { // 注意:浅比较可能不够,需根据实际 ID
|
||||
return { ...item, ...values }
|
||||
}
|
||||
return item
|
||||
})
|
||||
// 更稳妥的方式是通过 key 查找
|
||||
const targetIndex = tableData.value.findIndex(item => item.key === currentRecord.value?.key)
|
||||
if(targetIndex > -1) {
|
||||
tableData.value[targetIndex] = { ...tableData.value[targetIndex], ...values }
|
||||
}
|
||||
message.success('编辑成功')
|
||||
} else {
|
||||
// 新增逻辑
|
||||
const newRecord = { ...values, key: Date.now() }
|
||||
tableData.value = [newRecord, ...tableData.value]
|
||||
message.success('新增成功')
|
||||
// setTimeout(() => {
|
||||
if (currentRecord.value) {
|
||||
// 编辑逻辑
|
||||
|
||||
let res: any = await editFishDraft({
|
||||
...values
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
message.success("编辑成功");
|
||||
editModalVisible.value = false;
|
||||
tableRef.value?.getList();
|
||||
}
|
||||
submitLoading.value = false
|
||||
editModalVisible.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const getData = async (searchDataParam?: any, label?: string) => {
|
||||
loading.value = true
|
||||
// TODO: 实现实际的数据获取 API 调用
|
||||
console.log('Fetching data with:', searchDataParam)
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
submitLoading.value = false;
|
||||
// const newData = tableData.value.map((item) => {
|
||||
// // 简单比较,实际建议用 ID
|
||||
// if (JSON.stringify(item) === JSON.stringify(currentRecord.value)) {
|
||||
// // 注意:浅比较可能不够,需根据实际 ID
|
||||
// return { ...item, ...values };
|
||||
// }
|
||||
// return item;
|
||||
// });
|
||||
// // 更稳妥的方式是通过 key 查找
|
||||
// const targetIndex = tableData.value.findIndex(
|
||||
// (item) => item.key === currentRecord.value?.key
|
||||
// );
|
||||
// if (targetIndex > -1) {
|
||||
// tableData.value[targetIndex] = { ...tableData.value[targetIndex], ...values };
|
||||
// }
|
||||
} else {
|
||||
// 新增逻辑
|
||||
let res: any = await addFishDraft({
|
||||
...values,
|
||||
tm: dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
stcd: 1,
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
message.success("新增成功");
|
||||
editModalVisible.value = false;
|
||||
tableRef.value?.getList();
|
||||
}
|
||||
submitLoading.value = false;
|
||||
}
|
||||
// }, 500);
|
||||
};
|
||||
|
||||
const parseExcelFile = async (fileName: string, arrayBuffer: ArrayBuffer) => {
|
||||
try {
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
|
||||
const firstSheetName = workbook.SheetNames[0]
|
||||
if (!firstSheetName) throw new Error('Excel文件中没有工作表')
|
||||
const worksheet = workbook.Sheets[firstSheetName]
|
||||
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet)
|
||||
return jsonData
|
||||
const workbook = XLSX.read(arrayBuffer, {
|
||||
type: "array",
|
||||
cellDates: true, // 【关键】将日期单元格解析为 JS Date 对象
|
||||
dateNF: "yyyy-mm-dd", // 【关键】指定日期输出格式
|
||||
});
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
if (!firstSheetName) throw new Error("Excel文件中没有工作表");
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
console.error(`解析文件 ${fileName} 失败:`, error)
|
||||
message.error(`文件 ${fileName} 解析失败`)
|
||||
return []
|
||||
console.error(`解析文件 ${fileName} 失败:`, error);
|
||||
message.error(`文件 ${fileName} 解析失败`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalOk = () => {
|
||||
tableData.value = [...fileTableData.value]
|
||||
visible.value = false
|
||||
message.success('数据已导入至列表')
|
||||
}
|
||||
tableData.value = [...fileTableData.value];
|
||||
visible.value = false;
|
||||
message.success("数据已导入至列表");
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
visible.value = false
|
||||
editingKey.value = ''
|
||||
}
|
||||
visible.value = false;
|
||||
editingKey.value = "";
|
||||
};
|
||||
|
||||
const importBtn = async (file: File) => {
|
||||
fileLoading.value = true
|
||||
editingKey.value = ''
|
||||
const hideMessage = message.loading('正在解析压缩包...', 0)
|
||||
fileLoading.value = true;
|
||||
editingKey.value = "";
|
||||
const hideMessage = message.loading("正在解析压缩包...", 0);
|
||||
|
||||
try {
|
||||
const zip = await JSZip.loadAsync(file)
|
||||
const zipPathMap: Record<string, string> = {}
|
||||
const zip = await JSZip.loadAsync(file);
|
||||
const zipPathMap: Record<string, string> = {};
|
||||
|
||||
// 构建路径映射
|
||||
zip.forEach((relativePath, zipEntry) => {
|
||||
if (!zipEntry.dir) {
|
||||
const lowerPath = relativePath.toLowerCase()
|
||||
zipPathMap[lowerPath] = relativePath
|
||||
const pathParts = relativePath.split('/')
|
||||
const lowerPath = relativePath.toLowerCase();
|
||||
zipPathMap[lowerPath] = relativePath;
|
||||
const pathParts = relativePath.split("/");
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const subPath = pathParts.slice(i).join('/')
|
||||
if (subPath) zipPathMap[subPath.toLowerCase()] = relativePath
|
||||
const subPath = pathParts.slice(i).join("/");
|
||||
if (subPath) zipPathMap[subPath.toLowerCase()] = relativePath;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const fileNames = Object.keys(zip.files)
|
||||
const fileNames = Object.keys(zip.files);
|
||||
if (fileNames.length === 0) {
|
||||
hideMessage()
|
||||
message.warning('压缩包为空')
|
||||
fileLoading.value = false
|
||||
return
|
||||
hideMessage();
|
||||
message.warning("压缩包为空");
|
||||
fileLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let allExcelData: any[] = []
|
||||
let allExcelData: any[] = [];
|
||||
for (const fileName of fileNames) {
|
||||
const zipEntry = zip.files[fileName]
|
||||
if (zipEntry.dir) continue
|
||||
if (!fileName.match(/\.(xls|xlsx)$/i)) continue
|
||||
const zipEntry = zip.files[fileName];
|
||||
if (zipEntry.dir) continue;
|
||||
if (!fileName.match(/\.(xls|xlsx)$/i)) continue;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await zipEntry.async('arraybuffer')
|
||||
const data = await parseExcelFile(fileName, arrayBuffer)
|
||||
if (!data || data.length === 0) continue
|
||||
const arrayBuffer = await zipEntry.async("arraybuffer");
|
||||
const data = await parseExcelFile(fileName, arrayBuffer);
|
||||
if (!data || data.length === 0) continue;
|
||||
|
||||
const transformedData = await Promise.all(
|
||||
data.map(async (item: any) => {
|
||||
const newObj: any = {}
|
||||
const newObj: any = {};
|
||||
for (const excelKey in item) {
|
||||
if (!Object.prototype.hasOwnProperty.call(item, excelKey)) continue
|
||||
const value = item[excelKey]
|
||||
if (!Object.prototype.hasOwnProperty.call(item, excelKey)) continue;
|
||||
const value = item[excelKey];
|
||||
// 模糊匹配列标题
|
||||
const matchedCol = baseColumnsConfig.find((col) =>
|
||||
excelKey.includes(col.title) || col.title.includes(excelKey)
|
||||
)
|
||||
const matchedCol = baseColumnsConfig.find(
|
||||
(col) => excelKey.includes(col.title) || col.title.includes(excelKey)
|
||||
);
|
||||
|
||||
if (matchedCol) {
|
||||
let finalValue = value
|
||||
let finalValue = value;
|
||||
// 处理图片和视频路径提取
|
||||
if ((matchedCol.dataIndex === 'level5' || matchedCol.dataIndex === 'level6') && value && typeof value === 'string') {
|
||||
const trimPath = value.trim().replace(/\\/g, '/')
|
||||
if (
|
||||
(matchedCol.dataIndex === "level5" ||
|
||||
matchedCol.dataIndex === "level6") &&
|
||||
value &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
const trimPath = value.trim().replace(/\\/g, "/");
|
||||
if (trimPath) {
|
||||
const searchKey = trimPath.toLowerCase()
|
||||
const realPath = zipPathMap[searchKey]
|
||||
const searchKey = trimPath.toLowerCase();
|
||||
const realPath = zipPathMap[searchKey];
|
||||
if (realPath) {
|
||||
try {
|
||||
const zipFile = zip.file(realPath)
|
||||
const zipFile = zip.file(realPath);
|
||||
if (zipFile) {
|
||||
const blob = await zipFile.async('blob')
|
||||
finalValue = URL.createObjectURL(blob)
|
||||
const blob = await zipFile.async("blob");
|
||||
finalValue = URL.createObjectURL(blob);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to extract blob for: ${realPath}`, e)
|
||||
console.error(`Failed to extract blob for: ${realPath}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newObj[matchedCol.dataIndex] = finalValue
|
||||
newObj[matchedCol.dataIndex] = finalValue;
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
return newObj;
|
||||
})
|
||||
)
|
||||
allExcelData = [...allExcelData, ...transformedData]
|
||||
);
|
||||
allExcelData = [...allExcelData, ...transformedData];
|
||||
} catch (err) {
|
||||
console.error(`读取文件 ${fileName} 失败`, err)
|
||||
console.error(`读取文件 ${fileName} 失败`, err);
|
||||
}
|
||||
}
|
||||
|
||||
fileTableData.value = allExcelData
|
||||
visible.value = true
|
||||
hideMessage()
|
||||
message.success(`解析完成,共获取 ${allExcelData.length} 条数据`)
|
||||
fileTableData.value = allExcelData;
|
||||
visible.value = true;
|
||||
hideMessage();
|
||||
message.success(`解析完成,共获取 ${allExcelData.length} 条数据`);
|
||||
} catch (error) {
|
||||
hideMessage()
|
||||
console.error('ZIP 解析失败:', error)
|
||||
message.error('文件格式错误或解析失败')
|
||||
hideMessage();
|
||||
console.error("ZIP 解析失败:", error);
|
||||
message.error("文件格式错误或解析失败");
|
||||
} finally {
|
||||
fileLoading.value = false
|
||||
fileLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveBtn = async () => {
|
||||
// TODO: 实现保存逻辑
|
||||
console.log('Save button clicked')
|
||||
}
|
||||
console.log("Save button clicked");
|
||||
};
|
||||
|
||||
const handleSearchFinish = (e: any, label: string) => {
|
||||
const newSearchData = { ...searchData.value, ...e }
|
||||
searchData.value = newSearchData
|
||||
getData(newSearchData, label)
|
||||
}
|
||||
const handleSearchFinish = (values: any) => {
|
||||
console.log(values);
|
||||
// const newSearchData = { ...searchData.value, ...e };
|
||||
// searchData.value = newSearchData;
|
||||
// getData(newSearchData, label);
|
||||
};
|
||||
|
||||
const closeVideoPreview = () => {
|
||||
videoPreviewVisible.value = false
|
||||
currentVideoUrl.value = ''
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const paginationConfig = computed(() => ({
|
||||
size: 'small' as const,
|
||||
total: total.value,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
showQuickJumper: true,
|
||||
pageSize: size.value,
|
||||
current: page.value,
|
||||
onChange: (p: number, ps: number) => {
|
||||
page.value = p
|
||||
size.value = ps
|
||||
// 重新获取数据
|
||||
// getData(searchData.value)
|
||||
}
|
||||
}))
|
||||
videoPreviewVisible.value = false;
|
||||
currentVideoUrl.value = "";
|
||||
};
|
||||
|
||||
// --- 生命周期 ---
|
||||
onMounted(() => {
|
||||
// 初始化加载数据
|
||||
// getData()
|
||||
})
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.guoYuSheShiShuJuTianBao-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="isEdit ? '编辑数据' : '新增数据'"
|
||||
:opne="visible"
|
||||
v-model:open="modalVisible"
|
||||
:confirm-loading="loading"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
:destroy-on-close="true"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
@ -12,35 +12,35 @@
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
name="edit_form"
|
||||
:labelCol="{ span: 7 }"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="流域" name="stcd">
|
||||
<a-input v-model:value="formData.stcd" placeholder="请输入流域" />
|
||||
<a-form-item label="水电基地" name="engName">
|
||||
<a-input v-model:value="formData.engName" placeholder="请输入水电基地" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="电站名称" name="title">
|
||||
<a-input v-model:value="formData.title" placeholder="请输入电站名称" />
|
||||
<a-form-item label="电站名称" name="baseName">
|
||||
<a-input v-model:value="formData.baseName" placeholder="请输入电站名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼设施名称" name="office">
|
||||
<a-input v-model:value="formData.office" placeholder="请输入过鱼设施名称" />
|
||||
<a-form-item label="过鱼设施名称" name="fpname">
|
||||
<a-input v-model:value="formData.fpname" placeholder="请输入过鱼设施名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼时间" name="regionName">
|
||||
<a-form-item label="过鱼时间" name="strdt">
|
||||
<a-date-picker
|
||||
v-model:value="formData.regionName"
|
||||
v-model:value="formData.strdt"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期"
|
||||
/>
|
||||
</a-form-item>
|
||||
@ -49,228 +49,374 @@
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="鱼种类" name="location">
|
||||
<a-input v-model:value="formData.location" placeholder="请输入鱼种类" />
|
||||
<a-form-item label="鱼种类" name="ftp">
|
||||
<a-input v-model:value="formData.ftp" placeholder="请输入鱼种类" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="游向" name="DIRECTION">
|
||||
<a-select v-model:value="formData.DIRECTION" placeholder="请选择游向" allow-clear>
|
||||
<a-select-option value="上行">上行</a-select-option>
|
||||
<a-select-option value="下行">下行</a-select-option>
|
||||
<a-select-option value="其他">其他</a-select-option>
|
||||
</a-select>
|
||||
<a-form-item label="是否鱼苗" name="isfs">
|
||||
<a-radio-group v-model:value="formData.isfs">
|
||||
<a-radio :value="1">是</a-radio>
|
||||
<a-radio :value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼数量(尾)" name="level1">
|
||||
<a-form-item label="游向" name="direction">
|
||||
<a-select
|
||||
v-model:value="formData.direction"
|
||||
placeholder="请选择游向"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="上行">上行</a-select-option>
|
||||
<a-select-option value="下行">下行</a-select-option>
|
||||
<a-select-option value="上行折返">上行折返</a-select-option>
|
||||
<a-select-option value="下行折返">下行折返</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="过鱼数量(尾)" name="fcnt">
|
||||
<a-input-number
|
||||
v-model:value="formData.level1"
|
||||
v-model:value="formData.fcnt"
|
||||
style="width: 100%"
|
||||
placeholder="数量"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="体长" name="level2">
|
||||
<a-input-number
|
||||
v-model:value="formData.level2"
|
||||
style="width: 100%"
|
||||
placeholder="体长"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="平均体重" name="level3">
|
||||
<a-input-number
|
||||
v-model:value="formData.level3"
|
||||
style="width: 100%"
|
||||
placeholder="平均体重"
|
||||
:min="0"
|
||||
/>
|
||||
<a-form-item
|
||||
label="体长(cm)"
|
||||
:validate-status="bodyLengthError ? 'error' : ''"
|
||||
:help="bodyLengthError"
|
||||
>
|
||||
<div class="flex">
|
||||
<a-input-number
|
||||
v-model:value="formData.bodyLengthMin"
|
||||
style="width: 50%"
|
||||
placeholder="请输入"
|
||||
:min="0"
|
||||
@change="validateBodyLength"
|
||||
/>
|
||||
<span class="px-[10px]">~</span>
|
||||
<a-input-number
|
||||
v-model:value="formData.bodyLengthMax"
|
||||
style="width: 50%"
|
||||
placeholder="请输入"
|
||||
:min="0"
|
||||
@change="validateBodyLength"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水温" name="level4">
|
||||
<a-form-item
|
||||
label="平均体重(g)"
|
||||
:validate-status="weightError ? 'error' : ''"
|
||||
:help="weightError"
|
||||
>
|
||||
<div class="flex">
|
||||
<a-input-number
|
||||
v-model:value="formData.weightMin"
|
||||
style="width: 50%"
|
||||
placeholder="请输入"
|
||||
:min="0"
|
||||
@change="validateWeight"
|
||||
/>
|
||||
<span class="px-[10px]">~</span>
|
||||
<a-input-number
|
||||
v-model:value="formData.weightMax"
|
||||
style="width: 50%"
|
||||
placeholder="请输入"
|
||||
:min="0"
|
||||
@change="validateWeight"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="水温(℃)" name="wt">
|
||||
<a-input-number
|
||||
v-model:value="formData.level4"
|
||||
v-model:value="formData.wt"
|
||||
style="width: 100%"
|
||||
placeholder="水温"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="图片" name="picpth">
|
||||
<!-- <a-input v-model:value="formData.picpth" placeholder="图片路径" /> -->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="视频" name="vdpth">
|
||||
<!-- <a-input v-model:value="formData.vdpth" placeholder="视频路径" /> -->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item
|
||||
label="图片路径"
|
||||
name="level5"
|
||||
extra="请输入图片文件名或路径(如:images/photo.jpg)"
|
||||
>
|
||||
<a-input v-model:value="formData.level5" placeholder="图片路径" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="视频路径"
|
||||
name="level6"
|
||||
extra="请输入视频文件名或路径(如:videos/video.mp4)"
|
||||
>
|
||||
<a-input v-model:value="formData.level6" placeholder="视频路径" />
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { Rule } from 'ant-design-vue/es/form'
|
||||
import dayjs from 'dayjs' // Antdv 默认依赖 dayjs
|
||||
import { ref, reactive, watch, computed } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import type { Rule } from "ant-design-vue/es/form";
|
||||
|
||||
// 定义 Props
|
||||
interface Props {
|
||||
visible: boolean
|
||||
initialValues?: any | null
|
||||
loading?: boolean
|
||||
visible: boolean;
|
||||
initialValues?: any | null;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
initialValues: null,
|
||||
loading: false
|
||||
})
|
||||
|
||||
loading: false,
|
||||
});
|
||||
// --- 核心修复:创建计算属性处理 v-model ---
|
||||
const modalVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit("update:visible", val),
|
||||
});
|
||||
// 定义 Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'cancel'): void
|
||||
(e: 'ok', values: any): void
|
||||
}>()
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "cancel"): void;
|
||||
(e: "ok", values: any): void;
|
||||
}>();
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref()
|
||||
const formRef = ref();
|
||||
// 1. 新增用于存储手动验证错误信息的响应式变量
|
||||
const bodyLengthError = ref<string>("");
|
||||
const weightError = ref<string>("");
|
||||
|
||||
// 表单数据模型
|
||||
const formData = reactive({
|
||||
const defaultFormData = reactive({
|
||||
id: undefined,
|
||||
engName: undefined,
|
||||
baseName: undefined,
|
||||
fpname: undefined,
|
||||
stcd: undefined,
|
||||
title: undefined,
|
||||
office: undefined,
|
||||
regionName: undefined,
|
||||
location: undefined,
|
||||
DIRECTION: undefined,
|
||||
level1: undefined,
|
||||
level2: undefined,
|
||||
level3: undefined,
|
||||
level4: undefined,
|
||||
level5: undefined,
|
||||
level6: undefined,
|
||||
status: '正常'
|
||||
})
|
||||
strdt: undefined,
|
||||
ftp: undefined,
|
||||
isfs: 0,
|
||||
direction: undefined,
|
||||
fcnt: undefined,
|
||||
fsz: undefined,
|
||||
fwet: undefined,
|
||||
wt: undefined,
|
||||
picpth: undefined,
|
||||
vdpth: undefined,
|
||||
bodyLengthMin: undefined,
|
||||
bodyLengthMax: undefined,
|
||||
weightMin: undefined,
|
||||
weightMax: undefined,
|
||||
});
|
||||
const formData: any = reactive({ ...defaultFormData });
|
||||
|
||||
// 自定义验证器:检查最小值是否小于最大值
|
||||
// const validateBodyLengthRange = (rule: any, value: any) => {
|
||||
// const min = formData.bodyLengthMin;
|
||||
// const max = formData.bodyLengthMax;
|
||||
|
||||
// // 如果两个值都存在,则进行比较
|
||||
// if (min !== undefined && min !== null && max !== undefined && max !== null) {
|
||||
// if (Number(min) >= Number(max)) {
|
||||
// return Promise.reject("最小体长必须小于最大体长");
|
||||
// }
|
||||
// }
|
||||
// return Promise.resolve();
|
||||
// };
|
||||
// const validateWeightRange = (rule: any, value: any) => {
|
||||
// const min = formData.weightMin;
|
||||
// const max = formData.weightMax;
|
||||
|
||||
// // 如果两个值都存在,则进行比较
|
||||
// if (min !== undefined && min !== null && max !== undefined && max !== null) {
|
||||
// if (Number(min) >= Number(max)) {
|
||||
// return Promise.reject("最小体重必须小于最大体重");
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// 验证规则
|
||||
const rules: Record<string, Rule[]> = {
|
||||
stcd: [{ required: true, message: '请输入流域', trigger: 'blur' }],
|
||||
title: [{ required: true, message: '请输入电站名称', trigger: 'blur' }],
|
||||
office: [{ required: true, message: '请输入过鱼设施名称', trigger: 'blur' }],
|
||||
regionName: [{ required: true, message: '请选择过鱼时间', trigger: 'change' }]
|
||||
}
|
||||
// engName: [{ required: true, message: "请输入水电基地", trigger: "blur" }],
|
||||
// baseName: [{ required: true, message: "请输入电站名称", trigger: "blur" }],
|
||||
// fpname: [{ required: true, message: "请输入过鱼设施名称", trigger: "blur" }],
|
||||
// strdt: [{ required: true, message: "请选择过鱼时间", trigger: "change" }],
|
||||
// // 添加体长的验证规则
|
||||
// bodyLengthMin: [{ validator: validateBodyLengthRange, trigger: "change" }],
|
||||
// bodyLengthMax: [{ validator: validateBodyLengthRange, trigger: "change" }],
|
||||
// weightMin: [{ validator: validateWeightRange, trigger: "change" }],
|
||||
// weightMax: [{ validator: validateWeightRange, trigger: "change" }],
|
||||
};
|
||||
|
||||
// 计算是否为编辑模式
|
||||
const isEdit = computed(() => !!props.initialValues)
|
||||
const isEdit = computed(() => !!props.initialValues);
|
||||
// 联动验证:当一个值改变时,重新验证整个体长组
|
||||
const validateBodyLength = () => {
|
||||
const min = formData.bodyLengthMin;
|
||||
const max = formData.bodyLengthMax;
|
||||
|
||||
// 监听 visible 和 initialValues 变化,初始化表单
|
||||
watch(
|
||||
() => [props.visible, props.initialValues],
|
||||
([newVisible, newValues]) => {
|
||||
if (newVisible) {
|
||||
if (newValues) {
|
||||
// 编辑模式:回填数据
|
||||
// 注意:regionName 如果是字符串,需要转换为 dayjs 对象以便 DatePicker 显示
|
||||
// 如果 a-date-picker 使用了 value-format="YYYY-MM-DD",则可以直接传字符串,但通常内部处理需要 dayjs
|
||||
Object.keys(formData).forEach((key) => {
|
||||
if (key === 'regionName') {
|
||||
// 如果后端返回的是字符串格式 'YYYY-MM-DD'
|
||||
formData[key] = newValues[key] ? dayjs(newValues[key]) : undefined
|
||||
} else {
|
||||
formData[key] = newValues[key]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 新增模式:重置表单
|
||||
resetForm()
|
||||
// 重置错误
|
||||
bodyLengthError.value = "";
|
||||
|
||||
// 如果两个值都有,才进行比对
|
||||
if (min !== undefined && min !== null && max !== undefined && max !== null) {
|
||||
if (Number(min) >= Number(max)) {
|
||||
bodyLengthError.value = "最小体长必须小于最大体长";
|
||||
// 如果需要阻止提交,可以在 handleOk 中检查这个变量
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateWeight = () => {
|
||||
const min = formData.weightMin;
|
||||
const max = formData.weightMax;
|
||||
|
||||
// 重置错误
|
||||
weightError.value = "";
|
||||
|
||||
if (min !== undefined && min !== null && max !== undefined && max !== null) {
|
||||
if (Number(min) >= Number(max)) {
|
||||
weightError.value = "最小体重必须小于最大体重";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 1. 定义一个初始化表单的函数
|
||||
const initForm = () => {
|
||||
if (props.initialValues) {
|
||||
// --- 编辑模式:回填数据 ---
|
||||
const values = props.initialValues;
|
||||
|
||||
// 处理特殊字段拆分
|
||||
if (values.fwet) {
|
||||
const weights = values.fwet.split("~");
|
||||
formData.weightMin = weights[0];
|
||||
formData.weightMax = weights[1];
|
||||
} else {
|
||||
formData.weightMin = undefined;
|
||||
formData.weightMax = undefined;
|
||||
}
|
||||
|
||||
if (values.fsz) {
|
||||
const sizes = values.fsz.split("~");
|
||||
formData.bodyLengthMin = sizes[0];
|
||||
formData.bodyLengthMax = sizes[1];
|
||||
} else {
|
||||
formData.bodyLengthMin = undefined;
|
||||
formData.bodyLengthMax = undefined;
|
||||
}
|
||||
|
||||
// 回填其他普通字段
|
||||
Object.keys(formData).forEach((key) => {
|
||||
// 跳过已经特殊处理的字段
|
||||
if (
|
||||
key === "weightMin" ||
|
||||
key === "weightMax" ||
|
||||
key === "bodyLengthMin" ||
|
||||
key === "bodyLengthMax"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 确保字段存在于 initialValues 中再赋值,避免覆盖默认值
|
||||
if (values.hasOwnProperty(key)) {
|
||||
formData[key] = values[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// --- 新增模式:重置表单 ---
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 修改 watch,只监听 visible 的变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible) {
|
||||
// 弹窗打开时,初始化数据
|
||||
initForm();
|
||||
} else {
|
||||
// 弹窗关闭时,可以选择是否重置,或者留给下次打开时处理
|
||||
// 通常建议在关闭时重置,或者在下次打开时根据 initialValues 判断
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: false } // 不需要 immediate,因为初始状态通常是 false
|
||||
);
|
||||
|
||||
// 3. 删除原来的复杂 watch
|
||||
// 删除这段代码:
|
||||
// watch(
|
||||
// () => [props.visible, props.initialValues],
|
||||
// ...
|
||||
// );
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
// 手动重置 reactive 对象以确保默认值
|
||||
formData.stcd = undefined
|
||||
formData.title = undefined
|
||||
formData.office = undefined
|
||||
formData.regionName = undefined
|
||||
formData.location = undefined
|
||||
formData.DIRECTION = undefined
|
||||
formData.level1 = undefined
|
||||
formData.level2 = undefined
|
||||
formData.level3 = undefined
|
||||
formData.level4 = undefined
|
||||
formData.level5 = undefined
|
||||
formData.level6 = undefined
|
||||
formData.status = '正常'
|
||||
}
|
||||
Object.assign(formData, defaultFormData);
|
||||
// 清空手动验证的错误信息
|
||||
bodyLengthError.value = '';
|
||||
weightError.value = '';
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
emit('cancel')
|
||||
resetForm()
|
||||
}
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 确认操作
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
await formRef.value.validate()
|
||||
|
||||
// 准备提交数据
|
||||
const submitValues = { ...formData }
|
||||
|
||||
// 处理日期格式:将 dayjs 对象转回字符串
|
||||
if (submitValues.regionName && dayjs.isDayjs(submitValues.regionName)) {
|
||||
submitValues.regionName = submitValues.regionName.format('YYYY-MM-DD')
|
||||
// 先执行手动验证
|
||||
const isBodyLenValid = validateBodyLength();
|
||||
const isWeightValid = validateWeight();
|
||||
console.log(isBodyLenValid, isWeightValid);
|
||||
if (!isBodyLenValid || !isWeightValid) {
|
||||
message.error("请检查体长或体重填写是否正确");
|
||||
return;
|
||||
}
|
||||
// 验证表单
|
||||
await formRef.value.validate();
|
||||
// 准备提交数据
|
||||
const submitValues = {
|
||||
...formData,
|
||||
fwet: formData.weightMin + "~" + formData.weightMax,
|
||||
fsz: formData.bodyLengthMin + "~" + formData.bodyLengthMax,
|
||||
};
|
||||
|
||||
// 确保数值类型正确 (InputNumber 通常已经处理为 number,但以防万一)
|
||||
// 如果字段允许为空,需小心处理
|
||||
;['level1', 'level2', 'level3', 'level4'].forEach((key) => {
|
||||
if (submitValues[key] !== undefined && submitValues[key] !== null) {
|
||||
submitValues[key] = Number(submitValues[key])
|
||||
}
|
||||
})
|
||||
|
||||
emit('ok', submitValues)
|
||||
emit("ok", submitValues);
|
||||
} catch (error) {
|
||||
console.error('Validate Failed:', error)
|
||||
message.error('请检查表单填写是否正确')
|
||||
console.error("Validate Failed:", error);
|
||||
message.error("请检查表单填写是否正确");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 如有需要,添加局部样式 */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -16,18 +16,28 @@
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
>
|
||||
<template #typeDate="{ onChange }">
|
||||
<fishSearch
|
||||
v-model="localTypeDate"
|
||||
width="280px"
|
||||
:options="options"
|
||||
@update:modelValue="onChange"
|
||||
/>
|
||||
</template>
|
||||
<!-- 自定义重置及操作按钮区域 -->
|
||||
<template #actions="{ form }">
|
||||
<template #actions>
|
||||
<a-tooltip title="新增">
|
||||
<a-button @click="props.handleAdd">
|
||||
新增
|
||||
</a-button>
|
||||
<a-button @click="props.handleAdd"> 新增 </a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip title="导入zip">
|
||||
<a-button v-hasPerm="['sjtb:import-zip']" @click="triggerFileInput"> 导入zip </a-button>
|
||||
<a-button v-hasPerm="['sjtb:import-zip']" @click="triggerFileInput">
|
||||
导入zip
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-button @click="props.batchDel" :disabled="batchData.length === 0">
|
||||
批量删除
|
||||
</a-button>
|
||||
<a-tooltip title="提交数据">
|
||||
<a-button @click="props.saveBtn">
|
||||
<template #icon><SaveOutlined /></template>
|
||||
@ -41,24 +51,39 @@
|
||||
批量审批
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="leftBottom">
|
||||
<template #title>
|
||||
<div>1.</div>
|
||||
</template>
|
||||
<a-button>
|
||||
<template #icon><QuestionOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, nextTick } from "vue";
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { PlusOutlined, SaveOutlined, CheckSquareOutlined } from "@ant-design/icons-vue";
|
||||
import {
|
||||
SaveOutlined,
|
||||
CheckSquareOutlined,
|
||||
QuestionOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import dayjs from "dayjs";
|
||||
import BasicSearch from "@/components/BasicSearch/index.vue"; // 确保路径正确
|
||||
import { DateSetting } from "@/utils/enumeration";
|
||||
import { checkPerm, } from "@/directive/permission";
|
||||
import { checkPerm } from "@/directive/permission";
|
||||
import fishSearch from "@/components/fishSearch/index.vue";
|
||||
|
||||
// --- Props & Emits ---
|
||||
interface Props {
|
||||
importBtn: (file: File) => void;
|
||||
batchDel: () => void;
|
||||
saveBtn: () => void;
|
||||
batchData: any[];
|
||||
handleAdd: () => void;
|
||||
}
|
||||
|
||||
@ -67,9 +92,194 @@ const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
(e: "searchFinish", values: any, label: string): void;
|
||||
}>();
|
||||
|
||||
const localTypeDate = ref<string[]>([]);
|
||||
// --- State ---
|
||||
const fileInputRef = ref<HTMLInputElement>();
|
||||
const options = ref<any>([
|
||||
{
|
||||
_tls: {},
|
||||
id: "00DDF2A72147B2115384F64DDFE26A5E",
|
||||
recordUser: null,
|
||||
recordTime: null,
|
||||
modifyTime: null,
|
||||
displayRecordUser: null,
|
||||
departmentId: null,
|
||||
displayDepartment: null,
|
||||
index: 1,
|
||||
name: "异唇裂腹鱼",
|
||||
code: null,
|
||||
nameEn: null,
|
||||
alias: null,
|
||||
description: null,
|
||||
logo: null,
|
||||
introduce: null,
|
||||
inffile: null,
|
||||
genus: null,
|
||||
family: null,
|
||||
species: null,
|
||||
fsz: null,
|
||||
type: 1,
|
||||
typeName: "淡水",
|
||||
rare: null,
|
||||
specOrigin: null,
|
||||
specOriginName: null,
|
||||
ptype: null,
|
||||
ptypeName: null,
|
||||
rvcd: "null",
|
||||
rvcdName: "",
|
||||
zyFishId: "00DDF2A72147B2115384F64DDFE26A5E",
|
||||
habitMigrat: null,
|
||||
feedingHabit: null,
|
||||
spawnCharact: null,
|
||||
spawnMonth: null,
|
||||
food: null,
|
||||
timeFeed: null,
|
||||
orignDate: null,
|
||||
pretemp: null,
|
||||
flowRate: null,
|
||||
depth: null,
|
||||
botmMater: null,
|
||||
wqtq: null,
|
||||
habitat: null,
|
||||
situation: null,
|
||||
resourceType: null,
|
||||
shapedesc: null,
|
||||
protectlvl: null,
|
||||
habitation: null,
|
||||
fid: null,
|
||||
enable: null,
|
||||
internal: null,
|
||||
orderIndex: null,
|
||||
filterContent: null,
|
||||
platformId: null,
|
||||
isTempStorage: null,
|
||||
},
|
||||
{
|
||||
_tls: {},
|
||||
id: "0249006974f34c288d6cb4df54e3b19d",
|
||||
recordUser: null,
|
||||
recordTime: null,
|
||||
modifyTime: null,
|
||||
displayRecordUser: null,
|
||||
departmentId: null,
|
||||
displayDepartment: null,
|
||||
index: 2,
|
||||
name: "匙吻鲟",
|
||||
code: null,
|
||||
nameEn: "Polyodon spathula",
|
||||
alias: "美国匙吻鲟、鸭嘴鲟",
|
||||
description:
|
||||
"匙吻鲟(Polyodonspathula)亦称匙吻猫鱼(spoonbillcat)。产于北美洲的原始鱼,为桨吻鲟(paddlefish)的一种。属鲟形目、匙吻鲟科,是北美洲的一种名贵大型淡水经济鱼类。匙吻鲟的显著特点是吻呈扁平桨状,特别长。鱼的体表光滑无鳞,背部黑蓝灰色,有一些斑点在其间,体侧有点状赭色,腹部白色。个体大,这种大型淡水鱼可以长到220厘米,重达90公斤以上。",
|
||||
logo: "20240527221754634033127655455265",
|
||||
introduce: null,
|
||||
inffile:
|
||||
"20240527221811830658320352201158,20240527221805865127213075311524,20240527221822527347221377607671,20240527221828072460253583084314,20240527221800311481326028334838,20240527221817630761245563388673",
|
||||
genus: "匙吻鲟属",
|
||||
family: "匙吻鲟科",
|
||||
species: "匙吻鲟",
|
||||
fsz: "85~220",
|
||||
type: 1,
|
||||
typeName: "淡水",
|
||||
rare: null,
|
||||
specOrigin: 2,
|
||||
specOriginName: "外来鱼类",
|
||||
ptype: 4,
|
||||
ptypeName: "易危",
|
||||
rvcd: "SJLY148",
|
||||
rvcdName: "大渡河",
|
||||
zyFishId: "0249006974f34c288d6cb4df54e3b19d",
|
||||
habitMigrat: "繁殖洄游",
|
||||
feedingHabit: "肉食性",
|
||||
spawnCharact: "粘性卵类型",
|
||||
spawnMonth: "4-5",
|
||||
food: "主要以浮游动物,也以甲壳类和双壳类生物为食",
|
||||
timeFeed: "夜间觅食",
|
||||
orignDate:
|
||||
"匙吻鲟在美国密西西比河流域的22个洲均有发现。包括密苏里河到蒙大拿州,俄亥俄河,和它的主要支流流域。雄鱼在7~9龄达到性成熟,雌鱼晚一年,相对怀卵量约为每克体重3.5粒。匙吻鲟多在4~5月繁殖,适宜水温为16~18℃,繁殖期会游到江河上游产卵,受精卵灰黑色,直径2~2.5毫米,有黏性,往往粘在砾石上孵化,孵化期6~7天。",
|
||||
pretemp: "0~37℃",
|
||||
flowRate: "0.3m/s",
|
||||
depth: "2~2.5",
|
||||
botmMater: "泥质",
|
||||
wqtq: "适宜的pH范围为6.5~8,对溶解氧要求较高,应在5毫克/升以上。",
|
||||
habitat: null,
|
||||
situation: null,
|
||||
resourceType: null,
|
||||
shapedesc:
|
||||
"匙吻鲟有一个形如匙柄的长吻,长约为体长的三分之一。身体流线型,体表光滑无鳞。眼小,口较大,位于吻末端的腹面,不能伸缩;上颌背面具有粗糙的颗粒感觉器。鳃盖骨大而向后延伸,鳃盖膜长达胸鳍至腹鳍的1/2处。头部有一喷水孔和喷水腔。胸鳍较小,下位;腹鳍腹位,背鳍起点在腹鳍之后。尾鳍分叉,歪尾型,上叶长于下叶,尾柄披有梗栉状的甲鳞。背部黑蓝灰色,常有一些斑点间于其中,两侧逐渐变浅,体侧有点状褐色,腹部白色。",
|
||||
protectlvl: null,
|
||||
habitation: "缓流型;广温性;中上层水域",
|
||||
fid: null,
|
||||
enable: null,
|
||||
internal: null,
|
||||
orderIndex: null,
|
||||
filterContent: null,
|
||||
platformId: null,
|
||||
isTempStorage: null,
|
||||
},
|
||||
{
|
||||
_tls: {},
|
||||
id: "02A23B169BF240589B2C37C5E81A8DC2",
|
||||
recordUser: null,
|
||||
recordTime: null,
|
||||
modifyTime: null,
|
||||
displayRecordUser: null,
|
||||
departmentId: null,
|
||||
displayDepartment: null,
|
||||
index: 3,
|
||||
name: "南方马口鱼",
|
||||
code: null,
|
||||
nameEn: "Chinese hooksnout carp",
|
||||
alias:
|
||||
"午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公",
|
||||
description:
|
||||
"南方马口鱼,Opsariichthys uncirostris bidens (Gunther,1873),是鲤科马口鱼属的一种生活的溪流中的小型鱼类。体长,稍侧扁,腹部圆。头稍尖,头长大于体高。吻钝,吻长远比其宽为大。口特大,下颌前端突起,两侧面各有一凹陷,恰与上颌突出部分吻合。下咽齿3行。鳞圆形,背鳍条2,7,无硬刺。臀鳍条3,8-10。背部黑灰色,体侧下半部及腹面银白色,喉部、口唇及各鳍橙黄,背鳍上有黑色的小斑点,眼上部有一红色斑点,体两侧具有浅蓝色的垂直条纹。生殖季节时,雄鱼体色更为鲜艳。",
|
||||
logo: "20240527192500111683624865306342",
|
||||
introduce: null,
|
||||
inffile:
|
||||
"20240527192505300717052825727341,20240527192533035616525871580354,20240527192510217883087850433201,20240527192516128514164206355182,20240527192522835236402141341053,20240527192527583177528213025212",
|
||||
genus: "马口鱼属",
|
||||
family: "鲤科",
|
||||
species: "南方马口鱼",
|
||||
fsz: "7~20",
|
||||
type: 1,
|
||||
typeName: "淡水",
|
||||
rare: null,
|
||||
specOrigin: 1,
|
||||
specOriginName: "本土",
|
||||
ptype: 4,
|
||||
ptypeName: "易危",
|
||||
rvcd: "null",
|
||||
rvcdName: "",
|
||||
zyFishId: "02A23B169BF240589B2C37C5E81A8DC2",
|
||||
habitMigrat: "定居型",
|
||||
feedingHabit: "肉食性",
|
||||
spawnCharact: "沉性卵类型",
|
||||
spawnMonth: "6-8",
|
||||
food: "摄食小型鱼类和水生昆虫。",
|
||||
timeFeed: "白天觅食",
|
||||
orignDate:
|
||||
"产卵期在6~8月份。第一年生长较迅速,可达7~11厘米。1龄鱼即有繁殖能力,系小型鱼类。",
|
||||
pretemp: "0~30℃",
|
||||
flowRate: "0.3m/s",
|
||||
depth: "1~1.5",
|
||||
botmMater: "砂砾底质",
|
||||
wqtq: "pH在7.2~7.8之内,凉爽清洁、溶氧丰富的水质",
|
||||
habitat: null,
|
||||
situation: null,
|
||||
resourceType: null,
|
||||
shapedesc:
|
||||
"背鳍条3,7;臀鳍条3,9;侧线鳞45~47;下咽齿3行,1·4·5-4·4·1。鳃耙外侧10,脊椎骨35。体长为体高的3.1~4.3倍,为头长的3.5~3.9倍,为尾柄长的4.7~5.2倍,为尾柄高的10.2~11.3倍。头长为吻长的2.7~3.2倍,为眼径的5.0~6.2倍,为眼间距的3.1~3.3倍。体延长,侧扁。吻长,其长略大于宽,口大,端位,口裂向上倾斜,下颌后端延长达眼前缘,其前缘凸起,两侧凹陷,恰与上颌前端和两侧嵌合。眼中等大,位于头侧上方。鳃耙短小而稀疏。下咽齿圆柱性,顶端尖而长。侧线完全,前端弯向体侧腹方,后端向上延至尾柄正中。背鳍无硬刺,其起点至吻端稍大于至尾鳍基部的距离,胸鳍不达腹鳍,其末端可达胸、腹鳍间距的3/5处。腹鳍外缘略钝圆,起点约与背鳍不分支鳍条相对。鳔2视,后室约为前室的2倍,腹腔膜银白色。体背部灰黑色,腹部银白色,体侧有浅蓝色的垂直条纹,胸鳍、腹鳍和臀鳍为橙黄色。雄鱼在生殖期出现婚装,头部、吻部和臀部有显眼的珠星,臀鳍的第1~4根分支鳍条特别延长,全身具有很鲜艳的婚姻色。",
|
||||
protectlvl: null,
|
||||
habitation: "流水型;冷水性;中上水层",
|
||||
fid: null,
|
||||
enable: null,
|
||||
internal: null,
|
||||
orderIndex: null,
|
||||
filterContent: null,
|
||||
platformId: null,
|
||||
isTempStorage: null,
|
||||
},
|
||||
]);
|
||||
|
||||
// 模拟 initSearchData
|
||||
const initSearchData = {
|
||||
@ -80,8 +290,9 @@ const initSearchData = {
|
||||
hbrvcd: "",
|
||||
stcdId: "",
|
||||
},
|
||||
typeDate: "",
|
||||
RangePickerList: [
|
||||
mway: "1",
|
||||
typeDate: [],
|
||||
strdt: [
|
||||
dayjs().startOf("month").format("YYYY-MM-DD"),
|
||||
dayjs().endOf("day").format("YYYY-MM-DD"),
|
||||
],
|
||||
@ -94,8 +305,8 @@ const searchData = ref<any>({ ...initSearchData });
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: "waterStation",
|
||||
name: "stcd",
|
||||
label: "选择水电站",
|
||||
name: "engName",
|
||||
label: "水电基地",
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
@ -103,8 +314,8 @@ const searchList: any = computed(() => [
|
||||
},
|
||||
{
|
||||
type: "Select",
|
||||
name: "strdt",
|
||||
label: "过鱼设施编码",
|
||||
name: "fpname",
|
||||
label: "过鱼设施",
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
@ -112,27 +323,28 @@ const searchList: any = computed(() => [
|
||||
},
|
||||
{
|
||||
type: "Select",
|
||||
name: "DIRECTION",
|
||||
name: "direction",
|
||||
label: "游向",
|
||||
width: 120,
|
||||
options: [
|
||||
{ label: "上行", value: "0" },
|
||||
{ label: "下行", value: "1" },
|
||||
{ label: "上下行", value: "2" },
|
||||
{ label: "上行", value: "上行" },
|
||||
{ label: "下行", value: "下行" },
|
||||
{ label: "上行折返", value: "上行折返" },
|
||||
{ label: "下行折返", value: "下行折返" },
|
||||
],
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "Input",
|
||||
name: "typeDate",
|
||||
label: "鱼名称",
|
||||
type: "custom",
|
||||
name: "ftp",
|
||||
label: "鱼种类",
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
checkPerm(['sjtb:edit-ztcx']) && {
|
||||
checkPerm(["sjtb:edit-ztcx"]) && {
|
||||
width: 120,
|
||||
type: "Select",
|
||||
name: "status",
|
||||
@ -149,8 +361,8 @@ const searchList: any = computed(() => [
|
||||
{
|
||||
span: 12,
|
||||
type: "RangePicker",
|
||||
name: "RangePickerList",
|
||||
label: "时间",
|
||||
name: "strdt",
|
||||
label: "过鱼时间",
|
||||
picker: "date",
|
||||
fieldProps: {
|
||||
format: "YYYY-MM-DD",
|
||||
@ -210,11 +422,11 @@ const onSearchFinish = (values: any) => {
|
||||
// 在原 React 代码中,label 来自 options.find(...)
|
||||
// 这里简化处理,直接传递 ID 或固定名称,或者你需要维护一个电站列表映射
|
||||
const label = "默认水温站"; // TODO: 根据 values.dmStcd 查找真实名称
|
||||
const params: any = {};
|
||||
if (values.RangePickerList) {
|
||||
params.startDate = values.RangePickerList[0].format("YYYY-MM-DD");
|
||||
params.endDate = values.RangePickerList[1].format("YYYY-MM-DD");
|
||||
}
|
||||
// const params: any = {};
|
||||
// if (values.strdt) {
|
||||
// params.startDate = values.strdt[0].format("YYYY-MM-DD");
|
||||
// params.endDate = values.strdt[1].format("YYYY-MM-DD");
|
||||
// }
|
||||
|
||||
emit("searchFinish", values, label);
|
||||
};
|
||||
@ -223,33 +435,38 @@ const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
// 同步更新本地 searchData,以便其他逻辑使用
|
||||
searchData.value = { ...searchData.value, ...allValues };
|
||||
|
||||
if (changedValues.RangePickerList) {
|
||||
if (changedValues.strdt) {
|
||||
// 如果需要在时间改变时做额外处理
|
||||
console.log("Time changed:", changedValues.RangePickerList);
|
||||
console.log("Time changed:", changedValues.strdt);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = (form: any) => {
|
||||
// 重置表单
|
||||
if (form) {
|
||||
form.resetFields();
|
||||
}
|
||||
|
||||
// 重置后重新设置默认值并触发搜索
|
||||
nextTick(() => {
|
||||
if (form) {
|
||||
form.setFieldsValue(initSearchData);
|
||||
}
|
||||
// 触发初始搜索
|
||||
emit("searchFinish", initSearchData, "两河口出库水温站");
|
||||
});
|
||||
};
|
||||
// const handleReset = (form: any) => {
|
||||
// // 重置表单
|
||||
// if (form) {
|
||||
// form.resetFields();
|
||||
// }
|
||||
|
||||
// // 重置后重新设置默认值并触发搜索
|
||||
// nextTick(() => {
|
||||
// if (form) {
|
||||
// form.setFieldsValue(initSearchData);
|
||||
// }
|
||||
// // 触发初始搜索
|
||||
// emit("searchFinish", initSearchData, "两河口出库水温站");
|
||||
// });
|
||||
// };
|
||||
watch(
|
||||
() => initSearchData.typeDate,
|
||||
(newVal) => {
|
||||
localTypeDate.value = newVal || [];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// --- Lifecycle ---
|
||||
onMounted(() => {
|
||||
// 初始请求
|
||||
emit("searchFinish", initSearchData, "两河口出库水温站");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@ -87,7 +87,7 @@ function getTree() {
|
||||
});
|
||||
getData();
|
||||
})
|
||||
.catch((error:any)=>{
|
||||
.catch(()=>{
|
||||
treeloading.value = false
|
||||
})
|
||||
}
|
||||
@ -135,7 +135,7 @@ function switchChange(row: any) {
|
||||
id: row.id,
|
||||
};
|
||||
|
||||
deptIsVaild(params).then((res) => {
|
||||
deptIsVaild(params).then(() => {
|
||||
getData();
|
||||
ElMessage({
|
||||
type: "success",
|
||||
@ -169,7 +169,7 @@ function editisrepetition(formEl: any) {
|
||||
formEl.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (causeInfo.value.id) {
|
||||
updataTreelist(causeInfo.value).then((res) => {
|
||||
updataTreelist(causeInfo.value).then(() => {
|
||||
getTree();
|
||||
ElMessage({
|
||||
message: "修改成功",
|
||||
@ -177,7 +177,7 @@ function editisrepetition(formEl: any) {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
addTreelist(causeInfo.value).then((res) => {
|
||||
addTreelist(causeInfo.value).then(() => {
|
||||
getTree();
|
||||
ElMessage({
|
||||
message: "新建成功",
|
||||
@ -194,11 +194,11 @@ function confirmClick(formEl: any) {
|
||||
formEl.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (info.value.id) {
|
||||
reviseDepartment(info.value).then((item) => {
|
||||
reviseDepartment(info.value).then(() => {
|
||||
getData();
|
||||
});
|
||||
} else {
|
||||
addTreelist(info.value).then((item) => {
|
||||
addTreelist(info.value).then(() => {
|
||||
getData();
|
||||
});
|
||||
}
|
||||
@ -231,7 +231,7 @@ function remove(data: any) {
|
||||
let params = {
|
||||
id: data.id,
|
||||
};
|
||||
delTreelist(params).then((res) => {
|
||||
delTreelist(params).then(() => {
|
||||
getTree();
|
||||
ElMessage({
|
||||
message: "删除成功",
|
||||
|
||||
@ -127,7 +127,7 @@ function getTree() {
|
||||
if (res.data.length == 0) treeId.value = ''
|
||||
init();
|
||||
})
|
||||
.catch((error)=>{
|
||||
.catch(()=>{
|
||||
treeloading.value = false
|
||||
})
|
||||
}
|
||||
@ -229,7 +229,7 @@ function treenodeDrop(before: any, after: any) {
|
||||
|
||||
});
|
||||
}
|
||||
const allowDrop = (draggingNode: any, dropNode: any, type: any) => {
|
||||
const allowDrop:any = (draggingNode: any, dropNode: any, type: any) => {
|
||||
// 不能拖拽到级别里面
|
||||
if (type === 'inner' || Number(dropNode.data.pid) === 0) return
|
||||
if (draggingNode.nextSibling === undefined) {
|
||||
@ -263,7 +263,7 @@ function init() {
|
||||
querystr.value.size = result.data.size;
|
||||
querystr.value.current = result.data.current
|
||||
tableloading.value = false;
|
||||
}).catch((err: any) => {
|
||||
}).catch(() => {
|
||||
tableloading.value = false;
|
||||
});
|
||||
}
|
||||
@ -490,7 +490,7 @@ const total = ref()
|
||||
<p style="color:#f56c6c;font-size: 12px;width: 70%;margin: auto; margin-top: -15px; padding: 5px 0px;">
|
||||
注:如果添加项为子项,则需输入父项编码</p>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="dictInfoItem.custom1" rows="5" type="textarea" style="width:100%"
|
||||
<el-input v-model="dictInfoItem.custom1" :rows="5" type="textarea" style="width:100%"
|
||||
placeholder="请输入备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@ -58,7 +58,7 @@ function init() {
|
||||
pcode.value = result.data.size
|
||||
queryParams.value.current = result.data.current
|
||||
loading.value = false;
|
||||
}).catch((err: any) => {
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ function gettableData() {
|
||||
listRolePages(params).then((result) => {
|
||||
tableData.value = result;
|
||||
loading.value = false;
|
||||
}).catch((err) => {
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
@ -106,7 +106,7 @@ function confirmClick(formEl: any) {
|
||||
level: info.value.level,
|
||||
description: info.value.description,
|
||||
};
|
||||
addDept(params).then((res) => {
|
||||
addDept(params).then(() => {
|
||||
gettableData();
|
||||
dialogVisible.value = false;
|
||||
});
|
||||
@ -117,7 +117,7 @@ function confirmClick(formEl: any) {
|
||||
description: info.value.description,
|
||||
id: info.value.id,
|
||||
};
|
||||
renewDept(params).then((res) => {
|
||||
renewDept(params).then(() => {
|
||||
gettableData();
|
||||
dialogVisible.value = false;
|
||||
});
|
||||
@ -150,7 +150,7 @@ function editrole(row: any) {
|
||||
}
|
||||
//业务范围修改
|
||||
const businessVisible = ref(false);
|
||||
function businessclick(row: any) {
|
||||
function businessclick() {
|
||||
// businessVisible.value = true;
|
||||
ElMessageBox.confirm("此模块允许用户进行定制。", "提示信息", {
|
||||
confirmButtonText: "确定",
|
||||
@ -209,7 +209,7 @@ function organizesubmit() {
|
||||
id: roleIda.value,
|
||||
orgscope: allid.value.toString()
|
||||
}
|
||||
postOrgscope(params).then((res) => {
|
||||
postOrgscope(params).then(() => {
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "组织范围修改成功",
|
||||
@ -279,7 +279,7 @@ function assignment(row: any) {
|
||||
|
||||
|
||||
// 树形选择器
|
||||
function currentChecked(nodeObj: any, SelectedObj: any) {
|
||||
function currentChecked(_nodeObj: any, SelectedObj: any) {
|
||||
Passparameter.value = SelectedObj.checkedKeys.concat(SelectedObj.halfCheckedKeys)
|
||||
}
|
||||
// 权限范围-权限范围-确定
|
||||
@ -288,7 +288,7 @@ function accesssubmit() {
|
||||
id: rowid.value,
|
||||
menuIds: Passparameter.value.toString()
|
||||
}
|
||||
setMenuById(parans).then((res) => {
|
||||
setMenuById(parans).then(() => {
|
||||
accessVisible.value = false
|
||||
gettableData()
|
||||
ElMessage({
|
||||
@ -395,7 +395,7 @@ onMounted(() => {
|
||||
style="display: flex;display: -webkit-flex; justify-content: space-around;-webkit-justify-content: space-around; ">
|
||||
<img v-hasPerm="['update:role']" src="@/assets/MenuIcon/lbcz_xg.png" alt="" title="修改"
|
||||
@click="editrole(scope.row)" style="cursor: pointer; ">
|
||||
<img src="@/assets/MenuIcon/lbcz_zyw.png" alt="" title="业务范围" @click="businessclick(scope.row)"
|
||||
<img src="@/assets/MenuIcon/lbcz_zyw.png" alt="" title="业务范围" @click="businessclick"
|
||||
style="cursor: pointer;">
|
||||
<img src="@/assets/MenuIcon/u343.png" alt="" title="组织范围" @click="organizeclick(scope.row)"
|
||||
style="cursor: pointer; ">
|
||||
|
||||
@ -58,7 +58,7 @@ function getTree() {
|
||||
});
|
||||
getdata()
|
||||
})
|
||||
.catch((error: any) => {
|
||||
.catch(() => {
|
||||
treeloading.value = false
|
||||
})
|
||||
|
||||
@ -146,7 +146,7 @@ function switchChange(row: any) {
|
||||
status: row.status,
|
||||
id: row.id,
|
||||
};
|
||||
DataStatus(params).then((res) => {
|
||||
DataStatus(params).then(() => {
|
||||
getdata();
|
||||
ElMessage({
|
||||
type: "success",
|
||||
@ -200,7 +200,7 @@ function confirmClick(formEl: any) {
|
||||
orgid: orgId.value,
|
||||
};
|
||||
const roleids = String(info.value.roleinfo)
|
||||
updataUser(params, roleids).then((res) => {
|
||||
updataUser(params, roleids).then(() => {
|
||||
|
||||
getdata();
|
||||
dialogVisible.value = false;
|
||||
@ -533,7 +533,7 @@ function handleFishCheckChange(checkedNode: any, checkedInfo: any) {
|
||||
}
|
||||
// 这里可以根据业务需求使用resultIds,比如调用接口保存权限
|
||||
// saveFishPermissions(resultIds);
|
||||
getFishTableData(resultIds)
|
||||
getFishTableData()
|
||||
tableids.value = resultIds
|
||||
}
|
||||
//过鱼设施权限
|
||||
@ -560,7 +560,7 @@ const fishTableData = ref([
|
||||
const fishDialog = ref(false)
|
||||
const fishTableSelection = ref([])
|
||||
//获取过鱼设施权限数据
|
||||
function getFishTableData(ids: any[]) {
|
||||
function getFishTableData() {
|
||||
fishDialog.value = true
|
||||
fishDialog.value = false;
|
||||
// 获取过鱼设施权限数据
|
||||
|
||||
@ -70,7 +70,7 @@ const moderules = ref(
|
||||
],
|
||||
},
|
||||
)
|
||||
function confirmPassword(rule: any, value: any, callback: any) {
|
||||
function confirmPassword(_rule: any, value: any, callback: any) {
|
||||
if(value == null) {
|
||||
callback('请输入新密码');
|
||||
} else {
|
||||
@ -155,7 +155,7 @@ function savePass(formEl: any){
|
||||
id: info.value.id,
|
||||
password: encrypt(pass.value.newPassword)
|
||||
}
|
||||
updatePassword(params).then((res) => {
|
||||
updatePassword(params).then(() => {
|
||||
userStore.logout().then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
}).then(() => {
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": false, // 严格模式
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
@ -17,14 +17,15 @@
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"allowSyntheticDefaultImports": true /* 允许默认导入 */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"types": ["element-plus/global"],
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["vite/client"],
|
||||
"ignoreDeprecations": "6.0",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types/",
|
||||
"./types"
|
||||
] /* 指定多个文件夹,这些文件夹的作用类似于 './node_modules/@types'. */
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "dist", "**/*.js"]
|
||||
|
||||
@ -27,7 +27,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
// 线上API地址
|
||||
// target: 'http://10.84.121.4:8093/',
|
||||
// 本地API地址
|
||||
target: 'http://localhost:8093',
|
||||
// target: 'http://localhost:8093',
|
||||
// 汤伟
|
||||
target: 'http://10.84.121.21:8093',
|
||||
changeOrigin: true,
|
||||
rewrite: path =>
|
||||
path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
||||
@ -45,6 +47,26 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
// 1. 增加 chunk 大小警告限制(避免过多小文件)
|
||||
chunkSizeWarningLimit: 1000,
|
||||
|
||||
// 2. 优化 Rollup 配置
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 手动分块,将大型库单独打包,减少单个 chunk 的复杂度
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
// 将 element-plus, lodash 等大型库单独拆分
|
||||
if (id.includes('element-plus')) return 'element-plus';
|
||||
if (id.includes('lodash')) return 'lodash';
|
||||
// 其他 node_modules 归为 vendor
|
||||
return 'vendor';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
// Vite路径别名配置
|
||||
alias: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user