0%

C# 的 LINQ (Language Integrated Query,語言整合查詢) 是一個非常強大的功能,它讓你可以使用類似 SQL 的語法來查詢各種不同的資料來源。這意味著你可以用統一的方式來處理資料,無論資料是來自陣列、列表、資料庫、XML 或其他任何支援 LINQ 的來源。

LINQ 的兩種語法:

LINQ 有兩種主要的語法形式:

  • 查詢語法 (Query Syntax): 看起來很像 SQL 語法,使用 from、where、select 等關鍵字。這種語法比較易讀,尤其是在處理複雜的查詢時。
  • 方法語法 (Method Syntax): 使用擴充方法 (Extension Methods) 來表示查詢運算子。這種語法更簡潔,也更靈活。簡單的查詢或需要鏈式操作時,通常使用方法語法更為方便。
Read more »

class 是物件導向程式設計 (OOP) 語言的核心功能。它通過邏輯性地組織方法和屬性,來表示並封裝特定的問題概念。

建立 class 與 instance

假設要建立一 person 的 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class Person
{

}

public class Program
{
public static void Main()
{
Person person = new Person();
}
}

Read more »

新的一年有機會在公司內部接觸一些些後端語言,公司的後端是 ASP.Net,所以開啟了 C# 學習之路
最最初階的學習資源,目前是來自:
dotnetcademy
適用於初學者的 C# 基礎知識

宣告變數,需指定其類型並提供名稱

C# 是 強型別語言 (Strongly Typed Language),這表示每個變數 (Variable) 或常數 (Constant) 在使用之前都必須先明確定義其型別 (Type)。

1
2
3
4
string greeting;
int a, b, c;
List<double> xs;

Read more »

Write a class that allows getting and setting key-value pairs, however a time until expiration is associated with each key.

The class has three public methods:

set(key, value, duration): accepts an integer key, an integer value, and a duration in milliseconds. Once the duration has elapsed, the key should be inaccessible. The method should return true if the same un-expired key already exists and false otherwise. Both the value and duration should be overwritten if the key already exists.

get(key): if an un-expired key exists, it should return the associated value. Otherwise it should return -1.

count(): returns the count of un-expired keys.

中文說明

set(key, value, duration): 接受一個整數的 key、一個整數值 value,以及一個毫秒為單位的時間 duration。當持續時間結束後,這個鍵值對將不能被存取。如果相同且未過期的鍵存在,則回傳 true,若不存在則回傳 false。如果這個鍵已經存在,其值與持續時間都應該被新傳入的 value 與 duration 覆寫。
get(key):如果存在未過期的鍵,回傳其對應的值。若沒有相對應的鍵,則回傳 -1。
count():回傳現存未過期的鍵的總數量。

Read more »

Python 的安裝步驟

為什麼要學習 Python?

最近發現 GitHub 上許多新專案都適用 Python 或是要使用的 AI 工具也會是使用 Python 開發,於是決定也來學學 Python!

如何安裝 Python?

  • 前往 Python 的官方網站 下載安裝程式。
  • 按照安裝程式的指示完成安裝。
  • 安裝完成後,打開終端機(或命令提示字元),輸入以下指令確認安裝是否成功:
    1
    python --version
  • 若您有多個專案需使用不同的 Python 版本,建議使用版本管理工具,例如 pyenv。
Read more »

CSS Scroll Snap 是一種用於控制滾動行為的 CSS 模組,允許開發者在使用者停止滾動時,自動將視圖對齊到特定的元素。這種功能常用於創造更流暢的用戶體驗,例如分頁文章、圖片輪播等。

主要由兩個屬性來控制:

scroll-snap-type:設置在滾動容器(父元素)上,用於定義滾動軸向和滾動對齊的行為。

格式為:scroll-snap-type: [軸向] [行為]
軸向可以是 x(水平滾動)、y(垂直滾動)或 both(同時支持水平與垂直)。
行為可以是 mandatory 或 proximity:
mandatory:滾動必須對齊到指定位置,確保每次停下來都會對齊。
proximity:當滾動接近某個指定的位置時,會進行對齊。
例如:

1
2
3
.scroll-container {
scroll-snap-type: y mandatory; /* 在垂直方向強制對齊 */
}

scroll-snap-align:設置在滾動內容項目(子元素)上,定義這些元素在滾動容器中的對齊方式。

可選的值有 start、center、end,決定項目在滾動結束時如何與容器對齊。
例如:

