Compare commits

...

2 Commits

Author SHA1 Message Date
KQL
6443f2794e feat: 统一简历详情显示样式并更新修改版简历内容
- 统一所有岗位卡片的简历详情显示样式
- 移除ResumeInfoModal组件中的markdown渲染逻辑
- 重新导入10个岗位的修改版简历内容并清理格式
- 删除删除线内容和加粗格式符号

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 10:21:06 +08:00
KQL
4f64941d85 feat: 添加HR访问量弹窗和日历事项样式优化
- 新增HR访问详情弹窗组件,支持左右切换查看不同HR信息
- 优化日历事项样式系统,基于事件类型匹配样式配置
- 完善侧边栏HR访问量组件,添加重叠头像和点击交互
- 增加班级排名弹窗组件
- 更新专家支持页面布局和样式

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 18:40:40 +08:00
34 changed files with 7552 additions and 2041 deletions

View File

@@ -57,7 +57,8 @@
"Bash(kill:*)", "Bash(kill:*)",
"Bash(find:*)", "Bash(find:*)",
"Bash(pgrep:*)", "Bash(pgrep:*)",
"Bash(npm start)" "Bash(npm start)",
"Bash(xargs kill:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@arco-design/web-react": "^2.66.5", "@arco-design/web-react": "^2.66.5",
"@chatui/core": "^3.2.0",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",

54
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@arco-design/web-react': '@arco-design/web-react':
specifier: ^2.66.5 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) 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': '@dnd-kit/core':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.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: peerDependencies:
'@babel/core': ^7.0.0-0 '@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': '@babel/runtime@7.28.3':
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -199,6 +206,12 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'} 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': '@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies: peerDependencies:
@@ -710,6 +723,10 @@ packages:
character-reference-invalid@2.0.1: character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} 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: color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -749,6 +766,12 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'} 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: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -791,6 +814,9 @@ packages:
dom-helpers@5.2.1: dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dompurify@2.5.8:
resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1028,6 +1054,9 @@ packages:
inline-style-parser@0.2.4: inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
intersection-observer@0.12.2:
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
is-alphabetical@2.0.1: is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
@@ -1776,6 +1805,10 @@ snapshots:
'@babel/core': 7.28.3 '@babel/core': 7.28.3
'@babel/helper-plugin-utils': 7.27.1 '@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/runtime@7.28.3': {}
'@babel/template@7.27.2': '@babel/template@7.27.2':
@@ -1801,6 +1834,17 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 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)': '@dnd-kit/accessibility@3.1.1(react@18.3.1)':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -2207,6 +2251,8 @@ snapshots:
character-reference-invalid@2.0.1: {} character-reference-invalid@2.0.1: {}
clsx@1.2.1: {}
color-convert@1.9.3: color-convert@1.9.3:
dependencies: dependencies:
color-name: 1.1.3 color-name: 1.1.3
@@ -2243,6 +2289,10 @@ snapshots:
cookie@1.0.2: {} cookie@1.0.2: {}
core-js-pure@3.45.1: {}
core-js@3.45.1: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -2278,6 +2328,8 @@ snapshots:
'@babel/runtime': 7.28.3 '@babel/runtime': 7.28.3
csstype: 3.1.3 csstype: 3.1.3
dompurify@2.5.8: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -2552,6 +2604,8 @@ snapshots:
inline-style-parser@0.2.4: {} inline-style-parser@0.2.4: {}
intersection-observer@0.12.2: {}
is-alphabetical@2.0.1: {} is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1: is-alphanumerical@2.0.1:

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

View File

@@ -13,6 +13,12 @@
justify-content: flex-start; justify-content: flex-start;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
&::after { &::after {
content: ""; content: "";

View File

@@ -1,11 +1,14 @@
import { useState } from "react";
import { Avatar, Spin, Empty } from "@arco-design/web-react"; import { Avatar, Spin, Empty } from "@arco-design/web-react";
import IconFont from "@/components/IconFont"; import IconFont from "@/components/IconFont";
import ClassRankModal from "@/components/ClassRankModal";
import "./index.css"; import "./index.css";
const positions = ["item2", "item1", "item3"]; const positions = ["item2", "item1", "item3"];
const icons = ["icon2", "icon1", "icon3"]; const icons = ["icon2", "icon1", "icon3"];
const Rank = ({ className, data, loading }) => { const Rank = ({ className, data, loading }) => {
const [modalVisible, setModalVisible] = useState(false);
const rankings = data?.rankings?.slice(0, 6) || []; const rankings = data?.rankings?.slice(0, 6) || [];
// 安全处理领奖台学生确保至少有3个位置 // 安全处理领奖台学生确保至少有3个位置
@@ -17,8 +20,17 @@ const Rank = ({ className, data, loading }) => {
const listStudents = rankings.slice(3); const listStudents = rankings.slice(3);
const handleClick = () => {
setModalVisible(true);
};
return ( return (
<div className={`module-class-rank ${className}`}> <>
<div
className={`module-class-rank ${className}`}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
<p className="module-class-rank-title"> <p className="module-class-rank-title">
<IconFont className="title-icon" src="recuUY5nNf7DWT" /> <IconFont className="title-icon" src="recuUY5nNf7DWT" />
<span>班级排名</span> <span>班级排名</span>
@@ -74,7 +86,14 @@ const Rank = ({ className, data, loading }) => {
</ul> </ul>
</> </>
)} )}
</div> </div>
{/* 班级排名弹窗 */}
<ClassRankModal
visible={modalVisible}
onClose={() => setModalVisible(false)}
/>
</>
); );
}; };

View 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;
}
}
}

View 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;

View 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;
}

View 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;

View File

