Initial commit
1
admin/client/public/china.json
Normal file
@@ -43,6 +43,45 @@ service.interceptors.response.use(
|
|||||||
*/
|
*/
|
||||||
response => {
|
response => {
|
||||||
const res = response.data
|
const res = response.data
|
||||||
|
|
||||||
|
// Replace localhost/127.0.0.1 in response data with current window location hostname if needed
|
||||||
|
// This helps when accessing admin panel from LAN but backend returns localhost URLs
|
||||||
|
const replaceUrl = (data) => {
|
||||||
|
if (!data) return data
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
// Replace http://127.0.0.1:8000 or http://localhost:8000
|
||||||
|
// with http://<current-host>:8000
|
||||||
|
// We assume backend is on port 8000.
|
||||||
|
// If we are proxying, we might want to replace with relative path or proxy target.
|
||||||
|
// But simpler is to just replace the IP part with current window hostname if we are in dev/lan.
|
||||||
|
// However, hardcoding 8000 might be risky if backend port changes.
|
||||||
|
// Let's stick to replacing specific localhost/127.0.0.1:8000 patterns.
|
||||||
|
const currentHost = window.location.hostname;
|
||||||
|
// Only replace if current host is NOT localhost/127.0.0.1 (meaning we are on LAN)
|
||||||
|
if (currentHost !== 'localhost' && currentHost !== '127.0.0.1') {
|
||||||
|
return data.replace(/https?:\/\/(localhost|127\.0\.0\.1):8000/g, `http://${currentHost}:8000`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map(replaceUrl)
|
||||||
|
}
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
data[key] = replaceUrl(data[key])
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply replacement
|
||||||
|
try {
|
||||||
|
replaceUrl(res);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to replace URLs', e);
|
||||||
|
}
|
||||||
|
|
||||||
if(res.code>=200 && res.code<400){
|
if(res.code>=200 && res.code<400){
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,16 +70,15 @@ export default {
|
|||||||
if (echarts.getMap('china')) {
|
if (echarts.getMap('china')) {
|
||||||
this.setOptions(this.chartData)
|
this.setOptions(this.chartData)
|
||||||
} else {
|
} else {
|
||||||
// Fetch China Map GeoJSON
|
// Fetch China Map GeoJSON from local static assets
|
||||||
// Using a reliable public source
|
// This ensures the map loads even without internet access or on non-local environments
|
||||||
axios.get('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
|
axios.get(process.env.BASE_URL + 'china.json')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
echarts.registerMap('china', response.data)
|
echarts.registerMap('china', response.data)
|
||||||
this.setOptions(this.chartData)
|
this.setOptions(this.chartData)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to load China map data:', error)
|
console.error('Failed to load China map data:', error)
|
||||||
// Fallback or show error? For now just log.
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export default {
|
|||||||
.theme-dark {
|
.theme-dark {
|
||||||
.card-panel {
|
.card-panel {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: rgba(255, 250, 240, 0.1); /* 半透明米白色 */
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@@ -212,20 +212,22 @@ export default {
|
|||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-5px) scale(1.02);
|
transform: translateY(-5px) scale(1.02);
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
background-color: rgba(255, 250, 240, 0.15);
|
||||||
|
|
||||||
.card-panel-icon-wrapper {
|
.card-panel-icon-wrapper {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-icon-wrapper {
|
.card-panel-icon-wrapper {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-icon {
|
.card-panel-icon {
|
||||||
fill: #fff !important;
|
font-size: 32px;
|
||||||
color: #fff;
|
fill: currentColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-text {
|
.card-panel-text {
|
||||||
@@ -236,14 +238,35 @@ export default {
|
|||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradients - 使用 background-image 确保不被覆盖 */
|
/* Colors for icons in Dark Mode (since background is now uniform) */
|
||||||
&.card-panel-blue { background-image: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
|
&.card-panel-blue {
|
||||||
&.card-panel-purple { background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
.card-panel-icon { color: #4facfe; }
|
||||||
&.card-panel-orange { background-image: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
.card-panel-icon-wrapper { color: #4facfe; }
|
||||||
&.card-panel-green { background-image: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); }
|
}
|
||||||
&.card-panel-red { background-image: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
|
&.card-panel-purple {
|
||||||
&.card-panel-yellow { background-image: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%); }
|
.card-panel-icon { color: #8fd3f4; } /* Lighter purple for dark mode visibility */
|
||||||
&.card-panel-pink { background-image: linear-gradient(135deg, #ff0844 0%, #ffb199 100%); }
|
.card-panel-icon-wrapper { color: #8fd3f4; }
|
||||||
|
}
|
||||||
|
&.card-panel-orange {
|
||||||
|
.card-panel-icon { color: #ff9a9e; }
|
||||||
|
.card-panel-icon-wrapper { color: #ff9a9e; }
|
||||||
|
}
|
||||||
|
&.card-panel-green {
|
||||||
|
.card-panel-icon { color: #43e97b; }
|
||||||
|
.card-panel-icon-wrapper { color: #43e97b; }
|
||||||
|
}
|
||||||
|
&.card-panel-red {
|
||||||
|
.card-panel-icon { color: #fa709a; }
|
||||||
|
.card-panel-icon-wrapper { color: #fa709a; }
|
||||||
|
}
|
||||||
|
&.card-panel-yellow {
|
||||||
|
.card-panel-icon { color: #fbc2eb; }
|
||||||
|
.card-panel-icon-wrapper { color: #fbc2eb; }
|
||||||
|
}
|
||||||
|
&.card-panel-pink {
|
||||||
|
.card-panel-icon { color: #ff0844; }
|
||||||
|
.card-panel-icon-wrapper { color: #ff0844; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ module.exports = {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
// target: 'http://localhost:8000',
|
// target: 'http://localhost:8000',
|
||||||
target: process.env.PROXY_TARGET || 'http://127.0.0.1:8000',
|
target: process.env.PROXY_TARGET || 'http://192.168.5.95:8000',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 574 KiB |
|
Before Width: | Height: | Size: 49 KiB |
11
admin/server/.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.git/
|
||||||
|
.vscode/
|
||||||
|
.vs/
|
||||||
|
dist/
|
||||||
|
celerybeat.pid
|
||||||
|
celerybeat-schedule.*
|
||||||
|
db.sqlite3
|
||||||
|
.env
|
||||||
@@ -9,7 +9,7 @@ class Command(BaseCommand):
|
|||||||
help = 'Populate database with business mock data using existing local images'
|
help = 'Populate database with business mock data using existing local images'
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
self.base_url = "http://127.0.0.1:8000"
|
self.base_url = "http://192.168.5.95:8000"
|
||||||
self.media_root = settings.MEDIA_ROOT
|
self.media_root = settings.MEDIA_ROOT
|
||||||
self.projects_dir = os.path.join(self.media_root, 'projects')
|
self.projects_dir = os.path.join(self.media_root, 'projects')
|
||||||
self.showcases_dir = os.path.join(self.media_root, 'showcases')
|
self.showcases_dir = os.path.join(self.media_root, 'showcases')
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class DashboardStatsView(APIView):
|
|||||||
# Use default color if type not found
|
# Use default color if type not found
|
||||||
color = type_colors.get(type_code, '#909399')
|
color = type_colors.get(type_code, '#909399')
|
||||||
pie_chart_data.append({
|
pie_chart_data.append({
|
||||||
|
'type': type_code,
|
||||||
'name': type_mapping.get(type_code, type_code),
|
'name': type_mapping.get(type_code, type_code),
|
||||||
'value': item['total_students'],
|
'value': item['total_students'],
|
||||||
'itemStyle': { 'color': color }
|
'itemStyle': { 'color': color }
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -10,3 +10,4 @@ djangorestframework-simplejwt==4.8.0
|
|||||||
drf-yasg==1.20.0
|
drf-yasg==1.20.0
|
||||||
psutil==5.9.0
|
psutil==5.9.0
|
||||||
redis==4.5.5
|
redis==4.5.5
|
||||||
|
gunicorn
|
||||||
|
|||||||
@@ -1,6 +1,54 @@
|
|||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
|
||||||
|
function getOrigin(url) {
|
||||||
|
if (!url) return ''
|
||||||
|
const match = url.match(/^(https?:\/\/[^/]+)/)
|
||||||
|
return match ? match[1] : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceUrl(data, targetOrigin) {
|
||||||
|
if (!data || !targetOrigin) return data
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
// Replace localhost/127.0.0.1 with target origin
|
||||||
|
// Handle both http and https, though localhost usually http
|
||||||
|
return data.replace(/https?:\/\/(localhost|127\.0\.0\.1):8000/g, targetOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map(item => replaceUrl(item, targetOrigin))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
const newData = {}
|
||||||
|
for (const key in data) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||||
|
newData[key] = replaceUrl(data[key], targetOrigin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newData
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
function unwrap(data) {
|
function unwrap(data) {
|
||||||
|
// First, apply URL replacement if needed
|
||||||
|
// We need to access app.globalData which might not be ready if called too early,
|
||||||
|
// but usually request is called after app launch.
|
||||||
|
// Safely get app instance again inside function
|
||||||
|
try {
|
||||||
|
const app = getApp()
|
||||||
|
if (app && app.globalData && app.globalData.baseUrl) {
|
||||||
|
const origin = getOrigin(app.globalData.baseUrl)
|
||||||
|
if (origin) {
|
||||||
|
data = replaceUrl(data, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('URL replacement failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
if (data && typeof data === 'object' && 'code' in data && 'data' in data) {
|
if (data && typeof data === 'object' && 'code' in data && 'data' in data) {
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|||||||