添加数据查询 - 鱼类调查
This commit is contained in:
parent
8f999af2fe
commit
52858abd2a
114
frontend-sjtb/src/views/error-page/401.vue
Normal file
114
frontend-sjtb/src/views/error-page/401.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Page401'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const state = reactive({
|
||||
errGif: new URL(`../../assets/401_images/401.gif`, import.meta.url).href,
|
||||
|
||||
ewizardClap:
|
||||
'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
|
||||
dialogVisible: false
|
||||
});
|
||||
|
||||
const { errGif, ewizardClap, dialogVisible } = toRefs(state);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function back() {
|
||||
router.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="errPage-container">
|
||||
<el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back">
|
||||
返回
|
||||
</el-button>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<h1 class="text-jumbo text-ginormous">Oops!</h1>
|
||||
gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
|
||||
<h2>你没有权限去该页面</h2>
|
||||
<h6>如有不满请联系你领导</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li>或者你可以去:</li>
|
||||
<li class="link-type">
|
||||
<router-link to="/"> 回首页 </router-link>
|
||||
</li>
|
||||
<li class="link-type">
|
||||
<a href="https://www.taobao.com/">随便看看</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" @click.prevent="dialogVisible = true">点我看图</a>
|
||||
</li>
|
||||
</ul>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<img
|
||||
:src="errGif"
|
||||
width="313"
|
||||
height="428"
|
||||
alt="Girl has dropped her ice cream."
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog v-model="dialogVisible" title="随便看">
|
||||
<img :src="ewizardClap" class="pan-img" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.errPage-container {
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
margin: 100px auto;
|
||||
|
||||
.pan-back-btn {
|
||||
background: #008489;
|
||||
color: #fff;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.pan-gif {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pan-img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-jumbo {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
color: #484848;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
font-size: 14px;
|
||||
|
||||
li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #008489;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
280
frontend-sjtb/src/views/error-page/404.vue
Normal file
280
frontend-sjtb/src/views/error-page/404.vue
Normal file
@ -0,0 +1,280 @@
|
||||
<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Page404'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
function message() {
|
||||
return 'The webmaster said that you can not enter this page...';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img
|
||||
class="pic-404__parent"
|
||||
src="@/assets/404_images/404.png"
|
||||
alt="404"
|
||||
/>
|
||||
<img
|
||||
class="pic-404__child left"
|
||||
src="@/assets/404_images/404_cloud.png"
|
||||
alt="404"
|
||||
/>
|
||||
<img
|
||||
class="pic-404__child mid"
|
||||
src="@/assets/404_images/404_cloud.png"
|
||||
alt="404"
|
||||
/>
|
||||
<img
|
||||
class="pic-404__child right"
|
||||
src="@/assets/404_images/404_cloud.png"
|
||||
alt="404"
|
||||
/>
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">OOPS!</div>
|
||||
<div class="bullshit__info">
|
||||
All rights reserved
|
||||
<a
|
||||
style="color: #20a0ff"
|
||||
href="https://wallstreetcn.com"
|
||||
target="_blank"
|
||||
>wallstreetcn</a
|
||||
>
|
||||
</div>
|
||||
<div class="bullshit__headline">{{ message }}</div>
|
||||
<div class="bullshit__info">
|
||||
Please check that the URL you entered is correct, or click the button
|
||||
below to return to the homepage.
|
||||
</div>
|
||||
<a href="" class="bullshit__return-home">Back to home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wscn-http404-container {
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__child {
|
||||
position: absolute;
|
||||
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
997
frontend-sjtb/src/views/register/index.vue
Normal file
997
frontend-sjtb/src/views/register/index.vue
Normal file
@ -0,0 +1,997 @@
|
||||
<template>
|
||||
<div class="register-container">
|
||||
<div class="register-wrapper">
|
||||
<!-- 左侧:背景图区域 -->
|
||||
<div class="left-section">
|
||||
<div class="slogan">
|
||||
<p>{{ $t('login.title') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:注册表单区域 -->
|
||||
<div class="right-section">
|
||||
<a-tabs v-model:activeKey="activeTab" class="register-tabs">
|
||||
<a-tab-pane key="register" tab="用户注册">
|
||||
<a-form
|
||||
:model="registerData"
|
||||
:rules="registerRules"
|
||||
layout="horizontal"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="form-container"
|
||||
@finish="handleFormFinish"
|
||||
>
|
||||
<!-- 账号信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">账号信息</div>
|
||||
|
||||
<!-- 账号 -->
|
||||
<a-form-item name="username" label="账 号">
|
||||
<a-input
|
||||
v-model:value="registerData.username"
|
||||
placeholder="请输入登录账号(4-20个字符)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<a-form-item
|
||||
name="password"
|
||||
label="密 码"
|
||||
:dependencies="['username']"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="registerData.password"
|
||||
placeholder="请设置密码(6-20个字符)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<a-form-item name="confirmPassword" label="确认密码">
|
||||
<a-input-password
|
||||
v-model:value="registerData.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 个人信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">个人信息</div>
|
||||
|
||||
<!-- 真实姓名 -->
|
||||
<a-form-item name="realName" label="真实姓名">
|
||||
<a-input
|
||||
v-model:value="registerData.realName"
|
||||
placeholder="请输入真实姓名"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 所属单位 -->
|
||||
<a-form-item name="belongingUnit" label="所属单位">
|
||||
<a-input
|
||||
v-model:value="registerData.belongingUnit"
|
||||
placeholder="请输入所属单位(选填)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<a-form-item name="phone" label="手 机 号">
|
||||
<a-row class="w-full" :gutter="8">
|
||||
<a-col :span="16">
|
||||
<a-input
|
||||
v-model:value="registerData.phone"
|
||||
placeholder="请输入11位手机号"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="smsCountdown > 0"
|
||||
@click="handleSendSms"
|
||||
>
|
||||
{{
|
||||
smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码'
|
||||
}}
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 短信验证码 -->
|
||||
<a-form-item name="smsCode" label="验证码">
|
||||
<a-input
|
||||
v-model:value="registerData.smsCode"
|
||||
placeholder="请输入短信验证码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<a-form-item
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
htmlType="submit"
|
||||
:loading="loading"
|
||||
style="margin-left: 20px"
|
||||
>
|
||||
<span>立即注册</span>
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 返回登录 -->
|
||||
<a-form-item
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<a-button type="link" size="small" block @click="backToLogin">
|
||||
已有账号?返回登录
|
||||
</a-button>
|
||||
<a-button type="link" size="small" block> | </a-button>
|
||||
<div></div>
|
||||
<a
|
||||
href="/file/注册操作手册.docx"
|
||||
download="注册用户操作手册.docx"
|
||||
>
|
||||
<a-button type="link" size="small" block>
|
||||
注册用户操作手册
|
||||
</a-button>
|
||||
</a>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 注册确认弹框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
title="选择所属组织"
|
||||
width="600px"
|
||||
:confirm-loading="loading"
|
||||
@ok="onRegister"
|
||||
@cancel="handleModalCancel"
|
||||
:maskClosable="false"
|
||||
>
|
||||
<a-form
|
||||
:model="organizationData"
|
||||
:rules="organizationRules"
|
||||
layout="horizontal"
|
||||
ref="organizationFormRef"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<!-- 集团(单选,字符串) -->
|
||||
<a-form-item name="groupCode" label="集 团">
|
||||
<a-select
|
||||
v-model:value="organizationData.groupCode"
|
||||
placeholder="请选择集团"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="onGroupChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in groupList"
|
||||
:key="item.hycd"
|
||||
:value="item.hycd"
|
||||
:label="item.hynm"
|
||||
>
|
||||
{{ item.hynm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 公司(单选,字符串) -->
|
||||
<a-form-item name="companyCode" label="公 司">
|
||||
<a-select
|
||||
v-model:value="organizationData.companyCode"
|
||||
placeholder="请选择公司"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in companyList"
|
||||
:key="item.hycd"
|
||||
:value="item.hycd"
|
||||
:label="item.hynm"
|
||||
>
|
||||
{{ item.hynm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 流域(多选,数组,必填) -->
|
||||
<a-form-item name="rvcdCode" label="流 域" required>
|
||||
<a-select
|
||||
v-model:value="organizationData.rvcdCode"
|
||||
mode="multiple"
|
||||
placeholder="请选择流域"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="onRvcdChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in basinList"
|
||||
:key="item.rvcd"
|
||||
:value="item.rvcd"
|
||||
:label="item.rvnm"
|
||||
>
|
||||
{{ item.rvnm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 电站(多选,数组,必填,依赖流域) -->
|
||||
<a-form-item name="stationCode" label="电 站" required>
|
||||
<a-select
|
||||
v-model:value="organizationData.stationCode"
|
||||
mode="multiple"
|
||||
placeholder="请先选择流域"
|
||||
:disabled="organizationData.rvcdCode.length === 0"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in stationList"
|
||||
:key="item.stcd"
|
||||
:value="item.stcd"
|
||||
:label="item.ennm"
|
||||
>
|
||||
{{ item.ennm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||
import {
|
||||
getBasinList,
|
||||
getGroupList,
|
||||
getCompanyList,
|
||||
getStationList,
|
||||
sendSmsCode,
|
||||
verifySmsCode,
|
||||
registerUser,
|
||||
getAccessToken
|
||||
} from '@/api/auth';
|
||||
import { message } from 'ant-design-vue';
|
||||
import router from '@/router';
|
||||
import { encrypt } from '@/utils/rsaEncrypt';
|
||||
|
||||
// 注册表单数据
|
||||
const registerData = reactive({
|
||||
// 用户信息
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
realName: '',
|
||||
belongingUnit: '',
|
||||
phone: '',
|
||||
smsCode: ''
|
||||
});
|
||||
|
||||
const accessToken = ref(''); // access_token
|
||||
// 下拉选项数据
|
||||
const basinList = ref<any[]>([]);
|
||||
const groupList = ref<any[]>([]);
|
||||
const companyList = ref<any[]>([]);
|
||||
const stationList = ref<any[]>([]);
|
||||
|
||||
// 短信验证码倒计时
|
||||
const smsCountdown = ref(0);
|
||||
let smsTimer: any = null;
|
||||
|
||||
// ==================== 组织表单数据 ====================
|
||||
const organizationData = reactive({
|
||||
groupCode: '', // 集团(单选,字符串)
|
||||
companyCode: '', // 公司(单选,字符串)
|
||||
rvcdCode: [], // 流域(多选,数组)
|
||||
stationCode: [] // 电站(多选,数组)
|
||||
});
|
||||
|
||||
// 组织表单引用
|
||||
const organizationFormRef = ref();
|
||||
|
||||
// ==================== 组织表单验证规则 ====================
|
||||
const organizationRules = {
|
||||
rvcdCode: [
|
||||
{
|
||||
validator: (rule: any, value: any[]) => {
|
||||
if (!value || value.length === 0) {
|
||||
return Promise.reject('请至少选择一个流域');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
stationCode: [
|
||||
{
|
||||
validator: (rule: any, value: any[]) => {
|
||||
if (!value || value.length === 0) {
|
||||
return Promise.reject('请至少选择一个电站');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const registerRules = {
|
||||
// 登录账号
|
||||
username: [
|
||||
{ required: true, message: '请输入登录账号', trigger: 'blur' },
|
||||
{ min: 4, max: 20, message: '账号长度4-20个字符', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '只能包含字母、数字和下划线',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
|
||||
// 密码
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('请输入密码');
|
||||
}
|
||||
|
||||
// 1. 长度检查:不低于10位
|
||||
if (value.length < 10) {
|
||||
return Promise.reject('密码长度不能少于10位');
|
||||
}
|
||||
|
||||
// 2. 复杂度检查:四类字符中至少包含三类
|
||||
const hasUpperCase = /[A-Z]/.test(value);
|
||||
const hasLowerCase = /[a-z]/.test(value);
|
||||
const hasNumber = /[0-9]/.test(value);
|
||||
const hasSpecial = /[^a-zA-Z0-9]/.test(value);
|
||||
const typeCount = [
|
||||
hasUpperCase,
|
||||
hasLowerCase,
|
||||
hasNumber,
|
||||
hasSpecial
|
||||
].filter(Boolean).length;
|
||||
|
||||
if (typeCount < 3) {
|
||||
return Promise.reject(
|
||||
'密码必须包含大写字母、小写字母、数字、特殊字符中的至少三类'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 禁止连续重复序列检查
|
||||
// 3.1 检查是否有2位及以上相同字符重复
|
||||
for (let i = 0; i <= value.length - 2; i++) {
|
||||
if (value[i] === value[i + 1]) {
|
||||
return Promise.reject(
|
||||
'密码不能包含2位及以上相同字符的连续重复(如11、aa)'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3.2 检查是否有2位及以上连续递增/递减的数字
|
||||
for (let i = 0; i <= value.length - 2; i++) {
|
||||
const char1 = value.charCodeAt(i);
|
||||
const char2 = value.charCodeAt(i + 1);
|
||||
|
||||
// 检查是否都是数字
|
||||
if (/\d/.test(value[i]) && /\d/.test(value[i + 1])) {
|
||||
// 检查递增或递减
|
||||
if (Math.abs(char2 - char1) === 1) {
|
||||
return Promise.reject(
|
||||
'密码不能包含2位及以上连续递增或递减的数字(如12、21)'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3.3 检查是否有2位及以上连续递增/递减的字母
|
||||
for (let i = 0; i <= value.length - 2; i++) {
|
||||
const char1 = value.charCodeAt(i);
|
||||
const char2 = value.charCodeAt(i + 1);
|
||||
|
||||
// 检查是否都是字母
|
||||
if (/[a-zA-Z]/.test(value[i]) && /[a-zA-Z]/.test(value[i + 1])) {
|
||||
// 检查递增或递减
|
||||
if (Math.abs(char2 - char1) === 1) {
|
||||
return Promise.reject(
|
||||
'密码不能包含2位及以上连续递增或递减的字母(如ab、ba)'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 用户名关联检查(严格模式,忽略大小写)
|
||||
// 4.0 先判断用户名是否为空,若为空则跳过该检查
|
||||
const username = registerData.username;
|
||||
if (!username || username.trim() === '') {
|
||||
// 用户名为空,跳过用户名关联检查
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const usernameLower = username.toLowerCase();
|
||||
const passwordLower = value.toLowerCase();
|
||||
|
||||
// 4.1 检查密码是否完整包含用户名
|
||||
if (passwordLower.includes(usernameLower)) {
|
||||
return Promise.reject('密码不能包含用户名');
|
||||
}
|
||||
|
||||
// 4.2 提取密码中的字母部分
|
||||
const passwordLetters = passwordLower.replace(/[^a-z]/g, '');
|
||||
|
||||
// 4.3 如果密码字母部分长度 >= 3,检查是否是用户名的子串
|
||||
if (
|
||||
passwordLetters.length >= 3 &&
|
||||
usernameLower.includes(passwordLetters)
|
||||
) {
|
||||
return Promise.reject('密码的字母部分不能是用户名的子串');
|
||||
}
|
||||
|
||||
// 4.4 检查用户名中是否包含密码的任意连续3位字母
|
||||
for (let i = 0; i <= passwordLetters.length - 3; i++) {
|
||||
const substring = passwordLetters.substring(i, i + 3);
|
||||
if (usernameLower.includes(substring)) {
|
||||
return Promise.reject('密码不能与用户名存在明显关联');
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
|
||||
// 确认密码
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string) => {
|
||||
if (value && value !== registerData.password) {
|
||||
return Promise.reject('两次输入的密码不一致');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
|
||||
// 真实姓名
|
||||
realName: [
|
||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '姓名长度2-20个字符', trigger: 'blur' }
|
||||
],
|
||||
|
||||
// 所属单位(选填,无验证规则)
|
||||
|
||||
// 手机号
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的11位手机号',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
|
||||
// 短信验证码
|
||||
smsCode: [
|
||||
{ required: true, message: '请输入短信验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '验证码为6位数字', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const activeTab = ref('register');
|
||||
const modalVisible = ref(false);
|
||||
|
||||
// 初始化加载集团列表
|
||||
const loadGroupList = async () => {
|
||||
try {
|
||||
const res = await getGroupList();
|
||||
groupList.value = res.data || [];
|
||||
} catch (error) {
|
||||
// console.error("加载集团列表失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 集团变化
|
||||
const onGroupChange = async () => {
|
||||
// 加载公司列表
|
||||
// if (value) {
|
||||
try {
|
||||
// const res = await getCompanyList(organizationData.groupCode ? organizationData.groupCode : '');
|
||||
const res = await getCompanyList();
|
||||
companyList.value = res.data || [];
|
||||
} catch (error) {
|
||||
// message.error("加载公司列表失败");
|
||||
}
|
||||
// }
|
||||
};
|
||||
|
||||
// 公司变化
|
||||
const onCompanyChange = async () => {
|
||||
// 加载流域列表
|
||||
// if (value) {
|
||||
try {
|
||||
const res = await getBasinList();
|
||||
basinList.value = res.data || [];
|
||||
} catch (error) {
|
||||
// message.error("加载流域列表失败");
|
||||
}
|
||||
// }
|
||||
};
|
||||
|
||||
// 流域变化
|
||||
const onBasinChange = async (ids: any) => {
|
||||
// 加载电站列表
|
||||
// if (value) {
|
||||
try {
|
||||
const res = await getStationList(ids);
|
||||
stationList.value = res.data || [];
|
||||
} catch (error) {
|
||||
// message.error("加载电站列表失败");
|
||||
}
|
||||
// }
|
||||
};
|
||||
|
||||
// ==================== 流域变化处理(弹框内) ====================
|
||||
const onRvcdChange = () => {
|
||||
// 清空电站选择
|
||||
organizationData.stationCode = [];
|
||||
// 获取当前选中的所有流域ID,加载对应的电站列表
|
||||
if (organizationData.rvcdCode && organizationData.rvcdCode.length > 0) {
|
||||
onBasinChange({ rvcds: organizationData.rvcdCode });
|
||||
} else {
|
||||
// 如果没有选择流域,清空电站列表
|
||||
stationList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 发送短信验证码
|
||||
const handleSendSms = async () => {
|
||||
// 验证手机号格式
|
||||
if (!/^1[3-9]\d{9}$/.test(registerData.phone)) {
|
||||
message.error('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await sendSmsCode(registerData.phone, 1); // type=1 表示注册
|
||||
if (res.code == 1) {
|
||||
return;
|
||||
}
|
||||
message.success('验证码已发送');
|
||||
|
||||
// 启动60秒倒计时
|
||||
smsCountdown.value = 60;
|
||||
smsTimer = setInterval(() => {
|
||||
smsCountdown.value--;
|
||||
if (smsCountdown.value <= 0) {
|
||||
clearInterval(smsTimer);
|
||||
smsTimer = null;
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
// message.error(error.message || "发送失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 表单验证通过后的处理
|
||||
const handleFormFinish = async () => {
|
||||
// 第一步:验证短信验证码
|
||||
// const res: any = await verifySmsCode(registerData.phone, registerData.smsCode, 1);
|
||||
// if (res.code == 1) {
|
||||
// message.error('短信验证码错误,请重试')
|
||||
// return
|
||||
// }
|
||||
// 打开确认弹框
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
// 弹框取消
|
||||
const handleModalCancel = () => {
|
||||
// 关闭弹框,不做任何操作
|
||||
modalVisible.value = false;
|
||||
// 重置组织表单数据
|
||||
organizationData.groupCode = '';
|
||||
organizationData.companyCode = '';
|
||||
organizationData.rvcdCode = [];
|
||||
organizationData.stationCode = [];
|
||||
// 清除表单验证状态
|
||||
organizationFormRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 注册提交
|
||||
const onRegister = async () => {
|
||||
// 先验证组织表单
|
||||
try {
|
||||
await organizationFormRef.value.validate();
|
||||
} catch (error) {
|
||||
// message.error("请完善组织信息");
|
||||
return; // 验证失败,阻止提交
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 第二步:密码加密
|
||||
const encryptedPassword = encrypt(registerData.password);
|
||||
|
||||
// 第三步:构造注册数据(包含组织信息)
|
||||
const registerParams: any = {
|
||||
type: 1, // 固定值:注册类型
|
||||
phone: registerData.phone, // 手机号(必填)
|
||||
code: registerData.smsCode, // 短信验证码(必填,字段改名)
|
||||
username: registerData.username, // 用户名(必填)
|
||||
password: encryptedPassword, // 加密后的密码(必填)
|
||||
realName: registerData.realName // 真实姓名(必填)
|
||||
};
|
||||
|
||||
// 可选字段:所属单位(如果有值才传)
|
||||
if (registerData.belongingUnit) {
|
||||
registerParams.belongingUnit = registerData.belongingUnit;
|
||||
}
|
||||
|
||||
// 可选字段:集团(如果有值才传)
|
||||
if (organizationData.groupCode) {
|
||||
registerParams.groupCode = organizationData.groupCode;
|
||||
}
|
||||
|
||||
// 可选字段:公司(如果有值才传)
|
||||
if (organizationData.companyCode) {
|
||||
registerParams.companyCode = organizationData.companyCode;
|
||||
}
|
||||
|
||||
// 可选字段:流域(如果有值才传,数组转逗号分隔字符串)
|
||||
if (organizationData.rvcdCode && organizationData.rvcdCode.length > 0) {
|
||||
registerParams.rvcdCode = organizationData.rvcdCode.join(',');
|
||||
}
|
||||
|
||||
// 可选字段:电站(如果有值才传,数组转逗号分隔字符串)
|
||||
if (
|
||||
organizationData.stationCode &&
|
||||
organizationData.stationCode.length > 0
|
||||
) {
|
||||
registerParams.stationCode = organizationData.stationCode.join(',');
|
||||
}
|
||||
|
||||
// 第四步:调用注册接口
|
||||
const res1: any = await registerUser(registerParams);
|
||||
if (res1.code == 1) {
|
||||
message.error('注册失败,请重试');
|
||||
return;
|
||||
}
|
||||
|
||||
message.success('注册成功,等待管理员审核');
|
||||
|
||||
// 关闭弹框
|
||||
modalVisible.value = false;
|
||||
|
||||
// 延迟跳转到登录页
|
||||
setTimeout(() => {
|
||||
router.push({ path: '/login' });
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
// 区分验证码错误和注册错误
|
||||
// if (error.message && error.message.includes('验证码')) {
|
||||
// message.error("验证码错误,请重试");
|
||||
// } else {
|
||||
// message.error(error.message || "注册失败,请重试");
|
||||
// }
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 返回登录
|
||||
const backToLogin = () => {
|
||||
router.push({ path: '/login' });
|
||||
};
|
||||
const init = async () => {
|
||||
try {
|
||||
const res: any = await getAccessToken();
|
||||
if (res.code == 1) {
|
||||
message.error('获取令牌失败,请重试');
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('register-token', res.data.registerToken);
|
||||
accessToken.value = res.data.accessToken;
|
||||
loadGroupList();
|
||||
onGroupChange();
|
||||
onCompanyChange();
|
||||
} catch (error) {
|
||||
// message.error("获取令牌失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载集团列表
|
||||
onMounted(() => {
|
||||
init();
|
||||
|
||||
// modalVisible.value = true
|
||||
});
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
if (smsTimer) {
|
||||
clearInterval(smsTimer);
|
||||
smsTimer = null;
|
||||
}
|
||||
});
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
if (!option.label) return false;
|
||||
return option.label.indexOf(inputValue) !== -1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.register-container {
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 1200px; // 降低最小宽度要求
|
||||
background-color: #fff;
|
||||
|
||||
.register-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
background: url('@/assets/images/bg_sjtb.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover; // 改为cover以更好地适配不同尺寸
|
||||
background-position: center; // 居中显示背景图
|
||||
}
|
||||
|
||||
// 左侧背景区域
|
||||
.left-section {
|
||||
.slogan {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 5%; // 减小左边距,更靠左
|
||||
width: 40vw; // 使用视口宽度百分比
|
||||
max-width: 700px; // 保持最大宽度限制
|
||||
min-width: 300px; // 设置最小宽度
|
||||
color: #040504;
|
||||
font-size: clamp(24px, 3vw, 40px); // 响应式字体大小
|
||||
line-height: 1.5;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
word-wrap: break-word; // 允许长文本换行
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧注册卡片区域
|
||||
.right-section {
|
||||
position: absolute;
|
||||
right: 5%; // 改用right定位,更合理
|
||||
top: 50%; // 垂直居中
|
||||
transform: translateY(-50%); // 垂直居中对齐
|
||||
width: clamp(380px, 30vw, 500px); // 响应式宽度
|
||||
min-height: 600px; // 降低最小高度
|
||||
max-height: 90vh; // 限制最大高度为视口的90%
|
||||
border-radius: 8px; // 稍微增大圆角
|
||||
padding: clamp(15px, 2vw, 24px) clamp(15px, 2vw, 24px); // 响应式内边距
|
||||
padding-top: 0 !important;
|
||||
background-color: rgba(255, 255, 255, 0.98); // 轻微透明效果
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); // 添加阴影提升层次感
|
||||
|
||||
// 自定义滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
height: auto; // 自动高度
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
// 表单分区样式
|
||||
.form-section {
|
||||
margin-bottom: clamp(12px, 1.5vh, 20px); // 响应式间距
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(13px, 1.2vw, 15px); // 响应式字体
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding-left: 10px;
|
||||
margin-bottom: clamp(8px, 1vh, 12px);
|
||||
padding-bottom: clamp(6px, 0.8vh, 10px);
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: clamp(10px, 1.2vh, 16px); // 响应式表单项间距
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
text-align: right !important;
|
||||
line-height: 32px;
|
||||
|
||||
> label {
|
||||
font-size: clamp(12px, 1vw, 14px); // 响应式标签字体
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
&::after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-input-password) {
|
||||
font-size: clamp(12px, 1vw, 14px); // 输入框字体响应式
|
||||
}
|
||||
|
||||
:deep(.ant-btn) {
|
||||
font-size: clamp(12px, 1vw, 14px); // 按钮字体响应式
|
||||
}
|
||||
|
||||
:deep(.ant-radio-group) {
|
||||
display: flex;
|
||||
gap: clamp(12px, 1.5vw, 20px); // 响应式间距
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-control-input-content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式媒体查询 - 小屏幕适配
|
||||
@media (max-width: 1400px) {
|
||||
.register-container {
|
||||
.left-section {
|
||||
.slogan {
|
||||
left: 3%;
|
||||
width: 45vw;
|
||||
font-size: clamp(20px, 2.5vw, 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
right: 3%;
|
||||
width: clamp(350px, 35vw, 450px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 中等屏幕适配
|
||||
@media (min-width: 1401px) and (max-width: 1920px) {
|
||||
.register-container {
|
||||
.left-section {
|
||||
.slogan {
|
||||
left: 5%;
|
||||
width: 40vw;
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
right: 5%;
|
||||
width: clamp(380px, 30vw, 480px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 大屏幕适配
|
||||
@media (min-width: 1921px) {
|
||||
.register-container {
|
||||
.left-section {
|
||||
.slogan {
|
||||
left: 8%;
|
||||
width: 35vw;
|
||||
font-size: clamp(32px, 3.5vw, 48px);
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
right: 8%;
|
||||
width: clamp(420px, 28vw, 550px);
|
||||
padding: 24px 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 超小屏幕适配
|
||||
@media (max-width: 1200px) {
|
||||
.register-container {
|
||||
.left-section {
|
||||
.slogan {
|
||||
display: none; // 小屏幕隐藏标语,聚焦表单
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translate(-50%, -50%); // 水平垂直都居中
|
||||
width: clamp(320px, 90vw, 420px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 高缩放比例适配(125%、150%等)
|
||||
@media (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) {
|
||||
.register-container {
|
||||
.right-section {
|
||||
padding: clamp(12px, 1.5vw, 20px);
|
||||
|
||||
.form-section {
|
||||
margin-bottom: clamp(10px, 1.2vh, 16px);
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 横屏模式优化
|
||||
@media (orientation: landscape) and (max-height: 700px) {
|
||||
.register-container {
|
||||
.right-section {
|
||||
max-height: 95vh;
|
||||
padding: 10px 15px;
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 6px;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -643,7 +643,8 @@ onMounted(() => {
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.systemcode == '1'">Web端</span>
|
||||
<span v-else-if="scope.row.systemcode == '2'">手机App</span>
|
||||
<span v-else>Pad端</span>
|
||||
<span v-else-if="scope.row.systemcode == '3'">Pad端</span>
|
||||
<span v-else-if="scope.row.systemcode == '4'">数据填报</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="图标" width="100" prop="icon" align="center">
|
||||
|
||||
@ -7,7 +7,7 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
|
||||
VITE_APP_PORT = 3000
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
# 本地环境
|
||||
VITE_APP_BASE_URL = 'http://localhost:8093'
|
||||
VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
|
||||
# 测试环境
|
||||
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
|
||||
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
|
||||
|
||||
17
frontend/src/api/DataQueryMenuModule/index.ts
Normal file
17
frontend/src/api/DataQueryMenuModule/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export function getFishResourceList(data) {
|
||||
return request({
|
||||
url: '/fpr/fishDic/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function getFishResourceZYList(data) {
|
||||
return request({
|
||||
url: '/fpr/fishDic/zy/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
@ -43,7 +43,7 @@
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:presets="item.presets"
|
||||
style="width: 100%"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
@change="val => triggerManualValuesChange(item.name, val)"
|
||||
/>
|
||||
|
||||
<!-- 日期范围选择器 -->
|
||||
@ -58,7 +58,7 @@
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:presets="item.presets"
|
||||
style="width: 100%"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
@change="val => triggerManualValuesChange(item.name, val)"
|
||||
/>
|
||||
|
||||
<!-- 普通输入框 -->
|
||||
@ -68,11 +68,16 @@
|
||||
:placeholder="item.placeholder || '请输入'"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
|
||||
@change="
|
||||
e => triggerManualValuesChange(item.name, e.target.value)
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- 电站下拉框 -->
|
||||
<div class="flex gap-[10px]" v-else-if="item.type === 'waterStation'">
|
||||
<div
|
||||
class="flex gap-[10px]"
|
||||
v-else-if="item.type === 'waterStation'"
|
||||
>
|
||||
<a-form-item-rest>
|
||||
<!-- 基地下拉框 -->
|
||||
<!-- <a-select
|
||||
@ -103,7 +108,11 @@
|
||||
:loading="shuJuTianBaoStore.lyLoading"
|
||||
:filter-option="filterOption"
|
||||
style="width: 135px"
|
||||
:dropdown-style="{ maxHeight: '400px', overflow: 'auto', minWidth: '360px' }"
|
||||
:dropdown-style="{
|
||||
maxHeight: '400px',
|
||||
overflow: 'auto',
|
||||
minWidth: '360px'
|
||||
}"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in shuJuTianBaoStore.lyOption"
|
||||
@ -168,8 +177,9 @@
|
||||
v-model:value="formData[item.name]"
|
||||
:placeholder="item.placeholder || '请选择'"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:loading="item.loading || false"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
@change="val => triggerManualValuesChange(item.name, val)"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
@ -195,13 +205,13 @@
|
||||
item.fieldNames || {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
children: 'children',
|
||||
children: 'children'
|
||||
}
|
||||
"
|
||||
:show-search="item.showSearch !== false"
|
||||
:tree-checkable="item.treeCheckable"
|
||||
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
@change="val => triggerManualValuesChange(item.name, val)"
|
||||
/>
|
||||
|
||||
<!-- 单选框 -->
|
||||
@ -209,9 +219,15 @@
|
||||
v-else-if="item.type === 'Radio'"
|
||||
v-model:value="formData[item.name]"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
|
||||
@change="
|
||||
e => triggerManualValuesChange(item.name, e.target.value)
|
||||
"
|
||||
>
|
||||
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value">
|
||||
<a-radio
|
||||
v-for="opt in item.options"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
@ -245,16 +261,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, reactive, watch, onMounted, nextTick } from "vue";
|
||||
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
|
||||
import { ref, computed, reactive, watch, onMounted, nextTick } from 'vue';
|
||||
import { useShuJuTianBaoStore } from '@/store/modules/shuJuTianBao';
|
||||
const shuJuTianBaoStore = useShuJuTianBaoStore();
|
||||
|
||||
// --- 类型定义 ---
|
||||
export interface SearchItem {
|
||||
type?: "Input" | "Select" | "TreeSelect" | "DataPicker" | string;
|
||||
type?: 'Input' | 'Select' | 'TreeSelect' | 'DataPicker' | string;
|
||||
name: string;
|
||||
label: string;
|
||||
picker?: "year" | "month" | "date" | "week";
|
||||
picker?: 'year' | 'month' | 'date' | 'week';
|
||||
fieldProps?: any;
|
||||
placeholder?: string;
|
||||
span?: number;
|
||||
@ -292,13 +308,13 @@ interface Props {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialValues: () => ({}),
|
||||
showSearch: true,
|
||||
showReset: true,
|
||||
showReset: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "finish", values: any): void;
|
||||
(e: "valuesChange", changedValues: any, allValues: any): void;
|
||||
(e: "reset"): void;
|
||||
(e: 'finish', values: any): void;
|
||||
(e: 'valuesChange', changedValues: any, allValues: any): void;
|
||||
(e: 'reset'): void;
|
||||
}>();
|
||||
|
||||
const formRef = ref<any>();
|
||||
@ -311,7 +327,7 @@ const filterOption = (inputValue: string, option: any) => {
|
||||
};
|
||||
// 2. 创建计算属性,自动过滤掉 false/null/undefined 的项
|
||||
const validSearchList = computed(() => {
|
||||
return props.searchList.filter((item) => item);
|
||||
return props.searchList.filter(item => item);
|
||||
});
|
||||
|
||||
// --- 初始化逻辑 ---
|
||||
@ -319,14 +335,14 @@ const initForm = () => {
|
||||
const initial = JSON.parse(JSON.stringify(props.initialValues || {}));
|
||||
|
||||
// 1. 清空 formData
|
||||
Object.keys(formData).forEach((key) => delete formData[key]);
|
||||
Object.keys(formData).forEach(key => delete formData[key]);
|
||||
|
||||
// 2. 赋值初始数据
|
||||
Object.assign(formData, initial);
|
||||
|
||||
// 3. 过滤掉 searchList 中为 false/null/undefined 的项,并生成规则
|
||||
validSearchList.value.forEach((item) => {
|
||||
if (item.type == "waterStation") {
|
||||
validSearchList.value.forEach(item => {
|
||||
if (item.type == 'waterStation') {
|
||||
// 下拉菜单
|
||||
// shuJuTianBaoStore.getBaseOption();
|
||||
shuJuTianBaoStore.getSelectForOption();
|
||||
@ -334,7 +350,7 @@ const initForm = () => {
|
||||
}
|
||||
if (item.fieldProps?.required) {
|
||||
rules[item.name] = [
|
||||
{ required: true, message: `${item.label}不能为空`, trigger: "blur" },
|
||||
{ required: true, message: `${item.label}不能为空`, trigger: 'blur' }
|
||||
];
|
||||
}
|
||||
});
|
||||
@ -350,7 +366,7 @@ const triggerManualValuesChange = (changedKey: string, newValue: any) => {
|
||||
const changedValues = { [changedKey]: newValue };
|
||||
// 发射事件,传递变更值和当前所有值
|
||||
// 注意:这里使用 {...formData} 是为了传递当前的最新状态
|
||||
emit("valuesChange", changedValues, { ...formData });
|
||||
emit('valuesChange', changedValues, { ...formData });
|
||||
};
|
||||
|
||||
// const dataDimensionDataChange = (value: any) => {
|
||||
@ -364,24 +380,24 @@ const triggerManualValuesChange = (changedKey: string, newValue: any) => {
|
||||
|
||||
const lyChange = (value: any) => {
|
||||
formData.rvcd = value;
|
||||
formData.rstcd = "";
|
||||
formData.rstcd = '';
|
||||
shuJuTianBaoStore.getEngOption(formData.rvcd);
|
||||
|
||||
// 【关键修改】手动触发 valuesChange,因为 a-form-item-rest 阻断了自动监听
|
||||
triggerManualValuesChange("rvcd", formData.rvcd);
|
||||
triggerManualValuesChange('rvcd', formData.rvcd);
|
||||
};
|
||||
|
||||
const stcdIdChange = (value: any) => {
|
||||
if (props.zhujianfujian == "fu") {
|
||||
if (props.zhujianfujian == 'fu') {
|
||||
formData.rstcd = value;
|
||||
shuJuTianBaoStore.getFpssOption(formData.rvcd, value);
|
||||
// 【关键修改】手动触发 valuesChange
|
||||
triggerManualValuesChange("rstcd", formData.rstcd);
|
||||
triggerManualValuesChange('rstcd', formData.rstcd);
|
||||
} else {
|
||||
formData.stcd = value;
|
||||
shuJuTianBaoStore.getFpssOption(formData.rvcd, value);
|
||||
// 【关键修改】手动触发 valuesChange
|
||||
triggerManualValuesChange("stcd", formData.stcd);
|
||||
triggerManualValuesChange('stcd', formData.stcd);
|
||||
}
|
||||
};
|
||||
|
||||
@ -391,7 +407,7 @@ onMounted(() => {
|
||||
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
(newVal) => {
|
||||
newVal => {
|
||||
if (newVal && formRef.value) {
|
||||
formRef.value.setFieldsValue(newVal);
|
||||
Object.assign(formData, newVal);
|
||||
@ -403,11 +419,11 @@ watch(
|
||||
// --- 事件处理 ---
|
||||
const handleFinish = (values: any) => {
|
||||
const finalValues = { ...formData, ...values };
|
||||
emit("finish", finalValues);
|
||||
emit('finish', finalValues);
|
||||
};
|
||||
|
||||
const handleValuesChange = (changedValues: any, allValues: any) => {
|
||||
emit("valuesChange", changedValues, allValues);
|
||||
emit('valuesChange', changedValues, allValues);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
@ -416,7 +432,7 @@ const handleReset = () => {
|
||||
nextTick(() => {
|
||||
initForm();
|
||||
});
|
||||
emit("reset");
|
||||
emit('reset');
|
||||
}
|
||||
};
|
||||
|
||||
@ -424,7 +440,7 @@ defineExpose({
|
||||
form: formRef,
|
||||
formData,
|
||||
reset: handleReset,
|
||||
submit: () => formRef.value?.submit(),
|
||||
submit: () => formRef.value?.submit()
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
:pagination="paginationConfig"
|
||||
:scroll="scrollConfig"
|
||||
:row-key="rowKey"
|
||||
:customRow="customRow"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<!-- 合计行插槽 -->
|
||||
@ -85,6 +86,7 @@ const emit = defineEmits<{
|
||||
e: 'sort-change',
|
||||
sortInfo: { field: string; order: 'ascend' | 'descend' | null }
|
||||
): void;
|
||||
(e: 'row-click', record: any): void;
|
||||
}>();
|
||||
|
||||
// --- State ---
|
||||
@ -195,7 +197,7 @@ const getList = async (filter?: Record<string, any>) => {
|
||||
|
||||
if (props.transformData) {
|
||||
const result = props.transformData(res);
|
||||
finalRecords = result.records || [];
|
||||
finalRecords = result.records || result.data || [];
|
||||
finalTotal = result.total || 0;
|
||||
} else {
|
||||
finalRecords = res?.data?.records || res?.data?.data || res?.data || [];
|
||||
@ -227,6 +229,17 @@ const handleTableChange = (pag: any, filters: any, sorter: any) => {
|
||||
getList();
|
||||
}
|
||||
};
|
||||
const handleRowClick = (record: any) => {
|
||||
emit('row-click', record);
|
||||
};
|
||||
|
||||
const customRow = (record: any) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
handleRowClick(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
page.value = 1;
|
||||
@ -309,7 +322,7 @@ const processData = (records: any[]) => {
|
||||
const createMergeCellHandler = (dataIndex: string) => {
|
||||
return (record: any, index: number) => {
|
||||
const currentValue = record[dataIndex];
|
||||
|
||||
|
||||
// 检查当前行是否是该值的起始行
|
||||
let isStartRow = true;
|
||||
if (index > 0) {
|
||||
@ -318,7 +331,7 @@ const createMergeCellHandler = (dataIndex: string) => {
|
||||
isStartRow = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!isStartRow) {
|
||||
// 不是起始行,设置 rowSpan 为 0(隐藏该单元格)
|
||||
return { rowSpan: 0 };
|
||||
@ -332,7 +345,7 @@ const createMergeCellHandler = (dataIndex: string) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return rowSpan > 1 ? { rowSpan } : {};
|
||||
}
|
||||
};
|
||||
@ -347,7 +360,7 @@ const enhancedColumns = computed(() => {
|
||||
...col,
|
||||
customCell: createMergeCellHandler(col.dataIndex)
|
||||
};
|
||||
|
||||
|
||||
// 如果有 slots.customRender,移除 slots
|
||||
if (baseCol.slots?.customRender) {
|
||||
const { slots, ...restCol } = baseCol;
|
||||
@ -357,10 +370,10 @@ const enhancedColumns = computed(() => {
|
||||
...(props.enableEllipsis ? { ellipsis: true } : {})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return baseCol;
|
||||
}
|
||||
|
||||
|
||||
// 如果有 slots.customRender,移除 slots(由 bodyCell 处理),保留 dataIndex 用于匹配
|
||||
if (col.slots?.customRender) {
|
||||
const { slots, ...restCol } = col;
|
||||
@ -428,4 +441,9 @@ watch(
|
||||
::v-deep(.ant-table-tbody > tr > td) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
:deep(.ant-pagination) {
|
||||
.ant-select {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -19,6 +19,7 @@ service.interceptors.request.use(
|
||||
`Expected 'config' and 'config.headers' not to be undefined`
|
||||
);
|
||||
}
|
||||
config.headers.tenant_id = '1';
|
||||
if (
|
||||
config.url.includes('/dec-lygk-base-server') ||
|
||||
config.url.includes('/wmp-env-server') ||
|
||||
@ -33,7 +34,7 @@ service.interceptors.request.use(
|
||||
config.headers._sysid = '10EC2E0B-AEA9-4757-83A2-201BA1BC54E9';
|
||||
|
||||
config.headers.authorization =
|
||||
'bearer aa5be16b-d1a3-4b93-9886-b2490025c7d6';
|
||||
'bearer 3c424544-50d5-4787-9453-97bc18e44427';
|
||||
config.baseURL = '/';
|
||||
} else {
|
||||
const user = useUserStoreHook();
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
批复文件
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="h-full bg-red-500">
|
||||
基础
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
综合导出
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
过鱼监测数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
鱼类放鱼数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="approval-detail-search">
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="exportBtn">导出</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import moment from 'moment';
|
||||
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BasicSearch from '@/components/BasicSearch/index.vue';
|
||||
|
||||
interface Props {
|
||||
exportBtn: () => void;
|
||||
fish_ptype: any[];
|
||||
familyList: any[];
|
||||
ptypeLoading?: boolean;
|
||||
familyLoading?: boolean;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'search-finish', values: any): void;
|
||||
}>();
|
||||
|
||||
const localTypeDate = ref<string>(null);
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const initSearchData = {
|
||||
rvcd: 'all',
|
||||
stcd: null,
|
||||
ptype: 'all',
|
||||
family: '全部'
|
||||
};
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: 'waterStation',
|
||||
name: 'rvcd',
|
||||
label: '流域',
|
||||
fieldProps: {
|
||||
allowClear: true
|
||||
},
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: 'Select',
|
||||
name: 'ptype',
|
||||
label: '保护类别',
|
||||
fieldProps: {
|
||||
allowClear: false
|
||||
},
|
||||
loading: props.ptypeLoading,
|
||||
options: props.fish_ptype.map((item: any) => ({
|
||||
label: item.dictName,
|
||||
value: item.itemCode
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'Select',
|
||||
name: 'family',
|
||||
label: '科',
|
||||
fieldProps: {
|
||||
allowClear: false
|
||||
},
|
||||
loading: props.familyLoading,
|
||||
options: props.familyList.map((item: any) => ({
|
||||
label: item.key,
|
||||
value: item.key
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'Input',
|
||||
name: 'name',
|
||||
label: '',
|
||||
placeholder: '请输入鱼类名称',
|
||||
fieldProps: {},
|
||||
options: []
|
||||
}
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit('search-finish', values);
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
searchData.value = { ...searchData.value, ...allValues };
|
||||
if (
|
||||
Object.keys(changedValues)[0] == 'rstcd' ||
|
||||
Object.keys(changedValues)[0] == 'rvcd'
|
||||
) {
|
||||
const formInstance = basicSearchRef.value?.formData;
|
||||
formInstance.stcd = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
localTypeDate.value = null;
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
btnLoading,
|
||||
searchData
|
||||
});
|
||||
onMounted(() => {
|
||||
emit('search-finish', initSearchData);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<!-- 搜索组件 -->
|
||||
<FishResourceSearch
|
||||
:fish_ptype="fish_ptype_list"
|
||||
:familyList="familyList"
|
||||
:ptypeLoading="ptypeLoading"
|
||||
:familyLoading="familyLoading"
|
||||
:exportBtn="exportBtn"
|
||||
@search-finish="onSearchFinish"
|
||||
ref="searchRef"
|
||||
/>
|
||||
<a-row
|
||||
type="flex"
|
||||
class="mt-[20px] w-full"
|
||||
style="height: calc(100% - 80px)"
|
||||
>
|
||||
<a-col flex="75%" class="pr-4 flex overflow-auto">
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollX="tableScrollX"
|
||||
:scrollY="tableScrollY"
|
||||
:columns="columns"
|
||||
:list-url="getFishResourceZYList"
|
||||
:transformData="transformData"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
</BasicTable>
|
||||
</a-col>
|
||||
<a-col flex="25%" class="overflow-hidden">
|
||||
<div
|
||||
ref="rightPanelRef"
|
||||
class="bg-[#fafafa] h-full pt-[10px] pr-[15px] pl-[15px]"
|
||||
>
|
||||
<a-spin :spinning="rightPanelLoading">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div
|
||||
style="
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background-color: rgb(47, 107, 152);
|
||||
margin-right: 10px;
|
||||
"
|
||||
></div>
|
||||
<div style="font-size: 18px; color: rgba(0, 0, 0, 0.85)">
|
||||
{{ fishResource.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:style="{ width: carouselWidth + 'px' }"
|
||||
class="mt-[10px] px-[10px]"
|
||||
>
|
||||
<a-carousel
|
||||
arrows
|
||||
v-if="fishResource.logoList.length > 0"
|
||||
class="w-full h-[306px] slick-arrow"
|
||||
>
|
||||
<template #prevArrow>
|
||||
<div
|
||||
class="custom-slick-arrow"
|
||||
style="left: 10px; z-index: 1"
|
||||
>
|
||||
<left-circle-outlined />
|
||||
</div>
|
||||
</template>
|
||||
<template #nextArrow>
|
||||
<div class="custom-slick-arrow" style="right: 10px">
|
||||
<right-circle-outlined />
|
||||
</div>
|
||||
</template>
|
||||
<div v-for="(img, index) in fishResource.logoList" :key="index">
|
||||
<img
|
||||
:src="img"
|
||||
alt=""
|
||||
class="w-full h-[306px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
</a-carousel>
|
||||
<a-empty v-else description="暂无图片" class="h-[306px]" />
|
||||
</div>
|
||||
<div
|
||||
class="w-full mt-[10px] px-[10px]"
|
||||
style="
|
||||
margin-top: 24px;
|
||||
font-size: 18px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
max-height: 440px;
|
||||
overflow-y: scroll;
|
||||
"
|
||||
>
|
||||
{{ fishResource.shapedesc }}
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick,
|
||||
h
|
||||
} from 'vue';
|
||||
import FishResourceSearch from './FishResourceSearch.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { getDictItemsByCode } from '@/api/dict';
|
||||
import {
|
||||
getFishResourceList,
|
||||
getFishResourceZYList
|
||||
} from '@/api/DataQueryMenuModule';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
const fishResource = ref<any>({
|
||||
name: '',
|
||||
logoList: [],
|
||||
shapedesc: ''
|
||||
});
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
|
||||
const rightPanelRef = ref<HTMLElement | null>(null);
|
||||
const carouselWidth = ref(0);
|
||||
const tableScrollY = ref<string | number>(0);
|
||||
|
||||
const updateCarouselWidth = () => {
|
||||
if (rightPanelRef.value) {
|
||||
const rect = rightPanelRef.value.getBoundingClientRect();
|
||||
carouselWidth.value = rect.width - 20; // 减去 px-[10px] 的 padding
|
||||
}
|
||||
};
|
||||
|
||||
const updateTableScrollY = () => {
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(rightPanelRef.value);
|
||||
});
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateCarouselWidth();
|
||||
updateTableScrollY();
|
||||
});
|
||||
|
||||
const tableScrollX = computed(() =>
|
||||
Math.max(
|
||||
columns.reduce((sum: number, col: any) => sum + (col.width || 180), 0),
|
||||
600
|
||||
)
|
||||
);
|
||||
const columns: any = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
visible: true,
|
||||
width: 80,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: '中文名称',
|
||||
dataIndex: 'name',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
customRender: ({ text }: any) => {
|
||||
return text
|
||||
? h('span', { style: { color: '#2f6b98', cursor: 'pointer' } }, text)
|
||||
: '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'nameEn',
|
||||
title: '拉丁学名',
|
||||
dataIndex: 'nameEn',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
key: 'rvcdName',
|
||||
title: '所属流域',
|
||||
dataIndex: 'rvcdName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
key: 'typeName',
|
||||
title: '分类',
|
||||
dataIndex: 'typeName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'species',
|
||||
title: '种',
|
||||
dataIndex: 'species',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'genus',
|
||||
title: '属',
|
||||
dataIndex: 'genus',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'family',
|
||||
title: '科',
|
||||
dataIndex: 'family',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'orders',
|
||||
title: '目',
|
||||
dataIndex: 'orders',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
},
|
||||
// {
|
||||
// "key": "nameEn",
|
||||
// "title": "英文名称",
|
||||
// "dataIndex": "nameEn",
|
||||
// "visible": true,
|
||||
// width: 200,
|
||||
// ellipsis: true,
|
||||
|
||||
// },
|
||||
{
|
||||
key: 'alias',
|
||||
title: '俗名',
|
||||
dataIndex: 'alias',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'fsz',
|
||||
title: '成鱼大小(cm)',
|
||||
dataIndex: 'fsz',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'shapedesc',
|
||||
title: '形态描述',
|
||||
dataIndex: 'shapedesc',
|
||||
visible: true,
|
||||
width: 300,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'habitation',
|
||||
title: '栖息习性',
|
||||
dataIndex: 'habitation',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'habitMigrat',
|
||||
title: '洄游习性',
|
||||
dataIndex: 'habitMigrat',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'feedingHabit',
|
||||
title: '摄食习性',
|
||||
dataIndex: 'feedingHabit',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'food',
|
||||
title: '主要食物',
|
||||
dataIndex: 'food',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'timeFeed',
|
||||
title: '觅食时段',
|
||||
dataIndex: 'timeFeed',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'spawnMonth',
|
||||
title: '产卵期(月)',
|
||||
dataIndex: 'spawnMonth',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'orignDate',
|
||||
title: '产地及产期',
|
||||
dataIndex: 'orignDate',
|
||||
visible: true,
|
||||
width: 300,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
title: '内容',
|
||||
dataIndex: 'description',
|
||||
visible: true,
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'pretemp',
|
||||
title: '适宜温度',
|
||||
dataIndex: 'pretemp',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'flowRate',
|
||||
title: '适宜流速',
|
||||
dataIndex: 'flowRate',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'depth',
|
||||
title: '适宜水深(m)',
|
||||
dataIndex: 'depth',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'wqtq',
|
||||
title: '水质要求',
|
||||
dataIndex: 'wqtq',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'spawnCharact',
|
||||
title: '型',
|
||||
dataIndex: 'spawnCharact',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'botmMater',
|
||||
title: '地质',
|
||||
dataIndex: 'botmMater',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ptypeName',
|
||||
title: '保护类型',
|
||||
dataIndex: 'ptypeName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'specOriginName',
|
||||
title: '物种来源',
|
||||
dataIndex: 'specOriginName',
|
||||
visible: true,
|
||||
width: 100,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'vlsr',
|
||||
title: '数据来源',
|
||||
dataIndex: 'vlsr',
|
||||
visible: true,
|
||||
width: 150,
|
||||
ellipsis: true
|
||||
}
|
||||
// {
|
||||
// "key": "inffile",
|
||||
// "title": "附件预览",
|
||||
// "dataIndex": "inffile",
|
||||
// "visible": true,
|
||||
// width: 100,
|
||||
// ellipsis: true,
|
||||
// "fixed": "right",
|
||||
// },
|
||||
];
|
||||
const rightNumber = ref(0);
|
||||
const tableRef = ref();
|
||||
const fish_ptype_list = ref<any>([]);
|
||||
const familyList = ref<any>([]);
|
||||
const ptypeLoading = ref(false);
|
||||
const familyLoading = ref(false);
|
||||
const rightPanelLoading = ref(false);
|
||||
|
||||
const exportBtn = () => {
|
||||
console.log(fishResource.value);
|
||||
};
|
||||
const handleRowClick = (record: any) => {
|
||||
rightPanelLoading.value = true;
|
||||
|
||||
// 处理 inffile:支持多个图片(逗号分隔)、单个图片、无图片
|
||||
let logoList: string[] = [];
|
||||
if (record.inffile && record.inffile !== '-') {
|
||||
// 按逗号分割,过滤空值和占位符,拼接完整 URL
|
||||
logoList = record.inffile
|
||||
.split(',')
|
||||
.map((item: string) => item.trim())
|
||||
.filter((item: string) => item && item !== '-' && item.length > 0)
|
||||
.map((item: string) => `${baseUrl}/?${item}&view=jpg`);
|
||||
}
|
||||
|
||||
fishResource.value = {
|
||||
name: record.name,
|
||||
logoList,
|
||||
shapedesc: record.shapedesc
|
||||
};
|
||||
|
||||
// 右侧面板内容更新后关闭 loading
|
||||
setTimeout(() => {
|
||||
rightPanelLoading.value = false;
|
||||
}, 200);
|
||||
};
|
||||
const onSearchFinish = (values: any) => {
|
||||
fishResource.value = {
|
||||
name: '',
|
||||
logoList: [],
|
||||
shapedesc: ''
|
||||
};
|
||||
ininTable(values);
|
||||
};
|
||||
const transformData = (res: any) => {
|
||||
if (res?.data?.data) {
|
||||
if (rightNumber.value === 0) {
|
||||
handleRowClick(res?.data?.data[0]);
|
||||
}
|
||||
rightNumber.value++;
|
||||
}
|
||||
return res?.data || [];
|
||||
};
|
||||
const ininTable = (values: any) => {
|
||||
const params = {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
values.ptype && values.ptype !== 'all'
|
||||
? {
|
||||
field: 'ptype',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.ptype
|
||||
}
|
||||
: null,
|
||||
values.family && values.family !== 'all' && values.family !== '全部'
|
||||
? {
|
||||
field: 'family',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.family
|
||||
}
|
||||
: null,
|
||||
values.name
|
||||
? {
|
||||
field: 'name',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: values.name
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
tableRef.value.getList(params);
|
||||
};
|
||||
const init = () => {
|
||||
ptypeLoading.value = true;
|
||||
familyLoading.value = true;
|
||||
|
||||
getDictItemsByCode({ dictCode: 'FISH_PTYPE' }).then(res => {
|
||||
res.data.unshift({
|
||||
itemCode: 'all',
|
||||
dictName: '全部'
|
||||
});
|
||||
fish_ptype_list.value = res.data;
|
||||
ptypeLoading.value = false;
|
||||
});
|
||||
|
||||
const params = {
|
||||
group: [
|
||||
{
|
||||
field: 'family',
|
||||
dir: 'asc'
|
||||
}
|
||||
]
|
||||
};
|
||||
getFishResourceList(params).then(res => {
|
||||
res?.data?.data.unshift({
|
||||
count: '全部',
|
||||
key: '全部'
|
||||
});
|
||||
familyList.value = res?.data?.data || [];
|
||||
familyLoading.value = false;
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
init();
|
||||
nextTick(() => {
|
||||
updateCarouselWidth();
|
||||
updateTableScrollY();
|
||||
if (rightPanelRef.value) {
|
||||
resizeObserver.observe(rightPanelRef.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.slick-slide) {
|
||||
text-align: center;
|
||||
background: #364d79;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.slick-arrow.custom-slick-arrow) {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
font-size: 25px;
|
||||
color: #fff;
|
||||
background-color: rgba(31, 45, 61, 0.11);
|
||||
transition: ease all 0.3s;
|
||||
opacity: 0.3;
|
||||
z-index: 1;
|
||||
}
|
||||
:deep(.slick-arrow.custom-slick-arrow:before) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.slick-arrow.custom-slick-arrow:hover) {
|
||||
color: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:deep(.slick-slide h3) {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
生态流量运行数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
流量站数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
表层水温数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
垂向水温数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="">
|
||||
水质监测数据
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@ -1,5 +1,43 @@
|
||||
<template>
|
||||
<div class="modelContentDrak">
|
||||
<h2>基础数据</h2>
|
||||
<div class="conventionalHydropower-page">
|
||||
<a-tabs v-model:activeKey="activeTab" class="conventionalHydropower-tabs">
|
||||
<a-tab-pane tab="电站信息" key="BasicData">
|
||||
<BasicData />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="鱼类资源" key="FishResource">
|
||||
<FishResource />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="批复文件" key="Approval">
|
||||
<Approval />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import BasicData from "./components/BasicData/index.vue";
|
||||
import FishResource from "./components/FishResource/index.vue";
|
||||
import Approval from "./components/Approval/index.vue";
|
||||
import { ref } from "vue";
|
||||
const activeTab = ref("BasicData");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conventionalHydropower-page {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.conventionalHydropower-tabs {
|
||||
padding-left: 20px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
:deep(.ant-tabs-content) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>环保数据</h2>
|
||||
<div class="monitorData-page">
|
||||
<a-tabs v-model:activeKey="activeTab" class="monitorData-tabs">
|
||||
<a-tab-pane tab="生态流量运行数据" key="FlowData">
|
||||
<FlowData />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="水质监测数据" key="WaterData">
|
||||
<WaterData />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="垂向水温数据" key="VerticalTemp">
|
||||
<VerticalTemp />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="表层水温数据" key="SurfaceTemp">
|
||||
<SurfaceTemp />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="过鱼监测数据" key="FishData">
|
||||
<FishData />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="鱼类放鱼数据" key="FishRelease">
|
||||
<FishRelease />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="流量站数据" key="FlowStation">
|
||||
<FlowStation />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="综合导出" key="Export">
|
||||
<Export />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import FlowData from './components/FlowData/index.vue';
|
||||
import WaterData from './components/WaterData/index.vue';
|
||||
import VerticalTemp from './components/VerticalTemp/index.vue';
|
||||
import SurfaceTemp from './components/SurfaceTemp/index.vue';
|
||||
import FishData from './components/FishData/index.vue';
|
||||
import FishRelease from './components/FishRelease/index.vue';
|
||||
import FlowStation from './components/FlowStation/index.vue';
|
||||
import Export from './components/Export/index.vue';
|
||||
const activeTab = ref('FlowData');
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.monitorData-page {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monitorData-tabs {
|
||||
padding-left: 20px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
:deep(.ant-tabs-content) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user