feat: 增加HLS视频协议支持并优化招标项目管理

This commit is contained in:
2026-04-27 14:54:41 +08:00
parent 16b481e280
commit 0ade339435
6 changed files with 1599 additions and 144 deletions

View File

@@ -11,6 +11,7 @@
"dependencies": {
"axios": "^1.7.7",
"element-plus": "^2.8.1",
"hls.js": "^1.6.16",
"less": "^4.2.0",
"naive-ui": "^2.44.1",
"vue": "^3.4.37",

View File

@@ -20,11 +20,7 @@
'menu-entry--children': Boolean(item.children?.length),
}"
>
<button
type="button"
class="menu-strip__item"
@click="handleMenuClick(item)"
>
<button type="button" class="menu-strip__item" @click="handleMenuClick(item)">
{{ item.label }}
</button>
@@ -49,22 +45,11 @@
</nav>
<div class="action-strip">
<button
type="button"
class="ai-badge"
aria-label="AI 助手"
@click="handlePrototypeNotice"
>
<button type="button" class="ai-badge" aria-label="AI 助手" @click="handlePrototypeNotice">
<span class="ai-badge__icon">AI</span>
</button>
<button
type="button"
class="log-button"
@click="handlePrototypeNotice"
>
导出工作日志
</button>
<button type="button" class="log-button" @click="handlePrototypeNotice">导出工作日志</button>
<button
type="button"
@@ -142,7 +127,12 @@ const menuItems = [
{
label: '招投标信息管理',
routeName: 'TenderProjectManagement',
activeNames: ['TenderProjectManagement', 'AwardProjectManagement', 'SectorManagement'],
activeNames: [
'TenderProjectManagement',
'AwardProjectManagement',
'SectorManagement',
'TenderTaskConfig',
],
children: [
{
label: '招标项目管理',
@@ -158,6 +148,10 @@ const menuItems = [
label: '板块管理',
routeName: 'SectorManagement',
},
{
label: '任务配置',
routeName: 'TenderTaskConfig',
},
],
},
{ label: '系统管理' },

View File

@@ -23,4 +23,12 @@ export default [
title: '板块管理',
},
},
{
path: '/tendering/task-config',
name: 'TenderTaskConfig',
component: () => import('@/views/tendering/task/index.vue'),
meta: {
title: '任务配置',
},
},
];

View File

