2024.08.09 台北大巨蛋







參考 Learn How to Code a Simple JavaScript Calendar and Datepicker 這篇教學,做了一個簡單的月曆。
const previousMonth = document.querySelector('.previous-month');
const nextMonth = document.querySelector('.next-month');
const yearMonth = document.querySelector('.year-month');
const day = document.querySelector('.day');
let today = new Date();
// returns the full year of a date
// 4 digits
let year = today.getFullYear();
// returns the month (0 to 11) of a date
let month = today.getMonth();
showCalendar();
// 上個月
previousMonth.addEventListener('click', (e) => {
e.preventDefault();
month--;
if (month < 0) {
year--;
month = 11;
}
showCalendar();
});
// 下個月
nextMonth.addEventListener('click', (e) => {
e.preventDefault();
month++;
if (month > 11) {
year++;
month = 0;
}
showCalendar();
});
// function
function showCalendar() {
yearMonth.innerHTML = `${year} / ${month + 1}`;
// 當月第一天
const firstDay = new Date(year, month, 1);
// returns the day of the week (0 to 6) of a date
// Sunday = 0, Monday = 1, ....
const firstDayIndex = firstDay.getDay();
// 當月最後一天
const lastDay = new Date(year, month + 1, 0);
// returns the day of the month (1 to 31) of a date
const daysInMonth = lastDay.getDate();
let dayHtml = '';
for (let i = 1; i <= 42; i++) {
if (i < firstDayIndex + 1) {
dayHtml += '<div></div>';
} else if (i > firstDayIndex + daysInMonth) {
break;
} else {
const dayNumber = i - firstDayIndex;
if (
year === new Date().getFullYear() &&
month === new Date().getMonth() &&
dayNumber === new Date().getDate()
) {
dayHtml += `<div class="current-date">${dayNumber}</div>`;
} else {
dayHtml += `<div>${dayNumber}</div>`;
}
}
}
day.innerHTML = dayHtml;
}
[UPDATE] 之前的版本不能壓縮資料夾,用 ChatGPT 再修改一下….
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const ftpClient = require('ftp');
const nodemailer = require('nodemailer');
const toUser = '[email protected]';
let fileSize;
// 取得 .zip 檔名,預設為:YYYYMMDD-HHMMSS
const zipFileName = process.argv[2] || getDateTime();
if (!checkFileName(zipFileName)) {
console.error('檔名輸入有誤!!');
return false;
}
// input_files 資料夾
const inputDir = path.join(__dirname, 'input_files');
// 確認 output_zip 資料夾是否存在,若不存在則建立
const outputDir = path.join(__dirname, 'output_zip');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
// 建立一個輸出檔案流,指定存放在 output_zip 資料夾下
const outputFilePath = path.join(outputDir, `${zipFileName}.zip`);
// 壓縮
zipDirectory(inputDir, outputFilePath);
// --- function ---
function formatBytes(bytes) {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getDateTime() {
let today = new Date();
let year = today.getFullYear();
let month = ('0' + (today.getMonth() + 1)).slice(-2);
let date = ('0' + today.getDate()).slice(-2);
let hours = ('0' + today.getHours()).slice(-2);
let minutes = ('0' + today.getMinutes()).slice(-2);
let seconds = ('0' + today.getSeconds()).slice(-2);
return `${year}${month}${date}-${hours}${minutes}${seconds}`;
}
function checkFileName(fileName) {
const regex = /^[a-zA-Z0-9_-]+$/;
return regex.test(fileName);
}
function zipDirectory(sourceDir, outPath) {
const output = fs.createWriteStream(outPath);
const archive = archiver('zip', {
zlib: { level: 9 } // 設定壓縮等級 (1-9)
});
// 開始壓縮
archive.pipe(output);
archive.directory(sourceDir, false); // 將資料夾的內容添加至壓縮檔
archive.finalize();
output.on('close', () => {
fileSize = formatBytes(archive.pointer());
console.log(`[${zipFileName}.zip] 壓縮完成,總共 ${fileSize}`);
// 壓縮完成後上傳到 FTP 伺服器
uploadFileToFtp();
});
archive.on('error', (err) => {
throw err;
});
}
function uploadFileToFtp() {
const client = new ftpClient();
client.on('ready', () => {
client.put(
outputFilePath,
`/downloads/${zipFileName}.zip`,
(err) => {
if (err) {
throw err;
}
console.log('檔案上傳完成 OK');
client.end();
// 發送電子郵件通知
sendEmail();
}
);
});
client.on('error', (err) => {
console.error('FTP 連接錯誤:', err);
});
// 連接到 FTP 伺服器
client.connect({
host: '100.100.100.100',
user: 'user',
password: 'password',
});
}
function sendEmail() {
const transporter = nodemailer.createTransport({
host: 'smtp.office365.com',
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: 'user',
pass: 'pass',
},
});
const mailOptions = {
from: '"SYSTEM" <[email protected]>',
to: toUser,
subject: `${zipFileName}.zip`,
text: `File Size: ${fileSize}\nhttps://www.abc.com.tw/downloads/${zipFileName}.zip`,
};
transporter.sendMail(mailOptions, (err) => {
if (err) {
console.error('電子郵件發送失敗:', err);
return false;
}
console.log('電子郵件發送成功 OK');
});
}
這個小工具可以把特定資料夾內的檔案壓縮成一個 .zip 檔,再把 .zip 檔上傳到 FTP Server,上傳成功後會發送通知 Email。
有用到 archiver、ftp、nodemailer 三個 package。
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const ftpClient = require('ftp');
const nodemailer = require('nodemailer');
const toUser = '[email protected]'; // 要收到通知的 user
let fileSize;
const archive = archiver('zip', {
zlib: { level: 9 }, // 設定壓縮等級
});
// 取得 .zip 檔名,預設為:YYYYMMDD-HHMMSS
const zipFileName = process.argv[2] || getDateTime();
if (!checkFileName(zipFileName)) {
console.error('檔名輸入有誤!!');
return false;
}
// 確認 output_zip 資料夾是否存在,若不存在則建立
const outputDir = path.join(__dirname, 'output_zip');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
// 建立一個輸出檔案流,指定存放在 output_zip 資料夾下
const outputFilePath = path.join(outputDir, `${zipFileName}.zip`);
const output = fs.createWriteStream(outputFilePath);
// 連結檔案流至壓縮檔
archive.pipe(output);
// 獲取 input_files 資料夾中的所有檔案
const inputDir = path.join(__dirname, 'input_files');
fs.readdir(inputDir, (err, files) => {
if (err) {
throw err;
}
let count = 0;
// 使用 for 迴圈添加每個檔案至壓縮檔
for (const file of files) {
const filePath = path.join(inputDir, file);
archive.file(filePath, { name: file });
console.log(`[${file}] 加入壓縮檔....`);
count++;
}
console.log(`共 [${count}] 個檔案加入`);
// 完成壓縮檔案
archive.finalize();
});
output.on('close', () => {
fileSize = formatBytes(archive.pointer());
console.log(`[${zipFileName}.zip] 壓縮完成,總共 ${fileSize}`);
// 壓縮完成後上傳到 FTP 伺服器
uploadFileToFtp();
});
archive.on('warning', (err) => {
if (err.code !== 'ENOENT') {
throw err;
}
});
archive.on('error', (err) => {
throw err;
});
// --- function ---
function formatBytes(bytes) {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getDateTime() {
let today = new Date();
let year = today.getFullYear();
let month = ('0' + (today.getMonth() + 1)).slice(-2);
let date = ('0' + today.getDate()).slice(-2);
let hours = ('0' + today.getHours()).slice(-2);
let minutes = ('0' + today.getMinutes()).slice(-2);
let seconds = ('0' + today.getSeconds()).slice(-2);
return `${year}${month}${date}-${hours}${minutes}${seconds}`;
}
function checkFileName(fileName) {
const regex = /^[a-zA-Z0-9_-]+$/;
return regex.test(fileName);
}
function uploadFileToFtp() {
const client = new ftpClient();
client.on('ready', () => {
client.put(
outputFilePath,
`/downloads/${zipFileName}.zip`,
(err) => {
if (err) {
throw err;
}
console.log('檔案上傳完成 OK');
client.end();
// 發送電子郵件通知
sendEmail();
}
);
});
client.on('error', (err) => {
console.error('FTP 連接錯誤:', err);
});
// 連接到 FTP 伺服器
client.connect({
host: '100.100.100.100',
user: 'user',
password: 'password',
});
}
function sendEmail() {
const transporter = nodemailer.createTransport({
host: 'smtp.office365.com',
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: 'user',
pass: 'pass',
},
});
const mailOptions = {
from: '"SYSTEM" <[email protected]>',
to: toUser,
subject: `${zipFileName}.zip`,
text: `File Size: ${fileSize}\nhttps://www.abc.com.tw/downloads/${zipFileName}.zip`,
};
transporter.sendMail(mailOptions, (err) => {
if (err) {
console.error('電子郵件發送失敗:', err);
return false;
}
console.log('電子郵件發送成功 OK');
});
}
網站沒有做前後端分離,但想要用 Vue,而且還要用 Composition API,可以使用下面的方式:
// 引用的時候記得要加上 type="module"
// <script src="demo.js" type="module"></script>
// 開發的時候可以把 vue.esm-browser.prod.js 的 .prod 去掉,方便用 Vue.js devtools 除錯
import {
createApp,
ref,
computed,
watch,
nextTick,
onMounted,
} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js';
// 引入 component
import SelectComponent from './select-component.js';
const vueApp = createApp({
components: {
SelectComponent,
},
setup() {
...
return { ... }
}
});
vueApp.mount('#app');
// select-component.js
import { ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js';
export default {
name: 'SelectComponent',
props: ['modelValue', 'getPageNumber', 'itemsPerPage'],
emits: ['update:modelValue'],
template: `
<select
class="form-select"
v-model="page"
@change="$emit('update:modelValue', $event.target.value)"
>
<option
:value="n"
v-for="n in getPageNumber"
>
Episode {{ (n * itemsPerPage) - itemsPerPage + 1 }} ~ Episode {{ n * itemsPerPage }}
</option>
</select>
`,
setup(props) {
const page = ref(props.modelValue);
return { page };
},
};
// 在 html 使用 component
// tag 使用 <select-component></select-component>
<select-component
v-model="page"
:get-page-number="getPageNumber"
:items-per-page="ITEMS_PER_PAGE"
></select-component>
npm init -y
---
npm i sass gulp-sass gulp gulp-clean-css gulp-uglify gulp-babel @babel/core @babel/preset-env [email protected] --save-dev
npm i [email protected] gulp-sass gulp gulp-clean-css gulp-uglify gulp-babel @babel/core @babel/preset-env [email protected] eslint-config-prettier --save-dev
---
npm i [email protected]
---
npm init @eslint/config