This commit is contained in:
root 2026-04-01 15:49:37 +08:00
commit 0a39c7fa4d
150 changed files with 20351 additions and 5218 deletions

View File

@ -14,11 +14,22 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class MybitsPlusConfig { public class MybitsPlusConfig {
// @Bean
// public MybatisPlusInterceptor mybatisPlusInterceptor() {
// MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// return mybatisPlusInterceptor;
// }
/**
* 分页插件配置Oracle 兼容
*/
@Bean @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加分页拦截器指定数据库类型为 Oracle
return mybatisPlusInterceptor; interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.ORACLE));
return interceptor;
} }
} }

View File

@ -90,9 +90,11 @@ public class SysMenuController {
@PostMapping("/permissionAssignment") @PostMapping("/permissionAssignment")
@Operation(summary = "获取分配权限(不含按钮)") @Operation(summary = "获取分配权限(不含按钮)")
@ResponseBody @ResponseBody
public List<Map<String, Object>> permissionAssignment(String roleId) { public List<Map<String, Object>> permissionAssignment(String code, String roleId) {
if (StrUtil.isBlank(code)) {
return sysMenuService.permissionAssignment(roleId); code = "1";
}
return sysMenuService.permissionAssignment(code,roleId);
} }
/********************************** /**********************************

View File

@ -115,6 +115,9 @@ public class SysOrganizationController {
if (StrUtil.isEmpty(sysOrganization.getIsvaild())) { if (StrUtil.isEmpty(sysOrganization.getIsvaild())) {
sysOrganization.setIsvaild("1"); sysOrganization.setIsvaild("1");
} }
if("".equals(sysOrganization.getId())){
sysOrganization.setId(null);
}
//填写 当前用户名称 //填写 当前用户名称
sysOrganization.setLastmodifier(userService.getUsername()); sysOrganization.setLastmodifier(userService.getUsername());
//填写 当前日期 //填写 当前日期

View File

@ -1,6 +1,7 @@
package com.yfd.platform.system.controller; package com.yfd.platform.system.controller;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.yfd.platform.annotation.Log; import com.yfd.platform.annotation.Log;
@ -46,15 +47,8 @@ public class SysRoleController {
@PostMapping("/list") @PostMapping("/list")
@Operation(summary = "查询所有角色") @Operation(summary = "查询所有角色")
@ResponseBody @ResponseBody
public List<SysRole> list(String rolename) { public List<SysRole> list(@RequestParam(required = false) String rolename) {
QueryWrapper<SysRole> queryWrapper = new QueryWrapper<>(); return roleService.selectRoleList(rolename);
if (StrUtil.isNotEmpty(rolename)) {
//根据角色名称模糊查询
queryWrapper.like("rolename", rolename);
}
//根据角色级别角色编号 正序排序
queryWrapper.ne("level", "1").orderByAsc("level", "lastmodifydate");
return roleService.list(queryWrapper);
} }
/*********************************** /***********************************

View File

@ -64,7 +64,7 @@ public class UserController {
public ResponseResult queryUsers(String orgid, public ResponseResult queryUsers(String orgid,
String username, Page<SysUser> page) { String username, Page<SysUser> page) {
Page<Map<String, Object>> mapPage = userService.queryUsers(orgid, Page<SysUser> mapPage = userService.queryUsers(orgid,
username, page); username, page);
return ResponseResult.successData(mapPage); return ResponseResult.successData(mapPage);
} }

View File

@ -41,6 +41,7 @@ public class SysRole implements Serializable {
/** /**
* 1-超级管理员 2-单位管理员 3-普通用户 * 1-超级管理员 2-单位管理员 3-普通用户
*/ */
@TableField("\"LEVEL\"")
private String level; private String level;
/** /**

View File

@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
import java.io.Serializable; import java.io.Serializable;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.List;
/** /**
* <p> * <p>
@ -113,4 +114,7 @@ public class SysUser implements Serializable {
* 备用3 * 备用3
*/ */
private String custom3; private String custom3;
@TableField(exist = false)
List<SysRole> roles;
} }

View File

@ -101,4 +101,12 @@ public interface SysRoleMapper extends BaseMapper<SysRole> {
* 返回值说明角色 ID 列表 * 返回值说明角色 ID 列表
***********************************/ ***********************************/
List<String> getRoleIdsByUserId(String id); List<String> getRoleIdsByUserId(String id);
/**********************************
* 用途说明查询角色列表Oracle 兼容
* 参数说明rolename - 角色名称
* 返回值说明角色列表
***********************************/
List<SysRole> selectRoleList(@Param("rolename") String rolename);
} }

View File

@ -81,7 +81,7 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
************************************/ ************************************/
boolean delInRoleUsersByUserid(@Param("userid") String userid,@Param("roleids")String[] roleids); boolean delInRoleUsersByUserid(@Param("userid") String userid,@Param("roleids")String[] roleids);
Page<Map<String, Object>> queryUsers(String orgid, Page<SysUser> queryUsers(String orgid,
String username, String username,
Page<SysUser> page); Page<SysUser> page);

View File

@ -95,7 +95,7 @@ public interface ISysMenuService extends IService<SysMenu> {
* isdisplay 是否显示 * isdisplay 是否显示
* 返回值说明: 菜单结构树集合 * 返回值说明: 菜单结构树集合
***********************************/ ***********************************/
List<Map<String, Object>> permissionAssignment(String roleId); List<Map<String, Object>> permissionAssignment(String code,String roleId);
String uploadIcon(MultipartFile icon) throws FileNotFoundException; String uploadIcon(MultipartFile icon) throws FileNotFoundException;
} }

View File

@ -63,4 +63,6 @@ public interface ISysRoleService extends IService<SysRole> {
* 返回值说明: 是否分配成功 * 返回值说明: 是否分配成功
***********************************/ ***********************************/
boolean setMenuById(String id, String menuIds); boolean setMenuById(String id, String menuIds);
List<SysRole> selectRoleList(String rolename);
} }

View File

@ -128,7 +128,7 @@ public interface IUserService extends IService<SysUser> {
boolean addUserRoles(String roleid, String userid); boolean addUserRoles(String roleid, String userid);
//Page<SysUser> queryUsers(String orgid, String username, Page<SysUser> page); //Page<SysUser> queryUsers(String orgid, String username, Page<SysUser> page);
Page<Map<String,Object>> queryUsers(String orgid, String username, Page<SysUser> page); Page<SysUser> queryUsers(String orgid, String username, Page<SysUser> page);
/*********************************** /***********************************
* 用途说明根据ID批量删除用户 * 用途说明根据ID批量删除用户

View File

@ -451,18 +451,19 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
* 返回值说明: 菜单结构树集合 * 返回值说明: 菜单结构树集合
***********************************/ ***********************************/
@Override @Override
public List<Map<String, Object>> permissionAssignment(String roleId) { public List<Map<String, Object>> permissionAssignment(String code,String roleId) {
String code = sysMenuMapper.getSystemCodeById(roleId); // String code = sysMenuMapper.getSystemCodeById(roleId);
if (code == null) { // if (code == null) {
code = "1"; // code = "1";
} // }
LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysMenu::getSystemcode, code).select(SysMenu::getId, queryWrapper.eq(SysMenu::getSystemcode, code).select(SysMenu::getId,
SysMenu::getParentid, SysMenu::getName).orderByAsc SysMenu::getParentid, SysMenu::getName).orderByAsc
(SysMenu::getOrderno); (SysMenu::getOrderno);
List<Map<String, Object>> listAll = List<Map<String, Object>> mapList =
sysMenuMapper.selectMaps(queryWrapper); sysMenuMapper.selectMaps(queryWrapper);
List<Map<String, Object>> listAll = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList);
List<String> listRole = List<String> listRole =
sysMenuMapper.selectMenuByRoleId(roleId); sysMenuMapper.selectMenuByRoleId(roleId);
for (Map<String, Object> map : listAll) { for (Map<String, Object> map : listAll) {

View File

@ -13,6 +13,7 @@ import com.yfd.platform.system.mapper.SysOrganizationMapper;
import com.yfd.platform.system.mapper.SysRoleMapper; import com.yfd.platform.system.mapper.SysRoleMapper;
import com.yfd.platform.system.service.ISysOrganizationService; import com.yfd.platform.system.service.ISysOrganizationService;
import com.yfd.platform.system.service.IUserService; import com.yfd.platform.system.service.IUserService;
import com.yfd.platform.utils.ObjectConverterUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -128,12 +129,15 @@ public class SysOrganizationServiceImpl extends ServiceImpl<SysOrganizationMappe
QueryWrapper<SysOrganization> queryWrapper = new QueryWrapper<>(); QueryWrapper<SysOrganization> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parentid", parentid); //根据上级id 查询 queryWrapper.eq("parentid", parentid); //根据上级id 查询
listMap = this.listMaps(queryWrapper.orderByAsc("orgcode")); listMap = this.listMaps(queryWrapper.orderByAsc("orgcode"));
if (!listMap.isEmpty()) { //判断是否存在子集
for (int i = 0; i < listMap.size(); i++) { //遍历表数据 if (!listMap.isEmpty()) {
List<Map<String, Object>> mapList = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysOrganization.class, listMap);//判断是否存在子集
for (int i = 0; i < mapList.size(); i++) { //遍历表数据
List<Map<String, Object>> childList = List<Map<String, Object>> childList =
child(listMap.get(i).get("id").toString()); //循环获取下一子集 child(mapList.get(i).get("id").toString()); //循环获取下一子集
listMap.get(i).put("childList", childList); //添加新列 子集 mapList.get(i).put("childList", childList); //添加新列 子集
} }
return mapList;
} }
return listMap; return listMap;
} }
@ -162,9 +166,11 @@ public class SysOrganizationServiceImpl extends ServiceImpl<SysOrganizationMappe
} }
List<Object> max = this.listObjs(queryWrapper); List<Object> max = this.listObjs(queryWrapper);
//判断查询是否存在 存在转换成 int 类型并给 codeMax 替换值 //判断查询是否存在 存在转换成 int 类型并给 codeMax 替换值
if (max.size() > 0) { if (!max.isEmpty() && max.getFirst() != null) {
codeMax = String maxValue = max.getFirst().toString();
Integer.parseInt(max.get(0).toString().substring(max.get(0).toString().length() - 2)); if (maxValue.length() >= 2) {
codeMax = Integer.parseInt(maxValue.substring(maxValue.length() - 2));
}
} }
//2位数字编号 //2位数字编号
DecimalFormat df = new DecimalFormat("00"); DecimalFormat df = new DecimalFormat("00");

View File

@ -49,8 +49,14 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
QueryWrapper<SysRole> queryWrapper = new QueryWrapper<>(); QueryWrapper<SysRole> queryWrapper = new QueryWrapper<>();
List<Object> max = this.listObjs(queryWrapper.select("MAX(rolecode) " + List<Object> max = this.listObjs(queryWrapper.select("MAX(rolecode) " +
"rolecode"));// 查询最大的编号 "rolecode"));// 查询最大的编号
if (max.size() > 0) { // 存在转换成 int 类型并给 codeMax 替换值
codeMax = Integer.parseInt(max.get(0).toString());//判断查询是否存在 if (!max.isEmpty() && max.getFirst() != null) {
try {
codeMax = Integer.parseInt(max.getFirst().toString());
} catch (NumberFormatException e) {
// 如果转换失败保持默认值 0
e.printStackTrace();
}
} }
// 存在转换成int类型并给codeMax替换值 // 存在转换成int类型并给codeMax替换值
String code = df.format(codeMax + 1); // 最大编号累加 String code = df.format(codeMax + 1); // 最大编号累加
@ -160,4 +166,9 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
return true; return true;
} }
@Override
public List<SysRole> selectRoleList(String rolename) {
return roleMapper.selectRoleList(rolename);
}
} }

View File

@ -27,6 +27,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -224,46 +225,79 @@ public class UserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impleme
*roleids 角色id *roleids 角色id
* 返回值说明: 是否更新成功 * 返回值说明: 是否更新成功
************************************/ ************************************/
@Override // ... existing code ...
public Map updateById(SysUser sysUser, String roleids) {
//返回信息
Map<String, String> result = new HashMap<>();
//获取当前用户 最近修改者替换
sysUser.setLastmodifier(getUsername());
//获取当前时间 最近修改日期替换
sysUser.setLastmodifydate(new Timestamp(System.currentTimeMillis()));
//根据修改
boolean ok = this.updateById(sysUser);
if (ok) {
if (StrUtil.isNotEmpty(roleids)) {
String[] roles = roleids.split(",");
List<String> list = sysUserMapper.getRoleid(sysUser.getId());
for (String role : roles) {
if (!list.contains(role)) {
//系统生成id
String id = IdUtil.fastSimpleUUID();
//新增sys_role_users表数据
ok = ok && sysUserMapper.addUserRoles(id, role,
sysUser.getId());
}
}
//删除不包含的角色
sysUserMapper.delInRoleUsersByUserid(sysUser.getId(), roles);
} else { @Override
//根据用户id 删除该用户角色关联 @Transactional(rollbackFor = Exception.class)
ok = ok && sysUserMapper.delRoleUsersByUserid(sysUser.getId()); public Map updateById(SysUser sysUser, String roleids) {
} Map<String, String> result = new HashMap<>();
result.put("status", "sucess");
result.put("msg", "用户信息修改成功!"); try {
} else { // 设置修改信息
String currentUsername = getUsername();
Timestamp currentTime = new Timestamp(System.currentTimeMillis());
sysUser.setLastmodifier(currentUsername);
sysUser.setLastmodifydate(currentTime);
// 更新用户信息
boolean ok = this.updateById(sysUser);
if (!ok) {
result.put("status", "error"); result.put("status", "error");
result.put("msg", "用户信息修改失败!"); result.put("msg", "用户信息修改失败!");
return result;
}
// 处理角色分配
String userId = sysUser.getId();
if (StrUtil.isNotEmpty(roleids)) {
handleUserRoles(userId, roleids);
} else {
// 清空所有角色
sysUserMapper.delRoleUsersByUserid(userId);
}
result.put("status", "sucess");
result.put("msg", "用户信息修改成功!");
} catch (Exception e) {
log.error("更新用户信息失败", e);
result.put("status", "error");
result.put("msg", "操作失败:" + e.getMessage());
throw e; // 抛出异常触发事务回滚
} }
return result; return result;
} }
/**
* 处理用户角色分配增量更新
* @param userId 用户 ID
* @param roleIds 角色 ID 字符串逗号分隔
*/
private void handleUserRoles(String userId, String roleIds) {
// 获取用户当前角色
List<String> currentRoles = sysUserMapper.getRoleid(userId);
Set<String> currentRoleSet = new HashSet<>(currentRoles != null ? currentRoles : Collections.emptyList());
// 解析新角色列表
String[] newRoles = roleIds.split(",");
Set<String> newRoleSet = new HashSet<>(Arrays.asList(newRoles));
// 需要新增的角色新角色 - 当前角色
for (String roleId : newRoles) {
if (!currentRoleSet.contains(roleId)) {
String id = IdUtil.fastSimpleUUID();
sysUserMapper.addUserRoles(id, roleId, userId);
}
}
// 需要删除的角色当前角色 - 新角色
sysUserMapper.delInRoleUsersByUserid(userId, newRoles);
}
// ... existing code ...
@Override @Override
public Map getOneById(String id) { public Map getOneById(String id) {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>(); QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
@ -476,20 +510,16 @@ public class UserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impleme
}*/ }*/
@Override @Override
public Page<Map<String, Object>> queryUsers(String orgid, public Page<SysUser> queryUsers(String orgid,
String username, String username,
Page<SysUser> page) { Page<SysUser> page) {
Page<Map<String, Object>> mapPage = sysUserMapper.queryUsers(orgid, Page<SysUser> mapPage = sysUserMapper.queryUsers(orgid,
username, page); username, page);
List<Map<String, Object>> list = new ArrayList<>(); ;mapPage.getRecords().forEach(record -> {
List<Map<String, Object>> records = mapPage.getRecords(); String id = record.getId();
for (Map<String, Object> record : records) {
String id = (String) record.get("id");
List<SysRole> sysRoles = sysRoleMapper.getRoleByUserId(id); List<SysRole> sysRoles = sysRoleMapper.getRoleByUserId(id);
record.put("roles", sysRoles); record.setRoles(sysRoles);
list.add(record); });
}
mapPage.setRecords(list);
return mapPage; return mapPage;
} }