1
2
3
.scroll-item {
scroll-snap-align: start; /* 將項目對齊到滾動容器的頂端 */
}
屬性 描述
scroll-snap-type 定義滾動容器的捕捉行為,包括方向(x, y, both)和強制程度(mandatory, proximity)。
scroll-padding 設置容器內邊距,影響捕捉點的位置。
scroll-snap-align 定義子元素在容器中的對齊方式(start, center, end)。
scroll-snap-stop 控制是否在特定元素上停留,默認情況下僅在停止滾動時觸發。

使用時注意事項

  1. 跨瀏覽器不一致性
    雖然大多數現代瀏覽器都支持 Scroll Snap,但在行為上可能會有一些細微的差異。請務必在各種瀏覽器上測試你的實現,確保用戶能夠獲得一致的體驗。像是使用 cna I us 確認支援。
    對於某些舊版本仍然需要前綴。使用 WebKit 前綴來支持較舊的 Safari 和 Chrome。
1
2
3
4
.scroll-container {
-webkit-scroll-snap-type: y mandatory; /* 用於舊版 Safari 和 Chrome */
scroll-snap-type: y mandatory; /* 標準屬性 */
}

使用 @supports 進行功能檢測:你可以使用 @supports 檢測瀏覽器是否支持 Scroll Snap,並提供相應的回退方案。例如,如果某些瀏覽器不支持,則可以使用 JavaScript 實現類似的行為。

1
2
3
4
5
6
7
8
9
10
11
12

@supports (scroll-snap-type: y mandatory) {
.scroll-container {
scroll-snap-type: y mandatory;
}
}

@supports not (scroll-snap-type: y mandatory) {
.scroll-container {
/* 恢復樣式,或提供 JavaScript 實現滾動行為 */
}
}
  1. 預期外的滾動行為
    如果發現未對齊正確,或者容器的尺寸設置不當,可能會出現預期外的滾動行為。
    解決方案:
    設置固定尺寸:確保滾動容器和子元素的尺寸設置正確。對於橫向滾動,應確保每個項目的寬度是明確的,對於縱向滾動,應設置每個項目的高度。例如:
1
2
3
4
5
6
7
8
9
10
複製程式碼
.scroll-container {
overflow-x: scroll; /* 必須設置 overflow 屬性 */
scroll-snap-type: x mandatory; /* 水平捕捉點 */
}

.scroll-item {
width: 100%; /* 保證每個項目都佔據滾動區域 */
scroll-snap-align: start; /* scroll-snap-align 決定了子元素在滾動容器中如何對齊 */
}
  1. 性能影響

當頁面內容繁多,或包含大量動畫和捕捉點時,可能會導致性能問題。這會導致滾動卡頓,特別是在移動設備上。

解決方案:
避免過度使用捕捉點:不要對每一個小元素都設置 scroll-snap-align,僅在核心內容(如頁面段落、關鍵節點)設置捕捉點。例如,圖片輪播或長段落之間可以有捕捉點,但不需要對每個小項目設置。

除此之外,可以嘗試 lazy load:對於圖片或大型內容,使用懶加載技術(如 Intersection Observer API)可以避免一次性渲染過多內容,從而減輕滾動過程中的性能負擔。

  • 使用 Intersection Observer 實現懶加載: 用來觀察(observe)當指定元素接觸到父層以上或者是視窗的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const images = document.querySelectorAll('.lazy-load');
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 替換為真正的圖片
observer.unobserve(img); // 停止觀察
}
});
});

images.forEach(image => {
observer.observe(image); // 觀察每個懶加載圖片
});

  • 減少不必要的動畫:過多的動畫和滾動事件監聽器會降低滾動性能,特別是如果動畫效果頻繁更新。例如,可以使用 requestAnimationFrame 來控制滾動動畫的頻率,避免過多的重排和重繪。或是嘗試,節流滾動事件:
1
2
3
4
5
6
7
8
9
let isScrolling;
window.addEventListener('scroll', () => {
window.clearTimeout(isScrolling);
isScrolling = setTimeout(() => {
// 在滾動結束後觸發的操作
console.log('滾動結束');
}, 66); // 節流時間間隔
});

參考文章:
超好用的 Web API - Intersection Observer
No JS required — you can do this with CSS!

image

在 Quasar 的日期選擇器,要使用 QInput + QDate 方式,可以參考文件 Date Picker

但在此次功能需求,是讓使用者只能選擇年/月。所以首先依循查詢結果,找到 monthpicker ,在其中 codepen - QDate: DATE YEAR/MONTH PICKER,透過設置 setView 屬性,使得選擇器界面可以顯示年、月以及日的選擇視圖。

