<Project-23 Navigator Portal> Python flask web 网站导航应用 可编辑界面:添加图片、URL、描述、位置移动

目的:

浏览器的地址簿太厚,如下图:

开始,想给每个 Web 应用加 icon 来提高辨识度,发现很麻烦:create image, resize, 还要挑来挑去,重复性地添加代码。再看着这些密密麻麻的含有重复与有规则的字符,真刺眼!

做这个 Portal Web 应用来进行网站应用导航,docker 部署后,占用端口:9999,可以在app.py修改。

 <代码有 Claudi AI 参与>

Navigator Portal 应用

1. 界面展示

2. 目录结构

navigator_portal        #项目名称
│
├── app.py                 # Flask 应用主文件
├── requirements.txt       # Python 依赖包列表
├── Dockerfile             # docker部署文件
├── static/               
│   ├── css/
│   │   └── style.css    
│   ├── js/
│   │   └── main.js      
│   ├── uploads/         # 上传的图片存储目录
│   └── favicon.jpg      # 网站图标
├── templates/          # HTML files 目录
│   ├── base.html       
│   ├── index.html      
│   └── edit.html       # 编辑页面
└── data/               # 存储目录
    └── nav_links.json  # 导航链接数据文件

3. 完整代码

a. app.py

# app.py
from flask import Flask, render_template, request, jsonify, url_for
import json
from pathlib import Path
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.secret_key = 'your_secret_key_here'

# 配置文件上传
UPLOAD_FOLDER = Path('static/uploads')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

# 确保上传目录存在
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)

# 数据文件路径
DATA_FILE = Path('data/nav_links.json')

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def init_data_file():
    if not DATA_FILE.exists():
        default_links = [
            {"name": "主应用", "url": "http://davens:5000", "port": "5000", "image": "/static/images/default.png", "order": 0},
        ] + [
            {
                "name": f"应用 {port}", 
                "url": f"http://davens:{port}", 
                "port": str(port),
                "image": "/static/images/default.png",
                "order": i + 1
            }
            for i, port in enumerate(list(range(9001, 9012)) + [9999])
        ]
        DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(DATA_FILE, 'w', encoding='utf-8') as f:
            json.dump(default_links, f, indent=2, ensure_ascii=False)

def load_links():
    try:
        if not DATA_FILE.exists():
            init_data_file()
        with open(DATA_FILE, 'r', encoding='utf-8') as f:
            links = json.load(f)
            return sorted(links, key=lambda x: x.get('order', 0))
    except Exception as e:
        print(f"Error loading links: {e}")
        return []

def save_links(links):
    try:
        # 确保 data 目录存在
        DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(DATA_FILE, 'w', encoding='utf-8') as f:
            json.dump(links, f, indent=2, ensure_ascii=False)
        return True
    except Exception as e:
        print(f"Error saving links: {e}")
        return False

def clean_url(url):
    """清理 URL,移除域名部分只保留路径"""
    if url and url.startswith(('http://', 'https://')):
        return url
    elif url and '/static/' in url:
        return url.split('/static/')[-1]
    return url

@app.route('/')
def index():
    links = load_links()
    return render_template('index.html', links=links)

@app.route('/edit')
def edit():
    links = load_links()
    return render_template('edit.html', links=links)

@app.route('/api/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part'}), 400
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        return jsonify({'url': f'/static/uploads/{filename}'})
    return jsonify({'error': 'Invalid file type'}), 400

