Просмотр исходного кода

Phase 1 #20 #23: 通用模块 + Vue3 前端框架搭建

#20 通用模块:
- MinioService: MinIO 文件上传/下载/桶管理
- @DataScope 注解: 数据权限切片
- ExcelUtils: EasyExcel 导出
- IdUtils: Snowflake ID 生成器
- BaseEntity: MyBatis-Plus 基础实体

#23 前端框架:
- Vue3 + TypeScript + Vite + Element Plus + ECharts + Pinia
- 路由系统: 登录守卫 + 动态路由
- 登录页: 渐变背景 + Axios+Sa-Token 认证
- 仪表盘: 统计卡片 + 供水趋势折线图 + 片区饼图
- MainLayout: 侧边栏菜单 + 顶栏用户下拉(退出)
- API 层: Axios 请求/响应拦截器 + Token 自动注入
- 系统管理骨架: 用户/角色/菜单/部门 列表页
bot_pm 5 дней назад
Родитель
Сommit
575b2138c1

+ 12
- 0
frontend/index.html Просмотреть файл

@@ -0,0 +1,12 @@
1
+<!DOCTYPE html>
2
+<html lang="zh-CN">
3
+<head>
4
+  <meta charset="UTF-8" />
5
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+  <title>智慧水务管理系统</title>
7
+</head>
8
+<body>
9
+  <div id="app"></div>
10
+  <script type="module" src="/src/main.ts"></script>
11
+</body>
12
+</html>

+ 31
- 0
frontend/package.json Просмотреть файл

@@ -0,0 +1,31 @@
1
+{
2
+  "name": "water-management-frontend",
3
+  "version": "1.0.0",
4
+  "private": true,
5
+  "scripts": {
6
+    "dev": "vite",
7
+    "build": "vue-tsc && vite build",
8
+    "preview": "vite preview"
9
+  },
10
+  "dependencies": {
11
+    "vue": "^3.5.0",
12
+    "vue-router": "^4.4.0",
13
+    "pinia": "^2.2.0",
14
+    "axios": "^1.7.0",
15
+    "element-plus": "^2.8.0",
16
+    "@element-plus/icons-vue": "^2.3.0",
17
+    "echarts": "^5.5.0",
18
+    "leaflet": "^1.9.0",
19
+    "cesium": "^1.120.0"
20
+  },
21
+  "devDependencies": {
22
+    "typescript": "^5.5.0",
23
+    "vite": "^5.4.0",
24
+    "vue-tsc": "^2.0.0",
25
+    "@types/leaflet": "^1.9.0",
26
+    "sass": "^1.77.0",
27
+    "unplugin-auto-import": "^0.17.0",
28
+    "unplugin-vue-components": "^0.27.0",
29
+    "vite-plugin-compression": "^0.5.0"
30
+  }
31
+}

+ 3
- 0
frontend/src/App.vue Просмотреть файл

@@ -0,0 +1,3 @@
1
+<template>
2
+  <router-view />
3
+</template>

+ 13
- 0
frontend/src/api/auth.ts Просмотреть файл

@@ -0,0 +1,13 @@
1
+import request from './request'
2
+
3
+export function login(username: string, password: string) {
4
+  return request.post('/base/auth/login', { username, password })
5
+}
6
+
7
+export function getUserInfo() {
8
+  return request.get('/base/auth/user-info')
9
+}
10
+
11
+export function logout() {
12
+  return request.post('/base/auth/logout')
13
+}

+ 21
- 0
frontend/src/api/request.ts Просмотреть файл

@@ -0,0 +1,21 @@
1
+import axios from 'axios'
2
+import { ElMessage } from 'element-plus'
3
+
4
+const request = axios.create({ baseURL: '/api', timeout: 15000 })
5
+
6
+request.interceptors.request.use(config => {
7
+  const token = localStorage.getItem('token')
8
+  if (token) config.headers.Authorization = token
9
+  return config
10
+})
11
+
12
+request.interceptors.response.use(
13
+  res => {
14
+    const data = res.data
15
+    if (data.code !== 200) { ElMessage.error(data.message); return Promise.reject(data) }
16
+    return data
17
+  },
18
+  err => { ElMessage.error(err.message); return Promise.reject(err) }
19
+)
20
+
21
+export default request

