diff --git a/空气质量预测/源码/服务端/iapip-svr/src/controllers/config.ts b/空气质量预测/源码/服务端/iapip-svr/src/controllers/config.ts new file mode 100644 index 0000000..767fed2 --- /dev/null +++ b/空气质量预测/源码/服务端/iapip-svr/src/controllers/config.ts @@ -0,0 +1,1225 @@ +import Router from 'koa-router' +import { db } from '..' +import { ProjectSpaceCategory, MaterialCategory, CityInformation } from '@prisma/client' +import authn from '../middlewares/authn' +import { ADMIN_TYPE, AdminCert, AnyObj, CERT_TYPE } from '../common/types' +import validate from '../middlewares/validator' +import authz from '../middlewares/authz' +import { fail, isRepeat, toNum } from '../common/utils' +import { ERR_CODE } from '../common/constants' +import fs from 'fs' +import path from 'path' +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const mammoth = require('mammoth') +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const pdfParse = require('pdf-parse') + +const configRouter = new Router() +configRouter.prefix('/api/cfg') + +/** + * 项目-空间类型 + */ + +configRouter.post( + '/proj-space-cat', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + pry_no_1: { + type: 'number' + }, + project_category: { + type: 'string' + }, + space_categories: { + type: 'array', + items: { + type: 'object', + properties: { + pry_no_2: { + type: 'number' + }, + space_category: { + type: 'string' + } + }, + required: ['space_category'], + additionalProperties: false + }, + minItems: 1 + } + }, + required: ['project_category', 'space_categories'], + additionalProperties: false + } + }), + async ctx => { + const { project_category, space_categories } = ctx.request.body + if (await db.projectSpaceCategory.findFirst({ where: { project_category } })) { + ctx.throw(422, fail(ERR_CODE.CAT_EXIST, '项目类型已存在')) + return + } + if (isRepeat(space_categories.map((sc: AnyObj) => sc.space_category))) { + ctx.throw(422, fail(ERR_CODE.CAT_DUPL, '空间类型重复')) + return + } + const agg = await db.projectSpaceCategory.aggregate({ + _max: { + pry_no_1: true + } + }) + const nextPryNo1 = (agg._max.pry_no_1 ?? 0) + 1 + ctx.body = await db.projectSpaceCategory.createMany({ + data: space_categories.map((sc: AnyObj, idx: number) => ({ + pry_no_1: nextPryNo1, + pry_no_2: idx + 1, + project_category, + space_category: sc.space_category + })) + }) + } +) + +configRouter.put( + '/proj-space-cat', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + pry_no_1: { + type: 'number' + }, + project_category: { + type: 'string' + }, + space_categories: { + type: 'array', + items: { + type: 'object', + properties: { + pry_no_2: { + type: 'number' + }, + space_category: { + type: 'string' + } + }, + required: ['space_category'], + additionalProperties: false + }, + minItems: 1 + } + }, + required: ['pry_no_1', 'project_category', 'space_categories'], + additionalProperties: false + } + }), + async ctx => { + const { pry_no_1, project_category, space_categories } = ctx.request.body + if (await db.projectSpaceCategory.findFirst({ where: { pry_no_1: { not: pry_no_1 }, project_category } })) { + ctx.throw(422, fail(ERR_CODE.CAT_EXIST, '项目类型已存在')) + return + } + if (isRepeat(space_categories.map((sc: AnyObj) => sc.space_category))) { + ctx.throw(422, fail(ERR_CODE.CAT_DUPL, '空间类型重复')) + return + } + ctx.body = await db.$transaction([ + db.projectSpaceCategory.deleteMany({ + where: { + pry_no_1 + } + }), + db.projectSpaceCategory.createMany({ + data: space_categories.map((sc: AnyObj, idx: number) => ({ + pry_no_1, + pry_no_2: idx + 1, + project_category, + space_category: sc.space_category + })) + }) + ]) + } +) + +configRouter.patch( + '/proj-space-cat', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + bfr_no: { + type: 'number' + }, + afr_no: { + type: 'number' + } + }, + required: ['bfr_no', 'afr_no'], + additionalProperties: false + } + }), + async ctx => { + const agg = await db.projectSpaceCategory.aggregate({ + _max: { + pry_no_1: true + } + }) + const tempPryNo1 = (agg._max.pry_no_1 ?? 0) + 10 + const bfr_no: number = ctx.request.body.bfr_no + const afr_no: number = ctx.request.body.afr_no + ctx.body = await db.$transaction([ + db.projectSpaceCategory.updateMany({ + where: { + pry_no_1: bfr_no + }, + data: { + pry_no_1: tempPryNo1 + } + }), + ( + bfr_no <= afr_no ? + db.$executeRaw`UPDATE project_space_category SET pry_no_1 = pry_no_1 - 1 WHERE pry_no_1 BETWEEN ${bfr_no} AND ${afr_no}` : + db.$executeRaw`UPDATE project_space_category SET pry_no_1 = pry_no_1 + 1 WHERE pry_no_1 BETWEEN ${afr_no} AND ${bfr_no}` + ), + db.projectSpaceCategory.updateMany({ + where: { + pry_no_1: tempPryNo1 + }, + data: { + pry_no_1: afr_no + } + }), + ]) + } +) + +configRouter.delete( + '/proj-space-cat/:pry_no_1', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + async ctx => { + const pry_no_1 = Number(ctx.params.pry_no_1) + ctx.body = await db.projectSpaceCategory.deleteMany({ + where: { + pry_no_1 + } + }) + } +) + +configRouter.get( + '/proj-space-cat', + authn(CERT_TYPE.ACCOUNT), + async ctx => { + const project_category = ctx.state.query.project_category ?? '' + const space_category = ctx.state.query.space_category ?? '' + const categories = await db.$queryRaw`SELECT pry_no_1, project_category, GROUP_CONCAT(CONCAT(pry_no_2, '-', space_category) ORDER BY pry_no_2) AS space_category + FROM project_space_category + WHERE project_category LIKE CONCAT('%', ${project_category}, '%') + GROUP BY pry_no_1, project_category + HAVING space_category LIKE CONCAT('%', ${space_category}, '%') + ORDER BY pry_no_1` + ctx.body = categories.map(pc => ({ + pry_no_1: pc.pry_no_1, + project_category: pc.project_category, + space_categories: pc.space_category.split(',').map(sc => { + const arr = sc.split('-') + return { + pry_no_2: Number(arr[0]), + space_category: arr[1] + } + }) + })) + } +) + +configRouter.get( + '/proj-cat', + authn(CERT_TYPE.ACCOUNT), + async ctx => { + const projectCategories = await db.projectSpaceCategory.groupBy({ + by: ['pry_no_1', 'project_category'], + orderBy: { + pry_no_1: 'asc' + } + }) + ctx.body = projectCategories.map(pc => pc.project_category) + } +) + +configRouter.get( + '/proj-space-cat/by', + authn(CERT_TYPE.ACCOUNT), + validate({ + query: { + type: 'object', + properties: { + project_category: { + type: 'string' + } + }, + required: ['project_category'], + additionalProperties: false + } + }), + async ctx => { + const categories = await db.projectSpaceCategory.findMany({ + where: { + project_category: ctx.state.query.project_category + } + }) + ctx.body = categories.length ? { + pry_no_1: categories[0].pry_no_1, + project_category: categories[0].project_category, + space_categories: categories.map(c => ({ + pry_no_2: c.pry_no_2, + space_category: c.space_category + })) + } : null + } +) + +/** + * 材料类别 + */ + +configRouter.post( + '/mtrl-cat', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + pry_no_1: { + type: 'number' + }, + first_category: { + type: 'string' + }, + second_categories: { + type: 'array', + items: { + type: 'object', + properties: { + pry_no_2: { + type: 'number' + }, + second_category: { + type: 'string' + }, + unit: { + type: 'string' + } + }, + required: ['second_category'], + additionalProperties: false + }, + minItems: 1 + } + }, + required: ['first_category', 'second_categories'], + additionalProperties: false + } + }), + async ctx => { + const { first_category, second_categories } = ctx.request.body + if (await db.materialCategory.findFirst({ where: { first_category } })) { + ctx.throw(422, fail(ERR_CODE.CAT_EXIST, '一级分类已存在')) + return + } + if (isRepeat(second_categories.map((sc: AnyObj) => sc.second_category))) { + ctx.throw(422, fail(ERR_CODE.CAT_DUPL, '二级分类重复')) + return + } + const agg = await db.materialCategory.aggregate({ + _max: { + pry_no_1: true + } + }) + const nextPryNo1 = (agg._max.pry_no_1 ?? 0) + 1 + ctx.body = await db.materialCategory.createMany({ + data: second_categories.map((sc: AnyObj, idx: number) => ({ + pry_no_1: nextPryNo1, + pry_no_2: idx + 1, + first_category, + second_category: sc.second_category, + unit: sc.unit + })) + }) + } +) + +configRouter.put( + '/mtrl-cat', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + pry_no_1: { + type: 'number' + }, + first_category: { + type: 'string' + }, + second_categories: { + type: 'array', + items: { + type: 'object', + properties: { + pry_no_2: { + type: 'number' + }, + second_category: { + type: 'string' + }, + unit: { + type: 'string' + } + }, + required: ['second_category'], + additionalProperties: false + }, + minItems: 1 + } + }, + required: ['pry_no_1', 'first_category', 'second_categories'], + additionalProperties: false + } + }), + async ctx => { + const { pry_no_1, first_category, second_categories } = ctx.request.body + if (await db.materialCategory.findFirst({ where: { pry_no_1: { not: pry_no_1 }, first_category } })) { + ctx.throw(422, fail(ERR_CODE.CAT_EXIST, '一级分类已存在')) + return + } + if (isRepeat(second_categories.map((sc: AnyObj) => sc.second_category))) { + ctx.throw(422, fail(ERR_CODE.CAT_DUPL, '二级分类重复')) + return + } + ctx.body = await db.$transaction([ + db.materialCategory.deleteMany({ + where: { + pry_no_1 + } + }), + db.materialCategory.createMany({ + data: second_categories.map((sc: AnyObj, idx: number) => ({ + pry_no_1, + pry_no_2: idx + 1, + first_category, + second_category: sc.second_category, + unit: sc.unit + })) + }) + ]) + } +) + +configRouter.patch( + '/mtrl-cat', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + bfr_no: { + type: 'number' + }, + afr_no: { + type: 'number' + } + }, + required: ['bfr_no', 'afr_no'], + additionalProperties: false + } + }), + async ctx => { + const agg = await db.materialCategory.aggregate({ + _max: { + pry_no_1: true + } + }) + const tempPryNo1 = (agg._max.pry_no_1 ?? 0) + 10 + const bfr_no: number = ctx.request.body.bfr_no + const afr_no: number = ctx.request.body.afr_no + ctx.body = await db.$transaction([ + db.materialCategory.updateMany({ + where: { + pry_no_1: bfr_no + }, + data: { + pry_no_1: tempPryNo1 + } + }), + ( + bfr_no <= afr_no ? + db.$executeRaw`UPDATE material_category SET pry_no_1 = pry_no_1 - 1 WHERE pry_no_1 BETWEEN ${bfr_no} AND ${afr_no}` : + db.$executeRaw`UPDATE material_category SET pry_no_1 = pry_no_1 + 1 WHERE pry_no_1 BETWEEN ${afr_no} AND ${bfr_no}` + ), + db.materialCategory.updateMany({ + where: { + pry_no_1: tempPryNo1 + }, + data: { + pry_no_1: afr_no + } + }), + ]) + } +) + +configRouter.delete( + '/mtrl-cat/:pry_no_1', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + async ctx => { + const pry_no_1 = Number(ctx.params.pry_no_1) + ctx.body = await db.materialCategory.deleteMany({ + where: { + pry_no_1 + } + }) + } +) + +configRouter.get( + '/mtrl-cat', + authn(CERT_TYPE.ACCOUNT), + async ctx => { + const first_category = ctx.state.query.first_category ?? '' + const second_category = ctx.state.query.second_category ?? '' + const categories = await db.$queryRaw`SELECT pry_no_1, first_category, GROUP_CONCAT(CONCAT(pry_no_2, '-', second_category, '-', IF(unit IS NOT NULL, unit, '')) ORDER BY pry_no_2) AS second_category + FROM material_category + WHERE first_category LIKE CONCAT('%', ${first_category}, '%') + GROUP BY pry_no_1, first_category + HAVING second_category LIKE CONCAT('%', ${second_category}, '%') + ORDER BY pry_no_1` + ctx.body = categories.map(item => ({ + pry_no_1: item.pry_no_1, + first_category: item.first_category, + second_categories: item.second_category.split(',').map(sc => { + const info = sc.split('-') + return { + pry_no_2: Number(info[0]), + second_category: info[1], + unit: info[2] || undefined + } + }) + })) + } +) + +configRouter.get( + '/mtrl-first-cat', + authn(CERT_TYPE.ACCOUNT), + async ctx => { + const firstCategories = await db.materialCategory.groupBy({ + by: ['pry_no_1', 'first_category'], + orderBy: { + pry_no_1: 'asc' + } + }) + ctx.body = firstCategories.map(fc => fc.first_category) + } +) + +/** + * 城市信息 + */ + +configRouter.post( + '/city-info', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + pry_no_1: { + type: 'number' + }, + province: { + type: 'string' + }, + cities: { + type: 'array', + items: { + type: 'object', + properties: { + pry_no_2: { + type: 'number' + }, + city: { + type: 'string' + }, + climatic_zone: { + type: 'string' + }, + env_temp: { + type: 'number' + }, + env_hum: { + type: 'number' + } + }, + required: ['city', 'env_temp', 'env_hum'], + additionalProperties: false + }, + minItems: 1 + } + }, + required: ['province', 'cities'], + additionalProperties: false + } + }), + async ctx => { + const { province, cities } = ctx.request.body + if (await db.cityInformation.findFirst({ where: { province } })) { + ctx.throw(422, fail(ERR_CODE.CAT_EXIST, '行政区域已存在')) + return + } + if (isRepeat(cities.map((c: AnyObj) => c.city))) { + ctx.throw(422, fail(ERR_CODE.CAT_DUPL, '城市重复')) + return + } + const agg = await db.cityInformation.aggregate({ + _max: { + pry_no_1: true + } + }) + const nextPryNo1 = (agg._max.pry_no_1 ?? 0) + 1 + ctx.body = await db.cityInformation.createMany({ + data: cities.map((c: AnyObj, idx: number) => ({ + pry_no_1: nextPryNo1, + pry_no_2: idx + 1, + province, + city: c.city, + climatic_zone: c.climatic_zone, + env_temp: c.env_temp, + env_hum: c.env_hum + })) + }) + } +) + +configRouter.put( + '/city-info', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + pry_no_1: { + type: 'number' + }, + province: { + type: 'string' + }, + cities: { + type: 'array', + items: { + type: 'object', + properties: { + pry_no_2: { + type: 'number' + }, + city: { + type: 'string' + }, + climatic_zone: { + type: 'string' + }, + env_temp: { + type: 'number' + }, + env_hum: { + type: 'number' + } + }, + required: ['city', 'env_temp', 'env_hum'], + additionalProperties: false + }, + minItems: 1 + } + }, + required: ['pry_no_1', 'province', 'cities'], + additionalProperties: false + } + }), + async ctx => { + const { pry_no_1, province, cities } = ctx.request.body + if (await db.cityInformation.findFirst({ where: { pry_no_1: { not: pry_no_1 }, province } })) { + ctx.throw(422, fail(ERR_CODE.CAT_EXIST, '行政区域已存在')) + return + } + if (isRepeat(cities.map((c: AnyObj) => c.city))) { + ctx.throw(422, fail(ERR_CODE.CAT_DUPL, '城市重复')) + return + } + const updatedClimaticZones: AnyObj[] = [] + const origCities = await db.cityInformation.findMany({ + where: { + pry_no_1 + } + }) + cities.forEach((c: AnyObj) => { + if (!origCities.find(oc => oc.province == province && oc.city == c.city && oc.climatic_zone == c.climatic_zone)) { + updatedClimaticZones.push(c) + } + }) + const operations = [ + db.cityInformation.deleteMany({ + where: { + pry_no_1 + } + }), + db.cityInformation.createMany({ + data: cities.map((c: AnyObj, idx: number) => ({ + pry_no_1, + pry_no_2: idx + 1, + province, + city: c.city, + climatic_zone: c.climatic_zone, + env_temp: c.env_temp, + env_hum: c.env_hum + })) + }) + ] + updatedClimaticZones.forEach(c => { + operations.push(db.project.updateMany({ + where: { + province, + city: `${province}/${c.city}` + }, + data: { + climatic_zone: c.climatic_zone + } + })) + operations.push(db.publicTemplate.updateMany({ + where: { + province, + city: `${province}/${c.city}` + }, + data: { + climatic_zone: c.climatic_zone + } + })) + operations.push(db.selfTemplate.updateMany({ + where: { + province, + city: `${province}/${c.city}` + }, + data: { + climatic_zone: c.climatic_zone + } + })) + }) + ctx.body = await db.$transaction(operations) + } +) + +configRouter.patch( + '/city-info', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + bfr_no: { + type: 'number' + }, + afr_no: { + type: 'number' + } + }, + required: ['bfr_no', 'afr_no'], + additionalProperties: false + } + }), + async ctx => { + const agg = await db.cityInformation.aggregate({ + _max: { + pry_no_1: true + } + }) + const tempPryNo1 = (agg._max.pry_no_1 ?? 0) + 10 + const bfr_no: number = ctx.request.body.bfr_no + const afr_no: number = ctx.request.body.afr_no + ctx.body = await db.$transaction([ + db.cityInformation.updateMany({ + where: { + pry_no_1: bfr_no + }, + data: { + pry_no_1: tempPryNo1 + } + }), + ( + bfr_no <= afr_no ? + db.$executeRaw`UPDATE city_information SET pry_no_1 = pry_no_1 - 1 WHERE pry_no_1 BETWEEN ${bfr_no} AND ${afr_no}` : + db.$executeRaw`UPDATE city_information SET pry_no_1 = pry_no_1 + 1 WHERE pry_no_1 BETWEEN ${afr_no} AND ${bfr_no}` + ), + db.cityInformation.updateMany({ + where: { + pry_no_1: tempPryNo1 + }, + data: { + pry_no_1: afr_no + } + }), + ]) + } +) + +configRouter.delete( + '/city-info/:pry_no_1', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + async ctx => { + const pry_no_1 = Number(ctx.params.pry_no_1) + ctx.body = await db.cityInformation.deleteMany({ + where: { + pry_no_1 + } + }) + } +) + +configRouter.get( + '/city-info', + authn(CERT_TYPE.ACCOUNT), + validate({ + query: { + type: 'object', + properties: { + province: { + type: 'string' + }, + city: { + type: 'string' + } + }, + additionalProperties: false + } + }), + async ctx => { + const province = ctx.state.query.province ?? '' + const city = ctx.state.query.city ?? '' + const cities = await db.$queryRaw`SELECT pry_no_1, province, GROUP_CONCAT(CONCAT(pry_no_2, '-', city, '-', IF(climatic_zone IS NOT NULL, climatic_zone, ''), '-', env_temp, '-', env_hum) ORDER BY pry_no_2) AS city + FROM city_information + WHERE province LIKE CONCAT('%', ${province}, '%') + GROUP BY pry_no_1, province + HAVING city LIKE CONCAT('%', ${city}, '%') + ORDER BY pry_no_1` + ctx.body = cities.map(item => ({ + pry_no_1: item.pry_no_1, + province: item.province, + cities: item.city.split(',').map(city => { + const info = city.split('-') + return { + pry_no_2: Number(info[0]), + city: info[1], + climatic_zone: info[2] || undefined, + env_temp: toNum(info[3]), + env_hum: toNum(info[4]) + } + }) + })) + } +) + +configRouter.get( + '/province', + authn(CERT_TYPE.ACCOUNT), + async ctx => { + const provinces = await db.cityInformation.groupBy({ + by: ['pry_no_1', 'province'], + orderBy: { + pry_no_1: 'asc' + } + }) + ctx.body = provinces.map(p => p.province) + } +) + +configRouter.get( + '/city-info/by', + authn(CERT_TYPE.ACCOUNT), + validate({ + query: { + type: 'object', + properties: { + city: { + type: 'string' + } + }, + required: ['city'], + additionalProperties: false + } + }), + async ctx => { + ctx.body = await db.cityInformation.findFirst({ + where: { + city: ctx.state.query.city + } + }) + } +) + +/** + * 限值标准 + */ + +configRouter.get( + '/apc-lmt-std', + authn(CERT_TYPE.ACCOUNT), + async ctx => { + ctx.body = await db.apcLimitStandard.findMany() + } +) + +/** + * 落地页内容配置 + */ + +const UPLOADS_DIR = path.join(__dirname, '../../uploads') +const CASE_IMAGES_FILE = path.join(UPLOADS_DIR, 'case-images.json') + +const readCaseImages = (): Record => { + try { return JSON.parse(fs.readFileSync(CASE_IMAGES_FILE, 'utf8')) } catch { return {} } +} + +configRouter.get('/case-images', async ctx => { + ctx.body = readCaseImages() +}) + +configRouter.put( + '/case-images/:index', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + async ctx => { + const idx = ctx.params.index + const { url } = ctx.request.body as { url: string } + if (!url) { ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '缺少 url')); return } + if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR, { recursive: true }) + const data = readCaseImages() + data[idx] = url + fs.writeFileSync(CASE_IMAGES_FILE, JSON.stringify(data), 'utf8') + ctx.body = data + } +) + +configRouter.get( + '/landing', + async ctx => { + const row = await db.siteConfig.findUnique({ where: { key: 'landing_hero' } }) + ctx.body = row ? JSON.parse(row.value) : null + } +) + +configRouter.put( + '/landing', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + image: { type: 'string' } + }, + additionalProperties: false + } + }), + async ctx => { + const data = ctx.request.body + const existing = await db.siteConfig.findUnique({ where: { key: 'landing_hero' } }) + const merged = existing ? { ...JSON.parse(existing.value), ...data } : data + ctx.body = await db.siteConfig.upsert({ + where: { key: 'landing_hero' }, + create: { key: 'landing_hero', value: JSON.stringify(merged) }, + update: { value: JSON.stringify(merged) } + }) + } +) + +configRouter.post( + '/landing/upload', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + async ctx => { + const file = ctx.request.files?.file + if (!file || Array.isArray(file)) { + ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '请上传一个文件')) + return + } + const ext = path.extname(file.originalFilename || '.jpg').toLowerCase() + const allowed = ['.jpg', '.jpeg', '.png', '.webp', '.gif'] + if (!allowed.includes(ext)) { + ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '仅支持 jpg/png/webp/gif 格式')) + return + } + if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR, { recursive: true }) + const filename = `landing_${Date.now()}${ext}` + const dest = path.join(UPLOADS_DIR, filename) + fs.copyFileSync(file.filepath, dest) + fs.unlinkSync(file.filepath) + ctx.body = { url: `/api/cfg/uploads/${filename}` } + } +) + +configRouter.get( + '/landing-news', + async ctx => { + const row = await db.siteConfig.findUnique({ where: { key: 'landing_news' } }) + ctx.body = row ? JSON.parse(row.value) : null + } +) + +configRouter.put( + '/landing-news', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + sectionTitle: { type: 'string' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + tag: { type: 'string' }, + date: { type: 'string' }, + title: { type: 'string' }, + summary: { type: 'string' }, + image: { type: 'string' } + }, + required: ['id', 'title'], + additionalProperties: false + } + } + }, + additionalProperties: false + } + }), + async ctx => { + const data = ctx.request.body + const existing = await db.siteConfig.findUnique({ where: { key: 'landing_news' } }) + const merged = existing ? { ...JSON.parse(existing.value), ...data } : data + ctx.body = await db.siteConfig.upsert({ + where: { key: 'landing_news' }, + create: { key: 'landing_news', value: JSON.stringify(merged) }, + update: { value: JSON.stringify(merged) } + }) + } +) + +// 预测案例内容配置 +configRouter.get('/landing-cases', async ctx => { + const row = await db.siteConfig.findUnique({ where: { key: 'landing_cases' } }) + ctx.body = row ? JSON.parse(row.value) : null +}) + +configRouter.put( + '/landing-cases', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + validate({ + body: { + type: 'object', + properties: { + sectionTitle: { type: 'string' }, + sectionSub: { type: 'string' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string' }, + name: { type: 'string' }, + meta: { type: 'string' }, + w1: { type: 'number' }, + v1: { type: 'string' }, + w2: { type: 'number' }, + v2: { type: 'string' } + }, + required: ['type', 'name'], + additionalProperties: false + } + } + }, + additionalProperties: false + } + }), + async ctx => { + const data = ctx.request.body + const existing = await db.siteConfig.findUnique({ where: { key: 'landing_cases' } }) + const merged = existing ? { ...JSON.parse(existing.value), ...data } : data + ctx.body = await db.siteConfig.upsert({ + where: { key: 'landing_cases' }, + create: { key: 'landing_cases', value: JSON.stringify(merged) }, + update: { value: JSON.stringify(merged) } + }) + } +) + +// 静态文件:提供上传的图片 +configRouter.get( + '/uploads/:filename', + async ctx => { + const filename = ctx.params.filename + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + ctx.status = 400 + return + } + const filepath = path.join(UPLOADS_DIR, filename) + if (!fs.existsSync(filepath)) { + ctx.status = 404 + return + } + const extMap: Record = { + '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' + } + const ext = path.extname(filename).toLowerCase() + ctx.type = extMap[ext] || 'application/octet-stream' + ctx.body = fs.createReadStream(filepath) + } +) + +// 文章内容:上传 Word 文档 +configRouter.post( + '/article/:id/upload-doc', + authn(CERT_TYPE.ADMIN), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_sys_cfg == 1)), + async ctx => { + const articleId = ctx.params.id + if (!articleId || /[^a-zA-Z0-9_-]/.test(articleId)) { + ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '无效的文章 ID')) + return + } + const file = ctx.request.files?.file + if (!file || Array.isArray(file)) { + ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '请上传一个 Word 文档')) + return + } + const ext = path.extname(file.originalFilename || '').toLowerCase() + if (!['.docx', '.doc', '.pdf'].includes(ext)) { + ctx.throw(422, fail(ERR_CODE.INVALID_REQ_DATA, '仅支持 .docx / .doc / .pdf 格式')) + return + } + const imgDir = path.join(UPLOADS_DIR, 'article-images') + if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true }) + + let html = '' + let title = '' + + if (ext === '.pdf') { + const buf = fs.readFileSync(file.filepath) + const pdf = await pdfParse(buf) + const lines = (pdf.text || '').split(/\n/).map((l: string) => l.trim()).filter(Boolean) + title = lines[0] || '' + html = lines.map((l: string, i: number) => { + if (i === 0) return `