@app.route('/api/links', methods=['GET', 'POST', 'PUT', 'DELETE'])
def manage_links():
    try:
        if request.method == 'GET':
            return jsonify(load_links())
        
        elif request.method == 'POST':
            data = request.get_json()
            if not data:
                return jsonify({'status': 'error', 'message': 'No data provided'}), 400
            
            links = load_links()
            image_url = data.get('image', '/static/images/default.png')
            new_link = {
                'name': data.get('name', ''),
                'url': data.get('url', ''),
                'port': data.get('port', ''),
                'image': clean_url(image_url),
                'order': len(links)
            }
            links.append(new_link)
            if save_links(links):
                return jsonify({'status': 'success'})
            return jsonify({'status': 'error', 'message': 'Failed to save links'}), 500
        
        elif request.method == 'PUT':
            data = request.get_json()
            if not data:
                return jsonify({'status': 'error', 'message': 'No data provided'}), 400
            
            links = load_links()
            print("Received PUT data:", data)  # 调试日志
            
            if 'reorder' in data:
                new_order = data.get('new_order', [])
                if not new_order:
                    return jsonify({'status': 'error', 'message': 'Invalid order data'}), 400
                reordered_links = [links[i] for i in new_order]
                if save_links(reordered_links):
                    return jsonify({'status': 'success'})
            else:
                try:
                    index = int(data.get('index', -1))
                    if index < 0 or index >= len(links):
                        return jsonify({'status': 'error', 'message': f'Invalid index: {index}'}), 400
                    
                    image_url = data.get('image', links[index].get('image', '/static/images/default.png'))
                    
                    links[index].update({
                        'name': data.get('name', links[index]['name']),
                        'url': data.get('url', links[index]['url']),
                        'port': data.get('port', links[index]['port']),
                        'image': clean_url(image_url)
                    })
                    
                    print("Updated link:", links[index])  # 调试日志
                    
                    if save_links(links):
                        return jsonify({'status': 'success'})
                except ValueError as e:
                    return jsonify({'status': 'error', 'message': f'Invalid data: {str(e)}'}), 400
            
            return jsonify({'status': 'error', 'message': 'Failed to update links'}), 500
        
        elif request.method == 'DELETE':
            try:
                index = int(request.args.get('index', -1))
            except ValueError:
                return jsonify({'status': 'error', 'message': 'Invalid index'}), 400
                
            if index < 0:
                return jsonify({'status': 'error', 'message': 'Invalid index'}), 400
            
            links = load_links()
            if 0 <= index < len(links):
                del links[index]
                if save_links(links):
                    return jsonify({'status': 'success'})
            
            return jsonify({'status': 'error', 'message': 'Failed to delete link'}), 500
            
    except Exception as e:
        print(f"Error in manage_links: {e}")  # 调试日志
        import traceback
        traceback.print_exc()  # 打印完整的错误堆栈
        return jsonify({'status': 'error', 'message': str(e)}), 500

if __name__ == '__main__':
    init_data_file()
    app.run(host='0.0.0.0', port=9999, debug=True)

b. templates 目录下文件

i. index.html
{% extends "base.html" %}
{% block title %}Web应用导航{% endblock %}
{% block content %}
<div class="header">
    <h1>Web应用导航</h1>
    <a href="/edit" class="edit-btn">编辑导航</a>
</div>

<div class="grid" id="nav-grid">
    {% for link in links %}
    <!-- 将整个卡片变成链接 -->
    <a href="{{ link.url }}" class="card" data-index="{{ loop.index0 }}">
        <div class="card-content">
            <div class="card-image-container">
                <img src="{{ link.image }}" alt="{{ link.name }}">
            </div>
            <div class="card-title">{{ link.name }}</div>
            <div class="port">端口: {{ link.port }}</div>
        </div>
    </a>
    {% endfor %}
</div>
{% endblock %}

{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 处理所有卡片的点击事件
    document.querySelectorAll('.card').forEach(card => {
        card.addEventListener('click', function(e) {
            e.preventDefault(); // 阻止默认链接行为
            const url = this.getAttribute('href');
            if (url) {
                // 在同一个标签页中打开链接
                window.location.href = url;
            }
        });
    });
});
</script>
{% endblock %}
ii. base.html
{% extends "base.html" %}
{% block title %}Web应用导航{% endblock %}
{% block content %}
<div class="header">
    <h1>Web应用导航</h1>
    <a href="/edit" class="edit-btn">编辑导航</a>
</div>

<div class="grid" id="nav-grid">
    {% for link in links %}
    <!-- 将整个卡片变成链接 -->
    <a href="{{ link.url }}" class="card" data-index="{{ loop.index0 }}">
        <div class="card-content">
            <div class="card-image-container">
                <img src="{{ link.image }}" alt="{{ link.name }}">
            </div>
            <div class="card-title">{{ link.name }}</div>
            <div class="port">端口: {{ link.port }}</div>
        </div>
    </a>
    {% endfor %}
</div>
{% endblock %}

