# 数据服务 API 前端开发指南

> **模块说明**: 数据服务 API 提供数据产品的列表查询、详情获取、数据预览、Excel 导出、状态管理等功能。
>
> **基础路径**: `/api/dataservice`

---

## 目录

- [通用说明](#通用说明)
  - [响应格式](#响应格式)
  - [错误码说明](#错误码说明)
  - [Axios 配置](#axios-配置)
- [接口列表](#接口列表)
  1. [获取数据产品列表](#1-获取数据产品列表)
  2. [获取数据产品详情](#2-获取数据产品详情)
  3. [获取数据预览](#3-获取数据预览)
  4. [下载 Excel 文件](#4-下载-excel-文件)
  5. [标记已查看](#5-标记已查看)
  6. [刷新统计信息](#6-刷新统计信息)
  7. [删除数据产品](#7-删除数据产品)
  8. [注册数据产品](#8-注册数据产品)

---

## 通用说明

### 响应格式

所有接口返回统一的 JSON 格式：

```json
{
  "code": 200,
  "message": "操作成功",
  "data": { ... }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | number | 状态码，200 表示成功，其他表示失败 |
| `message` | string | 操作结果描述信息 |
| `data` | object \| array \| null | 返回的数据内容 |

### 错误码说明

| 状态码 | 说明 | 常见场景 |
|--------|------|----------|
| 200 | 成功 | 操作成功完成 |
| 400 | 请求参数错误 | 缺少必填字段、参数格式错误 |
| 404 | 资源不存在 | 数据产品 ID 不存在 |
| 500 | 服务器内部错误 | 数据库连接失败、SQL 执行异常 |

### Axios 配置

建议的 Axios 全局配置：

```javascript
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'

const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5050',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 响应拦截器
request.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message || 'Error'))
    }
    return res
  },
  error => {
    ElMessage.error(error.message || '网络错误')
    return Promise.reject(error)
  }
)

export default request
```

---

## 接口列表

---

### 1. 获取数据产品列表

分页获取数据产品列表，支持搜索和状态过滤。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `GET /api/dataservice/products` |
| **Method** | GET |
| **Content-Type** | - |

#### 请求参数 (Query String)

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `page` | integer | 否 | 1 | 页码 |
| `page_size` | integer | 否 | 20 | 每页数量 |
| `search` | string | 否 | "" | 搜索关键词（匹配名称、英文名、描述、表名） |
| `status` | string | 否 | - | 状态过滤：`active`、`inactive`、`error` |

#### 响应数据

```json
{
  "code": 200,
  "message": "获取数据产品列表成功",
  "data": {
    "list": [
      {
        "id": 1,
        "product_name": "人才数据产品",
        "product_name_en": "talent_data",
        "description": "包含所有人才的基本信息",
        "source_dataflow_id": 10,
        "source_dataflow_name": "人才数据清洗流程",
        "target_table": "dwd_talent_info",
        "target_schema": "public",
        "record_count": 15890,
        "column_count": 25,
        "last_updated_at": "2024-12-26T10:30:00",
        "last_viewed_at": "2024-12-26T09:00:00",
        "status": "active",
        "created_at": "2024-12-01T08:00:00",
        "created_by": "system",
        "updated_at": "2024-12-26T10:30:00",
        "has_new_data": true
      }
    ],
    "pagination": {
      "page": 1,
      "page_size": 20,
      "total": 45,
      "total_pages": 3
    }
  }
}
```

#### 数据产品字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | integer | 数据产品唯一 ID |
| `product_name` | string | 产品中文名称 |
| `product_name_en` | string | 产品英文名称 |
| `description` | string \| null | 产品描述 |
| `source_dataflow_id` | integer \| null | 关联的数据流 ID |
| `source_dataflow_name` | string \| null | 关联的数据流名称 |
| `target_table` | string | 目标数据表名 |
| `target_schema` | string | 目标 schema，默认 "public" |
| `record_count` | integer | 数据记录数 |
| `column_count` | integer | 数据列数 |
| `last_updated_at` | string \| null | 数据最后更新时间（ISO 8601 格式） |
| `last_viewed_at` | string \| null | 最后查看时间 |
| `status` | string | 状态：`active`、`inactive`、`error` |
| `created_at` | string | 创建时间 |
| `created_by` | string | 创建人 |
| `updated_at` | string | 更新时间 |
| `has_new_data` | boolean | 是否有新数据未查看（用于更新提示） |

#### Vue 接入示例

```vue
<template>
  <div class="data-product-list">
    <!-- 搜索栏 -->
    <div class="search-bar">
      <el-input
        v-model="searchParams.search"
        placeholder="搜索产品名称..."
        @keyup.enter="fetchProducts"
        clearable
      />
      <el-select v-model="searchParams.status" placeholder="状态" clearable>
        <el-option label="活跃" value="active" />
        <el-option label="非活跃" value="inactive" />
        <el-option label="错误" value="error" />
      </el-select>
      <el-button type="primary" @click="fetchProducts">查询</el-button>
    </div>

    <!-- 数据表格 -->
    <el-table :data="productList" v-loading="loading">
      <el-table-column prop="product_name" label="产品名称">
        <template #default="{ row }">
          <span>{{ row.product_name }}</span>
          <el-tag v-if="row.has_new_data" type="danger" size="small">新</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="target_table" label="目标表" />
      <el-table-column prop="record_count" label="记录数" />
      <el-table-column prop="status" label="状态">
        <template #default="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200">
        <template #default="{ row }">
          <el-button size="small" @click="handlePreview(row.id)">预览</el-button>
          <el-button size="small" type="success" @click="handleDownload(row.id)">
            下载
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <el-pagination
      v-model:current-page="searchParams.page"
      v-model:page-size="searchParams.page_size"
      :total="pagination.total"
      @current-change="fetchProducts"
    />
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import request from '@/utils/request'

const loading = ref(false)
const productList = ref([])
const pagination = ref({})
const searchParams = reactive({
  page: 1,
  page_size: 20,
  search: '',
  status: ''
})

const fetchProducts = async () => {
  loading.value = true
  try {
    const res = await request.get('/api/dataservice/products', {
      params: searchParams
    })
    productList.value = res.data.list
    pagination.value = res.data.pagination
  } finally {
    loading.value = false
  }
}

const getStatusType = (status) => {
  const types = { active: 'success', inactive: 'info', error: 'danger' }
  return types[status] || 'info'
}

onMounted(() => {
  fetchProducts()
})
</script>
```

---

### 2. 获取数据产品详情

根据 ID 获取单个数据产品的详细信息。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `GET /api/dataservice/products/{product_id}` |
| **Method** | GET |
| **Content-Type** | - |

#### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `product_id` | integer | 是 | 数据产品 ID |

#### 响应数据

```json
{
  "code": 200,
  "message": "获取数据产品详情成功",
  "data": {
    "id": 1,
    "product_name": "人才数据产品",
    "product_name_en": "talent_data",
    "description": "包含所有人才的基本信息",
    "source_dataflow_id": 10,
    "source_dataflow_name": "人才数据清洗流程",
    "target_table": "dwd_talent_info",
    "target_schema": "public",
    "record_count": 15890,
    "column_count": 25,
    "last_updated_at": "2024-12-26T10:30:00",
    "last_viewed_at": "2024-12-26T09:00:00",
    "status": "active",
    "created_at": "2024-12-01T08:00:00",
    "created_by": "system",
    "updated_at": "2024-12-26T10:30:00",
    "has_new_data": true
  }
}
```

#### 错误响应

**产品不存在 (404):**
```json
{
  "code": 404,
  "message": "数据产品不存在",
  "data": null
}
```

#### Vue 接入示例

```javascript
// 在 API 模块中定义
export const dataProductApi = {
  // 获取产品详情
  getDetail(productId) {
    return request.get(`/api/dataservice/products/${productId}`)
  }
}

// 在组件中使用
const fetchProductDetail = async (productId) => {
  try {
    const res = await dataProductApi.getDetail(productId)
    productDetail.value = res.data
  } catch (error) {
    console.error('获取产品详情失败:', error)
  }
}
```

---

### 3. 获取数据预览

获取数据产品的数据预览，支持自定义预览条数（默认 200 条，最大 1000 条）。

> **注意**: 调用此接口会自动将产品标记为已查看。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `GET /api/dataservice/products/{product_id}/preview` |
| **Method** | GET |
| **Content-Type** | - |

#### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `product_id` | integer | 是 | 数据产品 ID |

#### 请求参数 (Query String)

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `limit` | integer | 否 | 200 | 预览数据条数，最大 1000 |

#### 响应数据

```json
{
  "code": 200,
  "message": "获取数据预览成功",
  "data": {
    "product": {
      "id": 1,
      "product_name": "人才数据产品",
      "product_name_en": "talent_data",
      "target_table": "dwd_talent_info",
      "target_schema": "public",
      "record_count": 15890,
      "column_count": 25,
      "status": "active",
      "has_new_data": false
    },
    "columns": [
      { "name": "id", "type": "integer", "nullable": false },
      { "name": "name", "type": "character varying", "nullable": false },
      { "name": "email", "type": "character varying", "nullable": true },
      { "name": "created_at", "type": "timestamp without time zone", "nullable": false }
    ],
    "data": [
      { "id": 1, "name": "张三", "email": "zhangsan@example.com", "created_at": "2024-01-15T08:30:00" },
      { "id": 2, "name": "李四", "email": "lisi@example.com", "created_at": "2024-01-16T09:45:00" }
    ],
    "total_count": 15890,
    "preview_count": 200
  }
}
```

#### 响应字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `product` | object | 数据产品基本信息 |
| `columns` | array | 列定义数组 |
| `columns[].name` | string | 列名 |
| `columns[].type` | string | 数据类型 |
| `columns[].nullable` | boolean | 是否允许为空 |
| `data` | array | 数据行数组 |
| `total_count` | integer | 目标表总记录数 |
| `preview_count` | integer | 本次预览返回的记录数 |

#### 错误响应

**目标表不存在时:**
```json
{
  "code": 200,
  "message": "获取数据预览成功",
  "data": {
    "product": { ... },
    "columns": [],
    "data": [],
    "total_count": 0,
    "preview_count": 0,
    "error": "目标表 public.dwd_talent_info 不存在"
  }
}
```

#### Vue 接入示例

```vue
<template>
  <div class="data-preview">
    <!-- 产品信息卡片 -->
    <el-card class="product-info">
      <template #header>
        <span>{{ previewData.product?.product_name }}</span>
        <el-tag>{{ previewData.total_count }} 条记录</el-tag>
      </template>
      <p>目标表: {{ previewData.product?.target_schema }}.{{ previewData.product?.target_table }}</p>
    </el-card>

    <!-- 数据预览表格 -->
    <el-table :data="previewData.data" v-loading="loading" border>
      <el-table-column
        v-for="col in previewData.columns"
        :key="col.name"
        :prop="col.name"
        :label="col.name"
      >
        <template #header>
          <div>
            <span>{{ col.name }}</span>
            <br />
            <small style="color: #909399">{{ col.type }}</small>
          </div>
        </template>
      </el-table-column>
    </el-table>

    <!-- 预览条数控制 -->
    <div class="preview-controls">
      <span>已加载 {{ previewData.preview_count }} / {{ previewData.total_count }} 条</span>
      <el-button
        v-if="previewData.preview_count < previewData.total_count"
        @click="loadMore"
      >
        加载更多
      </el-button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import request from '@/utils/request'

const props = defineProps({
  productId: { type: Number, required: true }
})

const loading = ref(false)
const previewData = ref({
  product: null,
  columns: [],
  data: [],
  total_count: 0,
  preview_count: 0
})
const limit = ref(200)

const fetchPreview = async () => {
  loading.value = true
  try {
    const res = await request.get(
      `/api/dataservice/products/${props.productId}/preview`,
      { params: { limit: limit.value } }
    )
    previewData.value = res.data
  } finally {
    loading.value = false
  }
}

const loadMore = () => {
  limit.value = Math.min(limit.value + 200, 1000)
  fetchPreview()
}

onMounted(() => {
  fetchPreview()
})
</script>
```

---

### 4. 下载 Excel 文件

下载数据产品数据为 Excel 文件。

> **注意**: 调用此接口会自动将产品标记为已查看。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `GET /api/dataservice/products/{product_id}/download` |
| **Method** | GET |
| **Content-Type** | - |

#### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `product_id` | integer | 是 | 数据产品 ID |

#### 请求参数 (Query String)

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `limit` | integer | 否 | 200 | 导出数据条数，最大 10000 |

#### 响应数据

**成功**: 返回 Excel 文件（MIME 类型: `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`）

**文件名格式**: `{product_name_en}_{YYYYMMDD_HHMMSS}.xlsx`

例如: `talent_data_20241226_103000.xlsx`

#### 错误响应

**产品或表不存在 (JSON 格式):**
```json
{
  "code": 404,
  "message": "数据产品不存在: ID=999",
  "data": null
}
```

#### Vue 接入示例

```javascript
// 方式 1: 使用 a 标签下载（推荐简单场景）
const downloadExcel = (productId, limit = 200) => {
  const baseUrl = process.env.VUE_APP_API_BASE_URL || 'http://localhost:5050'
  const url = `${baseUrl}/api/dataservice/products/${productId}/download?limit=${limit}`
  window.open(url, '_blank')
}

// 方式 2: 使用 axios 下载（支持进度、Token 验证）
const downloadExcelWithAxios = async (productId, limit = 200) => {
  try {
    const response = await request.get(
      `/api/dataservice/products/${productId}/download`,
      {
        params: { limit },
        responseType: 'blob'
      }
    )

    // 从响应头获取文件名
    const contentDisposition = response.headers['content-disposition']
    let filename = 'download.xlsx'
    if (contentDisposition) {
      const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
      if (filenameMatch) {
        filename = filenameMatch[1].replace(/['"]/g, '')
      }
    }

    // 创建下载链接
    const blob = new Blob([response.data], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    })
    const url = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.download = filename
    link.click()
    window.URL.revokeObjectURL(url)
  } catch (error) {
    ElMessage.error('下载失败')
  }
}
```

```vue
<template>
  <div class="download-section">
    <el-input-number v-model="downloadLimit" :min="1" :max="10000" />
    <el-button type="primary" @click="handleDownload" :loading="downloading">
      下载 Excel
    </el-button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  productId: { type: Number, required: true }
})

const downloadLimit = ref(200)
const downloading = ref(false)

const handleDownload = async () => {
  downloading.value = true
  try {
    await downloadExcelWithAxios(props.productId, downloadLimit.value)
  } finally {
    downloading.value = false
  }
}
</script>
```

---

### 5. 标记已查看

手动标记数据产品为已查看，消除更新提示（红点）。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `POST /api/dataservice/products/{product_id}/viewed` |
| **Method** | POST |
| **Content-Type** | application/json |

#### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `product_id` | integer | 是 | 数据产品 ID |

#### 请求体

无需请求体。

#### 响应数据

```json
{
  "code": 200,
  "message": "标记已查看成功",
  "data": {
    "id": 1,
    "product_name": "人才数据产品",
    "has_new_data": false,
    "last_viewed_at": "2024-12-26T14:30:00"
  }
}
```

#### Vue 接入示例

```javascript
const markAsViewed = async (productId) => {
  try {
    await request.post(`/api/dataservice/products/${productId}/viewed`)
    // 更新本地数据状态
    const product = productList.value.find(p => p.id === productId)
    if (product) {
      product.has_new_data = false
    }
  } catch (error) {
    console.error('标记失败:', error)
  }
}
```

---

### 6. 刷新统计信息

刷新数据产品的统计信息（从目标表重新统计记录数和列数）。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `POST /api/dataservice/products/{product_id}/refresh` |
| **Method** | POST |
| **Content-Type** | application/json |

#### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `product_id` | integer | 是 | 数据产品 ID |

#### 请求体

无需请求体。

#### 响应数据

```json
{
  "code": 200,
  "message": "刷新统计信息成功",
  "data": {
    "id": 1,
    "product_name": "人才数据产品",
    "record_count": 16500,
    "column_count": 25,
    "status": "active",
    "last_updated_at": "2024-12-26T14:35:00"
  }
}
```

> **说明**: 如果目标表不存在，`status` 会更新为 `error`。

#### Vue 接入示例

```javascript
const refreshStats = async (productId) => {
  try {
    const res = await request.post(`/api/dataservice/products/${productId}/refresh`)
    ElMessage.success(`刷新成功，共 ${res.data.record_count} 条记录`)
    // 更新本地数据
    const index = productList.value.findIndex(p => p.id === productId)
    if (index !== -1) {
      productList.value[index] = res.data
    }
  } catch (error) {
    ElMessage.error('刷新失败')
  }
}
```

---

### 7. 删除数据产品

删除数据产品记录（仅删除注册信息，不删除实际数据表）。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `DELETE /api/dataservice/products/{product_id}` |
| **Method** | DELETE |
| **Content-Type** | - |

#### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `product_id` | integer | 是 | 数据产品 ID |

#### 响应数据

```json
{
  "code": 200,
  "message": "删除数据产品成功",
  "data": {}
}
```

#### 错误响应

```json
{
  "code": 404,
  "message": "数据产品不存在",
  "data": null
}
```

#### Vue 接入示例

```javascript
const deleteProduct = async (productId) => {
  try {
    await ElMessageBox.confirm('确定要删除该数据产品吗？此操作不可恢复。', '删除确认', {
      type: 'warning'
    })

    await request.delete(`/api/dataservice/products/${productId}`)
    ElMessage.success('删除成功')

    // 从本地列表移除
    productList.value = productList.value.filter(p => p.id !== productId)
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('删除失败')
    }
  }
}
```

---

### 8. 注册数据产品

手动注册新的数据产品。如果目标表已存在对应产品，则更新现有记录。

#### 请求信息

| 项目 | 说明 |
|------|------|
| **URL** | `POST /api/dataservice/products` |
| **Method** | POST |
| **Content-Type** | application/json |

#### 请求体参数

| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `product_name` | string | 是 | - | 产品中文名称 |
| `product_name_en` | string | 是 | - | 产品英文名称 |
| `target_table` | string | 是 | - | 目标数据表名 |
| `target_schema` | string | 否 | "public" | 目标 schema |
| `description` | string | 否 | null | 产品描述 |
| `source_dataflow_id` | integer | 否 | null | 关联的数据流 ID |
| `source_dataflow_name` | string | 否 | null | 关联的数据流名称 |
| `created_by` | string | 否 | "manual" | 创建人标识 |

#### 请求示例

```json
{
  "product_name": "人才简历数据",
  "product_name_en": "talent_resume_data",
  "target_table": "dwd_talent_resume",
  "target_schema": "public",
  "description": "经过清洗处理的人才简历数据",
  "source_dataflow_id": 15,
  "source_dataflow_name": "简历数据清洗流程"
}
```

#### 响应数据

```json
{
  "code": 200,
  "message": "注册数据产品成功",
  "data": {
    "id": 5,
    "product_name": "人才简历数据",
    "product_name_en": "talent_resume_data",
    "target_table": "dwd_talent_resume",
    "target_schema": "public",
    "description": "经过清洗处理的人才简历数据",
    "source_dataflow_id": 15,
    "source_dataflow_name": "简历数据清洗流程",
    "record_count": 0,
    "column_count": 0,
    "status": "active",
    "created_at": "2024-12-26T15:00:00",
    "created_by": "manual",
    "has_new_data": false
  }
}
```

#### 错误响应

**缺少必填字段 (400):**
```json
{
  "code": 400,
  "message": "缺少必填字段: product_name",
  "data": null
}
```

**请求体为空 (400):**
```json
{
  "code": 400,
  "message": "请求数据不能为空",
  "data": null
}
```

#### Vue 接入示例

```vue
<template>
  <el-dialog v-model="dialogVisible" title="注册数据产品">
    <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
      <el-form-item label="产品名称" prop="product_name">
        <el-input v-model="form.product_name" placeholder="请输入中文名称" />
      </el-form-item>
      <el-form-item label="英文名称" prop="product_name_en">
        <el-input v-model="form.product_name_en" placeholder="请输入英文名称" />
      </el-form-item>
      <el-form-item label="目标表名" prop="target_table">
        <el-input v-model="form.target_table" placeholder="例如: dwd_talent_info" />
      </el-form-item>
      <el-form-item label="Schema">
        <el-input v-model="form.target_schema" placeholder="默认 public" />
      </el-form-item>
      <el-form-item label="描述">
        <el-input v-model="form.description" type="textarea" :rows="3" />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit" :loading="submitting">
        确定
      </el-button>
    </template>
  </el-dialog>
</template>

<script setup>
import { ref, reactive } from 'vue'
import request from '@/utils/request'

const dialogVisible = ref(false)
const formRef = ref(null)
const submitting = ref(false)

const form = reactive({
  product_name: '',
  product_name_en: '',
  target_table: '',
  target_schema: 'public',
  description: ''
})

const rules = {
  product_name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
  product_name_en: [{ required: true, message: '请输入英文名称', trigger: 'blur' }],
  target_table: [{ required: true, message: '请输入目标表名', trigger: 'blur' }]
}

const handleSubmit = async () => {
  const valid = await formRef.value.validate()
  if (!valid) return

  submitting.value = true
  try {
    const res = await request.post('/api/dataservice/products', form)
    ElMessage.success('注册成功')
    dialogVisible.value = false
    emit('success', res.data)
  } catch (error) {
    ElMessage.error(error.message || '注册失败')
  } finally {
    submitting.value = false
  }
}

const emit = defineEmits(['success'])
</script>
```

---

## API 模块封装示例

建议将所有数据服务 API 封装到独立模块：

```javascript
// src/api/dataService.js
import request from '@/utils/request'

const BASE_URL = '/api/dataservice'

export const dataServiceApi = {
  /**
   * 获取数据产品列表
   */
  getProducts(params) {
    return request.get(`${BASE_URL}/products`, { params })
  },

  /**
   * 获取数据产品详情
   */
  getProductDetail(productId) {
    return request.get(`${BASE_URL}/products/${productId}`)
  },

  /**
   * 获取数据预览
   */
  getProductPreview(productId, limit = 200) {
    return request.get(`${BASE_URL}/products/${productId}/preview`, {
      params: { limit }
    })
  },

  /**
   * 下载 Excel (返回 Blob)
   */
  downloadExcel(productId, limit = 200) {
    return request.get(`${BASE_URL}/products/${productId}/download`, {
      params: { limit },
      responseType: 'blob'
    })
  },

  /**
   * 获取下载链接
   */
  getDownloadUrl(productId, limit = 200) {
    const baseUrl = process.env.VUE_APP_API_BASE_URL || ''
    return `${baseUrl}${BASE_URL}/products/${productId}/download?limit=${limit}`
  },

  /**
   * 标记为已查看
   */
  markAsViewed(productId) {
    return request.post(`${BASE_URL}/products/${productId}/viewed`)
  },

  /**
   * 刷新统计信息
   */
  refreshStats(productId) {
    return request.post(`${BASE_URL}/products/${productId}/refresh`)
  },

  /**
   * 删除数据产品
   */
  deleteProduct(productId) {
    return request.delete(`${BASE_URL}/products/${productId}`)
  },

  /**
   * 注册数据产品
   */
  registerProduct(data) {
    return request.post(`${BASE_URL}/products`, data)
  }
}

export default dataServiceApi
```

---

## 常见问题

### Q1: 数据预览加载很慢怎么办？

**A**: 减少 `limit` 参数值，默认 200 条已足够预览。对于大表，避免使用 1000 条限制。

### Q2: Excel 下载失败，返回 JSON 错误信息？

**A**: 检查响应的 Content-Type，如果是 `application/json`，说明发生了错误。需要解析 JSON 获取错误信息：

```javascript
const response = await request.get(url, { responseType: 'blob' })
if (response.data.type === 'application/json') {
  const text = await response.data.text()
  const error = JSON.parse(text)
  ElMessage.error(error.message)
}
```

### Q3: `has_new_data` 什么时候会变为 true？

**A**: 当数据产品的 `last_updated_at` 大于 `last_viewed_at` 时，表示有新数据更新但用户尚未查看。调用预览、下载或标记已查看接口后会自动重置。

### Q4: 如何处理跨域问题？

**A**: 后端已配置 CORS，支持任意 Origin。如遇问题，检查请求头是否正确设置 `Content-Type`。

---

## 更新日志

| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0.0 | 2024-12-26 | 初始版本，包含完整的 API 文档 |