@@ -86,13 +86,22 @@
} }
.visitor-count { .visitor-count {
width: 208px; width: 208px;
height: 44px; height: 64px;
border-radius: 8px; border-radius: 8px;
background-color: #e5f1ff; background-color: #e5f1ff;
position: relative; position: relative;
margin-top: 10px; margin-top: 10px;
box-sizing: border-box; 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 { &::after {
content: ""; content: "";
@@ -103,12 +112,63 @@
height: 100%; height: 100%;
background-image: url("@/assets/images/Sidebar/visitor_count_bg.png"); background-image: url("@/assets/images/Sidebar/visitor_count_bg.png");
background-size: 100% 100%; background-size: 100% 100%;
z-index: 1;
} }
.arco-statistic-value { .hr-visitor-content {
font-size: 12px; position: relative;
font-weight: 700; z-index: 2;
line-height: 41px; 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 { .sidebar-menu {

View File

@@ -1,7 +1,8 @@
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { Statistic } from "@arco-design/web-react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import IconFont from "@/components/IconFont"; import IconFont from "@/components/IconFont";
import HRVisitModal from "@/components/HRVisitModal";
import ICON from "@/assets/images/Sidebar/sidebar_icon.png"; import ICON from "@/assets/images/Sidebar/sidebar_icon.png";
import ICONRETRACT from "@/assets/images/Sidebar/logo.png"; import ICONRETRACT from "@/assets/images/Sidebar/logo.png";
import BTNICON from "@/assets/images/Sidebar/btn_icon.png"; import BTNICON from "@/assets/images/Sidebar/btn_icon.png";
@@ -12,6 +13,7 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const studentInfo = useSelector((state) => state.student.studentInfo); const studentInfo = useSelector((state) => state.student.studentInfo);
const [hrModalVisible, setHrModalVisible] = useState(false);
const handleNavClick = (path) => { const handleNavClick = (path) => {
navigate(path); navigate(path);
}; };
@@ -21,6 +23,16 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
setIsCollapsed((prev) => !prev); setIsCollapsed((prev) => !prev);
}; };
// 打开HR访问详情弹窗
const handleHRClick = () => {
setHrModalVisible(true);
};
// 关闭HR访问详情弹窗
const handleCloseHRModal = () => {
setHrModalVisible(false);
};
return ( return (
<div <div
className={`${ className={`${
@@ -45,12 +57,31 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
</div> </div>
)} )}
</div> </div>
<Statistic <div className="visitor-count" onClick={handleHRClick}>
className="visitor-count" <div className="hr-visitor-content">
groupSeparator <span className="hr-visitor-text">HR访问量</span>
value={87} <div className="hr-avatars-wrapper">
prefix="HR访问量" <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"> <ul className="sidebar-menu">
{routes {routes
.filter((item) => item.showMenu) .filter((item) => item.showMenu)
@@ -90,6 +121,12 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
<div className="sidebar-btn" onClick={toggleSidebar}> <div className="sidebar-btn" onClick={toggleSidebar}>
<img src={BTNICON} alt="btn" className="sidebar-btn-icon" /> <img src={BTNICON} alt="btn" className="sidebar-btn-icon" />
</div> </div>
{/* HR访问详情弹窗 */}
<HRVisitModal
visible={hrModalVisible}
onClose={handleCloseHRModal}
/>
</div> </div>
); );
}; };

View File

@@ -4193,41 +4193,41 @@ const resumeTemplates = {
### (二)实习岗位:景区运营助理 ### (二)实习岗位:景区运营助理
### (三)实习时间:XXXX时间 ### (三)实习时间:2023.12-2024.02
### (四)实习单位:某某公司 ### (四)实习单位:湖北视界文旅数字科技有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 协助提供现场咨询服务,包括研学项目、展览、节庆活动信息等,~~处理~~ **在指导下配合处理票务咨询、常见问询及游客投诉,确保游客能及时得到解答并维持顺畅的服务体验** 1. 协助提供现场咨询服务,包括研学项目、展览、节庆活动信息等, 在指导下配合处理票务咨询、常见问询及游客投诉,确保游客能及时得到解答并维持顺畅的服务体验;
2. 收集研学人数、游客满意度、活动参与率等数据,~~通过~~ **并协助整理成热力图或统计报表,为团队提供数据依据,支持高峰区域分析和排程优化** 2. 收集研学人数、游客满意度、活动参与率等数据, 并协助整理成热力图或统计报表,为团队提供数据依据,支持高峰区域分析和排程优化;
3. 支持花展、小自然音乐节、夜游等节庆活动的运营,包括~~流程组织、灯光布置、分区限流与应急预案配合~~ **协助流程组织、灯光布置检查、分区限流执行与应急预案落实,帮助活动顺利推进** 3. 支持花展、小自然音乐节、夜游等节庆活动的运营,包括 协助流程组织、灯光布置检查、分区限流执行与应急预案落实,帮助活动顺利推进;
4. 协助导视与游线设置,~~配合空间动线优化,提升~~ **参与空间动线优化,帮助提升**游客行进节奏与整体文化体验感; 4. 协助导视与游线设置, 参与空间动线优化,帮助提升游客行进节奏与整体文化体验感;
5. 支持文创市集运营,包括~~产品场景布置、IP传播素材拍摄与社区联动推广~~ **协助产品场景布置、配合IP传播素材拍摄及社区联动推广让市集氛围更完整** 5. 支持文创市集运营,包括 协助产品场景布置、配合IP传播素材拍摄及社区联动推广让市集氛围更完整
6. 配合设施维护、安全巡查与环境卫生管理,~~确保~~ **协助检查并确认导视、照明、打卡设施及垃圾分类落实到位,提升现场秩序和安全性** 6. 配合设施维护、安全巡查与环境卫生管理, 协助检查并确认导视、照明、打卡设施及垃圾分类落实到位,提升现场秩序和安全性;
7. 协助处理高峰期游客拥堵、设备故障等突发情况,~~配合实施~~ **在预案指引下配合实施疏散与应急引导,并及时反馈情况,减少突发事件影响** 7. 协助处理高峰期游客拥堵、设备故障等突发情况, 在预案指引下配合实施疏散与应急引导,并及时反馈情况,减少突发事件影响;
8. 协助整理活动文档、流程手册与导览模板,~~为未来复用提供标准资料支撑~~ **帮助团队形成可复用的标准资料,为后续活动执行和规范化提供支持** 8. 协助整理活动文档、流程手册与导览模板, 帮助团队形成可复用的标准资料,为后续活动执行和规范化提供支持。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. 熟悉景区现场运营流程,能~~协助导览服务、动线管理与活动节奏控制~~ **在团队指导下协助导览服务,参与动线管理与活动节奏控制,帮助维持整体秩序** 1. 熟悉景区现场运营流程,能 在团队指导下协助导览服务,参与动线管理与活动节奏控制,帮助维持整体秩序;
2. ~~擅长处理游客咨询与投诉,参与提升服务质量与满意度~~ 2.
**能配合处理游客咨询与一般性投诉,协助提升服务质量与游客满意度** 能配合处理游客咨询与一般性投诉,协助提升服务质量与游客满意度;
3. 具备基础的数据收集与报告能力,能~~辅助运营数据决策支持~~ **协助统计游客人数、满意度等基础数据,并整理成简要报表,为运营决策提供支持** 3. 具备基础的数据收集与报告能力,能 协助统计游客人数、满意度等基础数据,并整理成简要报表,为运营决策提供支持;
4. 能参与节庆活动~~点位布置、流程排练及时序控制~~ **点位布置、流程排练及现场时序的执行配合** 4. 能参与节庆活动 点位布置、流程排练及现场时序的执行配合;
5. 拥有现场应急协助经验,能~~配合处理突发状况~~ **在预案指导下配合处理游客拥堵、设备故障等突发状况** 5. 拥有现场应急协助经验,能 在预案指导下配合处理游客拥堵、设备故障等突发状况;
6. ~~了解文创传播节奏与素材管理可支持IP推广与现场体验~~ 6.
**了解文创传播节奏与素材管理能在现场协助IP推广与体验环节的执行** 了解文创传播节奏与素材管理能在现场协助IP推广与体验环节的执行
7. 具备设施巡检与环境维护~~指导经验,确保服务环境优化~~ 7. 具备设施巡检与环境维护
**协助进行设施巡检与环境维护,帮助确保导视、照明和环境卫生到位,优化服务环境** 协助进行设施巡检与环境维护,帮助确保导视、照明和环境卫生到位,优化服务环境。
### (二)复合能力 ### (二)复合能力
@@ -4243,7 +4243,7 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,所学"智慧旅游技术应用"专业背景~~支撑我对文旅行业的理解~~ **让我逐步建立起对文旅行业运行模式和前沿趋势的理解**。我在"武汉植物园生态科普与文旅运营项目"中,~~深度参与现场导览支持、活动执行、安全维护及数字互动部署~~ **主要参与现场导览支持、活动执行、安全维护及数字互动等环节的协助工作,积累了较为系统的一线运营经验**。同时,我关注环保与文旅融合策略,~~具备协同共赢与可持续运营意识~~ **并在项目中体会到协同共赢与可持续运营的重要性**。未来期望在景区运营与文旅生态融合方向继续成长,成为~~一名具备执行力、数据敏感度和服务意识的专业运营人才~~ **一名具备扎实执行力、数据敏感度和服务意识的专业运营型人才**` 我是一名刚完成实习的大专毕业生,所学智慧旅游技术应用专业背景 让我逐步建立起对文旅行业运行模式和前沿趋势的理解。我在武汉植物园生态科普与文旅运营项目中, 主要参与现场导览支持、活动执行、安全维护及数字互动等环节的协助工作,积累了较为系统的一线运营经验。同时,我关注环保与文旅融合策略, 并在项目中体会到协同共赢与可持续运营的重要性。未来期望在景区运营与文旅生态融合方向继续成长,成为 一名具备扎实执行力、数据敏感度和服务意识的专业运营型人才。`
}, },
studentInfo: { studentInfo: {
project_experience: { project_experience: {
@@ -4335,7 +4335,9 @@ const resumeTemplates = {
# 三、个人评价 # 三、个人评价
我具备文旅项目全流程实习经验曾参与武汉植物园生态科普与文旅运营项目系统接触战略定位、课程策划、活动执行与数据复盘等环节积累了扎实的实践能力。在项目中能够独立完成用户调研、数据整理与活动落地执行并协助推进政府合作与多方资源整合。熟练运用Excel及新媒体矩阵进行数据分析与内容运营具备良好的创意策划与执行力。性格上注重责任感与团队协作学习能力强能够快速适应新环境并持续提升综合能力。`, 我具备文旅项目全流程实习经验曾参与武汉植物园生态科普与文旅运营项目系统接触战略定位、课程策划、活动执行与数据复盘等环节积累了扎实的实践能力。在项目中能够独立完成用户调研、数据整理与活动落地执行并协助推进政府合作与多方资源整合。熟练运用Excel及新媒体矩阵进行数据分析与内容运营具备良好的创意策划与执行力。性格上注重责任感与团队协作学习能力强能够快速适应新环境并持续提升综合能力。`,
modified: `# 对应岗位:文旅运营总监助理 modified: `# 文旅运营总监助理
# 对应岗位:文旅运营总监助理
# 一、项目经历 # 一、项目经历
@@ -4343,30 +4345,31 @@ const resumeTemplates = {
### (二)实习岗位:文旅运营总监助理 ### (二)实习岗位:文旅运营总监助理
### (三)实习单位:某某公司 ### (三)实习单位:2024.02-2024.03
### (四)实习时间:XXXX时间 ### (四)实习时间:湖北视界文旅数字科技有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 协助项目组完成“战略定位—课程策划—空间优化—活动执行—复盘分析”的全流程任务,参与收集客流量、满意度等核心指标,整理并录入数据支持运营策略调整。 1. 项目组指导下参与完成“战略定位—课程策划—空间优化—活动执行—复盘分析”的全流程任务, 协助收集并整理客流量、满意度等核心指标, 录入数据支持运营策略调整。
2. 参与研学产品与节庆活动的创意策划,协助整理文案活动方案,配合落地执行“研学课程+主题市集+夜游演艺”等组合项目,完成现场物资与流程管控工作 2. 跟随团队参与研学产品与节庆活动的创意策划,协助完成文案活动方案整理,配合执行落地的“研学课程+主题市集+夜游演艺”等组合项目, 并参与现场物资准备和流程协助
3. 协助收集用户调研数据,包括问卷、访谈现场反馈,整理消费者画像与偏好要点,支持输出用户需求分析表跟进复盘优化建议。 3. 在实习过程中协助收集用户调研数据(如问卷、访谈现场反馈,整理消费者画像与偏好要点,支持形成用户需求分析表,并配合团队跟进复盘优化建议。
4. 跟随总监参与政府合作与政策资料准备,协助撰写项目支持性文档,整理旅行社文创品牌的合作资料,维护合作清单与联动档案。 4. 在总监带领下参与政府合作与政策资料准备,协助撰写项目相关文档,整理旅行社文创品牌的合作资料,配合维护合作清单与联动档案。
5. 使用Excel整理项目运营数据客单价、复购率、留存率),参与搭建指标看板,配合绘制甘特图跟踪活动进度,支持项目里程碑复盘。 5. 使用Excel整理项目运营数据客单价、复购率、留存率在团队指导下参与搭建指标看板,配合绘制甘特图跟踪进度,协助项目里程碑复盘。
6. 参与数字化导览与新媒体矩阵内容的日常运维,协助完成抖音、小红书等平台的内容发布与互动,收集曝光量转化数据,支持社群与会员体系建设。 6. 协助参与数字化导览与新媒体矩阵的日常运维,配合完成抖音、小红书等平台的内容发布与互动,整理曝光量转化数据,支持社群与会员体系建设。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. 深刻理解项目从孵化、策划、执行到复盘的全流程。能协助制定数据驱动的运营策略,并通过精细化执行确保项目目标(如知名度、客流量、满意度)达成。 1. 在学习和实习中逐步理解项目从孵化、策划、执行到复盘的全流程。能协助制定数据驱动的运营策略,并通过 配合执行支持项目目标(如知名度、客流量、满意度)达成。
2. 具备出色的创意发散与落地执行能力,善于挖掘文化IP与旅游资源的结合点策划主题市集、深度研学、国潮快闪等创新活动。精通线上线下整合营销,能有效利用新媒体矩阵(抖音、小红书、视频号)进行内容种草与事件营销,实现品牌声量与客流转化双提升 2. 具备一定的创意发散与执行能力,能协助挖掘文化IP与旅游资源的结合点参与策划主题市集、深度研学、国潮快闪等创新活动。线上线下整合营销有一定了解,能配合使用新媒体矩阵(抖音、小红书、视频号)进行内容种草与事件营销, 支持提升品牌声量与客流转化。
3. 熟练运用SWOT、PESTEL等分析模型具备宏观市场视野与微观用户洞察。能通过问卷调研、用户访谈、数据抓取等方式分析消费者偏好与行为路径并据此迭代运营策略,优化客户体验,提升复购率与忠诚度。 3. 在课程与实践中学习使用SWOT、PESTEL等分析模型具备宏观市场视野与微观用户洞察。能通过问卷调研、用户访谈、数据抓取等方式 参与分析消费者偏好与行为路径,并据此 提出改进建议,支持优化运营策略,优化客户体验,提升复购率与忠诚度。
4. 掌握与文化、旅游等政府部门的有效沟通方法与公文基础,能协助撰写项目申报材料,争取政策与资金支持。具备强大的资源整合与商务拓展能力,善于同景区、酒店、旅行社、文创品牌等建立战略合作,构建互利共赢的文旅生态联盟。 4. 逐步掌握与文化、旅游等政府部门的有效沟通方法与公文基础,能协助撰写项目申报材料, 支持申请政策与资金支持。具备 初步的资源整合意识,能参与与景区、酒店、旅行社、文创品牌 **,构建互利共赢的文旅生态联盟。
5. 具备优秀的多任务并行处理能力,能使用甘特图等工具管理项目里程碑。擅长作为沟通枢纽,高效协调内外部团队(市场、设计、地推、合作方),明确分工、拉通信息、解决梗阻,确保项目在预算期内高质量交付 5. 在项目中逐步锻炼多任务处理能力,能在指导下使用甘特图等工具跟进项目里程碑。协助作为沟通枢纽,参与协调市场、设计、地推、合作方等团队,配合分工与信息流转,支持项目在预算期内顺利推进
6. 以产品经理思维看待文旅项目能参与用户调研、需求分析、可行性论证及MVP测试等新项目开发环节。对现有项目能提出数据化的优化建议(如引入沉浸式科技、开发衍生文创品),持提升项目的市场竞争力和商业价值。 6. 在学习中尝试以产品思维理解文旅项目,能协助参与用户调研、需求分析、可行性论证及MVP测试等开发环节。对现有项目能配合提出优化建议(如引入沉浸式科技、开发文创衍生品),持提升项目竞争力和商业价值。
7. 精通 Excel数据透视表、VLOOKUP及基础BI工具能构建关键指标看板如客单价、转化率、留存率监控项目健康度。通过用户分层与精准触达策划会员成长体系与私域社群活动实现用户的生命周期管理与价值最大化。 7. 熟悉并逐步掌握 Excel数据透视表、VLOOKUP及基础BI工具 协助构建关键指标看板(如客单价、转化率、留存率),监控项目健康度。通过用户分层与精准触达, 参与策划会员成长体系与私域社群活动,实现用户的生命周期管理与价值最大化。
### (二)复合能力 ### (二)复合能力
@@ -4382,7 +4385,7 @@ const resumeTemplates = {
# 三、个人评价 # 三、个人评价
我具备文旅项目全流程实习经验,曾参与武汉植物园生态科普与文旅运营项目,系统接触战略定位、课程策划、活动执行与数据复盘等环节,积累了扎实的实践能力。在项目中能够独立完成用户调研、数据整理与活动落地执行,并协助推进政府合作多方资源整合。熟练运用Excel及新媒体矩阵进行数据分析与内容运营,具备良好的创意策划与执行力。性格上注重责任感与团队协作,学习能力强,能够快速适应新环境并持续提升综合能力。` 我具备文旅项目全流程实习经验,曾参与武汉植物园生态科普与文旅运营项目,系统接触战略定位、课程策划、活动执行与数据复盘等环节,积累了扎实的实践能力。在项目中 主要参与并协助完成用户调研、数据整理与活动落地执行, 同时在团队指导下配合推进政府合作多方资源整合。熟练运用Excel及 能够运用Excel进行基础数据整理并在新媒体矩阵中协助数据分析与内容运营**,具备良好的创意策划与执行力。性格上注重责任感与团队协作,学习能力强,能够快速适应新环境并持续提升综合能力。`
}, },
studentInfo: { studentInfo: {
project_experience: null, project_experience: null,
@@ -4450,41 +4453,45 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营抱有浓厚兴趣。在"汽车品牌线下推广活动"项目中我协助完成了从物料验收、供应链协调、现场流程控制到客户引导及UGC互动的多个环节。实习期间我表现细致且应变能力强能主动配合团队推进流程并快速响应突发情况。未来希望在文旅或品牌线下推广领域不断实践成长成为一名执行力出色、善于沟通与复盘的项目执行专家。`, 我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营抱有浓厚兴趣。在"汽车品牌线下推广活动"项目中我协助完成了从物料验收、供应链协调、现场流程控制到客户引导及UGC互动的多个环节。实习期间我表现细致且应变能力强能主动配合团队推进流程并快速响应突发情况。未来希望在文旅或品牌线下推广领域不断实践成长成为一名执行力出色、善于沟通与复盘的项目执行专家。`,
modified: `# 对应岗位:活动策划师 modified: `# 活动策划师
# 对应岗位:活动策划师
# 一、项目经历 # 一、项目经历
### (一)项目名称:春风 450MT新品上市营销活动策划项目 ### (一)项目名称:春风 450MT新品上市营销活动策划项目
### (二)实习岗位:活动策划助理 ### (二)实习岗位:活动策划助理
### 实习时间2024.06-2024.08 ### 实习时间2024.06-2024.07
### (四)实习单位:某某公司 ### (四)实习单位:浙江春风动力股份有限公司
### (五)岗位职责: ### (五)岗位职责:
1. ~~主导制定~~**协助制定**"T 系列车型'驭速觉醒'"传播总纲,~~拆解~~**参与拆解**线上曝光、线下参与、社交互动等核心 KPI~~规划~~**配合规划**成都/长沙/杭州三个城市的传播节奏及分站团队协作结构; 1. 协助制定T 系列车型驭速觉醒’”传播总纲, 参与拆解线上曝光、线下参与、社交互动等核心 KPI 配合规划成都/长沙/杭州三个城市的传播节奏及分站团队协作结构;
2. ~~收集 Z 世代用户调研及竞品数据,撰写传播主题互动脚本素材,搭建传播方案框架~~**协助收集用户调研数据,参与撰写基础文案素材**,并~~完成文案与软文投放结构设计~~**协助整理投放资料** 2. 协助收集 Z 世代用户调研及竞品数据,参与撰写传播主题互动脚本,配合搭建传播方案框架,并协助完成文案与软文投放结构整理
3. ~~统筹并设计~~**参与设计**线下互动场景,融合骑行线路与城市地标,~~打造~~**协助打造**沉浸式内容互动体验; 3. 参与设计线下互动场景,融合骑行线路与城市地标, 协助营造沉浸式内容互动体验;
4. ~~构建三级内容驱动矩阵~~**协助搭建内容矩阵**~~设定~~**参与设定**发布时间节奏与话题标签,~~推动~~**支持**内容裂变传播; 4. 配合团队构建三级内容驱动矩阵参与设定发布时间节奏与话题标签,支持内容裂变传播;
5. ~~撰写~~**协助撰写**城市站主持串场稿、互动指引语稿达人任务脚本及~~视觉稿校对~~**参与校对工作**~~确保~~**协助确保**线上线下语言与视觉调性统一; 5. 协助撰写城市站主持串场稿、互动指引语稿达人任务脚本,并参与视觉稿校对,帮助确保线上线下语言与视觉调性统一;
6. ~~制定活动预算~~**协助整理预算表**(含达人投入、物料成本、媒体费等),~~控制~~**学习控制**每人试乘成本; 6. 协助整理活动预算数据(达人投入、物料成本、媒体费等),并学习参与控制每人试乘成本;
7. ~~协调~~**协助联系**品牌方、执行方、设计、公关及场地团队~~监督~~**跟进**进度及质量,~~建立城市协调机制~~**配合团队**以应对现场突发状况; 7. 协助品牌方、执行方、设计、公关及场地团队的沟通,参与进度跟进和质量检查,配合应对现场突发状况;
8. ~~汇总~~**协助收集**试乘转化率、参与人数互动反馈等数据,~~撰写~~**参与撰写**传播成效报告并~~提炼~~**协助整理**可以复用的模板与优化建议; 8. 协助汇总试乘转化率、参与人数互动反馈等数据,配合撰写传播成效报告,并参与整理可复用的模板与优化建议;
9. ~~持续关注~~**学习了解**摩托车及文旅跨界营销趋势,~~支持~~**协助支持**后续内容规划与策略迭代。 9. 在学习过程中关注摩托车及文旅跨界营销趋势,协助支持后续内容规划与策略迭代。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~熟悉~~**了解**活动策划完整流程,能协助从前期调研、传播定位方案拆解与传播矩阵设计; 1. 熟悉活动策划完整流程,能在团队指导下协助完成前期调研、传播定位方案拆解与传播矩阵设计;
2. 能~~独立~~**在指导下**撰写策划文案与流程脚本,协助呈报优化执行逻辑; 2. 能参与撰写策划文案与流程脚本,协助呈报优化执行逻辑;
3. 具备场地互动形式与内容体验设计~~经验~~**基础知识**,能参与打卡装置及互动环节策划; 3. 具备一定的场地互动与内容体验设计经验,能协助参与打卡装置及互动环节策划;
4. ~~熟悉~~**了解**预算方案编制与成本监控,~~控制~~**协助控制**成本及物料资源消耗在合理的范围内 4. 熟悉预算方案编制与成本监控,能配合团队把控成本并协助监控物料资源消耗
5. ~~拥有~~**具备基础的**活动数据跟踪与传播效果分析能力,能~~生成~~**协助生成**复盘报告; 5. 拥有活动数据跟踪与传播效果分析能力,能协助进行数据跟踪与传播效果整理,参与撰写复盘报告;
6. ~~擅长~~**能够参与**品牌协调与资源统筹,对接品牌方、达人设计等跨界团队; 6. 能协助进行品牌协调与资源统筹,配合对接品牌方、达人设计等跨界团队;
7. ~~熟练~~**能够**操作办公软件与协作文档工具PPT/Excel/Notion/AIGC 工具等),能支内容制作与流程协作。 7. 熟练使用常用办公软件与协作文档工具PPT/Excel/Notion/AIGC 工具等),能支内容制作与流程协作。
### (二)复合能力 ### (二)复合能力
@@ -4500,7 +4507,7 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修会展策划与管理专业,对活动创意策划与传播运营有浓厚兴趣。在"摩托车新品上市营销活动"中,我全程参与方案撰写、场景设计、传播落地、达人协作与效果追踪,逐步理解从创意触发到客户体验再到复盘优化的活动闭环逻辑。实习让我锻炼了文案表达、资源协调与成本控制的能力,并提升了跨部门沟通和执行力。我热爱策划工作,愿意不断学习,未来希望成为一名善于创意、数据敏感且执行落地力强的活动策划师` 我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营有浓厚兴趣。在“汽车品牌线下推广活动”项目中,我 主要参与并协助完成了物料验收、供应链协调、现场流程控制、客户引导以及UGC互动等环节在过程中逐步积累了系统理解与实际操作经验。实习期间我表现细致且应变能力强 能够主动配合团队推进流程,在现场突发情况中快速响应并学习处理方法。未来希望在文旅或品牌线下推广领域不断实践成长,成为 具备扎实执行力、善于沟通与复盘能力的项目执行型人才`
} }
}, },
{ {
@@ -4559,7 +4566,9 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营抱有浓厚兴趣。在"汽车品牌线下推广活动"项目中我协助完成了从物料验收、供应链协调、现场流程控制到客户引导及UGC互动的多个环节。实习期间我表现细致且应变能力强能主动配合团队推进流程并快速响应突发情况。未来希望在文旅或品牌线下推广领域不断实践成长成为一名执行力出色、善于沟通与复盘的项目执行专家。`, 我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营抱有浓厚兴趣。在"汽车品牌线下推广活动"项目中我协助完成了从物料验收、供应链协调、现场流程控制到客户引导及UGC互动的多个环节。实习期间我表现细致且应变能力强能主动配合团队推进流程并快速响应突发情况。未来希望在文旅或品牌线下推广领域不断实践成长成为一名执行力出色、善于沟通与复盘的项目执行专家。`,
modified: `# 对应岗位:活动执行 modified: `# 活动执行
# 对应岗位:活动执行
# 一、项目经历 # 一、项目经历
@@ -4567,50 +4576,50 @@ const resumeTemplates = {
### (二)实习岗位:活动执行助理 ### (二)实习岗位:活动执行助理
### 实习时间2024.03-2024.05 ### 实习时间2024.04-2024.06
### (四)实习单位:某某公司 ### (四)实习单位:四川西行驿站文化传播有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 按照总部标准流程在活动开展的城市~~落地活动策划的"三大主题×五段流程"~~**协助布置活动现场**,包含物料布置及~~规范摆放审查~~**基础摆放工作** 1. 按照总部标准流程在活动开展的城市 协助落地活动策划的三大主题×五段流程,包含物料布置及 规范摆放的检查与整理
2. ~~跟进物料供应商交付、配送及搭建进度~~**协助跟进物料配送**~~确保~~**配合确保**活动搭建在开始日前完成,并参与流程彩排; 2. 协助跟进物料供应商交付、配送及搭建进度,配合团队确保活动搭建在开始日前完成,并参与流程彩排;
3. ~~主控现场活动流程~~**协助现场活动流程**,包括签到、串场、试驾、抽奖等环节,保障节奏符合流程要求; 3. 协助跟进现场活动流程,配合团队完成签到、串场、试驾、抽奖等环节,保障节奏符合流程要求;
4. 应对现场突发状况(如雨天变化、音响故障或客流异常),~~配合启用预案进行快速响应~~**在指导下参与应急处理** 4. 应对现场突发状况(如雨天变化、音响故障或客流异常), 在指导下配合启用预案,参与快速响应;
5. 协助收集客户登记信息、扫码采集、~~平台导入工作~~**数据整理工作** 5. 协助收集客户登记信息、扫码采集、平台导入工作;
6. ~~负责~~**协助**奖品发放及签到礼品等活动内容的执行,覆盖试驾礼包及竞拍奖品派发; 6. 参与奖品发放及签到礼品等活动执行,覆盖试驾礼包及竞拍奖品派发;
7. ~~实时监控~~**协助记录**签到率、互动频次、成交数等关键指标,~~并在要时协助调整现场执行话术~~**反馈给负责人** 7. 协助统计签到率、互动频次、成交数等关键指标,并在要时配合调整现场执行话术;
8. ~~指导客户参与UGC传播~~**引导客户参与社交媒体分享**(如抖音、小红书记录与打卡),协助扫码话题引导内容归档; 8. 指导客户参与UGC传播如抖音、小红书记录与打卡 并配合扫码话题引导内容归档;
9. 活动结束后~~负责~~**协助**现场物料、流程手册等文档分类归档,为后续执行标准化输出提供素材。 9. 活动结束后 协助整理现场物料、流程手册等文档分类归档,为后续执行标准化输出提供素材。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~熟悉~~**了解**活动现场安装流程与现场搭建物料管理,具备协助执行布场与摆放流程经验; 1. 了解活动现场安装流程与现场搭建物料管理,具备参与布场与摆放流程的协助经验;
2. ~~擅长~~**能够参与**供应商协调与进度跟进,支持搭建团队按时间节点完成场地落位彩排; 2. 能协助供应商协调与进度跟进,支持搭建团队按时间节点完成场地落位彩排;
3. 能够~~快速应对突发状况~~**在指导下应对突发状况**,协助启动应急预案保障现场活动顺利推进; 3. 能够快速应对突发状况,在需要时配合团队启动应急预案保障现场活动顺利推进;
4. 能助客户扫码登记与~~CRM信息录入~~**基础信息收集**,确保信息完整准确; 4. 能助客户扫码登记与CRM信息录入帮助确保信息完整准确;
5. 具备~~UGC互动引导能力~~**基础的社交媒体引导能力**,提升用户打卡互动频率及社交媒体参与度; 5. 能配合进行UGC互动引导协助提升用户打卡互动频率及社交媒体参与度;
6. 能协助监测现场关键指标,配合优化活动流程; 6. 能在现场协助监测关键指标,配合团队优化活动流程;
7. 能够~~管理~~**协助整理**活动资料与执行流程文档,支持团队后续的活动执行使用; 7. 能够参与整理活动资料与执行流程文档,支持团队后续的活动执行使用;
8. 拥有较强的执行力与协作能力,能在多部门协调下有效推进现场工作。 8. 拥有较强的执行力与协作能力,能在多部门协调下配合推进现场工作。
### (二)复合能力 ### (二)复合能力
1. 活动执行与现场管理能力:了解活动从前期策划、主题创意、文案撰写、宣传推广,到流程设计、现场布置、预算管理、应急预案及数据复盘的完整流程,具备基础的统筹与执行配合能力。 1. 活动执行与现场管理能力: 了解活动从前期策划、主题创意、文案撰写、宣传推广,到流程设计、现场布置、预算管理、应急预案及数据复盘的完整流程,具备基础的统筹与执行配合能力。
2. 服务体验优化和资源协同能力理解文旅服务的形象表达与情境化设计原则掌握文旅项目中各类资源如住宿、交通、商品的调度逻辑与协同机制具备在多元文化与B2B/B2C场景下开展基础服务支持与供应协作的能力。 2. 服务体验优化和资源协同能力: 理解文旅服务的形象表达与情境化设计原则掌握文旅项目中各类资源如住宿、交通、商品的调度逻辑与协同机制具备在多元文化与B2B/B2C场景下开展基础服务支持与供应协作的能力。
3. 文旅行业理解能力:具备现代文旅产业结构与政策环境的整体认知,了解旅游资源分类、游客行为特征与行业合规要点,初步建立了岗位适应能力。 3. 文旅行业理解能力: 具备现代文旅产业结构与政策环境的整体认知,了解旅游资源分类、游客行为特征与行业合规要点,初步建立了岗位适应能力。
4. 产品营销基础:具备基本营销思维与客户画像构建能力,能够理解产品定位、传播路径与沟通逻辑,支持策划与执行文旅项目的推广策略。 4. 产品营销基础: 具备基本营销思维与客户画像构建能力,能够理解产品定位、传播路径与沟通逻辑,支持策划与执行文旅项目的推广策略。
5. 商业视觉表达与工具应用能力了解平面设计、色彩搭配、字体表现与视觉规范的基础知识能使用Canva、Figma、Photoshop、剪映等工具进行简单的视觉表达与内容制作。 5. 商业视觉表达与工具应用能力: 了解平面设计、色彩搭配、字体表现与视觉规范的基础知识能使用Canva、Figma、Photoshop、剪映等工具进行简单的视觉表达与内容制作。
6. 文化IP策划与品牌传播能力理解本地文化IP的挖掘逻辑与数字化表达方式具备参与品牌定位、跨界联动与差异化推广的初步经验。 6. 文化IP策划与品牌传播能力 理解本地文化IP的挖掘逻辑与数字化表达方式具备参与品牌定位、跨界联动与差异化推广的初步经验。
7. 新媒体平台运营能力:具备新媒体平台账号经营、内容赛道规划与短视频制作的基础能力,能够参与直播搭建、私域维护与跨平台内容策划等简单的工作内容,辅助文旅项目的数字传播落地。 7. 新媒体平台运营能力: 具备新媒体平台账号经营、内容赛道规划与短视频制作的基础能力,能够参与直播搭建、私域维护与跨平台内容策划等简单的工作内容,辅助文旅项目的数字传播落地。
8. AIGC工具使用能力了解AIGC内容生成逻辑能够使用ChatGPT、Stable Diffusion、Suno等AI工具完成图像生成、文案创作与音视频剪辑等初级创作任务。 8. AIGC工具使用能力 了解AIGC内容生成逻辑能够使用ChatGPT、Stable Diffusion、Suno等AI工具完成图像生成、文案创作与音视频剪辑等初级创作任务。
9. 智慧文旅应用能力了解OTA平台、票务分销、导览系统、智能设备及智慧酒店等新型文旅科技应用具备智慧场景运营的基础认知与设备使用能力。 9. 智慧文旅应用能力: 了解OTA平台、票务分销、导览系统、智能设备及智慧酒店等新型文旅科技应用具备智慧场景运营的基础认知与设备使用能力。
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营抱有浓厚兴趣。在"汽车品牌线下推广活动"项目中,我协助完成了从物料验收、供应链协调、现场流程控制客户引导及UGC互动的多个环节。实习期间我表现细致且应变能力强,能主动配合团队推进流程并快速响应突发情况。未来希望在文旅或品牌线下推广领域不断实践成长,成为一名执行力出色、善于沟通与复盘的项目执行专家` 我是一名刚完成实习的大专毕业生,主修会展策划与管理,对活动执行与现场运营抱有浓厚兴趣。在汽车品牌线下推广活动项目中,我 主要协助完成物料验收、供应链协调、现场流程控制客户引导及UGC互动等具体环节,逐步积累了对活动执行的系统理解与实践经验。实习期间我表现细致且应变能力强, 能够在团队指导下主动配合流程推进,并在突发情况中快速响应和学习解决方式。未来希望在文旅或品牌线下推广领域不断实践成长,成为 具备扎实执行力、善于沟通与复盘的项目执行型人才`
} }
}, },
{ {
@@ -6335,7 +6344,9 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名主修市场营销的大专毕业生,对会展项目富有热情。在"2024年深圳国际家具展策划项目"中,我协助完成了从主题定位、视觉素材策划、动线设计、现场执行到传播复盘的多项核心环节,具备流程控制与资源协调的实操经验。我注重细节、具备快速响应能力与多方沟通意识,能够高效配合团队完成任务。未来,我希望继续在会展策划领域深入发展,成为一名兼具策略性思维与执行力的策划专家。`, 我是一名主修市场营销的大专毕业生,对会展项目富有热情。在"2024年深圳国际家具展策划项目"中,我协助完成了从主题定位、视觉素材策划、动线设计、现场执行到传播复盘的多项核心环节,具备流程控制与资源协调的实操经验。我注重细节、具备快速响应能力与多方沟通意识,能够高效配合团队完成任务。未来,我希望继续在会展策划领域深入发展,成为一名兼具策略性思维与执行力的策划专家。`,
modified: `# 对应岗位:会展策划师 modified: `# 会展策划师
# 对应岗位:会展策划师
# 一、项目经历 # 一、项目经历
@@ -6343,35 +6354,35 @@ const resumeTemplates = {
### (二)实习岗位:会展策划师助理 ### (二)实习岗位:会展策划师助理
### 实习时间2024.09-2024.10 ### 实习时间2023.09-2023.11
### (四)实习单位:某某公司 ### (四)实习单位:深圳市华奥展览服务有限公司
### (五)岗位职责: ### (五)岗位职责:
1. ~~协调制定~~ **协助参与**"设计×商业"主题体系及空间动线布局方案保障714号馆的内容叙事与观展体验顺畅 1. 协助参与设计×商业主题体系及空间动线布局方案保障714号馆的内容叙事与观展体验顺畅
2. ~~统筹"五件套"视觉资产主KV、票证样式、导视、大屏及宣传素材设计与执行保持品牌视觉一致性~~**帮忙跟进展会的宣传物料设计如KV、票证、导视牌等保证整体风格统一** 2. 帮忙跟进展会的宣传物料设计如KV、票证、导视牌等保证整体风格统一
3. 制定展商结构与招商节奏策略(头部、腰部、创新类别),~~参与签约计划及展位布局方案~~**配合展位布局与招商执行工作** 3. 制定展商结构与招商节奏策略(头部、腰部、创新类别),配合展位布局与招商执行工作;
4. ~~设计观众预报名与现场分流机制与人流仿真方案,优化观展动线与高峰疏导~~**协助整理预报名数据,并在现场帮忙观众分流和路线指引;** 4. 协助整理预报名数据,并在现场帮忙观众分流和路线指引;
5. 全面协助现场运营组织,包括安检引导、播报提醒、志愿者调配与突发事件应对,维持现场秩序; 5. 全面协助现场运营组织,包括安检引导、播报提醒、志愿者调配与突发事件应对,维持现场秩序;
6. ~~指导展后图文短视频传播、媒体稿件撰写及CRM买家回访并参与复盘总结与买家转化闭环设计~~**展会结束后协助收集图文和视频素材,帮忙写简单的文案,并参加团队复盘;** 6. 展会结束后协助收集图文和视频素材,帮忙写简单的文案,并参加团队复盘;
7. 从项目主题到展后传播落实,协助统筹整体流程节奏与内容节点控制; 7. 从项目主题到展后传播落实,协助统筹整体流程节奏与内容节点控制;
8. ~~支持预算管控、供应商协调与团队资源整合~~ **参与供应商沟通与资料对接**,确保设计方案与执行节奏匹配; 8. 参与供应商沟通与资料对接,确保设计方案与执行节奏匹配;
9. ~~开展市场调研与竞品分析,助力展会主题契合行业趋势及受众需求~~**完成基础的市场调研和竞品记录,作为策划参考;** 9. 完成基础的市场调研和竞品记录,作为策划参考;
10. ~~编写宣传文案与物料说明,确保传播语言一致、精准~~**帮忙写宣传文案,保持语言清楚一致。** 10. 帮忙写宣传文案,保持语言清楚一致。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~能统筹资源(场地、视觉、展商、物料等)并确保主题方案精准落地~~**能在指导下协助场地、视觉、展商、物料等方面的对接,保证方案顺利落地;** 1. 能在指导下协助场地、视觉、展商、物料等方面的对接,保证方案顺利落地;
2. ~~擅长~~ **具备**视觉资产整合与品牌表达,保证统一性与传播效果; 2. 具备视觉资产整合与品牌表达,保证统一性与传播效果;
3. 具备观众动线设计与人流管理意识,优化参观体验; 3. 具备观众动线设计与人流管理意识,优化参观体验;
4. 熟悉现场流程执行与秩序维护,能快速响应突发事件; 4. 熟悉现场流程执行与秩序维护,能快速响应突发事件;
5. 能协助撰写活动宣传与媒体文案,实现传播闭环; 5. 能协助撰写活动宣传与媒体文案,实现传播闭环;
6. ~~能配合预算计划与供应商管理~~ **具备初步预算与供应商管理认知**,支持活动执行节拍控制; 6. 具备初步预算与供应商管理认知,支持活动执行节拍控制;
7. ~~擅于~~ **能够参与**活动复盘与买家跟进,推动效果最大化; 7. 能够参与活动复盘与买家跟进,推动效果最大化;
8. ~~具备~~ **有一定的**市场与竞品分析基础,支持方案精准定位; 8. 有一定的市场与竞品分析基础,支持方案精准定位;
9. 具备跨职能协作意识,与客户、设计、供应、媒体多方协同配合。 9. 具备跨职能协作意识,与客户、设计、供应、媒体多方协同配合。
### (二)复合能力 ### (二)复合能力
@@ -6388,7 +6399,7 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名主修市场营销的大专毕业生,对会展项目富有热情。在"2024年深圳国际家具展策划项目"中,我协助完成了从主题定位、视觉素材策划、动线设计、现场执行到传播复盘的多项核心环节,具备流程控制与资源协调的实操经验。我注重细节、具备快速响应能力与多方沟通意识,能够高效配合团队完成任务。未来,我希续在会展策划领域深入发展,成为一名兼具策略性思维与执行力的策划专家。` 我是一名主修市场营销的大专毕业生,对会展项目富有热情。在2024年深圳国际家具展策划项目中,我协助完成了从主题定位、视觉素材策划、动线设计、现场执行到传播复盘的多项核心环节,具备流程控制与资源协调的实操经验。我注重细节、具备快速响应能力与多方沟通意识,能够高效配合团队完成任务。未来,我希望继续在会展策划领域深入发展,成为一名兼具策略性思维与执行力的策划专家。`
}, },
studentInfo: { studentInfo: {
project_experience: { project_experience: {
@@ -6497,36 +6508,36 @@ const resumeTemplates = {
### 项目名称2024年深圳国际家具展策划项目 ### 项目名称2024年深圳国际家具展策划项目
### (二)实习岗位:会展执行助理 ### (二)实习岗位:会展策划师助理
### 实习时间2024.09-2024.10 ### 实习时间2023.09-2023.11
### (四)实习单位:某某公司 ### (四)实习单位:深圳市华奥展览服务有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 协助布置展会"五件套"物料包含主KV、票证样式、大屏模板、导视牌、社媒素材确保会展视觉一致性~~与质量~~ 1. 协助布置展会五件套物料包含主KV、票证样式、大屏模板、导视牌、社媒素材确保会展视觉一致性
2. 跟进招商节奏与展位对接如90天锁定、60天签约、30天补位~~协调样品到馆与搭建进度~~ **协助展品进场及展位搭建准备** 2. 跟进招商节奏与展位对接如90天锁定、60天签约、30天补位 协助展品进场及展位搭建准备;
3. 协助观众入场组织与流线引导,分流预登记与现场登记流程,维护入口秩序; 3. 协助观众入场组织与流线引导,分流预登记与现场登记流程,维护入口秩序;
4. ~~配合场馆动线模拟与容量控制,协助布置主回路与支线动线示意~~ **参与场馆动线布置,协助流线引导,保证基本畅通** 4. 参与场馆动线布置,协助流线引导,保证基本畅通;
5. 支持现场运营与安全管控,如入口安检、证件核验、隔离引导、~~应急演练执行~~ **简单突发情况也能跟着处理;** 5. 支持现场运营与安全管控,如入口安检、证件核验、隔离引导、 简单突发情况也能跟着处理;
6. 协助撤展安排与交通流线控制,~~指引搬运人员并协调车辆流线,保障现场秩序~~ **帮忙指引搬运和车辆进出;** 6. 协助撤展安排与交通流线控制, 帮忙指引搬运和车辆进出;
7. 展后协助发布图文与短视频内容,整理意向客户线索~~导入CRM并协助生成后续推进清单~~ **交由团队统一整理** 7. 展后协助发布图文与短视频内容,整理意向客户线索 交由团队统一整理;
8. 参与跨部门对接,包括与展商、搭建商、主办方、媒体及志愿者等沟通联络; 8. 参与跨部门对接,包括与展商、搭建商、主办方、媒体及志愿者等沟通联络;
9. 完成活动日志、会议记录及简报撰写,确保信息及时反馈~~与执行闭环~~ **,方便团队后续跟进。** 9. 完成活动日志、会议记录及简报撰写,确保信息及时反馈 ,方便团队后续跟进。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~能协助统筹现场布场与视觉布置工作,保障物料展示与信息一致性~~**能在指导下帮忙布场和物料摆放,保证展示效果一致;** 1. 能在指导下帮忙布场和物料摆放,保证展示效果一致;
2. ~~熟悉展商对接及招商进度管理流程,能协助完成台账维护与多方协调~~ **具备展商对接与台账整理的基础经验,能协助完成进度跟进** 2. 具备展商对接与台账整理的基础经验,能协助完成进度跟进;
3. 具备观众入场和动线引导执行支持能力,配合流程设计与秩序维护; 3. 具备观众入场和动线引导执行支持能力,配合流程设计与秩序维护;
4. ~~能协助策划撤展与交通控制方案,支持现场流线与进出秩序管控~~**能配合撤展安排,帮忙维持现场秩序;** 4. 能配合撤展安排,帮忙维持现场秩序;
5. ~~具备现场客户导入与内容发布能力,能够协助快速整理线索与社媒传播支持~~**能参与展后素材收集和社媒内容的初步整理;** 5. 能参与展后素材收集和社媒内容的初步整理;
6. 善于跨部门沟通协作,可协助协调主办方、展商、媒体等多方执行任务; 6. 善于跨部门沟通协作,可协助协调主办方、展商、媒体等多方执行任务;
7. 熟练运用办公工具撰写执行日志与报告,保障现场信息传达与总结反馈; 7. 熟练运用办公工具撰写执行日志与报告,保障现场信息传达与总结反馈;
8. ~~擅长应急响应协助,包括协力处理突发状况,并支持现场安全维护~~ **具备基础应急响应意识,能在指导下协助处理简单突发情况** 8. 具备基础应急响应意识,能在指导下协助处理简单突发情况;
9. 工作认真、执行力强,具备良好的服务意识与现场组织配合能力。 9. 工作认真、执行力强,具备良好的服务意识与现场组织配合能力。
### (二)复合能力 ### (二)复合能力
@@ -6543,7 +6554,7 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修市场营销专业,对会展项目执行与现场管理充满热情。在"深圳国际家具展"中,我协助会场布展、动线控制、观众引导、展商协调与内容发布等多个关键环节,实际体验了执行流程中对细节与反应力的要求。实习过程中,我注重沟通效率、流程整理和应变能力,具备较强的执行力与团队协作意识。未来希望在会展和活动项目的现场管理与运营支持方向继续成长,成为~~一名专业可靠的~~ **一名值得信赖、不断成长的** 会展执行人才。` 我是一名刚完成实习的大专毕业生,主修市场营销专业,对会展项目执行与现场管理充满热情。在深圳国际家具展中,我协助会场布展、动线控制、观众引导、展商协调与内容发布等多个关键环节,实际体验了执行流程中对细节与反应力的要求。实习过程中,我注重沟通效率、流程整理和应变能力,具备较强的执行力与团队协作意识。未来希望在会展和活动项目的现场管理与运营支持方向继续成长,成为 一名值得信赖、不断成长的 会展执行人才。`
}, },
oldContent: `# 对应岗位:会展执行助理 oldContent: `# 对应岗位:会展执行助理
@@ -6611,7 +6622,9 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修市场营销,具有出色情商与表达能力。在"2024年深圳国际家具展策划项目"中,我担任会展讲解助理,负责展览现场引导、观众沟通与互动引导,通过精准传达展览主题与控制动线节奏,提升观众体验。实习过程中我注重形象仪态、语言表达与服务协调,并积极参与展后复盘与数据整理。未来希望在会展讲解、新媒体传播或展览运营领域持续发展,成为具有专业表达力与观众连接能力的优秀人才。`, 我是一名刚完成实习的大专毕业生,主修市场营销,具有出色情商与表达能力。在"2024年深圳国际家具展策划项目"中,我担任会展讲解助理,负责展览现场引导、观众沟通与互动引导,通过精准传达展览主题与控制动线节奏,提升观众体验。实习过程中我注重形象仪态、语言表达与服务协调,并积极参与展后复盘与数据整理。未来希望在会展讲解、新媒体传播或展览运营领域持续发展,成为具有专业表达力与观众连接能力的优秀人才。`,
modified: `# 对应岗位:会展讲解员 modified: `# 会展讲解员
# 对应岗位:会展讲解员
# 一、项目经历 # 一、项目经历
@@ -6619,38 +6632,40 @@ const resumeTemplates = {
### (二)实习岗位:会展讲解员 ### (二)实习岗位:会展讲解员
### 实习时间2024.09-2024.10 ### 实习时间2023.09-2023.11
### (四)实习单位:某某公司 ### (四)实习单位:深圳市华奥展览服务有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 担任"超级视觉吸引点"及功能展示区讲解与引导,**简单介绍**"设计×商业"主题理念,帮观众更快理解; 1. 担任超级视觉吸引点及功能展示区讲解与引导, 简单介绍设计×商业主题理念,帮观众更快理解;
2. 引导观众按"1个吸引点 + 2个功能展示点 + 1个互动体验点"动线结构进行参观,**按照统一话术去说,保证讲解不跑偏;** 2. 引导观众按1个吸引点 + 2个功能展示点 + 1个互动体验点动线结构进行参观, 按照统一话术去说,保证讲解不跑偏;
3. 提前熟悉展会"五件套"视觉资产如KV、票券样式、导视模板**讲解时尽量跟视觉保持一致;** 3. 提前熟悉展会五件套视觉资产如KV、票券样式、导视模板 讲解时尽量跟视觉保持一致;
4. 于入口处负责迎宾、信息核对与观众提醒(展期、馆号、购票须知),**帮忙解答一些常见问题;** 4. 于入口处负责迎宾、信息核对与观众提醒(展期、馆号、购票须知), 帮忙解答一些常见问题;
5. 协助维持"预登记 / 现场登记 / 问题处理"三列队入场秩序,支持志愿者**让入场更顺畅;** 5. 协助维持预登记 / 现场登记 / 问题处理三列队入场秩序,支持志愿者 让入场更顺畅;
6. **大概了解**场馆主/支回路动线,协助转接点方向指引,避免观众走回头路; 6. 大概了解场馆主/支回路动线,协助转接点方向指引,避免观众走回头路;
7. **在老师或同事带领下,帮忙引导重点观众去目标展区;** 7. 在老师或同事带领下,帮忙引导重点观众去目标展区;
8. **在高峰时帮忙用口播或小提示做引导;** 8. 在高峰时帮忙用口播或小提示做引导;
9. 配合协办单位与志愿者进行现场人群管理,及时响应**一些现场求助;** 9. 配合协办单位与志愿者进行现场人群管理,及时响应 一些现场求助;
10. **参与展后收集照片和视频,帮忙整理简单资料;** 10. 参与展后收集照片和视频,帮忙整理简单资料;
11. 参与收集观众问卷与反馈,协助整理**基础意见给团队;** 11. 参与收集观众问卷与反馈,协助整理 基础意见给团队;
12. 保持仪态端庄、语言得体,确保讲解员形象与展会品牌形象一致。 12. 保持仪态端庄、语言得体,确保讲解员形象与展会品牌形象一致。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. **具备较好的语言表达与讲解能力,口语清晰、亲和力较强;** 1. 具备较好的语言表达与讲解能力,口语清晰、亲和力较强;
2. **注意个人形象与礼仪,能够保持专业得体的讲解形象;**
2. 注意个人形象与礼仪,能够保持专业得体的讲解形象;
3. 强烈服务意识,能够敏锐理解并满足观众需求提供优质体验; 3. 强烈服务意识,能够敏锐理解并满足观众需求提供优质体验;
4. **具备一定沟通协调能力,能够配合展会团队和志愿者完成任务;** 4. 具备一定沟通协调能力,能够配合展会团队和志愿者完成任务;
5. **具备基础应变意识,能在指导下协助处理简单突发情况;** 5. 具备基础应变意识,能在指导下协助处理简单突发情况;
6. **学习能力较强,能够较快掌握展览重点并配合讲解;** 6. 学习能力较强,能够较快掌握展览重点并配合讲解;
7. 严谨引导能力,熟悉展会动线并确保观众高效有序参观; 7. 严谨引导能力,熟悉展会动线并确保观众高效有序参观;
8. **能参与入场秩序维护与现场互动引导;** 8. 能参与入场秩序维护与现场互动引导;
9. **能参与展后简单数据和资料整理工作。** 9. 能参与展后简单数据和资料整理工作。
### (二)复合能力 ### (二)复合能力
@@ -6666,7 +6681,7 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名刚完成实习的大专毕业生,主修市场营销,**具备较好的沟通与表达能力**。在"2024年深圳国际家具展策划项目"中,我担任会展讲解助理,负责展览现场引导、观众沟通与互动支持,通过传达展览主题与协助动线控制,提升了观众体验。实习过程中我注重形象仪态、语言表达与服务意识,并积极参与展后反馈整理。未来希望在会展讲解、新媒体传播或展览运营领域持续发展,成为一名不断进步、能与观众建立良好连接的专业人才。` 我是一名刚完成实习的大专毕业生,主修市场营销, 具备较好的沟通与表达能力。在2024年深圳国际家具展策划项目中,我担任会展讲解助理,负责展览现场引导、观众沟通与互动支持,通过传达展览主题与协助动线控制,提升了观众体验。实习过程中我注重形象仪态、语言表达与服务意识,并积极参与展后反馈整理。未来希望在会展讲解、新媒体传播或展览运营领域持续发展,成为一名不断进步、能与观众建立良好连接的专业人才。`
}, },
studentInfo: { studentInfo: {
project_experience: { project_experience: {
@@ -7054,41 +7069,41 @@ const resumeTemplates = {
### (二)实习岗位:漫展策划师助理 ### (二)实习岗位:漫展策划师助理
### (三)实习时间:XXXX时间 ### (三)实习时间:2023.10-2024.12
### (四)实习单位:某某公司 ### (四)实习单位:盐城东拓国际会展服务有限公司
### (五)岗位职责: ### (五)岗位职责:
1. ~~根据目标受众调研与二次元文化趋势,制定~~ **协助老师和团队参考目标受众调研与二次元文化趋势,一起参与制定**漫展整体主题、主视觉、口号与统一物料风格,包括海报、导视、票面与舞台等; 1. 协助老师和团队参考目标受众调研与二次元文化趋势,一起参与制定漫展整体主题、主视觉、口号与统一物料风格,包括海报、导视、票面与舞台等;
2. ~~将展会时间拆分为"开场、高峰、尾场"三个阶段,细化各环节节奏和互动流程,保证观众体验与现场流畅度~~ **在展会方案中跟着团队把时间分成"开场、高峰、尾场"三个阶段,学习并参与细化互动流程,帮助提升观众体验和现场流畅度** 2. 在展会方案中跟着团队把时间分成开场、高峰、尾场三个阶段,学习并参与细化互动流程,帮助提升观众体验和现场流畅度;
3. ~~设计票务流程和信息引导体系,统筹线上预售与现场检票流程,确保~~ **协助整理票务流程和信息引导表,配合统筹线上预售与现场检票,帮助确保**信息传递准确与入场效率; 3. 协助整理票务流程和信息引导表,配合统筹线上预售与现场检票,帮助确保信息传递准确与入场效率;
4. ~~协调COSER、唱见及品牌合作引入~~ **参与对接COSER、唱见及品牌合作协助引入**IP内容与社团资源提升漫展丰富度与吸引力 4. 参与对接COSER、唱见及品牌合作协助引入IP内容与社团资源提升漫展丰富度与吸引力
5. ~~设立入口问询台、母婴区、失物招领及现场导引机制、安检与应急巡查流程,保障~~ **协助现场入口问询台、母婴区、失物招领及导引点的安排,配合安检与应急巡查,帮助保障**现场秩序与安全; 5. 协助现场入口问询台、母婴区、失物招领及导引点的安排,配合安检与应急巡查,帮助保障现场秩序与安全;
6. ~~保证宣传信息一致性,通过主持与大屏统一口播控制,减少~~ **在现场配合保证宣传信息一致性,协助主持与大屏统一口播,帮助减少**信息差异与期望偏差; 6. 在现场配合保证宣传信息一致性,协助主持与大屏统一口播,帮助减少信息差异与期望偏差;
7. ~~活动结束后,对模板物料、流程手册与视觉文档进行系统归档,支持~~ **活动结束后协助整理模板物料、流程手册与视觉文档,支持**未来漫展内容可复用标准化。 7. 活动结束后协助整理模板物料、流程手册与视觉文档,支持未来漫展内容可复用标准化。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~深入理解~~ **在学习和实习中逐步理解**二次元市场文化,能~~洞察~~ **配合分析**目标观众需求和竞争趋势,用文化共鸣~~策划~~ **参与策划**展会内容; 1. 在学习和实习中逐步理解二次元市场文化,能 配合分析目标观众需求和竞争趋势,用文化共鸣 参与策划展会内容;
2. 具备从整体策划到细节执行的项目管理能力,~~能够制定~~ **能在指导下参与制定**时间表、任务分配及流程节点控制; 2. 具备从整体策划到细节执行的项目管理能力, 能在指导下参与制定时间表、任务分配及流程节点控制;
3. ~~擅长协调~~ **能协助协调**多方资源:搭建商、内容合作方、舞台演出、服务人员,~~统筹~~ **参与统筹**现场部署与流程顺序; 3. 能协助协调多方资源:搭建商、内容合作方、舞台演出、服务人员, 参与统筹现场部署与流程顺序;
4. 具备视觉表达能力,能~~通过~~ **参与制作**海报、导视系统和现场布置等设计,~~提升~~ **帮助提升**活动吸引力; 4. 具备视觉表达能力,能 参与制作海报、导视系统和现场布置等设计, 帮助提升活动吸引力;
5. ~~掌握策划方案和提案表达能力,能够清晰撰写和演示策划内容并与团队或客户沟通~~ 5.
**具备基础的策划方案和提案表达能力,能协助撰写与演示策划内容,并在团队支持下与客户沟通** 具备基础的策划方案和提案表达能力,能协助撰写与演示策划内容,并在团队支持下与客户沟通;
6. ~~熟悉预算与成本控制,能在成本和活动效果之间保持平衡~~ 6.
**对预算与成本控制有一定了解,能在执行中配合团队在成本和效果之间保持平衡** 对预算与成本控制有一定了解,能在执行中配合团队在成本和效果之间保持平衡;
7. ~~具备应急处理敏锐性,能灵活调整现场流程以应对突发事件并维持现场节奏~~ 7.
**具备应急处理意识,能在现场协助团队灵活调整流程,应对突发事件并维持整体节奏** 具备应急处理意识,能在现场协助团队灵活调整流程,应对突发事件并维持整体节奏;
8. 拥有二次元文化认同与热情~~驱动动力,对漫展策划具有强烈投入和传播使命感~~**,能积极参与漫展策划与执行,并保持投入与传播热情** 8. 拥有二次元文化认同与热情,能积极参与漫展策划与执行,并保持投入与传播热情。
### (二)复合能力 ### (二)复合能力
@@ -7104,7 +7119,7 @@ const resumeTemplates = {
# 三、个人总结 # 三、个人总结
我是一名主修文化创意与策划的大专毕业生,在"盐城第九届 ICGC 动漫嘉年华"策划项目中,~~协助完成~~ **参与并协助团队完成从前期调研到落地执行的多个环节,包括漫展主题规划、视觉设计、流程细节打磨、现场节奏把控及多方资源协调等具体任务**。在执行过程中,我始终保持对二次元文化的热情,注重流程严密、突发响应及时与参展体验的完整。我的组织执行能力与视觉策划表达能力~~相辅相成~~ **在实践中逐步得到锻炼与提升,例如在视觉物料整理和现场衔接中积累了实操经验,也在多方沟通中增强了统筹意识**,希望未来能在漫展或文化活动策划与执行路径上持续发展,成为~~兼具文化表达力与执行统筹力的一线策划者~~ **既能表达文化创意、又能把控执行落地的实干型策划人才**` 我是一名主修文化创意与策划的大专毕业生,在盐城第九届 ICGC 动漫嘉年华策划项目中, 参与并协助团队完成从前期调研到落地执行的多个环节,包括漫展主题规划、视觉设计、流程细节打磨、现场节奏把控及多方资源协调等具体任务。在执行过程中,我始终保持对二次元文化的热情,注重流程严密、突发响应及时与参展体验的完整。我的组织执行能力与视觉策划表达能力 在实践中逐步得到锻炼与提升,例如在视觉物料整理和现场衔接中积累了实操经验,也在多方沟通中增强了统筹意识,希望未来能在漫展或文化活动策划与执行路径上持续发展,成为 既能表达文化创意、又能把控执行落地的实干型策划人才。`
}, },
studentInfo: { studentInfo: {
project_experience: { project_experience: {
@@ -8009,28 +8024,28 @@ const resumeTemplates = {
### (二)实习岗位:旅游规划助理 ### (二)实习岗位:旅游规划助理
### 实习时间2024.09-2024.10 ### 实习时间2024.05-2024.07
### (四)实习单位:某某公司 ### (四)实习单位:上海好拾光旅游咨询有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 协助进行目的地资源调研:收集侗寨、苗寨等自然与人文资源及非遗技艺(侗族大歌、苗银、蜡染)资料,建立资源库并完成~~对比分析~~ **基础整理** 1. 协助进行目的地资源调研:收集侗寨、苗寨等自然与人文资源及非遗技艺(侗族大歌、苗银、蜡染)资料,建立资源库并完成 基础整理;
2. 参与旅游线路策划:根据导师指导,绘制"5天4晚"小团制行程草案,明确每日文化主题、体验亮点~~与交通逻辑~~ **和大致路线** 2. 参与旅游线路策划:根据导师指导,绘制5天4晚小团制行程草案,明确每日文化主题、体验亮点 和大致路线;
3. 协助完成市场调研与客群分析整理25-45岁核心客群的出行需求汇总调研数据并~~提出差异化定位建议~~ **提供一些参考意见** 3. 协助完成市场调研与客群分析整理25-45岁核心客群的出行需求汇总调研数据并 提供一些参考意见;
4. 支持规划方案编制参与撰写线路策划方案与说明文档协助制作PPT确保~~逻辑完整、~~图文清晰 **,内容易懂** 4. 支持规划方案编制参与撰写线路策划方案与说明文档协助制作PPT确保图文清晰 ,内容易懂;
5. 协助亮点设计:参与"火塘夜话+AI歌谱翻译"等特色体验的方案讨论,并完成相关资料整理; 5. 协助亮点设计:参与火塘夜话+AI歌谱翻译等特色体验的方案讨论,并完成相关资料整理;
6. 协助交通与住宿安排:收集大巴/高铁时刻表、民宿与酒店报价,整理对比文档,为最终选型~~提供数据支持~~ **打下基础** 6. 协助交通与住宿安排:收集大巴/高铁时刻表、民宿与酒店报价,整理对比文档,为最终选型 打下基础;
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~熟悉~~ **了解**旅游线路策划流程,能参与资源调研、方案设计及亮点创意工作; 1. 了解旅游线路策划流程,能参与资源调研、方案设计及亮点创意工作;
2. ~~掌握PPT、Visio、思维导图等工具能够辅助制作旅游策划方案与展示材料~~**会用PPT、思维导图等工具能帮忙做方案展示材料** 2. 会用PPT、思维导图等工具能帮忙做方案展示材料
3. ~~了解旅游市场调研方法,具备基础数据整理与用户偏好分析能力~~**接触过旅游市场调研,能整理数据并做一些简单分析;** 3. 接触过旅游市场调研,能整理数据并做一些简单分析;
4. 能够结合非遗、文化元素进行旅游产品设计,~~具备跨文化内容理解能力~~ **对文化元素有基本理解并能尝试结合;** 4. 能够结合非遗、文化元素进行旅游产品设计, 对文化元素有基本理解并能尝试结合;
5. ~~掌握Excel、Word等办公软件能完成数据表格与策划文本的编辑~~**能用Excel、Word处理表格和文档支持日常策划工作。** 5. 能用Excel、Word处理表格和文档支持日常策划工作。
### (二)复合能力 ### (二)复合能力
@@ -8141,27 +8156,27 @@ const resumeTemplates = {
### (二)实习岗位:旅游计调助理 ### (二)实习岗位:旅游计调助理
### 实习时间2024.09-2024.10 ### 实习时间2024.05-2024.07
### (四)实习单位:某某公司 ### (四)实习单位:上海好拾光旅游咨询有限公司
### (五)岗位职责: ### (五)岗位职责:
1. 协助收集和整理目的地调研资料:包括侗寨、苗寨等景点的交通、住宿及非遗技艺信息,建立资源~~对照表,确保数据准确完整~~ **清单,保证资料完整方便后续使用** 1. 协助收集和整理目的地调研资料:包括侗寨、苗寨等景点的交通、住宿及非遗技艺信息,建立资源 清单,保证资料完整方便后续使用;
2. 参与行程方案排布:在导师指导下绘制"5天4晚"行程初稿,标注每日出行时间、景点顺序~~与体验主题,检验行程节奏合理性~~ **和体验安排,初步检查行程是否顺畅** 2. 参与行程方案排布:在导师指导下绘制5天4晚行程初稿,标注每日出行时间、景点顺序 和体验安排,初步检查行程是否顺畅;
3. 协助交通与住宿落实:整理大巴/高铁班次及酒店、民宿报价,完成比价单,为最终预订~~提供参考数据~~ **打基础** 3. 协助交通与住宿落实:整理大巴/高铁班次及酒店、民宿报价,完成比价单,为最终预订 打基础;
4. 支持餐饮与体验安排:收集各站点餐饮特色及非遗体验活动(苗银工坊、蜡染工坊等),形成"景点—体验"对应清单,~~确保体验环节衔接顺畅~~ **让行程更连贯** 4. 支持餐饮与体验安排:收集各站点餐饮特色及非遗体验活动(苗银工坊、蜡染工坊等),形成景点—体验对应清单, 让行程更连贯;
5. 协助客户服务保障:参与设计"行中微调选项"流程图,整理游客需求问卷,完成数据分析并~~输出简要报告~~ **写简单的小结** 5. 协助客户服务保障:参与设计行中微调选项流程图,整理游客需求问卷,完成数据分析并 写简单的小结;
6. 协助项目总结与优化:分类整理用户反馈,统计行程满意度数据,并根据导师意见~~编写简要改进建议~~ **写一些改进小建议** 6. 协助项目总结与优化:分类整理用户反馈,统计行程满意度数据,并根据导师意见 写一些改进小建议。
# 二、专业技能 # 二、专业技能
### (一)核心能力 ### (一)核心能力
1. ~~熟悉旅游计调流程,能够完成行程单、报价单及比价表的制作~~**了解旅游计调流程,能在指导下完成行程单、报价单和比价表;** 1. 了解旅游计调流程,能在指导下完成行程单、报价单和比价表;
2. 掌握Excel、Word等办公软件操作能进行数据整理和基础分析 2. 掌握Excel、Word等办公软件操作能进行数据整理和基础分析
3. ~~了解交通与住宿资源对接流程,能协助完成酒店、车队等供应商的预订准备~~**接触过交通与住宿资源对接,能帮忙整理酒店、车队等供应商资料;** 3. 接触过交通与住宿资源对接,能帮忙整理酒店、车队等供应商资料;
4. ~~具备客户服务意识,能对游客问卷与反馈数据进行统计和简要总结~~**有基本的服务意识,能整理游客问卷和反馈,并做简单总结。** 4. 有基本的服务意识,能整理游客问卷和反馈,并做简单总结。
### (二)复合能力 ### (二)复合能力

View File

@@ -1,127 +1,145 @@
import { getMonthDays } from "@/data/mockData"; import { getMonthDays } from "@/data/mockData";
import { getEventStyleByType } from "@/utils/calendarEventStyles";
const MonthView = ({
currentDate, const MonthView = ({
events, currentDate,
onDateClick, events,
onEventClick, onDateClick,
selectedDate, onEventClick,
}) => { selectedDate,
const year = currentDate.getFullYear(); }) => {
const month = currentDate.getMonth(); const year = currentDate.getFullYear();
const days = getMonthDays(year, month); const month = currentDate.getMonth();
const weekDays = ["日", "一", "二", "三", "四", "五", "六"]; const days = getMonthDays(year, month);
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
// 获取指定日期的事件
const getEventsForDate = (date, month, year) => { // 获取指定日期的事件
if (!events || events.length === 0) return []; const getEventsForDate = (date, month, year) => {
if (!events || events.length === 0) return [];
const dateString = `${year}-${(month + 1)
.toString() const dateString = `${year}-${(month + 1)
.padStart(2, "0")}-${date.toString().padStart(2, "0")}`; .toString()
.padStart(2, "0")}-${date.toString().padStart(2, "0")}`;
return events.filter((event) => {
const eventDate = event.startTime.split(" ")[0]; return events.filter((event) => {
return eventDate === dateString; const eventDate = event.startTime.split(" ")[0];
}); return eventDate === dateString;
}; });
};
const handleDateClick = (day, dayEvents) => {
// 所有日期都可以点击 const handleDateClick = (day, dayEvents) => {
if (onDateClick) { // 所有日期都可以点击
const clickedDate = new Date(day.year, day.month, day.date); if (onDateClick) {
onDateClick(clickedDate, dayEvents || []); const clickedDate = new Date(day.year, day.month, day.date);
} onDateClick(clickedDate, dayEvents || []);
}; }
};
const isSelected = (day) => {
if (!selectedDate) return false; const isSelected = (day) => {
return ( if (!selectedDate) return false;
selectedDate.getFullYear() === day.year && return (
selectedDate.getMonth() === day.month && selectedDate.getFullYear() === day.year &&
selectedDate.getDate() === day.date selectedDate.getMonth() === day.month &&
); selectedDate.getDate() === day.date
}; );
};
const renderEventItem = (event, index, dayEvents) => {
const maxVisible = 3; // 每个日期最多显示3个事件 const renderEventItem = (event, index, dayEvents) => {
const maxVisible = 3; // 每个日期最多显示3个事件
if (
index >= maxVisible - 1 && if (
index === maxVisible - 1 && index >= maxVisible - 1 &&
dayEvents.length > maxVisible index === maxVisible - 1 &&
) { dayEvents.length > maxVisible
// 显示"更多"指示器 ) {
const remainingCount = dayEvents.length - maxVisible + 1; // 显示"更多"指示器
return ( const remainingCount = dayEvents.length - maxVisible + 1;
<div key={`more-${index}`} className="event-more"> return (
+{remainingCount}更多 <div key={`more-${index}`} className="event-more">
</div> +{remainingCount}更多
); </div>
} );
}
if (index >= maxVisible) return null;
if (index >= maxVisible) return null;
return (
<div // 获取事项样式
key={event.id} const eventStyle = getEventStyleByType(event.type);
className={`event-item ${event.type}`}
title={`${event.title} (${event.startTime.split(" ")[1]} - ${ return (
event.endTime.split(" ")[1] <div
})`} key={event.id}
onClick={(e) => { className="event-item-new"
e.stopPropagation(); title={`${event.title} (${event.startTime.split(" ")[1]} - ${
if (onEventClick) { event.endTime.split(" ")[1]
onEventClick(event); })`}
} else { style={{
} backgroundColor: eventStyle.backgroundColor,
}} color: eventStyle.textColor,
> }}
{event.title} onClick={(e) => {
</div> e.stopPropagation();
); if (onEventClick) {
}; onEventClick(event);
} else {
return ( }
<div className="month-view"> }}
{/* 星期标题 */} >
<div className="month-header"> <div className="event-content">
{weekDays.map((day) => ( {eventStyle.icon && (
<div key={day} className="weekday-header"> <img
{day} src={eventStyle.icon}
</div> alt=""
))} className="event-icon"
</div> style={{ width: '16px', height: '16px', marginRight: '4px' }}
/>
{/* 日期网格 */} )}
<div className="month-grid"> <span className="event-title">{event.title}</span>
{days.map((day, index) => { </div>
const dayEvents = getEventsForDate(day.date, day.month, day.year); </div>
const isToday = day.isToday; );
const isCurrentMonth = day.isCurrentMonth; };
const isSelectedDate = isSelected(day);
return (
return ( <div className="month-view">
<div {/* 星期标题 */}
key={index} <div className="month-header">
className={`day-cell ${!isCurrentMonth ? "other-month" : ""} ${ {weekDays.map((day) => (
isToday ? "today" : "" <div key={day} className="weekday-header">
} ${isSelectedDate ? "selected" : ""}`} {day}
onClick={() => handleDateClick(day, dayEvents)} </div>
> ))}
<div className="day-number">{day.date}</div> </div>
<div className="event-list"> {/* 日期网格 */}
{dayEvents.map((event, eventIndex) => <div className="month-grid">
renderEventItem(event, eventIndex, dayEvents) {days.map((day, index) => {
)} const dayEvents = getEventsForDate(day.date, day.month, day.year);
</div> const isToday = day.isToday;
</div> const isCurrentMonth = day.isCurrentMonth;
); const isSelectedDate = isSelected(day);
})}
</div> return (
</div> <div
); key={index}
}; className={`day-cell ${!isCurrentMonth ? "other-month" : ""} ${
isToday ? "today" : ""
export default MonthView; } ${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;

View File

@@ -1,165 +1,181 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { getWeekDays } from "@/data/mockData"; import { getWeekDays } from "@/data/mockData";
import { getEventStyleByType } from "@/utils/calendarEventStyles";
const WeekView = ({ currentDate, events, onDateClick, onEventClick }) => {
const containerRef = useRef(null); const WeekView = ({ currentDate, events, onDateClick, onEventClick }) => {
const weekDays = getWeekDays(currentDate); const containerRef = useRef(null);
const timeSlots = Array.from( const weekDays = getWeekDays(currentDate);
{ length: 24 }, const timeSlots = Array.from(
(_, i) => `${i.toString().padStart(2, "0")}:00` { length: 24 },
); (_, i) => `${i.toString().padStart(2, "0")}:00`
const weekDayNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]; );
const weekDayNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
// 滚动到当前时间
useEffect(() => { // 滚动到当前时间
const now = new Date(); useEffect(() => {
const currentHour = now.getHours(); const now = new Date();
const scrollTop = currentHour * 60; // 每小时60px const currentHour = now.getHours();
const scrollTop = currentHour * 60; // 每小时60px
if (containerRef.current) {
containerRef.current.scrollTop = Math.max(0, scrollTop - 120); // 提前2小时显示 if (containerRef.current) {
} containerRef.current.scrollTop = Math.max(0, scrollTop - 120); // 提前2小时显示
}, []); }
}, []);
// 获取指定日期的事件
const getEventsForDate = (date) => { // 获取指定日期的事件
if (!events || events.length === 0) return []; const getEventsForDate = (date) => {
if (!events || events.length === 0) return [];
const dateString = `${date.getFullYear()}-${(date.getMonth() + 1)
.toString() const dateString = `${date.getFullYear()}-${(date.getMonth() + 1)
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; .toString()
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
return events.filter((event) => {
const eventDate = event.startTime.split(" ")[0]; return events.filter((event) => {
return eventDate === dateString; const eventDate = event.startTime.split(" ")[0];
}); return eventDate === dateString;
}; });
};
// 计算事件在时间轴上的位置和高度
const calculateEventStyle = (event) => { // 计算事件在时间轴上的位置和高度
const startTime = event.startTime.split(" ")[1]; const calculateEventStyle = (event) => {
const endTime = event.endTime.split(" ")[1]; 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 startHour = parseInt(startTime.split(":")[0]);
const endHour = parseInt(endTime.split(":")[0]); const startMinute = parseInt(startTime.split(":")[1]);
const endMinute = parseInt(endTime.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 startOffset = startHour * 60 + startMinute; // 转换为分钟
const duration = endOffset - startOffset; const endOffset = endHour * 60 + endMinute;
const duration = endOffset - startOffset;
const top = startOffset; // 1分钟 = 1px
const height = Math.max(duration, 30); // 最小高度30px const top = startOffset; // 1分钟 = 1px
const height = Math.max(duration, 30); // 最小高度30px
return {
top: `${top}px`, return {
height: `${height}px`, top: `${top}px`,
}; height: `${height}px`,
}; };
};
const handleDateClick = (date) => {
if (onDateClick) { const handleDateClick = (date) => {
onDateClick(date); if (onDateClick) {
} onDateClick(date);
}; }
};
const getCurrentTimeLine = () => {
const now = new Date(); const getCurrentTimeLine = () => {
const currentHour = now.getHours(); const now = new Date();
const currentMinute = now.getMinutes(); const currentHour = now.getHours();
const totalMinutes = currentHour * 60 + currentMinute; const currentMinute = now.getMinutes();
const totalMinutes = currentHour * 60 + currentMinute;
// 只在今天显示当前时间线
const isToday = weekDays.some( // 只在今天显示当前时间线
(date) => date.toDateString() === now.toDateString() const isToday = weekDays.some(
); (date) => date.toDateString() === now.toDateString()
);
if (!isToday) return null;
if (!isToday) return null;
return (
<div className="current-time-line" style={{ top: `${totalMinutes}px` }} /> return (
); <div className="current-time-line" style={{ top: `${totalMinutes}px` }} />
}; );
};
return (
<div className="week-view"> return (
{/* 周标题 */} <div className="week-view">
<div className="week-header"> {/* 周标题 */}
<div className="time-header">时间</div> <div className="week-header">
{weekDays.map((date, index) => { <div className="time-header">时间</div>
const isToday = date.toDateString() === new Date().toDateString(); {weekDays.map((date, index) => {
const isToday = date.toDateString() === new Date().toDateString();
return (
<div return (
key={date.toISOString()} <div
className={`day-header ${isToday ? "today" : ""}`} key={date.toISOString()}
onClick={() => handleDateClick(date)} className={`day-header ${isToday ? "today" : ""}`}
> onClick={() => handleDateClick(date)}
<div className="day-name">{weekDayNames[index]}</div> >
<div className="day-date">{date.getDate()}</div> <div className="day-name">{weekDayNames[index]}</div>
</div> <div className="day-date">{date.getDate()}</div>
); </div>
})} );
</div> })}
</div>
{/* 周网格 */}
<div className="week-grid" ref={containerRef}> {/* 周网格 */}
{/* 时间列 */} <div className="week-grid" ref={containerRef}>
<div className="time-column"> {/* 时间列 */}
{timeSlots.map((time) => ( <div className="time-column">
<div key={time} className="time-slot"> {timeSlots.map((time) => (
{time} <div key={time} className="time-slot">
</div> {time}
))} </div>
</div> ))}
</div>
{/* 日期列 */}
{weekDays.map((date) => { {/* 日期列 */}
const dayEvents = getEventsForDate(date); {weekDays.map((date) => {
const dayEvents = getEventsForDate(date);
return (
<div key={date.toISOString()} className="day-column"> return (
{/* 小时格子 */} <div key={date.toISOString()} className="day-column">
{timeSlots.map((time) => ( {/* 小时格子 */}
<div key={time} className="hour-slot" /> {timeSlots.map((time) => (
))} <div key={time} className="hour-slot" />
))}
{/* 事件块 */}
{dayEvents.map((event) => { {/* 事件块 */}
const style = calculateEventStyle(event); {dayEvents.map((event) => {
const style = calculateEventStyle(event);
return ( const eventStyle = getEventStyleByType(event.type);
<div
key={event.id} return (
className={`event-block ${event.type}`} <div
style={style} key={event.id}
title={event.description} className="event-block-new"
onClick={(e) => { style={{
e.stopPropagation(); ...style,
if (onEventClick) { backgroundColor: eventStyle.backgroundColor,
onEventClick(event); color: eventStyle.textColor,
} else { }}
} title={event.description}
}} onClick={(e) => {
> e.stopPropagation();
<div className="event-title">{event.title}</div> if (onEventClick) {
<div className="event-time"> onEventClick(event);
{event.startTime.split(" ")[1]} -{" "} } else {
{event.endTime.split(" ")[1]} }
</div> }}
</div> >
); <div className="event-header">
})} {eventStyle.icon && (
</div> <img
); src={eventStyle.icon}
})} alt=""
className="event-icon"
{/* 当前时间线 */} style={{ width: '18px', height: '18px', marginRight: '4px' }}
{getCurrentTimeLine()} />
</div> )}
</div> <div className="event-title">{event.title}</div>
); </div>
}; <div className="event-time">
{event.startTime.split(" ")[1]} -{" "}
export default WeekView; {event.endTime.split(" ")[1]}
</div>
</div>
);
})}
</div>
);
})}
{/* 当前时间线 */}
{getCurrentTimeLine()}
</div>
</div>
);
};
export default WeekView;

View File

@@ -217,6 +217,47 @@
overflow: hidden; 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 { .event-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
@@ -397,6 +438,54 @@
background: #fbfcfd; 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 { .event-block {
position: absolute; position: absolute;
left: 4px; left: 4px;

View File

@@ -1,8 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Radio } from "@arco-design/web-react"; import { Radio } from "@arco-design/web-react";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import "./index.css"; import "./index.css";
export default ({ visible, onClose, data, initialVersion = "2" }) => { export default ({ visible, onClose, data, initialVersion = "2" }) => {
@@ -22,22 +20,6 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
onClose(); onClose();
}; };
// 判断是否应该使用markdown渲染有真实修改版的岗位
const shouldUseMarkdown = (positionTitle) => {
const markdownPositions = [
"会展策划师",
"会展讲解员",
"活动执行",
"活动策划师",
"漫展策划师",
"会展执行助理",
"旅游规划师",
"旅游计调专员",
"景区运营专员",
"文旅运营总监助理"
];
return markdownPositions.includes(positionTitle);
};
// Markdown解析器 - 解析简历内容 // Markdown解析器 - 解析简历内容
const parseResumeMarkdown = (markdownContent) => { const parseResumeMarkdown = (markdownContent) => {
@@ -117,99 +99,6 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
return result; return result;
}; };
// 渲染markdown分块内容
const renderMarkdownSections = (markdownContent) => {
if (!markdownContent) return null;
const sections = [];
// 首先添加教育经历板块
sections.push(
<li key="education" className="resume-info-moda-item">
<p className="resume-info-moda-item-title">教育经历</p>
<ul className="educational-experience-list">
<li className="educational-experience-list-item">
<p className="school-name">
苏州信息职业技术学院 - 旅游管理
</p>
<p className="study-time">2020.9-2023.6</p>
</li>
</ul>
</li>
);
// 按H1标题分割markdown内容# 开头的行)
const markdownSections = markdownContent.split(/^# /gm).filter(section => section.trim());
// 添加markdown渲染的section
markdownSections.forEach((section, index) => {
// 为每个section添加回# 前缀(除了第一个如果不是以#开头)
const fullSection = section.startsWith('#') ? section : `# ${section}`;
// 过滤掉"对应岗位"板块
if (fullSection.includes('对应岗位:') || fullSection.includes('对应岗位:')) {
return;
}
sections.push(
<li key={`markdown-${index}`} className="resume-info-moda-item">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 标题样式
h1: ({node, ...props}) => <p className="resume-info-moda-item-title" {...props} />,
h2: ({node, ...props}) => <p className="resume-info-moda-item-title" style={{
fontSize: '18px',
marginTop: '15px'
}} {...props} />,
h3: ({node, ...props}) => <p style={{
fontSize: '16px',
fontWeight: '600',
color: '#4e5969',
marginBottom: '10px',
marginTop: '15px',
textAlign: 'left'
}} {...props} />,
// 段落样式
p: ({node, ...props}) => <p style={{
fontSize: '14px',
lineHeight: '1.6',
color: '#1d2129',
marginBottom: '10px',
textAlign: 'left',
width: '100%',
wordWrap: 'break-word'
}} {...props} />,
// 列表样式
ol: ({node, ...props}) => <ol style={{
paddingLeft: '20px',
marginBottom: '15px'
}} {...props} />,
ul: ({node, ...props}) => <ul style={{
paddingLeft: '20px',
marginBottom: '15px'
}} {...props} />,
li: ({node, ...props}) => <li style={{
fontSize: '14px',
lineHeight: '1.6',
color: '#1d2129',
marginBottom: '8px',
textAlign: 'left'
}} {...props} />,
// 加粗和删除线样式
strong: ({node, ...props}) => <strong style={{color: '#ff4d4f', fontWeight: 'bold'}} {...props} />,
del: ({node, ...props}) => <del style={{textDecoration: 'line-through', color: '#999'}} {...props} />
}}
>
{fullSection}
</ReactMarkdown>
</li>
);
});
return sections;
};
// 获取简历数据 - 支持新的数据结构 // 获取简历数据 - 支持新的数据结构
let resumeContent = {}; let resumeContent = {};
@@ -354,12 +243,7 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
{data?.title || resumeContent.personalInfo?.name || "职位名称"} {data?.title || resumeContent.personalInfo?.name || "职位名称"}
</p> </p>
{/* 判断是否使用markdown渲染 */} {/* 统一使用结构化样式展示所有岗位 */}
{data && shouldUseMarkdown(data?.title) && data?.content?.original && data?.content?.modified ? (
<ul className="resume-info-moda-list">
{renderMarkdownSections(version === "1" ? data.content.original : data.content.modified)}
</ul>
) : (
<ul className="resume-info-moda-list"> <ul className="resume-info-moda-list">
{/* 教育经历 */} {/* 教育经历 */}
<li className="resume-info-moda-item"> <li className="resume-info-moda-item">
@@ -476,7 +360,6 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
</li> </li>
)} )}
</ul> </ul>
)}
</div> </div>
</Modal> </Modal>
</> </>

View 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;
}
}
}

View 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>
);
};

View 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;
}