{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 处理所有卡片的点击事件
    document.querySelectorAll('.card').forEach(card => {
        card.addEventListener('click', function(e) {
            e.preventDefault(); // 阻止默认链接行为
            const url = this.getAttribute('href');
            if (url) {
                // 在同一个标签页中打开链接
                window.location.href = url;
            }
        });
    });
});
</script>
{% endblock %}
iii. edit.html
# templates/edit.html
{% extends "base.html" %}
{% block title %}编辑导航{% endblock %}

{% block content %}
<div class="edit-container">
    <div class="header">
        <h1>编辑导航</h1>
        <a href="/" class="edit-btn">返回首页</a>
    </div>

    <div id="links-list">
        {% for link in links %}
        <div class="link-item" data-index="{{ loop.index0 }}">
            <i class="fas fa-grip-vertical drag-handle"></i>
            <div class="link-image-container">
                <img src="{{ link.image }}" class="link-image" alt="{{ link.name }}">
            </div>
            <div class="link-info">
                <input type="text" value="{{ link.name }}" placeholder="名称" class="name-input">
                <input type="text" value="{{ link.url }}" placeholder="URL" class="url-input">
                <input type="text" value="{{ link.port }}" placeholder="端口" class="port-input">
                <input type="file" class="image-input" accept="image/*" style="display: none;">
                <button class="btn" onclick="this.previousElementSibling.click()">更换图片</button>
            </div>
            <div class="link-actions">
                <button class="btn btn-primary" onclick="saveLink({{ loop.index0 }})">保存</button>
                <button class="btn btn-danger" onclick="deleteLink({{ loop.index0 }})">删除</button>
            </div>
        </div>
        {% endfor %}
    </div>

    <div class="form-container" style="margin-top: 20px;">
        <h2>添加新链接</h2>
        <div class="form-group">
            <label>名称</label>
            <input type="text" id="new-name">
        </div>
        <div class="form-group">
            <label>URL</label>
            <input type="text" id="new-url">
        </div>
        <div class="form-group">
            <label>端口</label>
            <input type="text" id="new-port">
        </div>
        <div class="form-group">
            <label>图片</label>
            <input type="file" id="new-image" accept="image/*">
        </div>
        <button class="btn btn-primary" onclick="addNewLink()">添加</button>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 初始化拖拽排序
    const linksList = document.getElementById('links-list');
    if (linksList) {
        new Sortable(linksList, {
            handle: '.drag-handle',
            animation: 150,
            onEnd: function() {
                const items = document.querySelectorAll('.link-item');
                const newOrder = Array.from(items).map(item => parseInt(item.dataset.index));
                
                fetch('/api/links', {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        reorder: true,
                        new_order: newOrder
                    })
                });
            }
        });
    }

    // 处理图片上传
    document.querySelectorAll('.image-input').forEach(input => {
        input.addEventListener('change', async function(e) {
            const file = e.target.files[0];
            if (!file) return;

            const formData = new FormData();
            formData.append('file', file);

            try {
                const response = await fetch('/api/upload', {
                    method: 'POST',
                    body: formData
                });
                const data = await response.json();
                if (data.url) {
                    const linkItem = this.closest('.link-item');
                    if (linkItem) {
                        linkItem.querySelector('.link-image').src = data.url;
                    }
                }
            } catch (error) {
                console.error('Error uploading image:', error);
                alert('图片上传失败,请重试!');
            }
        });
    });
});
</script>
{% endblock %}

c. static 目录下文件

i. ./css/style.css
/* static/css/style.css */
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 20px;
    background-color: #f5f5f5;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding: 0 20px;
}

.edit-btn {
    padding: 8px 16px;
    background-color: #007bff;
    color: white;
    text-decoration: none;
    border-radius: 4px;
}

/* 导航卡片网格 */
.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 20px;
    padding: 20px;
}

/* 卡片样式 */
.card {
    background: white;
    border-radius: 8px;
    padding: 15px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    transition: transform 0.2s;
    display: flex;
    flex-direction: column;
    text-decoration: none;  /* 移除链接的默认下划线 */
    color: inherit;        /* 继承颜色 */
}

.card:hover {
    transform: translateY(-5px);
}

.card-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    pointer-events: none;  /* 防止内部元素影响点击 */
}