View File

@ -33,7 +33,7 @@
DISTINCT DISTINCT
m.id, m.id,
m.parentid, m.parentid,
m.`name`, m.name,
m.opturl, m.opturl,
m.icon, m.icon,
m.orderno, m.orderno,

View File

@ -91,7 +91,15 @@
<!--根据用户id获取角色信息--> <!--根据用户id获取角色信息-->
<select id="getRoleByUserId" <select id="getRoleByUserId"
resultType="com.yfd.platform.system.domain.SysRole"> resultType="com.yfd.platform.system.domain.SysRole">
SELECT r.id,r.rolename,r.orgscope FROM sys_role_users ru INNER JOIN sys_role r ON ru.roleid =r.id WHERE ru.userid =#{id} SELECT
r.id,
r.rolename,
r.orgscope
FROM
sys_role_users ru
INNER JOIN sys_role r ON ru.roleid = r.id
WHERE
ru.userid =#{id}
</select> </select>
<!--根据角色id获取用户id--> <!--根据角色id获取用户id-->
@ -111,6 +119,20 @@
FROM sys_role_users FROM sys_role_users
WHERE userid = #{id} WHERE userid = #{id}
</select> </select>
<!--查询角色列表Oracle 兼容版)-->
<select id="selectRoleList" resultType="com.yfd.platform.system.domain.SysRole">
SELECT r.id, r.rolecode, r.rolename, r."LEVEL", r.description,
r.orgscope, r.optscope, r.busscope, r.isvaild,
r.lastmodifier, r.lastmodifydate, r.custom1, r.custom2, r.custom3
FROM sys_role r
<where>
<if test="rolename != null and rolename != ''">
AND r.rolename LIKE '%' || #{rolename} || '%'
</if>
AND r."LEVEL" != '1'
</where>
ORDER BY r."LEVEL" ASC, lastmodifydate ASC
</select>
<!--根据 角色id和用户id 删除系统角色用户对照 admin除外--> <!--根据 角色id和用户id 删除系统角色用户对照 admin除外-->
<delete id="deleteRoleUsers"> <delete id="deleteRoleUsers">
delete from sys_role_users where userid !=(select u.id from sys_user u where u.account="admin") and roleid=#{roleid} and userid=#{urserid} delete from sys_role_users where userid !=(select u.id from sys_user u where u.account="admin") and roleid=#{roleid} and userid=#{urserid}

View File