自制 monthlyPicker

因此參考上方的方式,建立一個 monthlyPicker 元件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div class="q-mr-md">
<q-input
dense
v-model="monthValue"
id="date-input"
class="single-month-picker"
readonly
>
<template v-slot:append>
<q-icon
name="fa-regular fa-calendar"
class="cursor-pointer"
size="1.1rem"
>
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<div class="q-date__header flex column items-start">
<span @click="handleToMonthView" class="cursor-pointer"
>{{ "Year - Month" }}</span
>
<div class="text-h5 cursor-pointer" @click="handleToYearView">
{{ monthValue }}
</div>
</div>
<q-date
class="date-disable-btn"
dense
ref="dateRef"
v-model="monthValue"
default-view="Years"
emit-immediately
@update:model-value="onUpdate"
mask="YYYY-MM"
minimal
years-in-month-view
:navigation-min-year-month="minYearMonth"
:navigation-max-year-month="maxYearMonth"
>
<div class="row items-center justify-end date-action-btn">
<q-btn
dense
flat
v-close-popup
class="btn--no-hover"
label="Close"
color="primary"
/>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script setup lang="ts">
import { date, QDate } from 'quasar'

const emit = defineEmits(['selectedDate'])
const props = defineProps({
month: {
type: String,
required: true
}
})
const monthValue = ref(props.month)
const dateRef = ref<InstanceType<typeof QDate> | null>(null)
const currentView = ref<'Years' | 'Months' | 'Days'>('Years')

// computed
const minYearMonth = computed(() => {
const oneYearAgo = date.subtractFromDate(new Date(), { month: 13 })
return date.formatDate(oneYearAgo, 'YYYY/MM')
})

const maxYearMonth = computed(() => {
const oneMonthAgo = date.subtractFromDate(new Date(), { month: 1 })
return date.formatDate(oneMonthAgo, 'YYYY/MM')
})

const onUpdate = () => {
emit('selectedDate', date.formatDate(new Date(monthValue.value), 'YYYY-MM'))
setCalendarView('Months')
}

const handleToMonthView = () => {
setCalendarView('Months')
}
const handleToYearView = () => {
setCalendarView('Years')
}

const setCalendarView = (view: 'Years' | 'Months') => {
currentView.value = view
dateRef.value?.setView(view)
}
</script>

簡要說明

  • QDate 組件:設置 default-view 為 Years,讓選擇器初始顯示年份視圖。mask 設置為 YYYY-MM,確保輸出格式為年-月。
  • 計算屬性:透過 minYearMonth 和 maxYearMonth 屬性,限制使用者只能選擇過去一年內的日期範圍。
  • 視圖切換:通過 handleToMonthView 和 handleToYearView 函數來實現視圖切換,並且用 setCalendarView 函數來控制視圖變更。

外層使用該元件方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<monthly-picker :month="selectedMonth" @selectedDate="handleDateSelected" />
<p>你選擇的月份是:{{ selectedMonth }}</p>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import MonthlyPicker from "./components/MonthlyPicker.vue";

const selectedMonth = ref("2024-08");

const handleDateSelected = (date: string) => {
selectedMonth.value = date;
};
</script>

在前端開發中,有時需要從後端提供數據並生成文件提供用戶下載。以下將以下載 Excel 為例,介绍如何使用 axios 從後端取得文件,並在前端處理文件的下載過程。我们還會顯示如何從 HTTP header 中提取文件名,以 f 確保下載的文件命名正確。

Api 設定

要記得設置告诉 axios 期望接收的類型為 blob,若是不設置,則會預設為 json,收到的資料會是亂碼。

1
2
3
4
5
6
7
8

exportFile(searchData) {
return axios.get("/data/export", {
params: searchData,
responseType: "blob"
});
}

前端處理文件下載

獲取到後端返回的文件數據後,我們需要在前端將其轉換為可下載的文件格式。以下是一個完整的示例,展示瞭如何將 blob 數據生成 Excel 文件並觸發下載。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const handleExportFile = async () => {
try {
const res = await exportFile(data);

const blob = new Blob([res.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});

const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;

const fileName = "export.xlsx";
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
link.remove();

window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error(error);
}
};

new Blob([res.data], { type: … }) 做了什麼?