.card-image-container {
    width: 100%;
    height: 200px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    margin-bottom: 10px;
    border-radius: 4px;
    background-color: #f8f9fa;
}

.card img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}

.card-title {
    color: #333;
    font-weight: bold;
    margin-top: 10px;
    font-size: 1.1em;
}

.port {
    color: #666;
    font-size: 0.9em;
    margin-top: 5px;
}

/* 编辑页面样式 */
.edit-container {
    max-width: 800px;
    margin: 0 auto;
}

.form-container {
    max-width: 800px;
    margin: 0 auto;
}

.form-group {
    margin-bottom: 15px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
}

.form-group input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.link-item {
    display: flex;
    align-items: center;
    background: white;
    padding: 15px;
    margin-bottom: 10px;
    border-radius: 4px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

/* 编辑页面的图片容器 */
.link-image-container {
    width: 100px;
    height: 100px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 15px;
    border-radius: 4px;
    background-color: #f8f9fa;
    overflow: hidden;
}

/* 编辑页面的图片 */
.link-image {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}

.link-info {
    flex-grow: 1;
    margin-right: 15px;
}

.link-info input {
    margin-bottom: 8px;
    width: 100%;
}

.link-actions {
    display: flex;
    gap: 10px;
}

.btn {
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.2s;
}

.btn:hover {
    opacity: 0.9;
}

.btn-primary {
    background-color: #007bff;
    color: white;
}

.btn-primary:hover {
    background-color: #0056b3;
}

.btn-danger {
    background-color: #dc3545;
    color: white;
}

.btn-danger:hover {
    background-color: #c82333;
}

.drag-handle {
    cursor: move;
    color: #666;
    margin-right: 10px;
    padding: 10px;
}

/* 响应式调整 */
@media (max-width: 768px) {
    .grid {
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    }

    .link-item {
        flex-direction: column;
        align-items: flex-start;
    }

    .link-image-container {
        width: 100%;
        margin-bottom: 10px;
        margin-right: 0;
    }

    .link-actions {
        width: 100%;
        justify-content: flex-end;
        margin-top: 10px;
    }
}
ii. ./js/main.js
// static/js/main.js
document.addEventListener('DOMContentLoaded', function() {
    // 初始化拖拽排序
    const linksList = document.getElementById('links-list');
    if (linksList) {
        new Sortable(linksList, {
            handle: '.drag-handle',
            animation: 150,
            onEnd: function() {
                // 获取新的排序
                const items = document.querySelectorAll('.link-item');
                const newOrder = Array.from(items).map(item => parseInt(item.dataset.index));
                
                // 发送到服务器
                fetch('/api/links', {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        reorder: true,
                        new_order: newOrder
                    })
                });
            }
        });
    }

    // 处理图片上传
    document.querySelectorAll('.image-input').forEach(input => {
        input.addEventListener('change', handleImageUpload);
    });

    // 绑定新增链接的图片上传
    const newImageInput = document.getElementById('new-image');
    if (newImageInput) {
        newImageInput.addEventListener('change', handleImageUpload);
    }
});

// 处理图片上传的函数
async function handleImageUpload(event) {
    const file = event.target.files[0];
    if (!file) return;

    if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
        alert('请上传 JPG、PNG 或 GIF 格式的图片!');
        return;
    }

    const formData = new FormData();
    formData.append('file', file);

    try {
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData
        });
        
        if (!response.ok) {
            throw new Error('上传失败');
        }

        const data = await response.json();
        
        if (data.url) {
            const linkItem = this.closest('.link-item');
            if (linkItem) {
                linkItem.querySelector('.link-image').src = data.url;
            }
        } else {
            throw new Error(data.error || '上传失败');
        }
    } catch (error) {
        console.error('Error uploading image:', error);
        alert('图片上传失败:' + error.message);
    }
}