View 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:00UTC+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;

View 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;
}
}
}
}
}

View 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

View File

@@ -1,543 +1,85 @@
import React, { useState, useEffect, useRef } from "react"; import { useState, useRef } from "react";
import { expertQAData, expertInfo } from "@/data/expertQAData"; import IconFont from "@/components/IconFont";
import SupportList from "./components/SupportList";
import MyIM from "./components/MyIM";
import FormModal from "./components/FormModal";
import "./index.css"; import "./index.css";
const titleList = [
{
title: "专家客服",
icon: "recuWi8z90DYHn",
},
{
title: "快速响应",
icon: "recuWi8zBJn42J",
},
{
title: "24小时服务",
icon: "recuWi8A1CNtCo",
},
];
const ExpertSupportPage = () => { const ExpertSupportPage = () => {
// 对话管理 const IMRef = useRef(null);
const [selectedConversation, setSelectedConversation] = useState(null); const [formVisible, setFormVisible] = useState(false);
const [messages, setMessages] = useState([]); const [initialMessages, setInitialMessages] = useState(null); // 设置消息
const [inputMessage, setInputMessage] = useState("");
const [showResolutionModal, setShowResolutionModal] = useState(null); const handleClose = () => {
const [isNewConversation, setIsNewConversation] = useState(false); setFormVisible(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 hanldeClickOpenModal = () => {
const checkInactivityStatus = () => { setFormVisible(true);
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) => { const handleSend = (type, val, showBtn) => {
// 如果是带有红点的对话,点击时询问是否已解决 if (IMRef.current) {
if ( IMRef.current.handleSend(type, val, showBtn);
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 ( return (
<div className="expert-support-page"> <>
{/* 左侧对话记录区域 */} <div className="expert-support-page">
<div className="conversation-sidebar"> <div className="expert-support-left-wrapper">
<div className="sidebar-header"> <SupportList />
<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>
<div className="expert-support-right-wrapper">
{/* 按时间分组的对话列表 */} <div className="expert-support-right-title-wrapper">
<div className="conversation-groups"> <div className="title-wrapper">
{Object.entries(conversationGroups).map( <IconFont className="right-title-icon" src="recuWi8ARUkXzm" />
([groupName, conversations]) => ( <div className="right-title-text">
<div key={groupName} className="conversation-group"> <p>专家支持中心</p>
<div className="group-header"> <span>Expert Tutor Support Center</span>
<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>
)} <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> </div>
<FormModal
{/* 右侧问答界面 */} visible={formVisible}
<div className="chat-interface"> onClose={handleClose}
{/* 顶部信息栏 */} handleSend={handleSend}
<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; export default ExpertSupportPage;

View 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%;
}
}

View 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;

View File

@@ -34,7 +34,7 @@
.unified-profile-right { .unified-profile-right {
flex: 1; flex: 1;
height: 100%; height: 785px;
border-radius: 16px; border-radius: 16px;
background-color: #e8f6ff; background-color: #e8f6ff;
position: relative; position: relative;

View 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();

File diff suppressed because it is too large Load Diff

View 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"
}
]

View 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"
}
]