@ -29,7 +29,7 @@
<!--用户分配角色 系统角色用户对照新增数据--> <!--用户分配角色 系统角色用户对照新增数据-->
<insert id="addUserRoles"> <insert id="addUserRoles">
insert into sys_role_users value (#{id},#{roleid},#{userid}) insert into sys_role_users values (#{id},#{roleid},#{userid})
</insert> </insert>
<!--根据用户id 和角色id 查询 系统角色用户对照表--> <!--根据用户id 和角色id 查询 系统角色用户对照表-->
@ -49,9 +49,11 @@
<!--根据用户表 id 查询角色表最大级别--> <!--根据用户表 id 查询角色表最大级别-->
<select id="getMaxLevel" resultType="String"> <select id="getMaxLevel" resultType="String">
select min(level) from sys_role where id in (select roleid from sys_role_users where userid=#{userid}) SELECT MIN(r."LEVEL")
FROM sys_role r
WHERE r.id IN ( SELECT roleid FROM sys_role_users WHERE userid = #{userId} )
</select> </select>
<select id="queryUsers" resultType="java.util.Map"> <select id="queryUsers" resultType="com.yfd.platform.system.domain.SysUser">
SELECT DISTINCT SELECT DISTINCT
u.id, u.id,
u.usertype, u.usertype,
@ -62,7 +64,7 @@
u.phone, u.phone,
u.avatar, u.avatar,
u.orgid, u.orgid,
u.`status`, u.status,
u.lastmodifier, u.lastmodifier,
u.lastmodifydate u.lastmodifydate
FROM FROM
@ -73,7 +75,7 @@
and u.orgid = #{orgid} and u.orgid = #{orgid}
</if> </if>
<if test="username != null"> <if test="username != null">
and u.username like concat('%', #{username},'%') and u.username LIKE '%' || #{username} || '%'
</if> </if>
ORDER BY u.lastmodifydate DESC ORDER BY u.lastmodifydate DESC
</select> </select>

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="NewFrameWork2023-WEB" /> <meta name="description" content="NewFrameWork2023-WEB" />
<meta name="keywords" content="NewFrameWork2023-WEB" /> <meta name="keywords" content="NewFrameWork2023-WEB" />

View File

@ -10,13 +10,16 @@
"prettier": "prettier --write ." "prettier": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@vueuse/core": "^9.1.1", "@vueuse/core": "^9.1.1",
"@wangeditor/editor": "^5.0.0", "@wangeditor/editor": "^5.0.0",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
"ant-design-vue": "^4.2.6",
"axios": "^1.2.0", "axios": "^1.2.0",
"better-scroll": "^2.4.2", "better-scroll": "^2.4.2",
"dayjs": "^1.11.20",
"default-passive-events": "^2.0.0", "default-passive-events": "^2.0.0",
"echarts": "^5.2.2", "echarts": "^5.2.2",
"element-plus": "^2.2.27", "element-plus": "^2.2.27",
@ -63,6 +66,5 @@
}, },
"repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git",
"author": "有来开源组织", "author": "有来开源组织",
"license": "MIT", "license": "MIT"
"__npminstall_done": false
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElConfigProvider } from 'element-plus'; import { ElConfigProvider } from 'element-plus';
import { useAppStore } from '@/store/modules/app'; import { useAppStore,usetTheme } from '@/store/modules/app';
const appStore = useAppStore(); const appStore = useAppStore();
</script> </script>
<template> <template>
<el-config-provider :locale="appStore.locale" :size="appStore.size"> <el-config-provider :locale="appStore.locale" :size="appStore.size">
<a-config-provider :theme="usetTheme">
<router-view /> <router-view />
</a-config-provider>
</el-config-provider> </el-config-provider>
</template> </template>

View File

@ -1,28 +0,0 @@
import request from '@/utils/request';
export function getMessageList(params:any) {
return request({
url: '/system/message/getMessageList',
method: 'get',
params
});
}
export function setMessageStatus(data:any) {
return request({
url: '/system/message/setMessageStatus?id=' + data,
method: 'post'
});
}
export function setAllMessageStatus() {
return request({
url: '/system/message/setAllMessageStatus',
method: 'post'
});
}
export function deleteMessageById(data:any) {
return request({
url: '/system/message/deleteMessageById?id=' + data,
method: 'post',
data
});
}

View File

@ -1,51 +0,0 @@
import request from '@/utils/request';
//获取表格内容
export function getTaskList(params: any) {
return request({
url: '/system/quartzjob/getQuartzJobList',
method: 'get',
params: params
});
}
//新增表格内容
export function addTaskList(params: any) {
return request({
url: '/system/quartzjob/addQuartzJob',
method: 'post',
data: params
});
}
//删除定时任务
export function delTaskList(params: any) {
return request({
url: '/system/quartzjob/deleteQuartzJob',
method: 'post',
params: params
});
}
//修改定时任务
export function updataTaskList(params: any) {
return request({
url: '/system/quartzjob/updateQuartzJob',
method: 'post',
data: params
});
}
//定时任务是否有效
export function setTaskList(params: any) {
return request({
url: '/system/quartzjob/setQuartzStatus',
method: 'post',
params: params
});
}
//拖拽
export function changeItemOrder(params: any) {
return request({
url: '/system/quartzjob/changeDictOrder',
method: 'post',
params: params
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

View File

@ -0,0 +1 @@
<svg width="16" height="88" xmlns="http://www.w3.org/2000/svg"><path d="M16 0a22.397 22.397 0 0 1-8.71 12.793l-2.265 1.618A12 12 0 0 0 0 24.175v39.65a12 12 0 0 0 5.025 9.764l2.265 1.618c4.2 3 7.23 7.356 8.588 12.325L16 88V0Z" fill="#E5EDF3" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 268 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1 @@
<svg width="106" height="51" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-32.8%" y="-233.3%" width="165.6%" height="566.7%" filterUnits="objectBoundingBox" id="a"><feGaussianBlur stdDeviation="7" in="SourceGraphic"/></filter><filter x="-23.7%" y="-289.3%" width="147.4%" height="678.6%" filterUnits="objectBoundingBox" id="b"><feGaussianBlur stdDeviation="3" in="SourceGraphic"/></filter><filter x="-4.7%" y="-300%" width="109.4%" height="700%" filterUnits="objectBoundingBox" id="d"><feGaussianBlur stdDeviation="1" in="SourceGraphic"/></filter><linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="c"><stop stop-color="#37FDF7" stop-opacity="0" offset="0%"/><stop stop-color="#37FDF7" offset="51.347%"/><stop stop-color="#37FDF7" stop-opacity="0" offset="100%"/></linearGradient></defs><g transform="translate(21 21)" fill="none" fill-rule="evenodd"><ellipse fill="#36FCF6" filter="url(#a)" cx="32" cy="4.5" rx="32" ry="4.5"/><ellipse fill="#11FFF2" filter="url(#b)" cx="32" cy="2" rx="19" ry="1.556"/><path fill="url(#c)" filter="url(#d)" d="M0 1h64v1H0z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,107 @@
<template>
<div class="baselayer-switcher">
<div
class="switcher-item"
v-for="item in data"
:key="item.name"
:class="{ active: item.name === activeLayer }"
@click="activeLayer = item.name"
>
<img :src="item.img" alt="" />
<div class="label">{{ item.name }}</div>
</div>
<div class="nineSectionsImg">
<img :src="nineSectionsImg" alt="" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import mapShiliangtu from "@/assets/images/map-shiliangtu.png";
import mapDixingtu from "@/assets/images/map-dixingtu.png";
import mapYingxiangtu from "@/assets/images/map-yingxiangtu.png";
import nineSectionsShiliang from "@/assets/images/nineSections-shiliang.png";
import nineSectionsDixing from "@/assets/images/nineSections-dixing.png";
import nineSectionsYingxiang from "@/assets/images/nineSections-yingxiang.png";
const data = ref([
{ name: "矢量", img: mapShiliangtu },
{ name: "地形", img: mapDixingtu },
{ name: "影像", img: mapYingxiangtu },
]);
const nineSectionsImg = ref(nineSectionsShiliang);
const nineSectionsData = ref([
{ name: "矢量", img: nineSectionsShiliang },
{ name: "地形", img: nineSectionsDixing },
{ name: "影像", img: nineSectionsYingxiang },
]);
const activeLayer = ref("矢量");
watch(activeLayer, (val) => {
nineSectionsImg.value =
nineSectionsData.value.find((item) => item.name === val)?.img || "";
});
</script>
<style lang="scss" scoped>
.baselayer-switcher {
display: flex;
position: absolute;
bottom: 20px;
right: 480px;
z-index: 200;
.switcher-item {
background: #d8d8d8;
border-radius: 2px;
width: 85px;
height: 60px;
display: none;
position: relative;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
.label {
position: absolute;
bottom: 0;
right: 0;
padding: 2px 3px;
background-color: #00000059;
color: #fff;
border-radius: 2px 0 0 2px / 2px 0px 0px 2px;
}
&:hover {
border: 1px solid #3a7098;
.label {
background-color: #005293;
}
}
}
.switcher-item:not(:first-child) {
margin-left: 4px;
}
.active {
display: block;
border: 1px solid #3a7098;
.label {
background-color: #005293;
}
}
&:hover {
.switcher-item {
display: block;
}
}
.nineSectionsImg {
position: absolute;
right: 56px;
bottom: 92px;
border: 1px solid grey;
pointer-events: none;
img {
max-width: 100px;
width: 100px !important;
}
}
}
</style>

View File

@ -1,105 +0,0 @@
<template>
<el-breadcrumb
separator-class="el-icon-arrow-right"
class="h-[50px] flex items-center"
>
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
<span
v-if="
item.redirect === 'noredirect' || index === breadcrumbs.length - 1
"
class="text-[#97a8be]"
>{{ generateTitle(item.meta.title) }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ generateTitle(item.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { onBeforeMount, ref, watch } from 'vue';
import { useRoute, RouteLocationMatched } from 'vue-router';
import { compile } from 'path-to-regexp';
import router from '@/router';
import { generateTitle } from '@/utils/i18n';
const currentRoute = useRoute();
const pathCompile = (path: string) => {
const { params } = currentRoute;
const toPath = compile(path);
return toPath(params);
};
const breadcrumbs = ref([] as Array<RouteLocationMatched>);
function getBreadcrumb() {
let matched = currentRoute.matched.filter(
item => item.meta && item.meta.title
);
const first = matched[0];
if (!isDashboard(first)) {
matched = [
{ path: '/dashboard', meta: { title: 'dashboard' } } as any
].concat(matched);
}
breadcrumbs.value = matched.filter(item => {
return item.meta && item.meta.title && item.meta.breadcrumb !== false;
});
}
function isDashboard(route: RouteLocationMatched) {
const name = route && route.name;
if (!name) {
return false;
}
return (
name.toString().trim().toLocaleLowerCase() ===
'Dashboard'.toLocaleLowerCase()
);
}
function handleLink(item: any) {
const { redirect, path } = item;
if (redirect) {
router.push(redirect).catch(err => {
console.warn(err);
});
return;
}
router.push(pathCompile(path)).catch(err => {
console.warn(err);
});
}
watch(
() => currentRoute.path,
path => {
if (path.startsWith('/redirect/')) {
return;
}
getBreadcrumb();
}
);
onBeforeMount(() => {
getBreadcrumb();
});
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@ -1,59 +0,0 @@
<template>
<a
href="https://github.com/hxrui"
target="_blank"
class="github-corner"
aria-label="View source on Github"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill: #40c9c6; color: #fff"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
class="octo-arm"
/>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
/>
</svg>
</a>
</template>
<style scoped>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
</style>

View File

@ -1,41 +0,0 @@
<template>
<div
@click="toggleClick"
class="px-[15px] hover:bg-gray-50 cursor-pointer h-[50px] leading-[50px] text-center fixed bottom-2 "
style="z-index:1005;display: flex;display: -webkit-flex; justify-content: center; align-items: center; -webkit-justify-content: center; -webkit-align-items: center;"
:style="appStore.sidebar.opened?'left:80px': 'left:2px'"
>
<img v-if="isActive" src="@/assets/MenuIcon/dh_sq.png" alt="">
<img v-else src="@/assets/MenuIcon/dh_sq1.png" alt="">
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
defineProps({
isActive: {
required: true,
type: Boolean,
default: false
}
});
const emit = defineEmits(['toggleClick']);
function toggleClick() {
emit('toggleClick');
}
</script>
<style scoped>
.hamburger {
width: 20px;
height: 20px;
fill: #409eff;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -0,0 +1,109 @@
<!-- SidePanelItem.vue -->
<template>
<div class="rightContentDrawer">
<div @click="handleToggle" class="drawerController1" v-if="!drawerOpen">
<img src="../../assets/components/arrow-left.png" alt="">
</div>
<!-- 使用 Vue Transition 组件控制动画 -->
<transition name="drawer-slide">
<a-drawer
:get-container="false"
:style="{ position: 'relative' }"
v-model:open="drawerOpen"
:mask="false"
placement="right"
width="450"
:closable="false"
:headerStyle="{ color: '#FAFCFE' }">
<div @click="handleToggle" class="drawerController">
<img src="../../assets/components/arrow-right.png" alt="">
</div>
<div style="padding:16px 8px 0;" class="text_she">
<slot />
</div>
</a-drawer>
</transition>
</div>
</template>
<script setup>
import { ref, defineOptions } from 'vue';
// (便)
defineOptions({
name: 'rightDrawer'
});
const drawerOpen = ref(true);
const handleToggle = () => {
drawerOpen.value = !drawerOpen.value;
};
</script>
<style lang="scss">
.rightContentDrawer {
height: 100%;
position: relative;
box-sizing: border-box;
.drawerController1 {
width: 18px;
position: absolute;
right: -2px;
cursor: pointer;
z-index: 10;
height: 88px;
line-height: 88px;
top: 45%;
vertical-align: middle;
background-image: url(../../assets/components/bg-toggle.e1dabcf3.svg);
background-repeat: no-repeat;
display: flex;
justify-content: center;
align-items: center;
}
}
.ant-drawer {
margin: 5px 0px;
.ant-drawer-content{
overflow:visible;
}
.ant-drawer-content-wrapper {
border: 2px solid #c5d6e2 !important;
box-shadow: 3px 3px 3px 9px #e5edf3 !important;
}
.ant-drawer-body {
padding: 0px !important;
}
}
.drawerController {
width: 18px;
position: absolute;
right: 10px;
cursor: pointer;
z-index: 10;
height: 88px;
line-height: 88px;
top: 45%;
left: -18px;
vertical-align: middle;
background-image: url(../../assets/components/bg-toggle.e1dabcf3.svg);
background-repeat: no-repeat;
display: flex;
justify-content: center;
align-items: center;
}
.text_she{
font-size: 14px;
color:#262626;
font-variant: tabular-nums;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
</style>

View File

@ -3,14 +3,11 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { addClass, removeClass } from '@/utils/index'; import { addClass, removeClass } from '@/utils/index';
// import { useSettingsStore } from '@/store/modules/settings';
// //
import { Close, Setting } from '@element-plus/icons-vue'; import { Close, Setting } from '@element-plus/icons-vue';
import { ElColorPicker } from 'element-plus'; import { ElColorPicker } from 'element-plus';
// const settingsStore = useSettingsStore();
const show = ref(false); const show = ref(false);
defineProps({ defineProps({
@ -32,21 +29,6 @@ watch(show, value => {
}); });
function addEventClick() { function addEventClick() {
window.addEventListener('click', closeSidebar, { passive: true });
}
function closeSidebar(evt: any) {
//
let parent = evt.target.closest('.theme-picker-dropdown');
if (parent) {
return;
}
parent = evt.target.closest('.right-panel');
if (!parent) {
show.value = false;
window.removeEventListener('click', closeSidebar);
}
} }
const rightPanel = ref(ElColorPicker); const rightPanel = ref(ElColorPicker);
@ -92,7 +74,6 @@ onBeforeUnmount(() => {
.showRightPanel { .showRightPanel {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: calc(100% - 15px);
} }
</style> </style>

View File

@ -0,0 +1,264 @@
<!-- SidePanelItem.vue -->
<template>
<div class="qgc-side-pannel-item">
<div class="qgc_title">
<div class="title_left">
<span>{{ title }}</span>
<span v-if="prompt.show" class="title_icon">
<a-tooltip placement="top" :title="prompt.value" :get-popup-container="getPopupContainer">
<QuestionCircleOutlined />
</a-tooltip>
</span>
<span v-if="clickprompt.show" class="title_icon">
<a-tooltip placement="top" trigger="click" :title="clickprompt.value"
:get-popup-container="getPopupContainer">
<InfoCircleOutlined />
</a-tooltip>
</span>
</div>
<div class="title_right">
<div v-if="select.show">
<a-select v-model:value="selectValue" show-search placeholder="请选择" style="width: 142px"
:options="select.options" :filter-option="filterOption" @focus="handleFocus" @blur="handleBlur"
@change="handleChange"></a-select>
</div>
<div v-if="shrink" class="title_shrink" @click="isExpand = !isExpand">
<img v-if="isExpand" src="@/assets/components/arrow-up.png" alt="">
<img v-else src="@/assets/components/arrow-down.png" alt="">
</div>
<div v-if="moreSelect.show">
<a-tree-select v-model:value="moreSelectValue" show-search style="width: 170px"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" placeholder="Please select"
allow-clear tree-default-expand-all :tree-data="moreSelect.options"
tree-node-filter-prop="label">
</a-tree-select>
</div>
<div v-if="datetimePicker.show">
<!-- 添加 locale 属性来设置语言 -->
<a-date-picker v-model:value="datetimeValue" show-time
:format="datetimePicker.format !== null ? datetimePicker.format : undefined"
:picker="datetimePicker.picker" placeholder="请选择时间" style="width: 180px"
@change="handleDateTimeChange" :locale="locale" />
<!-- 修改为 locale 变量 -->
</div>
<div v-if="scopeDate.show">
<a-range-picker v-model:value="scopeDateValue" :locale="locale" :picker="scopeDate.picker"
:format="'YYYY-MM-DD'" :range-separator="' 至 '" style="width: 200px" />
</div>
</div>
</div>
<div class="body">
<slot v-if="isExpand" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import {
QuestionCircleOutlined,
InfoCircleOutlined
} from '@ant-design/icons-vue';
import type { SelectProps } from 'ant-design-vue';
//
import zhCN from 'ant-design-vue/es/locale/zh_CN';
// dayjs
import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn'; //
dayjs.locale('zh-cn'); //
console.log(dayjs().format('MMMM'));
const locale = ref(zhCN);
//
interface PromptConfig {
show: boolean;
value: string;
}
interface SelectConfig {
picker: any;
format: any;
show: boolean;
value: string | undefined;
options: SelectProps['options'];
}
// 便
defineOptions({
name: 'SidePanelItem'
});
// props
const props = defineProps({
title: { //
type: String,
default: ''
},
shrink: { //
type: Boolean,
default: false
},
prompt: { //
type: Object as () => PromptConfig,
default: () => ({
show: false,
value: '',
})
},
clickprompt: { //
type: Object as () => PromptConfig,
default: () => ({
show: false,
value: '',
})
},
select: { //
type: Object as () => SelectConfig,
default: () => ({
show: false,
value: undefined,
options: []
})
},
moreSelect: {//
type: Object as () => SelectConfig,
default: () => ({
show: false,
value: undefined,
options: []
})
},
datetimePicker: { //
type: Object as () => SelectConfig,
default: () => ({
show: false,
value: undefined,
format: null, //YYYY-MM-DD HH
picker: 'date' //date | week | month | quarter | year
})
},
scopeDate: { //
type: Object as () => SelectConfig,
default: () => ({
show: false,
value: undefined,
picker: 'month' //date | week | month | quarter | year
})
},
});
const isExpand = ref(true);
const selectValue = ref(props.select.value)
const moreSelectValue = ref(props.select.value)
const datetimeValue = ref<Dayjs | null>(props.datetimePicker.value ? dayjs(props.datetimePicker.value) : null);
const scopeDateValue = ref(props.scopeDate.value);
// // locale
// const locale = zhCN;
// console.log(locale, "zhCN");
//
const handleChange = (value: string) => {
console.log(`selected ${value}`);
};
const handleBlur = () => {
console.log('blur');
};
const handleFocus = () => {
console.log('focus');
};
const filterOption = (input: string, option?: { value: string }) => {
if (!option) return false;
return option.value.toLowerCase().includes(input.toLowerCase());
};
//
const getPopupContainer = (trigger: HTMLElement) => {
return trigger.parentElement;
};
//
const handleDateTimeChange = (date: any | null, dateString: string) => {
console.log('Selected DateTime:', date, dateString);
};
//
onMounted(() => {
console.log(props.select);
});
</script>
<style lang="scss">
.qgc-side-pannel-item {
width: 100%;
display: flex;
justify-content: space-between;
flex-direction: column;
.qgc_title {
width: 100%;
background-color: #e5edf3;
border-radius: 2px;
font-size: 16px;
color: #2f6b98;
line-height: 36px;
padding-left: 16px;
padding-right: 8px;
display: flex;
justify-content: space-between;
position: relative;
font-weight: 500;
.title_shrink {
cursor: pointer;
}
.title_left {
display: flex;
align-items: center;
.title_icon {
display: inline-block;
margin-left: 5px;
cursor: pointer;
}
}
.title_right {
display: flex;
align-items: center;
}
}
.qgc_title:before {
position: absolute;
content: "";
display: inline-block;
left: 0;
width: 2px;
background-color: #005293;
top: 2px;
height: 32px;
border-radius: 3px;
}
.body {
width: 100%;
font-size: 14px;
line-height: 22px;
padding: 16px 0 0;
margin-bottom: 16px;
text-overflow: ellipsis;
overflow: hidden;
height: calc(100% - 36px);
box-sizing: border-box;
p {
text-indent: 2em;
}
}
}
</style>

View File

@ -16,8 +16,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import { useSettingsStore } from '@/store/modules/settings';
// const settingsStore = useSettingsStore();
</script> </script>
<style> <style>

View File

@ -0,0 +1,141 @@
<!-- SidePanelItem.vue -->
<template>
<div class="basic_body">
<div ref="chartContainer" class="chart-container"></div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
// 便
defineOptions({
name: 'developStatusChart'
});
const chartContainer = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
//
onMounted(() => {
if (chartContainer.value) {
initChart();
}
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
const initChart = () => {
if (!chartContainer.value) return;
chartInstance = echarts.init(chartContainer.value);
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
// position: function (point, params, dom, rect, size) {
// //
// return [size.viewSize.width - 120, point[1]];
// }
},
legend: {
bottom: '5%',
right: '5%',
orient: 'vertical', //
data: ['已建', '在建'],
itemWidth: 14,
itemHeight: 14,
itemStyle: {
borderRadius: 0
},
textStyle: {
fontSize: 11
},
itemGap: 12 //
},
series: [
{
name: '建设状态',
type: 'pie',
radius: ['80%', '95%'], //
center: ['35%', '50%'], //
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 0,
borderColor: '#fff',
borderWidth: 1
},
label: {
show: false //
},
emphasis: {
label: {
show: false
}
},
labelLine: {
show: false // 线
},
data: [
{ value: 80, name: '已建', itemStyle: { color: '#4CAF50' } },
{ value: 20, name: '在建', itemStyle: { color: '#2196F3' } }
]
},
{
name: '中心圆',
type: 'pie',
radius: ['0%', '55%'], //
center: ['35%', '50%'], //
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 0,
borderColor: '#fff',
borderWidth: 0
},
label: {
show: false
},
emphasis: {
label: {
show: false
}
},
labelLine: {
show: false
},
data: [
{ value: 80, name: '已建', itemStyle: { color: '#4CAF50' } },
{ value: 20, name: '在建', itemStyle: { color: '#2196F3' } }
]
}
]
};
chartInstance.setOption(option);
//
window.addEventListener('resize', () => {
chartInstance?.resize();
});
};
</script>
<style lang="scss">
.basic_body {
width: 100%;
height: 100%;
.chart-container {
width: 185px; //
height: 100px; //
}
}
</style>

View File

@ -0,0 +1,90 @@
<!-- engEnvironmentData/index.vue -->
<template>
<div class="eng-environment-data">
<div class="title">监测数据接入情况</div>
<div class="data-list">
<div class="data-item" v-for="item in dataList" :key="item.label">
<div class="item-content">
<span class="color-bar" :style="{ backgroundColor: item.color }"></span>
<span class="label">{{ item.label }}</span>
<span class="value" style=" color: #2f6b98 ">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
//
defineOptions({
name: 'EngEnvironmentData'
});
//
const dataList = ref([
{
label: '大中型已建在建电站',
value: '707',
color: '#00b894'
},
{
label: '已接入电站运行数据',
value: '452',
color: '#0984e3'
},
{
label: '已开展全过程监测工作',
value: '42',
color: '#6c5ce7'
}
]);
//
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.eng-environment-data {
padding: 0px 5px;
background: #fff;
border-radius: 4px;
width: 100%;
.title {
font-size: 14px;
font-weight: 500;
color: #333;
text-align: center;
}
.data-list {
.data-item {
border: 1px solid #edf2f8;
margin-bottom: 3px;
cursor: pointer;
.item-content {
display: flex;
align-items: center;
padding: 6px 4px;
.color-bar {
width: 1.6px;
height: 14px;
margin-right: 6px;
border-radius: 2px;
}
.label {
flex: 1;
}
}
}
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="gis-view">
<div id="mapContainer" />
<!-- 地图图例 -->
<!-- tabType="{baseType[0]?.tagType}"
legendData="{legendData}"
legendDataMap="{legendDataMap}"
setLegendDataMap="{updateLegendDataMap}"
dvtpType="{dvtpType}"
mapList="{mapList}"
loading="{loading}"
pointData="{pointData}" -->
<MapLegend />
<!-- 地图筛选器 -->
<MapFilter />
<!-- 地图控制器 -->
<MapController />
<!-- 基础图层切换器 -->
<BaseLayerSwitcher />
<!-- <MapFilter inverse={true} searchList={mapSearchList} pointData={pointData} fish={fish}
wqElementsList={wqElementsList} className={'map-filter'} initialValues={{ timeRange: searchTimeRange, dvtp: dvtpType, year: yearTime }}
getFormRef={(ref: any) => (mapFilterFormRef.current = ref)}
fetchPointData={fetchPointData} /> -->
</div>
</template>
<script setup lang="ts">
import MapLegend from "@/components/mapLegend/index.vue";
import MapFilter from "@/components/mapFilter/index.vue";
import MapController from "@/components/mapController/index.vue";
import BaseLayerSwitcher from "@/components/baseLayerSwitcher/index.vue";
</script>
<style lang="scss" scoped>
.gis-view {
width: 100%;
height: 100%;
position: absolute;
visibility: visible;
}
#mapContainer {
width: 100%;
height: 100%;
position: relative;
background-color: red;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<div class="map-controller">
<div class="map-controller-group">
<div class="map-controller-item" v-for="item in controllers" :key="item.key">
<a-tooltip :title="item.name" placement="left">
<i class="icon iconfont" :class="'icon-' + item.icon"></i>
</a-tooltip>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const isFullScreen = ref(false);
const mapType = ref("2D");
const controllers = ref([
{
name: "全屏",
key: "fullScreen",
icon: isFullScreen.value ? "exitFullScreen" : "fullScreen",
},
{
name: "定位",
key: "positioning",
icon: "iconGlobal",
},
{
name: "放大",
key: "zoomIn",
icon: "zoomIn",
},
{
name: "缩小",
key: "zoomOut",
icon: "zoomOut",
},
{
name: "3D",
key: "dim",
icon: mapType.value === "2D" ? "a-3D" : "a-2D",
},
{
name: "图层",
//
key: "layerController",
icon: "layer"
},
{
name: "下载",
key: "screenShot",
icon: "downLoad",
},
{
name: "梯级",
key: "TJ",
icon: "tiji",
},
{
name: "倾斜摄影",
key: "OSBGController",
icon: "obliquePhotography",
},
{
name: "三维漫游",
key: "threedRoam",
icon: "roaming",
},
]);
</script>
<style lang="scss" scoped>
.map-controller {
position: absolute;
right: 480px;
bottom: 114px;
z-index: 10;
.map-controller-group {
box-shadow: 0 1px 2px #00000026;
background-color: #fff;
border: none;
.map-controller-item {
height: 40px;
width: 40px;
color: #000;
line-height: 40px;
text-align: center;
position: relative;
cursor: pointer;
.iconfont {
font-size: 20px;
}
&:hover {
background-color: #005292;
color: #ffffff;
}
}
.map-controller-group:not(:first-child) {
margin-top: 10px;
}
}
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<div class="map-filter-container">
<div class="toolbar">
<a-form :model="formModel" :rules="rules" ref="formRef">
<a-row :gutter="10">
<a-col>
<a-form-item label="" name="siteRangePicker">
<a-select
v-model:value="formModel.siteRangePicker"
placeholder="装机容量"
style="width: 120px"
>
<a-select-option v-for="item in siteRangePicker" :key="item.value">{{
item.label
}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col>
<a-form-item label="" name="siteRangePicker">
<a-select
v-model:value="formModel.siteRangePicker"
placeholder="请输入关键字检索"
style="width: 200px"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const siteRangePicker = [
{ label: "0-10", value: "0-10" },
{ label: "10-20", value: "10-20" },
{ label: "20-30", value: "20-30" },
{ label: "30-40", value: "30-40" },
{ label: "40-50", value: "40-50" },
{ label: "50-60", value: "50-60" },
];
const formModel = ref({
siteRangePicker: [],
});
const rules = ref({
siteRangePicker: [{ required: true, message: "请选择装机容量" }],
});
const formRef = ref<any>(null);
</script>
<style lang="scss" scoped>
.map-filter-container {
position: absolute;
left: 220px;
top: 15px;
z-index: 99;
padding: 6px 0px;
background: #e5edf3;
border: none;
:deep(.ant-form-item) {
margin-bottom: 0;
}
:deep(.ant-row) {
margin: 0 !important;
}
.toolbar {
width: 100%;
display: flex;
flex-flow: row;
overflow: visible;
}
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<div class="mapLegendView">
<div class="legendTitle">
图例
<span class="legendBtn" @click="isOpen = !isOpen">
<i class="icon iconfont" :class="isOpen ? 'icon-fold' : 'icon-unFold'"></i>
</span>
</div>
<div class="legendContent" v-show="isOpen">
<a-spin :spinning="data === 0">
<div class="legendGroup" v-for="i in data">
<div class="groupTitle">工程</div>
<div class="groupContent">
<div class="legendItem" v-for="j in 10" :key="j">
<div class="legendIcon smallIcon"></div>
<div class="legendIconTitle">
大型水电站-已建
<!-- <span
><br />
2.装机容量(万kW)
</span> -->
</div>
</div>
</div>
</div>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const data = ref(0);
onMounted(() => {
setTimeout(() => {
data.value = 10;
}, 1000);
console.log(data.value);
});
const isOpen = ref(true);
</script>
<style lang="scss" scoped>
.mapLegendView {
position: absolute;
left: 0;
bottom: 0;
min-width: 72px;
margin: 24px 0 16px 16px;
border: 1px solid #ccdae7;
padding: 6px;
z-index: 9;
background-color: #ffffff;
}
.legendTitle {
font-size: 16px;
font-weight: 700;
display: flex;
justify-content: space-between;
cursor: pointer;
.legendBtn {
cursor: pointer;
}
}
.legendContent {
white-space: nowrap;
max-width: 450px;
max-height: 392px;
overflow-x: scroll;
overflow-y: scroll;
border-top: 1px solid #eeeeee;
overflow-y: hidden;
// &::-webkit-scrollbar {
// width: 0;
// }
.legendGroup {
display: inline-block;
vertical-align: top;
padding: 0 4px;
.groupTitle {
text-align: left;
font-weight: 700;
font-size: 14px;
cursor: pointer;
}
.groupContent {
font-size: 14px;
padding: 10px 0;
margin-bottom: 0;
.gray {
filter: grayscale(100%);
color: #00000073;
}
.legendItem {
cursor: pointer;
margin-right: 10px;
padding: 4px 0;
min-height: 30px;
display: flex;
flex-direction: row;
.legendIcon {
width: 22px !important;
height: 22px !important;
background-color: red;
}
.legendIconTitle {
cursor: pointer;
flex: 1 1;
}
}
}
}
}
</style>

View File

@ -6,11 +6,11 @@ export default {
}, },
// 登录页面国际化 // 登录页面国际化
login: { login: {
title: '公司开发平台框架', title: '水电水利建设项目全过程环境管理信息平台',
username: '用户名', username: '用户名',
rulesUsername: '请输入用户名', rulesUsername: '用户账号/身份证号/手机号 不能为空',
password: '密码', password: '密码',
rulesPassword: '请输入密码', rulesPassword: '密码 不能为空',
rulesPasswordPlace: '密码不能少于6位', rulesPasswordPlace: '密码不能少于6位',
login: '登 录', login: '登 录',
code: '请输入验证码', code: '请输入验证码',

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue" import { computed } from "vue";
import { useTagsViewStore } from '@/store/modules/tagsView'; import { useTagsViewStore } from "@/store/modules/tagsView";
import { useRoute, } from 'vue-router'; import { useRoute } from "vue-router";
import GisView from "@/components/gis/GisView.vue";
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
@ -11,6 +12,8 @@ const routeKey = computed(() => router.path + Math.random());
<template> <template>
<section class="app-main"> <section class="app-main">
<GisView />
<div class="gi-panels">
<router-view v-slot="{ Component, route }" :key="routeKey"> <router-view v-slot="{ Component, route }" :key="routeKey">
<transition name="router-fade" mode="out-in"> <transition name="router-fade" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews"> <keep-alive :include="tagsViewStore.cachedViews">
@ -18,32 +21,24 @@ const routeKey = computed(() => router.path + Math.random());
</keep-alive> </keep-alive>
</transition> </transition>
</router-view> </router-view>
</div>
</section> </section>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "@/styles/variables.module.scss" as *;
.app-main { .app-main {
min-height: calc(100vh - 114px); min-height: calc(100vh - $layout-header-height - $locationbar-height);
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background-color: #f0f2f5; background-color: #ffffff;
padding: 0 16px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
.gi-panels {
.fixed-header + .app-main { position: absolute;
padding-top: 50px; width: 100%;
} height: 100%;
z-index: 1;
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 114px);
}
.fixed-header + .app-main {
padding-top: 84px;
}
} }
</style> </style>

View File

@ -1,46 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'; import { ref, onMounted, onBeforeUnmount } from "vue";
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from "vue-router";
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from "element-plus";
import { getToken } from '@/utils/auth'; import { getToken } from "@/utils/auth";
import { UserOutlined, LogoutOutlined } from "@ant-design/icons-vue";
import Screenfull from '@/components/Screenfull/index.vue'; //
import SizeSelect from '@/components/SizeSelect/index.vue'; import { useI18n } from "vue-i18n";
import News from './news.vue'; const { t } = useI18n();
// import LangSelect from '@/components/LangSelect/index.vue'; // import LangSelect from '@/components/LangSelect/index.vue';
import MixNav from './Sidebar/MixNav.vue'; import Sidebar from "./Sidebar/index.vue";
// import { CaretBottom } from '@element-plus/icons-vue';
import Logo from './Sidebar/Logo.vue';
import { useAppStore } from '@/store/modules/app'; import { useTagsViewStore } from "@/store/modules/tagsView";
import { useTagsViewStore } from '@/store/modules/tagsView'; import { useUserStore } from "@/store/modules/user";
import { useUserStore } from '@/store/modules/user'; import Cookies from "js-cookie";
import { useSettingsStore } from '@/store/modules/settings'; import { storeToRefs } from "pinia";
import Cookies from 'js-cookie';
import { storeToRefs } from 'pinia';
const url = import.meta.env.VITE_APP_BASE_API; const url = import.meta.env.VITE_APP_BASE_API;
const username = Cookies.get('username'); const username = Cookies.get("username");
const settingsStore = useSettingsStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const isCollapse = computed(() => !appStore.sidebar.opened);
const appStore = useAppStore();
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
const userStore = useUserStore(); const userStore = useUserStore();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const querystr = ref('');
const dialogVisible = ref(false)
const device = computed(() => appStore.device);
const news = ref()
function logout() { function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', { ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: '确定', confirmButtonText: "确定",
cancelButtonText: '取消', cancelButtonText: "取消",
type: 'warning' type: "warning",
}).then(() => { }).then(() => {
userStore userStore
.logout() .logout()
@ -52,112 +39,126 @@ function logout() {
}); });
}); });
} }
function querystrChange() { const badgeval = ref(0);
} const isbadge = ref(true);
const badgeval = ref(0) var source = new EventSource(url + `/sse/connect/` + getToken());
const isbadge = ref(true)
var source = new EventSource(url+ `/sse/connect/` + getToken(),);
onMounted(() => { onMounted(() => {
if ("EventSource" in window) { if ("EventSource" in window) {
source.onmessage = function (e) { source.onmessage = function (e) {
if (e.data > 0) { if (e.data > 0) {
badgeval.value = e.data badgeval.value = e.data;
isbadge.value = false isbadge.value = false;
news.value.init()
} else { } else {
isbadge.value = true isbadge.value = true;
} }
}; };
source.onopen = function(e) { source.onopen = function (e) {};
};
source.onerror = function (e: any) { source.onerror = function (e: any) {
if (e.readyState == EventSource.CLOSED) { if (e.readyState == EventSource.CLOSED) {
} else { } else {
} }
}; };
} else { } else {
}; }
}) });
onBeforeUnmount(() => { onBeforeUnmount(() => {
source.close(); source.close();
}) });
</script> </script>
<template> <template>
<div class="navbar"> <div class="navbar">
<logo v-if="sidebarLogo" :collapse="isCollapse" /> <a-layout-header class="header">
<!-- <mix-nav v-if="device !== 'mobile' && settingsStore.layout === 'mix'" /> --> <transition class="bg-white-800">
<div v-if="settingsStore.layout === 'left'" class="flex justify-start"> <a
<div class="flex justify-center items-center"> href="/"
<!--全屏 --> class="h-[50px] min-w-[350px] flex items-center justify-center text-white"
<div v-if="device === 'desktop'" style="position: relative;"> >
<el-input class="keywords" v-model="querystr" placeholder="请输入搜索关键字" clearable <h1 class="text-blank font-bold fontSize-16">{{ t("login.title") }}</h1></a
@change="querystrChange"></el-input> >
<img src="@/assets/MenuIcon/top_ss.png" alt="" style="position: absolute;right: 30px;top: 10px;"> </transition>
</div> <Sidebar />
<div class="flex items-center cursor-pointer" style="margin-right:15px;margin-top: 6px;">
<el-badge :value="badgeval" :hidden="isbadge" :max="10">
<el-icon style="font-size: 22px;" @click="dialogVisible = true,news.init()"><Bell /></el-icon>
</el-badge>
</div>
<!-- 布局大小 -->
<el-tooltip content="字号大小" effect="dark" placement="bottom">
<size-select />
</el-tooltip>
<!-- 全屏 -->
<el-tooltip content="全屏缩放" effect="dark" placement="bottom">
<screenfull id="screenfull" />
</el-tooltip>
<!--语言选择-->
<!-- <lang-select /> -->
</div>
<!-- 头像 -->
<div style="heigth:100%;display: flex;display: -webkit-flex;align-items: center; -webkit-align-items: center; ">
<div class="heighta"></div>
<img v-if="userStore.avatar!== ''" :src="url + '/avatar/' + userStore.avatar + '?imageView2/1/w/80/h/80'"
class="w-[30px] h-[30px] rounded-lg rounded-full" style="border-radius: 50%;" />
<img v-else src="@/assets/MenuIcon/top_tx.png" alt="" class="w-[30px] h-[30px] rounded-lg rounded-full" style="border-radius: 50%;">
</div>
<el-tooltip content="个人中心" effect="dark" placement="bottom"> <a-dropdown :trigger="['click']" placement="bottomRight">
<div class="cursor-pointer flex justify-center items-center pl-[10px]" <a-space class="username">
:style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }" @click="$router.push('/personalCenter')">{{ <div>
username }}</div> <span class="icon">
</el-tooltip> <UserOutlined />
</span>
<div class="flex justify-center items-center cursor-pointer pr-[12px]" <span class="text">{{ username }}</span>
:style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }" @click="logout"><el-divider
direction="vertical" style="padding-right:5px" />{{ $t('navbar.logout') }}</div>
<News ref="news" v-model:dialog-visible="dialogVisible" />
</div> </div>
</a-space>
<template #overlay>
<a-menu>
<a-menu-item key="changePassword">
<UserOutlined />
修改密码
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="logout">
<LogoutOutlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-layout-header>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "@/styles/variables.module.scss" as *;
.navbar { .navbar {
width: 100%; width: 100%;
position: relative; position: relative;
height: 60px; height: 110px;
display: -webkit-flex; display: -webkit-flex;
z-index: 98;
.header {
width: 100%;
display: flex;
align-items: center;
background-color: #005293;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
height: $layout-header-height;
position: sticky;
top: 0;
z-index: 100;
padding-left: 0;
padding-right: 10px;
}
.username {
height: $layout-header-height;
line-height: $layout-header-height;
display: flex;
cursor: pointer;
div {
display: flex; display: flex;
align-items: center; align-items: center;
-webkit-align-items: center;
justify-content: space-between;
-webkit-justify-content: space-between;
box-shadow: 0px 0px 10px rgb(219 225 236);
z-index: 98;
} }
.keywords { .icon {
width: 240px; width: 32px;
height: 34px; height: 32px;
margin-right: 20px; line-height: 32px;
text-align: center;
color: $main-menu-color;
border-radius: 50%;
display: inline-block;
font-size: 18px;
background-color: rgba(255, 255, 255, 0.2);
}
:deep(.el-input__wrapper) { .text {
padding-left: 15px; color: $main-menu-color;
border-radius: 40px; padding-left: 10px;
margin-top: -5px;
} }
} }
}
.heighta { .heighta {
border-left: 1px solid #dcdfe6; border-left: 1px solid #dcdfe6;
height: 1em; height: 1em;

View File

@ -1,198 +0,0 @@
<script setup lang="ts">
import { Sunny, Moon } from '@element-plus/icons-vue';
import { useSettingsStore } from '@/store/modules/settings';
import { useDark, useToggle } from '@vueuse/core';
import { ElDivider, ElSwitch, ElTooltip } from 'element-plus';
import { onMounted } from 'vue';
const settingsStore = useSettingsStore();
const isDark = useDark();
function toggleTheme() {
const isDark = useDark();
useToggle(isDark);
}
onMounted(() => {
window.document.body.setAttribute('layout', settingsStore.layout);
});
function changeLayout(layout: string) {
settingsStore.changeSetting({ key: 'layout', value: layout });
window.document.body.setAttribute('layout', settingsStore.layout);
}
</script>
<template>
<div class="settings-container">
<h3 class="text-base font-bold">项目配置</h3>
<el-divider />
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</div>
<el-divider>主题</el-divider>
<div class="flex justify-center" @click.stop>
<el-switch
v-model="isDark"
inline-prompt
@change="toggleTheme"
:active-icon="Sunny"
:inactive-icon="Moon"
/>
</div>
<el-divider>导航栏布局</el-divider>
<ul class="layout">
<el-tooltip content="左侧模式" placement="bottom">
<li
:class="
'layout-item layout-left ' +
(settingsStore.layout == 'left' ? 'is-active' : '')
"
@click="changeLayout('left')"
>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip content="顶部模式" placement="bottom">
<li
:class="
'layout-item layout-top ' +
(settingsStore.layout == 'top' ? 'is-active' : '')
"
@click="changeLayout('top')"
>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip content="混合模式" placement="bottom">
<li
:class="
'layout-item layout-mix ' +
(settingsStore.layout == 'mix' ? 'is-active' : '')
"
@click="changeLayout('mix')"
>
<div />
<div />
</li>
</el-tooltip>
</ul>
</div>
</template>
<style lang="scss" scoped>
.settings-container {
padding: 16px;
font-size: 14px;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right;
}
.layout {
display: flex;
display: -webkit-flex;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
height: 50px;
padding: 0;
&-item {
width: 18%;
height: 45px;
background: #f0f2f5;
position: relative;
overflow: hidden;
cursor: pointer;
border-radius: 4px;
}
&-item.is-active {
border: 2px solid var(--el-color-primary);
}
&-left {
div {
&:nth-child(1) {
width: 30%;
height: 100%;
background: #1b2a47;
}
&:nth-child(2) {
width: 70%;
height: 30%;
top: 0;
right: 0;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
&-top {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
}
}
&-mix {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
&:nth-child(2) {
width: 30%;
height: 70%;
bottom: 0;
left: 0;
background: #1b2a47;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
}
}
</style>

View File

@ -1,37 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { isExternal } from '@/utils/validate';
import { useRouter } from 'vue-router';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const sidebar = computed(() => appStore.sidebar);
const device = computed(() => appStore.device);
const props = defineProps({
to: {
type: String,
required: true
}
});
const router = useRouter();
function push() {
if (device.value === 'mobile' && sidebar.value.opened == true) {
appStore.closeSideBar(false);
}
router.push(props.to).catch(err => {
console.error(err);
});
}
</script>
<template>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot />
</a>
<div v-else @click="push">
<slot />
</div>
</template>

View File

@ -1,33 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useAppStore } from '@/store/modules/app';
//
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const appStore = useAppStore();
defineProps({
collapse: {
type: Boolean,
required: true
}
});
const logo = ref<string>(
new URL(`../../../assets/logo.png`, import.meta.url).href
);
</script>
<template>
<transition class="ml-5 bg-white-800">
<router-link
key="collapse"
class="h-[50px] flex items-center justify-center"
to="/dashboard"
>
<img :src="logo" class="w-5 h-5" />
<h1 class="ml-3 text-blank font-bold" :style="{fontSize: appStore.size === 'default' ? '20px' : '22px'}">{{t('login.title')}}</h1>
</router-link>
</transition>
</template>

View File

@ -1,159 +0,0 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import {
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElMenu,
ElMessageBox,
ElTooltip
} from 'element-plus';
import Screenfull from '@/components/Screenfull/index.vue';
import SizeSelect from '@/components/SizeSelect/index.vue';
import LangSelect from '@/components/LangSelect/index.vue';
import { CaretBottom } from '@element-plus/icons-vue';
import SidebarItem from './SidebarItem.vue';
import variables from '@/styles/variables.module.scss';
import { useTagsViewStore } from '@/store/modules/tagsView';
import { useUserStore } from '@/store/modules/user';
import { usePermissionStore } from '@/store/modules/permission';
const tagsViewStore = useTagsViewStore();
const userStore = useUserStore();
const permissionStore = usePermissionStore();
const route = useRoute();
const router = useRouter();
const routes = [] as any[];
onMounted(() => {
permissionStore.routes.forEach(item => {
const { ...newItem } = item;
routes.push(newItem);
});
});
const activeMenu = computed<string>(() => {
const { meta, path } = route;
if (meta?.activeMenu) {
return meta.activeMenu as string;
}
return path;
});
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<template>
<div class="horizontal-header">
<el-menu
class="horizontal-header-menu"
:default-active="activeMenu"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
mode="horizontal"
>
<sidebar-item
v-for="route in routes"
:item="route"
:key="route.path"
:base-path="route.path"
/>
</el-menu>
<div class="horizontal-header-right">
<!--全屏 -->
<screenfull id="screenfull" />
<!-- 布局大小 -->
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select />
</el-tooltip>
123
<!--语言选择-->
<lang-select />
<el-dropdown trigger="click">
<div class="flex justify-center items-center pr-[20px]">
<img
:src="userStore.avatar + '?imageView2/1/w/80/h/80'"
class="w-[40px] h-[40px] rounded-lg"
/>
<CaretBottom class="w-3 h-3" />
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/">
<el-dropdown-item>{{ $t('navbar.dashboard') }}</el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/hxrui">
<el-dropdown-item>Github</el-dropdown-item>
</a>
<a target="_blank" href="https://gitee.com/haoxr">
<el-dropdown-item>{{ $t('navbar.gitee') }}</el-dropdown-item>
</a>
<a target="_blank" href="https://www.cnblogs.com/haoxianrui/">
<el-dropdown-item>{{ $t('navbar.document') }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{ $t('navbar.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<style lang="scss" scoped>
@use '@/styles/variables.module' as variables;
.horizontal-header {
display: flex;
display: -webkit-flex;
width: 100%;
align-items: center;
-webkit-align-items: center;
justify-content: space-around;
-webkit-justify-content: space-around;
background: #001529;
&-menu {
height: 100%;
width: 100%;
border: none;
background-color: transparent;
}
&-right {
display: flex;
display: -webkit-flex;
min-width: 340px;
align-items: center;
-webkit-align-items: center;
justify-content: flex-end;
-webkit-justify-content: flex-end;
color: #fff;
}
}
</style>

View File

@ -1,125 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { isExternal } from "@/utils/validate";
import AppLink from "./Link.vue";
import { generateTitle } from "@/utils/i18n";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const url = import.meta.env.VITE_APP_BASE_API;
defineProps({
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
required: false,
},
// basePath: {
// type: String,
// required: true,
// },
});
const onlyOneChild = ref();
function hasOneShowingChild(children = [] as any, parent: any) {
if (!children) {
children = [];
}
const showingChildren = children.filter((item: any) => {
if (item.meta && item.meta.hidden) {
return false;
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item;
return true;
}
});
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true;
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
function resolvePath(routePath: any) {
// if (isExternal(props.basePath)) {
// return props.basePath;
// }
if (routePath.islink == 0) {
if (isExternal(routePath.opturl)) {
return '';
}
}
return routePath.opturl;
}
onMounted(() => {
generateTitle
})
</script>
<template>
<div v-if="!item.meta || !item.meta.hidden || item.isdisplay == '1'">
<template v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
(!item.meta || !item.meta.alwaysShow)
">
<app-link v-if="onlyOneChild.meta || item.isdisplay == '1'" :to="resolvePath(onlyOneChild)">
<el-menu-item :index="resolvePath(onlyOneChild)" :class="{ 'submenu-title-noDropdown': !isNest }"
style="height: 44px;">
<img v-if="onlyOneChild.icon && onlyOneChild.name == '首页'" src="@/assets/MenuIcon/dh_sy.png"
style="margin-right:10px;width: 16px">
<img v-if="onlyOneChild.icon && onlyOneChild.name !== '首页'" :src="url + '/menu/' + item.icon"
style="margin-right:10px;width: 16px">
<template #title>
<span :style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }">{{ generateTitle(onlyOneChild.name)
}}</span>
</template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else :index="resolvePath(item)" telested>
<template #title>
<div style="height: 44px; display: flex;display: -webkit-flex; align-items: center;-webkit-align-items: center;justify-content: space-around;">
<img v-if="item.icon" :icon-class="item.icon" :src="url + '/menu/' + item.icon" alt=""
style="margin-right:10px;width: 16px; " class="management">
<span :style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }" style="display: block;" >{{
generateTitle(item.name) }}</span>
</div>
<!-- <svg-icon v-if="item.icon" :icon-class="item.icon" :style="{fontSize: appStore.size === 'default' ? '14px' : '16px'}" /> -->
</template>
<sidebar-item v-for="child in item.children" :key="child.opturl" :item="child" :is-nest="true"
:base-path="resolvePath(child)" />
</el-sub-menu>
</div>
</template>
<style scoped>
:deep(.el-sub-menu__title) {
height: 44px !important;
}
:deep(.el-icon) {
font-size: 16px;
}
:deep(.el-tooltip__trigger span) {
display: none !important;
}
:deep(.el-tooltip__trigger .management) {
margin-left:20px !important ;
}
:deep(.el-menu-tooltip__trigger){
width: 105% !important;
}
</style>

View File

@ -1,47 +1,173 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { ref, onBeforeMount, onMounted } from "vue";
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from "vue-router";
import SidebarItem from './SidebarItem.vue'; import { usePermissionStore } from "@/store/modules/permission";
import variables from '@/styles/variables.module.scss';
import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission';
import { useAppStore } from '@/store/modules/app';
import { storeToRefs } from 'pinia';
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const menus: any = ref([]); //
const subMenus: any = ref([]); //
const activeKey = ref(""); //
const subActiveKey = ref(""); //
const appStore = useAppStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const route = useRoute(); const route = useRoute();
const router = useRouter();
const isCollapse = computed(() => !appStore.sidebar.opened); //
const handleTabChange = (key: string) => {
const activeMenu = computed<string>(() => { let data = menus.value.find((item: any) => item.id === key);
const { meta, path } = route; subMenus.value = data.children;
if (meta?.activeMenu) { activeKey.value = key;
return meta.activeMenu as string; if (data.children.length > 0) {
subActiveKey.value = data.children[0].id;
let subData = subMenus.value.find((item: any) => item.id === subActiveKey.value);
router.push(subData.path || subData.opturl);
} else {
subActiveKey.value = "";
} }
return path; };
}); //
const handleSubTabChange = (key: string) => {
subActiveKey.value = key;
let data = subMenus.value.find((item: any) => item.id === key);
router.push(data.path || data.opturl);
};
onBeforeMount(() => {
console.log(route)
console.log(route)
console.log(permissionStore.routes)
permissionStore.routes.map((item: any) => {
if (item.meta?.hidden) {
return "";
} else {
if (item?.parentid == null) {
menus.value.push(item?.children?.[0]);
} else {
menus.value.push(item);
}
if (item.children) {
item.children.map((child: any) => {
if (child.path === route.path) {
activeKey.value = item.id;
subMenus.value = item.children;
subActiveKey.value = child.id;
}
});
}
}
});
});
onMounted(() => {});
</script> </script>
<template> <template>
<div :class="{ 'has-logo': sidebarLogo }"> <a-tabs
<el-scrollbar> class="sidebar-container"
<el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg" v-model:activeKey="activeKey"
:text-color="variables.menuText" :active-text-color="variables.menuActiveText" :unique-opened="false" @change="handleTabChange"
:collapse-transition="false" mode="vertical" > size="small"
<sidebar-item v-for="route in permissionStore.routes" :item="route" :key="route.opturl" >
:is-collapse="isCollapse" /> <a-tab-pane
</el-menu> v-for="route in menus"
</el-scrollbar> :key="route.id"
:tab="route.meta.title"
></a-tab-pane>
</a-tabs>
<div class="sub-menus">
<a-tabs v-model:activeKey="subActiveKey" @change="handleSubTabChange" size="small">
<a-tab-pane
v-for="route in subMenus"
:key="route.id"
:tab="route.meta.title"
></a-tab-pane>
</a-tabs>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/variables.module' as variables; @use "@/styles/variables.module.scss" as *;
.sidebar-container {
flex: 1;
overflow-y: hidden !important;
margin: 0 30px;
&::-webkit-scrollbar {
display: none;
}
:deep(.ant-tabs-nav) {
height: $layout-header-height;
margin-bottom: 0;
background-color: transparent;
&::-webkit-scrollbar {
display: none;
}
.ant-tabs-nav-operations {
color: $main-menu-color;
}
}
:deep(.ant-tabs-tab) {
padding: 0 20px;
margin: 0;
border: none;
color: $main-menu-color;
}
:deep(.ant-tabs-tab-active) {
color: $main-menu-color-active !important;
background: $main-menu-bg-color-active;
.ant-tabs-tab-btn {
color: $main-menu-color-active !important;
}
}
:deep(.ant-tabs-tab:hover) {
color: $main-menu-color-active;
}
:deep(.ant-tabs-ink-bar) {
visibility: visible;
border: none;
background: url("@/assets/icons/menuActiveBg.svg") no-repeat center 30px;
height: 51px;
}
}
.sub-menus {
position: absolute;
left: 0;
top: $layout-header-height;
height: $locationbar-height;
width: 100%;
background-color: #f8f8f8;
z-index: 100;
padding-left: 12px;
:deep(.ant-tabs-nav) {
margin-bottom: 0;
.ant-tabs-tab {
padding: 12px 0;
}
.ant-tabs-ink-bar {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAxMS8wMy8yMrqNQAoAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOklEQVQImWNgQAchkx3QhZhQeEFTExj+MexnCJoyH1mYEUUBwz8kScYFDOtyEhGKMBSgKmTErQChEAA6FRM7O0rIOgAAAABJRU5ErkJggg==")
no-repeat center bottom;
border-bottom: 2px solid #005293;
height: 7px;
}
.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #2f6b98 !important;
}
}
}
}
</style> </style>

View File

@ -1,130 +0,0 @@
<script setup lang="ts">
import {
ref,
computed,
onMounted,
onBeforeUnmount,
getCurrentInstance
} from 'vue';
import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance() as any;
const emits = defineEmits(['scroll']);
const emitScroll = () => {
emits('scroll');
};
const tagsViewStore = useTagsViewStore();
const scrollWrapper = computed(
() => proxy?.$refs.scrollContainer.$refs.wrapRef
);
onMounted(() => {
scrollWrapper.value.addEventListener('scroll', emitScroll, true);
});
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener('scroll', emitScroll);
});
function handleScroll(e: WheelEvent) {
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
scrollWrapper.value.scrollLeft =
scrollWrapper.value.scrollLeft + eventDelta / 4;
}
function moveToTarget(currentTag: TagView) {
const $container = proxy.$refs.scrollContainer.$el;
const $containerWidth = $container.offsetWidth;
const $scrollWrapper = scrollWrapper.value;
let firstTag = null;
let lastTag = null;
// find first tag and last tag
if (tagsViewStore.visitedViews.length > 0) {
firstTag = tagsViewStore.visitedViews[0];
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0;
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
} else {
const tagListDom = document.getElementsByClassName('tags-item');
const currentIndex = tagsViewStore.visitedViews.findIndex(
item => item === currentTag
);
let prevTag = null;
let nextTag = null;
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (
(tagListDom[k] as any).dataset.path ===
tagsViewStore.visitedViews[currentIndex - 1].path
) {
prevTag = tagListDom[k];
}
if (
(tagListDom[k] as any).dataset.path ===
tagsViewStore.visitedViews[currentIndex + 1].path
) {
nextTag = tagListDom[k];
}
}
}
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft =
(nextTag as any).offsetLeft +
(nextTag as any).offsetWidth +
tagAndTagSpacing.value;
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft =
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
}
}
}
defineExpose({
moveToTarget
});
</script>
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<style lang="scss" scoped>
.scroll-container {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
}
</style>

