feat: update demo page + add city_id for media and articles
This commit is contained in:
@@ -56,7 +56,7 @@ class ApiStore {
|
|||||||
carrier: GetCarrierResponse | null = null;
|
carrier: GetCarrierResponse | null = null;
|
||||||
city: GetCityResponse | null = null;
|
city: GetCityResponse | null = null;
|
||||||
|
|
||||||
private positionIndex = 0;
|
positionIndex = 0;
|
||||||
private positionInterval: ReturnType<typeof setInterval> | null = null;
|
private positionInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
simulationSpeed = 1;
|
simulationSpeed = 1;
|
||||||
|
|||||||
@@ -797,7 +797,9 @@ export const WebGLMap = observer(() => {
|
|||||||
const textBlockPositionX = rx + labelOffsetX;
|
const textBlockPositionX = rx + labelOffsetX;
|
||||||
const textBlockPositionY = ry + labelOffsetY;
|
const textBlockPositionY = ry + labelOffsetY;
|
||||||
|
|
||||||
const approximateTextWidth = st.name.length * fontSize * 0.6;
|
const nameLines = st.name.replace(/\\n/g, '\n').split('\n');
|
||||||
|
const longestLine = nameLines.reduce((a: string, b: string) => a.length > b.length ? a : b, '');
|
||||||
|
const approximateTextWidth = longestLine.length * fontSize * 0.6;
|
||||||
const textWidthInMapCoords = approximateTextWidth / scale;
|
const textWidthInMapCoords = approximateTextWidth / scale;
|
||||||
|
|
||||||
let anchorXOffset = 0;
|
let anchorXOffset = 0;
|
||||||
@@ -827,8 +829,8 @@ export const WebGLMap = observer(() => {
|
|||||||
result.push({
|
result.push({
|
||||||
x: sx,
|
x: sx,
|
||||||
y: sy,
|
y: sy,
|
||||||
name: st.name,
|
name: st.name.replace(/\\n/g, '\n'),
|
||||||
sub,
|
sub: sub ? sub.replace(/\\n/g, '\n') : sub,
|
||||||
anchorX: anchorX,
|
anchorX: anchorX,
|
||||||
anchorY: anchorY,
|
anchorY: anchorY,
|
||||||
distance: distanceInPixels,
|
distance: distanceInPixels,
|
||||||
@@ -878,6 +880,32 @@ export const WebGLMap = observer(() => {
|
|||||||
rotationAngle,
|
rotationAngle,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Сегментный индекс каждой упорядоченной станции на routePath (canvas-пространство)
|
||||||
|
const orderedStationSegs = useMemo(() => {
|
||||||
|
if (!orderedRouteStations || !stationData || routePath.length < 4) return [] as number[];
|
||||||
|
return (orderedRouteStations as any[]).map((ordStation) => {
|
||||||
|
const stIdx = stationData.findIndex((s: any) => String(s.id) === String(ordStation.id));
|
||||||
|
if (stIdx < 0) return -1;
|
||||||
|
const sx = stationPoints[stIdx * 2];
|
||||||
|
const sy = stationPoints[stIdx * 2 + 1];
|
||||||
|
if (sx === undefined || sy === undefined) return -1;
|
||||||
|
let best = -1, bestD = Infinity;
|
||||||
|
for (let i = 0; i < routePath.length - 2; i += 2) {
|
||||||
|
const p1x = routePath[i], p1y = routePath[i + 1];
|
||||||
|
const p2x = routePath[i + 2], p2y = routePath[i + 3];
|
||||||
|
const dx = p2x - p1x, dy = p2y - p1y;
|
||||||
|
const len2 = dx * dx + dy * dy;
|
||||||
|
if (!len2) continue;
|
||||||
|
const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2;
|
||||||
|
const cl = Math.max(0, Math.min(1, t));
|
||||||
|
const px = p1x + cl * dx, py = p1y + cl * dy;
|
||||||
|
const d = Math.hypot(sx - px, sy - py);
|
||||||
|
if (d < bestD) { bestD = d; best = i / 2; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
});
|
||||||
|
}, [orderedRouteStations, stationData, stationPoints, routePath]);
|
||||||
|
|
||||||
const sightPoints = useMemo(() => {
|
const sightPoints = useMemo(() => {
|
||||||
if (!sightData || !routeData) return new Float32Array();
|
if (!sightData || !routeData) return new Float32Array();
|
||||||
const centerLat = routeData.center_latitude;
|
const centerLat = routeData.center_latitude;
|
||||||
@@ -1095,6 +1123,8 @@ export const WebGLMap = observer(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const prevPositionIndexRef = useRef<number>(-1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const centerLat = routeData?.center_latitude;
|
const centerLat = routeData?.center_latitude;
|
||||||
const centerLon = routeData?.center_longitude;
|
const centerLon = routeData?.center_longitude;
|
||||||
@@ -1112,7 +1142,14 @@ export const WebGLMap = observer(() => {
|
|||||||
const rx = x * cos - y * sin;
|
const rx = x * cos - y * sin;
|
||||||
const ry = x * sin + y * cos;
|
const ry = x * sin + y * cos;
|
||||||
|
|
||||||
if (apiStore.simulationInstantMove) {
|
const curIdx = apiStore.positionIndex;
|
||||||
|
const prevIdx = prevPositionIndexRef.current;
|
||||||
|
const pathLen = apiStore.route?.path?.length ?? 0;
|
||||||
|
const isWrap = prevIdx >= 0 && pathLen > 0 &&
|
||||||
|
Math.abs(curIdx - prevIdx) > pathLen / 4;
|
||||||
|
prevPositionIndexRef.current = curIdx;
|
||||||
|
|
||||||
|
if (apiStore.simulationInstantMove || isWrap) {
|
||||||
setYellowDotImmediate(rx, ry);
|
setYellowDotImmediate(rx, ry);
|
||||||
} else {
|
} else {
|
||||||
animateYellowDotTo(rx, ry);
|
animateYellowDotTo(rx, ry);
|
||||||
@@ -1164,6 +1201,7 @@ export const WebGLMap = observer(() => {
|
|||||||
gl.vertexAttribPointer(attribs.a_pos, 2, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(attribs.a_pos, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
let tramSegIndex = getCurrentSegIndex();
|
let tramSegIndex = getCurrentSegIndex();
|
||||||
|
const simulationDirection = apiStore.simulationDirection;
|
||||||
|
|
||||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
const desiredRouteWidthCss = 7;
|
const desiredRouteWidthCss = 7;
|
||||||
@@ -1261,23 +1299,69 @@ export const WebGLMap = observer(() => {
|
|||||||
const r1 = ((PATH_COLOR >> 16) & 0xff) / 255;
|
const r1 = ((PATH_COLOR >> 16) & 0xff) / 255;
|
||||||
const g1 = ((PATH_COLOR >> 8) & 0xff) / 255;
|
const g1 = ((PATH_COLOR >> 8) & 0xff) / 255;
|
||||||
const b1 = (PATH_COLOR & 0xff) / 255;
|
const b1 = (PATH_COLOR & 0xff) / 255;
|
||||||
gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
|
const r2 = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
|
||||||
|
const g2 = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
|
||||||
|
const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
||||||
|
|
||||||
if (tramSegIndex >= 0) {
|
|
||||||
const animatedPos = animatedYellowDotPosition;
|
const animatedPos = animatedYellowDotPosition;
|
||||||
if (
|
if (
|
||||||
|
tramSegIndex >= 0 &&
|
||||||
animatedPos &&
|
animatedPos &&
|
||||||
animatedPos.x !== undefined &&
|
animatedPos.x !== undefined &&
|
||||||
animatedPos.y !== undefined
|
animatedPos.y !== undefined
|
||||||
) {
|
) {
|
||||||
|
if (simulationDirection === 1) {
|
||||||
|
// Вперёд: закрашено от начала до трамвая
|
||||||
|
gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
|
||||||
const passedPoints: number[] = [];
|
const passedPoints: number[] = [];
|
||||||
|
|
||||||
for (let i = 0; i <= tramSegIndex; i++) {
|
for (let i = 0; i <= tramSegIndex; i++) {
|
||||||
passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
passedPoints.push(animatedPos.x, animatedPos.y);
|
passedPoints.push(animatedPos.x, animatedPos.y);
|
||||||
|
if (passedPoints.length >= 4) {
|
||||||
|
const thickLineVertices = generateThickLine(
|
||||||
|
new Float32Array(passedPoints),
|
||||||
|
lineWidth,
|
||||||
|
);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
|
||||||
|
}
|
||||||
|
gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
|
||||||
|
const unpassedPoints: number[] = [];
|
||||||
|
unpassedPoints.push(animatedPos.x, animatedPos.y);
|
||||||
|
for (let i = tramSegIndex + 1; i < vertexCount; i++) {
|
||||||
|
unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||||
|
}
|
||||||
|
if (unpassedPoints.length >= 4) {
|
||||||
|
const thickLineVertices = generateThickLine(
|
||||||
|
new Float32Array(unpassedPoints),
|
||||||
|
lineWidth,
|
||||||
|
);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Назад: закрашено от трамвая до конца
|
||||||
|
gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
|
||||||
|
const unpassedPoints: number[] = [];
|
||||||
|
for (let i = 0; i <= tramSegIndex; i++) {
|
||||||
|
unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||||
|
}
|
||||||
|
unpassedPoints.push(animatedPos.x, animatedPos.y);
|
||||||
|
if (unpassedPoints.length >= 4) {
|
||||||
|
const thickLineVertices = generateThickLine(
|
||||||
|
new Float32Array(unpassedPoints),
|
||||||
|
lineWidth,
|
||||||
|
);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
|
||||||
|
}
|
||||||
|
gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
|
||||||
|
const passedPoints: number[] = [];
|
||||||
|
passedPoints.push(animatedPos.x, animatedPos.y);
|
||||||
|
for (let i = tramSegIndex + 1; i < vertexCount; i++) {
|
||||||
|
passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||||
|
}
|
||||||
if (passedPoints.length >= 4) {
|
if (passedPoints.length >= 4) {
|
||||||
const thickLineVertices = generateThickLine(
|
const thickLineVertices = generateThickLine(
|
||||||
new Float32Array(passedPoints),
|
new Float32Array(passedPoints),
|
||||||
@@ -1287,30 +1371,16 @@ export const WebGLMap = observer(() => {
|
|||||||
gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
|
gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Позиция трамвая неизвестна — рисуем весь маршрут серым
|
||||||
const r2 = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
|
|
||||||
const g2 = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
|
|
||||||
const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
|
||||||
gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
|
gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
|
||||||
|
const allPoints: number[] = [];
|
||||||
const animatedPos = animatedYellowDotPosition;
|
for (let i = 0; i < vertexCount; i++) {
|
||||||
if (
|
allPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||||
animatedPos &&
|
|
||||||
animatedPos.x !== undefined &&
|
|
||||||
animatedPos.y !== undefined
|
|
||||||
) {
|
|
||||||
const unpassedPoints: number[] = [];
|
|
||||||
|
|
||||||
unpassedPoints.push(animatedPos.x, animatedPos.y);
|
|
||||||
|
|
||||||
for (let i = tramSegIndex + 1; i < vertexCount; i++) {
|
|
||||||
unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
|
||||||
}
|
}
|
||||||
|
if (allPoints.length >= 4) {
|
||||||
if (unpassedPoints.length >= 4) {
|
|
||||||
const thickLineVertices = generateThickLine(
|
const thickLineVertices = generateThickLine(
|
||||||
new Float32Array(unpassedPoints),
|
new Float32Array(allPoints),
|
||||||
lineWidth,
|
lineWidth,
|
||||||
);
|
);
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
|
||||||
@@ -1345,92 +1415,32 @@ export const WebGLMap = observer(() => {
|
|||||||
|
|
||||||
gl.uniform1f(u_pointSize, pointInnerSizePx);
|
gl.uniform1f(u_pointSize, pointInnerSizePx);
|
||||||
|
|
||||||
let currentStationIndexInOrdered = -1;
|
if (tramSegIndex >= 0 && orderedRouteStations && stationData && orderedStationSegs.length > 0) {
|
||||||
if (currentStationId && orderedRouteStations) {
|
const passedPts1: number[] = [];
|
||||||
currentStationIndexInOrdered = orderedRouteStations.findIndex(
|
const unpassedPts1: number[] = [];
|
||||||
(station: any) => String(station.id) === String(currentStationId),
|
for (let i = 0; i < orderedRouteStations.length; i++) {
|
||||||
);
|
const orderedStation = (orderedRouteStations as any[])[i];
|
||||||
|
const stationSeg = orderedStationSegs[i] ?? -1;
|
||||||
|
if (!orderedStation || stationSeg < 0) continue;
|
||||||
|
const isPassed = simulationDirection === 1 ? stationSeg < tramSegIndex : stationSeg > tramSegIndex;
|
||||||
|
const stIdx = stationData.findIndex((s: any) => String(s.id) === String(orderedStation.id));
|
||||||
|
if (stIdx < 0) continue;
|
||||||
|
const sx = stationPoints[stIdx * 2] as number;
|
||||||
|
const sy = stationPoints[stIdx * 2 + 1] as number;
|
||||||
|
if (isPassed) { passedPts1.push(sx, sy); } else { unpassedPts1.push(sx, sy); }
|
||||||
}
|
}
|
||||||
|
if (passedPts1.length > 0) {
|
||||||
if (
|
gl.uniform4f(u_color_pts, ((PATH_COLOR >> 16) & 0xff) / 255, ((PATH_COLOR >> 8) & 0xff) / 255, (PATH_COLOR & 0xff) / 255, 1);
|
||||||
currentStationIndexInOrdered >= 0 &&
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(passedPts1), gl.STATIC_DRAW);
|
||||||
orderedRouteStations &&
|
gl.drawArrays(gl.POINTS, 0, passedPts1.length / 2);
|
||||||
stationData
|
|
||||||
) {
|
|
||||||
const passedStations: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < currentStationIndexInOrdered; i++) {
|
|
||||||
const orderedStation = orderedRouteStations[i];
|
|
||||||
if (orderedStation) {
|
|
||||||
const stationIndexInData = stationData.findIndex(
|
|
||||||
(station: any) =>
|
|
||||||
String(station.id) === String(orderedStation.id),
|
|
||||||
);
|
|
||||||
if (stationIndexInData >= 0) {
|
|
||||||
passedStations.push(
|
|
||||||
stationPoints[stationIndexInData * 2] as number,
|
|
||||||
stationPoints[stationIndexInData * 2 + 1] as number,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
if (unpassedPts1.length > 0) {
|
||||||
}
|
gl.uniform4f(u_color_pts, ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255, ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255, (UNPASSED_STATION_COLOR & 0xff) / 255, 1);
|
||||||
if (passedStations.length > 0) {
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpassedPts1), gl.STATIC_DRAW);
|
||||||
const r_passed = ((PATH_COLOR >> 16) & 0xff) / 255;
|
gl.drawArrays(gl.POINTS, 0, unpassedPts1.length / 2);
|
||||||
const g_passed = ((PATH_COLOR >> 8) & 0xff) / 255;
|
|
||||||
const b_passed = (PATH_COLOR & 0xff) / 255;
|
|
||||||
gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1);
|
|
||||||
gl.bufferData(
|
|
||||||
gl.ARRAY_BUFFER,
|
|
||||||
new Float32Array(passedStations),
|
|
||||||
gl.STATIC_DRAW,
|
|
||||||
);
|
|
||||||
gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentStationIndexInOrdered >= 0 &&
|
|
||||||
orderedRouteStations &&
|
|
||||||
stationData
|
|
||||||
) {
|
|
||||||
const unpassedStations: number[] = [];
|
|
||||||
|
|
||||||
for (
|
|
||||||
let i = currentStationIndexInOrdered + 1;
|
|
||||||
i < orderedRouteStations.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
const orderedStation = orderedRouteStations[i];
|
|
||||||
if (orderedStation) {
|
|
||||||
const stationIndexInData = stationData.findIndex(
|
|
||||||
(station: any) =>
|
|
||||||
String(station.id) === String(orderedStation.id),
|
|
||||||
);
|
|
||||||
if (stationIndexInData >= 0) {
|
|
||||||
unpassedStations.push(
|
|
||||||
stationPoints[stationIndexInData * 2] as number,
|
|
||||||
stationPoints[stationIndexInData * 2 + 1] as number,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unpassedStations.length > 0) {
|
|
||||||
const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
|
|
||||||
const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
|
|
||||||
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
|
||||||
gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1);
|
|
||||||
gl.bufferData(
|
|
||||||
gl.ARRAY_BUFFER,
|
|
||||||
new Float32Array(unpassedStations),
|
|
||||||
gl.STATIC_DRAW,
|
|
||||||
);
|
|
||||||
gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
|
gl.uniform4f(u_color_pts, ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255, ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255, (UNPASSED_STATION_COLOR & 0xff) / 255, 1);
|
||||||
const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
|
|
||||||
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
|
||||||
gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
|
||||||
gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
|
gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
|
||||||
}
|
}
|
||||||
@@ -1501,37 +1511,21 @@ export const WebGLMap = observer(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentStationIndexInOrdered = -1;
|
|
||||||
if (currentStationId && orderedRouteStations) {
|
|
||||||
currentStationIndexInOrdered = orderedRouteStations.findIndex(
|
|
||||||
(station: any) => String(station.id) === String(currentStationId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const passedStationIds = new Set<string>();
|
const passedStationIds = new Set<string>();
|
||||||
const unpassedStationIds = new Set<string>();
|
const unpassedStationIds = new Set<string>();
|
||||||
|
|
||||||
if (currentStationIndexInOrdered >= 0 && orderedRouteStations) {
|
if (tramSegIndex >= 0 && orderedRouteStations && orderedStationSegs.length === orderedRouteStations.length) {
|
||||||
for (let i = 0; i < currentStationIndexInOrdered; i++) {
|
for (let i = 0; i < orderedRouteStations.length; i++) {
|
||||||
const station = orderedRouteStations[i];
|
const station = (orderedRouteStations as any[])[i];
|
||||||
if (station) {
|
const seg = orderedStationSegs[i] ?? -1;
|
||||||
passedStationIds.add(String(station.id));
|
if (!station || seg < 0) continue;
|
||||||
}
|
const isPassed = simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex;
|
||||||
}
|
if (isPassed) passedStationIds.add(String(station.id));
|
||||||
|
else unpassedStationIds.add(String(station.id));
|
||||||
for (
|
|
||||||
let i = currentStationIndexInOrdered;
|
|
||||||
i < orderedRouteStations.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
const station = orderedRouteStations[i];
|
|
||||||
if (station) {
|
|
||||||
unpassedStationIds.add(String(station.id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (orderedRouteStations) {
|
if (orderedRouteStations) {
|
||||||
orderedRouteStations.forEach((station: any) => {
|
(orderedRouteStations as any[]).forEach((station) => {
|
||||||
unpassedStationIds.add(String(station.id));
|
unpassedStationIds.add(String(station.id));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1668,12 +1662,12 @@ export const WebGLMap = observer(() => {
|
|||||||
const cos = Math.cos(rotationAngle);
|
const cos = Math.cos(rotationAngle);
|
||||||
const sin = Math.sin(rotationAngle);
|
const sin = Math.sin(rotationAngle);
|
||||||
|
|
||||||
const startStationData = stationData.find(
|
const startStationData = orderedRouteStations?.[0]
|
||||||
(station: any) => station.id.toString() === apiStore.context?.startStopId,
|
? stationData.find((station: any) => station.id.toString() === String(orderedRouteStations[0].id))
|
||||||
);
|
: stationData.find((station: any) => station.id.toString() === apiStore.context?.startStopId);
|
||||||
const endStationData = stationData.find(
|
const endStationData = orderedRouteStations?.length
|
||||||
(station: any) => station.id.toString() === apiStore.context?.endStopId,
|
? stationData.find((station: any) => station.id.toString() === String(orderedRouteStations[orderedRouteStations.length - 1].id))
|
||||||
);
|
: stationData.find((station: any) => station.id.toString() === apiStore.context?.endStopId);
|
||||||
|
|
||||||
const terminalStations: number[] = [];
|
const terminalStations: number[] = [];
|
||||||
|
|
||||||
@@ -1773,7 +1767,7 @@ export const WebGLMap = observer(() => {
|
|||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
})();
|
})();
|
||||||
return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex;
|
return tramSegIndex !== -1 && seg !== -1 && (simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex);
|
||||||
})()
|
})()
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
@@ -1806,7 +1800,7 @@ export const WebGLMap = observer(() => {
|
|||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
})();
|
})();
|
||||||
return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex;
|
return tramSegIndex !== -1 && seg !== -1 && (simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex);
|
||||||
})()
|
})()
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
@@ -1832,20 +1826,11 @@ export const WebGLMap = observer(() => {
|
|||||||
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
||||||
|
|
||||||
if (startStationData && endStationData) {
|
if (startStationData && endStationData) {
|
||||||
gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1.0);
|
const startIsPassed = simulationDirection === 1 ? true : isStartPassed;
|
||||||
|
const endIsPassed = simulationDirection === -1 ? true : isEndPassed;
|
||||||
|
gl.uniform4f(u_color_pts, startIsPassed ? r_passed : r_unpassed, startIsPassed ? g_passed : g_unpassed, startIsPassed ? b_passed : b_unpassed, 1.0);
|
||||||
gl.drawArrays(gl.POINTS, 0, 1);
|
gl.drawArrays(gl.POINTS, 0, 1);
|
||||||
|
gl.uniform4f(u_color_pts, endIsPassed ? r_passed : r_unpassed, endIsPassed ? g_passed : g_unpassed, endIsPassed ? b_passed : b_unpassed, 1.0);
|
||||||
if (isEndPassed) {
|
|
||||||
gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1.0);
|
|
||||||
} else {
|
|
||||||
gl.uniform4f(
|
|
||||||
u_color_pts,
|
|
||||||
r_unpassed,
|
|
||||||
g_unpassed,
|
|
||||||
b_unpassed,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
gl.drawArrays(gl.POINTS, 1, 1);
|
gl.drawArrays(gl.POINTS, 1, 1);
|
||||||
} else {
|
} else {
|
||||||
const isStartStation = startStationData !== undefined;
|
const isStartStation = startStationData !== undefined;
|
||||||
@@ -1885,6 +1870,8 @@ export const WebGLMap = observer(() => {
|
|||||||
nearestStationId,
|
nearestStationId,
|
||||||
currentStationId,
|
currentStationId,
|
||||||
orderedRouteStations,
|
orderedRouteStations,
|
||||||
|
orderedStationSegs,
|
||||||
|
apiStore.simulationDirection,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2328,7 +2315,7 @@ export const WebGLMap = observer(() => {
|
|||||||
fontSize: primaryFontSize,
|
fontSize: primaryFontSize,
|
||||||
textShadow: "0 0 4px rgba(0,0,0,0.6)",
|
textShadow: "0 0 4px rgba(0,0,0,0.6)",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "pre-line",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{l.name}
|
{l.name}
|
||||||
@@ -2344,7 +2331,7 @@ export const WebGLMap = observer(() => {
|
|||||||
lineHeight: secondaryLineHeight,
|
lineHeight: secondaryLineHeight,
|
||||||
color: "#CBCBCB",
|
color: "#CBCBCB",
|
||||||
textShadow: "0 0 3px rgba(0,0,0,0.4)",
|
textShadow: "0 0 3px rgba(0,0,0,0.4)",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "pre-line",
|
||||||
...secondaryPositionStyle,
|
...secondaryPositionStyle,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, articlesStore, languageStore, SearchInput } from "@shared";
|
import { authStore, articlesStore, languageStore, SearchInput, selectedCityStore } from "@shared";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, Eye, Minus } from "lucide-react";
|
import { Trash2, Eye, Minus } from "lucide-react";
|
||||||
@@ -75,14 +75,16 @@ export const ArticleListPage = observer(() => {
|
|||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
const query = searchQuery.trim().toLowerCase();
|
const query = searchQuery.trim().toLowerCase();
|
||||||
|
const cityId = selectedCityStore.selectedCityId;
|
||||||
return articleList[language].data
|
return articleList[language].data
|
||||||
.filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query))
|
.filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query))
|
||||||
|
.filter((article) => !cityId || article.city_id === cityId)
|
||||||
.map((article) => ({
|
.map((article) => ({
|
||||||
id: article.id,
|
id: article.id,
|
||||||
heading: article.heading,
|
heading: article.heading,
|
||||||
body: article.body,
|
body: article.body,
|
||||||
}));
|
}));
|
||||||
}, [articleList[language].data, searchQuery]);
|
}, [articleList[language].data, searchQuery, selectedCityStore.selectedCityId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
|||||||
export const CityCreatePage = observer(() => {
|
export const CityCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { createCityData, setCreateCityData, setCreateCityWeatherCode } = cityStore;
|
const { createCityData, setCreateCityData, setCreateCityWeatherCode } =
|
||||||
|
cityStore;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
@@ -72,7 +73,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
createCityData[language].name,
|
createCityData[language].name,
|
||||||
createCityData.country_code,
|
createCityData.country_code,
|
||||||
media.id,
|
media.id,
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
e.target.value,
|
e.target.value,
|
||||||
createCityData.country_code,
|
createCityData.country_code,
|
||||||
createCityData.arms,
|
createCityData.arms,
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -127,7 +128,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
createCityData[language].name,
|
createCityData[language].name,
|
||||||
e.target.value,
|
e.target.value,
|
||||||
createCityData.arms,
|
createCityData.arms,
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -144,7 +145,6 @@ export const CityCreatePage = observer(() => {
|
|||||||
label="Код города для погоды"
|
label="Код города для погоды"
|
||||||
type="number"
|
type="number"
|
||||||
value={createCityData.weather_city_code ?? 0}
|
value={createCityData.weather_city_code ?? 0}
|
||||||
helperText="Числовой код города в источнике погоды (Кранштат)"
|
|
||||||
onChange={(e) => setCreateCityWeatherCode(Number(e.target.value))}
|
onChange={(e) => setCreateCityWeatherCode(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
createCityData[language].name,
|
createCityData[language].name,
|
||||||
createCityData.country_code,
|
createCityData.country_code,
|
||||||
"",
|
"",
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
setActiveMenuType(null);
|
setActiveMenuType(null);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ export const CityEditPage = observer(() => {
|
|||||||
>(null);
|
>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { editCityData, editCity, getCity, setEditCityData, setEditCityWeatherCode } = cityStore;
|
const {
|
||||||
|
editCityData,
|
||||||
|
editCity,
|
||||||
|
getCity,
|
||||||
|
setEditCityData,
|
||||||
|
setEditCityWeatherCode,
|
||||||
|
} = cityStore;
|
||||||
const { getCountries } = countryStore;
|
const { getCountries } = countryStore;
|
||||||
const { getMedia, getOneMedia } = mediaStore;
|
const { getMedia, getOneMedia } = mediaStore;
|
||||||
|
|
||||||
@@ -108,7 +114,7 @@ export const CityEditPage = observer(() => {
|
|||||||
: null;
|
: null;
|
||||||
const effectiveArmsUrl = isMediaIdEmpty(editCityData.arms)
|
const effectiveArmsUrl = isMediaIdEmpty(editCityData.arms)
|
||||||
? null
|
? null
|
||||||
: selectedMedia?.id ?? editCityData.arms;
|
: (selectedMedia?.id ?? editCityData.arms);
|
||||||
|
|
||||||
if (isLoadingData) {
|
if (isLoadingData) {
|
||||||
return (
|
return (
|
||||||
@@ -185,7 +191,6 @@ export const CityEditPage = observer(() => {
|
|||||||
label="Код города для погоды"
|
label="Код города для погоды"
|
||||||
type="number"
|
type="number"
|
||||||
value={editCityData.weather_city_code ?? 0}
|
value={editCityData.weather_city_code ?? 0}
|
||||||
helperText="Числовой код города в источнике погоды (Кранштат)"
|
|
||||||
onChange={(e) => setEditCityWeatherCode(Number(e.target.value))}
|
onChange={(e) => setEditCityWeatherCode(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
CreateRightTab,
|
CreateRightTab,
|
||||||
LeaveAgree,
|
LeaveAgree,
|
||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
function a11yProps(index: number) {
|
function a11yProps(index: number) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
import { mediaStore, MEDIA_TYPE_LABELS, selectedCityStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -23,7 +23,7 @@ export const MediaCreatePage = observer(() => {
|
|||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await mediaStore.createMedia(name, type);
|
await mediaStore.createMedia(name, type, selectedCityStore.selectedCityId);
|
||||||
toast.success("Медиа успешно создано");
|
toast.success("Медиа успешно создано");
|
||||||
navigate("/media");
|
navigate("/media");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput } from "@shared";
|
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput, selectedCityStore } from "@shared";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2, Minus } from "lucide-react";
|
import { Eye, Trash2, Minus } from "lucide-react";
|
||||||
@@ -98,14 +98,16 @@ export const MediaListPage = observer(() => {
|
|||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
const query = searchQuery.trim().toLowerCase();
|
const query = searchQuery.trim().toLowerCase();
|
||||||
|
const cityId = selectedCityStore.selectedCityId;
|
||||||
return media
|
return media
|
||||||
.filter((item) => !query || (item.media_name ?? "").toLowerCase().includes(query))
|
.filter((item) => !query || (item.media_name ?? "").toLowerCase().includes(query))
|
||||||
|
.filter((item) => !cityId || item.city_id === cityId)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
media_name: item.media_name,
|
media_name: item.media_name,
|
||||||
media_type: item.media_type,
|
media_type: item.media_type,
|
||||||
}));
|
}));
|
||||||
}, [media, searchQuery]);
|
}, [media, searchQuery, selectedCityStore.selectedCityId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -286,9 +286,9 @@ export const RouteCreatePage = observer(() => {
|
|||||||
newRoute.governor_appeal = governor_appeal;
|
newRoute.governor_appeal = governor_appeal;
|
||||||
}
|
}
|
||||||
|
|
||||||
await routeStore.createRoute(newRoute);
|
const newId = await routeStore.createRoute(newRoute);
|
||||||
toast.success("Маршрут успешно создан");
|
toast.success("Маршрут успешно создан");
|
||||||
navigate(-1);
|
navigate(`/route/${newId}/edit`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error("Произошла ошибка при создании маршрута");
|
toast.error("Произошла ошибка при создании маршрута");
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export function RightSidebar() {
|
|||||||
bgcolor="primary.main"
|
bgcolor="primary.main"
|
||||||
border="1px solid #e0e0e0"
|
border="1px solid #e0e0e0"
|
||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
|
zIndex={2}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
Настройка маршрута
|
Настройка маршрута
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Stack, Typography, Box, IconButton } from "@mui/material";
|
|||||||
import { Close } from "@mui/icons-material";
|
import { Close } from "@mui/icons-material";
|
||||||
import { Landmark } from "lucide-react";
|
import { Landmark } from "lucide-react";
|
||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
|
import { RouteWidget } from "./webgl-prototype/RouteWidget";
|
||||||
|
|
||||||
export function Widgets() {
|
export function Widgets() {
|
||||||
const { selectedSight, setSelectedSight } = useMapData();
|
const { selectedSight, setSelectedSight } = useMapData();
|
||||||
@@ -13,22 +14,11 @@ export function Widgets() {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
top={32}
|
top={32}
|
||||||
left={32}
|
left={32}
|
||||||
|
zIndex={2}
|
||||||
sx={{ pointerEvents: "none" }}
|
sx={{ pointerEvents: "none" }}
|
||||||
>
|
>
|
||||||
<Stack
|
{/* Виджет маршрута */}
|
||||||
bgcolor="primary.main"
|
<RouteWidget />
|
||||||
width={361}
|
|
||||||
height={96}
|
|
||||||
p={2}
|
|
||||||
m={2}
|
|
||||||
borderRadius={2}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: "#fff" }}>
|
|
||||||
Остановка
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{/* Виджет выбранной достопримечательности (заменяет виджет погоды) */}
|
{/* Виджет выбранной достопримечательности (заменяет виджет погоды) */}
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
.route-widget-label.marquee {
|
||||||
|
display: inline-block;
|
||||||
|
animation: marquee 14s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-subtitle.marquee {
|
||||||
|
display: inline-block;
|
||||||
|
animation: marquee 14s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget {
|
||||||
|
width: 361px;
|
||||||
|
height: 96px;
|
||||||
|
position: fixed;
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset,
|
||||||
|
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
|
||||||
|
padding: 1px; /* Чтобы контент не прилипал к рамке */
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom right,
|
||||||
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
),
|
||||||
|
rgba(179, 165, 152, 0.4);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 10000001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-number {
|
||||||
|
position: absolute;
|
||||||
|
width: fit-content;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
min-width: 94px;
|
||||||
|
max-width: 100px;
|
||||||
|
height: 96px;
|
||||||
|
background-color: #fcd500;
|
||||||
|
color: black;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 70px;
|
||||||
|
padding: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-content {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 257px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 13px;
|
||||||
|
margin-left: 109px;
|
||||||
|
margin-right: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 1px 0;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-label--medium {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-label--small {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-label--xsmall {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-widget-subtitle {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #cbcbcb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import styles from "./RouteWidget.module.css";
|
||||||
|
import { useMapData } from "../MapDataContext";
|
||||||
|
import { languageStore } from "@shared";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
const shouldAnimate = (text: string | undefined, maxLength: number) =>
|
||||||
|
(text?.length ?? 0) > maxLength;
|
||||||
|
|
||||||
|
const getLabelSizeClass = (text: string | undefined) => {
|
||||||
|
const length = text?.length ?? 0;
|
||||||
|
if (length <= 40) return "";
|
||||||
|
if (length <= 60) return styles["route-widget-label--medium"];
|
||||||
|
if (length <= 80) return styles["route-widget-label--small"];
|
||||||
|
return styles["route-widget-label--xsmall"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RouteWidget = observer(() => {
|
||||||
|
const { routeData, stationData } = useMapData();
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
const stations = stationData?.[language] ?? stationData?.["ru"] ?? [];
|
||||||
|
const stationsRu = stationData?.["ru"] ?? [];
|
||||||
|
|
||||||
|
const startStation = stations[0];
|
||||||
|
const endStation = stations[stations.length - 1];
|
||||||
|
|
||||||
|
const startStationRu = stationsRu[0];
|
||||||
|
const endStationRu = stationsRu[stationsRu.length - 1];
|
||||||
|
|
||||||
|
const enSubtitle = `${startStationRu?.name ?? ""} - ${endStationRu?.name ?? ""}`;
|
||||||
|
const zhSubtitle = `${startStation?.name ?? ""} - ${endStation?.name ?? ""}`;
|
||||||
|
const subtitle = language === "zh" ? zhSubtitle : enSubtitle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["route-widget"]} style={{ position: "relative" }}>
|
||||||
|
<div className={styles["route-widget-number"]}>
|
||||||
|
{routeData?.route_sys_number || ""}
|
||||||
|
</div>
|
||||||
|
<div className={styles["route-widget-content"]}>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
styles["route-widget-label"],
|
||||||
|
shouldAnimate(startStation?.name, 18) ? styles["marquee"] : "",
|
||||||
|
getLabelSizeClass(startStation?.name),
|
||||||
|
].join(" ")}
|
||||||
|
title={startStation?.name}
|
||||||
|
>
|
||||||
|
{startStation?.name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
styles["route-widget-label"],
|
||||||
|
shouldAnimate(endStation?.name, 18) ? styles["marquee"] : "",
|
||||||
|
getLabelSizeClass(endStation?.name),
|
||||||
|
].join(" ")}
|
||||||
|
title={endStation?.name}
|
||||||
|
>
|
||||||
|
{endStation?.name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
styles["route-widget-subtitle"],
|
||||||
|
shouldAnimate(subtitle, 50) ? styles["marquee"] : "",
|
||||||
|
].join(" ")}
|
||||||
|
title={subtitle}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -68,9 +68,9 @@ export const StationCreatePage = observer(() => {
|
|||||||
const executeCreate = async () => {
|
const executeCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await createStation();
|
const data = await createStation();
|
||||||
toast.success("Остановка успешно создана");
|
toast.success("Остановка успешно создана");
|
||||||
navigate("/station");
|
navigate(`/station/${data.id}/edit`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating station:", error);
|
console.error("Error creating station:", error);
|
||||||
toast.error("Ошибка при создании остановки");
|
toast.error("Ошибка при создании остановки");
|
||||||
|
|||||||
@@ -326,6 +326,9 @@ export const UserEditPage = observer(() => {
|
|||||||
if (!next.includes("snapshot_create")) {
|
if (!next.includes("snapshot_create")) {
|
||||||
next.push("snapshot_create");
|
next.push("snapshot_create");
|
||||||
}
|
}
|
||||||
|
if (!next.includes("devices_maintenance_rw")) {
|
||||||
|
next.push("devices_maintenance_rw");
|
||||||
|
}
|
||||||
next.push("admin");
|
next.push("admin");
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -347,7 +350,7 @@ export const UserEditPage = observer(() => {
|
|||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение</TableCell>
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение</TableCell>
|
||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение/Запись</TableCell>
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение/Запись</TableCell>
|
||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>
|
<TableCell align="center" sx={{ fontWeight: 600 }}>
|
||||||
Создание (snapshot_create)
|
Доп. права
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -386,6 +389,8 @@ export const UserEditPage = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDevicesResource = key === "devices";
|
||||||
|
|
||||||
const handleSnapshotCreateChange = (checked: boolean) => {
|
const handleSnapshotCreateChange = (checked: boolean) => {
|
||||||
if (!isSnapshotResource) {
|
if (!isSnapshotResource) {
|
||||||
return;
|
return;
|
||||||
@@ -400,6 +405,13 @@ export const UserEditPage = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMaintenanceChange = (checked: boolean) => {
|
||||||
|
setLocalRoles((prev) => {
|
||||||
|
const without = prev.filter((r) => r !== "devices_maintenance_rw");
|
||||||
|
return checked ? [...without, "devices_maintenance_rw"] : without;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={key} hover>
|
<TableRow key={key} hover>
|
||||||
<TableCell>{label}</TableCell>
|
<TableCell>{label}</TableCell>
|
||||||
@@ -447,6 +459,14 @@ export const UserEditPage = observer(() => {
|
|||||||
handleSnapshotCreateChange(e.target.checked)
|
handleSnapshotCreateChange(e.target.checked)
|
||||||
}
|
}
|
||||||
size="small"
|
size="small"
|
||||||
|
title="Разрешает создавать новые снапшоты"
|
||||||
|
/>
|
||||||
|
) : isDevicesResource ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={localRoles.includes("devices_maintenance_rw")}
|
||||||
|
onChange={(e) => handleMaintenanceChange(e.target.checked)}
|
||||||
|
size="small"
|
||||||
|
title="Разрешает переводить устройства в режим технического обслуживания"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
Language,
|
Language,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -232,6 +233,7 @@ export const ArticleSelectOrCreateDialog = observer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cityId = selectedCityStore.selectedCityId;
|
||||||
const response = await authInstance.post("/article", {
|
const response = await authInstance.post("/article", {
|
||||||
translations: {
|
translations: {
|
||||||
heading: {
|
heading: {
|
||||||
@@ -245,6 +247,7 @@ export const ArticleSelectOrCreateDialog = observer(
|
|||||||
zh: newArticleData.zh.body || "Новый текст (ZH)",
|
zh: newArticleData.zh.body || "Новый текст (ZH)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(cityId ? { city_id: cityId } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { id } = response.data;
|
const { id } = response.data;
|
||||||
@@ -519,9 +522,12 @@ export const ArticleSelectOrCreateDialog = observer(
|
|||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredArticles = articles[modalLanguage].filter((article) =>
|
const cityId = selectedCityStore.selectedCityId;
|
||||||
article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
|
const filteredArticles = articles[modalLanguage].filter((article) => {
|
||||||
);
|
if (!article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (cityId && article.city_id !== cityId) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
||||||
const [queuedPreviewId, setQueuedPreviewId] = useState<number | null>(null);
|
const [queuedPreviewId, setQueuedPreviewId] = useState<number | null>(null);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { articlesStore, authInstance, languageStore } from "@shared";
|
import { articlesStore, authInstance, languageStore, selectedCityStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -116,12 +116,16 @@ export const SelectArticleModal = observer(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cityId = selectedCityStore.selectedCityId;
|
||||||
|
|
||||||
const filteredArticles = articles[languageStore.language].filter(
|
const filteredArticles = articles[languageStore.language].filter(
|
||||||
(article) => !linkedArticleIds.includes(article.id)
|
(article) => {
|
||||||
|
if (linkedArticleIds.includes(article.id)) return false;
|
||||||
|
if (searchQuery && !article.service_name?.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (cityId && article.city_id !== cityId) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// .filter((article) =>
|
|
||||||
// article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
// );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Media, mediaStore } from "@shared";
|
import { Media, mediaStore, selectedCityStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -92,11 +92,17 @@ export const SelectMediaDialog = observer(
|
|||||||
};
|
};
|
||||||
}, [hoveredMediaId, onSelectMedia, onClose]); // Dependencies for keyboard listener
|
}, [hoveredMediaId, onSelectMedia, onClose]); // Dependencies for keyboard listener
|
||||||
|
|
||||||
|
const cityId = selectedCityStore.selectedCityId;
|
||||||
|
|
||||||
let filteredMedia = media
|
let filteredMedia = media
|
||||||
.filter((mediaItem) => !linkedMediaIds.includes(mediaItem.id)) // Use mediaItem to avoid name collision
|
.filter((mediaItem) => !linkedMediaIds.includes(mediaItem.id))
|
||||||
.filter((mediaItem) =>
|
.filter((mediaItem) =>
|
||||||
mediaItem.media_name.toLowerCase().includes(searchQuery.toLowerCase())
|
mediaItem.media_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
)
|
||||||
|
.filter((mediaItem) => {
|
||||||
|
if (!cityId) return true;
|
||||||
|
return mediaItem.city_id === cityId;
|
||||||
|
});
|
||||||
|
|
||||||
if (mediaType) {
|
if (mediaType) {
|
||||||
filteredMedia = filteredMedia.filter(
|
filteredMedia = filteredMedia.filter(
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import {
|
|||||||
editSightStore,
|
editSightStore,
|
||||||
generateDefaultMediaName,
|
generateDefaultMediaName,
|
||||||
clearBlobAndGLTFCache,
|
clearBlobAndGLTFCache,
|
||||||
|
authStore,
|
||||||
|
snapshotStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@@ -247,12 +250,16 @@ export const UploadMediaDialog = observer(
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
const uploadStartTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const effectiveMediaType = hardcodeType
|
||||||
|
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
|
||||||
|
: mediaType;
|
||||||
|
|
||||||
const media = await uploadMedia(
|
const media = await uploadMedia(
|
||||||
mediaFilename,
|
mediaFilename,
|
||||||
hardcodeType
|
effectiveMediaType,
|
||||||
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
|
|
||||||
: mediaType,
|
|
||||||
mediaFile,
|
mediaFile,
|
||||||
mediaName
|
mediaName
|
||||||
);
|
);
|
||||||
@@ -263,6 +270,40 @@ export const UploadMediaDialog = observer(
|
|||||||
await afterUpload(media);
|
await afterUpload(media);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (effectiveMediaType === 2) {
|
||||||
|
const uploadDurationSec = Math.round((Date.now() - uploadStartTime) / 1000);
|
||||||
|
const minutes = Math.floor(uploadDurationSec / 60);
|
||||||
|
const seconds = uploadDurationSec % 60;
|
||||||
|
const durationStr = minutes > 0
|
||||||
|
? `${minutes} мин ${seconds} сек`
|
||||||
|
: `${seconds} сек`;
|
||||||
|
|
||||||
|
const fileSizeMb = mediaFile.size / (1024 * 1024);
|
||||||
|
const fileSizeStr = fileSizeMb >= 1024
|
||||||
|
? `${(fileSizeMb / 1024).toFixed(2)} ГБ`
|
||||||
|
: `${fileSizeMb.toFixed(1)} МБ`;
|
||||||
|
|
||||||
|
if (authStore.canRead("snapshots")) {
|
||||||
|
try {
|
||||||
|
await snapshotStore.getStorageInfo();
|
||||||
|
const storage = snapshotStore.storageInfo;
|
||||||
|
if (storage) {
|
||||||
|
toast.success(
|
||||||
|
`Видео (${fileSizeStr}) загружено за ${durationStr}. Свободно на диске: ${storage.available_disk_space_gb.toFixed(2)} ГБ из ${storage.total_disk_space_gb.toFixed(2)} ГБ`,
|
||||||
|
{ autoClose: 8000 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(`Видео (${fileSizeStr}) загружено за ${durationStr}`, { autoClose: 6000 });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.success(`Видео (${fileSizeStr}) загружено за ${durationStr}`, { autoClose: 6000 });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.success(`Видео (${fileSizeStr}) загружено за ${durationStr}`, { autoClose: 6000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type Article = {
|
|||||||
heading: string;
|
heading: string;
|
||||||
body: string;
|
body: string;
|
||||||
service_name: string;
|
service_name: string;
|
||||||
|
city_id?: number | null;
|
||||||
ru?: {
|
ru?: {
|
||||||
heading: string;
|
heading: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
authInstance,
|
authInstance,
|
||||||
languageInstance,
|
languageInstance,
|
||||||
mediaStore,
|
mediaStore,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
@@ -129,6 +130,7 @@ class CreateSightStore {
|
|||||||
zh: articleZhData.body,
|
zh: articleZhData.body,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(selectedCityStore.selectedCityId ? { city_id: selectedCityStore.selectedCityId } : {}),
|
||||||
});
|
});
|
||||||
const { id } = articleRes.data;
|
const { id } = articleRes.data;
|
||||||
|
|
||||||
@@ -346,6 +348,7 @@ class CreateSightStore {
|
|||||||
const response = await languageInstance("ru").post("/article", {
|
const response = await languageInstance("ru").post("/article", {
|
||||||
heading: hasAnyName ? ruName : "",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "",
|
body: "",
|
||||||
|
...(selectedCityStore.selectedCityId ? { city_id: selectedCityStore.selectedCityId } : {}),
|
||||||
});
|
});
|
||||||
const newLeftArticleId = response.data.id;
|
const newLeftArticleId = response.data.id;
|
||||||
|
|
||||||
@@ -449,6 +452,7 @@ class CreateSightStore {
|
|||||||
const res = await languageInstance("ru").post("/article", {
|
const res = await languageInstance("ru").post("/article", {
|
||||||
heading: this.sight.ru.left.heading,
|
heading: this.sight.ru.left.heading,
|
||||||
body: this.sight.ru.left.body,
|
body: this.sight.ru.left.body,
|
||||||
|
...(selectedCityStore.selectedCityId ? { city_id: selectedCityStore.selectedCityId } : {}),
|
||||||
});
|
});
|
||||||
finalLeftArticleId = res.data.id;
|
finalLeftArticleId = res.data.id;
|
||||||
await languageInstance("en").patch(`/article/${finalLeftArticleId}`, {
|
await languageInstance("en").patch(`/article/${finalLeftArticleId}`, {
|
||||||
@@ -567,6 +571,9 @@ class CreateSightStore {
|
|||||||
formData.append("filename", filename);
|
formData.append("filename", filename);
|
||||||
if (media_name) formData.append("media_name", media_name);
|
if (media_name) formData.append("media_name", media_name);
|
||||||
formData.append("type", type.toString());
|
formData.append("type", type.toString());
|
||||||
|
if (selectedCityStore.selectedCityId) {
|
||||||
|
formData.append("city_id", selectedCityStore.selectedCityId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authInstance.post(`/media`, formData);
|
const response = await authInstance.post(`/media`, formData);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Language,
|
Language,
|
||||||
languageInstance,
|
languageInstance,
|
||||||
mediaStore,
|
mediaStore,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
@@ -270,6 +271,7 @@ class EditSightStore {
|
|||||||
const response = await languageInstance("ru").post(`/article`, {
|
const response = await languageInstance("ru").post(`/article`, {
|
||||||
heading: this.sight.ru.left.heading,
|
heading: this.sight.ru.left.heading,
|
||||||
body: this.sight.ru.left.body,
|
body: this.sight.ru.left.body,
|
||||||
|
...(selectedCityStore.selectedCityId ? { city_id: selectedCityStore.selectedCityId } : {}),
|
||||||
});
|
});
|
||||||
createdLeftArticleId = response.data.id;
|
createdLeftArticleId = response.data.id;
|
||||||
await languageInstance("en").patch(`/article/${createdLeftArticleId}`, {
|
await languageInstance("en").patch(`/article/${createdLeftArticleId}`, {
|
||||||
@@ -412,6 +414,7 @@ class EditSightStore {
|
|||||||
const response = await languageInstance("ru").post(`/article`, {
|
const response = await languageInstance("ru").post(`/article`, {
|
||||||
heading: hasAnyName ? ruName : "",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "",
|
body: "",
|
||||||
|
...(selectedCityStore.selectedCityId ? { city_id: selectedCityStore.selectedCityId } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sight.common.left_article = response.data.id;
|
this.sight.common.left_article = response.data.id;
|
||||||
@@ -510,6 +513,9 @@ class EditSightStore {
|
|||||||
formData.append("media_name", media_name);
|
formData.append("media_name", media_name);
|
||||||
}
|
}
|
||||||
formData.append("type", type.toString());
|
formData.append("type", type.toString());
|
||||||
|
if (selectedCityStore.selectedCityId) {
|
||||||
|
formData.append("city_id", selectedCityStore.selectedCityId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
const response = await authInstance.post(`/media`, formData);
|
const response = await authInstance.post(`/media`, formData);
|
||||||
this.fileToUpload = null;
|
this.fileToUpload = null;
|
||||||
@@ -652,6 +658,7 @@ class EditSightStore {
|
|||||||
zh: articleZhData.body,
|
zh: articleZhData.body,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(selectedCityStore.selectedCityId ? { city_id: selectedCityStore.selectedCityId } : {}),
|
||||||
});
|
});
|
||||||
const { id } = articleId.data;
|
const { id } = articleId.data;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type Media = {
|
|||||||
filename: string;
|
filename: string;
|
||||||
media_name: string;
|
media_name: string;
|
||||||
media_type: number;
|
media_type: number;
|
||||||
|
city_id?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MediaStore {
|
class MediaStore {
|
||||||
@@ -75,10 +76,11 @@ class MediaStore {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
createMedia = async (name: string, type: string) => {
|
createMedia = async (name: string, type: string, cityId?: number | null) => {
|
||||||
const response = await authInstance.post("/media", {
|
const response = await authInstance.post("/media", {
|
||||||
media_name: name,
|
media_name: name,
|
||||||
media_type: type,
|
media_type: type,
|
||||||
|
...(cityId ? { city_id: cityId } : {}),
|
||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.media.push(response.data);
|
this.media.push(response.data);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class RouteStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createRoute = async (route: any) => {
|
createRoute = async (route: any): Promise<number> => {
|
||||||
const response = await authInstance.post("/route", route);
|
const response = await authInstance.post("/route", route);
|
||||||
const id = response.data.id;
|
const id = response.data.id;
|
||||||
|
|
||||||
@@ -61,6 +61,8 @@ class RouteStore {
|
|||||||
this.route[id] = { ...route, id };
|
this.route[id] = { ...route, id };
|
||||||
this.routes.data = [...this.routes.data, { ...route, id }];
|
this.routes.data = [...this.routes.data, { ...route, id }];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRoute = async (id: number) => {
|
deleteRoute = async (id: number) => {
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ class StationsStore {
|
|||||||
this.stationLists[language].data.push(response.data);
|
this.stationLists[language].data.push(response.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
const stationId = response.data.id;
|
const stationId: number = response.data.id;
|
||||||
|
|
||||||
for (const lang of ["ru", "en", "zh"].filter(
|
for (const lang of ["ru", "en", "zh"].filter(
|
||||||
(lang) => lang !== language
|
(lang) => lang !== language
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ const transformToRows = (vehicles: Vehicle[]): RowData[] => {
|
|||||||
|
|
||||||
export const DevicesTable = observer(() => {
|
export const DevicesTable = observer(() => {
|
||||||
const canWriteDevices = authStore.canWrite("devices");
|
const canWriteDevices = authStore.canWrite("devices");
|
||||||
|
const canMaintenanceDevices = authStore.hasRole("devices_maintenance_rw");
|
||||||
|
const isMaintenanceOnly = canMaintenanceDevices && !canWriteDevices;
|
||||||
const {
|
const {
|
||||||
getDevices,
|
getDevices,
|
||||||
setSelectedDevice,
|
setSelectedDevice,
|
||||||
@@ -706,9 +708,24 @@ export const DevicesTable = observer(() => {
|
|||||||
demoConfirmSubmitting,
|
demoConfirmSubmitting,
|
||||||
routes,
|
routes,
|
||||||
canWriteDevices,
|
canWriteDevices,
|
||||||
|
isMaintenanceOnly,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const visibleColumns = useMemo(() => {
|
||||||
|
if (isMaintenanceOnly) {
|
||||||
|
return columns.filter((c) =>
|
||||||
|
["model", "tail_number", "maintenance_mode_on"].includes(c.field),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!canWriteDevices) {
|
||||||
|
return columns.filter(
|
||||||
|
(c) => c.field !== "maintenance_mode_on" && c.field !== "demo_mode_enabled",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}, [columns, isMaintenanceOnly, canWriteDevices]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -900,7 +917,7 @@ export const DevicesTable = observer(() => {
|
|||||||
<Box sx={{ p: 0 }}>
|
<Box sx={{ p: 0 }}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={groupRows}
|
rows={groupRows}
|
||||||
columns={columns}
|
columns={visibleColumns}
|
||||||
checkboxSelection={canWriteDevices}
|
checkboxSelection={canWriteDevices}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user