@@ -47,6 +47,19 @@
class="filter-field filter-field--code"
placeholder="国标号"
/>
<el-select
v-model="filters.videoProtocol"
clearable
class="filter-field"
placeholder="视频协议"
>
<el-option
v-for="option in protocolOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-select
v-model="filters.status"
clearable
@@ -63,10 +76,8 @@
<div class="panel-card">
<div class="table-toolbar">
<div class="table-toolbar__group">
<el-button type="primary" @click="openCreateDialog">新增视频点位</el-button>
<el-button :disabled="!selectedIds.length" @click="deleteSelected">
批量删除
</el-button>
<el-button type="primary" @click="openCreateDialog">新增监控视频</el-button>
<el-button :disabled="!selectedIds.length" @click="deleteSelected">批量删除</el-button>
</div>
<div class="table-toolbar__summary">
@@ -83,19 +94,29 @@
>
<el-table-column type="selection" width="52" />
<el-table-column label="工程名称" prop="projectName" min-width="220" show-overflow-tooltip />
<el-table-column label="主要工程地点" prop="projectLocation" min-width="200" show-overflow-tooltip />
<el-table-column label="摄像机点位" prop="cameraPoint" min-width="180" show-overflow-tooltip />
<el-table-column label="通道号" prop="channelCode" min-width="140" show-overflow-tooltip />
<el-table-column
label="国标号"
prop="nationalStandardCode"
min-width="210"
label="主要工程地点"
prop="projectLocation"
min-width="180"
show-overflow-tooltip
>
/>
<el-table-column label="摄像机点位" prop="cameraPoint" min-width="180" show-overflow-tooltip />
<el-table-column label="通道号" prop="channelCode" min-width="120" show-overflow-tooltip />
<el-table-column label="国标号" prop="nationalStandardCode" min-width="210" show-overflow-tooltip>
<template #default="{ row }">
<span class="code-badge">{{ row.nationalStandardCode }}</span>
</template>
</el-table-column>
<el-table-column label="视频协议" prop="videoProtocol" min-width="120">
<template #default="{ row }">
<span class="protocol-badge">{{ formatProtocol(row.videoProtocol) }}</span>
</template>
</el-table-column>
<el-table-column label="播放链接" prop="playUrl" min-width="280" show-overflow-tooltip>
<template #default="{ row }">
<span class="link-text">{{ row.playUrl }}</span>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" min-width="110">
<template #default="{ row }">
<span
@@ -115,11 +136,7 @@
直播
</button>
<button type="button" class="row-action" @click="openEditDialog(row)">编辑</button>
<button
type="button"
class="row-action row-action--danger"
@click="deleteRecord(row)"
>
<button type="button" class="row-action row-action--danger" @click="deleteRecord(row)">
删除
</button>
</div>
@@ -147,7 +164,7 @@
<el-dialog
v-model="dialogVisible"
:title="`${editingRecordId ? '编辑' : '新增'}监控视频`"
width="760px"
width="820px"
destroy-on-close
>
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="110px">
@@ -169,10 +186,7 @@
</el-select>
</el-form-item>
<el-form-item label="主要工程地点">
<el-input
:model-value="formModel.projectLocation || '请选择工程名称后自动带出'"
disabled
/>
<el-input :model-value="formModel.projectLocation || '选择项目后自动带出'" disabled />
</el-form-item>
<el-form-item label="摄像机点位" prop="cameraPoint">
<el-input v-model="formModel.cameraPoint" placeholder="请输入摄像机点位" />
@@ -183,6 +197,19 @@
<el-form-item label="国标号" prop="nationalStandardCode">
<el-input v-model="formModel.nationalStandardCode" placeholder="请输入国标号" />
</el-form-item>
<el-form-item label="视频协议" prop="videoProtocol">
<el-select v-model="formModel.videoProtocol" placeholder="请选择视频协议" class="select-field">
<el-option
v-for="option in protocolOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="播放链接" prop="playUrl" class="form-grid__span-2">
<el-input v-model="formModel.playUrl" placeholder="请输入播放链接" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formModel.status" placeholder="请选择状态" class="select-field">
<el-option label="在线" value="在线" />
@@ -206,7 +233,7 @@
<el-dialog
v-model="liveDialogVisible"
title="视频直播"
width="900px"
width="960px"
destroy-on-close
@closed="handleLiveDialogClosed"
>
@@ -236,6 +263,10 @@
<span class="live-meta-item__label">通道号</span>
<strong class="live-meta-item__value">{{ liveRecord.channelCode || '-' }}</strong>
</div>
<div class="live-meta-item">
<span class="live-meta-item__label">视频协议</span>
<strong class="live-meta-item__value">{{ formatProtocol(liveRecord.videoProtocol) }}</strong>
</div>
<div class="live-meta-item">
<span class="live-meta-item__label">国标号</span>
<strong class="live-meta-item__value">{{ liveRecord.nationalStandardCode }}</strong>
@@ -258,7 +289,7 @@
></video>
<div class="live-screen__overlay"></div>
<div v-if="isStartingLive" class="live-screen__mask">
<span>正在连接摄像头...</span>
<span>正在连接视频流...</span>
</div>
<div v-else-if="liveError" class="live-screen__mask live-screen__mask--error">
<p>{{ liveError }}</p>
@@ -277,6 +308,7 @@
</template>
<script setup>
import Hls from 'hls.js';
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import storage from '@/utils/storage';
@@ -284,6 +316,8 @@ import storage from '@/utils/storage';
const STORAGE_KEY = 'admin-pc:supervision-videos';
const DEFAULT_WEBRTC_STREAM_URL =
'webrtc://104.168.15.133:15123/proxy/sms/index/api/webrtc?app=rtp&stream=chhv1os&type=play';
const DEFAULT_HLS_STREAM_URL = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
const locationOptions = [
'江苏省/南京市',
'江苏省/无锡市',
@@ -300,6 +334,11 @@ const locationOptions = [
'江苏省/宿迁市',
];
const protocolOptions = [
{ label: 'WebRTC', value: 'webrtc' },
{ label: 'HLS', value: 'hls' },
];
const initialRecords = [
{
id: 'video_1',
@@ -308,6 +347,8 @@ const initialRecords = [
cameraPoint: '1号塔吊西南侧',
channelCode: 'CH-001',
nationalStandardCode: 'GB34020000001320000001',
videoProtocol: 'webrtc',
playUrl: DEFAULT_WEBRTC_STREAM_URL,
status: '在线',
creator: '王敏',
updatedAt: '2026-04-27 09:12:00',
@@ -319,6 +360,8 @@ const initialRecords = [
cameraPoint: '南门车辆通道',
channelCode: 'CH-002',
nationalStandardCode: 'GB34020000001320000002',
videoProtocol: 'hls',
playUrl: DEFAULT_HLS_STREAM_URL,
status: '在线',
creator: '李峰',
updatedAt: '2026-04-27 09:18:00',
@@ -330,6 +373,8 @@ const initialRecords = [
cameraPoint: '主设备吊装区',
channelCode: 'CH-003',
nationalStandardCode: 'GB34020000001320000003',
videoProtocol: 'webrtc',
playUrl: DEFAULT_WEBRTC_STREAM_URL,
status: '离线',
creator: '陈宇',
updatedAt: '2026-04-27 09:26:00',
@@ -341,6 +386,8 @@ const initialRecords = [
cameraPoint: '材料堆场东口',
channelCode: 'CH-004',
nationalStandardCode: 'GB34020000001320000004',
videoProtocol: 'hls',
playUrl: DEFAULT_HLS_STREAM_URL,
status: '在线',
creator: '赵晴',
updatedAt: '2026-04-27 09:33:00',
@@ -352,6 +399,8 @@ const initialRecords = [
cameraPoint: '外立面登高作业区',
channelCode: 'CH-005',
nationalStandardCode: 'GB34020000001320000005',
videoProtocol: 'webrtc',
playUrl: DEFAULT_WEBRTC_STREAM_URL,
status: '离线',
creator: '顾晨',
updatedAt: '2026-04-27 09:44:00',
@@ -363,6 +412,8 @@ const initialRecords = [
cameraPoint: '钢结构拼装场',
channelCode: 'CH-006',
nationalStandardCode: 'GB34020000001320000006',
videoProtocol: 'hls',
playUrl: DEFAULT_HLS_STREAM_URL,
status: '在线',
creator: '刘颖',
updatedAt: '2026-04-27 09:55:00',
@@ -374,6 +425,8 @@ const initialRecords = [
cameraPoint: '桥面摊铺区',
channelCode: 'CH-007',
nationalStandardCode: 'GB34020000001320000007',
videoProtocol: 'webrtc',
playUrl: DEFAULT_WEBRTC_STREAM_URL,
status: '在线',
creator: '王敏',
updatedAt: '2026-04-27 10:08:00',
@@ -385,6 +438,8 @@ const initialRecords = [
cameraPoint: '访客登记口',
channelCode: 'CH-008',
nationalStandardCode: 'GB34020000001320000008',
videoProtocol: 'hls',
playUrl: DEFAULT_HLS_STREAM_URL,
status: '离线',
creator: '李峰',
updatedAt: '2026-04-27 10:14:00',
@@ -396,6 +451,8 @@ const initialRecords = [
cameraPoint: 'AGV调度主通道',
channelCode: 'CH-009',
nationalStandardCode: 'GB34020000001320000009',
videoProtocol: 'webrtc',
playUrl: DEFAULT_WEBRTC_STREAM_URL,
status: '在线',
creator: '陈宇',
updatedAt: '2026-04-27 10:27:00',
@@ -407,6 +464,8 @@ const initialRecords = [
cameraPoint: '冷机吊装通道',
channelCode: 'CH-010',
nationalStandardCode: 'GB34020000001320000010',
videoProtocol: 'hls',
playUrl: DEFAULT_HLS_STREAM_URL,
status: '离线',
creator: '赵晴',
updatedAt: '2026-04-27 10:36:00',
@@ -425,6 +484,7 @@ const liveRecord = ref(null);
const isStartingLive = ref(false);
const liveError = ref('');
const rtcPeerConnection = ref(null);
const hlsPlayer = ref(null);
const filters = reactive({
projectName: '',
@@ -432,6 +492,7 @@ const filters = reactive({
cameraPoint: '',
channelCode: '',
nationalStandardCode: '',
videoProtocol: '',
status: '',
});
@@ -441,18 +502,27 @@ const createEmptyForm = () => ({
cameraPoint: '',
channelCode: '',
nationalStandardCode: '',
videoProtocol: 'webrtc',
playUrl: DEFAULT_WEBRTC_STREAM_URL,
status: '在线',
creator: '管理员',
updatedAt: '',
});
const normalizeRecord = (record) => ({
const normalizeRecord = (record) => {
const protocol = normalizeProtocol(record?.videoProtocol);
const playUrl = normalizePlayUrl(protocol, record?.playUrl);
return {
...createEmptyForm(),
...record,
projectLocation: normalizeProjectLocation(record?.projectLocation),
videoProtocol: protocol,
playUrl,
creator: record?.creator || '管理员',
updatedAt: record?.updatedAt || formatCurrentDateTime(),
});
};
};
const buildSeededRecords = () => {
const storedRecords = storage.getLocalStorage(STORAGE_KEY);
@@ -474,21 +544,20 @@ const buildSeededRecords = () => {
const records = ref(buildSeededRecords());
const formModel = reactive(createEmptyForm());
const projectOptions = computed(() => {
const projectMap = new Map();
records.value.forEach((record) => {
if (!record.projectName) {
if (!record.projectName || projectMap.has(record.projectName)) {
return;
}
if (!projectMap.has(record.projectName)) {
projectMap.set(record.projectName, {
label: record.projectName,
value: record.projectName,
location: normalizeProjectLocation(record.projectLocation),
});
}
});
return [...projectMap.values()];
@@ -499,34 +568,32 @@ const formRules = {
cameraPoint: [{ required: true, message: '请输入摄像机点位', trigger: 'blur' }],
channelCode: [{ required: true, message: '请输入通道号', trigger: 'blur' }],
nationalStandardCode: [{ required: true, message: '请输入国标号', trigger: 'blur' }],
videoProtocol: [{ required: true, message: '请选择视频协议', trigger: 'change' }],
playUrl: [{ required: true, message: '请输入播放链接', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
creator: [{ required: true, message: '请输入创建人', trigger: 'blur' }],
};
const filteredRecords = computed(() =>
records.value.filter((record) => {
const matchesProjectName = filters.projectName
? record.projectName === filters.projectName
: true;
const matchesProjectName = filters.projectName ? record.projectName === filters.projectName : true;
const matchesLocation = filters.projectLocation
? record.projectLocation === filters.projectLocation
: true;
const matchesCameraPoint = filters.cameraPoint
? record.cameraPoint.toLowerCase().includes(filters.cameraPoint.trim().toLowerCase())
: true;
const matchesChannelCode = filters.channelCode
? record.channelCode.toLowerCase().includes(filters.channelCode.trim().toLowerCase())
: true;
const matchesCode = filters.nationalStandardCode
? record.nationalStandardCode
.toLowerCase()
.includes(filters.nationalStandardCode.trim().toLowerCase())
: true;
const matchesProtocol = filters.videoProtocol
? record.videoProtocol === filters.videoProtocol
: true;
const matchesStatus = filters.status ? record.status === filters.status : true;
return (
@@ -535,6 +602,7 @@ const filteredRecords = computed(() =>
matchesCameraPoint &&
matchesChannelCode &&
matchesCode &&
matchesProtocol &&
matchesStatus
);
}),
@@ -562,6 +630,7 @@ const resetFilters = () => {
filters.cameraPoint = '';
filters.channelCode = '';
filters.nationalStandardCode = '';
filters.videoProtocol = '';
filters.status = '';
currentPage.value = 1;
};
@@ -574,7 +643,7 @@ const openCreateDialog = () => {
const openEditDialog = (record) => {
editingRecordId.value = record.id;
Object.assign(formModel, createEmptyForm(), record);
Object.assign(formModel, createEmptyForm(), normalizeRecord(record));
syncProjectLocation(record.projectName);
dialogVisible.value = true;
};
@@ -589,13 +658,15 @@ const submitForm = async () => {
? records.value.find((record) => record.id === editingRecordId.value) || null
: null;
const payload = {
const protocol = normalizeProtocol(formModel.videoProtocol);
const payload = normalizeRecord({
...(existingRecord || {}),
...createEmptyForm(),
...formModel,
id: editingRecordId.value || `video_${Date.now()}`,
videoProtocol: protocol,
playUrl: normalizePlayUrl(protocol, formModel.playUrl),
updatedAt: formatCurrentDateTime(),
};
});
if (editingRecordId.value) {
records.value = records.value.map((record) =>
@@ -676,7 +747,7 @@ const openLiveDialog = async (record) => {
return;
}
liveRecord.value = record;
liveRecord.value = normalizeRecord(record);
liveDialogVisible.value = true;
await startLiveStream();
};
@@ -693,7 +764,7 @@ const handleLiveDialogClosed = () => {
};
const startLiveStream = async () => {
if (!liveDialogVisible.value) {
if (!liveDialogVisible.value || !liveRecord.value) {
return;
}
@@ -708,6 +779,57 @@ const startLiveStream = async () => {
throw new Error('video element unavailable');
}
const protocol = normalizeProtocol(liveRecord.value.videoProtocol);
const playUrl = normalizePlayUrl(protocol, liveRecord.value.playUrl);
if (protocol === 'hls') {
await startHlsStream(playUrl);
} else {
await startWebRtcStream(playUrl);
}
} catch (error) {
liveError.value = error?.message || '视频流连接失败,请稍后重试。';
stopLiveStream();
} finally {
isStartingLive.value = false;
}
};
const startHlsStream = async (url) => {
if (!url) {
throw new Error('HLS 播放链接不能为空');
}
const video = liveVideoRef.value;
if (Hls.isSupported()) {
const hls = new Hls();
hlsPlayer.value = hls;
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.ERROR, (_, data) => {
if (data?.fatal) {
liveError.value = 'HLS 视频流播放失败,请检查播放链接。';
stopLiveStream();
}
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
} else {
throw new Error('当前浏览器不支持 HLS 播放');
}
const playTask = video.play?.();
if (playTask?.catch) {
await playTask.catch(() => undefined);
}
};
const startWebRtcStream = async (url) => {
if (!url) {
throw new Error('WebRTC 播放链接不能为空');
}
const peerConnection = new RTCPeerConnection();
rtcPeerConnection.value = peerConnection;
@@ -741,7 +863,7 @@ const startLiveStream = async () => {
await peerConnection.setLocalDescription(offer);
const response = await fetch(resolveWebRtcEndpoint(DEFAULT_WEBRTC_STREAM_URL), {
const response = await fetch(resolveWebRtcEndpoint(url), {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
@@ -765,15 +887,14 @@ const startLiveStream = async () => {
if (playTask?.catch) {
await playTask.catch(() => undefined);
}
} catch (error) {
liveError.value = error?.message || 'WebRTC 视频流连接失败,请稍后重试。';
stopLiveStream();
} finally {
isStartingLive.value = false;
}
};
const stopLiveStream = () => {
if (hlsPlayer.value) {
hlsPlayer.value.destroy();
hlsPlayer.value = null;
}
if (rtcPeerConnection.value) {
rtcPeerConnection.value.ontrack = null;
rtcPeerConnection.value.onconnectionstatechange = null;
@@ -797,6 +918,7 @@ watch(
filters.cameraPoint,
filters.channelCode,
filters.nationalStandardCode,
filters.videoProtocol,
filters.status,
],
() => {
@@ -814,12 +936,16 @@ watch(
{ immediate: true },
);
watch(
liveDialogVisible,
(visible) => {
watch(liveDialogVisible, (visible) => {
if (!visible) {
stopLiveStream();
}
});
watch(
() => formModel.videoProtocol,
(protocol) => {
formModel.playUrl = normalizePlayUrl(protocol, formModel.playUrl);
},
);
@@ -839,6 +965,20 @@ function formatCurrentDateTime() {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function normalizeProtocol(value) {
return String(value || 'webrtc').toLowerCase() === 'hls' ? 'hls' : 'webrtc';
}
function normalizePlayUrl(protocol, value) {
const trimmed = String(value || '').trim();
if (trimmed) {
return trimmed;
}
return normalizeProtocol(protocol) === 'hls' ? DEFAULT_HLS_STREAM_URL : DEFAULT_WEBRTC_STREAM_URL;
}
function normalizeProjectLocation(value) {
if (!value) {
return '';
@@ -873,6 +1013,10 @@ function syncProjectLocation(projectName) {
formModel.projectLocation = matchedProject?.location || '';
}
function formatProtocol(value) {
return normalizeProtocol(value) === 'hls' ? 'HLS' : 'WebRTC';
}
function resolveWebRtcEndpoint(url) {
if (!url) {
return '';
@@ -1041,6 +1185,21 @@ function extractAnswerSdp(payload, rawText) {
background: linear-gradient(135deg, rgba(226, 239, 255, 0.92), rgba(242, 248, 255, 0.98));
}
.protocol-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 64px;
padding: 5px 10px;
color: #1557b0;
font-size: 12px;
background: rgba(21, 87, 176, 0.1);
}
.link-text {
color: #385372;
}
.status-badge {
display: inline-flex;
align-items: center;
@@ -1103,6 +1262,10 @@ function extractAnswerSdp(payload, rawText) {
gap: 4px 18px;
}
.form-grid__span-2 {
grid-column: 1 / -1;
}
.live-panel {
display: flex;
flex-direction: column;
@@ -1183,6 +1346,7 @@ function extractAnswerSdp(payload, rawText) {
color: #182b44;
font-size: 14px;
line-height: 1.5;
word-break: break-all;
}
.live-screen {
@@ -1274,6 +1438,10 @@ function extractAnswerSdp(payload, rawText) {
grid-template-columns: 1fr;
}
.form-grid__span-2 {
grid-column: auto;
}
.live-screen,
.live-screen__video {
min-height: 280px;

View File

@@ -3,6 +3,9 @@
title="中标项目管理"
storage-key="admin-pc:award-projects"
date-range-prop="awardDate"
amount-range-prop="awardAmount"
amount-range-unit="万元"
:amount-scale="10000"
:columns="columns"
:form-fields="formFields"
:initial-records="initialRecords"
@@ -19,7 +22,7 @@ const columns = [
{ prop: 'sectionName', label: '标段名称', minWidth: 220 },
{ prop: 'constructionCompany', label: '建设单位名称', minWidth: 220 },
{ prop: 'awardCompany', label: '中标人名称', minWidth: 220 },
{ prop: 'awardAmount', label: '中标', minWidth: 160, type: 'currency' },
{ prop: 'awardAmount', label: '中标金额', minWidth: 160, type: 'wanAmount' },
{ prop: 'awardDate', label: '中标时间', minWidth: 140 },
{ prop: 'detailLink', label: '详情链接', minWidth: 96, width: 96, type: 'link' },
];
@@ -31,7 +34,7 @@ const formFields = [
{ prop: 'sectionName', label: '标段名称', type: 'text' },
{ prop: 'constructionCompany', label: '建设单位名称', type: 'text' },
{ prop: 'awardCompany', label: '中标人名称', type: 'text' },
{ prop: 'awardAmount', label: '中标', type: 'amount' },
{ prop: 'awardAmount', label: '中标金额', type: 'amount' },
{ prop: 'awardDate', label: '中标时间', type: 'date' },
{ prop: 'detailLink', label: '详情链接', type: 'url' },
];
@@ -48,7 +51,7 @@ const initialRecords = [
awardAmount: 17980000,
awardDate: '2026-04-22',
detailLink: 'https://example.com/award/1',
provinceCity: '江苏省南京市',
provinceCity: '江苏省/南京市',
sectorType: '基础设施',
projectType: '工程类',
},
@@ -63,7 +66,7 @@ const initialRecords = [
awardAmount: 8860000,
awardDate: '2026-04-20',
detailLink: 'https://example.com/award/2',
provinceCity: '江苏省苏州市',
provinceCity: '江苏省/苏州市',
sectorType: '智慧园区',
projectType: '服务类',
},
@@ -78,7 +81,7 @@ const initialRecords = [
awardAmount: 13250000,
awardDate: '2026-04-19',
detailLink: 'https://example.com/award/3',
provinceCity: '江苏省徐州市',
provinceCity: '江苏省/徐州市',
sectorType: '能源环保',
projectType: '采购类',
},
@@ -93,7 +96,7 @@ const initialRecords = [
awardAmount: 5980000,
awardDate: '2026-04-17',
detailLink: 'https://example.com/award/4',
provinceCity: '江苏省常州市',
provinceCity: '江苏省/常州市',
sectorType: '智慧园区',
projectType: '采购类',
},
@@ -108,7 +111,7 @@ const initialRecords = [
awardAmount: 11360000,
awardDate: '2026-04-16',
detailLink: 'https://example.com/award/5',
provinceCity: '江苏省南通市',
provinceCity: '江苏省/南通市',
sectorType: '公共服务',
projectType: '服务类',
},
@@ -123,7 +126,7 @@ const initialRecords = [
awardAmount: 14750000,
awardDate: '2026-04-14',
detailLink: 'https://example.com/award/6',
provinceCity: '江苏省扬州市',
provinceCity: '江苏省/扬州市',
sectorType: '智能制造',
projectType: '采购类',
},
@@ -138,7 +141,7 @@ const initialRecords = [
awardAmount: 26880000,
awardDate: '2026-04-12',
detailLink: 'https://example.com/award/7',
provinceCity: '江苏省镇江市',
provinceCity: '江苏省/镇江市',
sectorType: '基础设施',
projectType: '工程类',
},
@@ -153,7 +156,7 @@ const initialRecords = [
awardAmount: 3620000,
awardDate: '2026-04-10',
detailLink: 'https://example.com/award/8',
provinceCity: '江苏省盐城市',
provinceCity: '江苏省/盐城市',
sectorType: '能源环保',
projectType: '服务类',
},
@@ -168,7 +171,7 @@ const initialRecords = [
awardAmount: 8450000,
awardDate: '2026-04-08',
detailLink: 'https://example.com/award/9',
provinceCity: '江苏省连云港市',
provinceCity: '江苏省/连云港市',
sectorType: '物流仓储',
projectType: '其他',
},
@@ -183,7 +186,7 @@ const initialRecords = [
awardAmount: 21400000,
awardDate: '2026-04-06',
detailLink: 'https://example.com/award/10',
provinceCity: '江苏省宿迁市',
provinceCity: '江苏省/宿迁市',
sectorType: '文旅配套',
projectType: '工程类',
},

File diff suppressed because it is too large Load Diff