|
|
@@ -0,0 +1,358 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { useAuth } from '../hooks/AuthProvider';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import { silverUsersClient } from '@/client/api';
|
|
|
+import { App, Card, List, Button, Tag, Modal, Form, Input, Select, Upload, message } from 'antd';
|
|
|
+import { PlusOutlined, UploadOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
|
+import type { UploadProps } from 'antd';
|
|
|
+
|
|
|
+const { Option } = Select;
|
|
|
+
|
|
|
+const SkillsPage: React.FC = () => {
|
|
|
+ const { user } = useAuth();
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const { message } = App.useApp();
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [skills, setSkills] = useState<any[]>([]);
|
|
|
+ const [certificates, setCertificates] = useState<any[]>([]);
|
|
|
+ const [modalVisible, setModalVisible] = useState(false);
|
|
|
+ const [modalType, setModalType] = useState<'skill' | 'certificate'>('skill');
|
|
|
+ const [editingItem, setEditingItem] = useState<any>(null);
|
|
|
+ const [form] = Form.useForm();
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (user) {
|
|
|
+ loadSkills();
|
|
|
+ }
|
|
|
+ }, [user]);
|
|
|
+
|
|
|
+ const loadSkills = async () => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ // 加载技能标签
|
|
|
+ const response = await (silverUsersClient as any)['profiles'].$get({
|
|
|
+ query: { filters: JSON.stringify({ userId: user.id }) }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.status === 200) {
|
|
|
+ const data = await response.json();
|
|
|
+ if (data.data && data.data.length > 0) {
|
|
|
+ const profile = data.data[0];
|
|
|
+ setSkills(profile.skills?.split(',') || []);
|
|
|
+ setCertificates(profile.certificates || []);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载技能信息失败:', error);
|
|
|
+ message.error('加载技能信息失败');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleAddSkill = async (values: any) => {
|
|
|
+ try {
|
|
|
+ const newSkills = [...skills, values.skill];
|
|
|
+ await updateProfile({ skills: newSkills.join(',') });
|
|
|
+ setSkills(newSkills);
|
|
|
+ message.success('技能添加成功');
|
|
|
+ } catch (error) {
|
|
|
+ message.error('技能添加失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRemoveSkill = async (skillToRemove: string) => {
|
|
|
+ try {
|
|
|
+ const newSkills = skills.filter(skill => skill !== skillToRemove);
|
|
|
+ await updateProfile({ skills: newSkills.join(',') });
|
|
|
+ setSkills(newSkills);
|
|
|
+ message.success('技能删除成功');
|
|
|
+ } catch (error) {
|
|
|
+ message.error('技能删除失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleAddCertificate = async (values: any) => {
|
|
|
+ try {
|
|
|
+ const newCertificates = [...certificates, values];
|
|
|
+ await updateProfile({ certificates: newCertificates });
|
|
|
+ setCertificates(newCertificates);
|
|
|
+ message.success('证书添加成功');
|
|
|
+ } catch (error) {
|
|
|
+ message.error('证书添加失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRemoveCertificate = async (index: number) => {
|
|
|
+ try {
|
|
|
+ const newCertificates = certificates.filter((_, i) => i !== index);
|
|
|
+ await updateProfile({ certificates: newCertificates });
|
|
|
+ setCertificates(newCertificates);
|
|
|
+ message.success('证书删除成功');
|
|
|
+ } catch (error) {
|
|
|
+ message.error('证书删除失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const updateProfile = async (data: any) => {
|
|
|
+ const response = await (silverUsersClient as any)['profiles'].$get({
|
|
|
+ query: { filters: JSON.stringify({ userId: user.id }) }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.status === 200) {
|
|
|
+ const profileData = await response.json();
|
|
|
+ const profile = profileData.data[0];
|
|
|
+
|
|
|
+ if (profile?.id) {
|
|
|
+ await (silverUsersClient as any)['profiles'][profile.id].$put({
|
|
|
+ json: { ...profile, ...data }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ await (silverUsersClient as any)['profiles'].$post({
|
|
|
+ json: { userId: user.id, ...data }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUploadCertificate: UploadProps['customRequest'] = async (options) => {
|
|
|
+ const { file, onSuccess, onError } = options;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file);
|
|
|
+
|
|
|
+ const response = await fetch('/api/files/upload', {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ if (data.url) {
|
|
|
+ form.setFieldsValue({ imageUrl: data.url });
|
|
|
+ onSuccess?.(data);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ onError?.(error as Error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const showModal = (type: 'skill' | 'certificate', item?: any) => {
|
|
|
+ setModalType(type);
|
|
|
+ setEditingItem(item);
|
|
|
+ setModalVisible(true);
|
|
|
+
|
|
|
+ if (type === 'certificate' && item) {
|
|
|
+ form.setFieldsValue(item);
|
|
|
+ } else {
|
|
|
+ form.resetFields();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleModalSubmit = async (values: any) => {
|
|
|
+ if (modalType === 'skill') {
|
|
|
+ await handleAddSkill(values);
|
|
|
+ } else {
|
|
|
+ await handleAddCertificate(values);
|
|
|
+ }
|
|
|
+ setModalVisible(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ const skillCategories = [
|
|
|
+ '医疗健康',
|
|
|
+ '教育培训',
|
|
|
+ '农业技术',
|
|
|
+ '生活技能',
|
|
|
+ '艺术文化',
|
|
|
+ '体育健身',
|
|
|
+ '科技应用',
|
|
|
+ '心理咨询',
|
|
|
+ '法律咨询',
|
|
|
+ '财务管理'
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
|
+ <div className="text-center">
|
|
|
+ <Empty description="请登录管理技能" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-gray-50">
|
|
|
+ <div className="bg-white">
|
|
|
+ <div className="flex items-center p-4 border-b">
|
|
|
+ <Button type="text" onClick={() => navigate('/profile')}>
|
|
|
+ 返回
|
|
|
+ </Button>
|
|
|
+ <h1 className="flex-1 text-center text-lg font-semibold">技能管理</h1>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="p-4 space-y-6">
|
|
|
+ {/* 技能标签 */}
|
|
|
+ <Card
|
|
|
+ title="我的技能标签"
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={() => showModal('skill')}
|
|
|
+ >
|
|
|
+ 添加技能
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {skills.length > 0 ? (
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ {skills.map((skill, index) => (
|
|
|
+ <Tag
|
|
|
+ key={index}
|
|
|
+ closable
|
|
|
+ onClose={() => handleRemoveSkill(skill)}
|
|
|
+ className="px-3 py-1"
|
|
|
+ >
|
|
|
+ {skill}
|
|
|
+ </Tag>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <Empty description="暂无技能标签" />
|
|
|
+ )}
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* 技能证书 */}
|
|
|
+ <Card
|
|
|
+ title="技能证书"
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={() => showModal('certificate')}
|
|
|
+ >
|
|
|
+ 添加证书
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <List
|
|
|
+ dataSource={certificates}
|
|
|
+ renderItem={(item, index) => (
|
|
|
+ <List.Item
|
|
|
+ actions={[
|
|
|
+ <Button
|
|
|
+ key="edit"
|
|
|
+ type="text"
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ onClick={() => showModal('certificate', item)}
|
|
|
+ />,
|
|
|
+ <Button
|
|
|
+ key="delete"
|
|
|
+ type="text"
|
|
|
+ danger
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
+ onClick={() => handleRemoveCertificate(index)}
|
|
|
+ />
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <List.Item.Meta
|
|
|
+ title={item.name}
|
|
|
+ description={
|
|
|
+ <div>
|
|
|
+ <p>{item.description}</p>
|
|
|
+ <p className="text-gray-500 text-sm">
|
|
|
+ 颁发机构: {item.issuingAgency} | 有效期: {item.validityPeriod}
|
|
|
+ </p>
|
|
|
+ {item.imageUrl && (
|
|
|
+ <img
|
|
|
+ src={item.imageUrl}
|
|
|
+ alt={item.name}
|
|
|
+ className="w-32 h-20 object-cover rounded mt-2"
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </List.Item>
|
|
|
+ )}
|
|
|
+ locale={{ emptyText: <Empty description="暂无技能证书" /> }}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* 添加模态框 */}
|
|
|
+ <Modal
|
|
|
+ title={modalType === 'skill' ? '添加技能' : '添加证书'}
|
|
|
+ open={modalVisible}
|
|
|
+ onCancel={() => setModalVisible(false)}
|
|
|
+ onOk={() => form.submit()}
|
|
|
+ okText="保存"
|
|
|
+ cancelText="取消"
|
|
|
+ >
|
|
|
+ <Form form={form} onFinish={handleModalSubmit} layout="vertical">
|
|
|
+ {modalType === 'skill' ? (
|
|
|
+ <Form.Item
|
|
|
+ name="skill"
|
|
|
+ label="技能名称"
|
|
|
+ rules={[{ required: true, message: '请输入技能名称' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="请选择或输入技能" mode="tags">
|
|
|
+ {skillCategories.map(category => (
|
|
|
+ <Option key={category} value={category}>{category}</Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <Form.Item
|
|
|
+ name="name"
|
|
|
+ label="证书名称"
|
|
|
+ rules={[{ required: true, message: '请输入证书名称' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入证书名称" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="issuingAgency"
|
|
|
+ label="颁发机构"
|
|
|
+ rules={[{ required: true, message: '请输入颁发机构' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入颁发机构" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="validityPeriod"
|
|
|
+ label="有效期"
|
|
|
+ rules={[{ required: true, message: '请选择有效期' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="例:2025-12-31" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item name="description" label="证书描述">
|
|
|
+ <Input.TextArea
|
|
|
+ rows={3}
|
|
|
+ placeholder="请输入证书描述"
|
|
|
+ maxLength={200}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item name="imageUrl" label="证书图片">
|
|
|
+ <Upload
|
|
|
+ customRequest={handleUploadCertificate}
|
|
|
+ showUploadList={false}
|
|
|
+ accept="image/*"
|
|
|
+ >
|
|
|
+ <Button icon={<UploadOutlined />}>上传证书图片</Button>
|
|
|
+ </Upload>
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default SkillsPage;
|