一、工作概述

本次主要围绕项目网页端的视觉体验优化、历史记录分页功能修复以及初始化阶段的交互稳定性问题进行了多轮调整。
整体目标是提升页面的可读性、操作一致性和功能可用性,使前端界面在展示效果和使用体验上更加成熟、稳定。

本次工作重点包括:

  • 优化页面整体配色与卡片样式
  • 修正表单布局中下拉框与文字的相对位置问题
  • 美化所有下拉选项框及悬停交互效果
  • 重构历史记录展示区,使其更清晰、更具层次感
  • 修复历史记录分页逻辑,确保分页真正生效
  • 修复页面初始化时的空节点报错问题

二、页面视觉与布局优化

1. 整体风格调整

原始页面采用较为朴素的布局与配色,虽然功能完整,但在视觉统一性、层次感和现代感方面存在不足。
本次重点将页面主色调调整为更清爽的浅色风格,并统一了以下组件样式:

  • 页面背景
  • 主卡片容器
  • 输入框与下拉框
  • 按钮
  • 状态提示区域
  • 历史记录展示区

通过这些调整,页面整体观感更加轻盈,信息层级也更明确。

2. 表单布局修正

在表单区,尤其是“语言(可选)”和“模式”两个下拉项,之前存在文字与控件错位、对齐不整齐的问题。
为此调整了网格布局和控件高度,使多个字段在横向排列时保持更统一的视觉节奏。

相关结构示例如下:

<div class="row meta-row">
  <div class="field">
    <label>会话 ID(可选)</label>
    <input id="sessionId" type="text" placeholder="例如 2026-04-20-demo" />
  </div>
  <div class="field">
    <label>语言(可选)</label>
    <select id="lang">
      <option value="">自动/不传</option>
      <option value="zh">中文</option>
      <option value="en">English</option>
    </select>
  </div>
  <div class="field">
    <label>模式</label>
    <select id="mode">
      <option value="story">story(故事/长文本)</option>
      <option value="auto">auto(自动判断)</option>
      <option value="companion">companion(陪伴对话)</option>
    </select>
  </div>
</div>

三、下拉框样式优化

原来的下拉框在浏览器默认样式下略显陈旧,选项框颜色、悬停状态和交互反馈不够统一。
因此对 select 组件进行了重新设计,主要改动包括:

  • 自定义下拉箭头
  • 优化 hover 状态
  • 提升 focus 状态的可见性
  • 调整选项面板的配色
  • 与整体浅色主题保持一致

核心样式示例如下:

