Compare commits

..

10 Commits
master ... v2

Author SHA1 Message Date
b098b1fd58 升级4.0 2025-08-26 16:30:10 +08:00
1c7e608778 升级4.0 2025-08-26 16:25:14 +08:00
4946911f1c 登录和列表 2025-07-25 13:57:36 +08:00
90d40386ac 添加相关页面,调整登录方式 2025-07-24 18:15:22 +08:00
9b4f574093 code 2025-07-23 17:58:06 +08:00
7fe2aab7c7 修改样式 2025-07-23 17:50:13 +08:00
9dc930acce 修改样式 2025-07-23 17:46:44 +08:00
6547123a83 添加相关功能 2025-07-23 17:15:25 +08:00
5a533e15ce vue3 2025-07-22 18:28:50 +08:00
cba477a24d 应用中心单点登录 2025-07-22 18:19:25 +08:00
54 changed files with 2263 additions and 138 deletions

76
api/pom.xml Normal file
View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lktx.center</groupId>
<artifactId>app-center</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>api</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- web框架 -->
<dependency>
<artifactId>hserver-web-starter</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<dependency>
<artifactId>hserver-plugin-forest</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<dependency>
<artifactId>hserver-plugin-satoken</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.16.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.8-jre</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>hserver-maven</artifactId>
<groupId>cn.hserver</groupId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,14 @@
package com.lktx.center;
import cn.hserver.core.boot.HServerApplication;
import cn.hserver.core.boot.annotation.HServerBoot;
import cn.hserver.mvc.server.WebServer;
@HServerBoot
public class Main {
public static void main(String[] args) {
WebServer.webPort(8981);
HServerApplication.run(Main.class, args);
}
}

View File

@ -0,0 +1,20 @@
package com.lktx.center.config;
import cn.dev33.satoken.exception.NotLoginException;
import cn.hserver.core.ioc.annotation.Bean;
import cn.hserver.core.ioc.annotation.Component;
import cn.hserver.mvc.common.JsonResult;
import cn.hserver.mvc.context.WebContext;
import cn.hserver.mvc.exception.GlobalExceptionHandler;
@Component
public class AllException extends GlobalExceptionHandler {
@Override
public void handlerException(Throwable throwable, WebContext webContext) {
NotLoginException exception = getException(throwable, NotLoginException.class);
if (exception!=null ){
webContext.response.sendJson(JsonResult.error(-2, exception.getMessage()));
}
}
}

View File