${l}

` + if (l.length < 40 && !l.endsWith('。') && !l.endsWith('.')) return `

${l}

` + return `

${l}

` + }).join('\n') + } else { + let imgIdx = 0 + const result = await mammoth.convertToHtml( + { path: file.filepath }, + { + convertImage: mammoth.images.imgElement(async (image: any) => { + const imgExt = image.contentType === 'image/png' ? '.png' : '.jpg' + const imgName = `${articleId}_${Date.now()}_${imgIdx++}${imgExt}` + const imgPath = path.join(imgDir, imgName) + const imgBuf = await image.read() + fs.writeFileSync(imgPath, imgBuf) + return { src: `/api/cfg/uploads/article-images/${imgName}` } + }) + } + ) + html = result.value + const m = html.match(/]*>(.*?)<\/h1>/i) || html.match(/]*>(.*?)<\/h2>/i) || html.match(/]*>(.*?)<\/p>/i) + title = m ? m[1].replace(/<[^>]+>/g, '') : '' + } + fs.unlinkSync(file.filepath) + + const articleDir = path.join(UPLOADS_DIR, 'articles') + if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true }) + fs.writeFileSync(path.join(articleDir, `${articleId}.html`), html, 'utf8') + if (title) { + fs.writeFileSync(path.join(articleDir, `${articleId}.title`), title, 'utf8') + } + ctx.body = { html, title: title || undefined } + } +) + +// 获取文章 HTML 内容 +configRouter.get( + '/article/:id', + async ctx => { + const articleId = ctx.params.id + if (!articleId || /[^a-zA-Z0-9_-]/.test(articleId)) { ctx.status = 204; return } + const htmlPath = path.join(UPLOADS_DIR, 'articles', `${articleId}.html`) + const titlePath = path.join(UPLOADS_DIR, 'articles', `${articleId}.title`) + if (fs.existsSync(htmlPath)) { + const html = fs.readFileSync(htmlPath, 'utf8') + const title = fs.existsSync(titlePath) ? fs.readFileSync(titlePath, 'utf8') : undefined + ctx.body = { html, title } + return + } + const key = `article_body_${articleId}` + const row = await db.siteConfig.findUnique({ where: { key } }) + if (!row) { + ctx.status = 204 + return + } + ctx.body = { html: row.value } + } +) + +// 文章子目录静态文件 +configRouter.get( + '/uploads/:dir/:filename', + async ctx => { + const { dir, filename } = ctx.params + if ([dir, filename].some(s => s.includes('..') || s.includes('/') || s.includes('\\'))) { + ctx.status = 400 + return + } + const filepath = path.join(UPLOADS_DIR, dir, filename) + if (!fs.existsSync(filepath)) { + ctx.status = 404 + return + } + const extMap: Record = { + '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' + } + const e = path.extname(filename).toLowerCase() + ctx.type = extMap[e] || 'application/octet-stream' + ctx.body = fs.createReadStream(filepath) + } +) + +export const configRoutes = configRouter.routes() \ No newline at end of file diff --git a/空气质量预测/源码/用户端/iapip-web/.umirc.ts b/空气质量预测/源码/用户端/iapip-web/.umirc.ts index e3de07a..77408fa 100644 --- a/空气质量预测/源码/用户端/iapip-web/.umirc.ts +++ b/空气质量预测/源码/用户端/iapip-web/.umirc.ts @@ -128,6 +128,7 @@ export default defineConfig({ mfsu: { strategy: 'normal' }, + hash: true, esbuildMinifyIIFE: true, npmClient: 'pnpm' }) diff --git a/空气质量预测/源码/用户端/iapip-web/src/pages/landing/index.tsx b/空气质量预测/源码/用户端/iapip-web/src/pages/landing/index.tsx index f5af085..905c1ed 100644 --- a/空气质量预测/源码/用户端/iapip-web/src/pages/landing/index.tsx +++ b/空气质量预测/源码/用户端/iapip-web/src/pages/landing/index.tsx @@ -1,11 +1,11 @@ import { useEffect, useRef, useState } from 'react' import { history, useModel } from '@umijs/max' -import { Modal, Input, Upload, message, Checkbox } from 'antd' -import { EditOutlined, UploadOutlined, PlusOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons' +import { Modal, Input, InputNumber, Upload, message, Checkbox } from 'antd' +import { EditOutlined, UploadOutlined, PlusOutlined, DeleteOutlined, SearchOutlined, SaveOutlined } from '@ant-design/icons' import { LOC_STOR_KEY } from '@/common/constants' import PhoneAuthModal from '@/components/phone-auth-modal' import api from '@/services/api' -import type { LandingHero, LandingNewsItem } from '@/services/api/config' +import type { LandingHero, LandingNewsItem, LandingCaseItem } from '@/services/api/config' import type { EcoMaterial } from '@/services/api/material' import { articles } from '@/common/articles' import './landing.css' @@ -23,10 +23,10 @@ const ChevronIcon = ({ dir }: { dir: 'left' | 'right' }) => ( ) +const FALLBACK_HERO_IMAGE = '/iapip-web/images/hero-interior.jpg' const DEFAULT_HERO: LandingHero = { title: '装修住得安心,\n先把污染源头挡在门外', - description: '输入房间、环境与所用材料,系统用稳态质量平衡公式预测甲醛、苯、TVOC 等 6 项污染物浓度,判定是否超标,并溯源到具体污染材料,给出整改建议。', - image: '/iapip-web/images/hero-interior.jpg' + description: '输入房间、环境与所用材料,系统用稳态质量平衡公式预测甲醛、苯、TVOC 等 6 项污染物浓度,判定是否超标,并溯源到具体污染材料,给出整改建议。' } const DEFAULT_NEWS_TITLE = '读懂装修污染,从知道哪些是健康材料开始' @@ -40,11 +40,13 @@ const stats = [ { n: '6 项', t: '污染物 · 甲醛/苯/TVOC/氨/氡/VOC' }, { n: '2 部', t: '国标依据 · 18883 / 50325' } ] -const cases = [ +const DEFAULT_CASES: LandingCaseItem[] = [ { type: '住宅 · I类民用建筑', name: '锦绣华庭 · 主卧', meta: '主源:多层实木复合地板 · 人造板衣柜', w1: 82, v1: '0.18', w2: 36, v2: '0.08' }, { type: '住宅 · I类民用建筑', name: '翠湖天地 · 儿童房', meta: '主源:人造板衣柜 · 壁纸基膜', w1: 95, v1: '0.21', w2: 32, v2: '0.07' }, { type: '酒店客房 · II类民用建筑', name: '云栖精选酒店 · 标准间', meta: '主源:木器漆饰面 · 软包', w1: 90, v1: '0.72', w2: 58, v2: '0.46' } ] +const DEFAULT_CASES_TITLE = '预测 → 识别 → 优化,你家的装修健康卫士' +const DEFAULT_CASES_SUB = '真实流程演示:从预测超标,到识别主要污染材料,再到优化复测达标。' const steps = [ { h: '录入房间与材料', p: '选择房间、填写面积层高与通风换气率,勾选所用装修材料及用量。' }, { h: '预测', p: '按稳态质量平衡 C = Σ(EFᵢ·Aᵢ)/(n·V) 计算 6 项污染物浓度,对照国标判定达标。' }, @@ -124,6 +126,60 @@ export default function Landing() { } catch { message.error('上传失败') } } + // --- 预测案例编辑 --- + const [cases, setCases] = useState(DEFAULT_CASES) + const [casesTitle, setCasesTitle] = useState(DEFAULT_CASES_TITLE) + const [casesSub, setCasesSub] = useState(DEFAULT_CASES_SUB) + const [caseEditOpen, setCaseEditOpen] = useState(false) + const [caseEditForm, setCaseEditForm] = useState<{ title: string; sub: string; items: LandingCaseItem[] }>({ title: '', sub: '', items: [] }) + const [caseSaving, setCaseSaving] = useState(false) + + useEffect(() => { + api.config.getLandingCases({ skipErrorHandler: true }).then(data => { + if (data) { + if (data.sectionTitle) setCasesTitle(data.sectionTitle) + if (data.sectionSub) setCasesSub(data.sectionSub) + if (data.items?.length) setCases(data.items) + } + }).catch(() => {}) + }, []) + + const openCaseEdit = () => { + setCaseEditForm({ title: casesTitle, sub: casesSub, items: cases.map(c => ({ ...c })) }) + setCaseEditOpen(true) + } + const updateCaseField = (idx: number, field: keyof LandingCaseItem, val: any) => { + setCaseEditForm(prev => ({ + ...prev, + items: prev.items.map((c, i) => i === idx ? { ...c, [field]: val } : c) + })) + } + const addCase = () => { + setCaseEditForm(prev => ({ + ...prev, + items: [...prev.items, { type: '住宅 · I类民用建筑', name: '新案例', meta: '', w1: 50, v1: '0.10', w2: 30, v2: '0.05' }] + })) + } + const removeCase = (idx: number) => { + setCaseEditForm(prev => ({ ...prev, items: prev.items.filter((_, i) => i !== idx) })) + } + const saveCases = async () => { + setCaseSaving(true) + try { + await api.config.updateLandingCases({ + sectionTitle: caseEditForm.title, + sectionSub: caseEditForm.sub, + items: caseEditForm.items + }) + setCasesTitle(caseEditForm.title) + setCasesSub(caseEditForm.sub) + setCases(caseEditForm.items) + setCaseEditOpen(false) + message.success('预测案例已保存') + } catch { message.error('保存失败') } + finally { setCaseSaving(false) } + } + const openPredict = () => { if (hasToken()) history.push('/home') else setAuthOpen(true) @@ -135,22 +191,32 @@ export default function Landing() { // --- Hero 编辑 --- const [hero, setHero] = useState(DEFAULT_HERO) + const [configReady, setConfigReady] = useState(false) const [editOpen, setEditOpen] = useState(false) const [editForm, setEditForm] = useState({}) const [saving, setSaving] = useState(false) useEffect(() => { - api.config.getLandingConfig({ skipErrorHandler: true }) - .then(data => { if (data) setHero({ ...DEFAULT_HERO, ...data }) }) - .catch(() => {}) - api.config.getLandingNews({ skipErrorHandler: true }) - .then(data => { - if (data) { - if (data.sectionTitle) setNewsTitle(data.sectionTitle) - if (data.items?.length) setNewsItems(data.items) - } - }) - .catch(() => {}) + Promise.all([ + api.config.getLandingConfig({ skipErrorHandler: true }) + .then(data => { + setHero(prev => ({ + ...DEFAULT_HERO, + image: FALLBACK_HERO_IMAGE, + ...(data || {}), + ...(!data?.image ? { image: FALLBACK_HERO_IMAGE } : {}) + })) + }) + .catch(() => { setHero(prev => ({ ...prev, image: FALLBACK_HERO_IMAGE })) }), + api.config.getLandingNews({ skipErrorHandler: true }) + .then(data => { + if (data) { + if (data.sectionTitle) setNewsTitle(data.sectionTitle) + if (data.items?.length) setNewsItems(data.items) + } + }) + .catch(() => {}) + ]).finally(() => setConfigReady(true)) }, []) const openEdit = () => { @@ -248,6 +314,8 @@ export default function Landing() { } } + if (!configReady) return
+ return (
@@ -288,7 +356,7 @@ export default function Landing() {
- 室内装修实景 + {hero.image && 室内装修实景} {isAdmin && ( @@ -348,7 +416,7 @@ export default function Landing() {
-
预测案例

预测 → 识别 → 优化,你家的装修健康卫士

真实流程演示:从预测超标,到识别主要污染材料,再到优化复测达标。

+
预测案例

{casesTitle}{isAdmin && }

{casesSub}

{cases.map((c, i) => ( @@ -552,6 +620,78 @@ export default function Landing() {
+ {/* 预测案例编辑弹窗 */} + setCaseEditOpen(false)} + confirmLoading={caseSaving} + okText="保存" + cancelText="取消" + width={720} + styles={{ body: { maxHeight: '65vh', overflowY: 'auto' } }} + > +
+
+
栏目标题
+ setCaseEditForm(f => ({ ...f, title: e.target.value }))} /> +
+
+
栏目副标题
+ setCaseEditForm(f => ({ ...f, sub: e.target.value }))} /> +
+ + {caseEditForm.items.map((c, idx) => ( +
+
+ 案例 {idx + 1} + {caseEditForm.items.length > 1 && ( + + )} +
+
+
+
建筑类型
+ updateCaseField(idx, 'type', e.target.value)} placeholder="如: 住宅 · I类民用建筑" /> +
+
+
案例名称
+ updateCaseField(idx, 'name', e.target.value)} placeholder="如: 锦绣华庭 · 主卧" /> +
+
+
+
污染源描述
+ updateCaseField(idx, 'meta', e.target.value)} placeholder="如: 主源:多层实木复合地板 · 人造板衣柜" /> +
+
+
+
优化前浓度
+ updateCaseField(idx, 'v1', e.target.value)} placeholder="如: 0.18" /> +
+
+
优化前进度 %
+ updateCaseField(idx, 'w1', v ?? 0)} style={{ width: '100%' }} /> +
+
+
优化后浓度
+ updateCaseField(idx, 'v2', e.target.value)} placeholder="如: 0.08" /> +
+
+
优化后进度 %
+ updateCaseField(idx, 'w2', v ?? 0)} style={{ width: '100%' }} /> +
+
+
+ ))} + + +
+
{/* 环保建材编辑弹窗 */} (`/api/cfg/proj-space-cat`, { + method: 'GET', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + params: query, + ...(options || {}) + }) +} + +export async function getProjSpaceCat(project_category: string, options?: AnyObj) { + return request(`/api/cfg/proj-space-cat/by`, { + method: 'GET', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + params: { + project_category + }, + ...(options || {}) + }) +} + +export async function getMtrlCatList(options?: AnyObj) { + return request(`/api/cfg/mtrl-cat`, { + method: 'GET', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + ...(options || {}) + }) +} + +export type MtrlCatPayload = { + pry_no_1?: number + first_category: string + second_categories: { + second_category: string + unit?: string + }[] +} + +export async function createMtrlCat(body: MtrlCatPayload, options?: AnyObj) { + return request(`/api/cfg/mtrl-cat`, { + method: 'POST', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: body, + ...(options || {}) + }) +} + +export async function updateMtrlCat(body: MtrlCatPayload & { pry_no_1: number }, options?: AnyObj) { + return request(`/api/cfg/mtrl-cat`, { + method: 'PUT', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: body, + ...(options || {}) + }) +} + +export async function deleteMtrlCat(pry_no_1: number, options?: AnyObj) { + return request(`/api/cfg/mtrl-cat/${pry_no_1}`, { + method: 'DELETE', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + ...(options || {}) + }) +} + +export async function getCityInfoList(query?: { province?: string; city?: string }, options?: AnyObj) { + return request(`/api/cfg/city-info`, { + method: 'GET', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + params: query, + ...(options || {}) + }) +} + +export async function getCityInfo(city: string, options?: AnyObj) { + return request<{ + province: string + city: string + climatic_zone?: string + env_temp: number + env_hum: number + } | null>(`/api/cfg/city-info/by`, { + method: 'GET', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + params: { + city + }, + ...(options || {}) + }) +} + +export async function getAPCLmtStdList(options?: AnyObj) { + return request(`/api/cfg/apc-lmt-std`, { + method: 'GET', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + ...(options || {}) + }) +} + +export type LandingHero = { + title?: string + description?: string + image?: string +} + +export async function getLandingConfig(options?: AnyObj) { + return request('/api/cfg/landing', { + method: 'GET', + ...(options || {}) + }) +} + +export async function updateLandingConfig(body: LandingHero, options?: AnyObj) { + return request('/api/cfg/landing', { + method: 'PUT', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: body, + ...(options || {}) + }) +} + +export type LandingNewsItem = { + id: string + tag?: string + date?: string + title: string + summary?: string + image?: string +} + +export type LandingNews = { + sectionTitle?: string + items?: LandingNewsItem[] +} + +export async function getLandingNews(options?: AnyObj) { + return request('/api/cfg/landing-news', { + method: 'GET', + ...(options || {}) + }) +} + +export async function updateLandingNews(body: LandingNews, options?: AnyObj) { + return request('/api/cfg/landing-news', { + method: 'PUT', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: body, + ...(options || {}) + }) +} + +export async function getArticleContent(id: string, options?: AnyObj) { + return request<{ html: string } | null>(`/api/cfg/article/${id}`, { + method: 'GET', + ...(options || {}) + }) +} + +export async function uploadArticleDoc(id: string, file: File) { + const formData = new FormData() + formData.append('file', file) + return request<{ html: string; title?: string }>(`/api/cfg/article/${id}/upload-doc`, { + method: 'POST', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: formData + }) +} + +export async function getCaseImages(options?: any) { + return request>('/api/cfg/case-images', { method: 'GET', ...(options || {}) }) +} + +export async function setCaseImage(index: number, url: string) { + return request>(`/api/cfg/case-images/${index}`, { + method: 'PUT', + headers: { [HTTP_HEADER.AUTHORIZATION]: getLocalToken() }, + data: { url } + }) +} + +export type LandingCaseItem = { + type: string + name: string + meta?: string + w1?: number + v1?: string + w2?: number + v2?: string +} + +export type LandingCases = { + sectionTitle?: string + sectionSub?: string + items?: LandingCaseItem[] +} + +export async function getLandingCases(options?: AnyObj) { + return request('/api/cfg/landing-cases', { + method: 'GET', + ...(options || {}) + }) +} + +export async function updateLandingCases(body: LandingCases, options?: AnyObj) { + return request('/api/cfg/landing-cases', { + method: 'PUT', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: body, + ...(options || {}) + }) +} + +export async function uploadLandingImage(file: File) { + const formData = new FormData() + formData.append('file', file) + return request<{ url: string }>('/api/cfg/landing/upload', { + method: 'POST', + headers: { + [HTTP_HEADER.AUTHORIZATION]: getLocalToken() + }, + data: formData + }) +}