feat: 修复 H5 底部导航覆盖 + 更新项目进度文档

## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
This commit is contained in:
TradeMate Dev
2026-05-12 20:24:42 +08:00
parent 69e164dcae
commit 7b62c2f8b4
125 changed files with 19725 additions and 728 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ if config.config_file_name is not None:
fileConfig(config.config_file_name)
from app.database import Base
from app.models import User, Product, Customer, Conversation, Message, Quotation, QuotationItem, CorpusEntry
from app.models import User, Product, Customer, Conversation, Message, Quotation, QuotationItem, CorpusEntry, Team, TeamMember, UsageLog, Notification, Feedback, Subscription, PreferenceAnalysis, MarketingEffect, Device, FollowupStrategy, FollowupLog
target_metadata = Base.metadata
+2 -1
View File
@@ -25,6 +25,7 @@ def upgrade() -> None:
sa.Column('username', sa.String(length=100), nullable=True),
sa.Column('password_hash', sa.String(length=255), nullable=True),
sa.Column('tier', sa.String(length=50), nullable=True),
sa.Column('role', sa.String(length=20), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
@@ -171,7 +172,7 @@ def upgrade() -> None:
sa.Column('user_edited', sa.Boolean(), nullable=True),
sa.Column('user_rating', sa.Integer(), nullable=True),
sa.Column('usage_count', sa.Integer(), nullable=True),
sa.Column('embedding', postgresql.Vector(length=768), nullable=True),
sa.Column('embedding', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
@@ -0,0 +1,68 @@
"""add teams and analytics tables
Revision ID: 002
Revises: 001
Create Date: 2026-05-09
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = '002'
down_revision: Union[str, None] = '001'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('teams',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('member_count', sa.Integer(), nullable=True),
sa.Column('max_members', sa.Integer(), nullable=True),
sa.Column('tier', sa.String(length=50), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_teams_owner_id'), 'teams', ['owner_id'], unique=False)
op.create_table('team_members',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('team_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('role', sa.String(length=50), nullable=True),
sa.Column('invited_by', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('status', sa.String(length=50), nullable=True),
sa.Column('joined_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_team_members_team_id'), 'team_members', ['team_id'], unique=False)
op.create_index(op.f('ix_team_members_user_id'), 'team_members', ['user_id'], unique=False)
op.create_table('usage_logs',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('team_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('detail', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('ip_address', sa.String(length=50), nullable=True),
sa.Column('user_agent', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_usage_logs_user_id'), 'usage_logs', ['user_id'], unique=False)
op.create_index(op.f('ix_usage_logs_team_id'), 'usage_logs', ['team_id'], unique=False)
def downgrade() -> None:
op.drop_table('usage_logs')
op.drop_table('team_members')
op.drop_table('teams')
@@ -0,0 +1,107 @@
"""add notifications, feedback, subscription, and p3 tables
Revision ID: 003
Revises: 002
Create Date: 2026-05-09
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = '003'
down_revision: Union[str, None] = '002'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('notifications',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('title', sa.String(length=255), nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('notification_type', sa.String(length=50), nullable=True),
sa.Column('reference_type', sa.String(length=50), nullable=True),
sa.Column('reference_id', sa.String(length=255), nullable=True),
sa.Column('is_read', sa.Boolean(), nullable=True),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_notifications_user_id'), 'notifications', ['user_id'], unique=False)
op.create_table('feedbacks',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('category', sa.String(length=50), nullable=True),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('contact', sa.String(length=100), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_feedbacks_user_id'), 'feedbacks', ['user_id'], unique=False)
op.create_table('subscriptions',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('plan', sa.String(length=50), nullable=False),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('started_at', sa.DateTime(), nullable=True),
sa.Column('expires_at', sa.DateTime(), nullable=True),
sa.Column('auto_renew', sa.Boolean(), nullable=True),
sa.Column('payment_provider', sa.String(length=50), nullable=True),
sa.Column('payment_id', sa.String(length=255), nullable=True),
sa.Column('amount', sa.Float(), nullable=True),
sa.Column('currency', sa.String(length=10), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_subscriptions_user_id'), 'subscriptions', ['user_id'], unique=False)
op.create_table('preference_analyses',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('task_type', sa.String(length=50), nullable=False),
sa.Column('preferred_tone', sa.String(length=50), nullable=True),
sa.Column('preferred_style', sa.String(length=50), nullable=True),
sa.Column('common_replacements', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('avg_formality_score', sa.Float(), nullable=True),
sa.Column('greeting_style', sa.String(length=100), nullable=True),
sa.Column('sign_off_style', sa.String(length=100), nullable=True),
sa.Column('analysis_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('confidence', sa.Float(), nullable=True),
sa.Column('interaction_count', sa.Integer(), nullable=True),
sa.Column('last_analysis_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_preference_analyses_user_id'), 'preference_analyses', ['user_id'], unique=False)
op.create_table('marketing_effects',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('content_hash', sa.String(length=64), nullable=False),
sa.Column('product_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('product_name', sa.String(length=255), nullable=True),
sa.Column('channel', sa.String(length=50), nullable=True),
sa.Column('event_type', sa.String(length=50), nullable=False),
sa.Column('target_audience', sa.String(length=255), nullable=True),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_marketing_effects_user_id'), 'marketing_effects', ['user_id'], unique=False)
op.create_index(op.f('ix_marketing_effects_content_hash'), 'marketing_effects', ['content_hash'], unique=False)
def downgrade() -> None:
op.drop_table('marketing_effects')
op.drop_table('preference_analyses')
op.drop_table('subscriptions')
op.drop_table('feedbacks')
op.drop_table('notifications')
+36
View File
@@ -0,0 +1,36 @@
"""add devices table for push notification registration
Revision ID: 004
Revises: 003
Create Date: 2026-05-10
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = '004'
down_revision: Union[str, None] = '003'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('devices',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('platform', sa.String(length=50), nullable=True),
sa.Column('push_token', sa.String(length=500), nullable=True),
sa.Column('client_id', sa.String(length=255), nullable=False),
sa.Column('device_info', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_devices_user_id'), 'devices', ['user_id'], unique=False)
def downgrade() -> None:
op.drop_table('devices')
+66
View File
@@ -0,0 +1,66 @@
"""add followup_strategies and followup_logs tables
Revision ID: 005
Revises: 004
Create Date: 2026-05-10
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = '005'
down_revision: Union[str, None] = '004'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('followup_strategies',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('trigger_condition', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('channel', sa.String(length=50), nullable=True),
sa.Column('ai_prompt_template', sa.Text(), nullable=True),
sa.Column('priority', sa.Integer(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_table('followup_logs',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('strategy_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('status', sa.String(length=50), nullable=True),
sa.Column('channel', sa.String(length=50), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('ai_generated_content', sa.Text(), nullable=True),
sa.Column('user_edited_content', sa.Text(), nullable=True),
sa.Column('health_score_at_time', sa.Integer(), nullable=True),
sa.Column('silence_days_at_time', sa.Integer(), nullable=True),
sa.Column('sent_at', sa.DateTime(), nullable=True),
sa.Column('replied_at', sa.DateTime(), nullable=True),
sa.Column('response_status', sa.String(length=50), nullable=True),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_followup_logs_user_id'), 'followup_logs', ['user_id'], unique=False)
op.create_index(op.f('ix_followup_logs_customer_id'), 'followup_logs', ['customer_id'], unique=False)
op.create_foreign_key('fk_followup_logs_customer', 'followup_logs', 'customers', ['customer_id'], ['id'])
op.create_foreign_key('fk_followup_logs_strategy', 'followup_logs', 'followup_strategies', ['strategy_id'], ['id'])
def downgrade() -> None:
op.drop_constraint('fk_followup_logs_strategy', 'followup_logs', type_='foreignkey')
op.drop_constraint('fk_followup_logs_customer', 'followup_logs', type_='foreignkey')
op.drop_index(op.f('ix_followup_logs_customer_id'), table_name='followup_logs')
op.drop_index(op.f('ix_followup_logs_user_id'), table_name='followup_logs')
op.drop_table('followup_logs')
op.drop_table('followup_strategies')