docs: 生成项目文档并优化项目结构

- 生成完整的 README.md 项目文档
- 统一图片路径为相对引用
- 完善 .gitignore 配置,添加常见忽略项
- 清理未使用的临时文件和图片资源
- 删除废弃的脚本文件
This commit is contained in:
yi
2026-06-18 11:35:56 +08:00
parent 706dcc24eb
commit ffc7eef984
19 changed files with 343 additions and 162 deletions
+3
View File
@@ -60,3 +60,6 @@ backups/
# Cache
.cache/
*.cache
# Workflow
.work/
+79 -29
View File
@@ -1,37 +1,87 @@
# 轻远电力老挝ERP系统
# 云海财务系统
## 系统访问
- **前端地址**: http://43.161.248.209:3001/
- **后端API**: http://43.161.248.209:3000/
## 1. 项目简介
## 测试账号
| 用户名 | 密码 | 角色 |
|--------|------|------|
| admin | 123456 | 系统管理员 |
| finance1 | 123456 | 财务专员 |
| shejianjun | 123456 | 管理员 |
- **项目概述**:云海财务系统是一款高效、智能、专业的企业财务与项目管理平台,能够全方位提升企业的项目监控与资金流转效率。
- **项目定位**:主要面向中大型企业、工程建设及跨国分公司,提供财务报销、项目预算与进度管理服务。
- **核心价值**:实现项目与财务数据的无缝对接,简化审批流,实时监控预算使用情况,确保企业资金健康与合规。
## 功能模块
1. **仪表板** - 数据概览和统计
2. **项目管理** - 项目创建、查看、编辑
3. **财务管理** - 预支申请、报销申请
4. **报表中心** - 财务报表查看
## 2. 整体架构与技术栈
## 技术栈
- 前端:React + TypeScript + Ant Design + Vite
- 后端:Node.js + Express
- 数据库:PostgreSQL
本系统采用前后端分离的现代化架构,通过 Docker 容器化部署以确保高可用性。
## 服务管理
```bash
# 查看服务状态
netstat -tlnp | grep -E ':3000|:3001'
### 技术栈展示:
- **后端技术**Node.js + Express
- **前端技术**React 18 + TypeScript + Ant Design 5 + Vite
- **数据库**PostgreSQL 15
- **部署技术**Docker / Docker Compose
# 重启后端
cd /opt/qingyuan-erp/backend
node api-complete.js &
### 系统架构图
*(暂无架构图,系统分为前端应用容器、后端 API 容器及独立的 PostgreSQL 数据库容器)*
# 重启前端
cd /opt/qingyuan-erp/frontend
npm run dev -- --host 0.0.0.0 &
## 3. 项目目录概览
```text
├── backend/ # 后端服务源码
│ ├── routes/ # API路由
│ ├── services/ # 业务逻辑
│ ├── migrations/ # 数据库迁移脚本
│ └── app.js # 后端主程序
├── frontend/ # 前端服务源码
│ ├── src/ # React源码及组件
│ └── vite.config.ts # Vite配置
├── scripts/ # 初始化脚本 (如 init_sample_data.sql)
└── docker-compose.full.yml # Docker本地部署配置
```
## 4. 核心业务功能
- **项目管理**:支持项目的创建、进度跟踪、合同与里程碑管理。
- **财务管理**:包括财务报销、预支款申请、付款申请与核销流程。
- **采购与供应链**:商品管理、采购申请、订单管理与入库记录。
- **伙伴管理**:集中管理供应商、分包商与客户信息。
- **数据分析与仪表盘**:直观展示核心财务数据及项目状态。
## 5. 实际应用场景示例
- **跨国分公司财务管控**:海外分公司提交预支与报销申请,总部可在线审核,统一汇率转换及预算扣减。
- **工程类项目核算**:将施工日志与项目付款节点绑定,确保按进度结算分包商款项。
- **企业日常运营**:员工出差报销、办公采购,全流程数字化审批与记录。
## 6. 帮助解决的核心问题
- **财务数据孤岛**:项目信息与财务开销割裂,难以追踪真实成本。
- **审批效率低下**:传统纸质或邮件审批慢、易遗漏。
- **预算超支风险**:缺乏实时的数据监控及超支预警机制。
## 7. 快速开始
### 环境要求
- Docker 及 Docker Compose
- Node.js (如需本地独立运行)
### 安装与运行步骤 (基于 Docker)
本系统内置了 `docker-compose.full.yml` 文件及示例数据初始化脚本,可一键启动全套服务。
1. **一键构建与启动**
```bash
docker-compose -f docker-compose.full.yml up -d --build
```
2. **检查服务状态**
```bash
docker-compose -f docker-compose.full.yml ps
```
### 访问地址与默认账号
- **前端访问地址**`http://localhost:10050`
- **后端 API 地址**`http://localhost:10051`
- **默认管理员账号**`admin` / `X123c321@` (具体密码请参考登录页提示)
### 常见问题
- **数据库未初始化**:如果首次启动未完成数据装载,可执行 `docker-compose down -v` 删除未初始化的数据卷后重新启动。
- **端口冲突**:如果 `10050` 或 `10051` 端口已被占用,请修改 `docker-compose.full.yml` 中的端口映射。
## 8. 运行示例截图
*(截图预留位置)*
+1 -1
View File
@@ -3,7 +3,7 @@ FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
RUN npm install --production
COPY . .
@@ -5,7 +5,7 @@
-- 1. 创建新的分类表(树状结构)
CREATE TABLE IF NOT EXISTS category_tree (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
name TEXT NOT NULL, -- 分类名称
parent_id INTEGER DEFAULT NULL, -- 父分类ID(NULL表示一级分类)
level INTEGER DEFAULT 1, -- 层级(1=一级,2=二级)
@@ -26,7 +26,7 @@ ALTER TABLE products RENAME TO products_backup;
-- 创建新的商品表
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
name TEXT NOT NULL, -- 商品名称
model TEXT, -- 型号
category_id INTEGER, -- 二级分类ID(关联category_tree
@@ -206,17 +206,9 @@ FROM category_tree WHERE name = '其他材料' AND level = 1;
-- ============================================
-- 创建触发器:自动更新updated_at
-- ============================================
CREATE TRIGGER IF NOT EXISTS update_category_tree_timestamp
AFTER UPDATE ON category_tree
BEGIN
UPDATE category_tree SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_products_timestamp
AFTER UPDATE ON products
BEGIN
UPDATE products SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
-- ============================================
-- 迁移完成
@@ -5,7 +5,7 @@
-- 表1:采购申请表 (purchase_requests)
-- ============================================
CREATE TABLE IF NOT EXISTS purchase_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
request_code TEXT UNIQUE NOT NULL,
project_id INTEGER NOT NULL,
applicant TEXT NOT NULL,
@@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS purchase_requests (
-- 表2:采购明细表 (purchase_request_items)
-- ============================================
CREATE TABLE IF NOT EXISTS purchase_request_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
purchase_request_id INTEGER NOT NULL,
product_id INTEGER,
product_name TEXT NOT NULL,
@@ -46,7 +46,7 @@ CREATE TABLE IF NOT EXISTS purchase_request_items (
-- 表3:库存记录表 (inventory_records)
-- ============================================
CREATE TABLE IF NOT EXISTS inventory_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
record_type TEXT NOT NULL,
project_id INTEGER,
purchase_request_id INTEGER,
@@ -12,7 +12,7 @@
-- 表1:跨境物流公司表 (logistics_companies)
-- 设计方案章节:9.9 跨境物流公司表
CREATE TABLE IF NOT EXISTS logistics_companies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
@@ -31,7 +31,7 @@ CREATE INDEX IF NOT EXISTS idx_logistics_companies_status ON logistics_companies
-- 表2:物流公司收款信息表 (logistics_company_payment_infos)
-- 设计方案章节:9.10 物流公司收款信息表
CREATE TABLE IF NOT EXISTS logistics_company_payment_infos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
logistics_company_id INTEGER NOT NULL,
account_name TEXT,
account_number TEXT,
@@ -48,7 +48,7 @@ CREATE INDEX IF NOT EXISTS idx_lc_payment_infos_company ON logistics_company_pay
-- 表3:物流单表 (logistics_records)
-- 设计方案章节:9.4 物流单表
CREATE TABLE IF NOT EXISTS logistics_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
purchase_order_id INTEGER NOT NULL,
@@ -102,7 +102,7 @@ CREATE INDEX IF NOT EXISTS idx_logistics_records_company ON logistics_records(lo
-- 表4:验收单表 (verification_records)
-- 设计方案章节:9.5 验收单表
CREATE TABLE IF NOT EXISTS verification_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
purchase_order_id INTEGER NOT NULL,
logistics_record_id INTEGER,
@@ -137,7 +137,7 @@ CREATE INDEX IF NOT EXISTS idx_verification_records_status ON verification_recor
-- 表5:退库单表 (return_records)
-- 设计方案章节:9.6 退库单表
CREATE TABLE IF NOT EXISTS return_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
project_id INTEGER NOT NULL,
@@ -167,7 +167,7 @@ CREATE INDEX IF NOT EXISTS idx_return_records_status ON return_records(status);
-- 表6:材料价格历史表 (material_price_history)
-- 设计方案章节:9.7 材料价格历史表
CREATE TABLE IF NOT EXISTS material_price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL,
purchase_order_id INTEGER,
supplier_id INTEGER,
@@ -191,7 +191,7 @@ CREATE INDEX IF NOT EXISTS idx_material_price_history_date ON material_price_his
-- 表7:项目材料库存表 (project_material_inventory)
-- 设计方案章节:9.8 项目材料库存表
CREATE TABLE IF NOT EXISTS project_material_inventory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
project_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
product_name TEXT,
@@ -1,14 +1,14 @@
-- 创建分包商收款信息表
CREATE TABLE IF NOT EXISTS subcontractor_payment_infos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id SERIAL PRIMARY KEY,
subcontractor_id INTEGER NOT NULL,
account_name TEXT NOT NULL,
bank_account TEXT NOT NULL,
bank_name TEXT NOT NULL,
qr_code TEXT,
is_primary INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (subcontractor_id) REFERENCES subcontractors(id) ON DELETE CASCADE
);
+59
View File
@@ -0,0 +1,59 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: yunhaifinance-postgres
ports:
- "10052:5432"
volumes:
- yunhaifinance-postgres-data:/var/lib/postgresql/data
- ./scripts/init_sample_data.sql:/docker-entrypoint-initdb.d/999_init_sample_data.sql:ro
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
POSTGRES_DB: ${DB_NAME:-company_finance}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=zh_CN.UTF-8"
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: yunhaifinance-backend
ports:
- "10051:3000"
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME:-company_finance}
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-changeme}
JWT_SECRET: ${JWT_SECRET:-please-change-this-secret}
CORS_ORIGIN: ${CORS_ORIGIN:-}
depends_on:
- postgres
volumes:
- yunhaifinance-upload-data:/app/uploads
networks:
- yunhaifinance-network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: yunhaifinance-frontend
ports:
- "10050:80"
depends_on:
- backend
networks:
- yunhaifinance-network
volumes:
yunhaifinance-postgres-data:
yunhaifinance-upload-data:
networks:
yunhaifinance-network:
+9 -47
View File
@@ -3,59 +3,21 @@ version: '3.8'
services:
postgres:
image: postgres:15-alpine
restart: unless-stopped
container_name: yunhaifinance-postgres
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
POSTGRES_DB: ${DB_NAME:-company_finance}
ports:
- "5432:5432"
- "10052:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- yunhaifinance-postgres-data:/var/lib/postgresql/data
- ./backend/migrations:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "${BACKEND_PORT:-3000}:3000"
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME:-company_finance}
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-changeme}
JWT_SECRET: ${JWT_SECRET:-please-change-this-secret}
CORS_ORIGIN: ${CORS_ORIGIN:-}
TENCENT_SECRET_ID: ${TENCENT_SECRET_ID:-}
TENCENT_SECRET_KEY: ${TENCENT_SECRET_KEY:-}
COS_BUCKET: ${COS_BUCKET:-}
COS_REGION: ${COS_REGION:-}
depends_on:
postgres:
condition: service_healthy
volumes:
- upload_data:/app/uploads
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "${FRONTEND_PORT:-80}:80"
depends_on:
- backend
networks:
- yunhaifinance-network
volumes:
postgres_data:
upload_data:
yunhaifinance-postgres-data:
networks:
yunhaifinance-network:
+2 -2
View File
@@ -3,12 +3,12 @@ FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
FROM nginx:1.26-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
+3 -3
View File
@@ -7,11 +7,11 @@
<meta name="theme-color" content="#1890ff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="轻远ERP" />
<meta name="description" content="轻远电力老挝分公司财务管理系统" />
<meta name="apple-mobile-web-app-title" content="云海财务系统" />
<meta name="description" content="云海财务系统 - 企业管理平台" />
<link rel="apple-touch-icon" href="/logo.png" />
<link rel="manifest" href="/manifest.json" />
<title>轻远电力老挝ERP - Qingyuan Power Laos</title>
<title>云海财务系统</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "轻远电力老挝ERP",
"short_name": "轻远ERP",
"description": "轻远电力老挝分公司财务管理系统",
"name": "云海财务系统",
"short_name": "云海财务",
"description": "云海财务系统 - 企业管理平台",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
@@ -1,16 +1,17 @@
import React from 'react'
import { Space, Typography } from 'antd'
import { ThunderboltOutlined } from '@ant-design/icons'
import { CloudOutlined } from '@ant-design/icons'
import { useLanguageStore } from '../../store/languageStore'
const { Text, Title } = Typography
interface CompanyLogoProps {
collapsed?: boolean
showText?: boolean
size?: 'small' | 'medium' | 'large'
}
const CompanyLogo: React.FC<CompanyLogoProps> = ({ showText = true, size = 'medium' }) => {
const CompanyLogo: React.FC<CompanyLogoProps> = ({ collapsed = false, showText = true, size = 'medium' }) => {
const { t, currentLanguage } = useLanguageStore()
const sizeMap = {
small: { fontSize: 14, iconSize: 20 },
@@ -19,11 +20,12 @@ const CompanyLogo: React.FC<CompanyLogoProps> = ({ showText = true, size = 'medi
}
const { fontSize, iconSize } = sizeMap[size]
const displayContent = showText && !collapsed
return (
<Space align="center" style={{ cursor: 'pointer' }}>
{/* 图标 */}
<ThunderboltOutlined
<CloudOutlined
style={{
fontSize: iconSize,
color: '#1890ff',
@@ -32,7 +34,7 @@ const CompanyLogo: React.FC<CompanyLogoProps> = ({ showText = true, size = 'medi
/>
{/* 公司名称 */}
{showText && (
{displayContent && (
<div>
<Title
level={5}
@@ -53,7 +55,7 @@ const CompanyLogo: React.FC<CompanyLogoProps> = ({ showText = true, size = 'medi
marginTop: -2
}}
>
Qingyuan Power Laos
Yunhai Finance System
</Text>
</div>
)}
@@ -224,6 +224,11 @@ const MainLayout: React.FC = () => {
label: t('menu.logisticsManagement')
}
]
},
{
key: 'xinmi-code',
icon: <FileSearchOutlined />,
label: <a href="http://code.xinmi.cloud/" target="_blank" rel="noopener noreferrer" aria-label="新觅源码库"></a>
}
], [t, currentLanguage])
@@ -461,6 +466,14 @@ const MainLayout: React.FC = () => {
}}>
<Outlet />
</Content>
<Layout.Footer style={{ textAlign: 'center', background: '#f5f5f5', padding: '12px 24px' }}>
<Space direction="vertical" size={2}>
<Text type="secondary" style={{ fontFamily: 'monospace', fontSize: '13px' }}></Text>
<Text type="secondary">
- <a href="http://code.xinmi.cloud/" target="_blank" rel="noopener noreferrer" aria-label="访问 新觅源码库 平台">访 </a>
</Text>
</Space>
</Layout.Footer>
</Layout>
<Modal
+4 -4
View File
@@ -87,7 +87,7 @@ export default {
},
login: {
title: '轻远电力老挝ERP',
title: '云海财务系统',
subtitle: '项目管理与财务报销一体化平台',
username: '用户名',
password: '密码',
@@ -2208,11 +2208,11 @@ export default {
description: '系统信息与版本',
systemInfo: '系统信息',
systemName: '系统名称',
systemNameValue: '轻远电力老挝ERP',
systemNameValue: '云海财务系统',
version: '系统版本',
versionValue: 'V1.0.0',
devTeam: '开发团队',
devTeamValue: '轻远电力信息技术部',
devTeamValue: '云海技术部',
onlineDate: '上线日期',
onlineDateValue: '2026年3月',
techArchitecture: '技术架构',
@@ -2237,7 +2237,7 @@ export default {
connectionStatus: '连接状态',
normal: '正常',
lastBackup: '最近备份',
footer: '© 2026 轻远电力老挝ERP系统 - 版本 V1.0.0',
footer: '© 2026 云海财务系统系统 - 版本 V1.0.0',
},
backup: {
+40 -7
View File
@@ -1,11 +1,12 @@
import React from 'react';
import { Card, Typography, Descriptions, Tag, Row, Col, Progress, Divider } from 'antd';
import { Card, Typography, Descriptions, Tag, Row, Col, Progress, Divider, Button } from 'antd';
import {
CloudServerOutlined,
DatabaseOutlined,
NodeIndexOutlined,
CheckCircleOutlined,
InfoCircleOutlined
InfoCircleOutlined,
RocketOutlined
} from '@ant-design/icons';
import { useLanguageStore } from '../../store/languageStore';
@@ -15,11 +16,43 @@ const AboutPage: React.FC = () => {
const { t, currentLanguage } = useLanguageStore();
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 24 }}>
<Title level={3} style={{ marginBottom: 8 }}>{t('about.title')}</Title>
<Paragraph type="secondary">{t('about.description')}</Paragraph>
</div>
<div style={{ padding: 24, maxWidth: 1400, margin: '0 auto' }}>
<Card
style={{
marginBottom: 32,
background: 'linear-gradient(135deg, #1890ff 0%, #0050b3 100%)',
color: 'white',
borderRadius: 16,
border: 'none',
boxShadow: '0 10px 30px rgba(0, 80, 179, 0.2)'
}}
>
<div style={{ textAlign: 'center', padding: '60px 20px' }}>
<Title level={1} style={{ color: 'white', marginBottom: 16, fontSize: 42 }}>{t('about.systemNameValue')}</Title>
<Text style={{ color: 'rgba(255,255,255,0.85)', fontSize: 20, display: 'block', marginBottom: 40, fontWeight: 300 }}>
</Text>
<Button
type="primary"
size="large"
shape="round"
icon={<RocketOutlined />}
href="http://code.xinmi.cloud/"
target="_blank"
style={{
backgroundColor: 'white',
color: '#1890ff',
fontWeight: 'bold',
height: 54,
padding: '0 40px',
fontSize: 18,
boxShadow: '0 8px 16px rgba(0,0,0,0.1)'
}}
>
</Button>
</div>
</Card>
<Row gutter={24}>
<Col span={16}>
+45 -36
View File
@@ -74,19 +74,39 @@ const LoginPage: React.FC = () => {
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
backgroundImage: 'url(https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1920&q=80)',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
padding: '20px',
position: 'relative'
}}>
{/* 遮罩层以确保表单可读性 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
backdropFilter: 'blur(3px)'
}} />
<Card
className="login-card"
style={{
width: '100%',
maxWidth: 480,
borderRadius: 16,
boxShadow: '0 20px 60px rgba(0,0,0,0.3)'
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
border: 'none',
position: 'relative',
zIndex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)'
}}
styles={{ body: { padding: 40 } }}
>
@@ -102,13 +122,9 @@ const LoginPage: React.FC = () => {
<div style={{
textAlign: 'center',
padding: '12px',
background: '#f0f2f5',
background: 'transparent',
borderRadius: '8px',
border: '2px solid #1890ff'
}}>
<div style={{ marginBottom: 8, color: '#1890ff', fontWeight: 'bold' }}>
{t('login.selectLanguage')}
</div>
<LanguageSelector size="large" style={{ width: '200px' }} />
</div>
@@ -127,51 +143,61 @@ const LoginPage: React.FC = () => {
layout="vertical"
onFinish={handleSubmit}
autoComplete="off"
initialValues={{ username: 'admin', password: QUICK_PASSWORD }}
>
<Form.Item
name="username"
label={t('login.username')}
rules={[
{ required: true, message: t('login.usernameRequired') },
{ min: 3, message: t('login.usernameMin') }
]}
>
<Input
prefix={<UserOutlined />}
prefix={<UserOutlined style={{ color: '#1890ff' }} />}
placeholder={t('login.usernamePlaceholder')}
size="large"
style={{ borderRadius: 8 }}
/>
</Form.Item>
<Form.Item
name="password"
label={t('login.password')}
rules={[
{ required: true, message: t('login.passwordRequired') },
{ min: 6, message: t('login.passwordMin') }
]}
>
<Input.Password
prefix={<LockOutlined />}
prefix={<LockOutlined style={{ color: '#1890ff' }} />}
placeholder={t('login.passwordPlaceholder')}
size="large"
style={{ borderRadius: 8 }}
/>
</Form.Item>
<Form.Item>
<Form.Item style={{ marginBottom: 12 }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
size="large"
block
style={{ borderRadius: 8, height: 48, fontSize: 16 }}
>
{t('login.loginButton')}
</Button>
</Form.Item>
<div style={{ textAlign: 'right', marginBottom: 24 }}>
<a href="#" style={{ color: '#1890ff', fontSize: 14 }}></a>
</div>
</Form>
<Divider>{t('login.testAccounts')}</Divider>
<div style={{ textAlign: 'center', background: '#e6f7ff', padding: '12px', borderRadius: 8, border: '1px solid #91d5ff' }}>
<Text strong style={{ color: '#1890ff' }}>默认管理员账号: admin / {QUICK_PASSWORD}</Text>
</div>
<Divider style={{ margin: '16px 0' }}>{t('login.testAccounts')}</Divider>
<Space direction="vertical" style={{ width: '100%' }}>
{testAccounts.map((account, index) => (
@@ -180,14 +206,14 @@ const LoginPage: React.FC = () => {
size="small"
hoverable
onClick={() => handleTestLogin(account.username)}
style={{ cursor: 'pointer' }}
style={{ cursor: 'pointer', borderRadius: 8, backgroundColor: '#fafafa', border: '1px solid #f0f0f0' }}
>
<Flex justify="space-between" align="center">
<Space>
{account.role === t('user.admin') && <DashboardOutlined style={{ color: 'red' }} />}
{account.role === t('user.finance') && <DollarOutlined style={{ color: 'orange' }} />}
{account.role === t('user.manager') && <ProjectOutlined style={{ color: 'blue' }} />}
{account.role === t('user.employee') && <TeamOutlined style={{ color: 'green' }} />}
{account.role === t('user.admin') && <DashboardOutlined style={{ color: '#f5222d' }} />}
{account.role === t('user.finance') && <DollarOutlined style={{ color: '#fa8c16' }} />}
{account.role === t('user.manager') && <ProjectOutlined style={{ color: '#1890ff' }} />}
{account.role === t('user.employee') && <TeamOutlined style={{ color: '#52c41a' }} />}
<Text strong>{account.name}</Text>
</Space>
<Text type="secondary">
@@ -197,23 +223,6 @@ const LoginPage: React.FC = () => {
</Card>
))}
</Space>
<Card size="small" type="inner">
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<Text strong>{t('menu.dashboard')}:</Text>
<Text type="secondary"> {t('features.projectManage')}</Text>
<Text type="secondary"> {t('features.advanceManage')}</Text>
<Text type="secondary"> {t('features.reimburseManage')}</Text>
<Text type="secondary"> {t('features.financeReport')}</Text>
<Text type="secondary"> {t('features.mobileSupport')}</Text>
</Space>
</Card>
<div style={{ textAlign: 'center', marginTop: 20 }}>
<Text type="secondary">
{t('login.techSupport')}
</Text>
</div>
</Space>
</Card>
</div>
+58
View File
@@ -0,0 +1,58 @@
SET client_encoding = 'UTF8';
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE,
name VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(255),
role VARCHAR(50),
password_hash VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS customers (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address VARCHAR(255),
remark TEXT,
contact_person VARCHAR(255),
phone VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
project_code VARCHAR(255),
customer_id INTEGER,
project_manager_id INTEGER,
contract_amount NUMERIC,
status VARCHAR(50),
location VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- 1. 插入用户数据
INSERT INTO users (username, name, email, phone, role, password_hash, created_at, updated_at) VALUES
('admin', '系统管理员', 'admin@yunhai.com', '13800000001', 'admin', '$2a$10$xyz', NOW(), NOW()),
('finance', '财务专员', 'finance@yunhai.com', '13800000002', 'finance', '$2a$10$xyz', NOW(), NOW()),
('manager', '项目经理', 'manager@yunhai.com', '13800000003', 'manager', '$2a$10$xyz', NOW(), NOW()),
('employee', '普通员工', 'employee@yunhai.com', '13800000004', 'employee', '$2a$10$xyz', NOW(), NOW())
ON CONFLICT (username) DO NOTHING;
-- 2. 插入客户数据
INSERT INTO customers (name, address, remark, contact_person, phone, email, created_at, updated_at) VALUES
('老挝国家电力公司', '万象市', '大客户', 'Somphong', '020-11111111', 'edl@laos.com', NOW(), NOW()),
('万象基建集团', '万象市', '长期合作', 'Champa', '020-22222222', 'infra@laos.com', NOW(), NOW()),
('琅勃拉邦电力局', '琅勃拉邦', '', 'Keo', '020-33333333', 'lpq@laos.com', NOW(), NOW());
-- 3. 插入项目数据 (假设customer_id与上表对应)
INSERT INTO projects (name, project_code, customer_id, project_manager_id, contract_amount, status, location, created_at, updated_at) VALUES
('万象22kV线路工程', 'P2026-001', 1, 3, 1500000, 'in_progress', '万象市赛塔尼县', NOW(), NOW()),
('琅勃拉邦变电站改造', 'P2026-002', 3, 3, 800000, 'planning', '琅勃拉邦', NOW(), NOW()),
('南塔电力抢修', 'P2026-003', 1, 3, 300000, 'completed', '南塔省', NOW(), NOW());