This commit is contained in:
eleanor.mao
2025-06-03 00:03:08 +08:00
commit 48f2c49c2b
100 changed files with 34596 additions and 0 deletions

9
src/access.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* @see https://umijs.org/docs/max/access#access
* */
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {};
return {
canAdmin: !!currentUser,
};
}

122
src/app.tsx Normal file
View File

@ -0,0 +1,122 @@
import { LinkOutlined } from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from '@umijs/max';
import { Link, history } from '@umijs/max';
import Footer from '@/components/footer';
import { AvatarDropdown, AvatarName } from '@/components/right-content/avatar-dropdown';
import { LOGIN_PATH } from '@/constants/global';
import { requestConfig } from '@/requestConfig';
import { currentUser as queryCurrentUser } from '@/services/user';
import { gotoLogin } from '@/utils/login';
import defaultSettings from '../config/defaultSettings';
const isDev = process.env.NODE_ENV === 'development';
/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
const fetchUserInfo = async () => {
try {
const userInfo = await queryCurrentUser({
skipErrorHandler: true,
});
return userInfo;
} catch (error) {
gotoLogin();
}
return undefined;
};
// 如果不是登录页面,执行
const { location } = history;
if (location.pathname !== LOGIN_PATH) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
return {
actionsRender: () => [],
avatarProps: {
src:
initialState?.currentUser?.avatar ||
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
title: <AvatarName />,
render: (_, avatarChildren) => {
return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
},
},
waterMarkProps: {
content: initialState?.currentUser?.userName,
},
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== LOGIN_PATH) {
gotoLogin();
}
},
links: isDev
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined />
<span>OpenAPI </span>
</Link>,
]
: [],
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: children => {
// if (initialState?.loading) return <PageLoading />;
return (
<>
{children}
{isDev && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={settings => {
setInitialState(preInitialState => ({
...preInitialState,
settings,
}));
}}
/>
)}
</>
);
},
...initialState?.settings,
};
};
/**
* @name request 配置,可以配置错误处理
* 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
* @doc https://umijs.org/docs/max/request#配置
*/
export const request = {
...requestConfig,
};

View File

@ -0,0 +1,13 @@
import { DefaultFooter } from '@ant-design/pro-components';
import React from 'react';
const Footer: React.FC = () => {
return (
<DefaultFooter
style={{ background: 'none' }}
copyright="杭州播络科技有限公司"
/>
);
};
export default Footer;

View File