// 保存链接
window.saveLink = async function(index) {
    const linkItem = document.querySelector(`.link-item[data-index="${index}"]`);
    const name = linkItem.querySelector('.name-input').value.trim();
    const url = linkItem.querySelector('.url-input').value.trim();
    const port = linkItem.querySelector('.port-input').value.trim();
    const image = linkItem.querySelector('.link-image').src;

    // 验证数据
    if (!name || !url || !port) {
        alert('请填写所有必需的字段!');
        return;
    }

    try {
        const response = await fetch('/api/links', {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                index: index,
                name: name,
                url: url,
                port: port,
                image: image
            })
        });

        const result = await response.json();

        if (response.ok && result.status === 'success') {
            alert('保存成功!');
        } else {
            throw new Error(result.message || '保存失败');
        }
    } catch (error) {
        console.error('Error saving link:', error);
        alert('保存失败,请重试!错误信息:' + error.message);
    }
};

// 删除链接
window.deleteLink = async function(index) {
    if (!confirm('确定要删除这个链接吗?')) {
        return;
    }

    try {
        const response = await fetch(`/api/links?index=${index}`, {
            method: 'DELETE'
        });

        if (response.ok) {
            location.reload();
        } else {
            throw new Error('删除失败');
        }
    } catch (error) {
        console.error('Error deleting link:', error);
        alert('删除失败,请重试!');
    }
};

// 添加新链接
window.addNewLink = async function() {
    const name = document.getElementById('new-name').value;
    const url = document.getElementById('new-url').value;
    const port = document.getElementById('new-port').value;
    const imageFile = document.getElementById('new-image').files[0];

    let image = '/static/images/default.png';
    
    try {
        if (imageFile) {
            const formData = new FormData();
            formData.append('file', imageFile);
            const response = await fetch('/api/upload', {
                method: 'POST',
                body: formData
            });
            const data = await response.json();
            if (data.url) {
                image = data.url;
            }
        }

        const response = await fetch('/api/links', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                name,
                url,
                port,
                image
            })
        });

        if (response.ok) {
            location.reload();
        } else {
            throw new Error('添加失败');
        }
    } catch (error) {
        console.error('Error adding new link:', error);
        alert('添加失败,请重试!');
    }
};
iii. favicon.jpg

d. ./uploading/ 图片文件

图片会被 网站 打上水印,就不传。

推荐从 Midjourney.com 寻找与下载, AI created 图片是没有版权的,即:随便用。

4. 部署到 QNAP NAS Docker/Container上

a. Docker 部署文件

i. Dockerfile
# Dockerfile
FROM python:3.9-slim

# 工作目录
WORKDIR /app

# 环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    FLASK_APP=app.py \
    FLASK_ENV=production

# 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 复制文件
COPY requirements.txt .
COPY app.py .
COPY static static/
COPY templates templates/
COPY data data/

# 创建上传目录
RUN mkdir -p static/uploads && \
    chmod -R 777 static/uploads data

# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt

# 端口
EXPOSE 9999

# 启动命令
CMD ["python", "app.py"]
ii. requirements.txt min
flask
Werkzeug

b. 执行 docker 部署命令

i.CMD: docker build -t navigator_portal .
[/share/Multimedia/2024-MyProgramFiles/23.Navigator_Portal] # docker build -t navigator_portal .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/

Sending build context to Docker daemon  56.25MB
Step 1/13 : FROM python:3.9-slim
 ---> 6a22698eab0e
Step 2/13 : WORKDIR /app
...
...
 ---> d39c4c26f2c1
Successfully built d39c4c26f2c1
Successfully tagged navigator_portal:latest
[/share/Multimedia/2024-MyProgramFiles/23.Navigator_Portal] # 
ii. CMD:  docker run...
[/share/Multimedia/2024-MyProgramFiles/23.Navigator_Portal] # docker run -d -p 9999:9999 --name navigator_portal_container --restart always navigator_portal
31859f34dfc072740b38a4ebcdb9e9b6789acf95286b1e515126f2927c8467d5
[/share/Multimedia/2024-MyProgramFiles/23.Navigator_Portal] # 

5. 界面功能介绍

a. 页面总览

注:第一次使用这 app 代码,会因为缺失图片文件,而可能显示如下:撕裂的文件

b. 功能:

  • 鼠标移到图标,会向上移动,提醒被选中。
  • 点击右上角,蓝色 “编辑导航” 按钮,可能对图标内容修改

c. 编辑页面

