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

✨ feat(silver-jobs): 新增银龄岗管理系统完整功能

- 创建银龄岗实体类包含完整字段定义和状态枚举
- 实现基于GenericCrudService的通用CRUD API接口
- 开发管理员端银龄岗管理页面支持增删改查和状态管理
- 集成银龄岗菜单到管理员导航系统
- 添加客户端API调用封装支持前后端通信
- 优化银龄人才管理页面视觉样式和交互体验
- 提供详细实施文档包含技术方案和步骤说明
yourname 7 месяцев назад
Родитель
Сommit
47c2a40e06

+ 184 - 0
docs/silver-job-management-implementation-plan.md

@@ -0,0 +1,184 @@
+# 银龄岗管理功能实施方案
+
+## 项目概述
+基于现有架构,创建一套独立的银龄岗(SilverJob)管理系统,包含完整的前后端功能。
+
+## 技术方案
+采用标准通用CRUD模式实现,使用GenericCrudService和createCrudRoutes快速开发。
+
+## 实施步骤
+
+### 1. 银龄岗实体设计
+**文件位置**: `src/server/modules/silver-jobs/silver-job.entity.ts`
+
+```typescript
+// 银龄岗状态枚举
+export enum JobStatus {
+  DRAFT = 0,        // 草稿
+  PUBLISHED = 1,    // 已发布
+  CLOSED = 2,       // 已关闭
+  FILLED = 3        // 已招满
+}
+
+// 银龄岗实体类
+@Entity('silver_jobs')
+export class SilverJob {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'title', type: 'varchar', length: 255 })
+  title!: string;
+
+  @Column({ name: 'description', type: 'text' })
+  description!: string;
+
+  @Column({ name: 'requirements', type: 'text', nullable: true })
+  requirements!: string | null;
+
+  @Column({ name: 'location', type: 'varchar', length: 255 })
+  location!: string;
+
+  @Column({ name: 'salary_range', type: 'varchar', length: 100, nullable: true })
+  salaryRange!: string | null;
+
+  @Column({ name: 'work_hours', type: 'varchar', length: 100 })
+  workHours!: string;
+
+  @Column({ name: 'contact_person', type: 'varchar', length: 50 })
+  contactPerson!: string;
+
+  @Column({ name: 'contact_phone', type: 'varchar', length: 20 })
+  contactPhone!: string;
+
+  @Column({ name: 'contact_email', type: 'varchar', length: 255, nullable: true })
+  contactEmail!: string | null;
+
+  @Column({ name: 'employer_name', type: 'varchar', length: 255 })
+  employerName!: string;
+
+  @Column({ name: 'employer_address', type: 'varchar', length: 500, nullable: true })
+  employerAddress!: string | null;
+
+  @Column({ name: 'job_type', type: 'varchar', length: 50 })
+  jobType!: string;
+
+  @Column({ name: 'required_skills', type: 'text', nullable: true })
+  requiredSkills!: string | null;
+
+  @Column({ name: 'benefits', type: 'text', nullable: true })
+  benefits!: string | null;
+
+  @Column({ name: 'application_deadline', type: 'date', nullable: true })
+  applicationDeadline!: Date | null;
+
+  @Column({ name: 'start_date', type: 'date', nullable: true })
+  startDate!: Date | null;
+
+  @Column({ name: 'status', type: 'tinyint', default: JobStatus.DRAFT })
+  status!: JobStatus;
+
+  @Column({ name: 'view_count', type: 'int', unsigned: true, default: 0 })
+  viewCount!: number;
+
+  @Column({ name: 'application_count', type: 'int', unsigned: true, default: 0 })
+  applicationCount!: number;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', nullable: true })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', nullable: true })
+  updatedBy!: number | null;
+}
+```
+
+### 2. 数据源注册
+**文件位置**: `src/server/data-source.ts`
+**修改内容**: 在entities数组中添加SilverJob实体
+
+### 3. 通用CRUD API实现
+**文件位置**: `src/server/api/silver-jobs/index.ts`
+
+使用createCrudRoutes快速创建:
+- GET /api/v1/silver-jobs - 获取岗位列表
+- POST /api/v1/silver-jobs - 创建新岗位
+- GET /api/v1/silver-jobs/:id - 获取岗位详情
+- PUT /api/v1/silver-jobs/:id - 更新岗位
+- DELETE /api/v1/silver-jobs/:id - 删除岗位
+
+### 4. 管理员菜单配置
+**文件位置**: `src/client/admin/menu.tsx`
+**修改内容**: 在menuItems数组中添加:
+```typescript
+{
+  key: 'silver-jobs',
+  label: '银龄岗管理',
+  icon: <BriefcaseOutlined />,
+  path: '/admin/silver-jobs',
+  permission: 'silver-job:manage'
+}
+```
+
+### 5. 路由配置
+**文件位置**: `src/client/admin/routes.tsx`
+**修改内容**: 在children数组中添加:
+```typescript
+{
+  path: 'silver-jobs',
+  element: <SilverJobsPage />,
+  errorElement: <ErrorPage />
+}
+```
+
+### 6. 管理页面创建
+**文件位置**: `src/client/admin/pages/SilverJobs.tsx`
+
+页面功能包括:
+- 岗位列表展示(支持分页、搜索、筛选)
+- 岗位详情查看
+- 岗位创建/编辑
+- 状态管理(草稿/发布/关闭/招满)
+- 统计卡片展示
+
+### 7. 客户端API定义
+**文件位置**: `src/client/api.ts`
+**修改内容**: 添加silverJobsClient定义
+
+## 数据结构详细说明
+
+### 银龄岗字段清单
+| 字段名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| title | string | 是 | 岗位标题 |
+| description | text | 是 | 岗位描述 |
+| requirements | text | 否 | 岗位要求 |
+| location | string | 是 | 工作地点 |
+| salaryRange | string | 否 | 薪资范围 |
+| workHours | string | 是 | 工作时间 |
+| contactPerson | string | 是 | 联系人 |
+| contactPhone | string | 是 | 联系电话 |
+| contactEmail | string | 否 | 联系邮箱 |
+| employerName | string | 是 | 雇主名称 |
+| employerAddress | string | 否 | 雇主地址 |
+| jobType | string | 是 | 岗位类型 |
+| requiredSkills | text | 否 | 所需技能 |
+| benefits | text | 否 | 福利待遇 |
+| applicationDeadline | date | 否 | 申请截止日期 |
+| startDate | date | 否 | 开始日期 |
+| status | tinyint | 是 | 岗位状态 |
+
+## 实施时间预估
+- 后端实体和API:30分钟
+- 前端页面和集成:45分钟
+- 测试和调试:15分钟
+- 总计:约90分钟
+
+## 下一步操作
+1. 切换到代码模式实施具体开发
+2. 按顺序完成各个组件的创建
+3. 进行集成测试验证功能完整性

+ 8 - 0
src/client/admin/menu.tsx