@ -0,0 +1,27 @@
import { Dropdown } from 'antd';
import type { DropDownProps } from 'antd/es/dropdown';
import { createStyles } from 'antd-style';
import classNames from 'classnames';
import React from 'react';
const useStyles = createStyles(({ token }) => {
return {
dropdown: {
[`@media screen and (max-width: ${token.screenXS}px)`]: {
width: '100%',
},
},
};
});
export type HeaderDropdownProps = {
overlayClassName?: string;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => {
const { styles } = useStyles();
return <Dropdown overlayClassName={classNames(styles.dropdown, cls)} {...restProps} />;
};
export default HeaderDropdown;

View File

@ -0,0 +1,49 @@
import { Image } from 'antd';
import { createStyles } from 'antd-style';
import { ReactElement } from 'react';
interface IProps {
videos: API.MaterialVideoInfo[];
}
const useStyles = createStyles(() => {
return {
container: {
width: 330,
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginLeft: '-10px',
marginBottom: '-10px',
},
imageContainer: {
marginLeft: '10px',
marginBottom: '10px',
},
};
});
const Previewer = (props: IProps) => {
const { videos = [] } = props;
const { styles } = useStyles();
const imageRender = (originalNode: ReactElement, info: { current: number }) => {
const video = videos[info.current];
if (!video || video.type === 'image') {
return originalNode;
}
// console.log('============>>>>>>', info);
return <video controls autoPlay width="400px" src={video.url} />;
};
return (
<div className={styles.container}>
<Image.PreviewGroup preview={{ imageRender, toolbarRender: () => null, destroyOnClose: true }}>
{videos.map(video => (
<Image key={video.coverUrl} width={100} src={video.coverUrl} wrapperClassName={styles.imageContainer} />
))}
</Image.PreviewGroup>
</div>
);
};
export default Previewer;

View File

@ -0,0 +1,125 @@
import { stringify } from 'querystring';
import { LogoutOutlined } from '@ant-design/icons';
import { history, useModel } from '@umijs/max';
import { Spin } from 'antd';
import { createStyles } from 'antd-style';
import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react';
import { flushSync } from 'react-dom';
import { outLogin } from '@/services/user';
import { clearToken } from '@/utils/login';
import HeaderDropdown from '../header-dropdown';
export type GlobalHeaderRightProps = {
menu?: boolean;
children?: React.ReactNode;
};
export const AvatarName = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
return <span className="anticon">{currentUser?.userName}</span>;
};
const useStyles = createStyles(({ token }) => {
return {
action: {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
alignItems: 'center',
padding: '0 8px',
cursor: 'pointer',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
},
};
});
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ children }) => {
/**
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => {
await outLogin();
clearToken();
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: pathname + search,
}),
});
}
};
const { styles } = useStyles();
const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick = useCallback(
(event: MenuInfo) => {
const { key } = event;
if (key === 'logout') {
flushSync(() => {
setInitialState(s => ({ ...s, currentUser: undefined }));
});
loginOut();
return;
}
},
[setInitialState],
);
const loading = (
<span className={styles.action}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
if (!initialState) {
return loading;
}
const { currentUser } = initialState;
if (!currentUser || !currentUser.userName) {
return loading;
}
const menuItems = [
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
return (
<HeaderDropdown
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
>
{children}
</HeaderDropdown>
);
};

4
src/constants/anchor.ts Normal file
View File

@ -0,0 +1,4 @@
export enum AnchorStatusType {
Normal = 0,
Forbid = 1,
}

28
src/constants/api.ts Normal file
View File

@ -0,0 +1,28 @@
export enum AdminAPI {
// 用户级别
LOGIN = '/api/bo/adminUser/login', // 登录
OUT_LOGIN = '/api/bo/adminUser/outLogin',
USER = '/api/bo/adminUser/get',
// 群
GROUP_LIST = '/api/bo/imGroup/list',
GROUP_UPDATE = '/api/bo/imGroup/update',
// 通告
JOB_LIST = '/api/bo/job/list',
JOB_UPDATE = '/api/bo/job/update',
// 定制群
ANCHOR_GROUP_LIST = '/api/bo/myGroup/list',
ADD_ANCHOR_GROUP = '/api/bo/product/addGroup',
// 职位发布人
PUBLISHER_LIST = '/api/bo/publisher/list',
PUBLISHER_DECLARATION_LIST = '/api/bo/publisher/listWithDeclaration',
PUBLISHER_UPDATE = '/api/bo/publisher/update',
// 主播
ANCHOR_LIST = '/api/bo/user/list',
ANCHOR_UPDATE = '/api/bo/user/update',
// 主播报单(解锁)
DECLARATION_LIST = '/api/bo/declaration/list',
DECLARATION_UPDATE = '/api/bo/declaration/update',
// 模卡
MATERIAL_LIST = '/api/bo/resume/list',
MATERIAL_UPDATE = '/api/bo/resume/update',
}

3908
src/constants/city.ts Normal file

File diff suppressed because it is too large Load Diff

2
src/constants/global.ts Normal file
View File

@ -0,0 +1,2 @@
export const LOGIN_PATH = '/user/login';
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';

19
src/constants/http.ts Normal file
View File

@ -0,0 +1,19 @@
export enum HTTP_STATUS {
SUCCESS = 200,
FAIL = 417,
}
export enum RESPONSE_ERROR_CODE {
INVALID_PARAMETER = 'INVALID_PARAMETER',
NOT_FUND = 'NOT_FUND',
INNER_EXCEPTION = 'INNER_EXCEPTION',
SYSTEM_ERROR = 'SYSTEM_ERROR',
NEED_LOGIN = 'NEED_LOGIN',
}
export const RESPONSE_ERROR_MESSAGE = {
[RESPONSE_ERROR_CODE.INVALID_PARAMETER]: '参数无效',
[RESPONSE_ERROR_CODE.NOT_FUND]: '找到对应资源',
[RESPONSE_ERROR_CODE.INNER_EXCEPTION]: '服务内部错误',
[RESPONSE_ERROR_CODE.SYSTEM_ERROR]: '系统错误',
};

24
src/constants/job.ts Normal file
View File

@ -0,0 +1,24 @@
export enum JobType {
All = 'ALL',
Finery = 'FINERY', // 服饰
Makeups = 'MAKEUPS', // 美妆
Digital = 'DIGITAL', //数码
Foods = 'FOODS', // 食品酒饮
Jewelry = 'JEWELRY', // 珠宝
Appliance = 'APPLIANCE', // 家电
Furniture = 'FURNITURE', // 日用家具
PetFamily = 'PET_FAMILY', // 母婴宠物
Luxury = 'LUXURY', // 奢品
}
export enum EmployType {
All = 'ALL',
Full = 'FULL_TIME',
Part = 'PARTY_TIME',
}
export const EMPLOY_OPTIONS = [
{ label: '全职', value: EmployType.Full },
{ label: '兼职', value: EmployType.Part },
{ label: '不限', value: EmployType.All },
];

37
src/constants/material.ts Normal file
View File

@ -0,0 +1,37 @@
export enum WorkedYears {
LessOneYear = 0.5,
OneYear = 1,
TwoYear = 2,
MoreThreeYear = 3,
}
export enum GenderType {
MEN = 0,
WOMEN = 1,
}
// 1主播主动创建 2主播填写表单创建 3 运营人工创建 4 机器人创建
export enum ProfileCreateSource {
User = 1,
UserInput = 2,
Bl = 3,
Robot = 4,
}
export enum StyleType {
Broadcasting = 1,
HoldOrder = 2,
Passion = 3,
}
export enum MaterialStatus {
Open = 0,
Close = 1,
}
export const WORK_YEAR_LABELS = {
[WorkedYears.LessOneYear]: '1 年以下',
[WorkedYears.OneYear]: '1 年',
[WorkedYears.TwoYear]: '2 年',
[WorkedYears.MoreThreeYear]: '3 年以上',
};

13
src/constants/product.ts Normal file
View File

@ -0,0 +1,13 @@
export enum PayType {
Free = 'free',
VX = 'vx',
AliPay = 'alipay',
Other = 'other',
}
export enum DeclarationType {
// 直接连接通告主
Direct = 0,
// 客服联系 customer service
CS = 1,
}

52
src/global.less Normal file
View File

@ -0,0 +1,52 @@
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
left: unset;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: 768px) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}

93
src/global.tsx Normal file
View File

@ -0,0 +1,93 @@
import '@umijs/max';
import { Button, message, notification } from 'antd';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
const isHttps = document.location.protocol === 'https:';
const clearCache = () => {
// remove all caches
if (window.caches) {
caches
.keys()
.then(keys => {
keys.forEach(key => {
caches.delete(key);
});
})
.catch(e => console.log(e));
}
};
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning('当前处于离线状态');
});
// Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', (event: Event) => {
const e = event as CustomEvent;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail && e.detail.waiting;
if (!worker) {
return true;
}
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = msgEvent => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage(
{
type: 'skip-waiting',
},
[channel.port2],
);
});
clearCache();
window.location.reload();
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.destroy(key);
reloadSW();
}}
>
{'刷新'}
</Button>
);
notification.open({
message: '有新内容',
description: '请点击“刷新”按钮或者手动刷新页面',
btn,
key,
onClose: async () => null,
});
});
} else if ('serviceWorker' in navigator && isHttps) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
serviceWorker.getRegistrations().then(sws => {
sws.forEach(sw => {
sw.unregister();
});
});
}
serviceWorker.getRegistration().then(sw => {
if (sw) sw.unregister();
});
clearCache();
}

22
src/manifest.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "boluo",
"short_name": "boluo",
"display": "standalone",
"start_url": "./?utm_source=homescreen",
"theme_color": "#002140",
"background_color": "#001529",
"icons": [
{
"src": "icons/icon-192x192.png",
"sizes": "192x192"
},
{
"src": "icons/icon-128x128.png",
"sizes": "128x128"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512"
}
]
}

16
src/pages/404.tsx Normal file
View File

@ -0,0 +1,16 @@
import { history } from '@umijs/max';
import { Button, Result } from 'antd';
import React from 'react';
const NoFoundPage: React.FC = () => (
<Result
status="404"
title="404"
subTitle={'抱歉,您访问的页面不存在。'}
extra={
<Button type="primary" onClick={() => history.push('/')}>
{'返回首页'}
</Button>
}
/>
);
export default NoFoundPage;

View File

@ -0,0 +1,95 @@
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { PageContainer, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import dayjs from 'dayjs';
import React, { useRef } from 'react';
import { TIME_FORMAT } from '@/constants/global';
import { getAnchorGroupList } from '@/services/list';
const STATUS_OPTIONS = [
{ label: '已申请', value: 0 },
{ label: '已申请未进群 ', value: 1 },
{ label: '已进群 ', value: 2 },
];
const TableList: React.FC = () => {
const actionRef = useRef<ActionType>();
const columns: ProColumns<API.AnchorGroupListItem>[] = [
{
title: '主播昵称',
dataIndex: 'nickName',
valueType: 'textarea',
search: false,
},
{
title: '主播ID',
dataIndex: 'userId',
valueType: 'textarea',
copyable: true,
},
{
title: '主播手机号',
dataIndex: 'userPhone',
valueType: 'textarea',
copyable: true,
},
{
title: '群ID',
dataIndex: 'blGroupId',
valueType: 'textarea',
copyable: true,
},
{
title: '群名称',
dataIndex: 'imGroupNick',
valueType: 'textarea',
search: false,
copyable: true,
},
{
title: '加群状态',
dataIndex: 'status',
valueType: 'textarea',
renderText(status: number) {
return STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '绑定时间',
dataIndex: 'created',
valueType: 'dateTime',
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
// search: {
// transform: (created: string) => dayjs(created).valueOf().toString(),
// },
},
{
title: '绑定人',
dataIndex: 'creator',
valueType: 'textarea',
copyable: true,
},
];
return (
<PageContainer>
<ProTable<API.AnchorGroupListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getAnchorGroupList}
columns={columns}
/>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,147 @@
import type { ActionType, ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, PageContainer, ProFormSelect, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import { CITY_CODE_TO_NAME_MAP, CITY_OPTIONS } from '@/constants/city';
import { TIME_FORMAT } from '@/constants/global';
import { getAnchorList, updateAnchorInfo } from '@/services/list';
const STATUS_OPTIONS = [
{ label: '正常', value: 0 },
{ label: '封禁', value: 1 },
];
const TableList: React.FC = () => {
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.AnchorListItem>();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const columns: ProColumns<API.AnchorListItem>[] = [
{
title: '主播昵称',
dataIndex: 'nickName',
valueType: 'textarea',
},
{
title: '主播ID',
dataIndex: 'userId',
valueType: 'textarea',
copyable: true,
},
{
title: '主播手机号',
dataIndex: 'userPhone',
valueType: 'textarea',
copyable: true,
},
{
title: '注册时间',
dataIndex: 'created',
valueType: 'dateTime',
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
// search: {
// transform: (created: string) => dayjs(created).valueOf().toString(),
// },
},
{
title: '最后登录时间',
dataIndex: 'lastLoginDate',
valueType: 'textarea',
search: false,
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
},
{
title: '账号状态',
dataIndex: 'status',
valueType: 'textarea',
renderText(status: number) {
return STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '城市',
dataIndex: 'city',
valueType: 'textarea',
renderText(cityCode: string) {
return CITY_CODE_TO_NAME_MAP.get(cityCode);
},
renderFormItem() {
return (
<Select
showSearch
allowClear
options={CITY_OPTIONS}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
);
},
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
),
},
];
return (
<PageContainer>
<ProTable<API.AnchorListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getAnchorList}
columns={columns}
/>
<ModalForm
title="更新主播信息"
width="400px"
formRef={formRef}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async data => {
const params: API.UpdateAnchorParams = {
userId: currentRow!.userId,
status: data.status,
};
console.log('update confirm', data, params);
try {
await updateAnchorInfo(params);
actionRef.current?.reload();
formRef.current?.resetFields();
} catch (e) {}
handleUpdateModalOpen(false);
}}
>
<ProFormSelect
name="status"
label="账号状态"
options={STATUS_OPTIONS}
rules={[{ required: true, message: '必填项' }]}
/>
</ModalForm>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,228 @@
import type { ActionType, ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, PageContainer, ProFormSelect, ProFormTextArea, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import { CITY_CODE_TO_NAME_MAP, CITY_OPTIONS } from '@/constants/city';
import { TIME_FORMAT } from '@/constants/global';
import { DeclarationType } from '@/constants/product';
import { getDeclarationList, updateDeclarationInfo } from '@/services/list';
const STATUS_OPTIONS = [
{ label: '待处理', value: 0 },
{ label: '已处理', value: 1 },
];
const TYPE_OPTIONS = [
{ label: '直接联系', value: DeclarationType.Direct },
{ label: '客服联系', value: DeclarationType.CS },
];
const WE_COM_OPTIONS = [
{ label: '未加', value: 0 },
{ label: '已加', value: 1 },
];
const TableList: React.FC = () => {
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.DeclarationListItem>();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const columns: ProColumns<API.DeclarationListItem>[] = [
{
title: '主播昵称',
dataIndex: 'nickName',
valueType: 'textarea',
},
{
title: '主播ID',
dataIndex: 'userId',
valueType: 'textarea',
copyable: true,
},
{
title: '主播手机号',
dataIndex: 'userPhone',
valueType: 'textarea',
copyable: true,
},
{
title: '通告标题',
dataIndex: 'title',
valueType: 'textarea',
},
{
title: '通告ID',
dataIndex: 'jobId',
valueType: 'textarea',
copyable: true,
},
{
title: '解锁时间',
dataIndex: 'useDate',
valueType: 'dateTime',
renderText(time: string) {
return dayjs(Number(time)).format(TIME_FORMAT);
},
search: false,
},
{
title: '职位发布人昵称',
dataIndex: 'publisher',
valueType: 'textarea',
search: false,
copyable: true,
},
{
title: '职位发布人ID',
dataIndex: 'blPublisherId',
valueType: 'textarea',
copyable: true,
},
{
title: '职位发布人微信',
dataIndex: 'publisherAcctNo',
valueType: 'textarea',
copyable: true,
},
{
title: '报单类型',
dataIndex: 'type',
valueType: 'textarea',
renderText(type: number) {
return TYPE_OPTIONS.find(option => option.value === type)?.label;
},
renderFormItem() {
return <Select allowClear options={TYPE_OPTIONS} />;
},
},
{
title: '报单时间',
dataIndex: 'declarationDate',
valueType: 'dateTime',
renderText(time: string) {
return dayjs(Number(time)).format(TIME_FORMAT);
},
search: false,
},
{
title: '报单处理状态',
dataIndex: 'declaredStatus',
valueType: 'textarea',
renderText(status: number) {
const declaredStatus = Number(status);
return STATUS_OPTIONS.find(option => option.value === declaredStatus)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '报单处理时间',
dataIndex: 'declaredDate',
valueType: 'dateTime',
renderText(time: string) {
return dayjs(Number(time)).format(TIME_FORMAT);
},
search: false,
},
{
title: '是否加企微',
dataIndex: 'weComStatus',
valueType: 'textarea',
renderText(status: number) {
return WE_COM_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={WE_COM_OPTIONS} />;
},
},
{
title: '通告所属城市',
dataIndex: 'jobCityCode',
valueType: 'textarea',
renderText(cityCode: string) {
return CITY_CODE_TO_NAME_MAP.get(cityCode);
},
renderFormItem() {
return (
<Select
showSearch
allowClear
options={CITY_OPTIONS}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
);
},
},
{
title: '报单备注信息',
dataIndex: 'declaredMark',
valueType: 'textarea',
search: false,
},
{
title: '操作',
valueType: 'option',
fixed: 'right',
width: 100,
align: 'center',
render: (_, record) => (
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
),
},
];
return (
<PageContainer>
<ProTable<API.DeclarationListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getDeclarationList}
columns={columns}
scroll={{ x: 'max-content' }}
/>
<ModalForm
title="更新报单信息"
width="400px"
formRef={formRef}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async data => {
const params: API.UpdateDeclarationParams = {
id: currentRow!.id,
weComStatus: data.weComStatus,
};
console.log('update confirm', data, params);
try {
await updateDeclarationInfo(params);
actionRef.current?.reload();
formRef.current?.resetFields();
} catch (e) {}
handleUpdateModalOpen(false);
}}
>
<ProFormSelect
name="weComStatus"
label="是否加企微"
options={WE_COM_OPTIONS}
rules={[{ required: true, message: '必填项' }]}
/>
<ProFormTextArea name="declaredMark" label="报单备注" placeholder="输入备注信息" />
</ModalForm>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,131 @@
import type { ActionType, ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, PageContainer, ProFormSelect, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import React, { useRef, useState } from 'react';
import { CITY_CODE_TO_NAME_MAP, CITY_OPTIONS } from '@/constants/city';
import { getGroupList, updateGroupInfo } from '@/services/list';
const STATUS_OPTIONS = [
{ label: '正常', value: 0 },
{ label: '暂停', value: 1 },
];
const TableList: React.FC = () => {
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.GroupListItem>();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const columns: ProColumns<API.GroupListItem>[] = [
{
title: '群名称',
dataIndex: 'imGroupNick',
valueType: 'textarea',
copyable: true,
},
{
title: '群ID',
dataIndex: 'id',
valueType: 'textarea',
copyable: true,
},
{
title: '所属城市',
dataIndex: 'city',
valueType: 'textarea',
renderText(cityCode: string) {
return CITY_CODE_TO_NAME_MAP.get(cityCode);
},
renderFormItem() {
return (
<Select
showSearch
allowClear
options={CITY_OPTIONS}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
);
},
},
{
title: '群状态',
dataIndex: 'disable',
valueType: 'textarea',
renderText(disable: boolean) {
return disable ? '暂停' : '正常';
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '机器人ID',
dataIndex: 'robotId',
valueType: 'textarea',
},
{
title: '机器人微信昵称',
dataIndex: 'robotImNick',
valueType: 'textarea',
},
{
title: '机器人微信账号',
dataIndex: 'robotImNo',
valueType: 'textarea',
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
),
},
];
return (
<PageContainer>
<ProTable<API.GroupListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getGroupList}
columns={columns}
/>
<ModalForm
title="更新群信息"
width="400px"
formRef={formRef}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async formData => {
const params: API.UpdateGroupParams = {
id: currentRow!.id,
city: formData.city?.value,
disable: formData.disable,
};
console.log('update confirm', formData, params);
try {
await updateGroupInfo(params);
actionRef.current?.reload();
formRef.current?.resetFields();
} catch (e) {}
handleUpdateModalOpen(false);
}}
>
<ProFormSelect.SearchSelect name="city" mode="single" label="所属城市" options={CITY_OPTIONS} />
<ProFormSelect name="disable" label="群状态" options={STATUS_OPTIONS} />
</ModalForm>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,209 @@
import type { ActionType, ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, PageContainer, ProFormSelect, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import { CITY_CODE_TO_NAME_MAP, CITY_OPTIONS } from '@/constants/city';
import { TIME_FORMAT } from '@/constants/global';
import { getJobList, updateJobInfo } from '@/services/list';
const STATUS_OPTIONS = [
{ label: '正常', value: 0 },
{ label: '暂停', value: 1 },
];
const TableList: React.FC = () => {
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.JobListItem>();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const columns: ProColumns<API.JobListItem>[] = [
{
title: '职位名称',
dataIndex: 'title',
valueType: 'textarea',
},
{
title: '职位描述',
dataIndex: 'sourceText',
valueType: 'textarea',
colSize: 2,
search: false,
copyable: true,
renderText(sourceText: string) {
return sourceText?.substring(0, 30);
},
},
{
title: '职位ID',
dataIndex: 'jobId',
valueType: 'textarea',
copyable: true,
},
{
title: '城市',
dataIndex: 'cityCode',
valueType: 'textarea',
renderText(cityCode: string) {
return CITY_CODE_TO_NAME_MAP.get(cityCode);
},
renderFormItem() {
return (
<Select
showSearch
allowClear
options={CITY_OPTIONS}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
);
},
},
{
title: '所在群名称',
dataIndex: 'imGroupNick',
valueType: 'textarea',
copyable: true,
},
{
title: '群ID',
dataIndex: 'blGroupId',
valueType: 'textarea',
search: false,
copyable: true,
},
{
title: '发布人昵称',
dataIndex: 'publisher',
valueType: 'textarea',
copyable: true,
},
{
title: '发布人ID',
dataIndex: 'blPublisherId',
valueType: 'textarea',
copyable: true,
},
{
title: '发布人微信账号',
dataIndex: 'publisherAcctNo',
valueType: 'textarea',
copyable: true,
},
{
title: '发布群数量',
dataIndex: 'relateGroupCount',
valueType: 'textarea',
search: false,
},
{
title: '机器人ID',
dataIndex: 'robotId',
valueType: 'textarea',
},
{
title: '机器人微信昵称',
dataIndex: 'robotImNick',
valueType: 'textarea',
search: false,
},
{
title: '机器人微信账号',
dataIndex: 'robotImNo',
valueType: 'textarea',
search: false,
},
{
title: '通告状态',
dataIndex: 'disable',
valueType: 'textarea',
renderText(disable: boolean) {
return disable ? '暂停' : '正常';
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '小程序用户 id',
dataIndex: 'appUid',
valueType: 'textarea',
search: true,
},
{
title: '创建时间',
dataIndex: 'created',
valueType: 'dateTime',
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
},
{
title: '更新时间',
dataIndex: 'updated',
valueType: 'dateTime',
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
},
{
title: '操作',
valueType: 'option',
fixed: 'right',
width: 100,
align: 'center',
render: (_, record) => (
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
),
},
];
return (
<PageContainer>
<ProTable<API.JobListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getJobList}
columns={columns}
scroll={{ x: 'max-content' }}
/>
<ModalForm
title="更新通告信息"
width="400px"
formRef={formRef}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async formData => {
const params: API.UpdateJobParams = {
id: currentRow!.id,
jobId: currentRow!.jobId,
disable: Number(formData.disable) !== 0,
};
console.log('update confirm', formData, params);
try {
await updateJobInfo(params);
actionRef.current?.reload();
formRef.current?.resetFields();
} catch (e) {}
handleUpdateModalOpen(false);
}}
>
<ProFormSelect name="disable" label="通告状态" options={STATUS_OPTIONS} />
</ModalForm>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,198 @@
import type { ActionType, ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, PageContainer, ProFormSelect, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import Previewer from '@/components/previewer';
import { CITY_CODE_TO_NAME_MAP, CITY_OPTIONS } from '@/constants/city';
import { TIME_FORMAT } from '@/constants/global';
import { getMaterialList, updateMaterialInfo } from '@/services/list';
const STATUS_OPTIONS = [
{ label: '开放', value: true },
{ label: '封禁', value: false },
];
const TableList: React.FC = () => {
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.MaterialListItem>();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const columns: ProColumns<API.MaterialListItem>[] = [
{
title: '视频',
dataIndex: 'userId',
valueType: 'textarea',
copyable: true,
search: false,
render(_dom, entity) {
return <Previewer videos={entity.materialVideoInfoList} />;
},
},
{
title: '主播ID',
dataIndex: 'userId',
valueType: 'textarea',
copyable: true,
},
{
title: '模卡昵称',
dataIndex: 'name',
valueType: 'textarea',
},
{
title: '主播昵称',
dataIndex: 'nickname',
valueType: 'textarea',
search: false,
},
{
title: '自身优势',
dataIndex: 'advantages',
valueType: 'textarea',
search: false,
width: 200,
},
{
title: '模卡状态',
dataIndex: 'isOpen',
valueType: 'textarea',
renderText(status: boolean) {
return STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
search: false,
},
{
title: '主播模卡状态',
dataIndex: 'userOpen',
valueType: 'textarea',
renderText(status: boolean) {
return STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '后台模卡状态',
dataIndex: 'adminOpen',
valueType: 'textarea',
renderText(status: boolean) {
return STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '意向城市',
dataIndex: 'cityCode',
valueType: 'textarea',
renderText(cityCode: string) {
return CITY_CODE_TO_NAME_MAP.get(cityCode);
},
renderFormItem() {
return (
<Select
showSearch
allowClear
options={CITY_OPTIONS}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
);
},
},
{
title: '播过的品类',
dataIndex: 'workedSecCategoryStr',
valueType: 'textarea',
search: false,
width: 200,
},
{
title: '创建时间',
dataIndex: 'created',
valueType: 'dateTime',
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
sorter: true,
},
{
title: '修改时间',
dataIndex: 'updated',
valueType: 'dateTime',
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
sorter: true,
},
{
title: '操作',
valueType: 'option',
fixed: 'right',
width: 100,
align: 'center',
render: (_, record) => (
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
),
},
];
return (
<PageContainer>
<ProTable<API.MaterialListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getMaterialList}
columns={columns}
scroll={{ x: 'max-content' }}
/>
<ModalForm
title="更新模卡信息"
width="400px"
formRef={formRef}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async data => {
const params: API.UpdateMaterialParams = {
id: currentRow!.id,
adminOpen: Number(data.adminOpen),
};
console.log('update confirm', data, params);
try {
await updateMaterialInfo(params);
actionRef.current?.reload();
formRef.current?.resetFields();
} catch (e) {}
handleUpdateModalOpen(false);
}}
>
<ProFormSelect
name="adminOpen"
label="后台模卡状态"
options={STATUS_OPTIONS as any}
rules={[{ required: true, message: '必填项' }]}
/>
</ModalForm>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,195 @@
import type { ActionType, ProColumns, ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, PageContainer, ProFormSelect, ProFormText, ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Select } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import { TIME_FORMAT } from '@/constants/global';
import { getPublisherList, updatePublisherInfo } from '@/services/list';
const WX_STATUS_OPTIONS = [
{ label: '为空', value: 0 },
{ label: '不为空', value: 1 },
];
const STATUS_OPTIONS = [
{ label: '正常', value: 0 },
{ label: '暂停', value: 1 },
];
const ADD_WX_STATUS_OPTIONS = [
{ label: '待申请', value: 0 },
{ label: '已申请', value: 1 },
{ label: '不可添加', value: 2 },
{ label: '被封号', value: 3 },
];
const HAS_DECLARE_OPTIONS = [
{ label: '有报单', value: true },
{ label: '无报单', value: false },
];
const TableList: React.FC = () => {
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.PublisherListItem>();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const columns: ProColumns<API.PublisherListItem>[] = [
{
title: '发布人昵称',
dataIndex: 'publisher',
valueType: 'textarea',
copyable: true,
},
{
title: '发布人ID',
dataIndex: 'blPublisherId',
valueType: 'textarea',
copyable: true,
},
{
title: '账号状态',
dataIndex: 'status',
valueType: 'textarea',
renderText(status: number) {
return STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={STATUS_OPTIONS} />;
},
},
{
title: '发布人微信账号',
dataIndex: 'publisherAcctNo',
valueType: 'textarea',
copyable: true,
},
{
title: '发布人微信状态',
dataIndex: 'publisherAcctStatus',
valueType: 'textarea',
renderText(status: number) {
const publisherAcctStatus = Number(status);
return WX_STATUS_OPTIONS.find(option => option.value === publisherAcctStatus)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={WX_STATUS_OPTIONS} />;
},
},
{
title: '是否申请好友',
dataIndex: 'addAcctStatus',
valueType: 'textarea',
renderText(status: number) {
return ADD_WX_STATUS_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={ADD_WX_STATUS_OPTIONS} />;
},
},
{
title: '机器人微信昵称',
dataIndex: 'robotImNick',
valueType: 'textarea',
},
{
title: '来源群',
dataIndex: 'imGroupNick',
valueType: 'textarea',
copyable: true,
},
{
title: '是否有报单',
dataIndex: 'hasDeclareOrder',
hideInTable: true,
renderText(status: boolean) {
return HAS_DECLARE_OPTIONS.find(option => option.value === status)?.label;
},
renderFormItem() {
return <Select showSearch allowClear options={HAS_DECLARE_OPTIONS} />;
},
},
{
title: '最新报单时间',
dataIndex: 'declareTime',
valueType: 'dateTime',
sorter: true,
renderText(created: string) {
return created ? dayjs(Number(created)).format(TIME_FORMAT) : '';
},
search: false,
},
{
title: '小程序用户 id',
dataIndex: 'appUid',
valueType: 'textarea',
search: true,
},
{
title: '创建时间',
dataIndex: 'created',
valueType: 'dateTime',
sorter: true,
renderText(created: string) {
return dayjs(Number(created)).format(TIME_FORMAT);
},
search: false,
},
{
title: '操作',
valueType: 'option',
render: (_, record) => (
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
),
},
];
return (
<PageContainer>
<ProTable<API.PublisherListItem, API.PageParams>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{ labelWidth: 120, collapsed: false, collapseRender: false }}
request={getPublisherList}
columns={columns}
/>
<ModalForm
title="更新发布人信息"
width="400px"
formRef={formRef}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async data => {
const params: API.UpdatePublisherParams = {
blPublisherId: currentRow!.blPublisherId,
publisherAcctNo: data.publisherAcctNo,
status: data.status,
addAcctStatus: data.addAcctStatus,
};
console.log('update confirm', data, params);
try {
await updatePublisherInfo(params);
actionRef.current?.reload();
formRef.current?.resetFields();
} catch (e) {}
handleUpdateModalOpen(false);
}}
>
<ProFormText width="md" name="publisherAcctNo" label="发布人微信账号" />
<ProFormSelect name="status" label="账号状态" options={STATUS_OPTIONS} />
<ProFormSelect name="addAcctStatus" label="是否申请好友" options={ADD_WX_STATUS_OPTIONS} />
</ModalForm>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,104 @@
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormText } from '@ant-design/pro-components';
import { Helmet, history, useModel } from '@umijs/max';
import { Alert, message } from 'antd';
import { createStyles } from 'antd-style';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import Footer from '@/components/footer';
import { login } from '@/services/user';
import { setToken } from '@/utils/login';
import Settings from '../../../../config/defaultSettings';
const useStyles = createStyles(() => {
return {
container: {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
},
};
});
const Login: React.FC = () => {
const [loginError, setLoginError] = useState<boolean>(false);
const { initialState, setInitialState } = useModel('@@initialState');
const { styles } = useStyles();
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
setInitialState(s => ({
...s,
currentUser: userInfo,
}));
});
}
};
const handleSubmit = async (values: API.LoginParams) => {
try {
// 登录
console.log('login params', values);
const result = await login({ ...values });
if (result.token) {
setToken(result.token);
message.success('登录成功!');
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
console.log(result);
// 如果失败去设置用户错误信息
setLoginError(true);
} catch (error) {
console.log(error);
message.error('登录失败,请重试!');
}
};
return (
<div className={styles.container}>
<Helmet>
<title>
{'登录'}- {Settings.title}
</title>
</Helmet>
<div style={{ flex: '1', padding: '32px 0' }}>
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" src={Settings.logo} />}
subTitle=" "
title="播络管理后台"
initialValues={{
autoLogin: true,
}}
onFinish={async values => {
await handleSubmit(values as API.LoginParams);
}}
>
<ProFormText
name="userName"
fieldProps={{ size: 'large', prefix: <UserOutlined /> }}
placeholder="请输入账号"
rules={[{ required: true, message: '账号不能为空!' }]}
/>
<ProFormText.Password
name="pwd"
fieldProps={{ size: 'large', prefix: <LockOutlined /> }}
placeholder="请输入密码"
rules={[{ required: true, message: '密码不能为空!' }]}
/>
{loginError && <Alert style={{ marginBottom: 24 }} message="用户名或密码错误" type="error" showIcon />}
</LoginForm>
</div>
<Footer />
</div>
);
};
export default Login;

84
src/requestConfig.ts Normal file
View File

@ -0,0 +1,84 @@
import type { AxiosResponse, RequestOptions } from '@@/plugin-request/request';
import type { RequestConfig } from '@umijs/max';
import { message } from 'antd';
import { RESPONSE_ERROR_CODE, RESPONSE_ERROR_MESSAGE } from '@/constants/http';
import { clearToken, getToken, gotoLogin } from '@/utils/login';
import { IRequestResponse } from './types/http';
/**
* @name 全局请求配置
* @doc https://umijs.org/docs/max/request#配置
*/
export const requestConfig: RequestConfig = {
// baseURL: 'https://neighbourhood.cn',
baseURL: 'http://192.168.60.120:8082',
// 错误处理: umi@3 的错误处理方案。
errorConfig: {
// 错误抛出
errorThrower: res => {
const { code, msg, traceId } = res as IRequestResponse;
if (code) {
const error: any = new Error(msg);
error.name = 'BizError';
error.info = { code, msg, traceId };
throw error; // 抛出自制的错误
}
},
// 错误接收及处理
errorHandler: (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
// 我们的 errorThrower 抛出的错误。
if (error.name === 'BizError') {
const errorInfo: IRequestResponse | undefined = error.info;
if (!errorInfo) {
return;
}
const { code, msg, traceId } = errorInfo;
switch (code) {
case RESPONSE_ERROR_CODE.INVALID_PARAMETER:
default:
message.error(`Request error, msg: ${msg}, traceId: ${traceId}`);
}
} else if (error.response) {
// Axios 的错误
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
const { data, status } = error.response as AxiosResponse<IRequestResponse>;
const code = data?.code as RESPONSE_ERROR_CODE;
switch (code) {
case RESPONSE_ERROR_CODE.NEED_LOGIN:
clearToken();
gotoLogin();
return;
default:
message.error(`${RESPONSE_ERROR_MESSAGE[code] || '请求错误'}, 错误码:${status}`);
return;
}
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
message.error('None response! Please retry.');
} else {
// 发送请求时出了点问题
message.error('Request error, please retry.');
}
},
},
// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
const token = getToken();
if (token) {
config.headers = {
...(config.headers || {}),
Authorization: 'Bearer ' + token,
};
}
return config;
},
],
};