View File

@ -1,356 +0,0 @@
<script setup lang="ts">
import {
getCurrentInstance,
nextTick,
ref,
watch,
onMounted,
ComponentInternalInstance
} from 'vue';
import path from 'path-browserify';
import { useRoute, useRouter } from 'vue-router';
import { useAppStore } from '@/store/modules/app';
import ScrollPane from './ScrollPane.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { generateTitle } from '@/utils/i18n';
import { usePermissionStore } from '@/store/modules/permission';
import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
const route = useRoute();
const visible = ref(false);
const selectedTag = ref({});
const scrollPaneRef = ref();
const left = ref(0);
const top = ref(0);
const affixTags = ref<TagView[]>([]);
watch(
route,
() => {
addTags();
moveToCurrentTag();
},
{
//
immediate: true
}
);
watch(visible, value => {
if (value) {
document.body.addEventListener('click', closeMenu);
} else {
document.body.removeEventListener('click', closeMenu);
}
});
function filterAffixTags(routes: any[], basePath = '/') {
let tags: TagView[] = [];
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path);
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
});
}
if (route.children) {
const childTags = filterAffixTags(route.children, route.path);
if (childTags.length >= 1) {
tags = tags.concat(childTags);
}
}
});
return tags;
}
function initTags() {
const tags = filterAffixTags(permissionStore.routes);
affixTags.value = tags;
for (const tag of tags) {
// Must have tag name
if ((tag as TagView).name) {
tagsViewStore.addVisitedView(tag);
}
}
}
function addTags() {
if (route.name) {
tagsViewStore.addView(route);
}
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of tagsViewStore.visitedViews) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
// when query is different then update
if (r.fullPath !== route.fullPath) {
tagsViewStore.updateVisitedView(route);
}
}
}
});
}
function isActive(tag: TagView) {
return tag.path === route.path;
}
function isAffix(tag: TagView) {
return tag.meta && tag.meta.affix;
}
function isFirstView() {
try {
return (
(selectedTag.value as TagView).fullPath ===
tagsViewStore.visitedViews[1].fullPath ||
(selectedTag.value as TagView).fullPath === '/index'
);
} catch (err) {
return false;
}
}
function isLastView() {
try {
return (
(selectedTag.value as TagView).fullPath ===
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
);
} catch (err) {
return false;
}
}
function refreshSelectedTag(view: TagView) {
tagsViewStore.delCachedView(view);
const { fullPath } = view;
nextTick(() => {
router.replace({ path: '/redirect' + fullPath }).catch(err => {
console.warn(err);
});
});
}
function toLastView(visitedViews: TagView[], view?: any) {
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath });
} else {
router.push('/');
}
}
}
function closeSelectedTag(view: TagView) {
tagsViewStore.delView(view).then((res: any) => {
if (isActive(view)) {
toLastView(res.visitedViews, view);
}
});
}
function closeLeftTags() {
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews);
}
});
}
function closeRightTags() {
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews);
}
});
}
function closeOtherTags() {
router.push(selectedTag.value);
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
moveToCurrentTag();
});
}
function closeAllTags(view: TagView) {
tagsViewStore.delAllViews().then((res: any) => {
toLastView(res.visitedViews, view);
});
}
function openMenu(tag: TagView, e: any) {
const menuMinWidth = 105;
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
const offsetWidth = proxy?.$el.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const l = e.clientX - offsetLeft + 15; // 15: margin right
if (l > maxLeft) {
left.value = maxLeft;
} else {
left.value = l;
}
top.value = e.layerY + 10;
visible.value = true;
selectedTag.value = tag;
}
function closeMenu() {
visible.value = false;
}
function handleScroll() {
closeMenu();
}
onMounted(() => {
initTags();
});
</script>
<template>
<div class="w-full h-[54px] py-[10px]" style="background:#f0f2f5">
<scroll-pane ref="scrollPaneRef" class="tags-container" @scroll="handleScroll">
<router-link v-for="tag in tagsViewStore.visitedViews" :key="tag.path" :data-path="tag.path"
:class="isActive(tag) ? 'active' : ''" :to="{ path: tag.path, query: tag.query }" class="tags-item"
:style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @contextmenu.prevent="openMenu(tag, $event)">
{{ generateTitle(tag.name) }}
<span v-if="!isAffix(tag)" class="tags-item-remove" @click.prevent.stop="closeSelectedTag(tag)" >
<svg-icon icon-class="close" />
</span>
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="tags-item-menu">
<li @click="refreshSelectedTag(selectedTag)">
<svg-icon icon-class="refresh" />
刷新
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<svg-icon icon-class="close" />
关闭
</li>
<!-- <li @click="closeOtherTags">
<svg-icon icon-class="close_other" />
关闭其它
</li> -->
<li v-if="!isFirstView()" @click="closeLeftTags">
<svg-icon icon-class="close_left" />
关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<svg-icon icon-class="close_right" />
关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<svg-icon icon-class="close_all" />
关闭所有
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.tags-container {
.tags-item {
display: inline-block;
cursor: pointer;
border: 1px solid #d8dce5;
background: #ffffff;
padding: 4px 10px;
margin: 0 0 0 5px;
border-radius: 2px;
box-shadow: 0px 0px 10px rgb(219 225 236);
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&:hover {
color: var(--el-color-primary);
}
&.active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
&-remove {
width: 12px;
height: 12px;
line-height: 10px;
// text-align: center;
display: inline-block;
border-radius: 50%;
&:hover {
color: #fff;
background-color: #ee7b7b;
}
}
}
:deep(.el-scrollbar__view) {
height: 100%;
display: flex;
display: -webkit-flex;
align-items: center;
-webkit-align-items: center;
}
}
.tags-item-menu {
background: #fff;
z-index: 99;
position: absolute;
border-radius: 4px;
font-size: 12px;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
padding: 8px 15px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
</style>

View File

@ -1,4 +1,2 @@
export { default as Navbar } from './Navbar.vue'; export { default as Navbar } from './Navbar.vue';
export { default as AppMain } from './AppMain.vue'; export { default as AppMain } from './AppMain.vue';
export { default as Settings } from './Settings/index.vue';
export { default as TagsView } from './TagsView/index.vue';

View File

@ -1,257 +0,0 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { getMessageList, setMessageStatus, setAllMessageStatus, deleteMessageById } from '@/api/message/index'
import Page from '@/components/Pagination/page.vue'
const emit = defineEmits(['update:dialogVisible'])
const props = defineProps({
dialogVisible: {
type: Boolean
},
});
const active = ref('0')
const loading = ref(false)
const tableData = ref([])
const tableDataSel = ref([])
const isLookOver = ref(false)
const info = ref({
title: '',
createtime: '',
content: ''
})
const queryParams = ref({
current: 1,
size: 10,
title: '',
type: '',
status: active.value,
startDate: '',
endDate: ''
});
const createtime = ref('')
const total = ref()
const handleClose = () => {
emit(
'update:dialogVisible',
false
);
}
function handleSelectionChange(val: any) {
tableDataSel.value = val;
}
function reset() {
queryParams.value.title = ''
queryParams.value.type = ''
queryParams.value.startDate = ''
queryParams.value.endDate = ''
createtime.value = ''
init()
}
function init() {
loading.value = true
queryParams.value.status = active.value
if(createtime.value) {
queryParams.value.startDate = createtime.value[0]
queryParams.value.endDate = createtime.value[1]
}
if(Number(active.value) == 0) {
queryParams.value.title = ''
queryParams.value.type = ''
createtime.value = ''
}
getMessageList(queryParams.value).then((result) => {
loading.value = false
tableData.value = result.data.records
total.value = result.data.total
queryParams.value.size = result.data.size;
queryParams.value.current = result.data.current
}).catch(() => {
loading.value = false
});
}
function open(row: any) {
const id = [row.id]
setMessageStatus(id.join(',')).then(() => {
init()
})
isLookOver.value = true
info.value = JSON.parse(JSON.stringify(row))
}
function del(row:any) {
ElMessageBox.confirm(
'确定删除此消息吗',
'删除提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then( async () => {
const id = [] as any[]
if(row.id == null) {
tableDataSel.value.forEach((element:any) => {
id.push(element.id)
});
} else {
id.push(row.id)
}
deleteMessageById(id.join(',')).then(() => {
ElMessage({
message: "删除成功",
type: "success",
});
init()
})
})
}
function tabClick() {
ElMessageBox.confirm(
'确定将全部消息标记为已阅吗?',
'提示信息',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then( async () => {
setAllMessageStatus().then(() => {
ElMessage({
message: "已读成功",
type: "success",
});
init()
})
})
}
defineExpose({
init
});
onMounted(() => {
})
</script>
<template>
<el-dialog class="dialog" draggable v-model="props.dialogVisible" width="1100" top="3vh" append-to-body
:before-close="handleClose" :show-close="!isLookOver">
<el-tabs v-show="!isLookOver" v-model="active" @tab-change="init" type="border-card">
<el-tab-pane label="最新消息">
</el-tab-pane>
<el-tab-pane label="历史消息">
<div class="flex justify-between mb-4">
<div class="flex items-center ">
<el-input v-model="queryParams.title" placeholder="请输入消息标题" clearable style="width: 200px;margin-right:10px" @keyup.enter="init" />
<el-select v-model="queryParams.type" clearable default-first-option placeholder="消息类型" style="width: 200px;margin-right:10px" @change="init">
<el-option label="定时任务" value="1" />
<el-option label="工作流触发" value="2" />
<el-option label="人工任务" value="3" />
</el-select>
<el-date-picker
v-model="createtime"
type="daterange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
@change="init"
/>
<el-button type="primary" style="margin-left: 10px;" @click="init">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<el-button :disabled="tableDataSel.length==0" type="primary" @click="del">
<span>删除</span>
</el-button>
</div>
</el-tab-pane>
<el-table v-loading="loading" :data="tableData" style="width: 100%; margin-bottom: 20px;overflow:hidden;" height="400px"
row-key="id" border @selection-change="handleSelectionChange" default-expand-all
:header-cell-style="{ background: 'rgb(250 250 250)', height: '50px' }">
<el-table-column v-if="active == '1'" type="selection" align="center" width="50" />
<el-table-column type="index" label="序号" align="center" width="70" />
<el-table-column prop="title" label="消息标题">
<template #default="scope">
<span class="title" @click="open(scope.row)">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="type" label="消息类型" width="100">
<template #default="scope">
{{ scope.row.type == '1' ? '定时任务' : (scope.row.type == '2' ? '工作流触发' : '人工触发') }}
</template>
</el-table-column>
<el-table-column prop="senderName" label="发送者" width="120"></el-table-column>
<el-table-column prop="createtime" label="通知时间" width="180"></el-table-column>
<el-table-column label="状态" width="180">
<template #default="scope">
<span :style="{color:scope.row.status == '2'?'rgb(55, 196, 122)':scope.row.status == '9'?'rgb(255, 153, 0)':''}">{{ scope.row.status != '1'?'●':''}} {{ scope.row.status == '1' ? '初始创建' : (scope.row.status == '2' ? '消息已阅' : '消息过期') }}</span>
</template>
</el-table-column>
<el-table-column v-if="active == '1'" fixed="right" align="center" label="操作" width="80">
<template #default="scope">
<img src="@/assets/MenuIcon/lbcz_sc.png" alt="" @click="del(scope.row)"
title="删除" style="cursor: pointer;margin-left:10px">
</template>
</el-table-column>
</el-table>
<div class="flex justify-between items-center mb-4">
<Page :total="total" v-model:size="queryParams.size" v-model:current="queryParams.current" @pagination="init()" ></Page>
<el-button v-if="active == '0'" type="primary" @click="tabClick">
<span>全部已读</span>
</el-button>
</div>
</el-tabs>
<div v-show="isLookOver" class="p-6">
<div class="flex justify-between">
<span class="font-black" style="color: #000;font-size: 20px;">{{ info.title }}</span>
<el-button type="primary" plain @click="isLookOver = false">
返回
</el-button>
</div>
<div style="height:50px;line-height: 50px;border-bottom: 1px solid #e4e4e4;">
<span style="color: rgb(148, 148, 148);font-size:14px;">{{ info.createtime }}</span>
</div>
<div style="color:rgb(80, 80, 80);font-size: 14px;padding-top:15px;overflow:auto">
{{ info.content }}
</div>
</div>
</el-dialog>
</template>
<style lang="scss">
.title{
color:rgb(64, 158, 255);
cursor: pointer;
&:hover{
text-decoration: underline;
}
}
.dialog {
height: 500px;
img {
display: inline-block;
}
.el-tabs__content,
.el-tabs--border-card {
border: none !important;
}
.el-tabs__item {
height: 50px !important;
line-height: 50px !important;
}
.el-dialog__headerbtn {
z-index: 100;
top: 0px;
}
.el-dialog__header {
padding: 0 !important;
}
.el-dialog__body {
padding: 0 !important;
}
}
</style>

View File

@ -1,134 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, watchEffect } from 'vue'; import { computed } from "vue";
import { useWindowSize } from '@vueuse/core'; import { AppMain, Navbar } from "./components/index";
import { AppMain, Navbar, Settings, TagsView } from './components/index';
import Sidebar from './components/Sidebar/index.vue';
import RightPanel from '@/components/RightPanel/index.vue';
import Hamburger from '@/components/Hamburger/index.vue';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from "@/store/modules/app";
import { useSettingsStore } from '@/store/modules/settings';
const { width } = useWindowSize();
/**
* 响应式布局容器固定宽度
*
* 大屏>=1200px
* 中屏>=992px
* 小屏>=768px
*/
const WIDTH = 992;
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore();
const fixedHeader = computed(() => settingsStore.fixedHeader);
const showTagsView = computed(() => settingsStore.tagsView);
const showSettings = computed(() => settingsStore.showSettings);
const classObj = computed(() => ({ const classObj = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
withoutAnimation: appStore.sidebar.withoutAnimation, withoutAnimation: appStore.sidebar.withoutAnimation,
mobile: appStore.device === 'mobile'
})); }));
watchEffect(() => {
if (width.value < WIDTH) {
appStore.toggleDevice('mobile');
appStore.closeSideBar(true);
} else {
appStore.toggleDevice('desktop');
if (width.value >= 1200) {
//
appStore.openSideBar(true);
} else {
appStore.closeSideBar(true);
}
}
});
function handleOutsideClick() {
appStore.closeSideBar(false);
}
function toggleSideBar() {
appStore.toggleSidebar(true);
}
</script> </script>
<template> <template>
<div :class="classObj" class="app-wrapper"> <div :class="classObj" class="app-wrapper">
<!-- 手机设备 && 侧边栏 显示遮罩层 --> <navbar />
<div <div class="main-container">
class="drawer-bg"
v-if="classObj.mobile && classObj.openSidebar"
@click="handleOutsideClick"
></div>
<Sidebar class="sidebar-container" />
<navbar :class="{ 'fixed-header': fixedHeader }" />
<div v-show="fixedHeader" style="height:60px"></div>
<hamburger
:is-active="appStore.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<div :class="{ hasTagsView: showTagsView }" class="main-container">
<tags-view v-if="showTagsView" />
<!--主页面--> <!--主页面-->
<app-main /> <app-main />
<!-- 设置面板 -->
<RightPanel v-if="showSettings">
<settings />
</RightPanel>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/mixin.scss' as mixin; @use '@/styles/mixin.scss' as mixin;
@use '@/styles/variables.module.scss' as variables;
.app-wrapper { .app-wrapper {
// @include clearfix; // @include clearfix;
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 55;
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
z-index: 98;
width: 100%;
transition: width 0.28s;
background: #ffffff;
}
.hideSidebar .fixed-header {
// width: calc(100% - 54px);
width:100%;
}
.mobile .fixed-header {
width: 100%;
} }
</style> </style>

