hookehuyr

fix(build): 修复 API 文档生成器嵌套对象解析问题

- 添加递归函数支持任意深度嵌套
- 修复 list 字段无法展开的问题
- 支持四层嵌套结构
- 重新生成所有 API 文档
- 修复 AmountInput ESLint 错误
This diff is collapsed. Click to expand it.
......@@ -366,6 +366,48 @@ function generateParamJSDoc(parameters, bodyParams, method) {
}
/**
* 递归生成属性字段的 JSDoc 注释
* @param {object} properties - 属性对象
* @param {number} indent - 缩进级别(空格数)
* @returns {string} - JSDoc 注释
*/
function generatePropertiesJSDoc(properties, indent = 0) {
const lines = [];
const prefix = ' '.repeat(indent);
Object.entries(properties).forEach(([key, value]) => {
const type = value.type || 'any';
const desc = value.description || value.title || '';
// 处理嵌套对象
if (type === 'object' && value.properties) {
lines.push(`${prefix}${key}: {\n`);
// 递归处理嵌套对象的属性
lines.push(generatePropertiesJSDoc(value.properties, indent + 2));
lines.push(`${prefix}};\n`);
}
// 处理数组(元素是对象)
else if (type === 'array' && value.items && value.items.properties) {
lines.push(`${prefix}${key}: Array<{\n`);
// 递归处理数组元素的属性
lines.push(generatePropertiesJSDoc(value.items.properties, indent + 2));
lines.push(`${prefix}}>;\n`);
}
// 处理简单数组
else if (type === 'array' && value.items) {
const itemType = value.items.type || 'any';
lines.push(`${prefix}${key}: Array<${itemType}>; // ${desc}\n`);
}
// 处理基本类型
else {
lines.push(`${prefix}${key}: ${type}; // ${desc}\n`);
}
});
return lines.join('');
}
/**
* 生成 JSDoc 返回值注释
* @param {object} responseSchema - 响应 schema
* @returns {string} - JSDoc 返回值注释
......@@ -388,44 +430,14 @@ function generateReturnJSDoc(responseSchema) {
// 处理对象类型的 data
if (dataType === 'object' && data.properties) {
returnDesc += ' * data: {\n';
Object.entries(data.properties).forEach(([key, value]) => {
const type = value.type || 'any';
const desc = value.description || value.title || '';
if (type === 'object' && value.properties) {
returnDesc += ` * ${key}: {\n`;
Object.entries(value.properties).forEach(([subKey, subValue]) => {
const subType = subValue.type || 'any';
const subDesc = subValue.description || subValue.title || '';
returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
});
returnDesc += ` * };\n`;
} else if (type === 'array' && value.items && value.items.properties) {
returnDesc += ` * ${key}: Array<{\n`;
Object.entries(value.items.properties).forEach(([subKey, subValue]) => {
const subType = subValue.type || 'any';
const subDesc = subValue.description || subValue.title || '';
returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
});
returnDesc += ` * }>;\n`;
} else {
returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
}
});
// 使用递归函数处理 data 的所有属性
returnDesc += generatePropertiesJSDoc(data.properties, 4);
returnDesc += ' * };\n';
}
// 处理数组类型的 data(你的情况
// 处理数组类型的 data(元素是对象
else if (dataType === 'array' && data.items && data.items.properties) {
returnDesc += ' * data: Array<{\n';
Object.entries(data.items.properties).forEach(([key, value]) => {
const type = value.type || 'any';
const desc = value.description || value.title || '';
returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
});
returnDesc += generatePropertiesJSDoc(data.items.properties, 4);
returnDesc += ' * }>;\n';
}
// 处理简单数组类型
......
......@@ -43,13 +43,13 @@ export const delAPI = (params) => fn(fetch.post(Api.Del, params));
* code: number; // 状态码
* msg: string; // 消息
* data: {
* list: Array<{
* meta_id: integer; // 文件ID
* name: string; // 文件名称
* src: string; // 文件URL
* created_time: string; // 收藏时间
* size: string; // 文件大小
* }>;
list: Array<{
meta_id: integer; // 文件ID
name: string; // 文件名称
src: string; // 文件URL
created_time: string; // 收藏时间
size: string; // 文件大小
}>;
* };
* }>}
*/
......
......@@ -30,16 +30,16 @@ export const addAPI = (params) => fn(fetch.post(Api.Add, params));
* code: number; // 状态码
* msg: string; // 消息
* data: {
* list: Array<{
* id: integer; // 订单ID
* status: integer; // 3=待处理, 5=已处理
* category: string; // 1=功能建议, 3=界面设计, 5=车辆新鲜, 7=其他问题
* images: string; // 图片
* contact: string; // 联系方式
* note: string; // 反馈内容
* reply: string; // 回复
* reply_time: string; // 回复时间
* }>;
list: Array<{
id: integer; // 订单ID
status: integer; // 3=待处理, 5=已处理
category: string; // 1=功能建议, 3=界面设计, 5=车辆新鲜, 7=其他问题
images: string; // 图片
contact: string; // 联系方式
note: string; // 反馈内容
reply: string; // 回复
reply_time: string; // 回复时间
}>;
* };
* }>}
*/
......
......@@ -19,32 +19,46 @@ const Api = {
* code: number; // 状态码
* msg: string; // 消息
* data: {
* cate: {
* id: integer; // 分类id
* category_name: string; // 分类名称
* category_parent: integer; // 分类父级
* category_description: null; // 分类描述
* };
* children: Array<{
* id: integer; // 二级分类id
* category_name: string; // 二级分类名
* category_parent: integer; // 二级分类名父级id
* category_description: null; // 二级分类描述
* icon: string; // 二级分类图标
* list: array; // 二级分类的附件列表
* children: array; // 三级分类
* }>;
* list: Array<{
* id: integer; //
* name: string; // 附件名称
* value: string; // 附件地址
* extension: string; // 后缀名
* post_date: string; // 发布时间
* size: string; // 附件大小
* is_favorite: integer; // 是否收藏
* }>;
* total: integer; // 主分类附件数量
* max_level: integer; // 页面需要层级
cate: {
id: integer; // 分类id
category_name: string; // 分类名称
category_parent: integer; // 分类父级
category_description: null; // 分类描述
};
children: Array<{
id: integer; // 二级分类id
category_name: string; // 二级分类名
category_parent: integer; // 二级分类名父级id
category_description: null; // 二级分类描述
icon: string; // 二级分类图标
list: Array<{
name: string; // 附件名称
value: string; // 附件地址
extension: string; // 后缀名
post_date: string; // 发布时间
size: string; // 附件大小
is_favorite: integer; // 是否收藏
id: string; // 附件id
}>;
children: Array<{
id: integer; // 三级分类id
category_name: string; // 三级分类名
category_parent: integer; // 三级分类名父级id
category_description: null; // 三级分类描述
icon: string; // 二级分类图标
}>;
}>;
list: Array<{
id: integer; //
name: string; // 附件名称
value: string; // 附件地址
extension: string; // 后缀名
post_date: string; // 发布时间
size: string; // 附件大小
is_favorite: integer; // 是否收藏
}>;
total: integer; // 主分类附件数量
max_level: integer; // 页面需要层级
* };
* }>}
*/
......@@ -60,15 +74,15 @@ export const fileListAPI = (params) => fn(fetch.get(Api.FileList, params));
* code: number; // 状态码
* msg: string; // 消息
* data: {
* list: Array<{
* meta_id: integer; // 文件ID
* name: string; // 文件名称
* src: string; // 文件URL
* size: string; // 文件大小
* read_people_count: integer; // 学习人数
* read_people_percent: number; // 学习人数比例
* is_favorite: string; //
* }>;
list: Array<{
meta_id: integer; // 文件ID
name: string; // 文件名称
src: string; // 文件URL
size: string; // 文件大小
read_people_count: integer; // 学习人数
read_people_percent: number; // 学习人数比例
is_favorite: string; //
}>;
* };
* }>}
*/
......
......@@ -15,33 +15,33 @@ const Api = {
* code: number; // 状态码
* msg: string; // 消息
* data: {
* id: integer; // 产品id
* product_name: string; // 产品名
* recommend: string; // 推荐位: normal-普通, hot-热卖
* status: string; //
* created_by: integer; //
* created_time: string; //
* updated_by: integer; //
* updated_time: string; //
* form_sn: string; // 关联表单sn
* product_description: string; // 产品描述
* categories: Array<{
* id: string; // 分类id
* name: string; // 分类名称
* }>;
* tags: Array<{
* id: string; // 标签id
* name: string; // 标签名
* bg_color: string; // 标签背景色
* text_color: string; // 标签文字色
* }>;
* documents: Array<{
* file_url: string; // 附件地址
* file_name: string; // 附件名
* file_size: string; // 附件大小
* file_size_formatted: string; // 附件大小(转换过显示)
* }>;
* cover_image: string; // 产品封面图
id: integer; // 产品id
product_name: string; // 产品名
recommend: string; // 推荐位: normal-普通, hot-热卖
status: string; //
created_by: integer; //
created_time: string; //
updated_by: integer; //
updated_time: string; //
form_sn: string; // 关联表单sn
product_description: string; // 产品描述
categories: Array<{
id: string; // 分类id
name: string; // 分类名称
}>;
tags: Array<{
id: string; // 标签id
name: string; // 标签名
bg_color: string; // 标签背景色
text_color: string; // 标签文字色
}>;
documents: Array<{
file_url: string; // 附件地址
file_name: string; // 附件名
file_size: string; // 附件大小
file_size_formatted: string; // 附件大小(转换过显示)
}>;
cover_image: string; // 产品封面图
* };
* }>}
*/
......@@ -61,21 +61,29 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
* code: number; // 状态码
* msg: string; // 消息
* data: {
* categories: Array<{
* id: integer; // 分类id
* name: string; // 分类名
* }>;
* list: Array<{
* id: integer; // 产品id
* product_name: string; // 产品名
* recommend: string; // 推荐位: normal-普通, hot-热卖
* form_sn: string; //
* created_time: string; // 创建时间
* categories: array; // 产品所属分类
* tags: array; // 产品标签
* cover_image: string; // 产品封面图
* }>;
* total: integer; // 产品总数
categories: Array<{
id: integer; // 分类id
name: string; // 分类名
}>;
list: Array<{
id: integer; // 产品id
product_name: string; // 产品名
recommend: string; // 推荐位: normal-普通, hot-热卖
form_sn: string; //
created_time: string; // 创建时间
categories: Array<{
id: string; // 分类id
name: string; // 分类名
}>;
tags: Array<{
id: string; // 标签id
name: string; // 标签名
bg_color: string; // 标签背景色
text_color: string; // 标签文字色
}>;
cover_image: string; // 产品封面图
}>;
total: integer; // 产品总数
* };
* }>}
*/
......
......@@ -12,11 +12,11 @@ const Api = {
* code: number; // 状态码
* msg: string; // 消息
* data: Array<{
* id: integer; //
* name: string; //
* seq: integer; //
* link: string; //
* icon: string; //
id: integer; //
name: string; //
seq: integer; //
link: string; //
icon: string; //
* }>;
* }>}
*/
......
......@@ -14,10 +14,10 @@ const Api = {
* code: number; // 状态码
* msg: string; // 消息
* data: Array<{
* id: integer; // 消息id
* note: string; // 消息内容
* created_time: string; // 发消息的时间
* status: string; // send=以发送未读取,read=已读取
id: integer; // 消息id
note: string; // 消息内容
created_time: string; // 发消息的时间
status: string; // send=以发送未读取,read=已读取
* }>;
* }>}
*/
......@@ -33,10 +33,10 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
* code: number; // 状态码
* msg: string; // 消息
* data: Array<{
* id: integer; // 消息id
* note: string; // 消息内容
* created_time: string; // 发消息的时间
* status: string; // send=以发送未读取,read=已读取
id: integer; // 消息id
note: string; // 消息内容
created_time: string; // 发消息的时间
status: string; // send=以发送未读取,read=已读取
* }>;
* }>}
*/
......
import { fn, fetch } from '@/api/fn';
const Api = {
Search: '/srv/?a=search&t=icon',
}
/**
* @description 搜索
* @remark
* @param {Object} params 请求参数
* @param {string} params.keyword (可选)
* @param {string} params.type (可选) product=产品,file=文档
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: {
products: {
list: Array<{
id: integer; // 产品id
product_name: string; // 产品名
product_description: string; // 产品描述
recommend: string; // normal-普通, hot-热卖
created_time: string; // 创建时间
cover_image: string; // 封面图
tags: Array<{
id: string; // 标签id
name: string; // 标签名
bg_color: string; // 标签背景色
text_color: string; // 标签文字色
}>;
type: string; //
form_sn: string; // 表单类型
}>;
total: integer; // 产品总数
};
files: {
list: Array<{
id: integer; // 附件id
name: string; // 附件名
value: string; // 附件地址
extension: string; // 附件类型
post_date: string; // 发布时间
size: string; // 附件大小
is_favorite: integer; // 是否收藏
type: string; //
}>;
total: integer; // 附件数
};
* };
* }>}
*/
export const searchAPI = (params) => fn(fetch.get(Api.Search, params));
......@@ -16,12 +16,19 @@ const Api = {
* code: number; // 状态码
* msg: string; // 消息
* data: {
* user: {
* id: integer; // 用户ID
* name: string; // 姓名
* employee_no: string; // 工号
* avatar: object; // 头像
* };
user: {
id: integer; // 用户ID
name: string; // 姓名
employee_no: string; // 工号
avatar: {
name: string; // 文件名
hash: string; // 文件hash
src: string; // 文件地址
height: string; // 文件高度
width: string; // 文件宽度
size: integer; // 文件大小
};
};
* };
* }>}
*/
......@@ -49,8 +56,8 @@ export const loginAPI = (params) => fn(fetch.post(Api.Login, params));
* code: number; // 状态码
* msg: string; // 消息
* data: {
* is_login: boolean; // true=登录,false=未登录
* is_openid: boolean; // true=已授权,false=未授权
is_login: boolean; // true=登录,false=未登录
is_openid: boolean; // true=已授权,false=未授权
* };
* }>}
*/
......
......@@ -15,11 +15,11 @@ const Api = {
* code: number; // 状态码
* msg: string; // 消息
* data: {
* user: {
* id: integer; // 用户ID
* avatar_url: string; // 头像
* name: string; // 姓名
* };
user: {
id: integer; // 用户ID
avatar_url: string; // 头像
name: string; // 姓名
};
* };
* }>}
*/
......
<template>
<div>
<!-- 标签 -->
<div v-if="label" class="text-sm text-gray-600 mb-2">
{{ label }}
<span v-if="currencyText" class="text-gray-500">{{ currencyText }}</span>
</div>
<!-- 多币种模式(方案 2 - 未来扩展) -->
<div v-if="multiCurrencyEnabled" class="mb-2">
<div class="text-sm text-gray-600 mb-2">币种</div>
<div class="flex gap-2">
<button
v-for="curr in supportedCurrencies"
:key="curr.value"
:class="[
'px-4 py-2 rounded-lg text-sm border transition-colors',
selectedCurrency === curr.value
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-600 border-gray-200'
]"
@tap="selectCurrency(curr.value)"
>
{{ curr.label }}
</button>
</div>
</div>
<!-- 保额输入 -->
<div class="border border-gray-200 rounded-lg flex items-center overflow-hidden">
<nut-input
:model-value="formattedValue"
@input="onInput"
type="digit"
:placeholder="placeholder"
class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900"
:border="false"
/>
<span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span>
</div>
</div>
</template>
<script setup>
/**
* 保额输入组件
*
* @description 支持多币种的保额输入组件
* - 单位转换:内部存储为分(整数),显示为元(带2位小数)
* - 币种支持:CNY、USD、HKD、EUR
* - 多币种模式:通过 FEATURE_FLAGS.MULTI_CURRENCY_ENABLED 控制
* @author Claude Code
* @example
* <!-- 固定币种模式 -->
* <AmountInput
* v-model="coverage"
* label="保额"
* currency="USD"
* placeholder="请输入保额"
* />
*
* @example
* <!-- 多币种模式 -->
* <AmountInput
* v-model="coverage"
* label="保额"
* :config="{ supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }"
* placeholder="请输入保额"
* />
*/
import { ref, computed } from 'vue'
import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates'
/**
* 组件属性
*/
const props = defineProps({
/**
* 标签文本
* @type {string}
*/
label: {
type: String,
default: ''
},
/**
* 占位符文本
* @type {string}
*/
placeholder: {
type: String,
default: '请输入保额'
},
/**
* 绑定的值(单位:分)
* @type {number}
* @example 100000 表示 1000.00 元
*/
modelValue: {
type: Number,
default: null
},
/**
* 币种代码(固定币种模式)
* @type {string}
* @default 'CNY'
*/
currency: {
type: String,
default: 'CNY'
},
/**
* 模版配置(多币种模式)
* @type {Object}
* @property {Array<string>} supported_currencies - 支持的币种代码数组
* @property {string} default_currency - 默认币种代码
* @example { supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }
*/
config: {
type: Object,
default: () => ({})
}
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {number} value - 保额值(单位:分)
*/
'update:modelValue'
])
/**
* 判断是否启用多币种
* @type {ComputedRef<boolean>}
*/
const multiCurrencyEnabled = computed(() => FEATURE_FLAGS.MULTI_CURRENCY_ENABLED)
/**
* 当前选中的币种
* @type {Ref<string>}
*/
const selectedCurrency = ref(props.config.default_currency || props.currency || 'CNY')
/**
* 支持的币种列表(多币种模式)
* @type {ComputedRef<Array<{label: string, symbol: string, value: string}>>}
*/
const supportedCurrencies = computed(() => {
if (!multiCurrencyEnabled.value) return []
return (props.config.supported_currencies || ['CNY'])
.map(code => CURRENCY_MAP[code])
.filter(Boolean)
})
/**
* 当前币种符号
* @type {ComputedRef<string>}
* @example
* // CNY -> '¥'
* // USD -> '$'
*/
const currencySymbol = computed(() => {
if (multiCurrencyEnabled.value) {
// 多币种模式:使用用户选择的币种
const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
return curr?.symbol || '¥'
}
// 固定币种模式:使用 props.currency
return CURRENCY_SYMBOLS[props.currency] || '¥'
})
/**
* 币种文本(用于标签显示)
* @type {ComputedRef<string>}
*/
const currencyText = computed(() => {
if (multiCurrencyEnabled.value) {
const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
return curr?.label || ''
}
const CURRENCY_NAMES = {
CNY: '人民币',
USD: '美元',
HKD: '港币',
EUR: '欧元'
}
return CURRENCY_NAMES[props.currency] || ''
})
/**
* 格式化显示值(元,带2位小数)
* @description 将分转换为元进行显示
* @type {ComputedRef<string>}
* @example
* // modelValue = 100000 (分)
* // formattedValue() // 返回: '1000.00'
*/
const formattedValue = computed(() => {
if (props.modelValue === null || props.modelValue === undefined) {
return ''
}
// 分 -> 元,保留2位小数
return (props.modelValue / 100).toFixed(2)
})
/**
* 用户输入处理
* @description 将用户输入的元转换为分存储
* @param {string} value - 输入值
*
* @example
* // 用户输入: '1000.50'
* // onInput('1000.50')
* // -> emit('update:modelValue', 100050) // 分
*/
const onInput = (value) => {
// 移除非数字和小数点
const cleanValue = value.replace(/[^\d.]/g, '')
// 转换为分(整数)
const yuan = parseFloat(cleanValue)
if (!Number.isNaN(yuan)) {
emit('update:modelValue', Math.round(yuan * 100))
} else {
emit('update:modelValue', 0)
}
}
/**
* 选择币种(多币种模式)
* @param {string} value - 币种代码
*/
const selectCurrency = (value) => {
selectedCurrency.value = value
}
</script>
<style lang="less" scoped>
/* 组件样式 */
</style>