import type { WithAbortSignal } from "#Source/abort/index.ts" import { generalFetch } from "#Source/request/index.ts" import type { Lang } from "../base.ts" export type IndicesForecastDays = "1d" | "3d" export type IndicesForecastTypeId = string | number const internalIndicesForecastDaysSet = new Set(["1d", "3d"]) /** * @description 调用和风天气天气指数预报接口时使用的参数。 * * @example * ;``` * const options: IndicesForecastOptions = { * baseUrl: "https://abcxyz.qweatherapi.com", * jwt: "eyJhbGciOiJFZERTQSJ9.payload.signature", * days: "1d", * location: "101010100", * type: ["1", "2"], * lang: "zh-hans", * timeout: 5_000, * } * ``` */ export interface IndicesForecastOptions extends WithAbortSignal { /** * @description 和风天气 API Base URL,例如 `https://abcxyz.qweatherapi.com`。 */ baseUrl: string /** * @description 用于 `Authorization: Bearer` 请求头的 JWT。 */ jwt: string /** * @description 预报天数,仅支持 `1d` 或 `3d`。 */ days: IndicesForecastDays /** * @description 需要查询地区的 LocationID,或以英文逗号分隔的经度、纬度坐标。 */ location: string /** * @description 生活指数类型 ID,可传单个类型或多个类型列表。 */ type: IndicesForecastTypeId | readonly IndicesForecastTypeId[] /** * @description 返回结果的多语言代码。 */ lang?: Lang | undefined /** * @description 请求超时时间,单位为毫秒。 */ timeout?: number | undefined } /** * @description 天气指数预报响应中的单条指数数据。 * * @example * ;``` * const daily: IndicesForecastResultDaily = { * date: "2021-12-16", * type: "1", * name: "运动指数", * level: "3", * category: "较不宜", * text: "天气较好,但考虑天气寒冷,风力较强,推荐您进行室内运动。", * } * ``` */ export interface IndicesForecastResultDaily { /** * @description 预报日期。 */ date: string /** * @description 生活指数类型 ID。 */ type: string /** * @description 生活指数类型名称。 */ name: string /** * @description 生活指数预报等级。 */ level: string /** * @description 生活指数预报级别名称。 */ category: string /** * @description 生活指数预报的详细描述,可能为空。 */ text: string } /** * @description 天气指数预报响应中的来源与许可信息。 * * @example * ;``` * const refer: IndicesForecastResultRefer = { * sources: ["QWeather"], * license: ["QWeather Developers License"], * } * ``` */ export interface IndicesForecastResultRefer { /** * @description 原始数据来源或数据源说明。 */ sources?: string[] | undefined /** * @description 数据许可或版权声明。 */ license?: string[] | undefined } /** * @description 和风天气天气指数预报接口的响应结果。 * * @example * ;``` * const result: IndicesForecastResult = { * code: "200", * updateTime: "2021-12-16T18:35+08:00", * fxLink: "http://hfx.link/2ax2", * daily: [ * { * date: "2021-12-16", * type: "1", * name: "运动指数", * level: "3", * category: "较不宜", * text: "天气较好,但考虑天气寒冷,风力较强,推荐您进行室内运动。", * }, * { * date: "2021-12-16", * type: "2", * name: "洗车指数", * level: "3", * category: "较不宜", * text: "较不宜洗车,未来一天无雨,风力较大,如果执意擦洗汽车,要做好蒙上污垢的心理准备。", * }, * ], * refer: { * sources: ["QWeather"], * license: ["QWeather Developers License"], * }, * } * ``` */ export interface IndicesForecastResult { /** * @description 接口状态码。 */ code: string /** * @description 当前 API 的最近更新时间。 */ updateTime: string /** * @description 当前数据对应的响应式天气页面链接。 */ fxLink: string /** * @description 天气指数预报数据列表。 */ daily: IndicesForecastResultDaily[] /** * @description 响应中的来源与许可信息。 */ refer?: IndicesForecastResultRefer | undefined } const internalNormalizeIndicesForecastTypeId = (value: IndicesForecastTypeId): string => { if (typeof value === "number") { if (Number.isFinite(value) === false) { throw new TypeError("IndicesForecast type must be a finite number.") } return String(value) } const normalized = value.trim() if (normalized === "") { throw new TypeError("IndicesForecast type must not contain empty values.") } return normalized } const internalIsIndicesForecastTypeIdArray = ( value: IndicesForecastTypeId | readonly IndicesForecastTypeId[], ): value is readonly IndicesForecastTypeId[] => { return Array.isArray(value) } const internalNormalizeIndicesForecastTypes = ( value: IndicesForecastTypeId | readonly IndicesForecastTypeId[], ): string => { if (internalIsIndicesForecastTypeIdArray(value) === true) { if (value.length === 0) { throw new TypeError("IndicesForecast type must not be an empty array.") } return value.map((item) => internalNormalizeIndicesForecastTypeId(item)).join(",") } if (typeof value === "number" || typeof value === "string") { return internalNormalizeIndicesForecastTypeId(value) } throw new TypeError("IndicesForecast type must be a string, number, or array of them.") } const internalValidateIndicesForecastOptions = ( options: IndicesForecastOptions, ): { baseUrl: string jwt: string days: IndicesForecastDays location: string type: string } => { const baseUrl = options.baseUrl.trim() const jwt = options.jwt.trim() const location = options.location.trim() const type = internalNormalizeIndicesForecastTypes(options.type) if (baseUrl === "") { throw new TypeError("IndicesForecast baseUrl must not be empty.") } if (jwt === "") { throw new TypeError("IndicesForecast jwt must not be empty.") } if (location === "") { throw new TypeError("IndicesForecast location must not be empty.") } if (internalIndicesForecastDaysSet.has(options.days) === false) { throw new RangeError("IndicesForecast days must be one of: 1d, 3d.") } return { baseUrl, jwt, days: options.days, location, type, } } /** * @description 获取指定地区的天气指数预报数据。 * * @see {@link https://dev.qweather.com/docs/api/indices/indices-forecast/} */ export const indicesForecast = async ( options: IndicesForecastOptions, ): Promise => { const { baseUrl, jwt, days, location, type } = internalValidateIndicesForecastOptions(options) const { lang, timeout, abortSignal } = options return await generalFetch< { query: { location: string type: string lang?: Lang | undefined } }, { type: "json" data: IndicesForecastResult } >({ baseUrl, path: `/v7/indices/${days}`, method: "get", headers: { Authorization: `Bearer ${jwt}`, }, query: { location, type, lang, }, responseType: "json", timeout, abortSignal, }).getJson() }