View File

@ -8,6 +8,8 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'; import Pagination from '@/components/Pagination/index.vue';
import '@/permission'; import '@/permission';
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css' // Ant Design 全局样式重置
// 引入svg注册脚本 // 引入svg注册脚本
import 'virtual:svg-icons-register'; import 'virtual:svg-icons-register';
@ -39,6 +41,7 @@ app
.component('Pagination', Pagination) .component('Pagination', Pagination)
.use(router) .use(router)
.use(ElementPlus) .use(ElementPlus)
.use(Antd)
.use(WujieVue) .use(WujieVue)
.use(i18n) .use(i18n)
.mount('#app'); .mount('#app');

View File

@ -0,0 +1,162 @@
<!-- SidePanelItem.vue -->
<template>
<div class="carousel-container">
<SidePanelItem title="典型设施介绍">
<div v-if="carouselData.length > 0" class="carousel-wrapper">
<a-carousel v-model:current="currentIndex" autoplay class="tech-carousel"
:dot-style="{ bottom: '0px' }">
<div v-for="(item, index) in carouselData" :key="index" class="carousel-item">
<div class="image-container">
<img :src="item.image" :alt="item.title" class="carousel-image" />
</div>
</div>
</a-carousel>
<!-- 文字描述区域 -->
<div v-if="carouselData[currentIndex]" class="description-container">
<p class="item-description">{{ carouselData[currentIndex].description }}</p>
</div>
</div>
<div v-else>
<a-empty />
</div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
// 便
defineOptions({
name: 'dianxingcuoshijieshao'
});
//
const currentIndex = ref(0);
//
const carouselData = ref([
{
title: '叠梁门',
description: '叠梁门是一种用于调节水库流量和控制水位的设施。在低温季节,可以通过控制叠梁门的开闭程度来调节流量,减少低温水体的进...',
image: 'https://img.shetu66.com/2024/02/19/170835076078361368.png' //
},
{
title: '环保设施',
description: '现代化环保设施,采用先进技术,有效处理工业废水废气,实现达标排放,保护生态环境...',
image: 'https://img.shetu66.com/2024/02/19/170835076078361368.png'
},
{
title: '智能监控',
description: '24 小时智能监控系统,实时监测设备运行状态,确保设施安全稳定运行...',
image: 'https://img.shetu66.com/2024/02/19/170835076078361368.png'
},
{
title: '水处理系统',
description: '高效水处理系统,通过多级过滤和生物处理,实现水资源循环利用...',
image: 'https://img.shetu66.com/2024/02/19/170835076078361368.png'
}
]);
//
onMounted(() => {
//
});
</script>
<style lang="scss" scoped>
.carousel-container {
width: 415px;
padding-right: 12px;
}
.carousel-wrapper {
width: 100%;
:deep(.ant-carousel) {
width: 100%;
.slick-slide {
height: auto !important;
}
.slick-dots {
bottom: 0px !important;
margin: 0;
padding: 8px 0;
display: flex;
justify-content: center;
gap: 6px;
li {
width: 6px;
height: 6px;
margin: 0;
background-color: transparent;
border-radius: 50%;
button {
width: 6px;
height: 6px;
background-color: #005293;
opacity: 0.3;
border-radius: 50%;
transition: all 0.3s;
padding: 0;
border: none;
}
&.slick-active button {
width: 8px;
height: 8px;
opacity: 1;
background-color: #005293;
}
}
}
}
}
.carousel-item {
display: block;
}
.image-container {
width: 100%;
background-color: #f5f0e1; //
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.carousel-image {
width: 100%;
height: auto;
display: block;
object-fit: contain; //
}
.description-container {
padding: 12px 0 0 0;
text-align: left;
}
.item-title {
font-size: 16px;
font-weight: 600;
color: #1f1f1f;
margin: 0 0 8px 0;
line-height: 1.5;
}
.item-description {
font-size: 14px;
color: #595959;
margin: 0;
line-height: 1.8;
text-align: justify;
}
</style>

View File

@ -0,0 +1,134 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="环保设施情况">
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div class="facility-icon">
<component :is="facility.icon" />
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div> <span class="facility-count">{{ facility.count }}</span> </div>
</div>
</div>
</div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
// DropletOutlined,
// ThermometerOutlined,
HomeOutlined,
FileTextOutlined,
// FishOutlined,
EnvironmentOutlined,
HeartOutlined
} from '@ant-design/icons-vue';
// 便
defineOptions({
name: 'huanbaoMod'
});
//
const facilities = ref([
{
name: '生态流量泄放设施',
count: 145,
// icon: DropletOutlined
},
{
name: '低温水减缓设施',
count: 24,
// icon: ThermometerOutlined
},
{
name: '栖息地',
count: 142,
icon: HomeOutlined
},
{
name: '过鱼设施',
count: 60,
icon: FileTextOutlined
},
{
name: '鱼类增殖站',
count: 69,
// icon: FishOutlined
},
{
name: '珍稀植物园',
count: 41,
icon: EnvironmentOutlined
},
{
name: '动物救助站',
count: 4,
icon: HeartOutlined
}
]);
//
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.facility-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
.facility-card {
display: flex;
align-items: center;
padding: 6px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
transition: all 0.3s;
cursor: pointer;
}
.facility-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
margin-right: 8px;
background: #2f6b98;
border-radius: 50%;
.anticon {
font-size: 24px;
color: #fff;
}
}
.facility-info {
flex: 1;
}
.facility-name {
font-size: 16px;
color: #333;
margin-bottom: 4px;
// font-weight: 500;
}
.facility-count {
font-size: 18px;
color: #2f6b98;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,133 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="环保自动监测工作开展情况">
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div class="facility-icon">
<component :is="facility.icon" />
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div> <span class="facility-count">{{ facility.count }}</span> </div>
</div>
</div>
</div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
// DropletOutlined,
// ThermometerOutlined,
HomeOutlined,
FileTextOutlined,
// FishOutlined,
EnvironmentOutlined,
HeartOutlined
} from '@ant-design/icons-vue';
// 便
defineOptions({
name: 'huanbaoMod'
});
//
const facilities = ref([
{
name: '生态流量泄放设施',
count: 145,
// icon: DropletOutlined
},
{
name: '低温水减缓设施',
count: 24,
// icon: ThermometerOutlined
},
{
name: '栖息地',
count: 142,
icon: HomeOutlined
},
{
name: '过鱼设施',
count: 60,
icon: FileTextOutlined
},
{
name: '鱼类增殖站',
count: 69,
// icon: FishOutlined
},
{
name: '珍稀植物园',
count: 41,
icon: EnvironmentOutlined
},
{
name: '动物救助站',
count: 4,
icon: HeartOutlined
}
]);
//
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.facility-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.facility-card {
display: flex;
align-items: center;
padding: 6px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
transition: all 0.3s;
cursor: pointer;
}
.facility-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
margin-right: 8px;
background: #2f6b98;
border-radius: 50%;
.anticon {
font-size: 24px;
color: #fff;
}
}
.facility-info {
flex: 1;
}
.facility-name {
font-size: 16px;
color: #333;
margin-bottom: 4px;
// font-weight: 500;
}
.facility-count {
font-size: 18px;
color: #2f6b98;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,35 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="基本情况介绍" :shrink="true">
<p v-if="title_text" class="basic_body1">{{ title_text }}</p>
<div v-else class="zanwushujv"> <a-empty /></div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
// 便
defineOptions({
name: 'jidiInfoMod'
});
const title_text = ref('我国水能资源丰富,主要集中在金沙江、长江上游、雅砻江、黄河上游、大渡河、南盘江-红水河、乌江和西南诸河等流域总技术可开发量约3.81亿kW占全国技术可开发量的55.5%。截至2023年年底雅砻江、金沙江、大渡河、乌江、长江上游、南盘江-红水河等流域已建和在建比例超80%,水能资源开发程度较高,剩余待开发水利资源主要集中在西南诸河,发展潜力巨大。')
//
onMounted(() => {
});
</script>
<style lang="scss">
.zanwushujv {
display: flex;
align-items: center;
justify-content: center;
}
.basic_body1 {
width: 100%;
}
</style>

View File

@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref } from "vue";
const loading = ref(false);
const isOpen = ref(true);
const jidiData = ref([
{
id: 1,
name: "当前全部",
},
{
id: 2,
name: "水电基地2",
},
{
id: 3,
name: "水电基地3",
},
{
id: 4,
name: "水电基地4",
},
{
id: 5,
name: "水电基地5",
},
{
id: 6,
name: "水电基地6",
},
{
id: 7,
name: "水电基地7",
},
{
id: 8,
name: "水电基地8",
},
{
id: 9,
name: "水电基地9",
},
{
id: 10,
name: "水电基地10",
},
{
id: 11,
name: "水电基地11",
},
]);
const jidiDataNum = ref(9);
const selectedItem: any = ref(1);
</script>
<template>
<div class="jidiSelectorMod">
<a-spin :spinning="loading">
<div
class="qgc-dropdown-select"
@mouseenter="jidiDataNum = jidiData.length"
@mouseleave="jidiDataNum = 9"
>
<div class="title" @click="isOpen = !isOpen">
<div>水电基地</div>
<div style="padding-right: 5px;" :style="{ transform: !isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }">
<i class="icon iconfont icon-topOutline"></i>
</div>
</div>
<div
v-if="isOpen"
class="item"
v-for="i in jidiData.slice(0, jidiDataNum)"
:class="{ selected: selectedItem === i.id }"
@click="selectedItem = i.id"
>
<i class="icon iconfont icon-hydroPower"></i>
<span style="margin-left: 10px">{{ i.name }}</span>
</div>
</div>
</a-spin>
</div>
</template>
<style scoped lang="scss">
@use "@/styles/variables.module.scss" as *;
.jidiSelectorMod {
width: 175px;
max-height: 941px;
border: 1px solid #cedce8;
border-radius: 1px;
background-color: #e5edf3;
padding: 4px;
position: relative;
border-radius: 1px;
margin: 16px 0 0 16px;
z-index: 99;
pointer-events: auto;
.qgc-dropdown-select {
width: 100%;
max-height: 941px;
overflow: auto;
.title {
height: 26.14px;
padding: 0 2px;
font-size: 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
color: $primary-title-color;
cursor: pointer;
.icon-topOutline {
display: inline-block;
width: 12px;
height: 12px;
font-size: 12px;
scale: 0.6;
position: relative;
top: -3px;
}
}
.item {
margin: 4px 0;
border-radius: 2px;
box-sizing: border-box;
border: 1px solid #acc4d6;
line-height: 30px;
padding: 0 8px;
font-size: 14px;
background-color: #fff;
transition: background-color 0.3s, color 0.3s;
cursor: pointer;
&:hover,
&.selected {
background-color: #2f6b98;
color: #fff;
}
}
}
}
</style>

