Merge branch 'main' into online

This commit is contained in:
2026-07-11 17:15:44 +08:00
16 changed files with 292 additions and 103 deletions
+1
View File
@@ -1,4 +1,5 @@
# VITE_API_BASE=http://192.168.120.17:8000
#VITE_API_BASE=https://apiforeign.minzhong.cn
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
VITE_USE_MOCK=false
# Encryption disabled for dev — enable in production
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-B5S0hm2T.js"></script>
<script type="module" crossorigin src="/assets/index-tEZsB6Nw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head>
<body>
+2 -1
View File
@@ -387,10 +387,11 @@ export async function getPaymentStats(params?: {
return api.get(url);
}
export async function getAdminPaymentOrders(params?: { method?: string; status?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number }): Promise<{ items: any[]; total: number }> {
export async function getAdminPaymentOrders(params?: { method?: string; status?: string; phone?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number }): Promise<{ items: any[]; total: number }> {
const qs = new URLSearchParams();
if (params?.method) qs.set('payment_method', params.method);
if (params?.status) qs.set('status', params.status);
if (params?.phone) qs.set('phone', params.phone);
if (params?.startDate) qs.set('start_date', params.startDate);
if (params?.endDate) qs.set('end_date', params.endDate);
if (params?.page) qs.set('page', String(params.page));
+32 -16
View File
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import {
Card, Col, Row, Space, Table, Tag, Typography, Statistic, message, Select, DatePicker, Button, ConfigProvider, Popconfirm
Card, Col, Input, Row, Space, Table, Tag, Typography, Statistic, message, Select, DatePicker, Button, ConfigProvider, Popconfirm
} from 'antd';
import zhCN from 'antd/locale/zh_CN';
import {
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, ReloadOutlined, UndoOutlined
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, ReloadOutlined, UndoOutlined, SearchOutlined
} from '@ant-design/icons';
import { getPaymentStats, getAdminPaymentOrders, refundPaymentOrder } from '../api';
import { formatDate } from '../utils/formatDate';
@@ -22,6 +22,7 @@ const AdminPaymentStats: React.FC = () => {
const [filters, setFilters] = useState<{
paymentMethod?: string;
status?: string;
phone?: string;
startDate: string;
endDate: string;
}>({
@@ -36,6 +37,7 @@ const AdminPaymentStats: React.FC = () => {
getPaymentStats(filters),
getAdminPaymentOrders({
...filters,
phone: filters.phone,
page: orderPage,
pageSize: orderPageSize,
}),
@@ -61,6 +63,7 @@ const AdminPaymentStats: React.FC = () => {
startDate: dayjs().format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
});
setOrderPage(1);
};
const handleRefund = async (orderNo: string) => {
@@ -101,6 +104,7 @@ const AdminPaymentStats: React.FC = () => {
const columns = [
{ title: '订单号', dataIndex: 'orderNo', key: 'orderNo', width: 200 },
{ title: '用户', dataIndex: 'username', key: 'username', width: 120 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 120, render: (v: string) => v || '-' },
{
title: '支付方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 100,
render: (m: string) => {
@@ -237,28 +241,28 @@ const AdminPaymentStats: React.FC = () => {
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}
title={<Space><DollarOutlined /></Space>}>
{/* Filters */}
<Row gutter={[16, 16]} align="middle" style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} md={6}>
<span style={{ marginRight: 8 }}></span>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={8} md={4}>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}></Typography.Text>
<Select
placeholder="全部"
allowClear
style={{ width: 150 }}
style={{ width: '100%' }}
value={filters.paymentMethod}
onChange={(value) => setFilters(prev => ({ ...prev, paymentMethod: value }))}
onChange={(value) => { setFilters(prev => ({ ...prev, paymentMethod: value })); setOrderPage(1); }}
>
<Option value="alipay"></Option>
<Option value="wechat"></Option>
</Select>
</Col>
<Col xs={24} sm={12} md={6}>
<span style={{ marginRight: 8 }}></span>
<Col xs={24} sm={8} md={4}>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}></Typography.Text>
<Select
placeholder="全部"
allowClear
style={{ width: 150 }}
style={{ width: '100%' }}
value={filters.status}
onChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
onChange={(value) => { setFilters(prev => ({ ...prev, status: value })); setOrderPage(1); }}
>
<Option value="paid"></Option>
<Option value="pending"></Option>
@@ -266,9 +270,21 @@ const AdminPaymentStats: React.FC = () => {
<Option value="refunded">退</Option>
</Select>
</Col>
<Col xs={24} sm={12} md={8}>
<span style={{ marginRight: 8 }}></span>
<Col xs={24} sm={8} md={5}>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}></Typography.Text>
<Input
placeholder="搜索手机号"
allowClear
value={filters.phone}
onChange={(e) => setFilters(prev => ({ ...prev, phone: e.target.value }))}
onPressEnter={() => { setOrderPage(1); load(); }}
suffix={<SearchOutlined style={{ color: '#94a3b8' }} />}
/>
</Col>
<Col xs={24} sm={12} md={7}>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}></Typography.Text>
<RangePicker
style={{ width: '100%' }}
value={[
dayjs(filters.startDate),
dayjs(filters.endDate),
@@ -276,9 +292,9 @@ const AdminPaymentStats: React.FC = () => {
onChange={handleDateChange}
/>
</Col>
<Col xs={24} sm={12} md={4}>
<Button icon={<ReloadOutlined />} onClick={handleReset}>
<Col xs={24} sm={12} md={4} style={{ display: 'flex', alignItems: 'flex-end' }}>
<Button icon={<ReloadOutlined />} onClick={handleReset} style={{ marginBottom: 0 }}>
</Button>
</Col>
</Row>
@@ -35,7 +35,7 @@ const statusMap: Record<string, { text: string; color: string }> = {
};
const defaultTextWatermark: HomeMaterialTextWatermarkConfig = {
text: '民众普康 AI',
text: '民众智创',
opacityLevel: 2,
fontSizePx: 28,
color: '#ffffff',
@@ -27,7 +27,7 @@ const defaultConfig: HomeMaterialWatermarkConfig = {
marginX: 24,
marginY: 24,
textWatermark: {
text: '民众普康 AI',
text: '民众智创',
opacityLevel: 2,
fontSizePx: 28,
color: '#ffffff',
@@ -25,7 +25,7 @@ const positionOptions = [
];
const defaultTextWatermark = {
text: '民众普康 AI',
text: '民众智创',
opacityLevel: 2,
fontSizePx: 28,
color: '#ffffff',
+6 -2
View File
@@ -836,13 +836,14 @@ async def list_payment_orders(
page_size: int = Query(20, ge=1, le=500),
payment_method: str | None = Query(None),
status: str | None = Query(None),
phone: str | None = Query(None, description="按用户手机号模糊搜索"),
start_date: str | None = Query(None),
end_date: str | None = Query(None),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""Return paginated payment orders for admin dashboard."""
query = select(PaymentOrder, User.username).join(User, PaymentOrder.user_id == User.id)
query = select(PaymentOrder, User.username, User.phone).join(User, PaymentOrder.user_id == User.id)
count_query = select(func.count(PaymentOrder.id))
filters = []
@@ -850,6 +851,8 @@ async def list_payment_orders(
filters.append(PaymentOrder.payment_method == payment_method)
if status:
filters.append(PaymentOrder.status == status)
if phone:
filters.append(User.phone.ilike(f"%{phone.strip()}%"))
if start_date:
filters.append(PaymentOrder.created_at >= datetime.fromisoformat(start_date).replace(tzinfo=CST))
if end_date:
@@ -873,6 +876,7 @@ async def list_payment_orders(
"userId": o.user_id,
"user_id": o.user_id,
"username": username,
"phone": user_phone,
"amount": round(float(o.amount), 2),
"credits": round(float(o.credits), 2),
"paymentMethod": o.payment_method,
@@ -885,7 +889,7 @@ async def list_payment_orders(
"createdAt": o.created_at.isoformat() if o.created_at else None,
"created_at": o.created_at.isoformat() if o.created_at else None,
}
for o, username in rows
for o, username, user_phone in rows
]
return {"items": items, "total": total, "page": page, "page_size": page_size}
+1
View File
@@ -1,5 +1,6 @@
# VITE_API_BASE=http://192.168.120.17:8000
#VITE_API_BASE=https://apiforeign.minzhong.cn
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
VITE_USE_MOCK=false
# Encryption disabled for dev — enable in production
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-OcL9GWjl.js"></script>
<script type="module" crossorigin src="/assets/index-a7nRC5ui.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
</head>
<body>
+112 -10
View File
@@ -165,7 +165,8 @@ const AIChatPage: React.FC = () => {
} = useAppStore();
// 获取当前选中引擎的媒体上传限制
const currentEngineList = mediaType === 'image' ? enginesele?.image : enginesele?.video;
const enginesData = Array.isArray(enginesele) ? {} : enginesele;
const currentEngineList = mediaType === 'image' ? enginesData.image : enginesData.video;
const currentEngine = currentEngineList?.find((e: any) => e.id === countType);
const maxImageCount = currentEngine?.maxImageCount ?? 4;
const maxVideoCount = currentEngine?.maxVideoCount ?? 1;
@@ -174,6 +175,8 @@ const AIChatPage: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false);
const [enginesLoaded, setEnginesLoaded] = useState(false);
const [referenceMode, setReferenceMode] = useState<'universal' | 'first_last_frame'>('universal');
const [firstFrame, setFirstFrame] = useState<MediaReference | null>(null);
const [lastFrame, setLastFrame] = useState<MediaReference | null>(null);
@@ -203,6 +206,7 @@ const AIChatPage: React.FC = () => {
// @ 提及相关状态
const [mentionVisible, setMentionVisible] = useState(false);
const mentionInputRef = useRef<any>(null);
const isReEditingRef = useRef(false);
const [mentionPosition, setMentionPosition] = useState({ top: 0, left: 0 });
// 数字转中文数字(一、二、三、四)
@@ -539,16 +543,20 @@ const AIChatPage: React.FC = () => {
}, [gen_list.length, scrollToBottom]);
useEffect(() => {
const newOptions = mediaType === 'image' ? enginesele.image : enginesele.video;
if (isReEditingRef.current) {
isReEditingRef.current = false;
return;
}
if (!enginesLoaded) return;
const enginesData = Array.isArray(enginesele) ? {} : enginesele;
const newOptions = mediaType === 'image' ? enginesData.image : enginesData.video;
if (newOptions && newOptions.length > 0) {
// 如果当前 countType 不在新选项中,重置为第一个选项
const isValidOption = newOptions.some((item: any) => item.id === countType);
if (!isValidOption) {
setCountType(newOptions[0].id);
}
}
}, [mediaType, enginesele]);
}, [mediaType, enginesele, enginesLoaded]);
// 点击外部关闭弹窗
useEffect(() => {
@@ -570,17 +578,25 @@ const AIChatPage: React.FC = () => {
if (showVideoSettingsModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowVideoSettingsModal(false);
}
// 关闭参考模式选择弹窗
if (referenceModeDropdownVisible && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setReferenceModeDropdownVisible(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showMediaTypeModal, showEngineModal, showImageSettingsModal, showVideoSettingsModal]);
}, [showMediaTypeModal, showEngineModal, showImageSettingsModal, showVideoSettingsModal, referenceModeDropdownVisible]);
useEffect(() => {
if (isReEditingRef.current) {
isReEditingRef.current = false;
return;
}
if (mediaType !== 'video') return;
const engine = enginesele?.video?.find((e: any) => e.id === countType);
const engine = enginesData.video?.find((e: any) => e.id === countType);
if (!engine) return;
const supportsFLF = engine.supportsFirstLastFrame ?? false;
@@ -602,6 +618,7 @@ const AIChatPage: React.FC = () => {
// console.log('引擎', data);
setEnginesele(data.engine);
setEnginesLoaded(true);
// 如果是视频模式,初始化视频参数选项
if (data.engine.video && data.engine.video.length > 0) {
@@ -2107,7 +2124,7 @@ const AIChatPage: React.FC = () => {
)}
{/* 遍历消息列表 */}
{(() => {
const msgApi = message;
const msgApi = antdMessage;
return gen_list.map((msg) => (
<div
key={msg.id}
@@ -2183,7 +2200,61 @@ const AIChatPage: React.FC = () => {
onClick={(e) => {
e.stopPropagation();
setInputValue(msg.originalPrompt || '');
let engineFound = true;
const engineId = msg.engineId || msg.engineSnapshot?.id || msg.engine_id;
if (engineId && msg.genType) {
const enginesData = Array.isArray(enginesele) ? {} : enginesele;
const engineList = msg.genType === 'image' ? enginesData.image : enginesData.video;
const engine = engineList?.find((e: any) => e.id === engineId);
if (engine) {
setCountType(engine.id);
if (msg.genType === 'video') {
setEngineOptions({
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
});
}
if (msg.genType === 'image') {
const supportedSizes = engine.supportedSizes || {};
const resolutionLevels = Object.keys(supportedSizes).sort((a, b) => {
const levelOrder = { '1K': 0, '2K': 1, '4K': 2 };
return (levelOrder[a] || 0) - (levelOrder[b] || 0);
});
const primaryResolution = resolutionLevels[0] || '2K';
const supportedResolutions = Object.keys(supportedSizes[primaryResolution] || {});
const newRatioOptions = supportedResolutions.map((res: any) => ({
value: res,
label: res,
}));
setRatioOptions(newRatioOptions);
setCurrentEngineSupportedSizes(supportedSizes);
setResolutionOptions(resolutionLevels.map((level) => {
const match = level.match(/(\d+)K/);
const num = match ? parseInt(match[1]) : 1;
const labels: Record<number, string> = { 1: '标清', 2: '高清', 4: '超清' };
return {
value: level,
label: `${labels[num] || '高清'} ${level}`,
};
}));
if (supportedSizes[primaryResolution] && supportedSizes[primaryResolution][supportedResolutions[0]]) {
const defaultSize = supportedSizes[primaryResolution][supportedResolutions[0]];
const [w, h] = defaultSize.split(/[×x]/);
setWidth(Number(w));
setHeight(Number(h));
setSelectedRatio(supportedResolutions[0]);
setSelectedResolution(primaryResolution);
}
}
} else {
msgApi.error('该引擎已删除,无法重新编辑');
engineFound = false;
}
}
if (!engineFound) return;
if (msg.genType) {
isReEditingRef.current = true;
setMediaType(msg.genType);
}
if (msg.mediaReferences && msg.mediaReferences.length > 0) {
@@ -2206,12 +2277,43 @@ const AIChatPage: React.FC = () => {
})));
setFirstFrame(null);
setLastFrame(null);
if (msg.genType === 'video') {
const videoEngine = enginesData.video?.find((e: any) => e.id === engineId);
if (videoEngine) {
const supportsUR = videoEngine.supportsUniversalReference ?? true;
const supportsFLF = videoEngine.supportsFirstLastFrame ?? false;
if (supportsUR) {
setReferenceMode('universal');
} else if (supportsFLF) {
setReferenceMode('first_last_frame');
} else {
setReferenceMode('universal');
}
} else {
setReferenceMode('universal');
}
} else {
setReferenceMode('universal');
}
}
} else {
setCurrentMedia([]);
setFirstFrame(null);
setLastFrame(null);
if (msg.genType === 'video') {
const videoEngine = enginesele?.video?.find((e: any) => e.id === engineId);
if (videoEngine) {
const supportsUR = videoEngine.supportsUniversalReference ?? true;
const supportsFLF = videoEngine.supportsFirstLastFrame ?? false;
if (supportsUR) {
setReferenceMode('universal');
} else if (supportsFLF) {
setReferenceMode('first_last_frame');
} else {
setReferenceMode('universal');
}
}
}
}
msgApi.success('已加载到编辑区');
}}
@@ -3441,7 +3543,7 @@ const AIChatPage: React.FC = () => {
fontWeight: 500,
color: '#2f3440',
}}>
{mediaType === 'image' ? enginesele.image?.find((e: any) => e.id === countType)?.name : enginesele.video?.find((e: any) => e.id === countType)?.name || '选择引擎'}
{mediaType === 'image' ? enginesData.image?.find((e: any) => e.id === countType)?.name : enginesData.video?.find((e: any) => e.id === countType)?.name || '选择引擎'}
</Text>
<CaretDownOutlined style={{ fontSize: 10, color: '#8b5cf6', marginLeft: 'auto' }} />
</button>
@@ -3480,7 +3582,7 @@ const AIChatPage: React.FC = () => {
flexDirection: 'column',
gap: 4,
}}>
{(mediaType === 'image' ? enginesele.image : enginesele.video)?.map((engine: any) => (
{(mediaType === 'image' ? enginesData.image : enginesData.video)?.map((engine: any) => (
<button
key={engine.id}
onClick={() => {
+1 -1
View File
@@ -1037,7 +1037,7 @@ const HomePage: React.FC = () => {
</div>
{/* 素材案例网格 */}
<div className="stagger-children" style={{ display: 'flex', flexWrap: 'wrap', gap: 16, justifyContent: 'space-between', overflowX: 'auto', paddingBottom: 8 }}>
<div className="stagger-children" style={{ display: 'flex', flexWrap: 'wrap', gap: 16, justifyContent: 'flex-start', overflowX: 'auto', paddingBottom: 8 }}>
{caseAssets.length === 0 ? (
<div style={{
flex: 1,
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { Button, Card, Result, Typography } from 'antd';
import { Button, Card, Result, Typography, message } from 'antd';
const successStatuses = new Set(['group_active', 'callback_success']);
@@ -9,6 +9,40 @@ const PrivatePortraitAuthorizeResult: React.FC = () => {
const resultCode = params.get('resultCode') || '';
const isSuccess = successStatuses.has(status) || resultCode === '10000';
const handleClose = () => {
const isWeChat = /MicroMessenger/i.test(navigator.userAgent);
const isAlipay = /AlipayClient/i.test(navigator.userAgent);
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
if (isWeChat) {
try {
window.opener = null;
window.open('', '_self');
window.close();
} catch {
message.info('请点击右上角关闭按钮');
}
} else if (isAlipay) {
try {
window.close();
} catch {
message.info('请点击左上角返回');
}
} else if (isMobile) {
try {
window.history.back();
} catch {
window.location.href = '/';
}
} else {
try {
window.close();
} catch {
window.location.href = '/';
}
}
};
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f8fafc', padding: 20 }}>
<Card style={{ width: '100%', maxWidth: 520, borderRadius: 18 }}>
@@ -17,11 +51,11 @@ const PrivatePortraitAuthorizeResult: React.FC = () => {
title={isSuccess ? '真人认证已完成' : '真人认证未完成'}
subTitle={isSuccess ? '请回到电脑端查看,项目组已创建成功。' : '请回到电脑端重新发起创建项目组。'}
extra={[
<Button key="close" type="primary" onClick={() => window.close()}></Button>,
<Button key="close" type="primary" onClick={handleClose}></Button>,
]}
/>
<Typography.Paragraph type="secondary" style={{ textAlign: 'center', marginBottom: 0 }}>
{status || '-'}{resultCode || '-'}
<Typography.Paragraph type="secondary" style={{ textAlign: 'center', marginBottom: 0, fontSize: 13, marginTop: 12 }}>
</Typography.Paragraph>
</Card>
</div>
+37 -7
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button, Drawer, Input, Modal, Popconfirm, Spin, Table, Tag, Tooltip, Upload, message } from 'antd';
import { ArrowLeftOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
import { ArrowLeftOutlined, DeleteOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
import {uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotReplicationDetail, reanalyzeSegment, retrySplit, Removelist, removeCreate, reanalyzeShotReplication, splitCustom, uploadImage } from '../api';
@@ -1133,21 +1133,51 @@ function RemoveInfo() {
<span style={{ color: '#ef4444' }}>*</span>
</label>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ width: 120, height: 120, borderRadius: 8, border: '2px dashed rgba(99, 102, 241, 0.3)', background: 'rgba(99, 102, 241, 0.02)', position: 'relative', overflow: 'hidden' }}>
{productImage ? (
<>
<img
src={productImage}
alt="产品图"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
<button
onClick={() => {
setProductImage('');
setProductImageResourceId('');
}}
style={{
position: 'absolute',
bottom: 4,
right: 4,
width: 24,
height: 24,
borderRadius: '50%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<DeleteOutlined style={{ color: '#fff', fontSize: 12 }} />
</button>
</>
) : (
<Upload
listType="picture-card"
onChange={handleProductImageChange}
beforeUpload={beforeUploadProductImage}
maxCount={1}
accept="image/*"
style={{ width: 120, height: 120, borderRadius: 8, border: '2px dashed rgba(99, 102, 241, 0.3)', background: 'rgba(99, 102, 241, 0.02)' }}
style={{ width: '100%', height: '100%' }}
>
{!productImage && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, width: '100%', height: '100%' }}>
<PlusOutlined style={{ fontSize: 20, color: '#6366f1' }} />
<span style={{ fontSize: 12, color: '#64748b' }}></span>
</div>
)}
</Upload>
)}
</div>
</div>
</div>