task card generator.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>任务卡片生成器</title>
  7. <style>
  8. body {
  9. display: flex;
  10. flex-direction: column;
  11. justify-content: flex-start; /* 从顶部开始排列 */
  12. align-items: center;
  13. min-height: 100vh;
  14. background: #121212; /* 深色背景,让卡片在预览时效果更一致 */
  15. margin: 0;
  16. font-family: 'Arial', 'Helvetica Neue', Helvetica, sans-serif; /* 更现代的字体栈 */
  17. padding: 30px 20px; /* 给body一些padding */
  18. box-sizing: border-box;
  19. }
  20. .export-container {
  21. background: #282828; /* 图片中外部容器的深灰色 */
  22. padding: 20px; /* 容器内边距 */
  23. border-radius: 8px;
  24. display: inline-block;
  25. box-shadow: 0 8px 20px rgba(0,0,0,0.3); /* 给容器一些阴影 */
  26. }
  27. .card {
  28. width: 370px; /* 调整宽度以适应两列布局和间距 */
  29. background: #000000; /* 卡片纯黑背景 */
  30. padding: 25px;
  31. box-sizing: border-box;
  32. color: #ffffff;
  33. border-radius: 6px; /* 给卡片本身也加一点圆角 */
  34. }
  35. .card-header {
  36. border-bottom: 1px solid #444444; /* 更细、颜色更深的分割线 */
  37. padding-bottom: 15px;
  38. margin-bottom: 20px;
  39. }
  40. .task-id {
  41. font-size: 14px;
  42. color: #888888; /* 灰色ID文字 */
  43. margin-bottom: 8px; /* ID和标题间距 */
  44. min-height: 1.2em; /* 确保空的时候也有高度,placeholder可以显示 */
  45. }
  46. .task-title {
  47. font-size: 24px;
  48. font-weight: bold;
  49. color: #ffffff;
  50. word-wrap: break-word;
  51. min-height: 1.2em;
  52. }
  53. .task-description {
  54. font-size: 15px;
  55. color: #e0e0e0;
  56. line-height: 1.6;
  57. word-wrap: break-word;
  58. margin-top: 15px;
  59. margin-bottom: 25px;
  60. min-height: 45px; /* 描述区域最小高度 */
  61. padding: 5px 2px; /* 轻微内边距 */
  62. }
  63. .task-details {
  64. display: grid;
  65. grid-template-columns: 1fr 1fr; /* 固定两列 */
  66. gap: 15px;
  67. font-size: 14px;
  68. margin-top: 20px;
  69. }
  70. .detail-item {
  71. background-color: #1c1c1c; /* 详情项的背景色,比卡片黑底亮一点 */
  72. padding: 12px;
  73. border-radius: 6px;
  74. }
  75. /* 如果最后一个元素是奇数个中的最后一个(例如第3个),则让它占据整行 */
  76. .detail-item:last-child:nth-child(odd) {
  77. grid-column: 1 / -1; /* 跨越所有列 */
  78. }
  79. .detail-item label {
  80. font-weight: normal;
  81. color: #aaaaaa; /* 标签文字颜色 */
  82. display: block;
  83. margin-bottom: 8px;
  84. font-size: 13px;
  85. }
  86. .detail-item .value.editable,
  87. .detail-item input[type="date"].value {
  88. color: #ffffff;
  89. background-color: #000000; /* 值区域纯黑背景 */
  90. border: 1px solid #333333; /* 值区域的细边框 */
  91. width: 100%;
  92. padding: 8px 10px; /* 值区域内边距 */
  93. min-height: 1.5em; /* 确保有高度 */
  94. border-radius: 4px;
  95. font-size: 15px;
  96. line-height: 1.4;
  97. box-sizing: border-box; /* padding和border不增加额外宽度 */
  98. }
  99. .detail-item input[type="date"].value {
  100. -webkit-appearance: none;
  101. -moz-appearance: none;
  102. appearance: none;
  103. position: relative;
  104. }
  105. .detail-item input[type="date"].value::-webkit-calendar-picker-indicator {
  106. filter: invert(0.8) brightness(0.8);
  107. cursor: pointer;
  108. opacity: 0.7;
  109. }
  110. .detail-item .value.editable {
  111. position: relative;
  112. }
  113. .detail-item .value.editable:empty::before {
  114. content: attr(data-placeholder);
  115. color: #555555;
  116. position: absolute;
  117. left: 10px;
  118. top: 50%;
  119. transform: translateY(-50%);
  120. pointer-events: none;
  121. width: calc(100% - 20px);
  122. overflow: hidden;
  123. text-overflow: ellipsis;
  124. white-space: nowrap;
  125. }
  126. .editable {
  127. position: relative;
  128. min-height: 1.2em;
  129. }
  130. .editable:empty:not(:focus)::before {
  131. content: attr(data-placeholder);
  132. color: #666666;
  133. position: absolute;
  134. left: 0;
  135. top: 0;
  136. pointer-events: none;
  137. width: 100%;
  138. overflow: hidden;
  139. text-overflow: ellipsis;
  140. white-space: nowrap;
  141. }
  142. .task-id.editable:empty:not(:focus)::before,
  143. .task-title.editable:empty:not(:focus)::before,
  144. .task-description.editable:empty:not(:focus)::before {
  145. left: 2px;
  146. top: 2px;
  147. }
  148. .editable:focus {
  149. outline: 1px dashed #777777;
  150. background-color: #0a0a0a;
  151. }
  152. [contenteditable] {
  153. -webkit-user-select: text;
  154. user-select: text;
  155. }
  156. [contenteditable]:focus {
  157. background-color: #0a0a0a !important;
  158. }
  159. ::selection {
  160. background-color: #444444;
  161. color: #ffffff;
  162. }
  163. .button-container {
  164. margin-top: 25px;
  165. text-align: center;
  166. }
  167. .export-btn {
  168. padding: 12px 25px;
  169. background: #1c1c1c;
  170. color: #ffffff;
  171. border: 1px solid #444444;
  172. cursor: pointer;
  173. font-size: 16px;
  174. border-radius: 6px;
  175. transition: background-color 0.2s ease, border-color 0.2s ease;
  176. }
  177. .export-btn:hover {
  178. background: #282828;
  179. border-color: #555555;
  180. }
  181. </style>
  182. </head>
  183. <body>
  184. <div class="export-container" id="export-container">
  185. <div id="taskCard" class="card">
  186. <div class="card-header">
  187. <div class="task-id editable" contenteditable="true" data-placeholder="任务ID (可选)"></div>
  188. <div class="task-title editable" contenteditable="true" data-placeholder="点击输入任务标题"></div>
  189. </div>
  190. <div class="task-description editable" contenteditable="true" data-placeholder="点击输入任务详细描述..."></div>
  191. <div class="task-details">
  192. <div class="detail-item">
  193. <label for="assignee-value">负责人:</label>
  194. <div id="assignee-value" class="value editable" contenteditable="true" data-placeholder="未分配"></div>
  195. </div>
  196. <!-- 标签现在是第二个 -->
  197. <div class="detail-item">
  198. <label for="tags-value">标签:</label>
  199. <div id="tags-value" class="value editable" contenteditable="true" data-placeholder="例如: 项目A, Bug"></div>
  200. </div>
  201. <!-- 截止日期现在是第三个,会自动占据整行 -->
  202. <div class="detail-item">
  203. <label for="dueDate-value">截止日期:</label>
  204. <input type="date" id="dueDate-value" class="value">
  205. </div>
  206. </div>
  207. </div>
  208. </div>
  209. <div class="button-container">
  210. <button class="export-btn" onclick="exportToPng()">导出为PNG</button>
  211. </div>
  212. <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
  213. <script>
  214. function sanitizeFilename(name) {
  215. return name.replace(/[^a-z0-9_\-.\u4e00-\u9fa5\s]/gi, '').replace(/\s+/g, '_');
  216. }
  217. function exportToPng() {
  218. if (window.getSelection) {
  219. if (window.getSelection().empty) { window.getSelection().empty(); }
  220. else if (window.getSelection().removeAllRanges) { window.getSelection().removeAllRanges(); }
  221. } else if (document.selection) { document.selection.empty(); }
  222. const container = document.getElementById('export-container');
  223. // const card = document.getElementById('taskCard'); // card variable not strictly needed here
  224. const originalDueDate = document.getElementById('dueDate-value').value;
  225. html2canvas(container, {
  226. backgroundColor: null,
  227. scale: 2,
  228. useCORS: true,
  229. logging: false, // Changed to false to reduce console noise unless debugging
  230. onclone: (documentClone) => {
  231. // const clonedCard = documentClone.getElementById('taskCard'); // clonedCard not strictly needed
  232. const dueDateInputClone = documentClone.getElementById('dueDate-value');
  233. if (dueDateInputClone) {
  234. dueDateInputClone.value = originalDueDate;
  235. if (!originalDueDate) {
  236. dueDateInputClone.style.color = '#555555';
  237. } else {
  238. dueDateInputClone.style.color = '#ffffff';
  239. }
  240. }
  241. const editables = documentClone.querySelectorAll('.editable');
  242. editables.forEach(el => {
  243. // Placeholder handling is mostly done by CSS (:empty::before)
  244. // If specific adjustments needed for clone, add here.
  245. // For example, ensuring text color if content exists:
  246. // if (el.textContent.trim() !== '' && el.classList.contains('value')) {
  247. // el.style.color = '#ffffff';
  248. // }
  249. });
  250. }
  251. }).then(canvas => {
  252. const link = document.createElement('a');
  253. const taskTitleElement = document.getElementById('task-title');
  254. let filename = '任务卡片.png';
  255. if (taskTitleElement && taskTitleElement.textContent.trim() !== '') {
  256. const sanitizedTitle = sanitizeFilename(taskTitleElement.textContent.trim());
  257. if (sanitizedTitle) { // Ensure title isn't just invalid chars
  258. filename = sanitizedTitle + '.png';
  259. }
  260. }
  261. link.download = filename;
  262. link.href = canvas.toDataURL('image/png');
  263. link.click();
  264. }).catch(err => {
  265. console.error("导出PNG失败:", err);
  266. alert("导出PNG失败,请查看控制台获取更多信息。");
  267. });
  268. }
  269. document.querySelectorAll('.editable').forEach(element => {
  270. element.addEventListener('focus', function() {
  271. // CSS handles placeholder via :not(:focus)::before
  272. });
  273. element.addEventListener('blur', function() {
  274. // CSS handles placeholder
  275. });
  276. });
  277. </script>
  278. </body>
  279. </html>