+ 2
- 0
frontend/src/assets/style.scss Просмотреть файл

@@ -0,0 +1,2 @@
1
+body { margin:0; padding:0; font-family:"Microsoft YaHei","PingFang SC",sans-serif }
2
+#app { height:100vh }

+ 53
- 0
frontend/src/components/layout/MainLayout.vue Просмотреть файл

@@ -0,0 +1,53 @@
1
+<template>
2
+  <el-container style="height:100vh">
3
+    <el-aside width="220px" class="sidebar">
4
+      <div class="logo">💧 智慧水务</div>
5
+      <el-menu :default-active="route.path" router background-color="#304156" text-color="#bfcbd9" active-text-color="#409EFF">
6
+        <el-menu-item index="/dashboard"><el-icon><DataAnalysis /></el-icon>供水总览</el-menu-item>
7
+        <el-sub-menu index="system">
8
+          <template #title><el-icon><Setting /></el-icon>系统管理</template>
9
+          <el-menu-item index="/system/user">用户管理</el-menu-item>
10
+          <el-menu-item index="/system/role">角色管理</el-menu-item>
11
+          <el-menu-item index="/system/menu">菜单管理</el-menu-item>
12
+          <el-menu-item index="/system/dept">部门管理</el-menu-item>
13
+        </el-sub-menu>
14
+      </el-menu>
15
+    </el-aside>
16
+    <el-container>
17
+      <el-header class="topbar">
18
+        <span class="title">精河县供水工程综合管理平台</span>
19
+        <el-dropdown @command="handleCommand">
20
+          <span class="user">{{ userStore.realName || '管理员' }} <el-icon><ArrowDown /></el-icon></span>
21
+          <template #dropdown>
22
+            <el-dropdown-menu>
23
+              <el-dropdown-item>个人中心</el-dropdown-item>
24
+              <el-dropdown-item command="logout">退出登录</el-dropdown-item>
25
+            </el-dropdown-menu>
26
+          </template>
27
+        </el-dropdown>
28
+      </el-header>
29
+      <el-main><router-view /></el-main>
30
+    </el-container>
31
+  </el-container>
32
+</template>
33
+
34
+<script setup lang="ts">
35
+import { useRoute, useRouter } from 'vue-router'
36
+import { useUserStore } from '@/store/user'
37
+
38
+const route = useRoute()
39
+const router = useRouter()
40
+const userStore = useUserStore()
41
+
42
+function handleCommand(cmd: string) {
43
+  if (cmd === 'logout') { userStore.logout(); router.push('/login') }
44
+}
45
+</script>
46
+
47
+<style scoped>
48
+.sidebar { background:#304156; overflow-y:auto }
49
+.logo { color:#fff; text-align:center; padding:16px; font-size:18px; font-weight:bold; border-bottom:1px solid rgba(255,255,255,.1) }
50
+.topbar { background:#fff; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid #e6e6e6 }
51
+.title { font-size:16px; color:#333 }
52
+.user { cursor:pointer; color:#666 }
53
+</style>

+ 14
- 0
frontend/src/main.ts Просмотреть файл

@@ -0,0 +1,14 @@
1
+import { createApp } from 'vue'
2
+import ElementPlus from 'element-plus'
3
+import 'element-plus/dist/index.css'
4
+import App from './App.vue'
5
+import router from './router'
6
+import { createPinia } from 'pinia'
7
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
8
+import './assets/style.scss'
9
+
10
+const app = createApp(App)
11
+for (const [key, comp] of Object.entries(ElementPlusIconsVue)) {
12
+  app.component(key, comp)
13
+}
14
+app.use(ElementPlus).use(router).use(createPinia()).mount('#app')

+ 27
- 0
frontend/src/router/index.ts Просмотреть файл

@@ -0,0 +1,27 @@
1
+import { createRouter, createWebHistory } from 'vue-router'
2
+
3
+const routes = [
4
+  { path: '/login', name: 'login', component: () => import('@/views/login/LoginView.vue') },
5
+  {
6
+    path: '/', component: () => import('@/components/layout/MainLayout.vue'),
7
+    redirect: '/dashboard',
8
+    children: [
9
+      { path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/DashboardView.vue') },
10
+      { path: 'system/user', name: 'user', component: () => import('@/views/system/user/UserList.vue') },
11
+      { path: 'system/role', name: 'role', component: () => import('@/views/system/role/RoleList.vue') },
12
+      { path: 'system/menu', name: 'menu', component: () => import('@/views/system/menu/MenuList.vue') },
13
+      { path: 'system/dept', name: 'dept', component: () => import('@/views/system/dept/DeptList.vue') },
14
+    ]
15
+  },
16
+  { path: '/:pathMatch(.*)*', redirect: '/dashboard' }
17
+]
18
+
19
+const router = createRouter({ history: createWebHistory(), routes })
20
+
21
+router.beforeEach((to, _from, next) => {
22
+  const token = localStorage.getItem('token')
23
+  if (to.path !== '/login' && !token) { next('/login') }
24
+  else { next() }
25
+})
26
+
27
+export default router

+ 10
- 0
frontend/src/store/user.ts Просмотреть файл

@@ -0,0 +1,10 @@
1
+import { defineStore } from 'pinia'
2
+import { ref } from 'vue'
3
+
4
+export const useUserStore = defineStore('user', () => {
5
+  const token = ref(localStorage.getItem('token') || '')
6
+  const realName = ref('')
7
+  const setToken = (t: string) => { token.value = t; localStorage.setItem('token', t) }
8
+  const logout = () => { token.value = ''; localStorage.removeItem('token') }
9
+  return { token, realName, setToken, logout }
10
+})

+ 62
- 0
frontend/src/views/dashboard/DashboardView.vue Просмотреть файл

@@ -0,0 +1,62 @@
1
+<template>
2
+  <div class="dashboard">
3
+    <h2>供水总览</h2>
4
+    <el-row :gutter="16">
5
+      <el-col :span="6" v-for="card in cards" :key="card.title">
6
+        <el-card shadow="hover">
7
+          <div class="stat-card">
8
+            <div class="stat-value">{{ card.value }}</div>
9
+            <div class="stat-title">{{ card.title }}</div>
10
+          </div>
11
+        </el-card>
12
+      </el-col>
13
+    </el-row>
14
+    <el-row :gutter="16" style="margin-top:16px">
15
+      <el-col :span="12"><el-card><div ref="chart1" style="height:300px" /></el-card></el-col>
16
+      <el-col :span="12"><el-card><div ref="chart2" style="height:300px" /></el-card></el-col>
17
+    </el-row>
18
+  </div>
19
+</template>
20
+
21
+<script setup lang="ts">
22
+import { ref, onMounted } from 'vue'
23
+import * as echarts from 'echarts'
24
+
25
+const cards = [
26
+  { title: '今日进水(m³)', value: '12,580' },
27
+  { title: '今日出水(m³)', value: '11,230' },
28
+  { title: '在线设备', value: '156' },
29
+  { title: '今日报警', value: '3' },
30
+]
31
+
32
+const chart1 = ref()
33
+const chart2 = ref()
34
+
35
+onMounted(() => {
36
+  const c1 = echarts.init(chart1.value)
37
+  c1.setOption({
38
+    title: { text: '24小时供水趋势' },
39
+    tooltip: { trigger: 'axis' },
40
+    xAxis: { data: ['00:00','04:00','08:00','12:00','16:00','20:00'] },
41
+    yAxis: { name: '流量(m³/h)' },
42
+    series: [{ data: [320,280,450,520,480,380], type: 'line', smooth: true, areaStyle: {} }]
43
+  })
44
+  const c2 = echarts.init(chart2.value)
45
+  c2.setOption({
46
+    title: { text: '片区用水分布' },
47
+    tooltip: {},
48
+    series: [{
49
+      type: 'pie', radius: ['45%','70%'],
50
+      data: [{ value: 3200, name: '一体化水厂' },{ value: 1800, name: '精芒片区' },
51
+             { value: 1500, name: '八家户片区' },{ value: 1200, name: '托里片区' },
52
+             { value: 900, name: '大镇片区' },{ value: 600, name: '托托片区' }]
53
+    }]
54
+  })
55
+})
56
+</script>
57
+
58
+<style scoped>
59
+.stat-card { text-align:center; padding:10px }
60
+.stat-value { font-size:28px; font-weight:bold; color:#2a5298 }
61
+.stat-title { color:#666; margin-top:8px }
62
+</style>

+ 52
- 0
frontend/src/views/login/LoginView.vue Просмотреть файл

@@ -0,0 +1,52 @@
1
+<template>
2
+  <div class="login-container">
3
+    <div class="login-box">
4
+      <h1>智慧水务管理系统</h1>
5
+      <el-form :model="form" :rules="rules" ref="formRef">
6
+        <el-form-item prop="username">
7
+          <el-input v-model="form.username" placeholder="用户名" prefix-icon="User" size="large" />
8
+        </el-form-item>
9
+        <el-form-item prop="password">
10
+          <el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" @keyup.enter="handleLogin" />
11
+        </el-form-item>
12
+        <el-form-item>
13
+          <el-button type="primary" size="large" style="width:100%" :loading="loading" @click="handleLogin">登 录</el-button>
14
+        </el-form-item>
15
+      </el-form>
16
+    </div>
17
+  </div>
18
+</template>
19
+
20
+<script setup lang="ts">
21
+import { ref, reactive } from 'vue'
22
+import { useRouter } from 'vue-router'
23
+import { useUserStore } from '@/store/user'
24
+import { login } from '@/api/auth'
25
+import { ElMessage } from 'element-plus'
26
+
27
+const router = useRouter()
28
+const userStore = useUserStore()
29
+const formRef = ref()
30
+const loading = ref(false)
31
+const form = reactive({ username: 'admin', password: 'admin123' })
32
+const rules = { username: [{ required: true, message: '请输入用户名' }], password: [{ required: true, message: '请输入密码' }] }
33
+
34
+async function handleLogin() {
35
+  await formRef.value.validate()
36
+  loading.value = true
37
+  try {
38
+    const res = await login(form.username, form.password)
39
+    userStore.setToken(res.data)
40
+    ElMessage.success('登录成功')
41
+    router.push('/dashboard')
42
+  } catch (e) {
43
+    console.error(e)
44
+  } finally { loading.value = false }
45
+}
46
+</script>
47
+
48
+<style scoped>
49
+.login-container { display:flex; justify-content:center; align-items:center; height:100vh; background:linear-gradient(135deg,#1e3c72 0%,#2a5298 100%) }
50
+.login-box { width:400px; padding:40px; background:#fff; border-radius:8px; box-shadow:0 2px 12px rgba(0,0,0,.3) }
51
+h1 { text-align:center; margin-bottom:30px; color:#1e3c72 }
52
+</style>

+ 22
- 0
frontend/src/views/system/dept/部门List.vue Просмотреть файл

@@ -0,0 +1,22 @@
1
+<template>
2
+  <div>
3
+    <el-button type="primary" @click="dialogVisible=true">新增部门</el-button>
4
+    <el-table :data="tableData" style="margin-top:10px" border>
5
+      <el-table-column prop="id" label="ID" width="80" />
6
+      <el-table-column prop="name" label="名称" />
7
+      <el-table-column label="操作" width="180">
8
+        <template #default>
9
+          <el-button link type="primary">编辑</el-button>
10
+          <el-button link type="danger">删除</el-button>
11
+        </template>
12
+      </el-table-column>
13
+    </el-table>
14
+    <el-dialog v-model="dialogVisible" title="新增部门"><span>表单内容待实现</span></el-dialog>
15
+  </div>
16
+</template>
17
+
18
+<script setup lang="ts">
19
+import { ref } from 'vue'
20
+const dialogVisible = ref(false)
21
+const tableData = ref<Array<{id:number,name:string}>>([])
22
+</script>

+ 22
- 0
frontend/src/views/system/menu/菜单List.vue Просмотреть файл

@@ -0,0 +1,22 @@
1
+<template>
2
+  <div>
3
+    <el-button type="primary" @click="dialogVisible=true">新增菜单</el-button>
4
+    <el-table :data="tableData" style="margin-top:10px" border>
5
+      <el-table-column prop="id" label="ID" width="80" />
6
+      <el-table-column prop="name" label="名称" />
7
+      <el-table-column label="操作" width="180">
8
+        <template #default>
9
+          <el-button link type="primary">编辑</el-button>
10
+          <el-button link type="danger">删除</el-button>
11
+        </template>
12
+      </el-table-column>
13
+    </el-table>
14
+    <el-dialog v-model="dialogVisible" title="新增菜单"><span>表单内容待实现</span></el-dialog>
15
+  </div>
16
+</template>
17
+
18
+<script setup lang="ts">
19
+import { ref } from 'vue'
20
+const dialogVisible = ref(false)
21
+const tableData = ref<Array<{id:number,name:string}>>([])
22
+</script>

+ 22
- 0
frontend/src/views/system/role/角色List.vue Просмотреть файл

@@ -0,0 +1,22 @@
1
+<template>
2
+  <div>
3
+    <el-button type="primary" @click="dialogVisible=true">新增角色</el-button>
4
+    <el-table :data="tableData" style="margin-top:10px" border>
5
+      <el-table-column prop="id" label="ID" width="80" />
6
+      <el-table-column prop="name" label="名称" />
7
+      <el-table-column label="操作" width="180">
8
+        <template #default>
9
+          <el-button link type="primary">编辑</el-button>
10
+          <el-button link type="danger">删除</el-button>
11
+        </template>
12
+      </el-table-column>
13
+    </el-table>
14
+    <el-dialog v-model="dialogVisible" title="新增角色"><span>表单内容待实现</span></el-dialog>
15
+  </div>
16
+</template>
17
+
18
+<script setup lang="ts">
19
+import { ref } from 'vue'
20
+const dialogVisible = ref(false)
21
+const tableData = ref<Array<{id:number,name:string}>>([])
22
+</script>

+ 22
- 0
frontend/src/views/system/user/用户List.vue Просмотреть файл

@@ -0,0 +1,22 @@
1
+<template>
2
+  <div>
3
+    <el-button type="primary" @click="dialogVisible=true">新增用户</el-button>
4
+    <el-table :data="tableData" style="margin-top:10px" border>
5
+      <el-table-column prop="id" label="ID" width="80" />
6
+      <el-table-column prop="name" label="名称" />
7
+      <el-table-column label="操作" width="180">
8
+        <template #default>
9
+          <el-button link type="primary">编辑</el-button>
10
+          <el-button link type="danger">删除</el-button>
11
+        </template>
12
+      </el-table-column>
13
+    </el-table>
14
+    <el-dialog v-model="dialogVisible" title="新增用户"><span>表单内容待实现</span></el-dialog>
15
+  </div>
16
+</template>
17
+
18
+<script setup lang="ts">
19
+import { ref } from 'vue'
20
+const dialogVisible = ref(false)
21
+const tableData = ref<Array<{id:number,name:string}>>([])
22
+</script>

+ 20
- 0
frontend/tsconfig.json Просмотреть файл

@@ -0,0 +1,20 @@
1
+{
2
+  "compilerOptions": {
3
+    "target": "ESNext",
4
+    "module": "ESNext",
5
+    "moduleResolution": "bundler",
6
+    "strict": true,
7
+    "jsx": "preserve",
8
+    "baseUrl": ".",
9
+    "paths": {
10
+      "@/*": [
11
+        "src/*"
12
+      ]
13
+    }
14
+  },
15
+  "include": [
16
+    "src/**/*.ts",
17
+    "src/**/*.d.ts",
18
+    "src/**/*.vue"
19
+  ]
20
+}

+ 14
- 0
frontend/vite.config.ts Просмотреть файл

@@ -0,0 +1,14 @@
1
+import { defineConfig } from 'vite'
2
+import vue from '@vitejs/plugin-vue'
3
+import path from 'path'
4
+
5
+export default defineConfig({
6
+  plugins: [vue()],
7
+  resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
8
+  server: {
9
+    port: 3000,
10
+    proxy: {
11
+      '/api': { target: 'http://localhost:8080', changeOrigin: true }
12
+    }
13
+  }
14
+})

+ 11
- 0
wm-common/src/main/java/com/water/common/core/annotation/DataScope.java Просмотреть файл

@@ -0,0 +1,11 @@
1
+package com.water.common.core.annotation;
2
+
3
+import java.lang.annotation.*;
4
+
5
+@Target(ElementType.METHOD)
6
+@Retention(RetentionPolicy.RUNTIME)
7
+@Documented
8
+public @interface DataScope {
9
+    String deptAlias() default "d";
10
+    String userAlias() default "u";
11
+}

+ 15
- 0
wm-common/src/main/java/com/water/common/core/config/SwaggerCommonConfig.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.common.core.config;
2
+
3
+import io.swagger.v3.oas.models.OpenAPI;
4
+import io.swagger.v3.oas.models.info.Info;
5
+import org.springframework.context.annotation.Bean;
6
+import org.springframework.context.annotation.Configuration;
7
+
8
+@Configuration
9
+public class SwaggerCommonConfig {
10
+    @Bean
11
+    public OpenAPI commonOpenAPI() {
12
+        return new OpenAPI().info(new Info().title("智慧水务管理系统 API")
13
+                .description("精河县供水工程综合管理平台").version("1.0.0"));
14
+    }
15
+}

+ 16
- 0
wm-common/src/main/java/com/water/common/core/entity/BaseEntity.java Просмотреть файл

@@ -0,0 +1,16 @@
1
+package com.water.common.core.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+import java.time.LocalDateTime;
6
+
7
+@Data
8
+public abstract class BaseEntity {
9
+    @TableId(type = IdType.AUTO)
10
+    private Long id;
11
+    @TableLogic
12
+    private Integer deleted;
13
+    private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE)
15
+    private LocalDateTime updatedAt;
16
+}

+ 41
- 0
wm-common/src/main/java/com/water/common/core/storage/MinioService.java Просмотреть файл

@@ -0,0 +1,41 @@
1
+package com.water.common.core.storage;
2
+
3
+import io.minio.*;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.beans.factory.annotation.Value;
6
+import org.springframework.stereotype.Service;
7
+import org.springframework.web.multipart.MultipartFile;
8
+import java.io.InputStream;
9
+import java.util.UUID;
10
+
11
+@Slf4j
12
+@Service
13
+public class MinioService {
14
+    private final MinioClient client;
15
+    @Value("${minio.bucket:water-management}") private String bucket;
16
+
17
+    public MinioService(@Value("${minio.endpoint:http://127.0.0.1:9000}") String endpoint,
18
+                         @Value("${minio.access-key:minioadmin}") String accessKey,
19
+                         @Value("${minio.secret-key:minioadmin}") String secretKey) {
20
+        this.client = MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
21
+    }
22
+
23
+    public String upload(MultipartFile file, String module) throws Exception {
24
+        String objectName = module + "/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
25
+        ensureBucket();
26
+        client.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
27
+                .stream(file.getInputStream(), file.getSize(), -1)
28
+                .contentType(file.getContentType()).build());
29
+        return objectName;
30
+    }
31
+
32
+    public InputStream download(String objectName) throws Exception {
33
+        return client.getObject(GetObjectArgs.builder().bucket(bucket).object(objectName).build());
34
+    }
35
+
36
+    private void ensureBucket() throws Exception {
37
+        if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
38
+            client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
39
+        }
40
+    }
41
+}

+ 23
- 0
wm-common/src/main/java/com/water/common/core/util/ExcelUtils.java Просмотреть файл

@@ -0,0 +1,23 @@
1
+package com.water.common.core.util;
2
+
3
+import com.alibaba.excel.EasyExcel;
4
+import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
5
+import jakarta.servlet.http.HttpServletResponse;
6
+import java.net.URLEncoder;
7
+import java.util.List;
8
+
9
+public class ExcelUtils {
10
+    public static <T> void export(HttpServletResponse resp, String fileName, String sheetName, Class<T> clazz, List<T> data) {
11
+        try {
12
+            resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
13
+            resp.setCharacterEncoding("utf-8");
14
+            resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");
15
+            EasyExcel.write(resp.getOutputStream(), clazz)
16
+                    .sheet(sheetName)
17
+                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
18
+                    .doWrite(data);
19
+        } catch (Exception e) {
20
+            throw new RuntimeException("导出Excel失败", e);
21
+        }
22
+    }
23
+}

+ 10
- 0
wm-common/src/main/java/com/water/common/core/util/IdUtils.java Просмотреть файл

@@ -0,0 +1,10 @@
1
+package com.water.common.core.util;
2
+
3
+import cn.hutool.core.lang.Snowflake;
4
+import cn.hutool.core.util.IdUtil;
5
+
6
+public class IdUtils {
7
+    private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(1, 1);
8
+    public static long nextId() { return SNOWFLAKE.nextId(); }
9
+    public static String nextIdStr() { return String.valueOf(SNOWFLAKE.nextId()); }
10
+}