View File

@ -0,0 +1,190 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="生态流量达标情况" :clickprompt="obj">
<div class="body_topOne">
<a-radio-group v-model:value="mode">
<a-radio-button value="top">逐时</a-radio-button>
<a-radio-button value="left"></a-radio-button>
</a-radio-group>
<div class="title_text">95%座数/总座数</div>
</div>
<a-spin :spinning="spinning">
<div>
<div v-for="el in datalist">
<div :key='el.key'>
<div class="boy_one">
<div style="flex: 1; white-space: nowrap;margin-right: 5px;">{{ el.name }}</div>
<img style="flex: 1;" src="@/assets/components/fgx.svg" alt="">
</div>
<div class="body_zhu">
<div class="body_biao">
<div v-for="value in allArr">
<div :key="value" class="nei_body">
<div v-if="value < (el?.key == null ? 1 : el.qecTCnt == 0 ? 0 : el.qecCnt / el.qecTCnt) * 20"
class="little_body" :style="{ backgroundColor: el.color }"></div>
</div>
</div>
</div>
<div class="body_text">
{{ el.qecCnt }}/{{ el.qecTCnt }}
</div>
</div>
</div>
</div>
</div>
</a-spin>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
// 便
defineOptions({
name: 'shengtaidabiaoMod'
});
const obj =
{
show: true,
value: '1、统计电站范围接入过生态流量数据的电站,2、当来水不足时生态流量不小于入库流量判定为达标,3、“≥95%座数”表示统计电站范围内时段达标率大于等于95%的电站数量',
}
const mode = ref('top');
const dataArr: any = ref([
{
name: '生态环境部门要求',
qecCnt: '41', //
qecTCnt: '55', //
type: 1
},
{
name: '水利部门要求',
qecCnt: '41', //
qecTCnt: '55', //
type: 2
}
])
const list: any = ref(
[
{ name: '生态环境部门要求', type: 1, color: '#77C300', key: 'qecInterval' },
{ name: '水利部门要求', type: 2, color: '#56C3E3', key: 'mwrInterval' },
{ name: '多年平均流量 10%', type: 3, color: '#F76B01', key: 'avqInterval' },
{ name: '无生态流量要求', type: 4, color: '#B4B4B4', key: null },
]
)
const datalist: any = ref([])
const allArr: any = ref(Array.from({ length: 20 }, (_, i) => i))
const spinning = ref(false)
// type list dataArr
const setStyle = () => {
if (dataArr.value.length == 0) {
return false
}
dataArr.value.forEach((item: any) => {
// list type
const matched = list.value.find((listItem: any) => listItem.type === item.type)
if (matched) {
// dataArr
datalist.value.push({
...matched,
...item,
})
}
})
console.log(datalist.value)
}
//
onMounted(() => {
setStyle()
});
</script>
<style lang="scss" scoped>
.body_topOne {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.title_text {
font-size: 16px;
}
}
.boy_one {
display: flex;
align-items: center;
margin: 5px 0px;
}
.body_zhu {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.body_biao {
width: 100%;
height: 52px;
border: 1px solid #ccdae7;
border-radius: 2px;
display: flex;
padding: 5px 1px;
overflow: hidden;
.nei_body {
width: 12px;
height: 100%;
border: 1px solid #ccdae7;
padding: 2px;
margin-left: 4px;
.little_body {
width: 100%;
height: 100%;
}
}
}
.body_text {
display: flex;
flex-direction: column;
width: 90px;
height: 100%;
justify-content: space-around;
margin-left: 6px;
align-items: center;
font-size: 18px;
font-weight: 500;
}
}
.ant-radio-group {
// border: 3px solid #2f6b98 !important;
// border-radius: 10px !important;
.ant-radio-button-wrapper-checked {
border: 1px solid #2f6b98 !important;
background-color: #2f6b98 !important;
color: #fff !important;
}
.ant-radio-button-wrapper {
border: 2px solid #2f6b98 !important;
}
.ant-radio-button-wrapper-checked :before {
background-color: #2f6b98 !important;
}
}
</style>