d. 功能:

  • 图标排序:按住图标左侧的“6个点” 可以上下拖动 松手后即保存 (“编辑界面” 图1)
  • 图标体:可以删除、添加  (“编辑界面” 图3)
  • 图标内容可修改:描述, URL, 端口、图片更换  (“编辑界面” 图1 图2)
  • 对多条图标内容修改后,需要对每个图标都要点击 “保存”

已知问题:

  1. 图片不是 resize 保存,最好别使用太大的文件,尤其是在非 LAN 访问
  2. 图片的 URL 内容结尾不要有 "/" , 在移动图标顺序时会不成功

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/915982.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Kafka】集成案例:与Spark大数据组件的协同应用

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《大数据前沿&#xff1a;技术与应用并进》&#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、什么是kafka 2、Kafka 的主要特性 3、Kafka 的…

Windows上安装专业版IDEA2024并激活

1、IDEA官方下载 搜索IDEA官网点击进入&#xff0c;点击Download&#xff08;目前这个激活脚本只能激活2024.1.7&#xff0c;2024.2.x的版本都不能激活&#xff0c;2024.1.7版本已上传资源&#xff09;&#xff0c;如图&#xff1a; 2、开始安装 1&#xff09;、双击下载的.…

CSS教程(二)- CSS选择器

1. 作用 匹配文档中的某些元素为其应用样式。根据不同需求把不同的标签选出来。 2. 分类 分类 基础选择器 包含 标签选择器、ID选择器、类选择器、通用选择器等 复合选择器 包含 后代选择器、子代选择器、伪类选择器等 1 标签选择器 介绍 又称为元素选择器&#xff0c;根…

Unix进程

文章目录 命令行参数进程终止正常结束异常终止exit和_exitatexit 环境变量环境变量性质环境表shell中操作环境变量查看环境变量设置环境变量 环境变量接口获取环境变量设置环境变量 环境变量的继承性 进程资源shell命令查看进程的资源限制 进程关系进程标识进程组会话控制终端控…

c# onnx 调用yolo v11进行目标检测

先上图&#xff0c;支持图片&#xff0c;视频检测 FormYoloV11.cs using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using OpenCvSharp.Dnn; using System; using System.Collections.Generic; using System.Diagnostics; usin…

【多语言】每种语言打印helloworld,编译为exe会占多大空间

文章目录 背景c语言 53KBc 53KBgo 1.8Mdart 4.6Mpython未测试nodejs未测试rust未测试java未测试cmd || bash || powershell 未测试other 背景 各个版本的helloworld&#xff0c;纯属闲的, 环境如下: - win10 - mingw: gcc8.1.0 - go1.21 - dart3.5.4c语言 53KB gcc main.c -…

前端搭建低代码平台,微前端如何选型?

目录 背景 一、微前端是什么&#xff1f; 二、三大特性 三、现有微前端解决方案 1、iframe 2、Web Components 3、ESM 4、EMP 5、Fronts 6、无界&#xff08;文档&#xff09; 7、qiankun 四、我们选择的方案 引入qiankun并使用&#xff08;src外层作为主应用&#xff09; 主应…

CVE-2024-2961漏洞的简单学习

简单介绍 PHP利用glibc iconv()中的一个缓冲区溢出漏洞&#xff0c;实现将文件读取提升为任意命令执行漏洞 在php读取文件的时候可以使用 php://filter伪协议利用 iconv 函数, 从而可以利用该漏洞进行 RCE 漏洞的利用场景 PHP的所有标准文件读取操作都受到了影响&#xff1…

InternVL 多模态模型部署微调实践

