Add NPC creation status tracking and progress reporting in Admin module
- Implemented getNPCsCreationStatus method in AdminController to retrieve the status of NPC creation jobs. - Enhanced AdminService to manage NPC creation jobs, including job ID generation, progress updates, and error handling. - Updated frontend CreateNPCView to display progress of NPC creation, including estimated time remaining and job status. - Added localization strings for progress reporting in both German and English. - Improved overall user experience by providing real-time feedback during NPC creation processes.
This commit is contained in:
@@ -45,6 +45,7 @@ class AdminController {
|
|||||||
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
|
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
|
||||||
this.createNPCs = this.createNPCs.bind(this);
|
this.createNPCs = this.createNPCs.bind(this);
|
||||||
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
||||||
|
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenInterests(req, res) {
|
async getOpenInterests(req, res) {
|
||||||
@@ -393,6 +394,7 @@ class AdminController {
|
|||||||
if (countValue < 1 || countValue > 500) {
|
if (countValue < 1 || countValue > 500) {
|
||||||
return res.status(400).json({ error: 'Count must be between 1 and 500' });
|
return res.status(400).json({ error: 'Count must be between 1 and 500' });
|
||||||
}
|
}
|
||||||
|
console.log('[createNPCs] Request received:', { userId, regionIds, minAge, maxAge, minTitleId, maxTitleId, count: countValue });
|
||||||
const result = await AdminService.createNPCs(userId, {
|
const result = await AdminService.createNPCs(userId, {
|
||||||
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
|
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
|
||||||
minAge: parseInt(minAge) || 0,
|
minAge: parseInt(minAge) || 0,
|
||||||
@@ -401,11 +403,13 @@ class AdminController {
|
|||||||
maxTitleId: parseInt(maxTitleId) || 19,
|
maxTitleId: parseInt(maxTitleId) || 19,
|
||||||
count: countValue
|
count: countValue
|
||||||
});
|
});
|
||||||
|
console.log('[createNPCs] Job created:', result);
|
||||||
res.status(200).json(result);
|
res.status(200).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error('[createNPCs] Error:', error);
|
||||||
|
console.error('[createNPCs] Error stack:', error.stack);
|
||||||
const status = error.message === 'noaccess' ? 403 : 500;
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
res.status(status).json({ error: error.message });
|
res.status(status).json({ error: error.message || 'Internal server error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,6 +425,20 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNPCsCreationStatus(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { jobId } = req.params;
|
||||||
|
const status = await AdminService.getNPCsCreationStatus(userId, jobId);
|
||||||
|
res.status(200).json(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' || error.message === 'Access denied' ? 403 :
|
||||||
|
error.message === 'Job not found' ? 404 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getRoomTypes(req, res) {
|
async getRoomTypes(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ router.get('/falukant/region-distances', authenticate, adminController.getRegion
|
|||||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||||
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
||||||
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
||||||
|
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
||||||
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
||||||
|
|
||||||
// --- Minigames Admin ---
|
// --- Minigames Admin ---
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import Room from '../models/chat/room.js';
|
|||||||
import UserParam from '../models/community/user_param.js';
|
import UserParam from '../models/community/user_param.js';
|
||||||
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
|
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
|
||||||
import { sequelize } from '../utils/sequelize.js';
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
import npcCreationJobService from './npcCreationJobService.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
class AdminService {
|
class AdminService {
|
||||||
async hasUserAccess(userId, section) {
|
async hasUserAccess(userId, section) {
|
||||||
@@ -1113,7 +1115,7 @@ class AdminService {
|
|||||||
count // Anzahl der zu erstellenden NPCs
|
count // Anzahl der zu erstellenden NPCs
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Hole alle Städte, wenn keine spezifischen Regionen angegeben
|
// Berechne zuerst die Gesamtanzahl, um den Job richtig zu initialisieren
|
||||||
let targetRegions = [];
|
let targetRegions = [];
|
||||||
if (regionIds && regionIds.length > 0) {
|
if (regionIds && regionIds.length > 0) {
|
||||||
targetRegions = await RegionData.findAll({
|
targetRegions = await RegionData.findAll({
|
||||||
@@ -1140,7 +1142,6 @@ class AdminService {
|
|||||||
throw new Error('No cities found');
|
throw new Error('No cities found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hole alle Titles im Bereich
|
|
||||||
const titles = await TitleOfNobility.findAll({
|
const titles = await TitleOfNobility.findAll({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
@@ -1154,13 +1155,56 @@ class AdminService {
|
|||||||
throw new Error('No titles found in specified range');
|
throw new Error('No titles found in specified range');
|
||||||
}
|
}
|
||||||
|
|
||||||
const genders = ['male', 'female'];
|
|
||||||
const createdNPCs = [];
|
|
||||||
const totalNPCs = targetRegions.length * titles.length * count;
|
const totalNPCs = targetRegions.length * titles.length * count;
|
||||||
|
|
||||||
// Erstelle NPCs in einer Transaktion
|
// Erstelle Job-ID
|
||||||
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
|
const jobId = uuidv4();
|
||||||
await sequelize.transaction(async (t) => {
|
npcCreationJobService.createJob(userId, jobId);
|
||||||
|
npcCreationJobService.updateProgress(jobId, 0, totalNPCs);
|
||||||
|
npcCreationJobService.setStatus(jobId, 'running');
|
||||||
|
|
||||||
|
// Starte asynchronen Prozess
|
||||||
|
this._createNPCsAsync(jobId, userId, {
|
||||||
|
regionIds,
|
||||||
|
minAge,
|
||||||
|
maxAge,
|
||||||
|
minTitleId,
|
||||||
|
maxTitleId,
|
||||||
|
count,
|
||||||
|
targetRegions,
|
||||||
|
titles
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error in _createNPCsAsync:', error);
|
||||||
|
const errorMessage = error?.message || error?.toString() || 'Unknown error occurred';
|
||||||
|
npcCreationJobService.setError(jobId, errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { jobId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _createNPCsAsync(jobId, userId, options) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
regionIds,
|
||||||
|
minAge,
|
||||||
|
maxAge,
|
||||||
|
minTitleId,
|
||||||
|
maxTitleId,
|
||||||
|
count,
|
||||||
|
targetRegions,
|
||||||
|
titles
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const genders = ['male', 'female'];
|
||||||
|
const createdNPCs = [];
|
||||||
|
const totalNPCs = targetRegions.length * titles.length * count;
|
||||||
|
let currentNPC = 0;
|
||||||
|
|
||||||
|
console.log(`[NPC Creation Job ${jobId}] Starting creation of ${totalNPCs} NPCs`);
|
||||||
|
|
||||||
|
// Erstelle NPCs in einer Transaktion
|
||||||
|
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
for (const region of targetRegions) {
|
for (const region of targetRegions) {
|
||||||
for (const title of titles) {
|
for (const title of titles) {
|
||||||
// Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
|
// Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
|
||||||
@@ -1218,18 +1262,40 @@ class AdminService {
|
|||||||
region: region.name,
|
region: region.name,
|
||||||
title: title.labelTr
|
title: title.labelTr
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Progress
|
||||||
|
currentNPC++;
|
||||||
|
npcCreationJobService.updateProgress(jobId, currentNPC, totalNPCs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
console.log(`[NPC Creation Job ${jobId}] Completed: ${createdNPCs.length} NPCs created`);
|
||||||
success: true,
|
|
||||||
count: createdNPCs.length,
|
// Job abschließen
|
||||||
countPerCombination: count,
|
npcCreationJobService.setResult(jobId, {
|
||||||
totalCombinations: targetRegions.length * titles.length,
|
success: true,
|
||||||
npcs: createdNPCs
|
count: createdNPCs.length,
|
||||||
};
|
countPerCombination: count,
|
||||||
|
totalCombinations: targetRegions.length * titles.length,
|
||||||
|
npcs: createdNPCs
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NPC Creation Job ${jobId}] Error:`, error);
|
||||||
|
throw error; // Re-throw für den catch-Block in createNPCs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNPCsCreationStatus(userId, jobId) {
|
||||||
|
const job = npcCreationJobService.getJob(jobId);
|
||||||
|
if (!job) {
|
||||||
|
throw new Error('Job not found');
|
||||||
|
}
|
||||||
|
if (job.userId !== userId) {
|
||||||
|
throw new Error('Access denied');
|
||||||
|
}
|
||||||
|
return job;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
backend/services/npcCreationJobService.js
Normal file
86
backend/services/npcCreationJobService.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// In-Memory Job-Status-Service für NPC-Erstellung
|
||||||
|
// Für Produktion sollte man Redis oder eine Datenbank verwenden
|
||||||
|
|
||||||
|
const jobs = new Map();
|
||||||
|
|
||||||
|
class NPCCreationJobService {
|
||||||
|
createJob(userId, jobId) {
|
||||||
|
jobs.set(jobId, {
|
||||||
|
userId,
|
||||||
|
status: 'pending',
|
||||||
|
progress: 0,
|
||||||
|
total: 0,
|
||||||
|
current: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
estimatedTimeRemaining: null,
|
||||||
|
error: null,
|
||||||
|
result: null
|
||||||
|
});
|
||||||
|
return jobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getJob(jobId) {
|
||||||
|
return jobs.get(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(jobId, current, total) {
|
||||||
|
const job = jobs.get(jobId);
|
||||||
|
if (!job) return;
|
||||||
|
|
||||||
|
job.current = current;
|
||||||
|
job.total = total;
|
||||||
|
job.progress = total > 0 ? Math.round((current / total) * 100) : 0;
|
||||||
|
|
||||||
|
// Berechne verbleibende Zeit basierend auf bisheriger Geschwindigkeit
|
||||||
|
if (current > 0 && job.progress < 100) {
|
||||||
|
const elapsed = Date.now() - job.startTime;
|
||||||
|
const avgTimePerItem = elapsed / current;
|
||||||
|
const remaining = total - current;
|
||||||
|
job.estimatedTimeRemaining = Math.round(remaining * avgTimePerItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(jobId, status) {
|
||||||
|
const job = jobs.get(jobId);
|
||||||
|
if (!job) return;
|
||||||
|
job.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(jobId, error) {
|
||||||
|
const job = jobs.get(jobId);
|
||||||
|
if (!job) return;
|
||||||
|
job.status = 'error';
|
||||||
|
job.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResult(jobId, result) {
|
||||||
|
const job = jobs.get(jobId);
|
||||||
|
if (!job) return;
|
||||||
|
job.status = 'completed';
|
||||||
|
job.result = result;
|
||||||
|
job.progress = 100;
|
||||||
|
job.estimatedTimeRemaining = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteJob(jobId) {
|
||||||
|
jobs.delete(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup alte Jobs (älter als 1 Stunde)
|
||||||
|
cleanupOldJobs() {
|
||||||
|
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||||
|
for (const [jobId, job] of jobs.entries()) {
|
||||||
|
if (job.startTime < oneHourAgo) {
|
||||||
|
jobs.delete(jobId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup alle 10 Minuten
|
||||||
|
setInterval(() => {
|
||||||
|
const service = new NPCCreationJobService();
|
||||||
|
service.cleanupOldJobs();
|
||||||
|
}, 10 * 60 * 1000);
|
||||||
|
|
||||||
|
export default new NPCCreationJobService();
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<main>
|
<main class="contenthidden">
|
||||||
<router-view></router-view>
|
<div class="contentscroll">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -12,9 +14,13 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
main {
|
main {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contentscroll {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -135,7 +135,13 @@
|
|||||||
"errorCreating": "Fehler beim Erstellen der NPCs.",
|
"errorCreating": "Fehler beim Erstellen der NPCs.",
|
||||||
"invalidAgeRange": "Ungültiger Altersbereich.",
|
"invalidAgeRange": "Ungültiger Altersbereich.",
|
||||||
"invalidTitleRange": "Ungültiger Titel-Bereich.",
|
"invalidTitleRange": "Ungültiger Titel-Bereich.",
|
||||||
"invalidCount": "Ungültige Anzahl (1-500)."
|
"invalidCount": "Ungültige Anzahl (1-500).",
|
||||||
|
"progress": "Fortschritt",
|
||||||
|
"progressDetails": "{current} von {total} NPCs erstellt",
|
||||||
|
"timeRemainingSeconds": "Verbleibende Zeit: {seconds} Sekunden",
|
||||||
|
"timeRemainingMinutes": "Verbleibende Zeit: {minutes} Minuten {seconds} Sekunden",
|
||||||
|
"almostDone": "Fast fertig...",
|
||||||
|
"jobNotFound": "Job nicht gefunden oder abgelaufen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chatrooms": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -162,7 +162,13 @@
|
|||||||
"errorCreating": "Error creating NPCs.",
|
"errorCreating": "Error creating NPCs.",
|
||||||
"invalidAgeRange": "Invalid age range.",
|
"invalidAgeRange": "Invalid age range.",
|
||||||
"invalidTitleRange": "Invalid title range.",
|
"invalidTitleRange": "Invalid title range.",
|
||||||
"invalidCount": "Invalid count (1-500)."
|
"invalidCount": "Invalid count (1-500).",
|
||||||
|
"progress": "Progress",
|
||||||
|
"progressDetails": "{current} of {total} NPCs created",
|
||||||
|
"timeRemainingSeconds": "Time remaining: {seconds} seconds",
|
||||||
|
"timeRemainingMinutes": "Time remaining: {minutes} minutes {seconds} seconds",
|
||||||
|
"almostDone": "Almost done...",
|
||||||
|
"jobNotFound": "Job not found or expired."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chatrooms": {
|
"chatrooms": {
|
||||||
|
|||||||
@@ -58,6 +58,26 @@
|
|||||||
{{ creating ? $t('admin.falukant.createNPC.creating') : $t('admin.falukant.createNPC.create') }}
|
{{ creating ? $t('admin.falukant.createNPC.creating') : $t('admin.falukant.createNPC.create') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Fortschrittsanzeige -->
|
||||||
|
<div v-if="creating && jobStatus" class="progress-section">
|
||||||
|
<div class="progress-header">
|
||||||
|
<h3>{{ $t('admin.falukant.createNPC.progress') }}</h3>
|
||||||
|
<span class="progress-percentage">{{ jobStatus.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" :style="{ width: jobStatus.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-details">
|
||||||
|
<div>{{ $t('admin.falukant.createNPC.progressDetails', {
|
||||||
|
current: jobStatus.current || 0,
|
||||||
|
total: jobStatus.total || 0
|
||||||
|
}) }}</div>
|
||||||
|
<div v-if="jobStatus.estimatedTimeRemaining" class="time-remaining">
|
||||||
|
{{ formatTimeRemaining(jobStatus.estimatedTimeRemaining) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ergebnis-Anzeige -->
|
<!-- Ergebnis-Anzeige -->
|
||||||
@@ -105,9 +125,17 @@ export default {
|
|||||||
count: 1,
|
count: 1,
|
||||||
creating: false,
|
creating: false,
|
||||||
result: null,
|
result: null,
|
||||||
error: null
|
error: null,
|
||||||
|
jobId: null,
|
||||||
|
jobStatus: null,
|
||||||
|
statusPollInterval: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.statusPollInterval) {
|
||||||
|
clearInterval(this.statusPollInterval);
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadRegions();
|
await this.loadRegions();
|
||||||
await this.loadTitles();
|
await this.loadTitles();
|
||||||
@@ -162,6 +190,8 @@ export default {
|
|||||||
this.creating = true;
|
this.creating = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.result = null;
|
this.result = null;
|
||||||
|
this.jobStatus = null;
|
||||||
|
this.jobId = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post('/api/admin/falukant/npcs/create', {
|
const response = await apiClient.post('/api/admin/falukant/npcs/create', {
|
||||||
@@ -173,13 +203,62 @@ export default {
|
|||||||
count: this.count
|
count: this.count
|
||||||
});
|
});
|
||||||
|
|
||||||
this.result = response.data;
|
this.jobId = response.data.jobId;
|
||||||
|
this.startStatusPolling();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating NPCs:', error);
|
console.error('Error creating NPCs:', error);
|
||||||
this.error = error.response?.data?.error || this.$t('admin.falukant.createNPC.errorCreating');
|
this.error = error.response?.data?.error || this.$t('admin.falukant.createNPC.errorCreating');
|
||||||
} finally {
|
|
||||||
this.creating = false;
|
this.creating = false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
startStatusPolling() {
|
||||||
|
if (this.statusPollInterval) {
|
||||||
|
clearInterval(this.statusPollInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.statusPollInterval = setInterval(async () => {
|
||||||
|
if (!this.jobId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/api/admin/falukant/npcs/status/${this.jobId}`);
|
||||||
|
this.jobStatus = response.data;
|
||||||
|
|
||||||
|
if (this.jobStatus.status === 'completed') {
|
||||||
|
this.result = this.jobStatus.result;
|
||||||
|
this.creating = false;
|
||||||
|
clearInterval(this.statusPollInterval);
|
||||||
|
this.statusPollInterval = null;
|
||||||
|
} else if (this.jobStatus.status === 'error') {
|
||||||
|
this.error = this.jobStatus.error || this.$t('admin.falukant.createNPC.errorCreating');
|
||||||
|
this.creating = false;
|
||||||
|
clearInterval(this.statusPollInterval);
|
||||||
|
this.statusPollInterval = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error polling status:', error);
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
// Job nicht gefunden - möglicherweise abgelaufen
|
||||||
|
this.error = this.$t('admin.falukant.createNPC.jobNotFound');
|
||||||
|
this.creating = false;
|
||||||
|
clearInterval(this.statusPollInterval);
|
||||||
|
this.statusPollInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000); // Poll alle Sekunde
|
||||||
|
},
|
||||||
|
formatTimeRemaining(ms) {
|
||||||
|
if (!ms || ms <= 0) return this.$t('admin.falukant.createNPC.almostDone');
|
||||||
|
const seconds = Math.floor(ms / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
return this.$t('admin.falukant.createNPC.timeRemainingMinutes', {
|
||||||
|
minutes,
|
||||||
|
seconds: remainingSeconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.$t('admin.falukant.createNPC.timeRemainingSeconds', { seconds });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -311,4 +390,58 @@ export default {
|
|||||||
color: #155724;
|
color: #155724;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
background: #e7f3ff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid #b3d9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #28a745, #20c997);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-remaining {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user