View File

@ -0,0 +1,215 @@
<template>
<div>
<SidePanelItem title="生态流量达标情况" :clickprompt="obj">
<div class="body_topOne">
<a-radio-group v-model:value="mode">
<a-radio-button value="top">按基地</a-radio-button>
<a-radio-button value="left">按调节性能</a-radio-button>
</a-radio-group>
</div>
<a-spin :spinning="spinning">
<div ref="chartRef" class="chart-container"></div>
</a-spin>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import * as echarts from 'echarts';
// 便
defineOptions({
name: 'shengtaidabiaoMod'
});
const obj =
{
show: true,
value: '1、统计电站范围接入过生态流量数据的电站2、当来水不足时生态流量不小于入库流量判定为达标3、"≥95% 座数"表示统计电站范围内时段达标率大于等于 95% 的电站数量',
}
const mode = ref('top');
const spinning = ref(false)
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
//
const categoryData = [
'其他', '闽浙赣', '澜沧江干流', '东北', '南盘江·红水河',
'黄河中游干流', '黄河上游干流', '湘西', '长江上游干流',
'乌江干流', '大渡河干流', '雅砻江干流', '金沙江干流'
];
const currentData = Array(13).fill(0).map(() => Math.random() * 5 + 86);
const lastYearData = Array(13).fill(0).map(() => Math.random() * 5 + 86);
//
const initChart = () => {
if (!chartRef.value) return;
//
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(chartRef.value);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function(params: any) {
let result = params[0].name + '<br/>';
params.forEach((param: any) => {
const percentage = param.value.toFixed(2) + '%';
result += param.marker + param.seriesName + '&nbsp;&nbsp;<b>' + percentage + '</b><br/>';
});
return result;
}
},
legend: {
data: ['当前', '去年同期'],
top: 0,
left: 'center',
itemWidth: 20,
itemHeight: 10,
textStyle: {
color: '#666'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '50',
containLabel: true
},
dataZoom: [
{
type: 'inside',
xAxisIndex: 0,
filterMode: 'empty',
zoomOnMouseWheel: true,
moveOnMouseMove: false,
moveOnMouseWheel: true,
start: 0,
end: 100,
minValueSpan: 0,
maxValueSpan: 20
}
],
xAxis: {
type: 'value',
min: 80,
max: 100,
splitLine: {
show: true,
lineStyle: {
color: '#E8E8E8',
type: 'solid'
}
},
axisLabel: {
color: '#666',
formatter: '{value}'
}
},
yAxis: {
type: 'category',
data: categoryData,
inverse: true,
axisLabel: {
color: '#666',
fontSize: 12,
interval: 0,
rotate: 0,
margin: 10
},
axisLine: {
show: true,
lineStyle: {
color: '#666'
}
},
axisTick: {
show: true,
lineStyle: {
color: '#666'
}
},
splitLine: {
show: false
}
},
series: [
{
name: '当前',
type: 'bar',
data: currentData,
itemStyle: {
color: '#5470C6'
},
barWidth: 12,
barGap: '30%'
},
{
name: '去年同期',
type: 'bar',
data: lastYearData,
itemStyle: {
color: '#91CC75'
},
barWidth: 12,
barGap: '30%'
}
]
};
chartInstance.setOption(option);
};
//
onMounted(() => {
initChart();
//
window.addEventListener('resize', () => {
chartInstance?.resize();
});
});
// mode
watch(mode, () => {
// mode
initChart();
});
</script>
<style lang="scss" scoped>
.ant-radio-group {
// border: 3px solid #2f6b98 !important;
// border-radius: 10px !important;
.ant-radio-button-wrapper-checked {
border: 1px solid #2f6b98 !important;
background-color: #2f6b98 !important;
color: #fff !important;
}
.ant-radio-button-wrapper {
border: 2px solid #2f6b98 !important;
}
.ant-radio-button-wrapper-checked :before {
background-color: #2f6b98 !important;
}
}
.chart-container {
width: 100%;
height: 500px;
padding: 10px;
}
</style>