select {
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, #51607b 50%),
    linear-gradient(135deg, #51607b 50%, transparent 50%);
  background-position:
    calc(100% - 18px) calc(50% - 2px),
    calc(100% - 12px) calc(50% - 2px);
  background-size: 7px 7px, 7px 7px;
  background-repeat: no-repeat;
  padding-right: 38px;
  line-height: 1.25;
}

select:hover {
  border-color: rgba(59, 130, 246, 0.48);
  background-color: #fff;
}

select option {
  background: #fff;
  color: #1f2a3d;
}

该调整使下拉框不再显得“老旧”,同时增强了页面的整体统一性。


四、历史记录区优化

1. 卡片式历史记录展示

历史记录区域原先以较简单的文本堆叠方式呈现,信息密度较高,但层次感不足。
本次将其重构为更具可读性的卡片式结构,按“类型、会话、时间、发送内容、识别结果、返回结果、状态”分区展示。

历史记录展示核心结构如下:

historyList.innerHTML = history.map(msg => {
  const time = new Date(msg.created_at).toLocaleString('zh-CN');
  const typeLabel = {
    text: '文本',
    audio: '音频',
    file: '文件',
    asr: '语音识别'
  }[msg.message_type] || msg.message_type;

  const statusColor = msg.response_status === 200 ? 'var(--ok)' :
                      msg.response_status && msg.response_status > 0 ? 'var(--danger)' : 'var(--muted)';
  const statusText = msg.response_status === 200 ? '处理成功' : `状态: ${msg.response_status || 'N/A'}`;

  return `
    <article class="history-card">
      <div class="history-head">
        <div class="history-tags">
          <span class="pill">${typeLabel}</span>
          ${msg.session_id ? `<span class="pill">${escapeHtml(msg.session_id)}</span>` : ''}
        </div>
        <div class="history-time">${time}</div>
      </div>

      <div class="history-section">
        <span class="history-label">发送内容</span>
        <div class="history-text">${escapeHtml(msg.user_content)}</div>
      </div>

      ${msg.asr_result ? `
        <div class="history-section">
          <span class="history-label" style="color: var(--accent2);">识别结果</span>
          <div class="history-text">${escapeHtml(msg.asr_result)}</div>
        </div>
      ` : ''}

      ${msg.cloud_response ? `
        <div class="history-section">
          <span class="history-label" style="color: var(--ok);">返回结果</span>
          <div class="history-response">${formatResponse(msg.cloud_response)}</div>
        </div>
      ` : ''}

      <div class="history-status" style="color: ${statusColor};">${statusText}</div>
    </article>
  `;
}).join('');

2. 视觉表现优化

历史记录区增加了:

  • 更柔和的背景
  • 更明显的边框和阴影
  • 类型标签与会话标签
  • 分区标题
  • 状态颜色提示

这样可以更快识别记录类型和处理状态,也让大量历史数据更容易浏览。


五、分页功能修复

1. 问题描述

原先历史记录虽然表面上有分页按钮,但实际上第一页仍可能一次性展示大量记录,导致分页功能“看起来存在,实际并未生效”。

2. 修复思路

为保证分页真正生效,本次从后端和前端两端同时修复:

  • 后端历史接口支持 limit + offset
  • 前端每次只请求当前页需要的数据
  • 后端返回总数 total
  • 前端根据 total 判断是否存在下一页

3. 后端修改示例

历史记录接口现在返回结构化分页数据:

@app.get("/api/history/session/{session_id}")
async def get_session_history(session_id: str, limit: int = 50, offset: int = 0) -> dict[str, Any]:
    """获取指定会话的历史记录"""
    db = SessionLocal()
    try:
        messages = get_session_messages(db, session_id, limit=limit, offset=offset)
        total = count_session_messages(db, session_id)
        return {
            "items": [
                {
                    "id": msg.id,
                    "session_id": msg.session.session_id if msg.session else None,
                    "message_type": msg.message_type,
                    "user_content": msg.user_content,
                    "language": msg.language,
                    "asr_result": msg.asr_result,
                    "cloud_response": msg.cloud_response,
                    "response_status": msg.response_status,
                    "created_at": msg.created_at.isoformat() if msg.created_at else None,
                }
                for msg in messages
            ],
            "total": total,
            "limit": limit,
            "offset": offset,
        }
    finally:
        db.close()

4. 前端分页逻辑示例

async function loadHistory(sessionId = null, page = historyPage) {
  const pageSize = Number(pageSizeEl?.value || 10);
  const offset = Math.max(0, (page - 1) * pageSize);
  const url = sessionId
    ? `/api/history/session/${encodeURIComponent(sessionId)}?limit=${pageSize}&offset=${offset}`
    : `/api/history?limit=${pageSize}&offset=${offset}`;

  const r = await fetch(url);
  const payload = await r.json();

  const history = Array.isArray(payload) ? payload : (payload?.items || []);
  const total = typeof payload?.total === "number" ? payload.total : null;

  historyHasNextPage = total == null
    ? history.length === pageSize
    : offset + history.length < total;

  if (nextHistoryBtn) nextHistoryBtn.disabled = !historyHasNextPage;
}

通过这一改动,分页按钮已经从“展示型功能”变成真正的“数据分页控制”。


六、初始化报错修复

在页面初始化阶段,曾出现如下错误:

  • TypeError: Cannot set properties of null (setting 'textContent')

该问题的根因是:前端在 loadConfig() 阶段直接对某些 DOM 节点进行赋值,但这些节点在某些情况下未正确获取到。

修复方式

  • 所有相关节点都增加了判空保护
  • 读取配置失败时不再污染日志框
  • 避免在初始化阶段因空节点产生连锁报错

修改后的关键逻辑如下:

async function loadConfig() {
  try {
    const r = await fetch("/config");
    const cfg = await r.json();

    if (cfgDot) {
      if (cfg.upstreamConfigured) {
        cfgDot.classList.add("ok");
        if (cfgText) cfgText.textContent = "云端已配置";
      } else {
        cfgDot.classList.remove("ok");
        if (cfgText) cfgText.textContent = "云端未配置";
      }
    }

    if (textPathHint) textPathHint.textContent = `POST /api/text → ${cfg.upstreamTextPath || "/parse/text"}`;
    if (audioPathHint) audioPathHint.textContent = `POST /api/audio → ${cfg.upstreamAudioPath || "/parse/audio"}`;
    if (filePathHint) filePathHint.textContent = `POST /api/file → ${cfg.upstreamFilePath || "/parse/file"}`;
  } catch (e) {
    if (configBadge) configBadge.title = "读取配置失败,请检查后端是否已启动";
  }
}

这样可以避免页面一打开就出现错误提示,提升首次加载体验。


七、阶段性成果

本周完成后,项目在以下方面已有明显提升:

  1. 页面视觉统一性增强
  2. 下拉控件交互体验更现代
  3. 历史记录展示更清晰、更易读
  4. 分页功能真正可用
  5. 初始化阶段错误得到修复
  6. 整体页面稳定性提升
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