59
src/service-worker.js Normal file
View File

@ -0,0 +1,59 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-underscore-dangle */
/* globals workbox */
workbox.core.setCacheNameDetails({
prefix: 'antd-pro',
suffix: 'v5',
});
// Control all opened tabs ASAP
workbox.clientsClaim();
/**
* Use precaching list generated by workbox in build process.
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
*/
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
/**
* Register a navigation route.
* https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
*/
workbox.routing.registerNavigationRoute('/index.html');
/**
* Use runtime cache:
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
*
* Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
*/
/** Handle API requests */
workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
/** Handle third party requests */
workbox.routing.registerRoute(/^https:\/\/gw\.alipayobjects\.com\//, workbox.strategies.networkFirst());
workbox.routing.registerRoute(/^https:\/\/cdnjs\.cloudflare\.com\//, workbox.strategies.networkFirst());
workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
/** Response to client after skipping waiting with MessageChannel */
addEventListener('message', event => {
const replyPort = event.ports[0];
const message = event.data;
if (replyPort && message && message.type === 'skip-waiting') {
event.waitUntil(
self.skipWaiting().then(
() => {
replyPort.postMessage({
error: null,
});
},
error => {
replyPort.postMessage({
error,
});
},
),
);
}
});

224
src/services/list.ts Normal file
View File

@ -0,0 +1,224 @@
// @ts-ignore
/* eslint-disable */
import { AdminAPI } from '@/constants/api';
import { EmployType, JobType } from '@/constants/job';
import { request } from '@umijs/max';
import { SortOrder } from 'antd/es/table/interface';
function transformPageParams(params: API.PageParams & Record<string, any>) {
params.page = params.current;
delete params.current;
Object.keys(params).forEach((key: string) => {
if (typeof params[key] === 'string' && !params[key]) {
delete params[key];
}
});
return params;
}
function transformSort(sort: Record<string, SortOrder>) {
if (!sort) {
return {};
}
const sortField = Object.keys(sort)[0];
if (!sort[sortField]) {
return {};
}
const asc = sort[sortField] === 'ascend';
return { sortField, asc };
}
function sortTableList<T extends { created: number; updated: number }>(
response: API.TableList<T>,
{ sortField, asc }: ReturnType<typeof transformSort>,
): API.TableList<T> {
if (sortField === 'created' || sortField === 'updated') {
response.data.sort((itemA, itemB) => {
const valueA = Number(itemA[sortField]);
const valueB = Number(itemB[sortField]);
return asc ? valueA - valueB : valueB - valueA;
});
}
return response;
}
export async function getJobList(params: API.PageParams & Partial<API.JobListItem>, options?: {
[key: string]: any
}) {
if (!params.category) {
params.category = JobType.All;
}
if (!params.cityCode) {
params.cityCode = 'ALL';
}
if (!params.employType) {
params.employType = EmployType.All;
}
const result = await request<API.TableList<API.JobListItem>>(AdminAPI.JOB_LIST, {
method: 'POST',
data: {
...transformPageParams(params),
...(options || {}),
},
});
result.success = true;
return result;
}
/**
* 更新通告接口,必须传 数据库 id 和 jobId
*/
export async function updateJobInfo(options: API.UpdateJobParams) {
return request<API.JobListItem>(AdminAPI.JOB_UPDATE, {
method: 'POST',
data: {
...(options || {}),
},
});
}
export async function getGroupList(
params: API.PageParams & Partial<API.GroupListItem>,
options?: { [key: string]: any },
) {
const result = await request<API.TableList<API.GroupListItem>>(AdminAPI.GROUP_LIST, {
method: 'POST',
data: {
...transformPageParams(params),
...(options || {}),
},
});
result.success = true;
return result;
}
/**
* 更新群信息,必须传 数据库 id
*/
export async function updateGroupInfo(options: API.UpdateGroupParams) {
return request<API.GroupListItem>(AdminAPI.GROUP_UPDATE, {
method: 'POST',
data: {
...(options || {}),
},
});
}
export async function getAnchorGroupList(
params: API.PageParams & Partial<API.AnchorGroupListItem>,
options?: { [key: string]: any },
) {
const result = await request<API.TableList<API.AnchorGroupListItem>>(AdminAPI.ANCHOR_GROUP_LIST, {
method: 'POST',
data: {
...transformPageParams(params),
...(options || {}),
},
});
return result;
}
export async function addAnchorGroup(options?: { [key: string]: any }) {
return request<API.GroupListItem>(AdminAPI.ADD_ANCHOR_GROUP, {
method: 'POST',
data: {
...(options || {}),
},
});
}
export async function getPublisherList(
params: API.PageParams & Partial<API.PublisherListItem>,
sort: Record<string, SortOrder>,
) {
const url = params.hasDeclareOrder ? AdminAPI.PUBLISHER_DECLARATION_LIST : AdminAPI.PUBLISHER_LIST;
const result = await request<API.TableList<API.PublisherListItem>>(url, {
method: 'POST',
data: {
...transformPageParams(params),
...transformSort(sort),
},
});
return result;
}
export async function updatePublisherInfo(options: API.UpdatePublisherParams) {
return request<API.PublisherListItem>(AdminAPI.PUBLISHER_UPDATE, {
method: 'POST',
data: {
...(options || {}),
},
});
}
export async function getAnchorList(
params: API.PageParams & Partial<API.AnchorListItem>,
options?: { [key: string]: any },
) {
const result = await request<API.TableList<API.AnchorListItem>>(AdminAPI.ANCHOR_LIST, {
method: 'POST',
data: {
...transformPageParams(params),
...(options || {}),
},
});
return result;
}
export async function updateAnchorInfo(options: API.UpdateAnchorParams) {
return request<API.DeclarationListItem>(AdminAPI.ANCHOR_UPDATE, {
method: 'POST',
data: {
...(options || {}),
},
});
}
export async function getMaterialList(
params: API.PageParams & Partial<API.MaterialListItem>,
sort: Record<string, SortOrder>,
) {
const formatedSort = transformSort(sort);
const result = await request<API.TableList<API.MaterialListItem>>(AdminAPI.MATERIAL_LIST, {
method: 'POST',
data: {
...transformPageParams(params),
...formatedSort,
},
});
return sortTableList<API.MaterialListItem>(result, formatedSort);
}
export async function updateMaterialInfo(options: API.UpdateMaterialParams) {
return request<API.DeclarationListItem>(AdminAPI.MATERIAL_UPDATE, {
method: 'POST',
data: {
...(options || {}),
},
});
}
export async function getDeclarationList(
params: API.PageParams & Partial<API.DeclarationListItem>,
options?: { [key: string]: any },
) {
const result = await request<API.TableList<API.DeclarationListItem>>(AdminAPI.DECLARATION_LIST, {
method: 'POST',
data: {
...transformPageParams(params),
...(options || {}),
},
});
return result;
}
export async function updateDeclarationInfo(options: API.UpdateDeclarationParams) {
return request<API.DeclarationListItem>(AdminAPI.DECLARATION_UPDATE, {
method: 'POST',
data: {
...(options || {}),
},
});
}

325
src/services/typings.d.ts vendored Normal file
View File

@ -0,0 +1,325 @@
// @ts-ignore
/* eslint-disable */
declare namespace API {
type CurrentUser = {
id: string; // 数据库 id
userId: string;
userName: string;
created: string;
updated: string;
avatar?: string;
};
interface LoginParams {
userName?: string;
pwd?: string;
}
interface LoginResult {
token?: string;
expires?: number;
}
interface PageParams {
// proTable 给的分页页数
current?: number;
pageSize?: number;
// 实际接口需要的分页页数
page?: number;
}
interface JobListItem {
// 数据库 id
id: string;
// 小程序用户 id
appUid: string;
// 通告 id
jobId: string;
// 通告标题
title: string;
// 简介
jobDescription: string;
// 详细描述(原始信息)
sourceText: string;
// 城市 code查询时必传默认为 ALL
cityCode: string;
// 品类,查询时必传:默认为 ALL
category: string;
// 工作类型,查询时必传:默认为 ALL
employType: string;
// 发布人微信昵称
publisher: string;
// 发布人 id
blPublisherId: string;
// 发布人微信号
publisherAcctNo: string;
// 群昵称
imGroupNick: string;
// 微信群id
imGroupId: string;
// 播络群 id
blGroupId: string;
// 机器人 id
robotId: string;
// 机器人微信昵称
robotImNick: string;
// 机器人微信号
robotImNo: string;
// 通告发布群数量
relateGroupCount: number;
// 创建时间,时间戳
created: string;
// 更新时间,时间戳
updated: string;
// 是否禁用,默认为 false
disable: boolean;
}
interface GroupListItem {
// 播络群 id
id: string;
// 微信群id
imGroupId: string;
// 群昵称
imGroupNick: string;
groupType: string;
// 群主微信昵称
groupOwnerNick: string;
// 群主微信号
groupOwnerImAcctNo: string;
// 群主boluo账号
groupOwnerAcctNo: string;
// 机器人 id
robotId: string;
// 机器人微信昵称
robotImNick: string;
// 机器人微信号
robotImNo: string;
// 城市 code
city: string;
// 通告数量
jobCount: number;
// 创建时间,时间戳
created: string;
// 更新时间,时间戳
updated: string;
// 是否禁用,默认为 false
disable: boolean;
// 是否删除
isDeleted: boolean;
}
interface AnchorGroupListItem {
// 主播用户 id
userId: string;
// 主播昵称
nickName: string;
// 主播手机号
userPhone: string;
// 群名称
imGroupNick: string;
// 微信群id
imGroupId: string;
// 播络群 id
blGroupId: string;
// 是否已经支付
payed: boolean;
// 支付方式, vx、alipay、other
payType: string;
// 群定制时间,时间戳
created: string;
// 绑定群的客服操作人员
creator: string;
// 备注
mark?: string;
// 加群状态0 已申请加群 1已申请加群未进群 2 已申请加群已进群
status: number;
}
interface PublisherListItem {
// 发布人微信昵称
publisher: string;
// 发布人 id
blPublisherId: string;
// 小程序用户 id
appUid: string;
// 发布人微信号
publisherAcctNo: string;
// 发布人微信状态, 0 空1 非空
publisherAcctStatus: number;
// 账号状态: 0 正常1 暂停
status: number;
// 申请好友状态: 0 待申请1 已申请2 不能添加3 被封号
addAcctStatus: number;
phone: string;
email: string;
// 机器人微信昵称
robotImNick: string;
// 客服操作人员
operator: string;
// 群名称
imGroupNick: string;
// 微信群id
imGroupId: string;
// 播络群 id
blGroupId: string;
// 创建时间
created: string;
// 更新时间
updated: string;
// 是否有报单
hasDeclareOrder?: boolean;
// 最新报单时间
declareTime?: string;
jobId?: string;
}
interface AnchorListItem {
// 主播用户 id
userId: string;
// 主播昵称
nickName: string;
// 主播手机号
userPhone: string;
// 是否绑定了手机号
isBindPhone: boolean;
// 注册时间
created: string;
// 最后一次登录时间
lastLoginDate: string;
// 状态: 0 正常1 封禁
status: number;
// 所属城市 code
city: string;
}
interface DeclarationListItem {
// 报单 id
id: string;
// 报单类型
type: number;
// 主播用户 id
userId: string;
// 主播昵称
nickName: string;
// 主播手机号
userPhone: string;
// 通告 id
jobId: string;
// 通告标题
title: string;
// 解锁时间
useDate: string;
// 发布人微信昵称
publisher: string;
// 发布人 id
blPublisherId: string;
// 发布人微信号
publisherAcctNo: string;
// 报单时间
declarationDate: string;
// 报单处理状态
declaredStatus: number;
// 报单处理时间
declaredDate: string;
// 备注
declaredMark?: string;
// 加企微状态
weComStatus: number;
// 通告城市信息,如 400100
jobCityCode: string;
}
interface MaterialVideoInfo {
url: string;
coverUrl: string;
type: 'image' | 'video';
title: string;
isDefault: boolean;
}
interface MaterialProfile {
materialVideoInfoList: MaterialVideoInfo[];
// 基础信息
id: string;
userId: string;
name: string;
age: number;
height: number; // cm
weight: number; // kg
gender: GenderType;
shoeSize: number; // 鞋码
// 求职意向
employType: EmployType; // 工作类型
fullTimeMinPrice: number; // 全职期望薪资下限
fullTimeMaxPrice: number; // 全职期望薪资上限
partyTimeMinPrice: number; // 兼职期望薪资下限
partyTimeMaxPrice: number; // 兼职期望薪资上限
cityCode: string; // 城市
cityCodes: string; // 城市。多个城市用 、分割,如 '110100、141100'
acceptWorkForSit: boolean; // 是否接受坐班
// 直播经验
workedYear: WorkedYears; // 工作年限单位年无为0、半年 0.5、1、2、3、4、5年及以上用100表示默认为1年
workedAccounts: string; // 直播过的账号
newAccountExperience: number; // 是否有起号经验0无 1有
workedCategory: string; // 直播过的品类
workedSecCategoryStr: string; // 直播过的二级品类
style: string; // 风格。多个分割用 、分割
maxGmv: number; // 最高 GMV单位 w
maxOnline: number; // 最高在线人数
// 自身优势
advantages: string;
// 其他
approveStatus: boolean; // 审核状态0 待审 1 成功 2 不通过
isOpen: boolean; // 整体状态是否开放 1开放 0不开放
createType: ProfileCreateSource;
creator: string; // 创建人id
progressBar: number; // 进度百分比
filledItems: number; // 已填资料项数
created: number; // 时间戳
updated: number; // 时间戳
}
interface MaterialListItem extends MaterialProfile {
// 主播用户 id
userId: string;
// 主播昵称
nickname: string;
// 主播手机号
userPhone: string;
userOpen: boolean;
adminOpen: boolean; // 后台控制是否开放 1开放 0不开放
}
interface ListResult<T> {
data: T[];
total: number;
hasMore: boolean;
page: number;
pageSize: number;
// 排序字段
sortField: string;
// 是否升序
asc: string;
}
interface TableList<T> extends ListResult<T> {
success?: boolean;
}
type UpdateJobParams = Pick<JobListItem, 'id' | 'jobId' | 'disable'>;
type UpdateGroupParams = Pick<GroupListItem, 'id' | 'city' | 'disable'>;
type UpdatePublisherParams = Pick<
PublisherListItem,
'blPublisherId' | 'publisherAcctNo' | 'status' | 'addAcctStatus'
>;
type UpdateAnchorParams = Pick<AnchorListItem, 'userId' | 'status'>;
type UpdateDeclarationParams = Pick<DeclarationListItem, 'id' | 'weComStatus'>;
type UpdateMaterialParams = Pick<MaterialListItem, 'id'> & { adminOpen: number };
}

31
src/services/user.ts Normal file
View File

@ -0,0 +1,31 @@
// @ts-ignore
/* eslint-disable */
import { AdminAPI } from '@/constants/api';
import { request } from '@umijs/max';
import { md5 } from 'js-md5';
export async function currentUser(options?: { [key: string]: any }) {
return request<API.CurrentUser>(AdminAPI.USER, {
method: 'POST',
...(options || {}),
});
}
export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
body.pwd = md5(body.pwd || '');
return request<API.LoginResult>(AdminAPI.LOGIN, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
export async function outLogin(options?: { [key: string]: any }) {
// return request<Record<string, any>>(AdminAPI.OUT_LOGIN, {
// method: 'POST',
// ...(options || {}),
// });
}

9
src/types/http.ts Normal file
View File

@ -0,0 +1,9 @@
import { RESPONSE_ERROR_CODE } from '@/constants/http';
export interface IRequestResponse<T = any> {
data: T;
// 请求出错时才会返回下面几个字段
code: RESPONSE_ERROR_CODE;
msg: string;
traceId: string;
}

20
src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
declare module 'slash2';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module 'omit.js';
declare module 'numeral';
declare module '@antv/data-set';
declare module 'mockjs';
declare module 'react-fittext';
declare module 'bizcharts-plugin-slider';
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

6
src/utils/common.ts Normal file
View File

@ -0,0 +1,6 @@
export const isValidPhone = (phone: string, callback: (msg?: string) => void) => {
if (!/^1\d{10}$/.test(phone)) {
callback('手机号格式不正确');
}
callback();
};

22
src/utils/login.ts Normal file
View File

@ -0,0 +1,22 @@
import { history } from '@umijs/max';
import { LOGIN_PATH } from '@/constants/global';
let _token = '';
export const setToken = (token: string) => {
_token = token;
};
export const getToken = () => {
return _token;
};
export const clearToken = () => {
return (_token = '');
};
export const gotoLogin = () => {
console.trace('gotoLogin');
history.push(LOGIN_PATH);
};