這里,用 new Blob() 創建了一個 Blob 對象。
res.data 是從後端 API 接收到的文件數據。
我們將這個數據放入 Blob 中,同時指定了文件的類型,即 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,這是 Excel 文件的 MIME 類型。這樣瀏覽器就能識別出這是一個 Excel 文件。

動態創建一個 <a> 標簽,並通過模擬點擊觸發文件下載。

使用 window.URL.createObjectURL(blob) 創建了一個特殊的 URL,它指向我們剛才創建的 Blob 對象。
可以理解為它生成了一個臨時的下載地址,使用者可以透過這個地址下載文件。
接著 document.createElement(“a”) 創建 <a> 標籤,接下來要利用這個標籤來實現文件下載。
也就是 link.href = downloadUrl; 給 <a> 標籤賦值,將剛才生成的 downloadUrl 設定為這個 <a> 標籤的 link 地址 (href),也就是說,點擊這個 link 會指向我們的 Blob 物件(即文件資料)。

設定文件名並觸發下載

使用 setAttribute 方法給<a> 標籤添加一個 download 屬性,並設定文件名為 “export.xlsx”。這樣,當用戶點擊鏈接時,瀏覽器會提示下載文件,並自動將文件保存為 export.xlsx。
document.body.appendChild 將 <a> 標籤臨時加到 document 的 body 中。雖然使用者不會看到這個 link,但它在頁面上是存在的。
然後模擬一次使用者點擊這個連結 (link.click()),觸發文件的下載過程。
link.remove();下載操作完成後,我們把這個臨時創建的 <a> 標籤從頁面中移除。

Read more »

在現代網頁應用中,強大的文字編輯器是必不可少的。Tiptap 是一款基於 ProseMirror 的高擴展性編輯器。本文將介紹如何在 Vue3 中使用 Tiptap,並結合 Vuetify 實現文字編輯器。

官網文件

首先官網:https://tiptap.dev/docs/editor/getting-started/install/vue3

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<editor-content :editor="editor" />
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
content: '<p>I’m running Tiptap with Vue.js. 🎉</p>',
extensions: [StarterKit],
})
</script>

根據對應 EXTENSIONS 找出如何加入在編輯器,如:bold, italic

1
2
3
4
5
6
     <button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
Toggle bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
Toggle italic
</button>
Read more »

最近嘗試在專案內加入測試,詢問朋友後建議先從 utils 中共用的邏輯函式開始,而在共用的邏輯函式中,會有幾個是 i18n 相關的函式。
因為頁面中常會用到將數值,轉換為顯示在畫面上的文字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
const getType = (type: number) => {
switch (type) {
case 0:
return t('Type.Auto')
case 1:
return t('Type.Manual')
case 2:
return t('Type.Mix')
default:
return null
}
}

在進行測試時,總想說是否要先選定一語系,如中文\英文,然後確認他轉換的是否為該文字
但是經過嘗試後,覺得或許不應該是檢查轉換後的文字,應該要以對應 key 來檢查。
參考 overflow

  • 因此專案是使用 quasar,所以在 i18n.t 方法,引入的位置是 boot/i18n.ts
  • 在測試中,可以執行測試函式,帶入對應數值,並檢查是否回傳對應的 key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// 在測試中覆寫模擬 i18n.t 方法
vi.mock('../../../src/boot/i18n', () => ({
i18n: {
global: {
t: (key: string) => key, // 提供模擬的 t 函式
locale: 'en' // 假設默認的 locale 值
}
}
}))

describe('getType', () => {
it('should return the correct string for type 0', () => {
expect(getTradingType(0)).toBe('Type.Auto')
})

it('should return the correct string for type 1', () => {
expect(getTradingType(1)).toBe('Type.Manual')
})

it('should return the correct string for type 2', () => {
expect(getTradingType(2)).toBe('Type.Mix')
})
it('should return null for an unknown type', () => {
expect(getTradingType(999)).toBeNull()
})
})

小結上述方式:

使用全局模擬,vi.mock 全局模擬 i18n.t 方法,並提供模擬的 t 函式,這樣就可以在測試中,直接檢查是否回傳對應的 key,而不用檢查轉換後的文字。

另外,在跨頁面也會有共用的下拉選單,將此下拉選單的選項,也提取出來,並進行測試。

1
2
3
4
5
6

const directionOpt = computed(() => ([
{ label: t('Shared.right'), value: 0 },
{ label: t('Shared.wrong'), value: 1 }
]))

原本也想是否直接檢查 key 是否正確即可。但基於困惑,就嘗試丟 chatGpt 詢問

Read more »