diff --git a/palette.js b/palette.js new file mode 100644 index 0000000..a8504cb --- /dev/null +++ b/palette.js @@ -0,0 +1,170 @@ +module.exports = { + red: { + 1: '#FFECE8', + 2: '#FDCDC5', + 3: '#FBACA3', + 4: '#F98981', + 5: '#F76560', + 6: '#F53F3F', + 7: '#CB272D', + 8: '#A1151E', + 9: '#770813', + 10: '#4D000A' + }, + orangered: { + 1: '#FFF3E8', + 2: '#FDDDC3', + 3: '#FCC59F', + 4: '#FAAC7B', + 5: '#F99057', + 6: '#F77234', + 7: '#CC5120', + 8: '#A23511', + 9: '#771F06', + 10: '#4D0E00' + }, + orange: { + 1: '#FFF7E8', + 2: '#FFE4BA', + 3: '#FFCF8B', + 4: '#FFB65D', + 5: '#FF9A2E', + 6: '#FF7D00', + 7: '#D25F00', + 8: '#A64500', + 9: '#792E00', + 10: '#4D1B00' + }, + gold: { + 1: '#FFFCE8', + 2: '#FDF4BF', + 3: '#FCE996', + 4: '#FADC6D', + 5: '#F9CC45', + 6: '#F7BA1E', + 7: '#CC9213', + 8: '#A26D0A', + 9: '#774B04', + 10: '#4D2D00' + }, + yellow: { + 1: '#FEFFE8', + 2: '#FEFEBE', + 3: '#FDFA94', + 4: '#FCF26B', + 5: '#FBE842', + 6: '#FADC19', + 7: '#CFAF0F', + 8: '#A38408', + 9: '#785D03', + 10: '#4D3800' + }, + lime: { + 1: '#FCFFE8', + 2: '#EDF8BB', + 3: '#DCF190', + 4: '#C9E968', + 5: '#B5E241', + 6: '#9FDB1D', + 7: '#7EB712', + 8: '#5F940A', + 9: '#437004', + 10: '#2A4D00' + }, + green: { + 1: '#E8FFEA', + 2: '#AFF0B5', + 3: '#7BE188', + 4: '#4CD263', + 5: '#23C343', + 6: '#00B42A', + 7: '#009A29', + 8: '#008026', + 9: '#006622', + 10: '#004D1C' + }, + sggreen: { + 1: '#DCF5E9', + 2: '#95E8C1', + 3: '#69DBAA', + 4: '#42CF96', + 5: '#1FC286', + 6: '#00B578', + 7: '#008F64', + 8: '#00694D', + 9: '#004233', + 10: '#001C16' + }, + cyan: { + 1: '#E8FFFB', + 2: '#B7F4EC', + 3: '#89E9E0', + 4: '#5EDFD6', + 5: '#37D4CF', + 6: '#14C9C9', + 7: '#0DA5AA', + 8: '#07828B', + 9: '#03616C', + 10: '#00424D' + }, + blue: { + 1: '#E8F7FF', + 2: '#C3E7FE', + 3: '#9FD4FD', + 4: '#7BC0FC', + 5: '#57A9FB', + 6: '#3491FA', + 7: '#206CCF', + 8: '#114BA3', + 9: '#063078', + 10: '#001A4D' + }, + arcoblue: { + 1: '#E8F3FF', + 2: '#BEDAFF', + 3: '#94BFFF', + 4: '#6AA1FF', + 5: '#4080FF', + 6: '#165DFF', + 7: '#0E42D2', + 8: '#072CA6', + 9: '#031A79', + 10: '#000D4D' + }, + purple: { + 1: '#F5E8FF', + 2: '#DDBEF6', + 3: '#C396ED', + 4: '#A871E3', + 5: '#8D4EDA', + 6: '#722ED1', + 7: '#551DB0', + 8: '#3C108F', + 9: '#27066E', + 10: '#16004D' + }, + magenta: { + 1: '#FFE8F1', + 2: '#FDC2DB', + 3: '#FB9DC7', + 4: '#F979B7', + 5: '#F754A8', + 6: '#F5319D', + 7: '#CB1E83', + 8: '#A11069', + 9: '#77064F', + 10: '#4D0034' + }, + gray: { + 1: '#f7f8fa', + 2: '#f2f3f5', + 3: '#e5e6eb', + 4: '#c9cdd4', + 5: '#a9aeb8', + 6: '#86909c', + 7: '#6b7785', + 8: '#4e5969', + 9: '#272e3b', + 10: '#1d2129' + } +}; diff --git a/src/components/CompanyNoPark/index.tsx b/src/components/CompanyNoPark/index.tsx new file mode 100644 index 0000000..4e2055c --- /dev/null +++ b/src/components/CompanyNoPark/index.tsx @@ -0,0 +1,54 @@ +import { BaseResponse } from "@/shared/model-components"; +import {tenementSearchChoice, tenementSearchChoiceByName} from "@q/charge"; +import { useQuery } from "@tanstack/react-query"; +import { isCorrectResult } from "@u/asyncs"; +import { Select } from "antd"; +import { equals } from "ramda"; +import { useState } from "react"; +import { useDebounce } from "react-use"; +import 'twin.macro' + +interface P { + parkId?: string, + disabled?: boolean + allowClear?: boolean, + placeholder?: string, + onChange?: (e: any) => void, + api?: (keyword: string) => Promise +} + + +const CompanyNoPark = (props: P) => { + const [searchTenement, setSearchTenement] = useState(''); + const [searchTenementTemp, setSearchTenementTemp] = useState(''); + + // 商户列表 + const { data: tenements, status: tenementStatus } = useQuery( + ['tenements-getSelectList', searchTenement], + () => props.api ? props.api(searchTenement) : props.parkId ? tenementSearchChoice(props.parkId, searchTenement) : tenementSearchChoiceByName(searchTenement), + { enabled: !!searchTenement, select: res => (isCorrectResult(res) ? res.tenements : []) } + ) + + + useDebounce(() => setSearchTenement(searchTenementTemp), 800, [searchTenementTemp]); + return ( + { props.onChange && props.onChange(e) }} + onSearch={setSearchTenementTemp} + defaultActiveFirstOption={false} + value={props.tenement} + placeholder={props.placeholder || "请输入商户名称检索"} + options={(tenements || []).map(d => ({ value: props.valueKey ? d[props.valueKey]: d.id, label: props.labelKey ? d[props.labelKey] : d.fullName }))} + /> + ) +} + +export default Company; diff --git a/src/components/hoc/AuthRequired.tsx b/src/components/hoc/AuthRequired.tsx new file mode 100644 index 0000000..c43a4fd --- /dev/null +++ b/src/components/hoc/AuthRequired.tsx @@ -0,0 +1,147 @@ +//@ts-nocheck +import { useAuthenticated } from '@h/useAuthenticated'; +import { useAuthorization } from '@h/useAuthorization'; +import { isEmpty, mergeDeepLeft } from 'ramda'; +import { Component, FunctionComponent, PropsWithChildren } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; + +/** + * 用于配置高阶组件`requireAuthorize`的认证及权限配置。 + * + * 当配置项`all`被设置的时候,配置项`any`的内容将被忽略。如果不设置任何`all`或者`any`配置项,那么将仅判断用户是否已经登录。 + */ +export interface AuthorizeOptions { + /** + * 配置指定组件要求用户必须拥有的全部权限。 + */ + all?: number[]; + + /** + * 配置指定组件要求用户必须至少拥有其中一项的权限。 + */ + any?: number[]; + + /** + * 设定当用户权限认证失败或者用户没有登录的时候,是否跳转到登录页面。 + */ + toLogin?: boolean; +} + +/** + * 对被包裹组件进行用户权限验证的组件。 + * ! 用户如果没有登录,那么验证将始终是失败的。 + * + * @param param0 认证成功状态下路由转移到的组件以及所需要的权限声明 + * @returns 转向实际路由或者重定向到登录页面 + */ +export const AuthRequired: FC> = ({ + all, + any, + toLogin, + children +}: PropsWithChildren) => { + const requiredAllPrivileges = all ?? []; + const requiredAnyPrivileges = any ?? []; + const redirectToLogin = toLogin ?? false; + const isAuthenticated = useAuthenticated(); + const { hasAll, hasAny } = useAuthorization(); + const location = useLocation(); + + // 注意,这里的判断逻辑是: + // 如果所需全部权限和任意权限都为空,即不需要任何权限,那么判断为通过; + // 如果所需全部权限为空,那么取任意权限的判定; + // 如果全部权限不为空,那么取全部权限的判断。 + const isUserMatchNeeds = + (isEmpty(requiredAllPrivileges) && isEmpty(requiredAnyPrivileges)) || + (isEmpty(requiredAllPrivileges) + ? hasAny(...requiredAnyPrivileges) + : hasAll(...requiredAllPrivileges)); + + if (isAuthenticated && isUserMatchNeeds) { + return children; + } else { + return redirectToLogin ? ( + + ) : ( + + ); + } +}; + +/** + * 动态用户权限验证的高阶组件。 + * ! 用户如果没有登录,那么验证将始终是失败的。 + * + * @param Comp 需要被检验授权的组件 + * @param options 访问组件所需要的权限组合 + * @returns 新生成的具备动态判定权限的新组件 + */ +export const requireAuthorize = ( + Comp: Component | FunctionComponent, + options?: AuthorizeOptions +): FunctionComponent => { + const altedOptions = mergeDeepLeft(options, { any: [], all: [] }); + return props => { + const isAuthenticated = useAuthenticated(); + const { hasAll, hasAny } = useAuthorization(); + const location = useLocation(); + + const requiredAllPrivileges: string[] = altedOptions.all; + const requiredAnyPrivileges: string[] = altedOptions.any; + const redirectToLogin = altedOptions.toLogin ?? false; + + // 注意,这里的判断逻辑是: + // 如果所需全部权限和任意权限都为空,即不需要任何权限,那么判断为通过; + // 如果所需全部权限为空,那么取任意权限的判定; + // 如果全部权限不为空,那么取全部权限的判断。 + const isUserMatchNeeds = + (isEmpty(requiredAllPrivileges) && isEmpty(requiredAnyPrivileges)) || + (isEmpty(requiredAllPrivileges) + ? hasAny(...requiredAnyPrivileges) + : hasAll(...requiredAllPrivileges)); + + if (isAuthenticated && isUserMatchNeeds) { + return ; + } else { + return redirectToLogin ? ( + + ) : ( + + ); + } + }; +}; + +/** + * 包裹需要根据用户所拥有的权限展示的组件。用户在满足给定的权限条件时,被包裹的组件方能被展示。 + * ! 用户如果没有登录,那么验证将始终是失败的。 + * + * @param param0 认证成功状态下展示被包裹的组件所需要的权限定义 + * @returns 输出被包裹的组件,或者null + */ +export const WithAuth: FC> = ({ + all, + any, + children +}: PropsWithChildren) => { + const requiredAllPrivileges = all ?? []; + const requiredAnyPrivileges = any ?? []; + const isAuthenticated = useAuthenticated(); + const { hasAll, hasAny } = useAuthorization(); + + // 注意,这里的判断逻辑是: + // 如果所需全部权限和任意权限都为空,即不需要任何权限,那么判断为通过; + // 如果所需全部权限为空,那么取任意权限的判定; + // 如果全部权限不为空,那么取全部权限的判断。 + const isUserMatchNeeds = + (isEmpty(requiredAllPrivileges) && isEmpty(requiredAnyPrivileges)) || + (isEmpty(requiredAllPrivileges) + ? hasAny(...requiredAnyPrivileges) + : hasAll(...requiredAllPrivileges)); + + if (isAuthenticated && isUserMatchNeeds) { + return children; + } + + return null; +}; diff --git a/src/components/hoc/suspense.tsx b/src/components/hoc/suspense.tsx new file mode 100644 index 0000000..8862549 --- /dev/null +++ b/src/components/hoc/suspense.tsx @@ -0,0 +1,31 @@ +//@ts-nocheck +import { CentralSpin } from '@c/ui/CentralSpin'; +import { lazyLoad } from '@u/lazyload'; +import { prop } from 'ramda'; +import { FC, Suspense } from 'react'; + +/** + * 为异步加载组件套上一层统一的加载指示。 + * @param componentName 要导入的组件名称 + * @param loader 要导入的组件所在文件 + * @returns 在外部套上一层加载指示的异步加载组件。 + */ +export const suspense = (componentName: string, loader: () => Promise): FC => { + const Component = prop(componentName, lazyLoad(loader)); + return props => ( + }> + + + ); +}; + +/** + * 异步加载并导入指定的组件,将直接解析出指定组件,不做其他任何处理。 + * @param componentName 要导入的组件名称 + * @param loader + * @returns 导入的指定组件 + */ +export const asyncLoad = (componentName: string, loader: () => Promise): FC => { + const Component = prop(componentName, lazyLoad(loader)); + return props => ; +}; diff --git a/src/components/meter/index.tsx b/src/components/meter/index.tsx new file mode 100644 index 0000000..0136337 --- /dev/null +++ b/src/components/meter/index.tsx @@ -0,0 +1,54 @@ +// 用于生成一个根据关键字搜索商户的输入选择框 +import { meter04List } from "@q/park"; + +import { useQuery } from "@tanstack/react-query"; +import { isCorrectResult } from "@u/asyncs"; +import { Select } from "antd"; +import { equals } from "ramda"; +import { useState } from "react"; +import { useDebounce } from "react-use"; +import 'twin.macro' + +interface P { + parkId?: string, + Meter?: string, + disabled?: boolean + allowClear?: boolean + placeholder?: string + onChange?: (e) => void, + valueKey?: string, + labelKey?: string, +} + + +const Meter = (props: P) => { + const { parkId } = props; + const [searchMeter, setSearchMeter] = useState(''); + const [searchMeterTemp, setSearchMeterTemp] = useState(''); + // 商户列表 + const { data: Meters, status: MeterStatus } = useQuery( + ['meter-select-list', parkId, searchMeter], + () => { return meter04List(parkId, 1, searchMeter, undefined)}, + { enabled: !!searchMeter, select: res => (isCorrectResult(res) ? res.meters : []) } + ); + + useDebounce(() => setSearchMeter(searchMeterTemp), 800, [searchMeterTemp]); + return ( +