feat: 添加HR访问量弹窗和日历事项样式优化
- 新增HR访问详情弹窗组件,支持左右切换查看不同HR信息 - 优化日历事项样式系统,基于事件类型匹配样式配置 - 完善侧边栏HR访问量组件,添加重叠头像和点击交互 - 增加班级排名弹窗组件 - 更新专家支持页面布局和样式 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,8 @@
|
||||
"Bash(kill:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(pgrep:*)",
|
||||
"Bash(npm start)"
|
||||
"Bash(npm start)",
|
||||
"Bash(xargs kill:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-react": "^2.66.5",
|
||||
"@chatui/core": "^3.2.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
||||
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@arco-design/web-react':
|
||||
specifier: ^2.66.5
|
||||
version: 2.66.5(@types/react@19.1.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@chatui/core':
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -183,6 +186,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime-corejs3@7.28.4':
|
||||
resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.28.3':
|
||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -199,6 +206,12 @@ packages:
|
||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@chatui/core@3.2.0':
|
||||
resolution: {integrity: sha512-+XjiL/VzDCE2wqOZhPDquix2YFFiyD8UWqtzzChcoV75qYI8RyCewodxxpdS48zpzMD6DbWcCX4tQiAoxwDHEQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
@@ -710,6 +723,10 @@ packages:
|
||||
character-reference-invalid@2.0.1:
|
||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||
|
||||
clsx@1.2.1:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
|
||||
@@ -749,6 +766,12 @@ packages:
|
||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
core-js-pure@3.45.1:
|
||||
resolution: {integrity: sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==}
|
||||
|
||||
core-js@3.45.1:
|
||||
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -791,6 +814,9 @@ packages:
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
dompurify@2.5.8:
|
||||
resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1028,6 +1054,9 @@ packages:
|
||||
inline-style-parser@0.2.4:
|
||||
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||
|
||||
intersection-observer@0.12.2:
|
||||
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
@@ -1776,6 +1805,10 @@ snapshots:
|
||||
'@babel/core': 7.28.3
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/runtime-corejs3@7.28.4':
|
||||
dependencies:
|
||||
core-js-pure: 3.45.1
|
||||
|
||||
'@babel/runtime@7.28.3': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
@@ -1801,6 +1834,17 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@chatui/core@3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime-corejs3': 7.28.4
|
||||
clsx: 1.2.1
|
||||
core-js: 3.45.1
|
||||
dompurify: 2.5.8
|
||||
intersection-observer: 0.12.2
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
@@ -2207,6 +2251,8 @@ snapshots:
|
||||
|
||||
character-reference-invalid@2.0.1: {}
|
||||
|
||||
clsx@1.2.1: {}
|
||||
|
||||
color-convert@1.9.3:
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
@@ -2243,6 +2289,10 @@ snapshots:
|
||||
|
||||
cookie@1.0.2: {}
|
||||
|
||||
core-js-pure@3.45.1: {}
|
||||
|
||||
core-js@3.45.1: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -2278,6 +2328,8 @@ snapshots:
|
||||
'@babel/runtime': 7.28.3
|
||||
csstype: 3.1.3
|
||||
|
||||
dompurify@2.5.8: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -2552,6 +2604,8 @@ snapshots:
|
||||
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
intersection-observer@0.12.2: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
|
||||
BIN
src/assets/images/ExpertSupportPage/list_bg.png
Normal file
BIN
src/assets/images/ExpertSupportPage/list_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
BIN
src/assets/images/ExpertSupportPage/modal_bg.png
Normal file
BIN
src/assets/images/ExpertSupportPage/modal_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 910 KiB |
BIN
src/assets/images/ExpertSupportPage/title_bg.png
Normal file
BIN
src/assets/images/ExpertSupportPage/title_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 909 KiB |
@@ -13,6 +13,12 @@
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { Avatar, Spin, Empty } from "@arco-design/web-react";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import ClassRankModal from "@/components/ClassRankModal";
|
||||
import "./index.css";
|
||||
|
||||
const positions = ["item2", "item1", "item3"];
|
||||
const icons = ["icon2", "icon1", "icon3"];
|
||||
|
||||
const Rank = ({ className, data, loading }) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const rankings = data?.rankings?.slice(0, 6) || [];
|
||||
|
||||
// 安全处理领奖台学生,确保至少有3个位置
|
||||
@@ -17,8 +20,17 @@ const Rank = ({ className, data, loading }) => {
|
||||
|
||||
const listStudents = rankings.slice(3);
|
||||
|
||||
const handleClick = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`module-class-rank ${className}`}>
|
||||
<>
|
||||
<div
|
||||
className={`module-class-rank ${className}`}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<p className="module-class-rank-title">
|
||||
<IconFont className="title-icon" src="recuUY5nNf7DWT" />
|
||||
<span>班级排名</span>
|
||||
@@ -74,7 +86,14 @@ const Rank = ({ className, data, loading }) => {
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 班级排名弹窗 */}
|
||||
<ClassRankModal
|
||||
visible={modalVisible}
|
||||
onClose={() => setModalVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
273
src/components/ClassRankModal/index.css
Normal file
273
src/components/ClassRankModal/index.css
Normal file
@@ -0,0 +1,273 @@
|
||||
/* 班级排名弹窗样式 */
|
||||
.class-rank-modal {
|
||||
width: 600px;
|
||||
max-height: 80vh;
|
||||
background-color: #fff7f1;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.class-rank-modal-header {
|
||||
padding: 20px 30px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #fff7f1 0%, #ffe8d6 100%);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 180px;
|
||||
height: 110px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-image: url("@/assets/images/Rank/bg.png");
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.class-rank-modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("@/assets/images/icon/close.png");
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.class-rank-modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 复用班级排名板块的领奖台样式 */
|
||||
.class-rank-modal-content .module-class-rank-podium {
|
||||
width: 333px;
|
||||
height: 138px;
|
||||
margin: 20px auto 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
> li {
|
||||
width: 88px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
.module-class-rank-podium-avatar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -24px;
|
||||
transform: translateX(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -10px;
|
||||
width: 57px;
|
||||
height: 16px;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-podium-name {
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 30px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
> i {
|
||||
height: 27px;
|
||||
background-size: 100% 100%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-podium-item1 {
|
||||
height: 98px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -40px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Rank/first_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.module-class-rank-podium-avatar {
|
||||
border-color: #ffc15b;
|
||||
|
||||
&::before {
|
||||
background-image: url("@/assets/images/Rank/icon1.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-podium-item2 {
|
||||
height: 80px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -40px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Rank/second_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.module-class-rank-podium-avatar {
|
||||
border-color: #9ab9e3;
|
||||
&::before {
|
||||
background-image: url("@/assets/images/Rank/icon2.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-podium-item3 {
|
||||
height: 70px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -40px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Rank/third_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.module-class-rank-podium-avatar {
|
||||
border-color: #d7a770;
|
||||
&::before {
|
||||
background-image: url("@/assets/images/Rank/icon3.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 其余排名列表 */
|
||||
.class-rank-modal-list {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.class-rank-modal-list-item {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
> em {
|
||||
margin-left: 5px;
|
||||
color: #86909c;
|
||||
font-size: 22px;
|
||||
font-family: "HarmonyOS_Sans_TC_Bold";
|
||||
font-style: normal;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
> p {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: #1d2129;
|
||||
margin-left: 20px;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/components/ClassRankModal/index.jsx
Normal file
108
src/components/ClassRankModal/index.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Avatar } from "@arco-design/web-react";
|
||||
import Modal from "@/components/Modal";
|
||||
import classRankData from "../../../网页未导入数据/文旅产业/班级排名.json";
|
||||
import "./index.css";
|
||||
|
||||
const positions = ["item2", "item1", "item3"];
|
||||
const icons = ["icon2", "icon1", "icon3"];
|
||||
|
||||
const ClassRankModal = ({ visible, onClose }) => {
|
||||
const [rankings, setRankings] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// 处理班级排名数据,确保按排名排序
|
||||
const sortedData = [...classRankData].sort(
|
||||
(a, b) => parseInt(a.班级排名) - parseInt(b.班级排名)
|
||||
);
|
||||
setRankings(sortedData);
|
||||
}, []);
|
||||
|
||||
// 获取前三名的数据,按照领奖台顺序排列
|
||||
const podiumStudents = [
|
||||
rankings[1] || null, // 第2名
|
||||
rankings[0] || null, // 第1名
|
||||
rankings[2] || null, // 第3名
|
||||
];
|
||||
// 获取第4名及以后的数据
|
||||
const restRankings = rankings.slice(3);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} onClose={onClose}>
|
||||
<div className="class-rank-modal">
|
||||
<div className="class-rank-modal-header">
|
||||
<h2 className="class-rank-modal-title">
|
||||
<span>班级排名</span>
|
||||
</h2>
|
||||
<i className="close-icon" onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<div className="class-rank-modal-content">
|
||||
{/* 前三名展示区 - 复用ClassRank组件的样式 */}
|
||||
<ul className="module-class-rank-podium">
|
||||
{podiumStudents.map((student, index) => {
|
||||
// 使用具体的头像URL
|
||||
let avatarUrl = null;
|
||||
if (student) {
|
||||
const name = student.学员名称.trim();
|
||||
switch(name) {
|
||||
case "万圆":
|
||||
avatarUrl = "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/avatar/douyin/02393125baa474d558c484c0677664b1.jpg";
|
||||
break;
|
||||
case "李阳":
|
||||
avatarUrl = "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/avatar/douyin/07a0a14c8c8d5476b2c8d54de12e6a06.jpg";
|
||||
break;
|
||||
case "何晓彤":
|
||||
avatarUrl = "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/avatar/douyin/13823046201f0ef17517fb46da12bc35.jpg";
|
||||
break;
|
||||
default:
|
||||
avatarUrl = "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp";
|
||||
}
|
||||
}
|
||||
|
||||
return student ? (
|
||||
<li
|
||||
key={student.班级排名}
|
||||
className={`module-class-rank-podium-${positions[index]}`}
|
||||
>
|
||||
<Avatar className="module-class-rank-podium-avatar">
|
||||
<img alt="avatar" src={avatarUrl} />
|
||||
</Avatar>
|
||||
<span className="module-class-rank-podium-name">
|
||||
{student.学员名称.trim()}
|
||||
</span>
|
||||
<i className={`module-class-rank-podium-${icons[index]}`}></i>
|
||||
</li>
|
||||
) : (
|
||||
<li
|
||||
key={`empty-${index}`}
|
||||
className={`module-class-rank-podium-${positions[index]} empty`}
|
||||
>
|
||||
<div className="module-class-rank-podium-placeholder">
|
||||
<span>-</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* 其余排名列表 */}
|
||||
<div className="class-rank-modal-list">
|
||||
{restRankings.map((item) => (
|
||||
<div
|
||||
key={item.班级排名}
|
||||
className="class-rank-modal-list-item"
|
||||
>
|
||||
<em>{item.班级排名}</em>
|
||||
<p>{item.学员名称.trim()}</p>
|
||||
<span>{item.学分}学分</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassRankModal;
|
||||
202
src/components/HRVisitModal/index.css
Normal file
202
src/components/HRVisitModal/index.css
Normal file
@@ -0,0 +1,202 @@
|
||||
.hr-visit-modal .arco-modal {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hr-visit-modal .arco-modal-content {
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hr-visit-modal-content {
|
||||
padding: 40px 30px 30px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #88c999 50%, #67b26f 100%);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.hr-avatars-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatar-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 16px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.avatars-container {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-item {
|
||||
position: absolute;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.avatar-item.hidden {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.hr-avatar-large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.avatar-item.active .hr-avatar-large {
|
||||
border-color: white;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.company-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hr-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
padding: 6px 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.hr-label {
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.hr-name {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.visit-info {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.visit-message {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.indicators {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator.active {
|
||||
background: white;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.indicator:hover {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.hr-visit-modal .arco-modal-content {
|
||||
animation: modalSlideIn 0.4s ease-out;
|
||||
}
|
||||
134
src/components/HRVisitModal/index.jsx
Normal file
134
src/components/HRVisitModal/index.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from '@arco-design/web-react';
|
||||
import { IconLeft, IconRight, IconClose } from '@arco-design/web-react/icon';
|
||||
import './index.css';
|
||||
|
||||
const HRVisitModal = ({ visible, onClose }) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// 模拟HR数据
|
||||
const hrData = [
|
||||
{
|
||||
id: 1,
|
||||
name: '王先生',
|
||||
company: '武汉联影科技有限公司',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
visitMessage: '访问了您的个人档案'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李女士',
|
||||
company: '腾讯科技有限公司',
|
||||
avatar: '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
|
||||
visitMessage: '访问了您的项目经验'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '张先生',
|
||||
company: '阿里巴巴集团',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp',
|
||||
visitMessage: '访问了您的技能证书'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '陈女士',
|
||||
company: '字节跳动科技有限公司',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
visitMessage: '访问了您的求职意向'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '刘先生',
|
||||
company: '华为技术有限公司',
|
||||
avatar: '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
|
||||
visitMessage: '访问了您的工作经历'
|
||||
}
|
||||
];
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentIndex(prev => prev === 0 ? hrData.length - 1 : prev - 1);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentIndex(prev => prev === hrData.length - 1 ? 0 : prev + 1);
|
||||
};
|
||||
|
||||
const currentHR = hrData[currentIndex];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
closable={false}
|
||||
className="hr-visit-modal"
|
||||
width={500}
|
||||
>
|
||||
<div className="hr-visit-modal-content">
|
||||
{/* 关闭按钮 */}
|
||||
<div className="modal-close-btn" onClick={onClose}>
|
||||
<IconClose />
|
||||
</div>
|
||||
|
||||
{/* 头像区域 */}
|
||||
<div className="hr-avatars-section">
|
||||
<div className="avatar-navigation">
|
||||
<button className="nav-btn" onClick={handlePrev}>
|
||||
<IconLeft />
|
||||
</button>
|
||||
|
||||
<div className="avatars-container">
|
||||
{hrData.map((hr, index) => (
|
||||
<div
|
||||
key={hr.id}
|
||||
className={`avatar-item ${index === currentIndex ? 'active' : ''} ${
|
||||
Math.abs(index - currentIndex) > 2 ? 'hidden' : ''
|
||||
}`}
|
||||
style={{
|
||||
transform: `translateX(${(index - currentIndex) * 80}px)`,
|
||||
opacity: index === currentIndex ? 1 : 0.6,
|
||||
zIndex: index === currentIndex ? 10 : 5,
|
||||
scale: index === currentIndex ? 1.2 : 1
|
||||
}}
|
||||
>
|
||||
<img src={hr.avatar} alt={hr.name} className="hr-avatar-large" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="nav-btn" onClick={handleNext}>
|
||||
<IconRight />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 公司信息 */}
|
||||
<div className="company-info">
|
||||
<h3 className="company-name">{currentHR.company}</h3>
|
||||
<div className="hr-tag">
|
||||
<span className="hr-label">HR</span>
|
||||
<span className="hr-name">{currentHR.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 访问信息 */}
|
||||
<div className="visit-info">
|
||||
<p className="visit-message">{currentHR.visitMessage}</p>
|
||||
</div>
|
||||
|
||||
{/* 指示器 */}
|
||||
<div className="indicators">
|
||||
{hrData.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`indicator ${index === currentIndex ? 'active' : ''}`}
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HRVisitModal;
|
||||
@@ -86,13 +86,22 @@
|
||||
}
|
||||
.visitor-count {
|
||||
width: 208px;
|
||||
height: 44px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
background-color: #e5f1ff;
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
@@ -103,12 +112,63 @@
|
||||
height: 100%;
|
||||
background-image: url("@/assets/images/Sidebar/visitor_count_bg.png");
|
||||
background-size: 100% 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.arco-statistic-value {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 41px;
|
||||
.hr-visitor-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.hr-visitor-text {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.hr-avatars-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.hr-avatars {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.hr-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
&.hr-avatar-1 {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&.hr-avatar-2 {
|
||||
z-index: 2;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
&.hr-avatar-3 {
|
||||
z-index: 1;
|
||||
margin-left: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hr-count-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4e5969;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebar-menu {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { Statistic } from "@arco-design/web-react";
|
||||
import { useSelector } from "react-redux";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import HRVisitModal from "@/components/HRVisitModal";
|
||||
import ICON from "@/assets/images/Sidebar/sidebar_icon.png";
|
||||
import ICONRETRACT from "@/assets/images/Sidebar/logo.png";
|
||||
import BTNICON from "@/assets/images/Sidebar/btn_icon.png";
|
||||
@@ -12,6 +13,7 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const studentInfo = useSelector((state) => state.student.studentInfo);
|
||||
const [hrModalVisible, setHrModalVisible] = useState(false);
|
||||
const handleNavClick = (path) => {
|
||||
navigate(path);
|
||||
};
|
||||
@@ -21,6 +23,16 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
|
||||
setIsCollapsed((prev) => !prev);
|
||||
};
|
||||
|
||||
// 打开HR访问详情弹窗
|
||||
const handleHRClick = () => {
|
||||
setHrModalVisible(true);
|
||||
};
|
||||
|
||||
// 关闭HR访问详情弹窗
|
||||
const handleCloseHRModal = () => {
|
||||
setHrModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
@@ -45,12 +57,31 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Statistic
|
||||
className="visitor-count"
|
||||
groupSeparator
|
||||
value={87}
|
||||
prefix="HR访问量:"
|
||||
/>
|
||||
<div className="visitor-count" onClick={handleHRClick}>
|
||||
<div className="hr-visitor-content">
|
||||
<span className="hr-visitor-text">HR访问量</span>
|
||||
<div className="hr-avatars-wrapper">
|
||||
<div className="hr-avatars">
|
||||
<img
|
||||
src="//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp"
|
||||
alt="HR头像"
|
||||
className="hr-avatar hr-avatar-1"
|
||||
/>
|
||||
<img
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image"
|
||||
alt="HR头像"
|
||||
className="hr-avatar hr-avatar-2"
|
||||
/>
|
||||
<img
|
||||
src="//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp"
|
||||
alt="HR头像"
|
||||
className="hr-avatar hr-avatar-3"
|
||||
/>
|
||||
</div>
|
||||
<span className="hr-count-text">等87位HR</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="sidebar-menu">
|
||||
{routes
|
||||
.filter((item) => item.showMenu)
|
||||
@@ -90,6 +121,12 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
|
||||
<div className="sidebar-btn" onClick={toggleSidebar}>
|
||||
<img src={BTNICON} alt="btn" className="sidebar-btn-icon" />
|
||||
</div>
|
||||
|
||||
{/* HR访问详情弹窗 */}
|
||||
<HRVisitModal
|
||||
visible={hrModalVisible}
|
||||
onClose={handleCloseHRModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,127 +1,145 @@
|
||||
import { getMonthDays } from "@/data/mockData";
|
||||
|
||||
const MonthView = ({
|
||||
currentDate,
|
||||
events,
|
||||
onDateClick,
|
||||
onEventClick,
|
||||
selectedDate,
|
||||
}) => {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const days = getMonthDays(year, month);
|
||||
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
|
||||
// 获取指定日期的事件
|
||||
const getEventsForDate = (date, month, year) => {
|
||||
if (!events || events.length === 0) return [];
|
||||
|
||||
const dateString = `${year}-${(month + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}-${date.toString().padStart(2, "0")}`;
|
||||
|
||||
return events.filter((event) => {
|
||||
const eventDate = event.startTime.split(" ")[0];
|
||||
return eventDate === dateString;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateClick = (day, dayEvents) => {
|
||||
// 所有日期都可以点击
|
||||
if (onDateClick) {
|
||||
const clickedDate = new Date(day.year, day.month, day.date);
|
||||
onDateClick(clickedDate, dayEvents || []);
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (day) => {
|
||||
if (!selectedDate) return false;
|
||||
return (
|
||||
selectedDate.getFullYear() === day.year &&
|
||||
selectedDate.getMonth() === day.month &&
|
||||
selectedDate.getDate() === day.date
|
||||
);
|
||||
};
|
||||
|
||||
const renderEventItem = (event, index, dayEvents) => {
|
||||
const maxVisible = 3; // 每个日期最多显示3个事件
|
||||
|
||||
if (
|
||||
index >= maxVisible - 1 &&
|
||||
index === maxVisible - 1 &&
|
||||
dayEvents.length > maxVisible
|
||||
) {
|
||||
// 显示"更多"指示器
|
||||
const remainingCount = dayEvents.length - maxVisible + 1;
|
||||
return (
|
||||
<div key={`more-${index}`} className="event-more">
|
||||
+{remainingCount}更多
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (index >= maxVisible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`event-item ${event.type}`}
|
||||
title={`${event.title} (${event.startTime.split(" ")[1]} - ${
|
||||
event.endTime.split(" ")[1]
|
||||
})`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onEventClick) {
|
||||
onEventClick(event);
|
||||
} else {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="month-view">
|
||||
{/* 星期标题 */}
|
||||
<div className="month-header">
|
||||
{weekDays.map((day) => (
|
||||
<div key={day} className="weekday-header">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 日期网格 */}
|
||||
<div className="month-grid">
|
||||
{days.map((day, index) => {
|
||||
const dayEvents = getEventsForDate(day.date, day.month, day.year);
|
||||
const isToday = day.isToday;
|
||||
const isCurrentMonth = day.isCurrentMonth;
|
||||
const isSelectedDate = isSelected(day);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`day-cell ${!isCurrentMonth ? "other-month" : ""} ${
|
||||
isToday ? "today" : ""
|
||||
} ${isSelectedDate ? "selected" : ""}`}
|
||||
onClick={() => handleDateClick(day, dayEvents)}
|
||||
>
|
||||
<div className="day-number">{day.date}</div>
|
||||
|
||||
<div className="event-list">
|
||||
{dayEvents.map((event, eventIndex) =>
|
||||
renderEventItem(event, eventIndex, dayEvents)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthView;
|
||||
import { getMonthDays } from "@/data/mockData";
|
||||
import { getEventStyleByType } from "@/utils/calendarEventStyles";
|
||||
|
||||
const MonthView = ({
|
||||
currentDate,
|
||||
events,
|
||||
onDateClick,
|
||||
onEventClick,
|
||||
selectedDate,
|
||||
}) => {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const days = getMonthDays(year, month);
|
||||
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
|
||||
// 获取指定日期的事件
|
||||
const getEventsForDate = (date, month, year) => {
|
||||
if (!events || events.length === 0) return [];
|
||||
|
||||
const dateString = `${year}-${(month + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}-${date.toString().padStart(2, "0")}`;
|
||||
|
||||
return events.filter((event) => {
|
||||
const eventDate = event.startTime.split(" ")[0];
|
||||
return eventDate === dateString;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateClick = (day, dayEvents) => {
|
||||
// 所有日期都可以点击
|
||||
if (onDateClick) {
|
||||
const clickedDate = new Date(day.year, day.month, day.date);
|
||||
onDateClick(clickedDate, dayEvents || []);
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (day) => {
|
||||
if (!selectedDate) return false;
|
||||
return (
|
||||
selectedDate.getFullYear() === day.year &&
|
||||
selectedDate.getMonth() === day.month &&
|
||||
selectedDate.getDate() === day.date
|
||||
);
|
||||
};
|
||||
|
||||
const renderEventItem = (event, index, dayEvents) => {
|
||||
const maxVisible = 3; // 每个日期最多显示3个事件
|
||||
|
||||
if (
|
||||
index >= maxVisible - 1 &&
|
||||
index === maxVisible - 1 &&
|
||||
dayEvents.length > maxVisible
|
||||
) {
|
||||
// 显示"更多"指示器
|
||||
const remainingCount = dayEvents.length - maxVisible + 1;
|
||||
return (
|
||||
<div key={`more-${index}`} className="event-more">
|
||||
+{remainingCount}更多
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (index >= maxVisible) return null;
|
||||
|
||||
// 获取事项样式
|
||||
const eventStyle = getEventStyleByType(event.type);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="event-item-new"
|
||||
title={`${event.title} (${event.startTime.split(" ")[1]} - ${
|
||||
event.endTime.split(" ")[1]
|
||||
})`}
|
||||
style={{
|
||||
backgroundColor: eventStyle.backgroundColor,
|
||||
color: eventStyle.textColor,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onEventClick) {
|
||||
onEventClick(event);
|
||||
} else {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="event-content">
|
||||
{eventStyle.icon && (
|
||||
<img
|
||||
src={eventStyle.icon}
|
||||
alt=""
|
||||
className="event-icon"
|
||||
style={{ width: '16px', height: '16px', marginRight: '4px' }}
|
||||
/>
|
||||
)}
|
||||
<span className="event-title">{event.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="month-view">
|
||||
{/* 星期标题 */}
|
||||
<div className="month-header">
|
||||
{weekDays.map((day) => (
|
||||
<div key={day} className="weekday-header">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 日期网格 */}
|
||||
<div className="month-grid">
|
||||
{days.map((day, index) => {
|
||||
const dayEvents = getEventsForDate(day.date, day.month, day.year);
|
||||
const isToday = day.isToday;
|
||||
const isCurrentMonth = day.isCurrentMonth;
|
||||
const isSelectedDate = isSelected(day);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`day-cell ${!isCurrentMonth ? "other-month" : ""} ${
|
||||
isToday ? "today" : ""
|
||||
} ${isSelectedDate ? "selected" : ""}`}
|
||||
onClick={() => handleDateClick(day, dayEvents)}
|
||||
>
|
||||
<div className="day-number">{day.date}</div>
|
||||
|
||||
<div className="event-list">
|
||||
{dayEvents.map((event, eventIndex) =>
|
||||
renderEventItem(event, eventIndex, dayEvents)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthView;
|
||||
|
||||
@@ -1,165 +1,181 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getWeekDays } from "@/data/mockData";
|
||||
|
||||
const WeekView = ({ currentDate, events, onDateClick, onEventClick }) => {
|
||||
const containerRef = useRef(null);
|
||||
const weekDays = getWeekDays(currentDate);
|
||||
const timeSlots = Array.from(
|
||||
{ length: 24 },
|
||||
(_, i) => `${i.toString().padStart(2, "0")}:00`
|
||||
);
|
||||
const weekDayNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
|
||||
// 滚动到当前时间
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const scrollTop = currentHour * 60; // 每小时60px
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = Math.max(0, scrollTop - 120); // 提前2小时显示
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取指定日期的事件
|
||||
const getEventsForDate = (date) => {
|
||||
if (!events || events.length === 0) return [];
|
||||
|
||||
const dateString = `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
||||
|
||||
return events.filter((event) => {
|
||||
const eventDate = event.startTime.split(" ")[0];
|
||||
return eventDate === dateString;
|
||||
});
|
||||
};
|
||||
|
||||
// 计算事件在时间轴上的位置和高度
|
||||
const calculateEventStyle = (event) => {
|
||||
const startTime = event.startTime.split(" ")[1];
|
||||
const endTime = event.endTime.split(" ")[1];
|
||||
|
||||
const startHour = parseInt(startTime.split(":")[0]);
|
||||
const startMinute = parseInt(startTime.split(":")[1]);
|
||||
const endHour = parseInt(endTime.split(":")[0]);
|
||||
const endMinute = parseInt(endTime.split(":")[1]);
|
||||
|
||||
const startOffset = startHour * 60 + startMinute; // 转换为分钟
|
||||
const endOffset = endHour * 60 + endMinute;
|
||||
const duration = endOffset - startOffset;
|
||||
|
||||
const top = startOffset; // 1分钟 = 1px
|
||||
const height = Math.max(duration, 30); // 最小高度30px
|
||||
|
||||
return {
|
||||
top: `${top}px`,
|
||||
height: `${height}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const handleDateClick = (date) => {
|
||||
if (onDateClick) {
|
||||
onDateClick(date);
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentTimeLine = () => {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMinute = now.getMinutes();
|
||||
const totalMinutes = currentHour * 60 + currentMinute;
|
||||
|
||||
// 只在今天显示当前时间线
|
||||
const isToday = weekDays.some(
|
||||
(date) => date.toDateString() === now.toDateString()
|
||||
);
|
||||
|
||||
if (!isToday) return null;
|
||||
|
||||
return (
|
||||
<div className="current-time-line" style={{ top: `${totalMinutes}px` }} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="week-view">
|
||||
{/* 周标题 */}
|
||||
<div className="week-header">
|
||||
<div className="time-header">时间</div>
|
||||
{weekDays.map((date, index) => {
|
||||
const isToday = date.toDateString() === new Date().toDateString();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={date.toISOString()}
|
||||
className={`day-header ${isToday ? "today" : ""}`}
|
||||
onClick={() => handleDateClick(date)}
|
||||
>
|
||||
<div className="day-name">{weekDayNames[index]}</div>
|
||||
<div className="day-date">{date.getDate()}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 周网格 */}
|
||||
<div className="week-grid" ref={containerRef}>
|
||||
{/* 时间列 */}
|
||||
<div className="time-column">
|
||||
{timeSlots.map((time) => (
|
||||
<div key={time} className="time-slot">
|
||||
{time}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 日期列 */}
|
||||
{weekDays.map((date) => {
|
||||
const dayEvents = getEventsForDate(date);
|
||||
|
||||
return (
|
||||
<div key={date.toISOString()} className="day-column">
|
||||
{/* 小时格子 */}
|
||||
{timeSlots.map((time) => (
|
||||
<div key={time} className="hour-slot" />
|
||||
))}
|
||||
|
||||
{/* 事件块 */}
|
||||
{dayEvents.map((event) => {
|
||||
const style = calculateEventStyle(event);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`event-block ${event.type}`}
|
||||
style={style}
|
||||
title={event.description}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onEventClick) {
|
||||
onEventClick(event);
|
||||
} else {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="event-title">{event.title}</div>
|
||||
<div className="event-time">
|
||||
{event.startTime.split(" ")[1]} -{" "}
|
||||
{event.endTime.split(" ")[1]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 当前时间线 */}
|
||||
{getCurrentTimeLine()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeekView;
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getWeekDays } from "@/data/mockData";
|
||||
import { getEventStyleByType } from "@/utils/calendarEventStyles";
|
||||
|
||||
const WeekView = ({ currentDate, events, onDateClick, onEventClick }) => {
|
||||
const containerRef = useRef(null);
|
||||
const weekDays = getWeekDays(currentDate);
|
||||
const timeSlots = Array.from(
|
||||
{ length: 24 },
|
||||
(_, i) => `${i.toString().padStart(2, "0")}:00`
|
||||
);
|
||||
const weekDayNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
|
||||
// 滚动到当前时间
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const scrollTop = currentHour * 60; // 每小时60px
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = Math.max(0, scrollTop - 120); // 提前2小时显示
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取指定日期的事件
|
||||
const getEventsForDate = (date) => {
|
||||
if (!events || events.length === 0) return [];
|
||||
|
||||
const dateString = `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
||||
|
||||
return events.filter((event) => {
|
||||
const eventDate = event.startTime.split(" ")[0];
|
||||
return eventDate === dateString;
|
||||
});
|
||||
};
|
||||
|
||||
// 计算事件在时间轴上的位置和高度
|
||||
const calculateEventStyle = (event) => {
|
||||
const startTime = event.startTime.split(" ")[1];
|
||||
const endTime = event.endTime.split(" ")[1];
|
||||
|
||||
const startHour = parseInt(startTime.split(":")[0]);
|
||||
const startMinute = parseInt(startTime.split(":")[1]);
|
||||
const endHour = parseInt(endTime.split(":")[0]);
|
||||
const endMinute = parseInt(endTime.split(":")[1]);
|
||||
|
||||
const startOffset = startHour * 60 + startMinute; // 转换为分钟
|
||||
const endOffset = endHour * 60 + endMinute;
|
||||
const duration = endOffset - startOffset;
|
||||
|
||||
const top = startOffset; // 1分钟 = 1px
|
||||
const height = Math.max(duration, 30); // 最小高度30px
|
||||
|
||||
return {
|
||||
top: `${top}px`,
|
||||
height: `${height}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const handleDateClick = (date) => {
|
||||
if (onDateClick) {
|
||||
onDateClick(date);
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentTimeLine = () => {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMinute = now.getMinutes();
|
||||
const totalMinutes = currentHour * 60 + currentMinute;
|
||||
|
||||
// 只在今天显示当前时间线
|
||||
const isToday = weekDays.some(
|
||||
(date) => date.toDateString() === now.toDateString()
|
||||
);
|
||||
|
||||
if (!isToday) return null;
|
||||
|
||||
return (
|
||||
<div className="current-time-line" style={{ top: `${totalMinutes}px` }} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="week-view">
|
||||
{/* 周标题 */}
|
||||
<div className="week-header">
|
||||
<div className="time-header">时间</div>
|
||||
{weekDays.map((date, index) => {
|
||||
const isToday = date.toDateString() === new Date().toDateString();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={date.toISOString()}
|
||||
className={`day-header ${isToday ? "today" : ""}`}
|
||||
onClick={() => handleDateClick(date)}
|
||||
>
|
||||
<div className="day-name">{weekDayNames[index]}</div>
|
||||
<div className="day-date">{date.getDate()}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 周网格 */}
|
||||
<div className="week-grid" ref={containerRef}>
|
||||
{/* 时间列 */}
|
||||
<div className="time-column">
|
||||
{timeSlots.map((time) => (
|
||||
<div key={time} className="time-slot">
|
||||
{time}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 日期列 */}
|
||||
{weekDays.map((date) => {
|
||||
const dayEvents = getEventsForDate(date);
|
||||
|
||||
return (
|
||||
<div key={date.toISOString()} className="day-column">
|
||||
{/* 小时格子 */}
|
||||
{timeSlots.map((time) => (
|
||||
<div key={time} className="hour-slot" />
|
||||
))}
|
||||
|
||||
{/* 事件块 */}
|
||||
{dayEvents.map((event) => {
|
||||
const style = calculateEventStyle(event);
|
||||
const eventStyle = getEventStyleByType(event.type);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="event-block-new"
|
||||
style={{
|
||||
...style,
|
||||
backgroundColor: eventStyle.backgroundColor,
|
||||
color: eventStyle.textColor,
|
||||
}}
|
||||
title={event.description}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onEventClick) {
|
||||
onEventClick(event);
|
||||
} else {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="event-header">
|
||||
{eventStyle.icon && (
|
||||
<img
|
||||
src={eventStyle.icon}
|
||||
alt=""
|
||||
className="event-icon"
|
||||
style={{ width: '18px', height: '18px', marginRight: '4px' }}
|
||||
/>
|
||||
)}
|
||||
<div className="event-title">{event.title}</div>
|
||||
</div>
|
||||
<div className="event-time">
|
||||
{event.startTime.split(" ")[1]} -{" "}
|
||||
{event.endTime.split(" ")[1]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 当前时间线 */}
|
||||
{getCurrentTimeLine()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeekView;
|
||||
|
||||
@@ -217,6 +217,47 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 新的事项样式 - 使用JSON配置 */
|
||||
.event-item-new {
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 1px 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.event-item-new:hover {
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.event-item-new .event-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.event-item-new .event-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.event-item-new .event-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 保持原有样式作为备用 */
|
||||
.event-item {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
@@ -397,6 +438,54 @@
|
||||
background: #fbfcfd;
|
||||
}
|
||||
|
||||
/* 新的周视图事件块样式 */
|
||||
.event-block-new {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
z-index: 5;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.event-block-new:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.event-block-new .event-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.event-block-new .event-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.event-block-new .event-title {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.event-block-new .event-time {
|
||||
font-size: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 保持原有样式作为备用 */
|
||||
.event-block {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
|
||||
65
src/pages/ExpertSupportPage/components/FormModal/index.css
Normal file
65
src/pages/ExpertSupportPage/components/FormModal/index.css
Normal file
@@ -0,0 +1,65 @@
|
||||
.form-modal-content {
|
||||
width: 536px;
|
||||
height: 542px;
|
||||
border-radius: 16px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
content: "";
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("@/assets/images/Common/close.png");
|
||||
background-size: 100% 100%;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
content: "";
|
||||
width: 438px;
|
||||
height: 138px;
|
||||
background-image: url("@/assets/images/ExpertSupportPage/modal_bg.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
width: 100%;
|
||||
height: 138px;
|
||||
box-sizing: border-box;
|
||||
padding-top: 20px;
|
||||
.title {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 20px;
|
||||
color: #1d2129;
|
||||
font-weight: 800;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sub-title {
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
color: #4e5969;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
.arco-form-item-control-children {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.submit-btn {
|
||||
width: 320px;
|
||||
height: 36px;
|
||||
background-color: #0077ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/pages/ExpertSupportPage/components/FormModal/index.jsx
Normal file
98
src/pages/ExpertSupportPage/components/FormModal/index.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useRef } from "react";
|
||||
import { Form, Input, Select, Button } from "@arco-design/web-react";
|
||||
import Modal from "@/components/Modal";
|
||||
import "./index.css";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const TextArea = Input.TextArea;
|
||||
|
||||
export default ({ visible, onClose, handleSend }) => {
|
||||
const formRef = useRef();
|
||||
const handleSubmit = async (values) => {
|
||||
console.log(values);
|
||||
handleSend(
|
||||
"system",
|
||||
"正在为您自动匹配适合的专家,2小时内将给您答复,请耐心等待...",
|
||||
false
|
||||
);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={visible} onClose={onClose}>
|
||||
<div className="form-modal-content">
|
||||
<i className="close-icon" onClick={onClose} />
|
||||
<div className="form-modal-title">
|
||||
<p className="title">请填写表单</p>
|
||||
<p className="sub-title">
|
||||
这有助于更快地为你匹配专家以及 <br />
|
||||
解答疑问哦~
|
||||
</p>
|
||||
</div>
|
||||
<Form
|
||||
ref={formRef}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
scrollToFirstError
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FormItem label="问题类型" field="post" rules={[{ required: true }]}>
|
||||
<Select
|
||||
placeholder="请选择问题类型"
|
||||
options={[
|
||||
{
|
||||
label: "one",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: "two",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "three",
|
||||
value: 2,
|
||||
},
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="具体问题描述"
|
||||
field="name"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<TextArea
|
||||
placeholder="请输入您的问题描述"
|
||||
autoSize={{ minRows: 4, maxRows: 7 }}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="紧急程度" field="status">
|
||||
<Select
|
||||
placeholder="请选择紧急程度"
|
||||
options={[
|
||||
{
|
||||
label: "one",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: "two",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "three",
|
||||
value: 2,
|
||||
},
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" htmlType="submit" className="submit-btn">
|
||||
提交
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
167
src/pages/ExpertSupportPage/components/MyIM/index.css
Normal file
167
src/pages/ExpertSupportPage/components/MyIM/index.css
Normal file
@@ -0,0 +1,167 @@
|
||||
.my-im-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ChatUI 容器样式 */
|
||||
.my-im-content .chatui-chat {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 消息容器样式 */
|
||||
.my-im-content .chatui-message-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ChatApp {
|
||||
.user-avatar-wrapper {
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-avatar-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #1d2129;
|
||||
}
|
||||
.user-avatar-tag {
|
||||
width: 52px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 119, 255, 0.1);
|
||||
color: #0077ff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.user-avatar-time {
|
||||
margin-left: 5px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
.SystemMessage {
|
||||
.SystemMessage-inner {
|
||||
background-color: #e5f1ff;
|
||||
.system-message-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 10px 5px;
|
||||
|
||||
> p {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
> span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1d2129;
|
||||
}
|
||||
.system-message-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.system-message-btn {
|
||||
width: 72px;
|
||||
height: 24px;
|
||||
background-color: #0077ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
.user-avatar-wrapper {
|
||||
justify-content: flex-end;
|
||||
.user-avatar-time {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.content-item {
|
||||
.content-item-text {
|
||||
color: #fff;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
.content-item-tip,
|
||||
.question-tags-title,
|
||||
.question-tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.left {
|
||||
.user-avatar-wrapper {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.Message-author {
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.content-item {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.content-item-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.content-item-tip {
|
||||
color: #86909c;
|
||||
> span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
.question-tags-title {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.question-tags {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
> li {
|
||||
border: 1px solid #e5e6eb;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 8px;
|
||||
background-color: #fff;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义样式预留区域 - 用户可以在这里添加自定义样式 */
|
||||
:root {
|
||||
--app-bg: #fff;
|
||||
--footer-bg: #fff;
|
||||
--color-fill-1: #f4f7f9;
|
||||
--color-text-1: #1d2129;
|
||||
--brand-3: #0077ff;
|
||||
--btn-primary-bg: #0077ff;
|
||||
}
|
||||
191
src/pages/ExpertSupportPage/components/MyIM/index.jsx
Normal file
191
src/pages/ExpertSupportPage/components/MyIM/index.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useState, useEffect, useRef, useImperativeHandle } from "react";
|
||||
import Chat, { Bubble, useMessages, SystemMessage } from "@chatui/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import dayjs from "dayjs";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import "@chatui/core/dist/index.css";
|
||||
import "./index.css";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const ICONURL =
|
||||
"https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWmDuekBTlr.png";
|
||||
|
||||
const questionTags = [
|
||||
{ text: "专业知识", type: "text", val: "专业知识" },
|
||||
{ text: "求职策略", type: "text", val: "求职策略" },
|
||||
{ text: "面试模拟", type: "text", val: "面试模拟" },
|
||||
{ text: "企业内推岗位", type: "text", val: "企业内推岗位" },
|
||||
{
|
||||
text: "转专家服务",
|
||||
type: "system",
|
||||
val: "进入专家服务前,请先填写询前表单,这有助于更快地为你匹配专家以及解答疑问哦。",
|
||||
},
|
||||
];
|
||||
|
||||
const RobotAvatarDom = (
|
||||
<div className="user-avatar-wrapper">
|
||||
<span className="user-avatar-name">多多畅职专家服务台</span>
|
||||
<div className="user-avatar-tag">机器人</div>
|
||||
<span className="user-avatar-time">
|
||||
{dayjs().format("YYYY-MM-DD HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const init = [
|
||||
{
|
||||
type: "text",
|
||||
content: { text: "您好!我是您的专属专家顾问,有什么可以帮助您的吗?" },
|
||||
position: "left",
|
||||
user: {
|
||||
avatar: ICONURL,
|
||||
name: RobotAvatarDom,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const MyIM = React.forwardRef((props, ref) => {
|
||||
const { hanldeClickOpenModalBtn, initialMessages } = props;
|
||||
// 使用ChatUI的消息管理hook
|
||||
const { messages, appendMsg } = useMessages(initialMessages || init);
|
||||
const [isInit, setIsInit] = useState(true);
|
||||
const id = useRef(undefined);
|
||||
const childRef = useRef();
|
||||
const studentInfo = useSelector((state) => state.student.studentInfo);
|
||||
|
||||
const userAvatarDom = useMemo(
|
||||
() => (
|
||||
<div className="user-avatar-wrapper">
|
||||
<span className="user-avatar-time">
|
||||
{dayjs().format("YYYY-MM-DD HH:mm")}
|
||||
</span>
|
||||
<span className="user-avatar-name">{studentInfo?.realName}</span>
|
||||
</div>
|
||||
),
|
||||
[studentInfo]
|
||||
);
|
||||
|
||||
// 处理发送消息
|
||||
const handleSend = async (type, val, showBtn = false) => {
|
||||
setIsInit(false);
|
||||
switch (type) {
|
||||
case "text":
|
||||
// 添加用户消息
|
||||
appendMsg({
|
||||
type,
|
||||
content: { text: val.trim() },
|
||||
position: "right",
|
||||
user: {
|
||||
avatar: studentInfo?.avatar,
|
||||
name: userAvatarDom,
|
||||
},
|
||||
});
|
||||
// todo
|
||||
appendMsg({
|
||||
type: "text",
|
||||
content: { text: "收到您的消息,专家正在回复中..." },
|
||||
position: "left",
|
||||
user: {
|
||||
avatar: ICONURL,
|
||||
name: RobotAvatarDom,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "system":
|
||||
appendMsg({
|
||||
type,
|
||||
content: {
|
||||
text: (
|
||||
<div className="system-message-wrapper">
|
||||
<p>
|
||||
{!showBtn && (
|
||||
<IconFont
|
||||
className="system-message-icon"
|
||||
src="recuWi8Aq62JUo"
|
||||
/>
|
||||
)}
|
||||
<span style={{ color: !showBtn ? "#0077FF" : "#1D2129" }}>
|
||||
{val.trim()}
|
||||
</span>
|
||||
</p>
|
||||
{showBtn && (
|
||||
<div
|
||||
className="system-message-btn"
|
||||
onClick={hanldeClickOpenModalBtn}
|
||||
>
|
||||
填写表单
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "image":
|
||||
break;
|
||||
case "file":
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义消息渲染器
|
||||
const renderMessageContent = (MessageProps) => {
|
||||
const { content, _id, type } = MessageProps;
|
||||
|
||||
if (isInit) {
|
||||
id.current = _id;
|
||||
}
|
||||
return (
|
||||
<Bubble>
|
||||
<div className="content-item">
|
||||
<span className="content-item-text">{content.text}</span>
|
||||
{id.current === _id && (
|
||||
<>
|
||||
<div className="content-item-tip">
|
||||
机器人在线时间:<span>7*24</span>
|
||||
</div>
|
||||
<div className="content-item-tip">
|
||||
专家在线时间:<span>工作日13:00-20:00(UTC+8)</span>
|
||||
</div>
|
||||
<div className="question-tags-title">
|
||||
请选择分类,查看常见问题
|
||||
</div>
|
||||
<ul className="question-tags">
|
||||
{questionTags.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() =>
|
||||
handleSend(item.type, item.val, item.type === "system")
|
||||
}
|
||||
>
|
||||
{item.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Bubble>
|
||||
);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSend,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="my-im-content">
|
||||
<Chat
|
||||
messages={messages}
|
||||
renderMessageContent={renderMessageContent}
|
||||
onSend={handleSend}
|
||||
locale="zh-CN"
|
||||
placeholder="发送消息,Shift + Enter换行"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default MyIM;
|
||||
135
src/pages/ExpertSupportPage/components/SupportList/index.css
Normal file
135
src/pages/ExpertSupportPage/components/SupportList/index.css
Normal file
@@ -0,0 +1,135 @@
|
||||
.support-list-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.support-list-title {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-image: url("@/assets/images/ExpertSupportPage/list_bg.png");
|
||||
background-size: 100% 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
color: #1d2129;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
|
||||
.support-list-title-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e4ecf2;
|
||||
position: relative;
|
||||
|
||||
.support-list-title-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.support-list-title-new-btn {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-radius: 4px;
|
||||
background-color: #0077ff;
|
||||
cursor: pointer;
|
||||
> span {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.support-list {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.support-list-date {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.support-list-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.support-list-content-item {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
background-color: #f4f7f9;
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
|
||||
> P {
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
line-height: 44px;
|
||||
box-sizing: border-box;
|
||||
padding-left: 20px;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.support-list-content-item-status {
|
||||
width: 52px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.status-finish {
|
||||
color: #86909c;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.status-waiting {
|
||||
color: #ff3f43;
|
||||
background-color: #ffe0e1;
|
||||
border: 1px solid #ff3f43;
|
||||
}
|
||||
.status-processing {
|
||||
color: #ff6600;
|
||||
background-color: #ffedda;
|
||||
border: 1px solid #ff6600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/pages/ExpertSupportPage/components/SupportList/index.jsx
Normal file
139
src/pages/ExpertSupportPage/components/SupportList/index.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from "react";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import "./index.css";
|
||||
|
||||
const STATUS = {
|
||||
waiting: { key: "waiting", text: "待分配" },
|
||||
processing: { key: "processing", text: "进行中" },
|
||||
finish: { key: "finish", text: "已解决" },
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const handleClickNew = () => {
|
||||
console.log("点击了新对话");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="support-list-wrapper">
|
||||
<div className="support-list-title">
|
||||
<div className="support-list-title-content">
|
||||
<IconFont className="support-list-title-icon" src="recuUY5vFY3hVP" />
|
||||
<span>咨询对话</span>
|
||||
<div className="support-list-title-new-btn" onClick={handleClickNew}>
|
||||
<span>+</span>
|
||||
<span>新对话</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="support-list">
|
||||
<>
|
||||
<p className="support-list-date">今天</p>
|
||||
<ul className="support-list-content">
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-waiting">
|
||||
{STATUS.waiting.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-processing">
|
||||
{STATUS.processing.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
<>
|
||||
<p className="support-list-date">7天内</p>
|
||||
<ul className="support-list-content">
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
<>
|
||||
<p className="support-list-date">30天内</p>
|
||||
<ul className="support-list-content">
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,543 +1,85 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { expertQAData, expertInfo } from "@/data/expertQAData";
|
||||
import { useState, useRef } from "react";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import SupportList from "./components/SupportList";
|
||||
import MyIM from "./components/MyIM";
|
||||
import FormModal from "./components/FormModal";
|
||||
import "./index.css";
|
||||
|
||||
const titleList = [
|
||||
{
|
||||
title: "专家客服",
|
||||
icon: "recuWi8z90DYHn",
|
||||
},
|
||||
{
|
||||
title: "快速响应",
|
||||
icon: "recuWi8zBJn42J",
|
||||
},
|
||||
{
|
||||
title: "24小时服务",
|
||||
icon: "recuWi8A1CNtCo",
|
||||
},
|
||||
];
|
||||
|
||||
const ExpertSupportPage = () => {
|
||||
// 对话管理
|
||||
const [selectedConversation, setSelectedConversation] = useState(null);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
const IMRef = useRef(null);
|
||||
const [formVisible, setFormVisible] = useState(false);
|
||||
const [initialMessages, setInitialMessages] = useState(null); // 设置消息
|
||||
|
||||
const [showResolutionModal, setShowResolutionModal] = useState(null);
|
||||
const [isNewConversation, setIsNewConversation] = useState(false);
|
||||
|
||||
// 用于跟踪最后活动时间
|
||||
|
||||
// 初始对话数据 - 整合文旅产业问答内容
|
||||
const [conversationGroups, setConversationGroups] = useState({
|
||||
今天: [], // 清空今天板块的对话记录
|
||||
"7天内": [
|
||||
// 导入文旅产业问答数据中的最近记录
|
||||
...expertQAData.slice(0, 3).map(qa => ({
|
||||
...qa,
|
||||
lastActivityTime: Date.now() - (Math.floor(Math.random() * 6) + 1) * 24 * 60 * 60 * 1000
|
||||
})),
|
||||
{
|
||||
id: 7,
|
||||
title: "文创产品开发咨询",
|
||||
lastMessage: "如何设计有文化内涵的旅游纪念品",
|
||||
time: "01-03",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 5 * 24 * 60 * 60 * 1000,
|
||||
messages: []
|
||||
},
|
||||
],
|
||||
"30天内": [
|
||||
// 导入文旅产业问答数据中的其余记录
|
||||
...expertQAData.slice(3, 6).map(qa => ({
|
||||
...qa,
|
||||
lastActivityTime: Date.now() - (Math.floor(Math.random() * 10) + 15) * 24 * 60 * 60 * 1000
|
||||
})),
|
||||
{
|
||||
id: 11,
|
||||
title: "智慧旅游技术应用",
|
||||
lastMessage: "如何利用数字技术提升游客体验",
|
||||
time: "12-12",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 26 * 24 * 60 * 60 * 1000,
|
||||
messages: []
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 当前活跃对话的消息记录
|
||||
const conversationMessages = [
|
||||
{
|
||||
id: 1,
|
||||
type: "user",
|
||||
content: "您好,我遇到了一些问题",
|
||||
time: "2025-01-08 16:45",
|
||||
avatar: "👤",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "expert",
|
||||
content:
|
||||
"你好我是多多ai智能问答小助手,有什么问题就来问我吧",
|
||||
time: "2025-01-08 16:46",
|
||||
avatar: "👨💻",
|
||||
expertName: "多多机器人",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "user",
|
||||
content: "我需要了解更多关于泛型的使用,特别是在复杂数据结构中的应用",
|
||||
time: "2025-01-08 16:48",
|
||||
avatar: "👤",
|
||||
},
|
||||
];
|
||||
|
||||
// 创建新对话
|
||||
const createNewConversation = () => {
|
||||
const newConversation = {
|
||||
id: Date.now(),
|
||||
title: "新的问题咨询",
|
||||
lastMessage: "",
|
||||
time: new Date().toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
status: "进行中",
|
||||
statusType: "in-progress",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
|
||||
setConversationGroups((prev) => ({
|
||||
...prev,
|
||||
今天: [newConversation, ...prev["今天"]],
|
||||
}));
|
||||
|
||||
setSelectedConversation(newConversation);
|
||||
setMessages([]);
|
||||
setIsNewConversation(true);
|
||||
const handleClose = () => {
|
||||
setFormVisible(false);
|
||||
};
|
||||
|
||||
// 检查非活跃状态
|
||||
const checkInactivityStatus = () => {
|
||||
const currentTime = Date.now();
|
||||
const INACTIVITY_THRESHOLD = 10 * 60 * 1000; // 10分钟
|
||||
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.statusType === "in-progress") {
|
||||
const timeSinceLastActivity =
|
||||
currentTime - conversation.lastActivityTime;
|
||||
const shouldShowAlert =
|
||||
timeSinceLastActivity >= INACTIVITY_THRESHOLD;
|
||||
|
||||
if (shouldShowAlert !== conversation.hasInactivityAlert) {
|
||||
return { ...conversation, hasInactivityAlert: shouldShowAlert };
|
||||
}
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
const hanldeClickOpenModal = () => {
|
||||
setFormVisible(true);
|
||||
};
|
||||
|
||||
// 处理对话选择
|
||||
const handleConversationSelect = (conversation) => {
|
||||
// 如果是带有红点的对话,点击时询问是否已解决
|
||||
if (
|
||||
conversation.hasInactivityAlert &&
|
||||
conversation.statusType === "in-progress"
|
||||
) {
|
||||
setShowResolutionModal(conversation);
|
||||
} else {
|
||||
setSelectedConversation(conversation);
|
||||
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
|
||||
if (conversation.messages && conversation.messages.length > 0) {
|
||||
setMessages(conversation.messages);
|
||||
} else {
|
||||
setMessages(conversationMessages);
|
||||
}
|
||||
setIsNewConversation(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 标记问题为已解决
|
||||
const markAsResolved = (conversationId) => {
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.id === conversationId) {
|
||||
return {
|
||||
...conversation,
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastMessage: "问题已解决",
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
setShowResolutionModal(null);
|
||||
|
||||
// 如果当前选中的是这个对话,取消选择
|
||||
if (selectedConversation?.id === conversationId) {
|
||||
setSelectedConversation(null);
|
||||
setMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 继续对话
|
||||
const continueConversation = (conversation) => {
|
||||
// 更新最后活动时间
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conv) => {
|
||||
if (conv.id === conversation.id) {
|
||||
return {
|
||||
...conv,
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
}
|
||||
return conv;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
setSelectedConversation(conversation);
|
||||
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
|
||||
if (conversation.messages && conversation.messages.length > 0) {
|
||||
setMessages(conversation.messages);
|
||||
} else {
|
||||
setMessages(conversationMessages);
|
||||
}
|
||||
setShowResolutionModal(null);
|
||||
setIsNewConversation(false);
|
||||
};
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = () => {
|
||||
if (!inputMessage.trim()) return;
|
||||
|
||||
const newMessage = {
|
||||
id: messages.length + 1,
|
||||
type: "user",
|
||||
content: inputMessage,
|
||||
time: new Date().toLocaleString("zh-CN"),
|
||||
avatar: "👤",
|
||||
};
|
||||
|
||||
setMessages([...messages, newMessage]);
|
||||
setInputMessage("");
|
||||
|
||||
// 更新当前对话的标题和最后消息(如果是新对话)
|
||||
if (selectedConversation && isNewConversation && messages.length === 0) {
|
||||
const title =
|
||||
inputMessage.length > 20
|
||||
? inputMessage.substring(0, 20) + "..."
|
||||
: inputMessage;
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.id === selectedConversation.id) {
|
||||
return {
|
||||
...conversation,
|
||||
title: title,
|
||||
lastMessage: inputMessage,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
setSelectedConversation((prev) => ({
|
||||
...prev,
|
||||
title: title,
|
||||
lastMessage: inputMessage,
|
||||
lastActivityTime: Date.now(),
|
||||
}));
|
||||
|
||||
setIsNewConversation(false);
|
||||
} else if (selectedConversation) {
|
||||
// 更新现有对话的最后活动时间
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.id === selectedConversation.id) {
|
||||
return {
|
||||
...conversation,
|
||||
lastMessage: inputMessage,
|
||||
lastActivityTime: Date.now(),
|
||||
hasInactivityAlert: false,
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 组件生命周期
|
||||
useEffect(() => {
|
||||
// 页面加载时自动创建新对话
|
||||
createNewConversation();
|
||||
|
||||
// 定时检查非活跃状态
|
||||
const interval = setInterval(checkInactivityStatus, 30000); // 每30秒检查一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// 组件挂载后进行初始检查
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(checkInactivityStatus, 1000);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
// 转专家服务
|
||||
const handleTransferToExpert = () => {
|
||||
if (
|
||||
selectedConversation &&
|
||||
selectedConversation.statusType === "in-progress"
|
||||
) {
|
||||
// 更新对话状态逻辑
|
||||
alert("已转交人工专家,将有专业老师为您服务");
|
||||
// 调用子组件的方法
|
||||
const handleSend = (type, val, showBtn) => {
|
||||
if (IMRef.current) {
|
||||
IMRef.current.handleSend(type, val, showBtn);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="expert-support-page">
|
||||
{/* 左侧对话记录区域 */}
|
||||
<div className="conversation-sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h2 className="expert-support-sidebar-title">对话记录</h2>
|
||||
<div className="new-conversation-btn-wrapper" style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
className="new-conversation-btn disabled"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
disabled
|
||||
style={{
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.6,
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}
|
||||
>
|
||||
<span className="btn-icon">💬</span>
|
||||
<span className="btn-text">新建对话</span>
|
||||
</button>
|
||||
<div className="hover-tooltip" style={{
|
||||
position: 'absolute',
|
||||
top: '-35px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
color: '#fff',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'none',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
非学员无权限
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<div className="expert-support-page">
|
||||
<div className="expert-support-left-wrapper">
|
||||
<SupportList />
|
||||
</div>
|
||||
|
||||
{/* 按时间分组的对话列表 */}
|
||||
<div className="conversation-groups">
|
||||
{Object.entries(conversationGroups).map(
|
||||
([groupName, conversations]) => (
|
||||
<div key={groupName} className="conversation-group">
|
||||
<div className="group-header">
|
||||
<h3 className="group-title">{groupName}</h3>
|
||||
</div>
|
||||
<div className="conversation-list">
|
||||
{conversations.map((conversation) => (
|
||||
<div
|
||||
key={conversation.id}
|
||||
className={`conversation-item ${
|
||||
selectedConversation?.id === conversation.id
|
||||
? "selected"
|
||||
: ""
|
||||
} ${conversation.hasInactivityAlert ? "has-alert" : ""}`}
|
||||
onClick={() => handleConversationSelect(conversation)}
|
||||
>
|
||||
<div className="conversation-content">
|
||||
<div className="conversation-title-row">
|
||||
<h4 className="conversation-title">
|
||||
{conversation.title}
|
||||
</h4>
|
||||
{conversation.hasInactivityAlert && (
|
||||
<div className="inactivity-alert">
|
||||
<span className="alert-dot"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="conversation-preview">
|
||||
{conversation.lastMessage}
|
||||
</p>
|
||||
</div>
|
||||
<div className="conversation-meta">
|
||||
<span className="conversation-time">
|
||||
{conversation.time}
|
||||
</span>
|
||||
<span
|
||||
className={`conversation-status status-${conversation.statusType}`}
|
||||
>
|
||||
{conversation.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="expert-support-right-wrapper">
|
||||
<div className="expert-support-right-title-wrapper">
|
||||
<div className="title-wrapper">
|
||||
<IconFont className="right-title-icon" src="recuWi8ARUkXzm" />
|
||||
<div className="right-title-text">
|
||||
<p>专家支持中心</p>
|
||||
<span>Expert Tutor Support Center</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<ul>
|
||||
{titleList.map((item) => (
|
||||
<li key={item.title}>
|
||||
<IconFont src={item.icon} className="item-icon" />
|
||||
<span>{item.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="expert-support-im-wrapper">
|
||||
<MyIM
|
||||
ref={IMRef}
|
||||
initialMessages={initialMessages}
|
||||
hanldeClickOpenModalBtn={hanldeClickOpenModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧问答界面 */}
|
||||
<div className="chat-interface">
|
||||
{/* 顶部信息栏 */}
|
||||
<div className="chat-header">
|
||||
<div className="header-info">
|
||||
<div className="service-badge">
|
||||
<span className="badge-icon">🎓</span>
|
||||
<span className="badge-text">专家客服</span>
|
||||
</div>
|
||||
<div className="service-title">学有所问,向必有答</div>
|
||||
<div className="service-subtitle">
|
||||
「我们有创新学习习惯养成方案」
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-actions">
|
||||
<div className="service-tag">专家答疑</div>
|
||||
<div className="service-tag">快速响应</div>
|
||||
<div className="service-tag">24小时服务</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 对话内容区域 */}
|
||||
<div className="chat-content">
|
||||
{selectedConversation ? (
|
||||
<>
|
||||
<div className="chat-title-bar">
|
||||
<h3 className="active-conversation-title">
|
||||
{selectedConversation.title}
|
||||
</h3>
|
||||
{selectedConversation.statusType === "in-progress" && (
|
||||
<button
|
||||
className="transfer-expert-btn"
|
||||
onClick={handleTransferToExpert}
|
||||
>
|
||||
转专家服务
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="messages-container">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`message ${message.type}-message`}
|
||||
>
|
||||
<div className="message-avatar">{message.avatar}</div>
|
||||
<div className="message-bubble">
|
||||
{message.type === "expert" && (
|
||||
<div className="expert-name">{message.expertName}</div>
|
||||
)}
|
||||
<div className="message-text">{message.content}</div>
|
||||
<div className="message-time">{message.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="no-conversation-selected">
|
||||
<div className="placeholder-icon">💬</div>
|
||||
<div className="placeholder-text">请选择一个对话开始</div>
|
||||
<div className="placeholder-subtitle">
|
||||
从左侧列表中选择对话记录
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
{selectedConversation && (
|
||||
<div className="chat-input-area">
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
placeholder="请输入您的问题..."
|
||||
className="message-input"
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
|
||||
/>
|
||||
<button
|
||||
className="send-button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputMessage.trim()}
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
<div className="input-tips">
|
||||
<span className="tip-text">
|
||||
Shift + Enter 可以换行,Enter 发送消息
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 问题解决确认模态框 */}
|
||||
{showResolutionModal && (
|
||||
<div className="resolution-modal-overlay">
|
||||
<div className="resolution-modal">
|
||||
<div className="modal-header">
|
||||
<h3 className="modal-title">问题解决确认</h3>
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={() => setShowResolutionModal(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-content">
|
||||
<div className="modal-icon">❓</div>
|
||||
<p className="modal-text">
|
||||
您在「{showResolutionModal.title}」中已经超过10分钟没有活动,
|
||||
请问这个问题是否已经得到解决?
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={() => continueConversation(showResolutionModal)}
|
||||
>
|
||||
继续讨论
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => markAsResolved(showResolutionModal.id)}
|
||||
>
|
||||
已解决
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FormModal
|
||||
visible={formVisible}
|
||||
onClose={handleClose}
|
||||
handleSend={handleSend}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpertSupportPage;
|
||||
export default ExpertSupportPage;
|
||||
899
src/pages/ExpertSupportPage/index_old.css
Normal file
899
src/pages/ExpertSupportPage/index_old.css
Normal file
@@ -0,0 +1,899 @@
|
||||
/* 专家支持中心页面样式 - 重新设计 */
|
||||
.expert-support-page {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
height: calc(100vh - 80px);
|
||||
background: #f5f7fa;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===================
|
||||
左侧对话记录区域
|
||||
=================== */
|
||||
|
||||
.conversation-sidebar {
|
||||
background: white;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 24px 20px;
|
||||
border-bottom: 1px solid #f0f1f3;
|
||||
background: #fafbfc;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.expert-support-sidebar-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 新建对话按钮 */
|
||||
.new-conversation-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.new-conversation-btn:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.new-conversation-btn .btn-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.new-conversation-btn .btn-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 禁用状态的新建对话按钮 */
|
||||
.new-conversation-btn.disabled {
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.6;
|
||||
background-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.new-conversation-btn.disabled:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
background-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
/* hover提示容器 */
|
||||
.new-conversation-btn-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* hover提示气泡 */
|
||||
.new-conversation-btn-wrapper:hover .hover-tooltip {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.hover-tooltip {
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 6px 12px;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 小三角箭头 */
|
||||
.hover-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* 对话分组 */
|
||||
.conversation-groups {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.conversation-group {
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.conversation-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
padding: 16px 20px 8px 20px;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #f0f1f3;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 对话列表 */
|
||||
.conversation-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.conversation-item:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.conversation-item.selected {
|
||||
background: #eff6ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.conversation-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 对话标题行 */
|
||||
.conversation-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.conversation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 非活跃提醒 */
|
||||
.inactivity-alert {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alert-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
animation: pulse-red 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-red {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-item.has-alert {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.conversation-item.has-alert:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.conversation-preview {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conversation-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.conversation-time {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-status {
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-resolved {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* ===================
|
||||
右侧问答界面
|
||||
=================== */
|
||||
|
||||
.chat-interface {
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部信息栏 */
|
||||
.chat-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 24px 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.service-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 12px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.service-subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.service-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.service-tag {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 对话内容区域 */
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.active-conversation-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.transfer-expert-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.transfer-expert-btn:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 消息容器 */
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 70%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-message .message-bubble {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.expert-message .message-bubble {
|
||||
background: #f8fafc;
|
||||
color: #111827;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.expert-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-message .expert-name {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-message .message-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-message .message-time {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.expert-message .message-time {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.no-conversation-selected {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.placeholder-subtitle {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 输入区域 */
|
||||
.chat-input-area {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
padding: 20px 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
transition: all 150ms ease;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-button:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.input-tips {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* ===================
|
||||
响应式设计
|
||||
=================== */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.expert-support-page {
|
||||
grid-template-columns: 300px 1fr;
|
||||
}
|
||||
|
||||
.conversation-sidebar {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.new-conversation-btn .btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-conversation-btn {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.expert-support-page {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.conversation-sidebar {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.new-conversation-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.new-conversation-btn .btn-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.conversation-groups {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
min-width: 280px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: none;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.chat-title-bar {
|
||||
padding: 16px 20px;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.service-subtitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================
|
||||
问题解决确认模态框
|
||||
=================== */
|
||||
|
||||
.resolution-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.resolution-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
animation: modalAppear 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
flex: 1;
|
||||
background: white;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 响应式模态框 */
|
||||
@media (max-width: 768px) {
|
||||
.resolution-modal {
|
||||
width: 95%;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
padding: 16px 20px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
543
src/pages/ExpertSupportPage/index_old.jsx
Normal file
543
src/pages/ExpertSupportPage/index_old.jsx
Normal file
@@ -0,0 +1,543 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { expertQAData, expertInfo } from "@/data/expertQAData";
|
||||
import "./index.css";
|
||||
|
||||
const ExpertSupportPage = () => {
|
||||
// 对话管理
|
||||
const [selectedConversation, setSelectedConversation] = useState(null);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
|
||||
const [showResolutionModal, setShowResolutionModal] = useState(null);
|
||||
const [isNewConversation, setIsNewConversation] = useState(false);
|
||||
|
||||
// 用于跟踪最后活动时间
|
||||
|
||||
// 初始对话数据 - 整合文旅产业问答内容
|
||||
const [conversationGroups, setConversationGroups] = useState({
|
||||
今天: [], // 清空今天板块的对话记录
|
||||
"7天内": [
|
||||
// 导入文旅产业问答数据中的最近记录
|
||||
...expertQAData.slice(0, 3).map(qa => ({
|
||||
...qa,
|
||||
lastActivityTime: Date.now() - (Math.floor(Math.random() * 6) + 1) * 24 * 60 * 60 * 1000
|
||||
})),
|
||||
{
|
||||
id: 7,
|
||||
title: "文创产品开发咨询",
|
||||
lastMessage: "如何设计有文化内涵的旅游纪念品",
|
||||
time: "01-03",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 5 * 24 * 60 * 60 * 1000,
|
||||
messages: []
|
||||
},
|
||||
],
|
||||
"30天内": [
|
||||
// 导入文旅产业问答数据中的其余记录
|
||||
...expertQAData.slice(3, 6).map(qa => ({
|
||||
...qa,
|
||||
lastActivityTime: Date.now() - (Math.floor(Math.random() * 10) + 15) * 24 * 60 * 60 * 1000
|
||||
})),
|
||||
{
|
||||
id: 11,
|
||||
title: "智慧旅游技术应用",
|
||||
lastMessage: "如何利用数字技术提升游客体验",
|
||||
time: "12-12",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 26 * 24 * 60 * 60 * 1000,
|
||||
messages: []
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 当前活跃对话的消息记录
|
||||
const conversationMessages = [
|
||||
{
|
||||
id: 1,
|
||||
type: "user",
|
||||
content: "您好,我遇到了一些问题",
|
||||
time: "2025-01-08 16:45",
|
||||
avatar: "👤",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "expert",
|
||||
content:
|
||||
"你好我是多多ai智能问答小助手,有什么问题就来问我吧",
|
||||
time: "2025-01-08 16:46",
|
||||
avatar: "👨💻",
|
||||
expertName: "多多机器人",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "user",
|
||||
content: "我需要了解更多关于泛型的使用,特别是在复杂数据结构中的应用",
|
||||
time: "2025-01-08 16:48",
|
||||
avatar: "👤",
|
||||
},
|
||||
];
|
||||
|
||||
// 创建新对话
|
||||
const createNewConversation = () => {
|
||||
const newConversation = {
|
||||
id: Date.now(),
|
||||
title: "新的问题咨询",
|
||||
lastMessage: "",
|
||||
time: new Date().toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
status: "进行中",
|
||||
statusType: "in-progress",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
|
||||
setConversationGroups((prev) => ({
|
||||
...prev,
|
||||
今天: [newConversation, ...prev["今天"]],
|
||||
}));
|
||||
|
||||
setSelectedConversation(newConversation);
|
||||
setMessages([]);
|
||||
setIsNewConversation(true);
|
||||
};
|
||||
|
||||
// 检查非活跃状态
|
||||
const checkInactivityStatus = () => {
|
||||
const currentTime = Date.now();
|
||||
const INACTIVITY_THRESHOLD = 10 * 60 * 1000; // 10分钟
|
||||
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.statusType === "in-progress") {
|
||||
const timeSinceLastActivity =
|
||||
currentTime - conversation.lastActivityTime;
|
||||
const shouldShowAlert =
|
||||
timeSinceLastActivity >= INACTIVITY_THRESHOLD;
|
||||
|
||||
if (shouldShowAlert !== conversation.hasInactivityAlert) {
|
||||
return { ...conversation, hasInactivityAlert: shouldShowAlert };
|
||||
}
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// 处理对话选择
|
||||
const handleConversationSelect = (conversation) => {
|
||||
// 如果是带有红点的对话,点击时询问是否已解决
|
||||
if (
|
||||
conversation.hasInactivityAlert &&
|
||||
conversation.statusType === "in-progress"
|
||||
) {
|
||||
setShowResolutionModal(conversation);
|
||||
} else {
|
||||
setSelectedConversation(conversation);
|
||||
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
|
||||
if (conversation.messages && conversation.messages.length > 0) {
|
||||
setMessages(conversation.messages);
|
||||
} else {
|
||||
setMessages(conversationMessages);
|
||||
}
|
||||
setIsNewConversation(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 标记问题为已解决
|
||||
const markAsResolved = (conversationId) => {
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.id === conversationId) {
|
||||
return {
|
||||
...conversation,
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastMessage: "问题已解决",
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
setShowResolutionModal(null);
|
||||
|
||||
// 如果当前选中的是这个对话,取消选择
|
||||
if (selectedConversation?.id === conversationId) {
|
||||
setSelectedConversation(null);
|
||||
setMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 继续对话
|
||||
const continueConversation = (conversation) => {
|
||||
// 更新最后活动时间
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conv) => {
|
||||
if (conv.id === conversation.id) {
|
||||
return {
|
||||
...conv,
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
}
|
||||
return conv;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
setSelectedConversation(conversation);
|
||||
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
|
||||
if (conversation.messages && conversation.messages.length > 0) {
|
||||
setMessages(conversation.messages);
|
||||
} else {
|
||||
setMessages(conversationMessages);
|
||||
}
|
||||
setShowResolutionModal(null);
|
||||
setIsNewConversation(false);
|
||||
};
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = () => {
|
||||
if (!inputMessage.trim()) return;
|
||||
|
||||
const newMessage = {
|
||||
id: messages.length + 1,
|
||||
type: "user",
|
||||
content: inputMessage,
|
||||
time: new Date().toLocaleString("zh-CN"),
|
||||
avatar: "👤",
|
||||
};
|
||||
|
||||
setMessages([...messages, newMessage]);
|
||||
setInputMessage("");
|
||||
|
||||
// 更新当前对话的标题和最后消息(如果是新对话)
|
||||
if (selectedConversation && isNewConversation && messages.length === 0) {
|
||||
const title =
|
||||
inputMessage.length > 20
|
||||
? inputMessage.substring(0, 20) + "..."
|
||||
: inputMessage;
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.id === selectedConversation.id) {
|
||||
return {
|
||||
...conversation,
|
||||
title: title,
|
||||
lastMessage: inputMessage,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
setSelectedConversation((prev) => ({
|
||||
...prev,
|
||||
title: title,
|
||||
lastMessage: inputMessage,
|
||||
lastActivityTime: Date.now(),
|
||||
}));
|
||||
|
||||
setIsNewConversation(false);
|
||||
} else if (selectedConversation) {
|
||||
// 更新现有对话的最后活动时间
|
||||
setConversationGroups((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((groupKey) => {
|
||||
updated[groupKey] = updated[groupKey].map((conversation) => {
|
||||
if (conversation.id === selectedConversation.id) {
|
||||
return {
|
||||
...conversation,
|
||||
lastMessage: inputMessage,
|
||||
lastActivityTime: Date.now(),
|
||||
hasInactivityAlert: false,
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 组件生命周期
|
||||
useEffect(() => {
|
||||
// 页面加载时自动创建新对话
|
||||
createNewConversation();
|
||||
|
||||
// 定时检查非活跃状态
|
||||
const interval = setInterval(checkInactivityStatus, 30000); // 每30秒检查一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// 组件挂载后进行初始检查
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(checkInactivityStatus, 1000);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
// 转专家服务
|
||||
const handleTransferToExpert = () => {
|
||||
if (
|
||||
selectedConversation &&
|
||||
selectedConversation.statusType === "in-progress"
|
||||
) {
|
||||
// 更新对话状态逻辑
|
||||
alert("已转交人工专家,将有专业老师为您服务");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="expert-support-page">
|
||||
{/* 左侧对话记录区域 */}
|
||||
<div className="conversation-sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h2 className="expert-support-sidebar-title">对话记录</h2>
|
||||
<div className="new-conversation-btn-wrapper" style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
className="new-conversation-btn disabled"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
disabled
|
||||
style={{
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.6,
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}
|
||||
>
|
||||
<span className="btn-icon">💬</span>
|
||||
<span className="btn-text">新建对话</span>
|
||||
</button>
|
||||
<div className="hover-tooltip" style={{
|
||||
position: 'absolute',
|
||||
top: '-35px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
color: '#fff',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'none',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
非学员无权限
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 按时间分组的对话列表 */}
|
||||
<div className="conversation-groups">
|
||||
{Object.entries(conversationGroups).map(
|
||||
([groupName, conversations]) => (
|
||||
<div key={groupName} className="conversation-group">
|
||||
<div className="group-header">
|
||||
<h3 className="group-title">{groupName}</h3>
|
||||
</div>
|
||||
<div className="conversation-list">
|
||||
{conversations.map((conversation) => (
|
||||
<div
|
||||
key={conversation.id}
|
||||
className={`conversation-item ${
|
||||
selectedConversation?.id === conversation.id
|
||||
? "selected"
|
||||
: ""
|
||||
} ${conversation.hasInactivityAlert ? "has-alert" : ""}`}
|
||||
onClick={() => handleConversationSelect(conversation)}
|
||||
>
|
||||
<div className="conversation-content">
|
||||
<div className="conversation-title-row">
|
||||
<h4 className="conversation-title">
|
||||
{conversation.title}
|
||||
</h4>
|
||||
{conversation.hasInactivityAlert && (
|
||||
<div className="inactivity-alert">
|
||||
<span className="alert-dot"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="conversation-preview">
|
||||
{conversation.lastMessage}
|
||||
</p>
|
||||
</div>
|
||||
<div className="conversation-meta">
|
||||
<span className="conversation-time">
|
||||
{conversation.time}
|
||||
</span>
|
||||
<span
|
||||
className={`conversation-status status-${conversation.statusType}`}
|
||||
>
|
||||
{conversation.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧问答界面 */}
|
||||
<div className="chat-interface">
|
||||
{/* 顶部信息栏 */}
|
||||
<div className="chat-header">
|
||||
<div className="header-info">
|
||||
<div className="service-badge">
|
||||
<span className="badge-icon">🎓</span>
|
||||
<span className="badge-text">专家客服</span>
|
||||
</div>
|
||||
<div className="service-title">学有所问,向必有答</div>
|
||||
<div className="service-subtitle">
|
||||
「我们有创新学习习惯养成方案」
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-actions">
|
||||
<div className="service-tag">专家答疑</div>
|
||||
<div className="service-tag">快速响应</div>
|
||||
<div className="service-tag">24小时服务</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 对话内容区域 */}
|
||||
<div className="chat-content">
|
||||
{selectedConversation ? (
|
||||
<>
|
||||
<div className="chat-title-bar">
|
||||
<h3 className="active-conversation-title">
|
||||
{selectedConversation.title}
|
||||
</h3>
|
||||
{selectedConversation.statusType === "in-progress" && (
|
||||
<button
|
||||
className="transfer-expert-btn"
|
||||
onClick={handleTransferToExpert}
|
||||
>
|
||||
转专家服务
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="messages-container">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`message ${message.type}-message`}
|
||||
>
|
||||
<div className="message-avatar">{message.avatar}</div>
|
||||
<div className="message-bubble">
|
||||
{message.type === "expert" && (
|
||||
<div className="expert-name">{message.expertName}</div>
|
||||
)}
|
||||
<div className="message-text">{message.content}</div>
|
||||
<div className="message-time">{message.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="no-conversation-selected">
|
||||
<div className="placeholder-icon">💬</div>
|
||||
<div className="placeholder-text">请选择一个对话开始</div>
|
||||
<div className="placeholder-subtitle">
|
||||
从左侧列表中选择对话记录
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
{selectedConversation && (
|
||||
<div className="chat-input-area">
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
placeholder="请输入您的问题..."
|
||||
className="message-input"
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
|
||||
/>
|
||||
<button
|
||||
className="send-button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputMessage.trim()}
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
<div className="input-tips">
|
||||
<span className="tip-text">
|
||||
Shift + Enter 可以换行,Enter 发送消息
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 问题解决确认模态框 */}
|
||||
{showResolutionModal && (
|
||||
<div className="resolution-modal-overlay">
|
||||
<div className="resolution-modal">
|
||||
<div className="modal-header">
|
||||
<h3 className="modal-title">问题解决确认</h3>
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={() => setShowResolutionModal(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-content">
|
||||
<div className="modal-icon">❓</div>
|
||||
<p className="modal-text">
|
||||
您在「{showResolutionModal.title}」中已经超过10分钟没有活动,
|
||||
请问这个问题是否已经得到解决?
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={() => continueConversation(showResolutionModal)}
|
||||
>
|
||||
继续讨论
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => markAsResolved(showResolutionModal.id)}
|
||||
>
|
||||
已解决
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpertSupportPage;
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
.unified-profile-right {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
height: 785px;
|
||||
border-radius: 16px;
|
||||
background-color: #e8f6ff;
|
||||
position: relative;
|
||||
|
||||
49
src/utils/calendarEventStyles.js
Normal file
49
src/utils/calendarEventStyles.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import eventTypesData from "../../网页未导入数据/文旅产业/日历事项.json";
|
||||
|
||||
// 创建事项名称到样式的映射
|
||||
export const createEventStyleMapping = () => {
|
||||
const styleMapping = {};
|
||||
|
||||
eventTypesData.forEach(item => {
|
||||
styleMapping[item.事项名称] = {
|
||||
icon: item.icon,
|
||||
textColor: `#${item.字体颜色}`,
|
||||
backgroundColor: `#${item.事项背景色}`,
|
||||
name: item.事项名称
|
||||
};
|
||||
});
|
||||
|
||||
return styleMapping;
|
||||
};
|
||||
|
||||
// 根据事项标题获取样式
|
||||
export const getEventStyleByType = (eventType) => {
|
||||
// 事件类型到日历事项配置的映射
|
||||
const typeMapping = {
|
||||
'ai-course': 'AI课',
|
||||
'public-course': '企业高管公开课',
|
||||
'marketing-course': '营销课',
|
||||
'compound-skill': '复合能力课',
|
||||
'vertical-skill': '垂直能力课',
|
||||
'one-on-one': '1v1求职规划',
|
||||
'interview': '线下面试模拟'
|
||||
};
|
||||
|
||||
const styleMapping = createEventStyleMapping();
|
||||
const configName = typeMapping[eventType];
|
||||
|
||||
if (configName && styleMapping[configName]) {
|
||||
return styleMapping[configName];
|
||||
}
|
||||
|
||||
// 默认样式
|
||||
return {
|
||||
icon: null,
|
||||
textColor: '#1d2129',
|
||||
backgroundColor: '#f3f4f6',
|
||||
name: '其他'
|
||||
};
|
||||
};;
|
||||
|
||||
// 导出样式映射对象供其他地方使用
|
||||
export const EVENT_STYLE_MAPPING = createEventStyleMapping();
|
||||
3302
网页未导入数据/文旅产业/头像列表.json
Normal file
3302
网页未导入数据/文旅产业/头像列表.json
Normal file
File diff suppressed because it is too large
Load Diff
44
网页未导入数据/文旅产业/日历事项.json
Normal file
44
网页未导入数据/文旅产业/日历事项.json
Normal file
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"事项名称": "AI课",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJAm0i3SW.png",
|
||||
"字体颜色": "57369A",
|
||||
"事项背景色":"EBE1FF"
|
||||
},
|
||||
{
|
||||
"事项名称": "企业高管公开课",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJzSrMX99.png",
|
||||
"字体颜色": "0069A7",
|
||||
"事项背景色":"C8EAFF"
|
||||
},
|
||||
{
|
||||
"事项名称": "营销课",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJC7yjPIF.png",
|
||||
"字体颜色": "A04B6A",
|
||||
"事项背景色":"FFDAE8"
|
||||
},
|
||||
{
|
||||
"事项名称": "复合能力课",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJBHPA28K.png",
|
||||
"字体颜色": "7C6E08",
|
||||
"事项背景色":"FFF6B5"
|
||||
},
|
||||
{
|
||||
"事项名称": "垂直能力课",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJAQr5D4g.png",
|
||||
"字体颜色": "22935C",
|
||||
"事项背景色":"BAFFD6"
|
||||
},
|
||||
{
|
||||
"事项名称": "1v1求职规划",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJBgCSLjW.png",
|
||||
"字体颜色": "9E542A",
|
||||
"事项背景色":"FFDDC9"
|
||||
},
|
||||
{
|
||||
"事项名称": "线下面试模拟",
|
||||
"icon": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWtJBgCSLjW.png",
|
||||
"字体颜色": "9E542A",
|
||||
"事项背景色":"FFDDC9"
|
||||
}
|
||||
]
|
||||
202
网页未导入数据/文旅产业/班级排名.json
Normal file
202
网页未导入数据/文旅产业/班级排名.json
Normal file
@@ -0,0 +1,202 @@
|
||||
[
|
||||
{
|
||||
"学员名称": "万圆",
|
||||
"学分": "99",
|
||||
"班级排名": "1"
|
||||
},
|
||||
{
|
||||
"学员名称": " 李阳 ",
|
||||
"学分": "97",
|
||||
"班级排名": "2"
|
||||
},
|
||||
{
|
||||
"学员名称": " 何晓彤 ",
|
||||
"学分": "96",
|
||||
"班级排名": "3"
|
||||
},
|
||||
{
|
||||
"学员名称": "李维",
|
||||
"学分": "95",
|
||||
"班级排名": "4"
|
||||
},
|
||||
{
|
||||
"学员名称": " 程子涵 ",
|
||||
"学分": "94",
|
||||
"班级排名": "5"
|
||||
},
|
||||
{
|
||||
"学员名称": " 马一鸣 ",
|
||||
"学分": "93",
|
||||
"班级排名": "6"
|
||||
},
|
||||
{
|
||||
"学员名称": "魏韬",
|
||||
"学分": "92",
|
||||
"班级排名": "8"
|
||||
},
|
||||
{
|
||||
"学员名称": " 唐思远 ",
|
||||
"学分": "92",
|
||||
"班级排名": "7"
|
||||
},
|
||||
{
|
||||
"学员名称": " 赵子墨 ",
|
||||
"学分": "90",
|
||||
"班级排名": "9"
|
||||
},
|
||||
{
|
||||
"学员名称": " 梁静怡 ",
|
||||
"学分": "89",
|
||||
"班级排名": "10"
|
||||
},
|
||||
{
|
||||
"学员名称": "方凯旋",
|
||||
"学分": "88",
|
||||
"班级排名": "11"
|
||||
},
|
||||
{
|
||||
"学员名称": " 张梓涵 ",
|
||||
"学分": "88",
|
||||
"班级排名": "12"
|
||||
},
|
||||
{
|
||||
"学员名称": " 林雅婷 ",
|
||||
"学分": "87",
|
||||
"班级排名": "13"
|
||||
},
|
||||
{
|
||||
"学员名称": " 陈磊 ",
|
||||
"学分": "86",
|
||||
"班级排名": "14"
|
||||
},
|
||||
{
|
||||
"学员名称": " 宋心怡 ",
|
||||
"学分": "85",
|
||||
"班级排名": "15"
|
||||
},
|
||||
{
|
||||
"学员名称": " 孙思琪 ",
|
||||
"学分": "84",
|
||||
"班级排名": "16"
|
||||
},
|
||||
{
|
||||
"学员名称": " 吴晓雯 ",
|
||||
"学分": "84",
|
||||
"班级排名": "17"
|
||||
},
|
||||
{
|
||||
"学员名称": " 周嘉怡 ",
|
||||
"学分": "83",
|
||||
"班级排名": "18"
|
||||
},
|
||||
{
|
||||
"学员名称": " 罗俊杰 ",
|
||||
"学分": "82",
|
||||
"班级排名": "19"
|
||||
},
|
||||
{
|
||||
"学员名称": "苏艺",
|
||||
"学分": "81",
|
||||
"班级排名": "20"
|
||||
},
|
||||
{
|
||||
"学员名称": " 萧欣悦 ",
|
||||
"学分": "80",
|
||||
"班级排名": "21"
|
||||
},
|
||||
{
|
||||
"学员名称": " 胡梓豪 ",
|
||||
"学分": "79",
|
||||
"班级排名": "22"
|
||||
},
|
||||
{
|
||||
"学员名称": " 黄思源 ",
|
||||
"学分": "79",
|
||||
"班级排名": "23"
|
||||
},
|
||||
{
|
||||
"学员名称": " 谢睿 ",
|
||||
"学分": "78",
|
||||
"班级排名": "24"
|
||||
},
|
||||
{
|
||||
"学员名称": " 徐天佑 ",
|
||||
"学分": "77",
|
||||
"班级排名": "25"
|
||||
},
|
||||
{
|
||||
"学员名称": "赵唯",
|
||||
"学分": "76",
|
||||
"班级排名": "26"
|
||||
},
|
||||
{
|
||||
"学员名称": " 王志远 ",
|
||||
"学分": "75",
|
||||
"班级排名": "27"
|
||||
},
|
||||
{
|
||||
"学员名称": " 郭明轩 ",
|
||||
"学分": "74",
|
||||
"班级排名": "28"
|
||||
},
|
||||
{
|
||||
"学员名称": "刘何",
|
||||
"学分": "73",
|
||||
"班级排名": "29"
|
||||
},
|
||||
{
|
||||
"学员名称": " 董嘉豪 ",
|
||||
"学分": "73",
|
||||
"班级排名": "30"
|
||||
},
|
||||
{
|
||||
"学员名称": "孙可寒",
|
||||
"学分": "72",
|
||||
"班级排名": "31"
|
||||
},
|
||||
{
|
||||
"学员名称": " 刘凯文 ",
|
||||
"学分": "71",
|
||||
"班级排名": "32"
|
||||
},
|
||||
{
|
||||
"学员名称": " 郑浩宇 ",
|
||||
"学分": "70",
|
||||
"班级排名": "33"
|
||||
},
|
||||
{
|
||||
"学员名称": " 曹文轩 ",
|
||||
"学分": "69",
|
||||
"班级排名": "34"
|
||||
},
|
||||
{
|
||||
"学员名称": " 朱佳怡 ",
|
||||
"学分": "68",
|
||||
"班级排名": "35"
|
||||
},
|
||||
{
|
||||
"学员名称": "王烜皓",
|
||||
"学分": "67",
|
||||
"班级排名": "36"
|
||||
},
|
||||
{
|
||||
"学员名称": " 杨鹏飞 ",
|
||||
"学分": "66",
|
||||
"班级排名": "37"
|
||||
},
|
||||
{
|
||||
"学员名称": " 高文博 ",
|
||||
"学分": "65",
|
||||
"班级排名": "38"
|
||||
},
|
||||
{
|
||||
"学员名称": " 冯慧敏 ",
|
||||
"学分": "63",
|
||||
"班级排名": "39"
|
||||
},
|
||||
{
|
||||
"学员名称": "丁嘉敏",
|
||||
"学分": "58",
|
||||
"班级排名": "40"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user