@ -1,8 +1,8 @@
package com.lktx.center.config;
import cn.hserver.core.config.annotation.Configuration;
import cn.hserver.core.config.annotation.Value;
import cn.hserver.core.ioc.annotation.Bean;
import cn.hserver.core.ioc.annotation.Configuration;
import cn.hserver.core.ioc.annotation.Value;
import me.zhyd.oauth.config.AuthConfig;
import java.util.List;
@ -10,13 +10,13 @@ import java.util.List;
@Configuration
public class AuthRequestConfig {
@Value("oauth.clientId")
@Value("oauth.client-id")
private String clientId;
@Value("oauth.clientSecret")
@Value("oauth.client-secret")
private String clientSecret;
@Value("oauth.redirectUri")
@Value("oauth.redirect-uri")
private String redirectUri;
@Value("oauth.url")

View File

@ -0,0 +1,27 @@
package com.lktx.center.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hserver.core.ioc.annotation.Autowired;
import cn.hserver.mvc.annotation.Controller;
import cn.hserver.mvc.annotation.router.GET;
import cn.hserver.mvc.common.JsonResult;
import com.lktx.center.service.AppCenterService;
import java.util.Map;
@Controller("/app-center")
public class AppCenterController {
@Autowired
private AppCenterService appCenterService;
@GET("/list")
@SaCheckLogin
public JsonResult list(){
Map<String, Object> appList = appCenterService.getAppList();
if (appList != null) {
return JsonResult.ok().put("data", appList);
}
return JsonResult.error();
}
}

View File

@ -3,10 +3,9 @@ package com.lktx.center.controller;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hserver.core.ioc.annotation.Autowired;
import cn.hserver.plugin.web.annotation.Controller;
import cn.hserver.plugin.web.annotation.GET;
import cn.hserver.plugin.web.interfaces.HttpResponse;
import cn.hutool.json.JSONUtil;
import cn.hserver.mvc.annotation.Controller;
import cn.hserver.mvc.annotation.router.GET;
import cn.hserver.mvc.response.Response;
import com.lktx.center.config.Data;
import com.lktx.center.config.SsoAuthRequest;
import com.lktx.center.domain.bean.SsoApp;
@ -26,7 +25,7 @@ public class HomeController {
private SsoAuthRequest authRequest;
@GET("/")
public void index(HttpResponse response) {
public void index(Response response) {
if (StpUtil.isLogin()){
try {
SaSession session = StpUtil.getSession();
@ -52,19 +51,4 @@ public class HomeController {
}
@GET("/logout")
public void logout(HttpResponse response) {
if (StpUtil.isLogin()){
//可以全局退出
SaSession session = StpUtil.getSession();
AuthToken authToken = session.get(Data.AuthToken,null);
if (authToken != null){
AuthResponse revoke = authRequest.revoke(authToken);
System.out.println(revoke.getMsg());
}
//子系统退出
StpUtil.logout();
}
response.redirect("/");
}
}

View File

@ -0,0 +1,78 @@
package com.lktx.center.controller;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hserver.core.ioc.annotation.Autowired;
import cn.hserver.mvc.annotation.Controller;
import cn.hserver.mvc.annotation.router.GET;
import cn.hserver.mvc.annotation.router.RequestMapping;
import cn.hserver.mvc.common.JsonResult;
import cn.hserver.mvc.request.Request;
import cn.hserver.mvc.response.Response;
import com.lktx.center.config.Data;
import com.lktx.center.config.SsoAuthRequest;
import com.lktx.center.domain.vo.LoginInfo;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.AuthStateUtils;
@Slf4j
@Controller("/oauth")
public class RestAuthController {
@Autowired
private SsoAuthRequest authRequest;
@RequestMapping("/render")
public void renderAuth(Response response) {
String authorize = authRequest.authorize(AuthStateUtils.createState());
response.redirect(authorize);
}
@RequestMapping("/callback")
public JsonResult login(AuthCallback callback, Request request) {
try {
String rawData = request.getRawData();
System.out.println(rawData);
AuthResponse<AuthUser> login = authRequest.login(callback);
AuthUser data = login.getData();
AuthToken token = login.getData().getToken();
StpUtil.login(login.getData().getUuid());
SaSession session = StpUtil.getSession();
session.set(Data.AuthToken, token);
LoginInfo build = LoginInfo.builder()
.userId(data.getUuid())
.avatar(data.getAvatar())
.username(data.getUsername())
.nickname(data.getNickname())
.token(StpUtil.getTokenInfo().tokenValue)
.build();
return JsonResult.ok().put("data", build);
}catch (Exception e) {
log.error("login error",e);
}
return JsonResult.error();
}
@GET("/logout")
public JsonResult logout() {
System.out.println(StpUtil.getSession().getId());
if (StpUtil.isLogin()){
SaSession session = StpUtil.getSession();
AuthToken authToken = session.get(Data.AuthToken,null);
if (authToken != null){
AuthResponse<Void> revoke = authRequest.revoke(authToken);
System.out.println(revoke.getMsg());
}
//子系统退出
StpUtil.logout();
}
return JsonResult.ok();
}
}

View File

@ -0,0 +1,14 @@
package com.lktx.center.domain.vo;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class LoginInfo {
private String userId;
private String nickname;
private String username;
private String avatar;
private String token;
}

View File

@ -0,0 +1,23 @@
package com.lktx.center.filter;
import cn.hserver.core.ioc.annotation.Bean;
import cn.hserver.core.ioc.annotation.Component;
import cn.hserver.core.ioc.annotation.Order;
import cn.hserver.mvc.constants.HttpMethod;
import cn.hserver.mvc.context.WebContext;
import cn.hserver.mvc.filter.FilterAdapter;
@Component
@Order(1)
public class CorsFilter implements FilterAdapter {
@Override
public void doFilter(WebContext webkit) throws Exception {
webkit.response.addHeader("Access-Control-Allow-Origin", "*");
webkit.response.addHeader("Access-Control-Allow-Methods", "*");
webkit.response.addHeader("Access-Control-Allow-Credentials", "*");
webkit.response.addHeader("Access-Control-Allow-Headers", "*");
if (webkit.request.getRequestMethod()== HttpMethod.OPTIONS) {
webkit.response.sendHtml("");
}
}
}

View File

@ -0,0 +1,57 @@
package com.lktx.center.service;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hserver.core.ioc.annotation.Autowired;
import cn.hserver.core.ioc.annotation.Component;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.lktx.center.config.Data;
import com.lktx.center.config.SsoAuthRequest;
import com.lktx.center.domain.bean.SsoApp;
import com.lktx.center.domain.vo.SsoUserAppVO;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
public class AppCenterService {
@Autowired
private SsoAuthRequest ssoAuthRequest;
private final Cache<String, Map<String,Object>> expiringCache = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES) // 写入后30分钟过期
.maximumSize(1)
.build();
public Map<String,Object> getAppList(){
Map<String, Object> appList = expiringCache.getIfPresent("appList");
if(appList != null){
return appList;
}
SaSession session = StpUtil.getSession();
AuthToken authToken = session.get(Data.AuthToken,null);
if (authToken != null){
AuthResponse<SsoUserAppVO> center = ssoAuthRequest.center(authToken);
if (center.ok()) {
Map<String, Object> data = Map.of(
"user", center.getData().getSsoUser(),
"appList", center.getData().getSsoAppList(),
"appGroup", center.getData().getSsoAppList().stream().map(SsoApp::getSsoAppGroup).collect(Collectors.toSet())
);
expiringCache.put("appList", data);
return data;
}
}
return null;
}
}

View File

@ -1,5 +1,5 @@
oauth:
client-id: 65013a3d89d14fab8ff3eb2c0f3981a3
client-secret: 22b5ce70d67f41b79b27cbedb57c976a
redirect-uri: http://127.0.0.1:8981/oauth/callback
url: http://192.168.0.206:8911/21/
redirect-uri: http://127.0.0.1:5173/login
url: http://127.0.0.1:8888/21/

63
pom.xml
View File

@ -7,10 +7,15 @@
<groupId>com.lktx.center</groupId>
<artifactId>app-center</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>api</module>
<module>web</module>
</modules>
<parent>
<artifactId>hserver-parent</artifactId>
<groupId>cn.hserver</groupId>
<version>3.7.0</version>
<version>4.0.0-beta.5</version>
</parent>
<properties>
@ -20,62 +25,6 @@
</properties>
<dependencies>
<!-- 核心依赖-->
<dependency>
<artifactId>hserver</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<!-- web框架 -->
<dependency>
<artifactId>hserver-plugin-web</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<dependency>
<artifactId>hserver-plugin-forest</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<dependency>
<artifactId>hserver-plugin-satoken</artifactId>
<groupId>cn.hserver</groupId>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.16.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>hserver-plugin-maven</artifactId>
<groupId>cn.hserver</groupId>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,12 +0,0 @@
package com.lktx.center;
import cn.hserver.HServerApplication;
import cn.hserver.core.ioc.annotation.HServerBoot;
@HServerBoot
public class Main {
public static void main(String[] args) {
HServerApplication.run(Main.class,8981, args);
}
}

View File

@ -1,42 +0,0 @@
package com.lktx.center.controller;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hserver.core.ioc.annotation.Autowired;
import cn.hserver.plugin.web.annotation.Controller;
import cn.hserver.plugin.web.annotation.RequestMapping;
import cn.hserver.plugin.web.interfaces.HttpResponse;
import com.lktx.center.config.Data;
import com.lktx.center.config.SsoAuthRequest;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.AuthStateUtils;
@Controller("/oauth")
public class RestAuthController {
@Autowired
private SsoAuthRequest authRequest;
@RequestMapping("/render")
public void renderAuth(HttpResponse response) {
String authorize = authRequest.authorize(AuthStateUtils.createState());
response.redirect(authorize);
}
@RequestMapping("/callback")
public void login(AuthCallback callback,HttpResponse response) {
try {
AuthResponse<AuthUser> login = authRequest.login(callback);
AuthToken token = login.getData().getToken();
StpUtil.login(login.getData().getUuid());
SaSession session = StpUtil.getSession();
session.set(Data.AuthToken, token);
response.redirect("/");
}catch (Exception e) {
response.redirect("/");
}
}
}

24
web/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

24
web/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "app-center",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.11.0",
"element-plus": "^2.10.4",
"font-awesome": "^4.7.0",
"vue": "^3.5.17",
"vue-router": "4",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"vite": "^7.0.4"
}
}

20
web/pom.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lktx.center</groupId>
<artifactId>app-center</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>web</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

1
web/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

8
web/src/App.vue Normal file
View File

@ -0,0 +1,8 @@
<script setup>
</script>
<template>
<router-view />
</template>
<style scoped>
</style>

8
web/src/api/appcenter.js Normal file
View File

@ -0,0 +1,8 @@
import http from '../data/http'
export function appCenterList() {
return http({
url: '/app-center/list',
method: 'get',
})
}

15
web/src/api/login.js Normal file
View File

@ -0,0 +1,15 @@
import http from '../data/http'
export function login(data) {
return http({
url: '/oauth/callback',
method: 'post',
data
})
}
export function logout() {
return http({
url: '/oauth/logout',
method: 'get',
})
}

1
web/src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

4
web/src/data/host.js Normal file
View File

@ -0,0 +1,4 @@
//API地址
export const host = 'http://127.0.0.1:8981'
//登录回调调整地址
export const login = 'http://127.0.0.1:8981/oauth/render'

67
web/src/data/http.js Normal file
View File

@ -0,0 +1,67 @@
import axios from 'axios'
import userInfo from './userInfo.js'
import {host} from './host.js'
// create an axios instance
const service = axios.create({
// baseURL: "http://127.0.0.1:9090", // url = base url + request url
// baseURL: "http://xxx.com", // url = base url + request url
baseURL: host, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 500000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (userInfo.getUserInfo()) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['satoken'] = userInfo.getUserInfo().token
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 200) {
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === -2) {
location.href = "/login"
return null
}
return res
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default service

16
web/src/data/userInfo.js Normal file
View File

@ -0,0 +1,16 @@
export default {
KEY: "USER_INFO",
setUserInfo(userInfo) {
localStorage.setItem(this.KEY, JSON.stringify(userInfo))
},
getUserInfo() {
try {
return JSON.parse(localStorage.getItem(this.KEY))
} catch (e) {
return null
}
},
removeUserInfo(){
localStorage.removeItem(this.KEY)
}
}

8
web/src/main.js Normal file
View File

@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index.js'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'font-awesome/css/font-awesome.min.css'
createApp(App).use(router).use(ElementPlus).mount('#app')

36
web/src/router/index.js Normal file
View File

@ -0,0 +1,36 @@
import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
import index from '../views/index.vue'
import login from '../views/login.vue'
import home from '../views/home.vue'
import appcenter from '../views/appcenter.vue'
import news from '../views/news.vue'
import announcement from '../views/announcement.vue'
import staffstyle from '../views/staffstyle.vue'
import chat from '../views/chat.vue'
import suggestionbox from '../views/suggestionbox.vue'
const routes = [
{path: '/login', component: login},
{
path: '/', component: index,
children:[
{path: '', redirect: '/home'},
{ path: '/home', component: home },
{ path: '/app-center', component: appcenter },
{ path: '/news', component: news },
{ path: '/announcement', component: announcement },
{ path: '/staff-style', component: staffstyle },
{ path: '/chat', component: chat },
{ path: '/suggestion-box', component: suggestionbox }
]
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router;

12
web/src/style.css Normal file
View File

@ -0,0 +1,12 @@
@import 'tailwindcss';
/*
* 配置教程 https://juejin.cn/post/7480450288421109787
*/
@theme inline {
--color-primary: #085ce6;
--color-secondary: #FFB81C;
--color-dark: #12192C;
--color-light: #F5F7FA;
--color-muted: #6B7280;
--font-display: 'Inter', 'system-ui', 'sans-serif';
}

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElCard, ElTag, ElDivider } from 'element-plus';
//
const announcements = ref([
{
id: 1,
title: '系统维护通知',
content: '为了给大家提供更稳定、高效的服务,本系统将于本周日凌晨 02:00 - 04:00 进行维护,届时系统将暂停服务,请您提前做好相应准备,感谢您的理解与支持!',
date: '2025-07-25',
type: '维护'
},
{
id: 2,
title: '假期安排通知',
content: '根据国家法定节假日安排结合公司实际情况现将今年国庆节放假安排通知如下10 月 1 日至 10 月 7 日放假调休,共 7 天。10 月 8 日星期六、10 月 9 日(星期日)上班。请各位员工提前做好工作安排。',
date: '2025-07-24',
type: '假期'
},
{
id: 3,
title: '新政策发布',
content: '公司新的绩效考核政策已经正式发布,该政策将从下个月开始实施。请各位员工仔细阅读政策文件,如有疑问可随时咨询人力资源部门。',
date: '2025-07-23',
type: '政策'
}
]);
const getTagType = (type: string) => {
switch (type) {
case '维护':
return 'warning';
case '假期':
return 'success';
case '政策':
return 'info';
default:
return 'default';
}
};
</script>
<template>
<div class="max-w-7xl mx-auto p-6">
<h1 class="text-3xl font-bold text-gray-800 mb-8 text-center">企业公告</h1>
<div class="space-y-6">
<ElCard v-for="announcement in announcements" :key="announcement.id" class="shadow-md hover:shadow-xl transition-shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-700">{{ announcement.title }}</h2>
<el-tag :type="getTagType(announcement.type)" size="small">{{ announcement.type }}</el-tag>
</div>
<ElDivider />
<p class="text-gray-600 mb-4">{{ announcement.content }}</p>
<div class="text-right text-sm text-gray-500">发布时间{{ announcement.date }}</div>
</ElCard>
</div>
</div>
</template>
<style scoped>
</style>

510
web/src/views/appcenter.vue Normal file
View File

@ -0,0 +1,510 @@
<template>
<div class="min-h-screen bg-gray-50 p-6">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-800 mb-2">应用中心</h1>
<p class="text-gray-600">发现和使用企业内部的各类应用系统</p>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-8">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="relative w-full md:w-1/3">
<el-input
v-model="searchQuery"
placeholder="搜索应用..."
:prefix-icon="Search"
clearable
></el-input>
</div>
<div class="flex flex-wrap gap-2">
<el-button
v-for="category in categories"
:key="category.id"
:type="activeCategory === category.id ? 'primary' : 'text'"
@click="activeCategory = category.id"
>
{{ category.name }}
</el-button>
<el-button type="text" @click="activeCategory = 'all'">全部</el-button>
</div>
</div>
</div>
<!-- 我的应用 -->
<div class="mb-10">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800">我的应用</h2>
<div class="flex items-center gap-2">
<el-tooltip content="拖拽可调整顺序" placement="top">
<el-button type="text" icon="el-icon-rank" size="small"></el-button>
</el-tooltip>
<el-tooltip content="管理我的应用" placement="top">
<el-button type="text" icon="el-icon-setting" @click="showManageMyApps = true"></el-button>
</el-tooltip>
</div>
</div>
<!-- 使用draggable实现拖拽排序 -->
<draggable
v-model="myApps"
group="people"
@start="drag=true"
@end="onDragEnd"
class="grid grid-cols-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"
>
<template #item="{element}">
<el-card
class="app-card cursor-pointer hover:shadow-lg transition-shadow duration-300 relative"
@click="openApp(element)"
>
<div class="absolute top-2 left-2 drag-handle cursor-move opacity-50 hover:opacity-100 transition-opacity mt-1">
<el-icon size="16"><Rank /></el-icon>
</div>
<!-- 卡片内容保持不变 -->
<div class="absolute top-2 right-2">
<el-button
type="text"
:icon="Delete"
@click.stop="confirmRemoveFromMyApps(element)"
></el-button>
</div>
<div class="flex flex-col items-center p-4">
<div class="w-14 h-14 rounded-lg bg-primary/10 flex items-center justify-center mb-3">
<el-avatar :src="element.icon" />
</div>
<h3 class="font-medium text-gray-800 mb-1 text-center truncate w-full">{{ element.name }}</h3>
<p class="text-xs text-gray-500 text-center truncate w-full">{{ element.description }}</p>
</div>
</el-card>
</template>
</draggable>
<!-- 没有我的应用时的提示 -->
<div
v-if="myApps.length === 0 && !searchQuery.trim()"
class="col-span-full flex flex-col items-center justify-center py-10 text-gray-500"
>
<el-icon class="text-4xl mb-3"><Collection /></el-icon>
<p>您还没有添加任何应用到"我的应用"</p>
<el-button type="text" size="small" @click="showManageMyApps = true">添加应用</el-button>
</div>
<!-- 搜索无结果提示 -->
<div
v-if="filteredMyApps.length === 0 && searchQuery.trim()"
class="col-span-full flex flex-col items-center justify-center py-10 text-gray-500"
>
<el-icon class="text-4xl mb-3"><Search /></el-icon>
<p>没有找到匹配的应用</p>
</div>
</div>
<!-- 全部应用 -->
<div>
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800">全部应用</h2>
<span class="text-sm text-gray-500">{{ filteredAllApps.length }} 个应用</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<el-card
v-for="app in filteredAllApps"
:key="app.id"
class="app-card cursor-pointer hover:shadow-lg transition-shadow duration-300"
@click="openApp(app)"
>
<div class="flex flex-col items-center p-4">
<div class="w-14 h-14 rounded-lg bg-primary/10 flex items-center justify-center mb-3">
<el-avatar :src="app.icon" />
</div>
<h3 class="font-medium text-gray-800 mb-1 text-center truncate w-full">{{ app.name }}</h3>
<p class="text-xs text-gray-500 text-center truncate w-full">{{ app.description }}</p>
<!-- 添加到我的应用按钮 -->
<el-button
v-if="!isAppInMyApps(app.id)"
type="text"
size="mini"
icon="el-icon-plus"
class="mt-2"
@click.stop="addToMyApps(app)"
>
添加
</el-button>
</div>
</el-card>
<!-- 没有应用时的提示 -->
<div
v-if="filteredAllApps.length === 0"
class="col-span-full flex flex-col items-center justify-center py-10 text-gray-500"
>
<el-icon class="text-4xl mb-3"><Search /></el-icon>
<p>没有找到匹配的应用</p>
</div>
</div>
</div>
<!-- 管理我的应用对话框 -->
<el-dialog
title="管理我的应用"
:visible.sync="showManageMyApps"
width="40%"
:before-close="handleCloseManageMyApps"
>
<template #content>
<div class="p-4">
<p class="text-sm text-gray-600 mb-4">从下方选择您常用的应用添加到"我的应用"</p>
<el-input
v-model="manageSearchQuery"
placeholder="搜索应用..."
:prefix-icon="Search"
clearable
class="mb-4"
>
</el-input>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<el-checkbox-group v-model="selectedApps">
<el-checkbox
v-for="app in filteredManageApps"
:key="app.id"
:label="app.id"
class="flex items-center"
>
<el-avatar :src="app.icon" />
<span class="text-sm">{{ app.name }}</span>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
<template #footer>
<el-button @click="showManageMyApps = false">取消</el-button>
<el-button type="primary" @click="saveMyApps">保存</el-button>
</template>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog
title="确认删除"
:model-value="showDeleteConfirm"
width="30%"
>
<p>确定要从"我的应用"中移除 <span class="font-medium">{{ deletingAppName }}</span> </p>
<template #footer>
<el-button @click="showDeleteConfirm = false">取消</el-button>
<el-button type="danger" @click="confirmDelete">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import {appCenterList} from '../api/appcenter'
import draggable from 'vuedraggable'
import {
Collection,
Search,
Delete,
Remove,
Rank,
//
Monitor,
Document,
Setting,
User,
Message,
Calendar,
PieChart,
Download,
Upload,
Folder,
Clock,
Bell,
Lock,
Share,
Star
} from '@element-plus/icons-vue'
import { ElMessage, ElDialog } from 'element-plus'
//
const categories = ref([
])
//
interface App {
id: number
name: string
description: string
icon: any
category: string
url: string
}
const drag=ref(false)
//
const allApps = ref<App[]>([
])
// ID
const myAppIds = ref<number[]>([])
//
const searchQuery = ref('')
const activeCategory = ref('all')
const showManageMyApps = ref(false)
const manageSearchQuery = ref('')
const selectedApps = ref<number[]>([])
//
const showDeleteConfirm = ref(false)
const deletingAppId = ref<number | null>(null)
const deletingAppName = ref('')
//
const myApps = computed<App[]>({
get() {
// myAppIds
return myAppIds.value.map(id => allApps.value.find(app => app.id === id)).filter(Boolean) as App[]
},
set(newValue) {
// myAppIds
myAppIds.value = newValue.map(app => app.id)
}
})
//
const filteredMyApps = computed(() => {
if (!searchQuery.value.trim()) return myApps.value
const query = searchQuery.value.toLowerCase().trim()
return myApps.value.filter(app =>
app.name.toLowerCase().includes(query) ||
app.description.toLowerCase().includes(query)
)
})
//
const filteredAllApps = computed(() => {
let apps = allApps.value
//
if (activeCategory.value !== 'all') {
apps = apps.filter(app => app.category === activeCategory.value)
}
//
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase().trim()
apps = apps.filter(app =>
app.name.toLowerCase().includes(query) ||
app.description.toLowerCase().includes(query)
)
}
return apps
})
//
const filteredManageApps = computed(() => {
if (!manageSearchQuery.value.trim()) return allApps.value
const query = manageSearchQuery.value.toLowerCase().trim()
return allApps.value.filter(app =>
app.name.toLowerCase().includes(query) ||
app.description.toLowerCase().includes(query)
)
})
//
const isAppInMyApps = (appId: number) => {
return myAppIds.value.includes(appId)
}
//
const addToMyApps = (app: App) => {
if (!isAppInMyApps(app.id)) {
myAppIds.value.push(app.id)
saveMyAppsToLocalStorage()
//
ElMessage({
message: `已将"${app.name}"添加到我的应用`,
type: 'success'
})
}
}
//
const confirmRemoveFromMyApps = (app: App) => {
console.log(app)
deletingAppId.value = app.id
deletingAppName.value = app.name
showDeleteConfirm.value = true
}
//
const confirmDelete = () => {
if (deletingAppId.value) {
myAppIds.value = myAppIds.value.filter(id => id !== deletingAppId.value)
saveMyAppsToLocalStorage()
//
ElMessage({
message: `已从我的应用中移除"${deletingAppName.value}"`,
type: 'info'
})
//
showDeleteConfirm.value = false
deletingAppId.value = null
deletingAppName.value = ''
}
}
//
const openApp = (app: App) => {
//
console.log(`Opening app: ${app.name} (${app.url})`)
//
ElMessage({
message: `正在打开"${app.name}"`,
type: 'info'
})
}
//
const saveMyApps = () => {
myAppIds.value = [...selectedApps.value]
saveMyAppsToLocalStorage()
showManageMyApps.value = false
//
ElMessage({
message: '我的应用设置已保存',
type: 'success'
})
}
//
const handleCloseManageMyApps = (done: () => void) => {
//
selectedApps.value = [...myAppIds.value]
done()
}
//
const onDragEnd = () => {
drag.value=false
//
saveMyAppsToLocalStorage()
ElMessage({
message: '应用顺序已更新',
type: 'success',
duration: 1000
})
}
//
const saveMyAppsToLocalStorage = () => {
localStorage.setItem('myApps', JSON.stringify(myAppIds.value))
}
//
const loadMyAppsFromLocalStorage = () => {
const savedMyApps = localStorage.getItem('myApps')
if (savedMyApps) {
try {
myAppIds.value = JSON.parse(savedMyApps)
} catch (e) {
console.error('Failed to parse saved my apps', e)
// 使
myAppIds.value = [1, 2, 3, 4]
}
} else {
//
myAppIds.value = [1, 2, 3, 4]
}
//
selectedApps.value = [...myAppIds.value]
}
const loadAppList = () => {
appCenterList().then(res=>{
if (res.code===200){
categories.value = res.data.appGroup.map(item=>({
id: item.ssoAppGroupId,
name: item.name
}))
allApps.value = res.data.appList.map(item=>({
id: item.ssoAppId,
name: item.appName,
description: item.remark,
icon: item.appIcon,
category: item.ssoAppGroupId,
url: item.url
}))
console.log(res)
// allApps.value = res.data
}
})
}
//
onMounted(() => {
loadAppList()
loadMyAppsFromLocalStorage()
})
</script>
<style scoped>
.app-card {
border-radius: 10px;
transition: all 0.3s ease;
}
.app-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* 拖拽相关样式 */
.drag-handle {
transition: all 0.2s;
}
.drag-handle:hover {
color: #409eff;
}
/* 隐藏拖拽过程中的默认高亮样式 */
:deep(.ghost) {
opacity: 0.5;
}
:deep(.dragging) {
opacity: 0.8;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
</style>

417
web/src/views/chat.vue Normal file
View File

@ -0,0 +1,417 @@
<script setup lang="ts">
</script>
<template>
<div class="font-inter bg-gray-100 text-dark flex overflow-hidden h-full">
<!-- 左侧聊天列表 -->
<div class="w-80 bg-white border-r border-gray-200 flex flex-col h-full shadow-sm z-10">
<!-- 头部 -->
<div class="p-4 border-b border-gray-200">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-bold text-primary">企业沟通</h1>
<button class="p-2 rounded-full hover:bg-gray-100 transition-colors">
<i class="fa fa-plus text-gray-600"></i>
</button>
</div>
<!-- 搜索框 -->
<div class="relative">
<input type="text" placeholder="搜索聊天..." class="w-full py-2 pl-10 pr-4 rounded-lg bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary/30 transition-all">
<i class="fa fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
<!-- 聊天列表导航 -->
<div class="flex border-b border-gray-200">
<button class="flex-1 py-3 text-primary border-b-2 border-primary font-medium">
最近聊天
</button>
<button class="flex-1 py-3 text-gray-500 hover:text-gray-700 transition-colors">
群聊
</button>
</div>
<!-- 聊天列表内容 -->
<div class="flex-1 overflow-y-auto scrollbar-hide">
<!-- 当前选中的聊天 -->
<div class="flex items-center p-3 bg-primary/5 border-l-4 border-primary cursor-pointer">
<img src="https://picsum.photos/id/1005/200/200" alt="研发部群聊" class="w-12 h-12 rounded-full object-cover">
<div class="ml-3 flex-1 min-w-0">
<div class="flex justify-between items-center">
<h3 class="text-sm font-semibold text-gray-900 truncate">研发部群聊</h3>
<span class="text-xs text-gray-500">14:32</span>
</div>
<p class="text-sm text-gray-600 truncate mt-1">
<span class="font-medium"></span>这个需求我已经完成了
</p>
</div>
</div>
<!-- 未读消息聊天 -->
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer transition-colors">
<img src="https://picsum.photos/id/1012/200/200" alt="张经理" class="w-12 h-12 rounded-full object-cover">
<div class="ml-3 flex-1 min-w-0">
<div class="flex justify-between items-center">
<h3 class="text-sm font-semibold text-gray-900 truncate">张经理</h3>
<span class="text-xs text-gray-500">13:45</span>
</div>
<p class="text-sm text-gray-600 truncate mt-1">
下周的项目评审会议需要提前准备
</p>
<div class="absolute right-4 top-4 w-5 h-5 bg-primary rounded-full flex items-center justify-center text-white text-xs font-medium">
2
</div>
</div>
</div>
<!-- 普通聊天 -->
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer transition-colors">
<img src="https://picsum.photos/id/1027/200/200" alt="产品组" class="w-12 h-12 rounded-full object-cover">
<div class="ml-3 flex-1 min-w-0">
<div class="flex justify-between items-center">
<h3 class="text-sm font-semibold text-gray-900 truncate">产品组</h3>
<span class="text-xs text-gray-500">昨天</span>
</div>
<p class="text-sm text-gray-500 truncate mt-1">
李华新功能原型已经上传到共享文件夹
</p>
</div>
</div>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer transition-colors">
<img src="https://picsum.photos/id/1025/200/200" alt="王工程师" class="w-12 h-12 rounded-full object-cover">
<div class="ml-3 flex-1 min-w-0">
<div class="flex justify-between items-center">
<h3 class="text-sm font-semibold text-gray-900 truncate">王工程师</h3>
<span class="text-xs text-gray-500">昨天</span>
</div>
<p class="text-sm text-gray-500 truncate mt-1">
数据库优化方案我已经发你邮箱了
</p>
</div>
</div>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer transition-colors">
<img src="https://picsum.photos/id/1066/200/200" alt="市场部" class="w-12 h-12 rounded-full object-cover">
<div class="ml-3 flex-1 min-w-0">
<div class="flex justify-between items-center">
<h3 class="text-sm font-semibold text-gray-900 truncate">市场部</h3>
<span class="text-xs text-gray-500">周一</span>
</div>
<p class="text-sm text-gray-500 truncate mt-1">
张敏下个月的市场活动计划已更新
</p>
</div>
</div>
</div>
</div>
<!-- 中间聊天界面 -->
<div class="flex-1 flex flex-col bg-gray-50 h-full overflow-hidden">
<!-- 聊天头部 -->
<div class="bg-white border-b border-gray-200 p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center">
<img src="https://picsum.photos/id/1005/200/200" alt="研发部群聊" class="w-10 h-10 rounded-full object-cover">
<div class="ml-3">
<h2 class="font-semibold">研发部群聊</h2>
<p class="text-xs text-gray-500">12名成员8人在线</p>
</div>
</div>
<div class="flex space-x-2">
<button class="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600">
<i class="fa fa-search"></i>
</button>
<button class="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600">
<i class="fa fa-phone"></i>
</button>
<button class="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600">
<i class="fa fa-video-camera"></i>
</button>
<button class="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600">
<i class="fa fa-ellipsis-v"></i>
</button>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="flex-1 overflow-y-auto p-6 space-y-6 scrollbar-hide" id="chat-messages">
<!-- 日期分隔线 -->
<div class="flex justify-center">
<span class="text-xs bg-gray-200 text-gray-500 px-3 py-1 rounded-full">今天</span>
</div>
<!-- 他人消息 -->
<div class="flex items-start">
<img src="https://picsum.photos/id/1025/200/200" alt="王工程师" class="w-8 h-8 rounded-full object-cover">
<div class="ml-2 max-w-[80%]">
<div class="flex items-center mb-1">
<span class="text-xs font-medium text-gray-700">王工程师</span>
<span class="text-xs text-gray-400 ml-2">09:32</span>
</div>
<div class="bg-white p-3 rounded-lg message-bubble-left shadow-sm">
<p>大家上午好昨天部署的新版本运行情况如何有没有发现什么问题</p>
</div>
</div>
</div>
<!-- 他人消息 - 图片 -->
<div class="flex items-start">
<img src="https://picsum.photos/id/1012/200/200" alt="张经理" class="w-8 h-8 rounded-full object-cover">
<div class="ml-2 max-w-[80%]">
<div class="flex items-center mb-1">
<span class="text-xs font-medium text-gray-700">张经理</span>
<span class="text-xs text-gray-400 ml-2">10:15</span>
</div>
<div class="bg-white p-3 rounded-lg message-bubble-left shadow-sm">
<p>我这边发现一个界面显示问题主要在IE浏览器上</p>
<div class="mt-2 rounded overflow-hidden border border-gray-100">
<img src="https://picsum.photos/id/0/400/200" alt="问题截图" class="w-full h-auto hover:opacity-90 transition-opacity cursor-pointer">
</div>
</div>
</div>
</div>
<!-- 他人消息 - 文件 -->
<div class="flex items-start">
<img src="https://picsum.photos/id/1066/200/200" alt="李华" class="w-8 h-8 rounded-full object-cover">
<div class="ml-2 max-w-[80%]">
<div class="flex items-center mb-1">
<span class="text-xs font-medium text-gray-700">李华</span>
<span class="text-xs text-gray-400 ml-2">11:45</span>
</div>
<div class="bg-white p-3 rounded-lg message-bubble-left shadow-sm">
<p>这是修复方案的文档请查收</p>
<div class="mt-2 flex items-center p-2 bg-gray-50 rounded">
<div class="w-10 h-10 bg-primary/10 rounded flex items-center justify-center text-primary">
<i class="fa fa-file-pdf-o text-xl"></i>
</div>
<div class="ml-3 flex-1">
<p class="text-sm font-medium text-gray-800 truncate">前端兼容性修复方案.pdf</p>
<p class="text-xs text-gray-500">2.4 MB</p>
</div>
<button class="text-primary hover:text-primary/80 transition-colors">
<i class="fa fa-download"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 自己的消息 -->
<div class="flex items-start justify-end">
<div class="mr-2 max-w-[80%]">
<div class="flex items-center justify-end mb-1">
<span class="text-xs text-gray-400">13:20</span>
</div>
<div class="bg-primary text-white p-3 rounded-lg message-bubble-right shadow-sm">
<p>我看了一下这个问题是由于IE对flex布局的支持不完善导致的我会尽快修复</p>
</div>
</div>
<img src="https://picsum.photos/id/1001/200/200" alt="我" class="w-8 h-8 rounded-full object-cover">
</div>
<!-- 自己的消息 - 表情包 -->
<div class="flex items-start justify-end">
<div class="mr-2 max-w-[80%]">
<div class="flex items-center justify-end mb-1">
<span class="text-xs text-gray-400">13:22</span>
</div>
<div class="bg-primary text-white p-3 rounded-lg message-bubble-right shadow-sm">
<p>收到文档了谢谢👍👍</p>
</div>
</div>
<img src="https://picsum.photos/id/1001/200/200" alt="我" class="w-8 h-8 rounded-full object-cover">
</div>
<!-- 他人消息 -->
<div class="flex items-start">
<img src="https://picsum.photos/id/1025/200/200" alt="王工程师" class="w-8 h-8 rounded-full object-cover">
<div class="ml-2 max-w-[80%]">
<div class="flex items-center mb-1">
<span class="text-xs font-medium text-gray-700">王工程师</span>
<span class="text-xs text-gray-400 ml-2">14:30</span>
</div>
<div class="bg-white p-3 rounded-lg message-bubble-left shadow-sm">
<p>数据库性能监控系统已经部署完成大家可以通过内部地址访问查看实时数据</p>
</div>
</div>
</div>
<!-- 自己的消息 -->
<div class="flex items-start justify-end">
<div class="mr-2 max-w-[80%]">
<div class="flex items-center justify-end mb-1">
<span class="text-xs text-gray-400">14:32</span>
</div>
<div class="bg-primary text-white p-3 rounded-lg message-bubble-right shadow-sm">
<p>这个需求我已经完成了</p>
</div>
</div>
<img src="https://picsum.photos/id/1001/200/200" alt="我" class="w-8 h-8 rounded-full object-cover">
</div>
</div>
<!-- 输入区域 -->
<div class="bg-white border-t border-gray-200 p-3">
<div class="flex items-center mb-2 space-x-1">
<button class="p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="表情">
<i class="fa fa-smile-o text-lg"></i>
</button>
<button class="p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="图片">
<i class="fa fa-picture-o text-lg"></i>
</button>
<button class="p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="文件">
<i class="fa fa-paperclip text-lg"></i>
</button>
<button class="p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="视频">
<i class="fa fa-video-camera text-lg"></i>
</button>
<button class="p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="截图">
<i class="fa fa-desktop text-lg"></i>
</button>
<div class="flex-1"></div>
<button class="p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="更多选项">
<i class="fa fa-ellipsis-h text-lg"></i>
</button>
</div>
<div class="flex">
<textarea placeholder="输入消息..." class="flex-1 border border-gray-200 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary p-3 resize-none transition-all" rows="3"></textarea>
<button class="bg-primary hover:bg-primary/90 text-white px-6 rounded-r-lg transition-colors flex items-center">
<span>发送</span>
<i class="fa fa-paper-plane ml-2"></i>
</button>
</div>
</div>
</div>
<!-- 右侧在线人员 -->
<div class="w-72 bg-white border-l border-gray-200 flex flex-col h-full shadow-sm hidden lg:block">
<!-- 头部 -->
<div class="p-4 border-b border-gray-200">
<h2 class="font-semibold">在线成员 (8)</h2>
</div>
<!-- 搜索 -->
<div class="p-3 border-b border-gray-200">
<div class="relative">
<input type="text" placeholder="搜索成员..." class="w-full py-2 pl-10 pr-4 rounded-lg bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary/30 transition-all text-sm">
<i class="fa fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
<!-- 分类 -->
<div class="p-2 border-b border-gray-200">
<button class="text-sm text-primary font-medium">全部</button>
<button class="text-sm text-gray-500 ml-3 hover:text-gray-700 transition-colors">部门</button>
<button class="text-sm text-gray-500 ml-3 hover:text-gray-700 transition-colors">角色</button>
</div>
<!-- 成员列表 -->
<div class="flex-1 overflow-y-auto scrollbar-hide p-2">
<!-- 自己 -->
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1001/200/200" alt="自己" class="w-10 h-10 rounded-full object-cover">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-success border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900"> (自己)</h3>
<p class="text-xs text-gray-500">前端开发</p>
</div>
</div>
<!-- 在线成员 -->
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1012/200/200" alt="张经理" class="w-10 h-10 rounded-full object-cover">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-success border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">张经理</h3>
<p class="text-xs text-gray-500">研发经理</p>
</div>
</div>
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1025/200/200" alt="王工程师" class="w-10 h-10 rounded-full object-cover">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-success border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">王工程师</h3>
<p class="text-xs text-gray-500">后端开发</p>
</div>
</div>
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1066/200/200" alt="李华" class="w-10 h-10 rounded-full object-cover">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-success border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">李华</h3>
<p class="text-xs text-gray-500">前端开发</p>
</div>
</div>
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1027/200/200" alt="赵设计师" class="w-10 h-10 rounded-full object-cover">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-success border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">赵设计师</h3>
<p class="text-xs text-gray-500">UI/UX设计</p>
</div>
</div>
<!-- 离开状态 -->
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1074/200/200" alt="陈测试" class="w-10 h-10 rounded-full object-cover">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-warning border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">陈测试</h3>
<p class="text-xs text-gray-500">测试工程师</p>
</div>
</div>
<!-- 离线成员 -->
<div class="mt-4">
<h3 class="text-xs text-gray-500 uppercase font-medium px-2 mb-2">离线成员 (4)</h3>
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1083/200/200" alt="孙产品" class="w-10 h-10 rounded-full object-cover opacity-70">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-gray-300 border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-600">孙产品</h3>
<p class="text-xs text-gray-400">产品经理</p>
</div>
</div>
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
<div class="relative">
<img src="https://picsum.photos/id/1076/200/200" alt="周运维" class="w-10 h-10 rounded-full object-cover opacity-70">
<span class="absolute bottom-0 right-0 w-3 h-3 bg-gray-300 border-2 border-white rounded-full"></span>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-600">周运维</h3>
<p class="text-xs text-gray-400">运维工程师</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

70
web/src/views/home.vue Normal file
View File

@ -0,0 +1,70 @@
<script setup lang="ts">
</script>
<template>
<div class="max-w-7xl mx-auto">
<h1 class="text-2xl font-bold text-gray-800 mb-6">欢迎使用企业内网门户</h1>
<!-- 页面内容示例 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">系统公告</h2>
<el-alert
title="系统升级通知"
type="info"
description="本系统将于本周六凌晨2点进行例行维护预计维护时间为2小时。维护期间系统将暂停服务请提前做好工作安排。"
show-icon
:closable="false"
></el-alert>
</div>
<!-- 统计卡片示例 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<el-card class="bg-white rounded-lg shadow-md p-4">
<div slot="header" class="flex justify-between items-center">
<span>项目统计</span>
<el-button icon="el-icon-refresh" circle size="small" type="text"></el-button>
</div>
<div class="flex items-center justify-center">
<el-progress :percentage="75" type="circle" :width="120"></el-progress>
</div>
<div class="text-center mt-2">
<p class="text-gray-500 text-sm">进行中项目</p>
<p class="text-blue-600 text-2xl font-bold">12/16</p>
</div>
</el-card>
<el-card class="bg-white rounded-lg shadow-md p-4">
<div slot="header" class="flex justify-between items-center">
<span>待办事项</span>
<el-button icon="el-icon-refresh" circle size="small" type="text"></el-button>
</div>
<div class="flex items-center justify-center">
<el-progress :percentage="40" type="circle" :width="120" color="#f56c6c"></el-progress>
</div>
<div class="text-center mt-2">
<p class="text-gray-500 text-sm">今日待办</p>
<p class="text-red-500 text-2xl font-bold">6/15</p>
</div>
</el-card>
<el-card class="bg-white rounded-lg shadow-md p-4">
<div slot="header" class="flex justify-between items-center">
<span>通知消息</span>
<el-button icon="el-icon-refresh" circle size="small" type="text"></el-button>
</div>
<div class="flex items-center justify-center">
<el-progress :percentage="20" type="circle" :width="120" color="#e6a23c"></el-progress>
</div>
<div class="text-center mt-2">
<p class="text-gray-500 text-sm">未读消息</p>
<p class="text-orange-500 text-2xl font-bold">3/15</p>
</div>
</el-card>
</div>
</div>
</template>
<style scoped>
</style>

13
web/src/views/index.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import Layout from "./menu/index.vue";
</script>
<template>
<layout></layout>
</template>
<style scoped>
</style>

128
web/src/views/login.vue Normal file
View File

@ -0,0 +1,128 @@
<script setup>
import {onMounted, ref} from "vue";
import { useRoute } from 'vue-router'
import {login as loginUrl} from "../data/host.js";
import {login} from "../api/login.js";
import userInfo from "../data/userInfo.js";
const route = useRoute()
const loginState = ref(0)
const setBar=(progress)=>{
const progressBar = document.getElementById('progress-bar');
progressBar.style.width = `${progress}%`;
}
const jumpLogin = () => {
location.href=loginUrl
}
const handlerLogin = () => {
const code= route.query.code
const state= route.query.state
if (code&&state) {
setBar(50)
login({code,state}).then(res=>{
console.log(res)
if (res.code===200){
setBar(100)
loginState.value=1
userInfo.setUserInfo(res.data)
location.href="/"
}else {
setBar(0)
loginState.value=2
}
})
}else {
jumpLogin()
}
}
onMounted(()=>{
handlerLogin()
})
</script>
<template>
<div class="font-inter bg-gray-50 min-h-screen flex items-center justify-center p-4">
<div class="max-w-md w-full bg-white rounded-xl shadow-lg overflow-hidden animate-fade-in">
<!-- 头部区域 -->
<div class="bg-primary p-6 text-white text-center">
<div class="flex items-center justify-center">
<i class="fa fa-sign-in text-4xl mr-3"></i>
<h1 class="text-2xl font-bold">企业登录系统</h1>
</div>
<p class="mt-2 text-primary-100 opacity-90">正在验证您的身份请稍候...</p>
</div>
<!-- 内容区域 -->
<div class="p-8">
<!-- 状态指示器 -->
<div id="status-container" class="flex flex-col items-center">
<!-- 加载状态 -->
<div v-if="loginState===0" class="animate-slide-up">
<div class="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin mx-auto"></div>
<p class="mt-4 text-gray-600 text-center">正在验证授权码...</p>
<p id="loading-details" class="mt-2 text-center text-sm text-gray-500">请不要关闭此页面</p>
</div>
<!-- 成功状态 (默认隐藏) -->
<div v-if="loginState===1" class=" animate-slide-up">
<p class="mt-4 text-gray-600 text-center">验证成功</p>
<p id="success-details" class="mt-2 text-sm text-center text-gray-500">正在跳转至应用...</p>
</div>
<!-- 错误状态 (默认隐藏) -->
<div v-if="loginState===2" class=" animate-slide-up text-center">
<p class="mt-4 text-gray-600 text-center">登录失败</p>
<p id="error-details" class="mt-2 text-sm text-gray-500 text-center">授权码无效或已过期</p>
<button @click="jumpLogin" class=" mt-6 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors">
重试
</button>
</div>
</div>
<!-- 进度条 -->
<div class="mt-8">
<div class="h-2 bg-gray-100 rounded-full overflow-hidden">
<div id="progress-bar" class="h-full bg-primary rounded-full w-0 transition-all duration-300"></div>
</div>
</div>
</div>
<!-- 底部区域 -->
<div class="p-4 bg-gray-50 text-center text-sm text-gray-500">
<p>© 2025 企业内部系统 | 技术支持: it-support@company.com</p>
</div>
</div>
</div>
</template>
<style scoped>
@layer utilities {
.content-auto {
content-visibility: auto;
}
.animate-pulse-slow {
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-fade-in {
animation: fadeIn 0.5s ease-in-out;
}
.animate-slide-up {
animation: slideUp 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
}
</style>

View File

@ -0,0 +1,230 @@
<template>
<div class="flex flex-col h-screen overflow-hidden bg-gray-50">
<!-- 顶部导航栏 -->
<header class="bg-primary text-white shadow-md h-16 flex items-center justify-between px-6">
<!-- 左侧Logo和菜单按钮 -->
<div class="flex items-center">
<div class="flex">
<button @click="isCollapse = !isCollapse" class="mr-4 text-white focus:outline-none">
<el-icon><Fold /></el-icon>
</button>
<div class="flex items-center">
<el-icon><Platform /></el-icon>
<span class="ml-2 text-lg font-semibold hidden md:block">企业内网门户</span>
</div>
</div>
<div class="top-header">
<el-menu
mode="horizontal"
:ellipsis="false"
@open="handleOpen"
router
>
<el-menu-item index="/news">新闻中心</el-menu-item>
<el-menu-item index="/announcement">公告</el-menu-item>
</el-menu>
</div>
</div>
<!-- 右侧用户信息 -->
<div class="flex items-center">
<div class="relative mr-6">
<button class="relative text-white focus:outline-none">
<el-icon><Bell /></el-icon>
<span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">3</span>
</button>
</div>
<div class="flex items-center">
<img :src="userInfoData?.avatar" alt="User Avatar" class="w-8 h-8 rounded-full object-cover">
<el-dropdown trigger="click">
<span class="el-dropdown-link text-white cursor-pointer ml-2">{{ userInfoData?.nickname }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handelLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</header>
<!-- 主内容区 -->
<div class="flex flex-1 overflow-hidden">
<!-- 侧边栏菜单 -->
<aside class="bg-white shadow-md z-10 transition-all duration-300" :style="{ width: isCollapse ? '64px' : '200px' }">
<el-menu
default-active="/home"
:collapse="isCollapse"
@open="handleOpen"
@close="handleClose"
router
>
<el-menu-item index="/home">
<el-icon><IconMenu /></el-icon>
<template #title>首页</template>
</el-menu-item>
<el-menu-item index="/app-center">
<el-icon><Orange /></el-icon>
<template #title>应用中心</template>
</el-menu-item>
<el-menu-item index="/staff-style">
<el-icon><UserFilled /></el-icon>
<template #title>员工风采</template>
</el-menu-item>
<el-menu-item index="/app-center">
<el-icon><StarFilled /></el-icon>
<template #title>企业论坛</template>
</el-menu-item>
<el-menu-item index="/chat">
<el-icon><Comment /></el-icon>
<template #title>企业聊天</template>
</el-menu-item>
<el-menu-item index="/suggestion-box">
<el-icon><QuestionFilled /></el-icon>
<template #title>意见箱</template>
</el-menu-item>
<!-- <el-sub-menu index="2">-->
<!-- <template #title>-->
<!-- <el-icon><Location /></el-icon>-->
<!-- <span>导航一</span>-->
<!-- </template>-->
<!-- <el-menu-item-group>-->
<!-- <template #title><span>分组一</span></template>-->
<!-- <el-menu-item index="1-1">菜单项一</el-menu-item>-->
<!-- <el-menu-item index="1-2">菜单项二</el-menu-item>-->
<!-- </el-menu-item-group>-->
<!-- <el-menu-item-group title="分组二">-->
<!-- <el-menu-item index="1-3">菜单项三</el-menu-item>-->
<!-- </el-menu-item-group>-->
<!-- <el-sub-menu index="1-4">-->
<!-- <template #title><span>菜单项四</span></template>-->
<!-- <el-menu-item index="1-4-1">子菜单项一</el-menu-item>-->
<!-- </el-sub-menu>-->
<!-- </el-sub-menu>-->
<!-- <el-menu-item index="3" disabled>-->
<!-- <el-icon><Document /></el-icon>-->
<!-- <template #title>导航三</template>-->
<!-- </el-menu-item>-->
<!-- <el-menu-item index="4">-->
<!-- <el-icon><Setting /></el-icon>-->
<!-- <template #title>导航四</template>-->
<!-- </el-menu-item>-->
</el-menu>
</aside>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<router-view></router-view>
</main>
</div>
</div>
</template>
<script setup>
import {onMounted, ref} from 'vue'
import {
Document,
Menu as IconMenu,
Location,
Setting,
Switch,
StarFilled,
UserFilled,
Comment,
QuestionFilled,
Orange,
// 使
Fold,
Platform,
Bell,
CaretBottom
} from '@element-plus/icons-vue'
import userInfo from '../../data/userInfo.js'
import {useRouter} from "vue-router";
import { logout } from '../../api/login';
const router=useRouter();
const userInfoData=ref(null);
const isCollapse = ref(false)
const handleOpen = (key, keyPath) => {
console.log(key, keyPath)
}
const handleClose = (key, keyPath) => {
console.log(key, keyPath)
}
onMounted(()=>{
const info=userInfo.getUserInfo()
if (!info){
router.push({path: '/login'})
}else {
userInfoData.value=info
}
})
const handelLogout = () => {
logout().then(res=>{
if (res.code===200){
userInfo.removeUserInfo()
location.reload()
}
})
}
</script>
<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.top-header{
border:none;
:deep(.el-menu-item){
background-color: #085ce6;
color: #fff !important;
border: none !important;
}
:deep(.el-menu-item):hover{
color: #ffffff !important;
background-color: #206ce8 !important;
border: none !important;
}
:deep(.el-menu-item):active{
color: #ffffff !important;
background-color: #085ce6 !important;
border: none !important;
}
:deep(.el-menu-item):focus{
color: #ffffff !important;
background-color: #206ce8 !important;
border: none !important;
}
margin-left: 20px;
}
:deep(.el-menu--horizontal.el-menu ){
border-bottom: none !important;
}
:deep(.el-menu) {
background-color: transparent !important;
}
</style>

52
web/src/views/news.vue Normal file
View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElCard, ElTag } from 'element-plus';
// imageUrl
const newsList = ref([
{
id: 1,
title: '公司年度会议通知',
content: '公司将于下周五召开年度会议,请各位员工提前做好准备。',
date: '2025-07-24',
imageUrl: 'https://picsum.photos/600/400?random=1'
},
{
id: 2,
title: '新产品上线公告',
content: '我们的全新产品已正式上线,欢迎大家体验。',
date: '2025-07-23',
imageUrl: 'https://picsum.photos/600/400?random=2'
},
{
id: 3,
title: '团队建设活动安排',
content: '本周末将组织团队建设活动,具体安排请查看内部通知。',
date: '2025-07-22',
imageUrl: 'https://picsum.photos/600/400?random=3'
},
]);
</script>
<template>
<div class="max-w-7xl mx-auto p-6">
<h1 class="text-2xl font-bold text-gray-800 mb-6 text-center">新闻列表</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ElCard v-for="news in newsList" :key="news.id" class="shadow-md hover:shadow-lg transition-shadow">
<!-- 展示新闻图片 -->
<img :src="news.imageUrl" alt="news image" class="w-full h-48 object-cover rounded-t-md">
<template #header>
<div class="flex justify-between items-center">
<span class="text-xl font-semibold text-gray-700">{{ news.title }}</span>
<ElTag type="info" size="small">{{ news.date }}</ElTag>
</div>
</template>
<p class="text-gray-600 p-4">{{ news.content }}</p>
</ElCard>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElCard, ElTag } from 'element-plus';
//
const staffList = ref([
{
id: 1,
name: '张三',
position: '高级前端开发工程师',
avatar: 'https://picsum.photos/200/200?random=1',
intro: '张三在前端开发领域拥有丰富的经验,擅长 Vue、React 等主流框架,对前端性能优化有深入研究。',
honors: ['优秀员工', '技术创新奖']
},
{
id: 2,
name: '李四',
position: '后端架构师',
avatar: 'https://picsum.photos/200/200?random=2',
intro: '李四专注于后端系统设计与开发,精通 Java、Go 语言,主导过多个大型项目的架构设计。',
honors: ['杰出贡献奖', '最佳团队成员']
},
{
id: 3,
name: '王五',
position: '产品经理',
avatar: 'https://picsum.photos/200/200?random=3',
intro: '王五具备敏锐的市场洞察力和出色的产品规划能力,成功打造过多款用户喜爱的产品。',
honors: ['优秀产品奖', '最佳创意奖']
}
]);
</script>
<template>
<div class="max-w-7xl mx-auto p-6">
<h1 class="text-3xl font-bold text-gray-800 mb-8 text-center">员工风采</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ElCard v-for="staff in staffList" :key="staff.id" class="shadow-md hover:shadow-xl transition-shadow">
<div class="flex flex-col items-center">
<img :src="staff.avatar" alt="staff avatar" class="w-32 h-32 rounded-full object-cover mb-4">
<h2 class="text-xl font-semibold text-gray-700">{{ staff.name }}</h2>
<p class="text-gray-600 text-sm mb-4">{{ staff.position }}</p>
</div>
<ElDivider />
<p class="text-gray-600 mb-4">{{ staff.intro }}</p>
<div class="flex flex-wrap gap-2">
<ElTag v-for="honor in staff.honors" :key="honor" type="success" size="small">
{{ honor }}
</ElTag>
</div>
</ElCard>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,127 @@
<script setup lang="ts">
import { ref } from 'vue';
//
interface FeedbackForm {
department: string;
contact: string;
phone: string;
content: string;
}
const formData = ref<FeedbackForm>({
department: '',
contact: '',
phone: '',
content: ''
});
//
const handleSubmit = () => {
console.log('提交的表单数据:', formData.value);
// axios
};
</script>
<template>
<div class="feedback-container">
<h2 class="title">意见反馈</h2>
<div class="form-wrapper">
<div class="form-item">
<label class="form-label">部门</label>
<input
class="form-input"
v-model="formData.department"
placeholder="请输入部门名称"
/>
</div>
<div class="form-item">
<label class="form-label">联系人</label>
<input
class="form-input"
v-model="formData.contact"
placeholder="请输入联系人姓名"
/>
</div>
<div class="form-item">
<label class="form-label">联系电话</label>
<input
class="form-input"
v-model="formData.phone"
placeholder="请输入联系电话"
type="tel"
/>
</div>
<div class="form-item">
<label class="form-label">需求内容</label>
<textarea
class="form-textarea"
v-model="formData.content"
placeholder="请详细描述需求内容"
></textarea>
</div>
<button class="submit-btn" @click="handleSubmit">提交意见</button>
</div>
</div>
</template>
<style scoped>
.feedback-container {
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.title {
text-align: center;
margin-bottom: 20px;
font-size: 20px;
font-weight: bold;
}
.form-wrapper {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-item {
display: flex;
flex-direction: column;
}
.form-label {
margin-bottom: 6px;
font-size: 14px;
font-weight: 500;
}
.form-input,
.form-textarea {
caret-color: white;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.submit-btn {
align-self: flex-end;
padding: 10px 20px;
background-color: #0078d4;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.submit-btn:hover {
background-color: #005a9e;
}
</style>

12
web/vite.config.js Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss(
),
],
})