v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成

- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
hankin
2026-03-16 07:31:37 +00:00
commit 423baff73b
2578 changed files with 824643 additions and 0 deletions
+642
View File
@@ -0,0 +1,642 @@
# -*- coding: utf-8 -*-
"""
[项目配置]
文件名: app.py
版本: v1.3.7
更新日期: 2026-02-24
更新内容: 代码审查修复 - 连接泄漏/SQL注入/emoji兼容性/版本号
"""
import streamlit as st
import sqlite3
import pandas as pd
import hashlib
import plotly.express as px
from datetime import datetime, timedelta
import io
# ==========================================
# 1. 配置与常量定义
# ==========================================
DB_FILE = 'crm_data.db'
st.set_page_config(
page_title="天津硕博霖客户信息管理系统",
page_icon=None,
layout="wide",
initial_sidebar_state="expanded"
)
# 业务常量
INDUSTRIES = ["海洋船舶", "港口机械", "电力", "空气行业", "其他"]
EQUIPMENT_TYPES = ["空气压缩机", "柴油发动机", "发电机组", "车辆", "液压设备", "齿轮箱", "其他"]
STATUS_OPTIONS = ["意向客户", "报价谈判", "成交签约", "维护"]
# --- 报销相关常量 (已更新) ---
# 1. 通用基础费用类型
BASE_EXPENSE_TYPES = ["办公费", "招待费", "差旅费", "交通费", "物流费", "油费", "福利费", "其他"]
# 2. 原始票据类型 (直接使用基础类型)
EXPENSE_TYPES_ORIGINAL = BASE_EXPENSE_TYPES
# 3. 冲顶票据类型 (新增置顶项 + 基础类型)
EXPENSE_TYPES_OFFSET = ["客户费用", "税务费用", "工资提成"] + BASE_EXPENSE_TYPES
EXPENSE_STATUS = ["待审核", "已通过", "已驳回"]
# ==========================================
# 2. 数据库管理模块
# ==========================================
def get_db_connection():
conn = sqlite3.connect(DB_FILE, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
c = conn.cursor()
# Users 表
c.execute('''
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
permissions TEXT NOT NULL
)
''')
# Clients 表
c.execute('''
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
industry TEXT,
address TEXT,
equipment_type TEXT,
status TEXT,
contact_person TEXT,
phone TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Follow_ups 表
c.execute('''
CREATE TABLE IF NOT EXISTS follow_ups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER,
note TEXT,
operator TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES clients (id)
)
''')
# Expenses 表
c.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
applicant TEXT,
apply_date DATE,
description TEXT,
location TEXT,
amount REAL,
invoice_type_original TEXT,
invoice_type_offset TEXT,
remarks TEXT,
status TEXT DEFAULT '待审核',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 默认管理员
c.execute('SELECT count(*) FROM users')
if c.fetchone()[0] == 0:
default_pwd_hash = hashlib.sha256("Yu@crm123".encode()).hexdigest()
c.execute(
'INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)',
('admin', default_pwd_hash, 'admin', 'view,edit,delete')
)
print("系统初始化:默认管理员账户已创建")
conn.commit()
conn.close()
init_db()
# ==========================================
# 3. 工具函数
# ==========================================
def hash_password(password):
return hashlib.sha256(password.encode()).hexdigest()
def verify_user(username, password):
conn = get_db_connection()
c = conn.cursor()
c.execute('SELECT * FROM users WHERE username = ?', (username,))
user = c.fetchone()
conn.close()
if user and user['password_hash'] == hash_password(password):
return user
return None
def run_query(query, params=(), fetch=False):
conn = get_db_connection()
try:
c = conn.cursor()
c.execute(query, params)
if fetch:
data = c.fetchall()
return data
conn.commit()
return True
except Exception as e:
st.error(f"数据库操作错误: {e}")
return False
finally:
conn.close()
def to_excel(df):
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='Sheet1')
processed_data = output.getvalue()
return processed_data
# ==========================================
# 4. 页面功能模块
# ==========================================
def login_page():
st.markdown("<h1 style='text-align: center;'>天津硕博霖 CRM 系统登录</h1>", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
with st.form("login_form"):
username = st.text_input("用户名")
password = st.text_input("密码", type="password")
submitted = st.form_submit_button("登录", use_container_width=True)
if submitted:
user = verify_user(username, password)
if user:
st.session_state['logged_in'] = True
st.session_state['username'] = user['username']
st.session_state['role'] = user['role']
st.session_state['permissions'] = user['permissions']
st.rerun()
else:
st.error("用户名或密码错误")
def sidebar_menu():
st.sidebar.title(f"欢迎, {st.session_state['username']}")
st.sidebar.markdown("---")
menu = st.sidebar.radio("功能导航", ["客户录入", "客户列表与跟进", "经营看板", "报销管理"])
# 管理员菜单
if st.session_state['role'] == 'admin':
st.sidebar.markdown("---")
with st.sidebar.expander("管理员工具 (用户管理)"):
tab_add, tab_manage = st.tabs(["新增", "管理"])
with tab_add:
with st.form("add_user_form"):
new_u = st.text_input("用户名")
new_p = st.text_input("密码", type="password")
new_r = st.selectbox("角色", ["user", "admin"])
new_perm = st.text_input("权限", value="view,edit")
if st.form_submit_button("创建"):
if new_u and new_p:
run_query('INSERT INTO users (username, password_hash, role, permissions) VALUES (?, ?, ?, ?)',
(new_u, hash_password(new_p), new_r, new_perm))
st.success("创建成功")
with tab_manage:
conn = get_db_connection()
users_df = pd.read_sql("SELECT username FROM users", conn)
conn.close()
target_user = st.selectbox("选择用户", users_df['username'])
if target_user:
new_pwd = st.text_input("重置密码", type="password")
if st.button("保存密码"):
run_query("UPDATE users SET password_hash=? WHERE username=?", (hash_password(new_pwd), target_user))
st.success("密码已重置")
if st.button("删除用户", type="primary"):
if target_user != st.session_state['username']:
run_query("DELETE FROM users WHERE username=?", (target_user,))
st.rerun()
st.sidebar.markdown("---")
if st.sidebar.button("退出登录"):
st.session_state.clear()
st.rerun()
return menu
# --- 业务模块:客户录入 ---
def page_new_client():
st.header("新增客户录入")
with st.form("new_client"):
col1, col2 = st.columns(2)
with col1:
name = st.text_input("客户名称 (必填)")
industry = st.selectbox("所属行业", INDUSTRIES)
contact = st.text_input("联系人")
phone = st.text_input("联系电话")
with col2:
eq_type = st.selectbox("关键设备类型", EQUIPMENT_TYPES)
status = st.selectbox("目前状态", STATUS_OPTIONS)
address = st.text_input("地址")
desc = st.text_area("备注")
if st.form_submit_button("提交录入") and name:
run_query('INSERT INTO clients (name, description, industry, address, equipment_type, status, contact_person, phone) VALUES (?,?,?,?,?,?,?,?)',
(name, desc, industry, address, eq_type, status, contact, phone))
st.success(f"客户 {name} 已录入")
# --- 业务模块:客户列表与跟进 ---
def page_client_hub():
st.header("客户列表与跟进中心")
search = st.text_input("搜索客户", "")
conn = get_db_connection()
sql = "SELECT * FROM clients WHERE name LIKE ? ORDER BY created_at DESC" if search else "SELECT * FROM clients ORDER BY created_at DESC"
df = pd.read_sql(sql, conn, params=(f'%{search}%',) if search else ())
conn.close()
st.dataframe(df, use_container_width=True, height=200, hide_index=True, column_config={"id":"ID", "name":"名称", "status":"状态"})
if not df.empty:
opts = {row['id']: f"{row['name']}" for _, row in df.iterrows()}
sel_id = st.selectbox("选择客户查看详情", options=list(opts.keys()), format_func=lambda x: opts[x])
if sel_id:
tab1, tab2 = st.tabs(["基础信息", "跟进记录"])
with tab1:
conn = get_db_connection()
info = pd.read_sql("SELECT * FROM clients WHERE id=?", conn, params=(sel_id,)).iloc[0]
conn.close()
can_edit = "edit" in st.session_state['permissions'] or st.session_state['role'] == "admin"
with st.form("edit_c"):
c1, c2 = st.columns(2)
nm = c1.text_input("名称", info['name'], disabled=not can_edit)
stt = c2.selectbox("状态", STATUS_OPTIONS, index=STATUS_OPTIONS.index(info['status']) if info['status'] in STATUS_OPTIONS else 0, disabled=not can_edit)
dsc = st.text_area("备注", info['description'], disabled=not can_edit)
if can_edit:
s1, s2 = st.columns(2)
with s1:
if st.form_submit_button("保存修改"):
run_query("UPDATE clients SET name=?, status=?, description=? WHERE id=?", (nm, stt, dsc, sel_id))
st.success("已更新")
st.rerun()
with s2:
if st.session_state['role'] == 'admin':
if st.form_submit_button("删除客户", type="primary"):
run_query("DELETE FROM follow_ups WHERE client_id=?", (sel_id,))
run_query("DELETE FROM clients WHERE id=?", (sel_id,))
st.success("已删除")
st.rerun()
with tab2:
with st.form("add_f"):
note = st.text_area("新跟进")
if st.form_submit_button("添加") and note:
run_query("INSERT INTO follow_ups (client_id, note, operator) VALUES (?,?,?)", (sel_id, note, st.session_state['username']))
st.success("已添加")
st.rerun()
conn = get_db_connection()
hist = pd.read_sql("SELECT * FROM follow_ups WHERE client_id=? ORDER BY timestamp DESC", conn, params=(sel_id,))
conn.close()
for _, r in hist.iterrows():
st.info(f"{r['timestamp']} | {r['operator']}: {r['note']}")
if r['operator'] == st.session_state['username'] or st.session_state['role'] == 'admin':
if st.button("删除", key=f"del_f_{r['id']}"):
run_query("DELETE FROM follow_ups WHERE id=?", (r['id'],))
st.rerun()
# --- 业务模块:经营看板 ---
def page_dashboard():
st.header("经营看板")
conn = get_db_connection()
try:
df = pd.read_sql("SELECT status, count(*) as count FROM clients GROUP BY status", conn)
if not df.empty:
fig = px.pie(df, values='count', names='status', title='客户状态分布', hole=0.4)
st.plotly_chart(fig, use_container_width=True)
finally:
conn.close()
# ==========================================
# 5. 报销管理模块 (v1.3.7: 批量审批 + 信息完善版)
# ==========================================
def page_expenses():
st.header("销售报销管理")
is_admin = st.session_state['role'] == 'admin'
if 'expense_buffer' not in st.session_state:
st.session_state['expense_buffer'] = []
tabs = ["新建申请 (批量)", "我的记录"]
if is_admin:
tabs = ["新建申请 (批量)", "我的记录", "审批中心", "统计与全局维护"]
active_tab = st.tabs(tabs)
# --- Tab 1: 新建申请 ---
with active_tab[0]:
col_input, col_preview = st.columns([1, 1])
with col_input:
st.markdown("#### 1. 录入明细")
st.caption("填完点击“加入列表”,最后统一提交。")
with st.form("expense_entry_form", clear_on_submit=True):
r1, r2 = st.columns(2)
app_date = r1.date_input("发生日期", datetime.now())
amount = r2.number_input("金额 (元)", min_value=0.0, step=1.0)
desc = st.text_input("费用描述", placeholder="例如: 招待天津港客户")
r3_1, r3_2 = st.columns(2)
location = r3_1.text_input("消费地点")
invoice_orig = r3_1.selectbox("原始票据", EXPENSE_TYPES_ORIGINAL)
invoice_offset = st.selectbox("冲顶票据", EXPENSE_TYPES_OFFSET)
remarks = st.text_area("备注", height=60)
if st.form_submit_button("[+] 加入待提交列表"):
if amount > 0 and desc:
st.session_state['expense_buffer'].append({
"apply_date": app_date, "amount": amount, "description": desc,
"location": location, "invoice_type_original": invoice_orig,
"invoice_type_offset": invoice_offset, "remarks": remarks
})
st.success("已加入!")
st.rerun()
else:
st.warning("请补全金额和描述")
with col_preview:
st.markdown(f"#### 2. 待提交列表 ({len(st.session_state['expense_buffer'])})")
if st.session_state['expense_buffer']:
for i, item in enumerate(st.session_state['expense_buffer']):
with st.container():
c_info, c_del = st.columns([5, 1])
with c_info:
st.markdown(f"**¥{item['amount']}** | {item['description']}")
st.caption(f"{item['apply_date']} | 原:{item['invoice_type_original']} / 冲:{item['invoice_type_offset']}")
with c_del:
if st.button("删除", key=f"del_item_{i}", help="删除此条"):
st.session_state['expense_buffer'].pop(i)
st.rerun()
st.divider()
c1, c2 = st.columns([2,1])
with c1:
if st.button("批量提交所有", type="primary", use_container_width=True):
conn = get_db_connection()
c = conn.cursor()
try:
for item in st.session_state['expense_buffer']:
c.execute('''INSERT INTO expenses (applicant, apply_date, description, location, amount,
invoice_type_original, invoice_type_offset, remarks, status) VALUES (?,?,?,?,?,?,?,?,?)''',
(st.session_state['username'], item['apply_date'], item['description'], item['location'],
item['amount'], item['invoice_type_original'], item['invoice_type_offset'], item['remarks'], '待审核'))
conn.commit()
st.session_state['expense_buffer'] = []
st.success("提交成功!")
st.rerun()
finally: conn.close()
with c2:
if st.button("清空列表"):
st.session_state['expense_buffer'] = []
st.rerun()
else:
st.info("暂无待提交记录。")
# --- Tab 2: 我的记录 ---
with active_tab[1]:
conn = get_db_connection()
my_df = pd.read_sql("SELECT * FROM expenses WHERE applicant=? ORDER BY apply_date DESC", conn, params=(st.session_state['username'],))
conn.close()
st.dataframe(my_df, use_container_width=True, hide_index=True)
pending = my_df[my_df['status']=='待审核']
if not pending.empty:
st.markdown("---")
with st.expander("撤回申请 (退回修改)", expanded=True):
st.caption("撤回后,单据将退回【新建申请】列表,方便修改后重新提交。")
del_id = st.selectbox("选择要撤回的记录", pending['id'], format_func=lambda x: f"#{x} - ¥{pending[pending['id']==x]['amount'].values[0]} - {pending[pending['id']==x]['description'].values[0]}")
if st.button("撤回并修改"):
row_data = pending[pending['id'] == del_id].iloc[0]
st.session_state['expense_buffer'].append({
"apply_date": row_data['apply_date'], "amount": row_data['amount'],
"description": row_data['description'], "location": row_data['location'],
"invoice_type_original": row_data['invoice_type_original'],
"invoice_type_offset": row_data['invoice_type_offset'], "remarks": row_data['remarks']
})
run_query("DELETE FROM expenses WHERE id=?", (del_id,))
st.success(f"单据 #{del_id} 已撤回!")
st.rerun()
st.markdown("---")
st.markdown("#### 打印我的报销单")
if not my_df.empty:
my_print_ids = st.multiselect("选择要打印的明细", my_df['id'], format_func=lambda x: f"#{x} | {my_df[my_df['id']==x]['apply_date'].values[0]} | ¥{my_df[my_df['id']==x]['amount'].values[0]}", key="print_multiselect_user")
if my_print_ids:
print_df = my_df[my_df['id'].isin(my_print_ids)]
total_print_amt = print_df['amount'].sum()
html_content = """
<style>
@media print {
.stApp { visibility: hidden; height: 0; overflow: hidden; }
.invoice-box, .invoice-box * { visibility: visible; }
.invoice-box { position: absolute; left: 0; top: 0; width: 100%; margin: 0; padding: 30px; border: none; }
@page { margin: 0; }
body { margin: 1cm; }
}
body { display: flex; justify-content: center; background-color: white; }
.invoice-box { width: 750px; padding: 30px; border: 2px solid #000; font-family: 'SimHei', Arial, sans-serif; color: #000; background-color: white; margin-top: 20px; }
.header { text-align: center; margin-bottom: 25px; }
.header h2 { margin: 0; padding-bottom: 10px; border-bottom: 2px solid #000; letter-spacing: 2px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: 14px; }
th { background: #f0f0f0; font-weight: bold; border: 1px solid #000; padding: 10px; text-align: center; }
td { border: 1px solid #000; padding: 8px; text-align: center; vertical-align: middle; }
.col-desc { text-align: left; }
.total-row td { border: 2px solid #000; background: #fff; font-weight: bold; font-size: 16px; }
.footer { display: flex; justify-content: space-between; margin-top: 60px; padding: 0 10px; }
.sig-line { width: 30%; border-top: 1px solid #000; text-align: center; padding-top: 10px; font-size: 15px; }
</style>
<div class="invoice-box">
<div class="header">
<h2>天津硕博霖 - 费用报销汇总单</h2>
<p style="text-align:right; margin-top:8px; font-size:14px;">打印日期: """ + datetime.now().strftime("%Y-%m-%d") + """</p>
</div>
<table>
<thead>
<tr>
<th width="10%">单号</th>
<th width="12%">申请人</th>
<th width="12%">日期</th>
<th width="36%">费用描述 / 票据类型</th>
<th width="15%">消费地点</th>
<th width="15%">金额 (元)</th>
</tr>
</thead>
<tbody>
"""
for _, row in print_df.iterrows():
html_content += f"""
<tr>
<td>#{row['id']}</td>
<td>{row['applicant']}</td>
<td>{row['apply_date']}</td>
<td class="col-desc">
{row['description']}<br>
<span style="font-size:12px; color:#555;">(原:{row['invoice_type_original']} / 冲:{row['invoice_type_offset']})</span>
</td>
<td>{row['location']}</td>
<td>¥{row['amount']}</td>
</tr>
"""
html_content += f"""
<tr class="total-row">
<td colspan="5" style="text-align:right; padding-right:15px;">合计金额 (Total)</td>
<td>¥{total_print_amt:,.2f}</td>
</tr>
</tbody>
</table>
<div class="footer">
<div class="sig-line">申请人签字</div>
<div class="sig-line">部门领导审批</div>
<div class="sig-line">主管会计审核</div>
</div>
</div>
"""
st.components.v1.html(html_content, height=900, scrolling=True)
if is_admin:
# --- Tab 3: 审批中心 (核心更新区域) ---
with active_tab[2]:
st.markdown("#### 批量审批中心")
# 1. 获取所有待审核数据
conn = get_db_connection()
# 读取所有字段
pending_df = pd.read_sql("SELECT * FROM expenses WHERE status='待审核' ORDER BY apply_date", conn)
conn.close()
if not pending_df.empty:
# 2. 增加一个 'Select' 列,默认为 False,放在第一列
pending_df.insert(0, "选择", False)
# 3. 使用 data_editor 实现可勾选的表格
st.caption("请勾选左侧复选框选择单据,然后点击下方按钮批量处理。可直接在表格中查看完整信息。")
edited_df = st.data_editor(
pending_df,
column_config={
"选择": st.column_config.CheckboxColumn(
"选择",
help="勾选以进行批量操作",
default=False,
),
"id": st.column_config.NumberColumn("单号", width="small", help="系统唯一ID"),
"applicant": st.column_config.TextColumn("申请人", width="small"),
"apply_date": st.column_config.DateColumn("日期", format="YYYY-MM-DD"),
"amount": st.column_config.NumberColumn("金额", format="¥%.2f"),
"description": st.column_config.TextColumn("费用描述", width="medium"),
"location": st.column_config.TextColumn("地点"),
"invoice_type_original": st.column_config.TextColumn("原始票据"),
"invoice_type_offset": st.column_config.TextColumn("冲顶票据"),
"remarks": st.column_config.TextColumn("备注", width="medium"),
"status": st.column_config.TextColumn("状态", disabled=True),
"created_at": st.column_config.DatetimeColumn("提交时间", format="D MMM, HH:mm", disabled=True),
},
disabled=["id", "applicant", "apply_date", "amount", "description",
"location", "invoice_type_original", "invoice_type_offset",
"remarks", "status", "created_at"], # 禁止修改数据,只允许勾选
hide_index=True,
use_container_width=True
)
# 4. 批量操作按钮
# 筛选出被勾选的行
selected_rows = edited_df[edited_df["选择"] == True]
col_approve, col_reject = st.columns([1, 1])
with col_approve:
# 批量通过按钮
if st.button(f"✅ 批量通过 ({len(selected_rows)}项)", type="primary", use_container_width=True):
if not selected_rows.empty:
id_list = selected_rows['id'].tolist()
# 批量更新数据库
conn = get_db_connection()
c = conn.cursor()
# 使用 executemany 或者循环更新,这里为了安全用参数化查询
placeholders = ', '.join(['?'] * len(id_list))
sql = f"UPDATE expenses SET status='已通过' WHERE id IN ({placeholders})"
c.execute(sql, id_list)
conn.commit()
conn.close()
st.success(f"已通过 {len(id_list)} 条申请!")
st.rerun()
else:
st.warning("请先勾选需要通过的单据。")
with col_reject:
# 批量驳回按钮
if st.button(f"❌ 批量驳回 ({len(selected_rows)}项)", use_container_width=True):
if not selected_rows.empty:
id_list = selected_rows['id'].tolist()
conn = get_db_connection()
c = conn.cursor()
placeholders = ', '.join(['?'] * len(id_list))
sql = f"UPDATE expenses SET status='已驳回' WHERE id IN ({placeholders})"
c.execute(sql, id_list)
conn.commit()
conn.close()
st.error(f"已驳回 {len(id_list)} 条申请!")
st.rerun()
else:
st.warning("请先勾选需要驳回的单据。")
else:
st.info("目前没有待审批的单据。")
# --- Tab 4: 统计与维护 ---
with active_tab[3]:
st.markdown("#### 全局统计与导出")
start_d = st.date_input("起始日期", datetime.now().replace(day=1))
conn = get_db_connection()
all_df = pd.read_sql("SELECT * FROM expenses WHERE apply_date >= ? ORDER BY apply_date DESC", conn, params=(str(start_d),))
conn.close()
if not all_df.empty:
st.metric("总金额", f"¥{all_df['amount'].sum():,.2f}")
st.download_button("导出Excel", to_excel(all_df), "所有报销.xlsx")
with st.expander("全局单据维护", expanded=True):
opts = {row['id']: f"#{row['id']} {row['applicant']} ¥{row['amount']}" for _, row in all_df.iterrows()}
mid = st.selectbox("选择单据", list(opts.keys()), format_func=lambda x: opts[x])
c1, c2 = st.columns(2)
if c1.button("重置为待审核"): run_query("UPDATE expenses SET status='待审核' WHERE id=?", (mid,)); st.rerun()
if c2.button("彻底删除", type="primary"): run_query("DELETE FROM expenses WHERE id=?", (mid,)); st.rerun()
# ==========================================
# 6. 主程序入口
# ==========================================
def main():
if 'logged_in' not in st.session_state:
st.session_state['logged_in'] = False
if not st.session_state['logged_in']:
login_page()
else:
selected_page = sidebar_menu()
if selected_page == "客户录入":
page_new_client()
elif selected_page == "客户列表与跟进":
page_client_hub()
elif selected_page == "经营看板":
page_dashboard()
elif selected_page == "报销管理":
page_expenses()
if __name__ == '__main__':
main()