View File

@ -0,0 +1,297 @@
<!-- DataTable.vue -->
<template>
<div class="data-table-container">
<a-table :columns="columns" :data-source="tableData" :pagination="false" size="middle" :customRow="customRow"
class="custom-table">
</a-table>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import type { ColumnsType } from 'ant-design-vue/es/table/interface';
//
defineOptions({
name: 'DataTable'
});
//
const columns: ColumnsType = [
{
title: '基地名称',
dataIndex: 'name',
key: 'name',
fixed: 'left',
// width: 65,
align: 'left'
},
{
title: '装机容量 (万 kW)',
key: 'capacity',
align: 'center',
children: [
{
title: '总计',
dataIndex: 'total',
key: 'total',
width: 65,
align: 'center'
},
{
title: '已建',
dataIndex: 'built',
key: 'built',
width: 65,
align: 'center'
},
{
title: '在建',
dataIndex: 'building',
key: 'building',
width: 65,
align: 'center'
},
{
title: '未建',
dataIndex: 'unbuilt',
key: 'unbuilt',
width: 65,
align: 'center'
},
]
}
];
//
const tableData = ref([
{
key: '1',
name: '金沙江干流',
total: '7952.00',
built: '6370.00',
building: '902.00',
unbuilt: '680.00'
},
{
key: '2',
name: '雅砻江干流',
total: '2773.65',
built: '1920.00',
building: '492.00',
unbuilt: '361.65'
},
{
key: '3',
name: '大渡河干流',
total: '2689.65',
built: '1925.65',
building: '534.00',
unbuilt: '230.00'
},
{
key: '4',
name: '乌江干流',
total: '1181.36',
built: '1133.36',
building: '48.00',
unbuilt: '0.00'
},
{
key: '5',
name: '长江上游干流',
total: '3212.65',
built: '2523.65',
building: '0.00',
unbuilt: '689.00'
},
{
key: '6',
name: '湘西',
total: '1054.32',
built: '959.30',
building: '38.52',
unbuilt: '56.65'
},
{
key: '7',
name: '黄河上游干流',
total: '2794.59',
built: '1749.89',
building: '0.00',
unbuilt: '1044.65'
},
{
key: '8',
name: '黄河中游干流',
total: '835.65',
built: '401.65',
building: '0.00',
unbuilt: '434.00'
},
{
key: '9',
name: '南盘江 - 红水河',
total: '1271.00',
built: '1271.00',
building: '0.00',
unbuilt: '0.00'
},
{
key: '10',
name: '东北',
total: '1331.95',
built: '749.05',
building: '0.00',
unbuilt: '582.90'
},
{
key: '11',
name: '澜沧江干流',
total: '2535.00',
built: '2275.00',
building: '260.00',
unbuilt: '0.00'
},
{
key: '12',
name: '怒江干流',
total: '3138.00',
built: '0.00',
building: '102.00',
unbuilt: '3036.00'
},
{
key: '13',
name: '闽浙赣',
total: '962.08',
built: '920.68',
building: '0.00',
unbuilt: '41.40'
},
{
key: '14',
name: '其他',
total: '7460.57',
built: '7273.27',
building: '121.90',
unbuilt: '65.40'
},
{
key: '15',
name: '总计',
total: '7460.57',
built: '7273.27',
building: '121.90',
unbuilt: '65.40'
}
]);
//
const customRow = (record: any, index: number) => {
return {
style: {
backgroundColor: index % 2 === 1 ? '#fafafa' : '#ffffff'
}
};
};
//
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.data-table-container {
padding: 0;
background: #fff;
margin-top: 10px;
}
.custom-table {
:deep(.ant-table) {
font-size: 14px;
border: 1px solid #e8e8e8;
.ant-table-thead {
>tr {
>th {
background-color: #e5eff8 !important;
color: #2f6b98;
// font-weight: 600;
border: 1px solid #e8e8e8 !important;
padding: 6px 8px;
text-align: center;
&:first-child {
background-color: #e5eff8 !important;
text-align: left;
}
}
&:first-child {
>th {
background-color: #e5eff8 !important;
&:first-child {
background-color: #e5eff8 !important;
}
}
}
}
}
.ant-table-tbody {
>tr {
>td {
border: 1px solid #e8e8e8;
padding: 6px 8px;
text-align: center;
&:first-child {
text-align: left;
// font-weight: 500;
}
}
&:hover {
background-color: #e6f7ff !important;
}
}
}
.ant-table-footer {
padding: 0;
background-color: #fafafa;
.table-footer {
.footer-row {
display: flex;
align-items: center;
padding: 12px 8px;
border-top: 1px solid #e8e8e8;
background-color: #fafafa;
.footer-item {
flex: 1;
text-align: center;
// font-weight: 600;
color: #333;
&:first-child {
flex: 0 0 140px;
text-align: center;
}
}
}
}
}
// footer
.ant-table-footer::before {
display: none;
}
}
}
</style>

View File

@ -0,0 +1,53 @@
<!-- SidePanelItem.vue -->
<template>
<div class="basic_body">
<SidePanelItem title="大中型水电开发及环境监测数据接入情况" :prompt="prompt">
<div>
<div class="body_top">
<!-- {/* 水电开发情况图表 */} -->
<div style="flex: 1;">
<div>水电开发情况</div>
<div style="color: #757575;font-size: 12px;line-height: 16px;">图释数量/装机容量</div>
<div>
<DevelopStatusChart />
</div>
</div>
<!-- style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center" }} -->
<div style="flex: 1;display: flex; flex-direction: column; align-items: center;">
<EngEnvironmentData />
</div>
</div>
<DataTable />
</div>
<!-- <div v-else class="zanwushujv"> <a-empty /></div> -->
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import DevelopStatusChart from "@/components/developStatusChart/index.vue"
import EngEnvironmentData from "@/components/engEnvironmentData/index.vue"
import DataTable from "./DataTable.vue"
// 便
defineOptions({
name: 'shuidianhuangjingjieruMod'
});
const prompt = ref({
show: true,
value: '统计大中型已建,在建水电站',
})
//
onMounted(() => {
});
</script>
<style lang="scss">
.body_top {
display: flex;
justify-content: space-between;
}
</style>

View File

@ -5,20 +5,35 @@ import { usePermissionStoreHook } from '@/store/modules/permission';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
NProgress.configure({ showSpinner: false }); // 进度条 NProgress.configure({ showSpinner: false });
const permissionStore = usePermissionStoreHook(); const permissionStore = usePermissionStoreHook();
// 白名单路由 // 白名单路由
const whiteList = ['/login']; const whiteList = ['/login'];
if (import.meta.env.DEV) { // 查找第一个可用路由
whiteList.push('/process/antd-demo'); function findFirstAvailableRoute(routes: RouteRecordRaw[]): string | undefined {
for (const route of routes) {
if (route.meta?.hidden) continue;
if (route.children?.length > 0) {
const child = route.children[0];
// 优先使用 opturl 或 path
const targetPath = child.opturl || child.path;
return targetPath?.startsWith('/') ? targetPath : `/${targetPath}`;
}
const targetPath = route.opturl || route.path;
return targetPath;
}
return '/404';
} }
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); NProgress.start();
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
if (userStore.Token) { if (userStore.Token) {
// 登录成功,跳转到首页 // 登录成功,跳转到首页
if (to.path === '/login') { if (to.path === '/login') {
@ -26,9 +41,19 @@ router.beforeEach(async (to, from, next) => {
NProgress.done(); NProgress.done();
} else { } else {
const hasGetUserInfo = userStore.roles.length > 0; const hasGetUserInfo = userStore.roles.length > 0;
// const hasGetUserInfo = true;
if (hasGetUserInfo) { if (hasGetUserInfo) {
// 已获取用户信息,检查路由匹配
if (to.matched.length === 0) { if (to.matched.length === 0) {
// 路由未匹配,可能是访问根路径
if (to.path === '/') {
const firstRoute = findFirstAvailableRoute(permissionStore.routes);
if (firstRoute) {
next(firstRoute);
NProgress.done();
return;
}
}
from.name ? next({ name: from.name as any }) : next('/401'); from.name ? next({ name: from.name as any }) : next('/401');
} else { } else {
next(); next();
@ -36,15 +61,25 @@ router.beforeEach(async (to, from, next) => {
} else { } else {
try { try {
const { roles } = await userStore.getInfo(); const { roles } = await userStore.getInfo();
const accessRoutes: RouteRecordRaw[] = const accessRoutes: RouteRecordRaw[] = await permissionStore.generateRoutes(roles);
await permissionStore.generateRoutes(roles);
accessRoutes.forEach((route: any) => { accessRoutes.forEach((route: any) => {
router.addRoute(route); router.addRoute(route);
}); });
// 关键:如果是根路径,加载完路由后跳转到第一个可用路由
if (to.path === '/') {
const firstRoute = findFirstAvailableRoute(accessRoutes);
if (firstRoute) {
next(firstRoute);
NProgress.done();
return;
}
}
next({ ...to, replace: true }); next({ ...to, replace: true });
} catch (error) { } catch (error) {
// 移除 token 并跳转登录页 console.log(error);
await userStore.resetToken(); await userStore.resetToken();
next(`/login?redirect=${to.path}`); next(`/login?redirect=${to.path}`);
NProgress.done(); NProgress.done();

View File

@ -26,48 +26,11 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/error-page/404.vue'), component: () => import('@/views/error-page/404.vue'),
meta: { hidden: true } meta: { hidden: true }
}, },
{ {
path: '/', path: '/401',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
opturl: '/dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: '首页',
icon: 'homepage',
meta: { title: 'dashboard', icon: 'homepage', affix: true }
},
{
path: 'personalCenter',
component: () => import('@/views/system/user/personalCenter.vue'),
name: '个人中心',
meta: { title: 'personalCenter',hidden: true, icon: 'personalCenter' }
},
{
path: '401',
component: () => import('@/views/error-page/401.vue'), component: () => import('@/views/error-page/401.vue'),
meta: { hidden: true } meta: { hidden: true }
}, },
]
},
{
path: '/process',
opturl: '/process',
component: Layout,
redirect: '/process/antd-demo',
children: [
{
path: 'antd-demo',
opturl: '/process/antd-demo',
component: () => import('@/views/process/antd-demo.vue'),
name: 'AntD Demo',
meta: { title: 'AntD Demo' }
}
]
}
]; ];
// 创建路由 // 创建路由

View File

@ -10,7 +10,7 @@ interface DefaultSettings {
} }
const defaultSettings: DefaultSettings = { const defaultSettings: DefaultSettings = {
title: '公司开发平台框架', title: '水电水利建设项目全过程环境管理信息平台',
showSettings: false, showSettings: false,
tagsView: true, tagsView: true,
fixedHeader: true, fixedHeader: true,

View File

@ -1,5 +1,4 @@
import { import {
getSidebarStatus,
setSidebarStatus, setSidebarStatus,
getSize, getSize,
setSize, setSize,
@ -24,7 +23,12 @@ export enum SizeType {
large, large,
small small
} }
export const usetTheme = {
token: {
colorPrimary: '#1890ff',
borderRadius: 2,
},
};
// setup // setup
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
// state // state
@ -32,7 +36,6 @@ export const useAppStore = defineStore('app', () => {
const size = ref(getSize() || 'default'); const size = ref(getSize() || 'default');
const language = ref(getLanguage()); const language = ref(getLanguage());
const sidebar = reactive({ const sidebar = reactive({
opened: getSidebarStatus() !== 'closed',
withoutAnimation: false withoutAnimation: false
}); });
@ -44,32 +47,6 @@ export const useAppStore = defineStore('app', () => {
} }
}); });
// actions
function toggleSidebar(withoutAnimation: boolean) {
sidebar.opened = !sidebar.opened;
sidebar.withoutAnimation = withoutAnimation;
if (sidebar.opened) {
setSidebarStatus('opened');
} else {
setSidebarStatus('closed');
}
}
function closeSideBar(withoutAnimation: boolean) {
sidebar.opened = false;
sidebar.withoutAnimation = withoutAnimation;
setSidebarStatus('closed');
}
function openSideBar(withoutAnimation: boolean) {
sidebar.opened = true;
sidebar.withoutAnimation = withoutAnimation;
setSidebarStatus('opened');
}
function toggleDevice(val: string) {
device.value = val;
}
function changeSize(val: string) { function changeSize(val: string) {
size.value = val; size.value = val;
@ -87,11 +64,7 @@ export const useAppStore = defineStore('app', () => {
language, language,
locale, locale,
size, size,
toggleDevice,
changeSize, changeSize,
changeLanguage, changeLanguage,
toggleSidebar,
closeSideBar,
openSideBar
}; };
}); });

View File

@ -8,24 +8,18 @@ import { ref } from 'vue';
const modules = import.meta.glob('../../views/**/**.vue'); const modules = import.meta.glob('../../views/**/**.vue');
export const Layout = () => import('@/layout/index.vue'); export const Layout = () => import('@/layout/index.vue');
// const hasPermission = (roles: string[], route: RouteRecordRaw) => {
// if (route.meta && route.meta.roles) {
// if (roles.includes('ROOT')) {
// return true;
// }
// return roles.some(role => {
// if (route.meta?.roles !== undefined) {
// return (route.meta.roles as string[]).includes(role);
// }
// });
// }
// return false;
// };
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const res: RouteRecordRaw[] = []; const res: RouteRecordRaw[] = [];
routes.forEach(route => { routes.forEach(route => {
const tmp = { ...route } as any; const tmp = { ...route } as any;
// ✅ 保存原始名称到 meta用于菜单显示
tmp.meta = {
...tmp.meta,
title: tmp.name || tmp.menuName, // 原始名称用于显示
};
// ✅ name 使用路径生成唯一值
tmp.name = tmp.path || tmp.opturl;
// if (hasPermission(roles, tmp)) { // if (hasPermission(roles, tmp)) {
tmp.path = tmp.opturl; tmp.path = tmp.opturl;
if (tmp.type == '0') { if (tmp.type == '0') {

View File

@ -1,64 +0,0 @@
import { defineStore } from 'pinia';
import defaultSettings from '../../settings';
import { ref } from 'vue';
import { useStorage } from '@vueuse/core';
/**
*
*/
export enum ThemeType {
light,
dark
}
/**
*
*/
export enum LayoutType {
left,
top,
mix
}
export const useSettingsStore = defineStore('setting', () => {
// state
const showSettings = ref<boolean>(defaultSettings.showSettings);
const tagsView = useStorage<boolean>('tagsView', defaultSettings.tagsView);
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
const layout = useStorage<string>('layout', defaultSettings.layout);
// actions
function changeSetting(param: { key: string; value: any }) {
const { key, value } = param;
switch (key) {
case 'showSettings':
showSettings.value = value;
break;
case 'fixedHeader':
fixedHeader.value = value;
break;
case 'tagsView':
tagsView.value = value;
break;
case 'sidevarLogo':
sidebarLogo.value = value;
break;
case 'layout':
layout.value = value;
break;
default:
break;
}
}
return {
showSettings,
tagsView,
fixedHeader,
sidebarLogo,
layout,
changeSetting
};
});

View File

@ -4,56 +4,4 @@
--el-color-primary-dark: #0d84ff; --el-color-primary-dark: #0d84ff;
// --el-font-size-base: 16px !important; // --el-font-size-base: 16px !important;
} }
.el-button--large, .el-input--large, .el-table--large, .el-form--large, .el-select__tags-text{
font-size: 16px !important;
.el-form-item__label{
font-size: 16px !important;
}
--el-font-size-base: 16px !important;
}
// 覆盖 element-plus 的样式
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type='file'] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// dropdown
.el-dropdown-menu {
a {
display: block;
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
// 选中行背景色值
.el-table__body tr.current-row td {
background-color: #e1f3d8b5 !important;
}
// card 的header统一高度
.el-card__header {
height: 60px !important;
}
// 表格表头和表体未对齐
.el-table__header col[name='gutter'] {
display: table-cell !important;
}

View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

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