初始提交:人事共享服务中心钉钉登录功能

This commit is contained in:
zsc
2026-05-16 11:15:24 +08:00
commit 7ba21d6413
23 changed files with 1770 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
{% extends 'base.html' %}
{% block title %}管理后台{% endblock %}
{% block content %}
<h2>管理后台 - 所有需求</h2>
{% if demands %}
<div class="question-list">
{% for demand in demands %}
<div class="question-item">
<div class="question-header">
<span class="branch">{{ get_branch_name(demand.branch) }}</span>
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<span class="status {% if demand.is_public %}public{% else %}private{% endif %}">
{% if demand.is_public %}公开{% else %}私有{% endif %}
</span>
<span class="user">提交者: {{ demand.user.username }}</span>
</div>
<h3 class="demand-title">{{ demand.title }}</h3>
<p class="content">{{ demand.content }}</p>
<div class="question-footer">
<span class="contact">联系方式: {{ demand.contact }}</span>
{% if demand.answer %}
<div class="answer">
<strong>回答:</strong>
<p>{{ demand.answer }}</p>
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
{% else %}
<span class="status waiting">待回答</span>
{% endif %}
<div class="actions">
<a href="{{ url_for('edit_demand', id=demand.id) }}" class="btn btn-edit">编辑</a>
<a href="{{ url_for('answer_demand', id=demand.id) }}" class="btn btn-answer">回答</a>
<form method="POST" action="{{ url_for('toggle_public', id=demand.id) }}" style="display: inline;">
<button type="submit" class="btn btn-toggle">
{% if demand.is_public %}设为私有{% else %}设为公开{% endif %}
</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty">暂无需求</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block title %}回答需求{% endblock %}
{% block content %}
<h2>回答需求</h2>
<div class="question-preview">
<h3>需求内容</h3>
<h4 class="demand-title">{{ demand.title }}</h4>
<p>{{ demand.content }}</p>
<p class="meta">分会: {{ get_branch_name(demand.branch) }} | 联系方式: {{ demand.contact }}</p>
</div>
<form method="POST" class="form">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.answer.label }}
{{ form.answer(class="form-control", rows=8) }}
</div>
<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('index') }}" class="btn btn-cancel">取消</a>
</div>
</form>
{% endblock %}