@@ -9,6 +9,7 @@ import {
   InfoCircleOutlined,
   FileTextOutlined,
   UsergroupAddOutlined,
+  BriefcaseOutlined,
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -94,6 +95,13 @@ export const useMenu = () => {
       path: '/admin/silver-talents',
       permission: 'silver-talent:manage'
     },
+    {
+      key: 'silver-jobs',
+      label: '银龄岗管理',
+      icon: <BriefcaseOutlined />,
+      path: '/admin/silver-jobs',
+      permission: 'silver-job:manage'
+    },
     {
       key: 'files',
       label: '文件管理',

+ 621 - 0
src/client/admin/pages/SilverJobs.tsx

@@ -0,0 +1,621 @@
+import React, { useState, useEffect } from 'react';
+import {
+  Table,
+  Card,
+  Space,
+  Button,
+  Input,
+  Select,
+  Tag,
+  Modal,
+  Form,
+  message,
+  Descriptions,
+  Statistic,
+  Row,
+  Col,
+  DatePicker,
+  Switch,
+  Popconfirm
+} from 'antd';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import type { SilverJobRoutes } from '@/server/api';
+import { silverJobClient } from '@/client/api';
+import { SearchOutlined, EyeOutlined, EditOutlined, DeleteOutlined, PlusOutlined, FileTextOutlined, CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+
+const { Option } = Select;
+const { TextArea } = Input;
+const { RangePicker } = DatePicker;
+
+// 响应类型定义
+type SilverJobListResponse = InferResponseType<typeof silverJobClient.$get, 200>;
+type SilverJob = SilverJobListResponse['data'][0];
+type CreateSilverJobRequest = InferRequestType<typeof silverJobClient.$post>['json'];
+type UpdateSilverJobRequest = InferRequestType<typeof silverJobClient[":id"]["$put"]>['json'];
+
+// 岗位状态枚举
+const JobStatus = {
+  DRAFT: 0,
+  PUBLISHED: 1,
+  CLOSED: 2,
+  FILLED: 3
+};
+
+// 状态显示文本
+const getStatusText = (status: number) => {
+  switch (status) {
+    case 0: return '草稿';
+    case 1: return '已发布';
+    case 2: return '已关闭';
+    case 3: return '已招满';
+    default: return '未知';
+  }
+};
+
+// 状态颜色
+const getStatusColor = (status: number) => {
+  switch (status) {
+    case 0: return 'default';
+    case 1: return 'success';
+    case 2: return 'warning';
+    case 3: return 'error';
+    default: return 'default';
+  }
+};
+
+export const SilverJobsPage: React.FC = () => {
+  const [loading, setLoading] = useState(false);
+  const [data, setData] = useState<SilverJob[]>([]);
+  const [total, setTotal] = useState(0);
+  const [current, setCurrent] = useState(1);
+  const [pageSize, setPageSize] = useState(20);
+  const [searchParams, setSearchParams] = useState({
+    keyword: '',
+    status: undefined as number | undefined,
+  });
+  const [selectedJob, setSelectedJob] = useState<SilverJob | null>(null);
+  const [detailModalVisible, setDetailModalVisible] = useState(false);
+  const [editModalVisible, setEditModalVisible] = useState(false);
+  const [editForm] = Form.useForm();
+  const [createModalVisible, setCreateModalVisible] = useState(false);
+  const [createForm] = Form.useForm();
+
+  // 列配置
+  const columns = [
+    {
+      title: '岗位标题',
+      dataIndex: 'title',
+      key: 'title',
+      width: 200,
+      ellipsis: true,
+      render: (text: string) => (
+        <span style={{ color: '#1f2937', fontWeight: 500 }}>{text}</span>
+      ),
+    },
+    {
+      title: '雇主名称',
+      dataIndex: 'employerName',
+      key: 'employerName',
+      width: 150,
+      ellipsis: true,
+      render: (text: string) => (
+        <span style={{ color: '#6b7280' }}>{text}</span>
+      ),
+    },
+    {
+      title: '工作地点',
+      dataIndex: 'location',
+      key: 'location',
+      width: 150,
+      ellipsis: true,
+      render: (text: string) => (
+        <span style={{ color: '#6b7280' }}>{text}</span>
+      ),
+    },
+    {
+      title: '薪资范围',
+      dataIndex: 'salaryRange',
+      key: 'salaryRange',
+      width: 120,
+      render: (text: string) => text || '-',
+    },
+    {
+      title: '工作时间',
+      dataIndex: 'workHours',
+      key: 'workHours',
+      width: 120,
+      render: (text: string) => text || '-',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      width: 100,
+      fixed: 'left' as const,
+      render: (status: number) => (
+        <Tag
+          color={getStatusColor(status)}
+          style={{
+            borderRadius: '12px',
+            padding: '4px 8px',
+            fontSize: '12px'
+          }}
+        >
+          {getStatusText(status)}
+        </Tag>
+      ),
+    },
+    {
+      title: '浏览量',
+      dataIndex: 'viewCount',
+      key: 'viewCount',
+      width: 80,
+      sorter: true,
+      render: (count: number) => (
+        <span style={{ color: '#3b82f6' }}>{count}</span>
+      ),
+    },
+    {
+      title: '申请人数',
+      dataIndex: 'applicationCount',
+      key: 'applicationCount',
+      width: 80,
+      sorter: true,
+      render: (count: number) => (
+        <span style={{ color: '#10b981' }}>{count}</span>
+      ),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 150,
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      fixed: 'right' as const,
+      render: (_: any, record: SilverJob) => (
+        <Space size="small">
+          <Button
+            type="text"
+            icon={<EyeOutlined />}
+            onClick={() => handleViewDetail(record)}
+            style={{ color: '#3b82f6' }}
+          >
+            详情
+          </Button>
+          <Button
+            type="text"
+            icon={<EditOutlined />}
+            onClick={() => handleEdit(record)}
+            style={{ color: '#f59e0b' }}
+          >
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除这个岗位吗?"
+            onConfirm={() => handleDelete(record)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button
+              type="text"
+              danger
+              icon={<DeleteOutlined />}
+            >
+              删除
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  // 获取岗位列表数据
+  const fetchData = async () => {
+    setLoading(true);
+    try {
+      const response = await silverJobClient.$get({
+        query: {
+          page: current,
+          pageSize,
+          keyword: searchParams.keyword || undefined,
+          status: searchParams.status,
+        }
+      });
+
+      if (response.status === 200) {
+        const result = await response.json();
+        setData(result.data);
+        setTotal(result.pagination.total);
+      }
+    } catch (error) {
+      message.error('获取岗位数据失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 获取统计数据
+  const getStats = () => {
+    const stats = {
+      total: data.length,
+      published: data.filter(item => item.status === 1).length,
+      draft: data.filter(item => item.status === 0).length,
+      closed: data.filter(item => item.status === 2).length,
+      filled: data.filter(item => item.status === 3).length,
+    };
+    return stats;
+  };
+
+  useEffect(() => {
+    fetchData();
+  }, [current, pageSize, searchParams]);
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams(values);
+    setCurrent(1);
+  };
+
+  // 处理重置
+  const handleReset = () => {
+    setSearchParams({
+      keyword: '',
+      status: undefined,
+    });
+    setCurrent(1);
+  };
+
+  // 处理查看详情
+  const handleViewDetail = (record: SilverJob) => {
+    setSelectedJob(record);
+    setDetailModalVisible(true);
+  };
+
+  // 处理编辑
+  const handleEdit = (record: SilverJob) => {
+    setSelectedJob(record);
+    editForm.setFieldsValue({
+      ...record,
+      applicationDeadline: record.applicationDeadline ? dayjs(record.applicationDeadline) : null,
+      startDate: record.startDate ? dayjs(record.startDate) : null,
+    });
+    setEditModalVisible(true);
+  };
+
+  // 处理创建
+  const handleCreate = () => {
+    createForm.resetFields();
+    setCreateModalVisible(true);
+  };
+
+  // 处理创建提交
+  const handleCreateSubmit = async (values: any) => {
+    try {
+      const payload: CreateSilverJobRequest = {
+        ...values,
+        applicationDeadline: values.applicationDeadline ? values.applicationDeadline.format('YYYY-MM-DD') : null,
+        startDate: values.startDate ? values.startDate.format('YYYY-MM-DD') : null,
+      };
+
+      const response = await silverJobClient.$post({
+        json: payload
+      });
+
+      if (response.status === 200) {
+        message.success('岗位创建成功');
+        setCreateModalVisible(false);
+        fetchData();
+        createForm.resetFields();
+      }
+    } catch (error) {
+      message.error('创建岗位失败');
+    }
+  };
+
+  // 处理编辑提交
+  const handleEditSubmit = async (values: any) => {
+    if (!selectedJob) return;
+
+    try {
+      const payload: UpdateSilverJobRequest = {
+        ...values,
+        applicationDeadline: values.applicationDeadline ? values.applicationDeadline.format('YYYY-MM-DD') : null,
+        startDate: values.startDate ? values.startDate.format('YYYY-MM-DD') : null,
+      };
+
+      const response = await silverJobClient[`:id`]['$put']({
+        json: payload,
+        param: {
+          id: selectedJob.id.toString()
+        }
+      });
+
+      if (response.status === 200) {
+        message.success('岗位更新成功');
+        setEditModalVisible(false);
+        fetchData();
+      }
+    } catch (error) {
+      message.error('更新岗位失败');
+    }
+  };
+
+  // 处理删除
+  const handleDelete = async (record: SilverJob) => {
+    try {
+      const response = await silverJobClient[`:id`]['$delete']({
+        param: {
+          id: record.id.toString()
+        }
+      });
+
+      if (response.status === 200) {
+        message.success('岗位删除成功');
+        fetchData();
+      }
+    } catch (error) {
+      message.error('删除岗位失败');
+    }
+  };
+
+  const stats = getStats();
+
+  return (
+    <div style={{ padding: 24, backgroundColor: '#f5f5f5', minHeight: '100vh' }}>
+      
+      {/* 统计卡片 */}
+      <Row gutter={24} style={{ marginBottom: 32 }}>
+        <Col span={6}>
+          <Card style={{ height: 120 }}>
+            <Statistic
+              title="总岗位数"
+              value={stats.total}
+              prefix={<FileTextOutlined />}
+              valueStyle={{ color: '#1f2937' }}
+            />
+          </Card>
+        </Col>
+        <Col span={6}>
+          <Card style={{ height: 120 }}>
+            <Statistic
+              title="已发布"
+              value={stats.published}
+              prefix={<CheckCircleOutlined />}
+              valueStyle={{ color: '#10b981' }}
+            />
+          </Card>
+        </Col>
+        <Col span={6}>
+          <Card style={{ height: 120 }}>
+            <Statistic
+              title="草稿"
+              value={stats.draft}
+              prefix={<ClockCircleOutlined />}
+              valueStyle={{ color: '#6b7280' }}
+            />
+          </Card>
+        </Col>
+        <Col span={6}>
+          <Card style={{ height: 120 }}>
+            <Statistic
+              title="已招满"
+              value={stats.filled}
+              prefix={<CloseCircleOutlined />}
+              valueStyle={{ color: '#ef4444' }}
+            />
+          </Card>
+        </Col>
+      </Row>
+
+      {/* 搜索表单 */}
+      <Card style={{ marginBottom: 32 }}>
+        <Form layout="inline" onFinish={handleSearch}>
+          <Form.Item name="keyword" label="关键词">
+            <Input
+              placeholder="岗位标题/雇主名称/地点"
+              prefix={<SearchOutlined />}
+              style={{ width: 200 }}
+            />
+          </Form.Item>
+          <Form.Item name="status" label="状态">
+            <Select style={{ width: 120 }} allowClear placeholder="全部状态">
+              <Option value={0}>草稿</Option>
+              <Option value={1}>已发布</Option>
+              <Option value={2}>已关闭</Option>
+              <Option value={3}>已招满</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">搜索</Button>
+              <Button onClick={handleReset}>重置</Button>
+              <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
+                新建岗位
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </Card>
+
+      {/* 数据表格 */}
+      <Card>
+        <Table
+          columns={columns}
+          dataSource={data}
+          loading={loading}
+          rowKey="id"
+          pagination={{
+            current,
+            pageSize,
+            total,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
+            onChange: (page, size) => {
+              setCurrent(page);
+              setPageSize(size);
+            },
+          }}
+          scroll={{ x: 'max-content' }}
+        />
+      </Card>
+
+      {/* 详情弹窗 */}
+      <Modal
+        title="岗位详情"
+        width={800}
+        open={detailModalVisible}
+        onCancel={() => setDetailModalVisible(false)}
+        footer={[
+          <Button key="close" onClick={() => setDetailModalVisible(false)}>
+            关闭
+          </Button>,
+        ]}
+      >
+        {selectedJob && (
+          <Descriptions bordered column={2}>
+            <Descriptions.Item label="岗位标题">{selectedJob.title}</Descriptions.Item>
+            <Descriptions.Item label="雇主名称">{selectedJob.employerName}</Descriptions.Item>
+            <Descriptions.Item label="工作地点">{selectedJob.location}</Descriptions.Item>
+            <Descriptions.Item label="薪资范围">{selectedJob.salaryRange || '-'}</Descriptions.Item>
+            <Descriptions.Item label="工作时间">{selectedJob.workHours}</Descriptions.Item>
+            <Descriptions.Item label="岗位类型">{selectedJob.jobType}</Descriptions.Item>
+            <Descriptions.Item label="状态">
+              <Tag color={getStatusColor(selectedJob.status)}>
+                {getStatusText(selectedJob.status)}
+              </Tag>
+            </Descriptions.Item>
+            <Descriptions.Item label="联系人">{selectedJob.contactPerson}</Descriptions.Item>
+            <Descriptions.Item label="联系电话">{selectedJob.contactPhone}</Descriptions.Item>
+            <Descriptions.Item label="联系邮箱">{selectedJob.contactEmail || '-'}</Descriptions.Item>
+            <Descriptions.Item label="申请截止">{selectedJob.applicationDeadline ? dayjs(selectedJob.applicationDeadline).format('YYYY-MM-DD') : '-'}</Descriptions.Item>
+            <Descriptions.Item label="开始日期">{selectedJob.startDate ? dayjs(selectedJob.startDate).format('YYYY-MM-DD') : '-'}</Descriptions.Item>
+            <Descriptions.Item label="岗位描述" span={2}>
+              {selectedJob.description}
+            </Descriptions.Item>
+            <Descriptions.Item label="岗位要求" span={2}>
+              {selectedJob.requirements || '-'}
+            </Descriptions.Item>
+            <Descriptions.Item label="所需技能" span={2}>
+              {selectedJob.requiredSkills || '-'}
+            </Descriptions.Item>
+            <Descriptions.Item label="福利待遇" span={2}>
+              {selectedJob.benefits || '-'}
+            </Descriptions.Item>
+          </Descriptions>
+        )}
+      </Modal>
+
+      {/* 创建/编辑表单 */}
+      <Modal
+        title={editModalVisible ? "编辑岗位" : "新建岗位"}
+        width={600}
+        open={editModalVisible || createModalVisible}
+        onCancel={() => {
+          editModalVisible ? setEditModalVisible(false) : setCreateModalVisible(false);
+        }}
+        onOk={() => editModalVisible ? editForm.submit() : createForm.submit()}
+      >
+        <Form
+          form={editModalVisible ? editForm : createForm}
+          onFinish={editModalVisible ? handleEditSubmit : handleCreateSubmit}
+          layout="vertical"
+        >
+          <Form.Item
+            name="title"
+            label="岗位标题"
+            rules={[{ required: true, message: '请输入岗位标题' }]}
+          >
+            <Input placeholder="请输入岗位标题" />
+          </Form.Item>
+          <Form.Item
+            name="description"
+            label="岗位描述"
+            rules={[{ required: true, message: '请输入岗位描述' }]}
+          >
+            <TextArea rows={4} placeholder="请输入岗位描述" />
+          </Form.Item>
+          <Form.Item
+            name="employerName"
+            label="雇主名称"
+            rules={[{ required: true, message: '请输入雇主名称' }]}
+          >
+            <Input placeholder="请输入雇主名称" />
+          </Form.Item>
+          <Form.Item
+            name="location"
+            label="工作地点"
+            rules={[{ required: true, message: '请输入工作地点' }]}
+          >
+            <Input placeholder="请输入工作地点" />
+          </Form.Item>
+          <Form.Item
+            name="workHours"
+            label="工作时间"
+            rules={[{ required: true, message: '请输入工作时间' }]}
+          >
+            <Input placeholder="如:每周二、四下午2-4点" />
+          </Form.Item>
+          <Form.Item name="salaryRange" label="薪资范围">
+            <Input placeholder="如:2000-3000元/月" />
+          </Form.Item>
+          <Form.Item name="jobType" label="岗位类型">
+            <Select placeholder="请选择岗位类型">
+              <Option value="兼职教师">兼职教师</Option>
+              <Option value="社区服务">社区服务</Option>
+              <Option value="志愿活动">志愿活动</Option>
+              <Option value="技术支持">技术支持</Option>
+              <Option value="其他">其他</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item name="contactPerson" label="联系人">
+            <Input placeholder="请输入联系人姓名" />
+          </Form.Item>
+          <Form.Item
+            name="contactPhone"
+            label="联系电话"
+            rules={[{ required: true, message: '请输入联系电话' }]}
+          >
+            <Input placeholder="请输入联系电话" />
+          </Form.Item>
+          <Form.Item name="contactEmail" label="联系邮箱">
+            <Input placeholder="请输入联系邮箱" />
+          </Form.Item>
+          <Form.Item name="requirements" label="岗位要求">
+            <TextArea rows={3} placeholder="请输入岗位要求" />
+          </Form.Item>
+          <Form.Item name="requiredSkills" label="所需技能">
+            <TextArea rows={3} placeholder="请输入所需技能" />
+          </Form.Item>
+          <Form.Item name="benefits" label="福利待遇">
+            <TextArea rows={3} placeholder="请输入福利待遇" />
+          </Form.Item>
+          <Form.Item name="applicationDeadline" label="申请截止日期">
+            <DatePicker style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item name="startDate" label="开始日期">
+            <DatePicker style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item
+            name="status"
+            label="状态"
+            initialValue={1}
+          >
+            <Select placeholder="请选择状态">
+              <Option value={0}>草稿</Option>
+              <Option value={1}>已发布</Option>
+              <Option value={2}>已关闭</Option>
+              <Option value={3}>已招满</Option>
+            </Select>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 268 - 78
src/client/admin/pages/SilverTalents.tsx

@@ -106,7 +106,11 @@ export const SilverTalentsPage: React.FC = () => {
           size={48}
           src={url}
           icon={<UserOutlined />}
-          style={{ backgroundColor: '#87d068' }}
+          style={{
+            backgroundColor: 'rgba(139, 115, 85, 0.2)',
+            border: '2px solid rgba(139, 115, 85, 0.3)',
+            borderRadius: '50%'
+          }}
         />
       ),
     },
@@ -115,6 +119,9 @@ export const SilverTalentsPage: React.FC = () => {
       dataIndex: 'realName',
       key: 'realName',
       sorter: true,
+      render: (text: string) => (
+        <span style={{ color: '#2f1f0f', fontWeight: 500 }}>{text}</span>
+      ),
     },
     {
       title: '年龄',
@@ -122,19 +129,27 @@ export const SilverTalentsPage: React.FC = () => {
       key: 'age',
       width: 80,
       sorter: true,
+      render: (age: number) => (
+        <span style={{ color: '#8b7355' }}>{age}岁</span>
+      ),
     },
     {
       title: '性别',
       dataIndex: 'gender',
       key: 'gender',
       width: 80,
-      render: (gender: number) => getGenderText(gender),
+      render: (gender: number) => (
+        <span style={{ color: '#8b7355' }}>{getGenderText(gender)}</span>
+      ),
     },
     {
       title: '所属机构',
       dataIndex: 'organization',
       key: 'organization',
       ellipsis: true,
+      render: (text: string) => (
+        <span style={{ color: '#8b7355' }}>{text || '-'}</span>
+      ),
     },
     {
       title: '认证状态',
@@ -142,7 +157,18 @@ export const SilverTalentsPage: React.FC = () => {
       key: 'certificationStatus',
       width: 100,
       render: (status: number) => (
-        <Tag color={getCertificationStatusColor(status)}>
+        <Tag
+          color={getCertificationStatusColor(status)}
+          style={{
+            borderRadius: '12px',
+            padding: '4px 8px',
+            fontSize: '12px',
+            backgroundColor: status === 2 ? 'rgba(92, 124, 92, 0.1)' :
+                           status === 3 ? 'rgba(168, 92, 92, 0.1)' :
+                           status === 1 ? 'rgba(74, 107, 124, 0.1)' :
+                           'rgba(139, 115, 85, 0.1)'
+          }}
+        >
           {getCertificationStatusText(status)}
         </Tag>
       ),
@@ -153,7 +179,13 @@ export const SilverTalentsPage: React.FC = () => {
       key: 'jobSeekingStatus',
       width: 100,
       render: (status: number) => (
-        <Tag>
+        <Tag style={{
+          borderRadius: '12px',
+          padding: '4px 8px',
+          fontSize: '12px',
+          backgroundColor: 'rgba(139, 115, 85, 0.1)',
+          color: '#8b7355'
+        }}>
           {getJobSeekingStatusText(status)}
         </Tag>
       ),
@@ -164,6 +196,9 @@ export const SilverTalentsPage: React.FC = () => {
       key: 'totalPoints',
       width: 100,
       sorter: true,
+      render: (points: number) => (
+        <span style={{ color: '#5c7c5c', fontWeight: 600 }}>{points}</span>
+      ),
     },
     {
       title: '知识排名',
@@ -171,39 +206,85 @@ export const SilverTalentsPage: React.FC = () => {
       key: 'knowledgeRankingScore',
       width: 100,
       sorter: true,
+      render: (score: number) => (
+        <span style={{ color: '#4a6b7c', fontWeight: 600 }}>{score}</span>
+      ),
     },
     {
       title: '创建时间',
       dataIndex: 'createdAt',
       key: 'createdAt',
       width: 180,
-      render: (date: string) => new Date(date).toLocaleDateString(),
+      render: (date: string) => (
+        <span style={{ color: '#8b7355', fontSize: '12px' }}>
+          {new Date(date).toLocaleDateString()}
+        </span>
+      ),
     },
     {
       title: '操作',
       key: 'action',
-      width: 200,
+      width: 220,
       fixed: 'right' as const,
       render: (_: any, record: SilverTalent) => (
-        <Space>
+        <Space size="small">
           <Button
-            type="link"
+            type="text"
             icon={<EyeOutlined />}
             onClick={() => handleViewDetail(record)}
+            style={{
+              color: '#4a6b7c',
+              border: '1px solid rgba(74, 107, 124, 0.3)',
+              borderRadius: '16px',
+              padding: '4px 12px',
+              fontSize: '12px'
+            }}
+            onMouseEnter={(e) => {
+              e.currentTarget.style.backgroundColor = 'rgba(74, 107, 124, 0.1)';
+            }}
+            onMouseLeave={(e) => {
+              e.currentTarget.style.backgroundColor = 'transparent';
+            }}
           >
             详情
           </Button>
           <Button
-            type="link"
+            type="text"
             icon={<EditOutlined />}
             onClick={() => handleEdit(record)}
+            style={{
+              color: '#8b7355',
+              border: '1px solid rgba(139, 115, 85, 0.3)',
+              borderRadius: '16px',
+              padding: '4px 12px',
+              fontSize: '12px'
+            }}
+            onMouseEnter={(e) => {
+              e.currentTarget.style.backgroundColor = 'rgba(139, 115, 85, 0.1)';
+            }}
+            onMouseLeave={(e) => {
+              e.currentTarget.style.backgroundColor = 'transparent';
+            }}
           >
             编辑
           </Button>
           <Button
-            type="link"
+            type="text"
             icon={<CheckCircleOutlined />}
             onClick={() => handleCertification(record)}
+            style={{
+              color: '#5c7c5c',
+              border: '1px solid rgba(92, 124, 92, 0.3)',
+              borderRadius: '16px',
+              padding: '4px 12px',
+              fontSize: '12px'
+            }}
+            onMouseEnter={(e) => {
+              e.currentTarget.style.backgroundColor = 'rgba(92, 124, 92, 0.1)';
+            }}
+            onMouseLeave={(e) => {
+              e.currentTarget.style.backgroundColor = 'transparent';
+            }}
           >
             认证
           </Button>
@@ -352,63 +433,76 @@ export const SilverTalentsPage: React.FC = () => {
   };
 
   return (
-    <div style={{ padding: 24 }}>
+    <div style={{
+      padding: 24,
+      backgroundColor: '#f5f3f0',
+      minHeight: '100vh'
+    }}>
+
       {/* 统计卡片 */}
-      <Row gutter={24} style={{ marginBottom: 24 }}>
+      <Row gutter={24} style={{ marginBottom: 32 }}>
         <Col span={6}>
-          <Card>
+          <div className="stat-card">
             <Statistic
-              title="总人才数"
+              title={<span style={{ color: '#2f1f0f', fontSize: '16px', fontWeight: 500 }}>总人才数</span>}
               value={stats?.totalCount || 0}
-              prefix={<TeamOutlined />}
+              valueStyle={{ color: '#3a2f26', fontSize: '28px', fontWeight: 'bold' }}
+              prefix={<TeamOutlined style={{ color: '#8b7355', fontSize: '24px' }} />}
             />
-          </Card>
+          </div>
         </Col>
         <Col span={6}>
-          <Card>
+          <div className="stat-card">
             <Statistic
-              title="已认证"
+              title={<span style={{ color: '#2f1f0f', fontSize: '16px', fontWeight: 500 }}>已认证</span>}
               value={stats?.certifiedCount || 0}
-              prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
+              valueStyle={{ color: '#5c7c5c', fontSize: '28px', fontWeight: 'bold' }}
+              prefix={<CheckCircleOutlined style={{ color: '#5c7c5c', fontSize: '24px' }} />}
             />
-          </Card>
+          </div>
         </Col>
         <Col span={6}>
-          <Card>
+          <div className="stat-card">
             <Statistic
-              title="认证中"
+              title={<span style={{ color: '#2f1f0f', fontSize: '16px', fontWeight: 500 }}>认证中</span>}
               value={stats?.pendingCount || 0}
-              prefix={<BarChartOutlined style={{ color: '#1890ff' }} />}
+              valueStyle={{ color: '#4a6b7c', fontSize: '28px', fontWeight: 'bold' }}
+              prefix={<BarChartOutlined style={{ color: '#4a6b7c', fontSize: '24px' }} />}
             />
-          </Card>
+          </div>
         </Col>
         <Col span={6}>
-          <Card>
+          <div className="stat-card">
             <Statistic
-              title="未认证"
+              title={<span style={{ color: '#2f1f0f', fontSize: '16px', fontWeight: 500 }}>未认证</span>}
               value={stats?.unCertifiedCount || 0}
-              prefix={<CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
+              valueStyle={{ color: '#a85c5c', fontSize: '28px', fontWeight: 'bold' }}
+              prefix={<CloseCircleOutlined style={{ color: '#a85c5c', fontSize: '24px' }} />}
             />
-          </Card>
+          </div>
         </Col>
       </Row>
 
       {/* 搜索表单 */}
-      <Card style={{ marginBottom: 24 }}>
+      <div className="ink-card" style={{ marginBottom: 32, padding: 24 }}>
         <Form layout="inline" onFinish={handleSearch}>
-          <Form.Item name="keyword" label="关键词">
-            <Input placeholder="姓名/机构/技能" prefix={<SearchOutlined />} />
+          <Form.Item name="keyword" label={<span style={{ color: '#2f1f0f', fontWeight: 500 }}>关键词</span>}>
+            <Input
+              placeholder="姓名/机构/技能"
+              prefix={<SearchOutlined style={{ color: '#8b7355' }} />}
+              className="ink-input"
+            />
           </Form.Item>
-          <Form.Item name="certificationStatus" label="认证状态">
-            <Select style={{ width: 120 }} allowClear>
+          <Form.Item name="certificationStatus" label={<span style={{ color: '#2f1f0f', fontWeight: 500 }}>认证状态</span>}>
+            <Select style={{ width: 140 }} allowClear className="ink-select">
               <Option value={0}>未认证</Option>
               <Option value={1}>认证中</Option>
               <Option value={2}>已认证</Option>
               <Option value={3}>已拒绝</Option>
             </Select>
           </Form.Item>
-          <Form.Item name="jobSeekingStatus" label="求职状态">
-            <Select style={{ width: 120 }} allowClear>
+          <Form.Item name="jobSeekingStatus" label={<span style={{ color: '#2f1f0f', fontWeight: 500 }}>求职状态</span>}>
+            <Select style={{ width: 140 }} allowClear className="ink-select">
               <Option value={0}>未求职</Option>
               <Option value={1}>积极求职</Option>
               <Option value={2}>观望机会</Option>
@@ -416,15 +510,19 @@ export const SilverTalentsPage: React.FC = () => {
           </Form.Item>
           <Form.Item>
             <Space>
-              <Button type="primary" htmlType="submit">搜索</Button>
-              <Button onClick={handleReset}>重置</Button>
+              <Button type="primary" htmlType="submit" className="ink-button">
+                搜索
+              </Button>
+              <Button onClick={handleReset} className="ink-secondary-button">
+                重置
+              </Button>
             </Space>
           </Form.Item>
         </Form>
-      </Card>
+      </div>
 
       {/* 数据表格 */}
-      <Card>
+      <div className="ink-card" style={{ overflow: 'hidden' }}>
         <Table
           columns={columns}
           dataSource={data}
@@ -436,51 +534,97 @@ export const SilverTalentsPage: React.FC = () => {
             total,
             showSizeChanger: true,
             showQuickJumper: true,
-            showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
+            showTotal: (total, range) => <span style={{ color: '#2f1f0f' }}>第 {range[0]}-{range[1]} 条/共 {total} 条</span>,
             onChange: (page, size) => {
               setCurrent(page);
               setPageSize(size);
             },
           }}
           scroll={{ x: 'max-content' }}
+          className="silver-talents-table"
         />
-      </Card>
+      </div>
 
       {/* 详情弹窗 */}
       <Modal
-        title="人才详情"
+        title={<span style={{ color: '#2f1f0f', fontSize: '20px', fontWeight: 'bold' }}>人才详情</span>}
         width={800}
         open={detailModalVisible}
         onCancel={() => setDetailModalVisible(false)}
         footer={[
-          <Button key="close" onClick={() => setDetailModalVisible(false)}>
+          <Button key="close" onClick={() => setDetailModalVisible(false)} className="ink-secondary-button">
             关闭
           </Button>,
         ]}
+        className="ink-modal"
+        style={{
+          background: 'rgba(255, 255, 255, 0.95)',
+          backdropFilter: 'blur(10px)'
+        }}
+        bodyStyle={{
+          background: 'rgba(255, 255, 255, 0.9)',
+          borderRadius: '12px'
+        }}
       >
         {selectedTalent && (
-          <Descriptions bordered column={2}>
-            <Descriptions.Item label="姓名">{selectedTalent.realName}</Descriptions.Item>
-            <Descriptions.Item label="年龄">{selectedTalent.age}岁</Descriptions.Item>
-            <Descriptions.Item label="性别">{getGenderText(selectedTalent.gender)}</Descriptions.Item>
-            <Descriptions.Item label="所属机构">{selectedTalent.organization || '-'}</Descriptions.Item>
-            <Descriptions.Item label="联系电话">{selectedTalent.phone}</Descriptions.Item>
-            <Descriptions.Item label="邮箱">{selectedTalent.email || '-'}</Descriptions.Item>
+          <Descriptions
+            bordered
+            column={2}
+            style={{
+              background: 'rgba(255, 255, 255, 0.8)',
+              borderRadius: '8px',
+              border: '1px solid #d4c4a8'
+            }}
+            labelStyle={{
+              background: 'rgba(212, 196, 168, 0.3)',
+              color: '#2f1f0f',
+              fontWeight: 600,
+              borderRight: '1px solid #d4c4a8'
+            }}
+            contentStyle={{
+              color: '#2f1f0f',
+              background: 'rgba(255, 255, 255, 0.9)'
+            }}
+          >
+            <Descriptions.Item label="姓名"><strong style={{ color: '#3a2f26' }}>{selectedTalent.realName}</strong></Descriptions.Item>
+            <Descriptions.Item label="年龄"><span style={{ color: '#8b7355' }}>{selectedTalent.age}岁</span></Descriptions.Item>
+            <Descriptions.Item label="性别"><span style={{ color: '#8b7355' }}>{getGenderText(selectedTalent.gender)}</span></Descriptions.Item>
+            <Descriptions.Item label="所属机构"><span style={{ color: '#8b7355' }}>{selectedTalent.organization || '-'}</span></Descriptions.Item>
+            <Descriptions.Item label="联系电话"><span style={{ color: '#8b7355' }}>{selectedTalent.phone}</span></Descriptions.Item>
+            <Descriptions.Item label="邮箱"><span style={{ color: '#8b7355' }}>{selectedTalent.email || '-'}</span></Descriptions.Item>
             <Descriptions.Item label="认证状态">
-              <Tag color={getCertificationStatusColor(selectedTalent.certificationStatus)}>
+              <Tag color={getCertificationStatusColor(selectedTalent.certificationStatus)} style={{ fontSize: '14px', padding: '4px 12px' }}>
                 {getCertificationStatusText(selectedTalent.certificationStatus)}
               </Tag>
             </Descriptions.Item>
             <Descriptions.Item label="求职状态">
-              {getJobSeekingStatusText(selectedTalent.jobSeekingStatus)}
+              <span style={{ color: '#8b7355' }}>{getJobSeekingStatusText(selectedTalent.jobSeekingStatus)}</span>
             </Descriptions.Item>
-            <Descriptions.Item label="积分">{selectedTalent.totalPoints}</Descriptions.Item>
-            <Descriptions.Item label="知识排名分">{selectedTalent.knowledgeRankingScore}</Descriptions.Item>
+            <Descriptions.Item label="积分"><span style={{ color: '#5c7c5c', fontWeight: 'bold' }}>{selectedTalent.totalPoints}</span></Descriptions.Item>
+            <Descriptions.Item label="知识排名分"><span style={{ color: '#4a6b7c', fontWeight: 'bold' }}>{selectedTalent.knowledgeRankingScore}</span></Descriptions.Item>
             <Descriptions.Item label="个人简介" span={2}>
-              {selectedTalent.personalIntro || '-'}
+              <div style={{
+                background: 'rgba(245, 243, 240, 0.5)',
+                padding: '12px',
+                borderRadius: '8px',
+                border: '1px solid #d4c4a8',
+                color: '#2f1f0f',
+                lineHeight: '1.6'
+              }}>
+                {selectedTalent.personalIntro || '-'}
+              </div>
             </Descriptions.Item>
             <Descriptions.Item label="个人技能" span={2}>
-              {selectedTalent.personalSkills || '-'}
+              <div style={{
+                background: 'rgba(245, 243, 240, 0.5)',
+                padding: '12px',
+                borderRadius: '8px',
+                border: '1px solid #d4c4a8',
+                color: '#2f1f0f',
+                lineHeight: '1.6'
+              }}>
+                {selectedTalent.personalSkills || '-'}
+              </div>
             </Descriptions.Item>
           </Descriptions>
         )}
@@ -488,63 +632,109 @@ export const SilverTalentsPage: React.FC = () => {
 
       {/* 编辑弹窗 */}
       <Modal
-        title="编辑人才信息"
+        title={<span style={{ color: '#2f1f0f', fontSize: '20px', fontWeight: 'bold' }}>编辑人才信息</span>}
         width={600}
         open={editModalVisible}
         onCancel={() => setEditModalVisible(false)}
         onOk={() => editForm.submit()}
+        okButtonProps={{ className: 'ink-button' }}
+        cancelButtonProps={{ className: 'ink-secondary-button' }}
+        className="ink-modal"
+        style={{
+          background: 'rgba(255, 255, 255, 0.95)',
+          backdropFilter: 'blur(10px)'
+        }}
+        bodyStyle={{
+          background: 'rgba(255, 255, 255, 0.9)',
+          borderRadius: '12px'
+        }}
       >
         <Form form={editForm} onFinish={handleEditSubmit} layout="vertical">
-          <Form.Item name="realName" label="真实姓名" rules={[{ required: true }]}>
-            <Input />
+          <Form.Item
+            name="realName"
+            label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>真实姓名</span>}
+            rules={[{ required: true, message: '请输入真实姓名' }]}
+          >
+            <Input className="ink-input" />
           </Form.Item>
-          <Form.Item name="age" label="年龄" rules={[{ required: true }]}>
-            <Input type="number" min={50} max={100} />
+          <Form.Item
+            name="age"
+            label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>年龄</span>}
+            rules={[{ required: true, message: '请输入年龄' }]}
+          >
+            <Input type="number" min={50} max={100} className="ink-input" />
           </Form.Item>
-          <Form.Item name="gender" label="性别" rules={[{ required: true }]}>
-            <Select>
+          <Form.Item
+            name="gender"
+            label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>性别</span>}
+            rules={[{ required: true, message: '请选择性别' }]}
+          >
+            <Select className="ink-select">
               <Option value={0}>男</Option>
               <Option value={1}>女</Option>
               <Option value={2}>其他</Option>
             </Select>
           </Form.Item>
-          <Form.Item name="organization" label="所属机构">
-            <Input />
+          <Form.Item name="organization" label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>所属机构</span>}>
+            <Input className="ink-input" />
           </Form.Item>
-          <Form.Item name="phone" label="联系电话" rules={[{ required: true }]}>
-            <Input />
+          <Form.Item
+            name="phone"
+            label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>联系电话</span>}
+            rules={[{ required: true, message: '请输入联系电话' }]}
+          >
+            <Input className="ink-input" />
           </Form.Item>
-          <Form.Item name="email" label="邮箱">
-            <Input />
+          <Form.Item name="email" label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>邮箱</span>}>
+            <Input className="ink-input" />
           </Form.Item>
-          <Form.Item name="personalIntro" label="个人简介">
-            <TextArea rows={3} />
+          <Form.Item name="personalIntro" label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>个人简介</span>}>
+            <TextArea rows={3} className="ink-input" />
           </Form.Item>
-          <Form.Item name="personalSkills" label="个人技能">
-            <TextArea rows={3} />
+          <Form.Item name="personalSkills" label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>个人技能</span>}>
+            <TextArea rows={3} className="ink-input" />
           </Form.Item>
         </Form>
       </Modal>
 
       {/* 认证弹窗 */}
       <Modal
-        title="更新认证状态"
+        title={<span style={{ color: '#2f1f0f', fontSize: '20px', fontWeight: 'bold' }}>更新认证状态</span>}
         width={500}
         open={certModalVisible}
         onCancel={() => setCertModalVisible(false)}
         onOk={() => certForm.submit()}
+        okButtonProps={{ className: 'ink-button' }}
+        cancelButtonProps={{ className: 'ink-secondary-button' }}
+        className="ink-modal"
+        style={{
+          background: 'rgba(255, 255, 255, 0.95)',
+          backdropFilter: 'blur(10px)'
+        }}
+        bodyStyle={{
+          background: 'rgba(255, 255, 255, 0.9)',
+          borderRadius: '12px'
+        }}
       >
         <Form form={certForm} onFinish={handleCertSubmit} layout="vertical">
-          <Form.Item name="certificationStatus" label="认证状态" rules={[{ required: true }]}>
-            <Select>
+          <Form.Item
+            name="certificationStatus"
+            label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>认证状态</span>}
+            rules={[{ required: true, message: '请选择认证状态' }]}
+          >
+            <Select className="ink-select">
               <Option value={0}>未认证</Option>
               <Option value={1}>认证中</Option>
               <Option value={2}>已认证</Option>
               <Option value={3}>已拒绝</Option>
             </Select>
           </Form.Item>
-          <Form.Item name="certificationInfo" label="认证信息">
-            <TextArea rows={4} placeholder="请输入认证信息或拒绝理由" />
+          <Form.Item name="certificationInfo" label={<span style={{ color: '#2f1f0f', fontWeight: 600 }}>认证信息</span>}>
+            <TextArea
+              rows={4}
+              placeholder="请输入认证信息或拒绝理由"
+              className="ink-input"
+            />
           </Form.Item>
         </Form>
       </Modal>

+ 6 - 0
src/client/admin/routes.tsx

@@ -9,6 +9,7 @@ import { UsersPage } from './pages/Users';
 import { LoginPage } from './pages/Login';
 import { FilesPage } from './pages/Files';
 import { SilverTalentsPage } from './pages/SilverTalents';
+import { SilverJobsPage } from './pages/SilverJobs';
 
 export const router = createBrowserRouter([
   {
@@ -51,6 +52,11 @@ export const router = createBrowserRouter([
         element: <SilverTalentsPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'silver-jobs',
+        element: <SilverJobsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 8 - 2
src/client/api.ts

@@ -22,7 +22,8 @@ import type {
   SilverUsersKnowledgeInteractionsRoutes,
   SilverUsersKnowledgeRankingsRoutes,
   SilverTalentsAdminRoutes,
-  MyCompanyRoutes
+  MyCompanyRoutes,
+  SilverJobRoutes
 } from '@/server/api';
 import { axiosFetch } from './utils/axios'
 
@@ -130,4 +131,9 @@ export const silverTalentsClient = hc<SilverTalentsRoutes>('/', {
 // 银龄库管理客户端
 export const silverTalentsAdminClient = hc<SilverTalentsAdminRoutes>('/', {
   fetch: axiosFetch,
-}).api.v1['admin']['silver-talents']
+}).api.v1['admin']['silver-talents']
+
+// 银龄岗管理客户端
+export const silverJobClient = hc<SilverJobRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['silver-jobs']

+ 5 - 0
src/server/api.ts

@@ -21,6 +21,7 @@ import silverUserProfileRoutes from './api/silver-users/profiles/index'
 import silverTalentsAdminRoutes from './api/silver-talents-admin/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
+import silverJobRoutes from './api/silver-jobs/index'
 
 const api = new OpenAPIHono<AuthContext>()
 
@@ -95,6 +96,9 @@ const silverUserProfileApiRoutes = api.route('/api/v1/silver-users/profiles', si
 // 注册银龄库管理后台路由
 const silverTalentsAdminApiRoutes = api.route('/api/v1/admin/silver-talents', silverTalentsAdminRoutes)
 
+// 注册银龄岗管理后台路由
+const silverJobApiRoutes = api.route('/api/v1/silver-jobs', silverJobRoutes)
+
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
@@ -119,5 +123,6 @@ export type SilverUsersKnowledgeInteractionsRoutes = typeof silverUsersKnowledge
 export type SilverUsersKnowledgeRankingsRoutes = typeof silverUsersKnowledgeRankingsApiRoutes
 export type SilverUserProfileRoutes = typeof silverUserProfileApiRoutes
 export type SilverTalentsAdminRoutes = typeof silverTalentsAdminApiRoutes
+export type SilverJobRoutes = typeof silverJobApiRoutes
 
 export default api

+ 22 - 23
src/server/api/silver-jobs/index.ts

@@ -1,25 +1,24 @@
-import { OpenAPIHono } from '@hono/zod-openapi';
-import companyRoutes from './companies';
-import jobRoutes from './jobs';
-import applicationRoutes from './applications';
-import favoriteRoutes from './favorites';
-import viewRoutes from './views';
-import companyImageRoutes from './company-images';
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { SilverJob } from '@/server/modules/silver-jobs/silver-job.entity';
+import {
+  SilverJobSchema,
+  CreateSilverJobDto,
+  UpdateSilverJobDto
+} from '@/server/modules/silver-jobs/silver-job.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
 
-// const silverJobsApi = new OpenAPIHono()
-//   .route('/companies', companyRoutes)
-//   .route('/jobs', jobRoutes)
-//   .route('/applications', applicationRoutes)
-//   .route('/favorites', favoriteRoutes)
-//   .route('/views', viewRoutes)
-//   .route('/company-images', companyImageRoutes);
+const silverJobRoutes = createCrudRoutes({
+  entity: SilverJob,
+  createSchema: CreateSilverJobDto,
+  updateSchema: UpdateSilverJobDto,
+  getSchema: SilverJobSchema,
+  listSchema: SilverJobSchema,
+  searchFields: ['title', 'description', 'requirements', 'location', 'employerName', 'jobType'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
 
-// export default silverJobsApi;
-export default {
-  companyRoutes,
-  jobRoutes,
-  applicationRoutes,
-  favoriteRoutes,
-  viewRoutes,
-  companyImageRoutes,
-}
+export default silverJobRoutes;

+ 2 - 1
src/server/data-source.ts

@@ -28,6 +28,7 @@ import { SilverKnowledgeInteraction } from "./modules/silver-users/silver-knowle
 import { ElderlyUniversity } from "./modules/silver-users/elderly-university.entity"
 import { PolicyNews } from "./modules/silver-users/policy-news.entity"
 import { UserPreference } from "./modules/silver-users/user-preference.entity"
+import { SilverJob } from "./modules/silver-jobs/silver-job.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -42,7 +43,7 @@ export const AppDataSource = new DataSource({
     TimeBankIntro, TimeBankCase, TimeBankStats,
     SilverKnowledge, SilverKnowledgeCategory, SilverKnowledgeTag,
     SilverKnowledgeTagRelation, SilverKnowledgeStats, SilverKnowledgeInteraction,
-    ElderlyUniversity, PolicyNews, UserPreference,
+    ElderlyUniversity, PolicyNews, UserPreference, SilverJob,
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 152 - 0
src/server/modules/silver-jobs/silver-job.entity.ts

@@ -0,0 +1,152 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+export enum JobStatus {
+  DRAFT = 0,        // 草稿
+  PUBLISHED = 1,    // 已发布
+  CLOSED = 2,       // 已关闭
+  FILLED = 3        // 已招满
+}
+
+@Entity('silver_jobs')
+export class SilverJob {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'title', type: 'varchar', length: 255, comment: '岗位标题' })
+  title!: string;
+
+  @Column({ name: 'description', type: 'text', comment: '岗位描述' })
+  description!: string;
+
+  @Column({ name: 'requirements', type: 'text', nullable: true, comment: '岗位要求' })
+  requirements!: string | null;
+
+  @Column({ name: 'location', type: 'varchar', length: 255, comment: '工作地点' })
+  location!: string;
+
+  @Column({ name: 'salary_range', type: 'varchar', length: 100, nullable: true, comment: '薪资范围' })
+  salaryRange!: string | null;
+
+  @Column({ name: 'work_hours', type: 'varchar', length: 100, comment: '工作时间' })
+  workHours!: string;
+
+  @Column({ name: 'contact_person', type: 'varchar', length: 50, comment: '联系人' })
+  contactPerson!: string;
+
+  @Column({ name: 'contact_phone', type: 'varchar', length: 20, comment: '联系电话' })
+  contactPhone!: string;
+
+  @Column({ name: 'contact_email', type: 'varchar', length: 255, nullable: true, comment: '联系邮箱' })
+  contactEmail!: string | null;
+
+  @Column({ name: 'employer_name', type: 'varchar', length: 255, comment: '雇主名称' })
+  employerName!: string;
+
+  @Column({ name: 'employer_address', type: 'varchar', length: 500, nullable: true, comment: '雇主地址' })
+  employerAddress!: string | null;
+
+  @Column({ name: 'job_type', type: 'varchar', length: 50, comment: '岗位类型' })
+  jobType!: string;
+
+  @Column({ name: 'required_skills', type: 'text', nullable: true, comment: '所需技能' })
+  requiredSkills!: string | null;
+
+  @Column({ name: 'benefits', type: 'text', nullable: true, comment: '福利待遇' })
+  benefits!: string | null;
+
+  @Column({ name: 'application_deadline', type: 'date', nullable: true, comment: '申请截止日期' })
+  applicationDeadline!: Date | null;
+
+  @Column({ name: 'start_date', type: 'date', nullable: true, comment: '开始日期' })
+  startDate!: Date | null;
+
+  @Column({ name: 'status', type: 'tinyint', default: JobStatus.DRAFT, comment: '岗位状态:0-草稿,1-已发布,2-已关闭,3-已招满' })
+  status!: JobStatus;
+
+  @Column({ name: 'view_count', type: 'int', unsigned: true, default: 0, comment: '浏览次数' })
+  viewCount!: number;
+
+  @Column({ name: 'application_count', type: 'int', unsigned: true, default: 0, comment: '申请人数' })
+  applicationCount!: number;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', nullable: true, comment: '创建人ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', nullable: true, comment: '更新人ID' })
+  updatedBy!: number | null;
+}
+
+// Zod Schemas for API
+export const SilverJobSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '银龄岗ID', example: 1 }),
+  title: z.string().max(255).openapi({ description: '岗位标题', example: '社区老年大学书法教师' }),
+  description: z.string().openapi({ description: '岗位描述', example: '负责社区老年大学书法课程教学,每周2次课,每次2小时' }),
+  requirements: z.string().nullable().optional().openapi({ description: '岗位要求', example: '有书法教学经验,耐心细致,善于与老年人沟通' }),
+  location: z.string().max(255).openapi({ description: '工作地点', example: '北京市朝阳区XX街道社区服务中心' }),
+  salaryRange: z.string().max(100).nullable().optional().openapi({ description: '薪资范围', example: '2000-3000元/月' }),
+  workHours: z.string().max(100).openapi({ description: '工作时间', example: '每周二、四下午2-4点' }),
+  contactPerson: z.string().max(50).openapi({ description: '联系人', example: '张主任' }),
+  contactPhone: z.string().max(20).openapi({ description: '联系电话', example: '13800138000' }),
+  contactEmail: z.string().max(255).email().nullable().optional().openapi({ description: '联系邮箱', example: 'hr@community.com' }),
+  employerName: z.string().max(255).openapi({ description: '雇主名称', example: '朝阳区XX街道社区服务中心' }),
+  employerAddress: z.string().max(500).nullable().optional().openapi({ description: '雇主地址', example: '北京市朝阳区XX街道XX号' }),
+  jobType: z.string().max(50).openapi({ description: '岗位类型', example: '兼职教师' }),
+  requiredSkills: z.string().nullable().optional().openapi({ description: '所需技能', example: '书法、国画、教学经验' }),
+  benefits: z.string().nullable().optional().openapi({ description: '福利待遇', example: '交通补贴、工作餐、节日福利' }),
+  applicationDeadline: z.coerce.date().nullable().optional().openapi({ description: '申请截止日期', example: '2024-12-31' }),
+  startDate: z.coerce.date().nullable().optional().openapi({ description: '开始日期', example: '2025-01-15' }),
+  status: z.number().int().min(0).max(3).openapi({ description: '岗位状态:0-草稿,1-已发布,2-已关闭,3-已招满', example: 1 }),
+  viewCount: z.number().int().min(0).openapi({ description: '浏览次数', example: 150 }),
+  applicationCount: z.number().int().min(0).openapi({ description: '申请人数', example: 5 }),
+  createdAt: z.date().openapi({ description: '创建时间', example: '2024-01-01T00:00:00Z' }),
+  updatedAt: z.date().openapi({ description: '更新时间', example: '2024-01-01T00:00:00Z' }),
+  createdBy: z.number().int().positive().nullable().optional().openapi({ description: '创建人ID', example: 1 }),
+  updatedBy: z.number().int().positive().nullable().optional().openapi({ description: '更新人ID', example: 1 })
+});
+
+export const CreateSilverJobDto = z.object({
+  title: z.string().max(255).openapi({ description: '岗位标题', example: '社区老年大学书法教师' }),
+  description: z.string().openapi({ description: '岗位描述', example: '负责社区老年大学书法课程教学,每周2次课,每次2小时' }),
+  requirements: z.string().optional().openapi({ description: '岗位要求', example: '有书法教学经验,耐心细致,善于与老年人沟通' }),
+  location: z.string().max(255).openapi({ description: '工作地点', example: '北京市朝阳区XX街道社区服务中心' }),
+  salaryRange: z.string().max(100).optional().openapi({ description: '薪资范围', example: '2000-3000元/月' }),
+  workHours: z.string().max(100).openapi({ description: '工作时间', example: '每周二、四下午2-4点' }),
+  contactPerson: z.string().max(50).openapi({ description: '联系人', example: '张主任' }),
+  contactPhone: z.string().max(20).openapi({ description: '联系电话', example: '13800138000' }),
+  contactEmail: z.string().max(255).email().optional().openapi({ description: '联系邮箱', example: 'hr@community.com' }),
+  employerName: z.string().max(255).openapi({ description: '雇主名称', example: '朝阳区XX街道社区服务中心' }),
+  employerAddress: z.string().max(500).optional().openapi({ description: '雇主地址', example: '北京市朝阳区XX街道XX号' }),
+  jobType: z.string().max(50).openapi({ description: '岗位类型', example: '兼职教师' }),
+  requiredSkills: z.string().optional().openapi({ description: '所需技能', example: '书法、国画、教学经验' }),
+  benefits: z.string().optional().openapi({ description: '福利待遇', example: '交通补贴、工作餐、节日福利' }),
+  applicationDeadline: z.coerce.date().optional().openapi({ description: '申请截止日期', example: '2024-12-31' }),
+  startDate: z.coerce.date().optional().openapi({ description: '开始日期', example: '2025-01-15' }),
+  status: z.coerce.number().int().min(0).max(3).default(JobStatus.DRAFT).openapi({ description: '岗位状态:0-草稿,1-已发布,2-已关闭,3-已招满', example: 1 })
+});
+
+export const UpdateSilverJobDto = z.object({
+  title: z.string().max(255).optional().openapi({ description: '岗位标题', example: '社区老年大学书法教师' }),
+  description: z.string().optional().openapi({ description: '岗位描述', example: '负责社区老年大学书法课程教学,每周2次课,每次2小时' }),
+  requirements: z.string().optional().openapi({ description: '岗位要求', example: '有书法教学经验,耐心细致,善于与老年人沟通' }),
+  location: z.string().max(255).optional().openapi({ description: '工作地点', example: '北京市朝阳区XX街道社区服务中心' }),
+  salaryRange: z.string().max(100).optional().openapi({ description: '薪资范围', example: '2000-3000元/月' }),
+  workHours: z.string().max(100).optional().openapi({ description: '工作时间', example: '每周二、四下午2-4点' }),
+  contactPerson: z.string().max(50).optional().openapi({ description: '联系人', example: '张主任' }),
+  contactPhone: z.string().max(20).optional().openapi({ description: '联系电话', example: '13800138000' }),
+  contactEmail: z.string().max(255).email().optional().openapi({ description: '联系邮箱', example: 'hr@community.com' }),
+  employerName: z.string().max(255).optional().openapi({ description: '雇主名称', example: '朝阳区XX街道社区服务中心' }),
+  employerAddress: z.string().max(500).optional().openapi({ description: '雇主地址', example: '北京市朝阳区XX街道XX号' }),
+  jobType: z.string().max(50).optional().openapi({ description: '岗位类型', example: '兼职教师' }),
+  requiredSkills: z.string().optional().openapi({ description: '所需技能', example: '书法、国画、教学经验' }),
+  benefits: z.string().optional().openapi({ description: '福利待遇', example: '交通补贴、工作餐、节日福利' }),
+  applicationDeadline: z.coerce.date().optional().openapi({ description: '申请截止日期', example: '2024-12-31' }),
+  startDate: z.coerce.date().optional().openapi({ description: '开始日期', example: '2025-01-15' }),
+  status: z.coerce.number().int().min(0).max(3).optional().openapi({ description: '岗位状态:0-草稿,1-已发布,2-已关闭,3-已招满', example: 1 })
+});