目录 0 什么是MLLM 1 开发机创建与使用 2 LMDeploy部署 2.1 环境配置 2.2 LMDeploy基本用法介绍 2.3 网页应用部署体验 3 XTuner微调实践 3.1 环境配置 3.2.配置文件参数解读 3.3 开始微调 4.体验模型美食鉴赏能力 0 什么是MLLM 多模态大语言模型 ( Multimodal Larg…

干货分享之Python爬虫与代理

嗨伙伴们&#xff0c;今天是干货分享哦&#xff0c;可千万不要错过。今天小蝌蚪教大家使用phthon时学会巧妙借用代理ip来更好地完成任务。 让我们先了解一下为什么说咱们要用爬虫代理ip呢&#xff0c;那是因为很多网站为了防止有人过度爬取数据&#xff0c;对自身资源造成损害…

鸿蒙学习生态应用开发能力全景图-赋能套件(1)

文章目录 赋能套件鸿蒙生态应用开发能力全景图 赋能套件 鸿蒙生态白皮书: 全面阐释了鸿蒙生态下应用开发核心理念、关键能力以及创新体验,旨在帮助开发者快速、准确、全面的了解鸿蒙开发套件给开发者提供的能力全景和未来的愿景。 视频课程: 基于真实的开发场景,提供向导式…

netcat工具安装和使用

netcat是一个功能强大的网络实用工具&#xff0c;可以从命令⾏跨⽹络读取和写⼊数据。 netcat是为Nmap项⽬编写的&#xff0c;是⽬前分散的Netcat版本系列的经典。 它旨在成为可靠的后端⼯具&#xff0c;可⽴即为其他应⽤程序和⽤户提供⽹络连接。 一&#xff0c;下载安装 1&a…

【PHP】ThinkPHP基础

下载composer ComposerA Dependency Manager for PHPhttps://getcomposer.org/ 安装composer 查看composer是否安装 composer composer --version 安装 ThinkPHP6 如果你是第一次安装的话&#xff0c;首次安装咱们需要打开控制台&#xff1a; 进入后再通过命令,在命令行下面&a…

【HarmonyOS】应用实现读取剪切板内容(安全控件和自读取)

【HarmonyOS】应用实现读取粘贴板内容(安全控件和自读取) 前言 三方应用 读取系统剪切板是比较常见的功能。可以实现功能入口的快捷激活跳转&#xff0c;以及用户粘贴操作的简化&#xff0c;增强用户的体验感。 但是在用户日渐注重隐私的今天&#xff0c;系统对于剪切板权限的…

飞牛云fnOS本地部署WordPress个人网站并一键发布公网远程访问

文章目录 前言1. Docker下载源设置2. Docker下载WordPress3. Docker部署Mysql数据库4. WordPress 参数设置5. 飞牛云安装Cpolar工具6. 固定Cpolar公网地址7. 修改WordPress配置文件8. 公网域名访问WordPress 前言 本文旨在详细介绍如何在飞牛云NAS上利用Docker部署WordPress&a…

解析安卓镜像包和提取DTB文件的操作日志

概述 想查看一下安卓的镜像包里都存了什么内容 步骤 使用RKDevTool_v3.15对RK3528_DC_HK1_RBOX_K8_Multi_WIFI_13_20230915.2153.img解包 路径: 高级(Advancing) > 固件(firmware) > 解包(unpacking)得到\Output\Android\Image boot.imguboot.imgsuper.img 处理boot.…

LeetCode 热题100(八)【二叉树】(3)

目录 8.11二叉树展开为链表&#xff08;中等&#xff09; 8.12从前序与中序遍历序列构造二叉树&#xff08;中等&#xff09; 8.13路径总和III&#xff08;中等&#xff09; 8.14二叉树的最近公共祖先&#xff08;中等&#xff09; 8.15二叉树中的最大路径和&#xff08;困…

FPGA实现PCIE3.0视频采集转SDI输出,基于XDMA+GS2971架构,提供工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的PCIE方案本博已有的 SDI 编解码方案本博客方案的PCIE2.0版本 3、PCIE基础知识扫描4、工程详细设计方案工程设计原理框图电脑端视频QT上位机XDMA配置及使用XDMA中断模块FDMA图像缓存Native视频时序生成RGB转BT1120SDI转HDM…

纽约大学:指导LLM提出澄清性问题

&#x1f4d6;标题&#xff1a;Modeling Future Conversation Turns to Teach LLMs to Ask Clarifying Questions &#x1f310;来源&#xff1a;arXiv, 2410.13788 &#x1f31f;摘要 &#x1f538;大型语言模型&#xff08;LLM&#xff09;必须经常对高度模糊的用户请求做出…

STM32F1学习——I2C通信

一、I2C通信一带多 在学习通信的时候&#xff0c;我们常会听到串口通信。但串口通信只限定两个设备之间&#xff0c;如果有多个设备&#xff0c;通信的两个设备就要连接上&#xff0c;接线复杂。所以有了总线式通信&#xff0c;在一条总线上可以连接多个设备&#xff0c;这些根…