47
templates/base.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}人事共享服务中心"码"上办{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<div class="container">
<h1>
<img src="{{ url_for('static', filename='logo.png') }}" alt="logo" class="header-logo">
<a href="{{ url_for('index') }}">人事共享服务中心"码"上办</a>
</h1>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">需求汇总</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('new_demand') }}">提交新需求</a></li>
<li><a href="{{ url_for('my_demands') }}">我的需求</a></li>
{% if current_user.is_admin() %}
<li><a href="{{ url_for('admin_demands') }}">管理后台</a></li>
{% endif %}
<li class="user-name">{{ current_user.dingtalk_name or current_user.username }}</li>
{% endif %}
</ul>
</nav>
</div>
</header>
<main class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer>
<div class="container">
<p>© 2026 人事共享服务中心"码"上办</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<h2>{{ title }}</h2>
<form method="POST" class="form">
{{ form.hidden_tag() }}
<div class="form-group">
<label>{{ form.title.label.text }} *</label>
{{ form.title(class="form-control") }}
</div>
<div class="form-group">
<label>{{ form.content.label.text }} *</label>
{{ form.content(class="form-control textarea") }}
</div>
<div class="form-group">
<label>{{ form.branch.label.text }}</label>
{{ form.branch(class="form-control") }}
</div>
<div class="form-group">
<label>{{ form.contact.label.text }} *</label>
{{ form.contact(class="form-control") }}
</div>
{{ form.is_public() }} <label for="is_public">{{ form.is_public.label.text }}</label>
<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('index') }}" class="btn btn-cancel">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,186 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>人事共享服务中心"码"上办</title>
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.15/dingtalk.open.js"></script>
<!-- Aplus SDK for performance monitoring -->
<script src="https://g.alicdn.com/a-plus/a-plus-web-sdk/1.1.2/index.js"></script>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
text-align: center;
color: white;
}
.loading {
font-size: 18px;
margin-top: 20px;
}
.error {
font-size: 16px;
color: #ffcccc;
margin-top: 20px;
padding: 15px;
background: rgba(255,0,0,0.2);
border-radius: 8px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.logo {
width: 80px;
height: 80px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<img src="/static/logo.png" alt="logo" class="logo">
<div class="spinner"></div>
<p class="loading">正在跳转...</p>
</div>
<script>
console.log('[前端] ============== 钉钉登录流程开始 ==============');
const APP_KEY = '{{ app_key }}';
const TARGET_URL = '{{ target_url }}';
const CORP_ID = '{{ corp_id }}';
const AGENT_ID = '{{ agent_id }}';
console.log('[前端] 配置信息:', { APP_KEY, TARGET_URL, CORP_ID, AGENT_ID });
function showError(msg) {
console.error('[前端] 错误:', msg);
document.querySelector('.loading').textContent = '配置失败';
document.querySelector('.spinner').style.display = 'none';
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = msg;
document.querySelector('.container').appendChild(errorDiv);
}
function getCleanUrl() {
var url = window.location.protocol + '//' + window.location.hostname;
if (window.location.port && window.location.port !== '80' && window.location.port !== '443') {
url += ':' + window.location.port;
}
url += window.location.pathname;
return url;
}
function handleAuthCode(code) {
console.log('[前端] 3. 获取到 code:', code);
console.log('[前端] 4. 开始请求后端 /requirement-collection/dingtalk/api/getDingUser?code=' + code);
fetch('/requirement-collection/dingtalk/api/getDingUser?code=' + code)
.then(function(resp) {
console.log('[前端] 5. 收到后端响应, 状态码:', resp.status);
return resp.json();
})
.then(function(user) {
console.log('[前端] 6. 解析响应数据:', user);
if (user.userid) {
var userId = user.userid;
var name = user.name || user.nickname || '';
var dept = user.department || '';
if (Array.isArray(dept)) {
dept = dept.join(',');
}
console.log('[前端] 7. 用户信息解析:', { userId: userId, name: name, dept: dept });
var targetUrl = TARGET_URL + '?userId=' + encodeURIComponent(userId) + '&name=' + encodeURIComponent(name) + '&dept=' + encodeURIComponent(dept);
console.log('[前端] 8. 准备跳转:', targetUrl);
window.location.href = targetUrl;
} else {
console.error('[前端] 7. 无userid, 无法登录');
showError('获取用户信息失败,请重试');
}
})
.catch(function(err) {
console.error('[前端] 6. fetch请求失败:', err);
showError('网络请求失败,请重试');
});
}
function initDingTalk() {
var url = getCleanUrl();
console.log('[前端] 0. 当前页面URL (清理后):', url);
var apiUrl = '/requirement-collection/dingtalk/api/getSignature?url=' + encodeURIComponent(url);
console.log('[前端] 1. 请求后端获取签名:', apiUrl);
fetch(apiUrl)
.then(function(resp) {
console.log('[前端] 2. 收到响应, 状态码:', resp.status);
return resp.json();
})
.then(function(data) {
console.log('[前端] 2. 收到签名数据:', data);
if (!data.signature) {
showError('获取签名失败: ' + (data.error || '未知错误'));
return;
}
console.log('[前端] 3. 配置dd.config...');
dd.config({
agentId: data.agentId ? Number(data.agentId) : (AGENT_ID ? Number(AGENT_ID) : 0),
corpId: CORP_ID,
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
signature: data.signature,
type: 0,
jsApiList: ['runtime.permission.requestAuthCode']
});
dd.ready(function() {
console.log('[前端] 4. dd.ready 成功 - 钉钉SDK已就绪');
console.log('[前端] 5. 开始调用 requestAuthCode 获取授权码...');
dd.runtime.permission.requestAuthCode({
corpId: CORP_ID,
onSuccess: function(res) {
handleAuthCode(res.code);
},
onFail: function(err) {
console.error('[前端] 6. requestAuthCode 失败:', err);
showError('获取授权码失败: ' + JSON.stringify(err));
}
});
});
dd.error(function(err) {
console.error('[前端] 4. dd.error - 钉钉SDK配置失败:', err);
showError('钉钉SDK配置失败: ' + JSON.stringify(err));
});
})
.catch(function(err) {
console.error('[前端] 1. 请求签名失败:', err);
showError('初始化失败: ' + err.message);
});
}
initDingTalk();
</script>
</body>
</html>

51
templates/index.html Normal file
View File

@@ -0,0 +1,51 @@
{% extends 'base.html' %}
{% block title %}需求汇总{% endblock %}
{% block content %}
<h2>需求汇总</h2>
{% if demands %}
<div class="question-list">
{% for demand in demands %}
<div class="question-item">
<h3 class="demand-title">{{ demand.title }} <span class="branch">{{ get_branch_name(demand.branch) }}</span></h3>
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<p class="content">{{ demand.content }}</p>
{% if demand.answer %}
<div class="answer-toggle" onclick="toggleAnswer(this)">
<span class="toggle-icon"></span>
<span class="toggle-text">查看回答</span>
</div>
<div class="answer-content" style="display: none;">
<div class="answer">
<strong>回答:</strong>
<p>{{ demand.answer }}</p>
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="empty">暂无公开需求</p>
{% endif %}
<script>
function toggleAnswer(element) {
var content = element.nextElementSibling;
var icon = element.querySelector('.toggle-icon');
var text = element.querySelector('.toggle-text');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▼';
text.textContent = '收起回答';
} else {
content.style.display = 'none';
icon.textContent = '▶';
text.textContent = '查看回答';
}
}
</script>
{% endblock %}

56
templates/my_demands.html Normal file
View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}我的需求{% endblock %}
{% block content %}
<h2>我的需求</h2>
{% if demands %}
<div class="question-list">
{% for demand in demands %}
<div class="question-item">
<h3 class="demand-title">{{ demand.title }} <span class="branch">{{ get_branch_name(demand.branch) }}</span></h3>
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<p class="content">{{ demand.content }}</p>
{% if demand.answer %}
<div class="answer-toggle" onclick="toggleAnswer(this)">
<span class="toggle-icon"></span>
<span class="toggle-text">查看回答</span>
</div>
<div class="answer-content" style="display: none;">
<div class="answer">
<strong>回答:</strong>
<p>{{ demand.answer }}</p>
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
</div>
{% endif %}
{% if not demand.answer %}
<div class="actions">
<a href="{{ url_for('edit_demand', id=demand.id) }}" class="btn btn-edit">编辑</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="empty">暂无需求</p>
{% endif %}
<script>
function toggleAnswer(element) {
var content = element.nextElementSibling;
var icon = element.querySelector('.toggle-icon');
var text = element.querySelector('.toggle-text');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▼';
text.textContent = '收起回答';
} else {
content.style.display = 'none';
icon.textContent = '▶';
text.textContent = '查看回答';
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block title %}未登录 - 人事共享服务中心"码"上办{% endblock %}
{% block content %}
<div class="login-required">
<div class="login-icon">🔒</div>
<h2>请先通过钉钉登录</h2>
<p>本系统需要通过钉钉账号登录才能使用</p>
</div>
{% endblock %}