Quasar Vite Vue3 專案安裝

npm 進行 quasar 建置

使用 npm 進行 quasar 建置。
目前使用 Node v20.17.0

1
npm init quasar

npx
create-quasar

? What would you like to build? › - Use arrow-keys. Return to submit.
? 您想要建造什麼? ›——使用箭頭鍵。返回提交。
❯ App with Quasar CLI, let’s go! <= 選擇這個
❯ 帶有Quasar CLI的應用程序,我們開始吧!
spa/pwa/ssr/bex/electron/capacitor/cordova
AppExtension (AE) for Quasar CLI
Quasar UI kit (Quasar UI 套件)

✔ What would you like to build? › App with Quasar CLI, let’s go!
? Project folder: › quasar-project
? Pick script type: › - Use arrow-keys. Return to submit.
Javascript
❯ Typescript <= 選擇這個
? Pick Quasar App CLI variant: › - Use arrow-keys. Return to submit.
❯ Quasar App CLI with Vite - recommended ( Quasar App CLI 與 Vite - 推薦)<= 選擇這個
Quasar App CLI with Webpack
? Project product name: (must start with letter if building mobile apps) › Quasar App
?專案產品名稱:(如果建立行動應用程序,必須以字母開頭)› Quasar App
? Project description: › A Quasar Project
? Pick a Vue component style: › - Use arrow-keys. Return to submit.
❯ Composition API with < script setup > <= 選擇這個
recommended
Composition API
Options API
? Pick your CSS preprocessor: › - Use arrow-keys. Return to submit.
❯ Sass with SCSS syntax <= 選擇這個
Sass with indented syntax
None (the others will still be available)
Instructions:
↑/↓: Highlight option
←/→/[space]: Toggle selection
a: Toggle all
enter/return: Complete answer
◉ Linting (vite-plugin-checker + ESLint + vue-tsc)
recommended
◉ State Management (Pinia)
◉ axios
◉ vue-i18n

a =>全選
? Add Prettier for code formatting? › (Y/n) ?新增 Prettier 來進行程式碼格式化? › (是/否)<= 選擇是

Quasar • Generating files…

  • README.md
  • .editorconfig
  • .gitignore
  • .npmrc
  • .vscode/extensions.json
  • .vscode/settings.json
  • package.json
  • tsconfig.json
  • index.html
  • postcss.config.js
  • public/favicon.ico
  • public/icons/favicon-128x128.png
  • public/icons/favicon-16x16.png
  • public/icons/favicon-32x32.png
  • public/icons/favicon-96x96.png
  • quasar.config.ts
  • src/App.vue
  • src/assets/quasar-logo-vertical.svg
  • src/boot/.gitkeep
  • src/components/EssentialLink.vue
  • src/components/ExampleComponent.vue
  • src/components/models.ts
  • src/env.d.ts
  • src/layouts/MainLayout.vue
  • src/pages/ErrorNotFound.vue
  • src/pages/IndexPage.vue
  • src/router/index.ts
  • src/router/routes.ts
  • src/css/app.scss
  • src/css/quasar.variables.scss
  • src/boot/axios.ts
  • src/boot/i18n.ts
  • src/i18n/en-US/index.ts
  • src/i18n/index.ts
  • eslint.config.js
  • .prettierrc.json
  • src/stores/example-store.ts
  • src/stores/index.ts

Quasar • SUCCESS • The project has been scaffolded

? Install project dependencies? (recommended) › - Use arrow-keys. Return to submit.
❯ Yes, use npm <= 選擇
No, I will handle that myself

✔ Install project dependencies? (recommended) › Yes, use npm

quasar-project@0.0.1 postinstall
quasar prepare

App • Using quasar.config.ts in “ts” format
App • Generated tsconfig.json and types files in .quasar directory
App • The app is now prepared for linting, type-checking, IDE integration, etc.

added 547 packages, and audited 548 packages in 6m

144 packages are looking for funding
run npm fund for details

found 0 vulnerabilities

Quasar • Initialized Git repository 🚀
Quasar • 初始化 Git 儲存庫
To get started:開始吧:
啟動

1
2
cd quasar-project
npm run dev

Documentation can be found at: https://quasar.dev
文件可在以下網址找到:https://quasar.dev
Quasar is relying on donations to evolve. We’d be very grateful if you can
read our manifest on “Why donations are important”: https://quasar.dev/why-donate
Donation campaign: https://donate.quasar.dev
Any amount is very welcome.
If invoices are required, please first contact Razvan Stoenescu.

Please give us a star on Github if you appreciate our work:
https://github.com/quasarframework/quasar
Quasar 依靠捐贈來發展。如果你能
閱讀我們的「為什麼捐款很重要」宣言:https://quasar.dev/why-donate
捐款活動:https://donate.quasar.dev
無論數量多少,都非常歡迎。
如果需要發票,請先聯絡 Razvan Stoenescu。

如果您欣賞我們的工作,請在 Github 上給我們一顆星:
https://github.com/quasarframework/quasar
Enjoy! - Quasar Team

App • Using quasar.config.ts in “ts” format

» Reported at………… 2025/3/21 下午10:40:30
» App dir……………. /Users/larahuang/Desktop/作品集/天昕科技/quasar-project
» App URL……………. http://localhost:9000/
http://192.168.25.60:9000/
» Dev mode…………… spa
» Pkg quasar…………. v2.18.1
» Pkg @quasar/app-vite… v2.1.4
» Browser target……… es2022|firefox115|chrome115|safari14

App • Opening default browser at http://localhost:9000/

Angular 利用HttpClient取回後端資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {HttpClient, HttpHeaders } from '@angular/common/http';

@Component({
selector: 'app-test-list',
imports: [NgFor],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})

export class TestListComponent implements OnInit {
getLists() {
const apiUrl ="https://xxxx"
this.http.get<addListsType[]>(`${apiUrl}/api/toDoLists`, this.httpOptions).pipe(catchError(this.handleError))
.subscribe((lists )=> {
consle.log(lists)
this.todoDataList = lists;
})
}
}

上範例Ts

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
import {HttpClient, HttpHeaders } from '@angular/common/http';

@Component({
selector: 'app-test-list',
imports: [NgFor],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})

export class TestListComponent implements OnInit {
constructor( private http: HttpClient ) { }
private readonly httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
private handleError(error: any) {
console.error('API 錯誤:', error);
return throwError(() => new Error(error.message || '錯誤已消失'));
}
todoDataList: addListsType[] = [];
tableHeader: string[] = ['項目','名稱','建立日期','更新日期','操作'];
getLists() {
const apiUrl ="https://xxxx"
this.http.get<addListsType[]>(`${apiUrl}/api/toDoLists`, this.httpOptions).pipe(catchError(this.handleError))
.subscribe((lists )=> {
let arrLists:any[] = [];
lists.map( (item: any,index:number) => {
let query: any = {
_id: item._id,
title: item.title,
Editing: false, // 編輯
Status: false, //選取狀態
CanEdit: true, //可以編輯
buildDate: item.buildDate,
updataDate: item.buildDate,
}
// 新增的欄位push 到陣列
arrLists.push(query);

//排序
arrLists.sort((a, b) => {
return b.buildDate - a.buildDate
})
})
//渲染的陣列等於新增後後的陣列
this.todoDataList = arrLists;
})
}
}

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<table class="table table-striped">
<thead >
<tr >
<th *ngFor="let item of tableHeader| slice: 0 : 5 ;let index = index " scope="col">{{item}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of todoDataList;let index = index">
<td>{{index +1}}</td>
<td>{{item.title | uppercase }} </td>
<td>{{item.buildDate | date:'yyyy-MM-dd HH:mm' }} </td>
<td>{{item.updataDate | TaiwanYear }} </td>

<td>
<button type="button" class="btn btn-secondary">Cancel</button>
<button type="button" class="btn btn-success ml-2">Submit</button></td>
</tr>
</tbody>
</table>


參考

Angular HttpClient 封裝

HttpClient 是 Angular 對 XMLHttpRequest 和 Fetch 的封裝
理解 HttpClient

provideHttpClient 源碼在 provider.ts

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
export function provideHttpClient(
...features: HttpFeature<HttpFeatureKind>[]
): EnvironmentProviders {
if (ngDevMode) {
const featureKinds = new Set(features.map((f) => f.ɵkind));
if (
featureKinds.has(HttpFeatureKind.NoXsrfProtection) &&
featureKinds.has(HttpFeatureKind.CustomXsrfConfiguration)
) {
throw new Error(
ngDevMode
? `Configuration error: found both withXsrfConfiguration() and withNoXsrfProtection() in the same call to provideHttpClient(), which is a contradiction.`
: '',
);
}
}

const providers: Provider[] = [
// 1. HttpClient 是最常用的Service Providers
HttpClient,
HttpXhrBackend,
HttpInterceptorHandler,
{provide: HttpHandler, useExisting: HttpInterceptorHandler},
// 2. default Angular 內部使用XMLHttpRequest發送請求
{provide: HttpBackend, useExisting: HttpXhrBackend},
{
provide: HTTP_INTERCEPTOR_FNS,
useValue: xsrfInterceptorFn,
multi: true,
},
{provide: XSRF_ENABLED, useValue: true},
{provide: HttpXsrfTokenExtractor, useClass: HttpXsrfCookieExtractor},
];

for (const feature of features) {
providers.push(...feature.ɵproviders);
}
// 製作環境提供者
return makeEnvironmentProviders(providers);
}
  • HttpClient 是最常用的Service Providers
  • Angular 默認內部使用XMLHttpRequest發送請求

全局載入

  1. 添加 HttpClient 相關的 providers
    app.config.ts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
    import { provideRouter } from '@angular/router';
    + import {provideHttpClient} from '@angular/common/http';
    import { routes } from './app.routes';
    import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

    export const appConfig: ApplicationConfig = {
    providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    // 添加 HttpClient 相關的 providers
    + provideHttpClient(),
    provideRouter(routes),
    provideClientHydration(withEventReplay())
    ]
    };
    Fetch 不支持上傳進度,這個是 Fetch 目前最大的缺陷,這也是為什么 Angular 任然以 XMLHttpRequest 作为默認。

src/app 新增 api 資料夾 => 新增 web-api.service.ts

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
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class WebApiService {
private readonly http = inject(HttpClient);

private readonly httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

get(url: string): Observable<any> {
return this.http.get(url, this.httpOptions)
.pipe(catchError(this.handleError));
}

post(url: string, model: any): Observable<any> {
return this.http.post(url, model, this.httpOptions)
.pipe(catchError(this.handleError));
}

put(url: string, model: any): Observable<any> {
return this.http.put(url, model, this.httpOptions)
.pipe(catchError(this.handleError));
}

delete(url: string): Observable<any> {
return this.http.delete(url, this.httpOptions)
.pipe(catchError(this.handleError));
}

private handleError(error: any) {
console.error('API 錯誤:', error);
return throwError(() => new Error(error.message || '錯誤已消失'));
}
}

src/app 新增 api 資料夾=> 新增http-provider.service.ts

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
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { WebApiService } from './web-api.service';
import { environment } from '../../environments/environment';
import dayjs from 'dayjs'
@Injectable({ providedIn: 'root' })
export class HttpProviderService {
private readonly apiUrl = environment.apiUrl;
private readonly webApiService = inject(WebApiService);

getAllLists(): Observable<any> {
return this.webApiService.get(`${this.apiUrl}/api/toDoLists`);
}
// 編輯
deleteTodoById(_id: string): Observable<any> {
return this.webApiService.delete(`${this.apiUrl}/api/toDo/${encodeURIComponent(_id)}`);
}

getTodoById(id: string): Observable<any> {
return this.webApiService.get(`${this.apiUrl}/api/toDo/${encodeURIComponent(id)}`);
}
// 新增
addList(newTask: string): Observable<any> {
let query:any = {
title: newTask,
buildDate: Date.now(),
updataDate: Date.now(),
}
return this.webApiService.post(`${this.apiUrl}/api/toDo`,query);
}

// 編輯
updateTodo(item: any): Observable<any> {
console.log('updateTodo', item)
let query:any = {
title: item.title,
buildDate: dayjs(item.buildDate).valueOf(),//改為時間搓
updataDate: Date.now(),
}
return this.webApiService.put(`${this.apiUrl}/api/toDo/${encodeURIComponent(item._id)}`, query);
}
}

參考資料

Vite Angular

Vue 安裝
Node v20.17.0
Angular CLI 19.2.5.

1
npm create vite@latest

到專案內

1
cd 專案

Port 4200 is already in use.
Would you like to use a different port? (Y/n)
連接埠 4200 已被使用。
您想使用其他連接埠嗎? (是/否)
Would you like to use a different port? Yes
Component HMR has been enabled.
If you encounter application reload issues, you can manually reload the page to bypass HMR and/or disable this feature with the --no-hmr command line option.
Please consider reporting any issues you encounter here: https://github.com/angular/angular-cli/issues

Browser bundles
Initial chunk files | Names | Raw size
polyfills.js | polyfills | 90.20 kB |
main.js | main | 23.25 kB |
styles.css | styles | 96 bytes |

                 | Initial total    | 113.55 kB

Server bundles
Initial chunk files | Names | Raw size
polyfills.server.mjs | polyfills.server | 570.97 kB |
chunk-VIHJC4M4.mjs | - | 23.69 kB |
server.mjs | server | 1.39 kB |
main.server.mjs | main.server | 800 bytes |

Application bundle generation complete. [2.570 seconds]

Watch mode enabled. Watching for file changes…
NOTE: Raw file sizes do not reflect development server per-request transformations.

參考資料

Angular 19安裝

Angular 分頁套件

Angular 分頁套件

ngx-pagination

1
npm install ngx-pagination --save

ChangeDetectionStrategy 變更偵測策略

Typescript
元件內的變更偵測策略,打造高效能元件
使用 OnPush

https://ithelp.ithome.com.tw/articles/10209130
ChangeDetectionStrategy 變更偵測策略
  • 引入ngx-pagination
  • 裝飾器引入ChangeDetectionStrategy.OnPush
  • 父對子傳值 數據接收
  • 定義分頁:每頁幾筆,當前頁面,全部筆數,選擇變換每頁顯示筆數
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import {ChangeDetectionStrategy,Component,Input,OnInit,Output,EventEmitter,} from '@angular/core';
import { addListsType } from '../../Types/toDo'
import { NgFor, NgIf, NgClass, CommonModule } from '@angular/common';
+ import {NgxPaginationModule} from 'ngx-pagination';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-to-do-list',
imports: [NgxPaginationModule,NgFor,NgIf,NgClass,FormsModule,CommonModule ],
templateUrl: './to-do-list.component.html',
styleUrl: './to-do-list.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
})

+ export class BasicExampleComponent implements OnInit {
+ @Input() sendTodoList?:addListsType[] |any ;
@Input() sendItemId?: string;
@Input() sendErrorItemMessage?: string;
@Input() sendEditItem?: Observable<addListsType[]>;

@Output() newItem = new EventEmitter<addListsType>();
@Output() newId = new EventEmitter<string>();
@Output() newEvent = new EventEmitter<any>();
@Output() newUpdateItem = new EventEmitter<addListsType>();

openEditItem(item:any) {
this.newItem.emit(item);
}
editItem(item: any) {
this.newUpdateItem.emit(item);
}
openDeleteAlert(id:string) {
this.newId.emit(id)
}
deleteItem(id:string) {
this.newId.emit(id)
}
blurEvent(event:any) {
this.newEvent.emit(event)
}
// 每頁幾筆
+ currentPage: number | undefined = 1;
// 當前頁面
+ itemsPerPage: number = 10;
// 全部筆數
+ totalItems: number | any

+ pagePerOptions = [
+ { value: 5, label: '5筆' },
+ { value: 10, label: '10筆' },
+ { value: 15, label: '15筆' },
+ { value: 20, label: '20筆' },
+ { value: 30, label: '30筆' },
+ { value: 40, label: '40筆' },
+ { value: 50, label: '50筆' },
+ ]
// 選擇變換每頁顯示筆數
+ changeItemsPerPage(item: any) {
+ this.itemsPerPage = item.value;
+ }
// 設備變換時改變每頁顯示
deviceWidth:number =screen.width;
deviceDisplay() {
if (typeof Screen !== undefined) {
switch (this.deviceWidth) {
case 1920:
this.itemsPerPage=20;
break;
case 1560:
this.itemsPerPage=15;
break;
case 1440:
this.itemsPerPage=10;
break;
}
}
}
ngOnInit(): void {
this.deviceDisplay()
}
}

html

  • *ngFor="let item of sendTodoList | paginate: { itemsPerPage: itemsPerPage, currentPage: currentPage} "
  • (pageChange)="currentPage = $event"
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<div class="lists">
<ul *ngIf="sendTodoList">
<li
*ngFor="let item of sendTodoList |
paginate: { itemsPerPage: itemsPerPage, currentPage: currentPage} ">
<div class="subject">
<label [for]="item._id">
<span class="edit_area"
[ngClass]="sendItemId === item._id ? 'noEditing' : ''"
>
{{item.title}}
</span>
<div class="edit_input_area"
[ngClass]="sendItemId === item._id? 'editing' : ''"
>
<input
type="text"
(blur)="blurEvent($event)"
[(ngModel)]="item.title"
[value]="item.title"
>
<div class="errorMessage">
{{sendErrorItemMessage}}</div>
</div>
</label>
</div>
<div class="buildDate">
{{item.buildDate}}
</div>
<div class="updataDate">
{{item.updataDate}}
</div>
<div class="btns_group">
<button
(click)="openEditItem(item)"
[ngClass]="sendItemId === item._id ? 'noEditing' : ''"
class="btns edit" >
<span>編輯</span><i class="fa-solid fa-pencil"></i>
</button>
<button (click)="editItem(item)"
[ngClass]="sendItemId === item._id? 'editing' : ''"
class="btns add"><span>儲存</span><i class="fa-solid fa-plus"></i></button>
<button
(click)="openDeleteAlert(item._id)"
class="btns delete"><span>刪除</span> <i class="fa-solid fa-xmark"></i></button>
</div>
</li>
</ul>
</div>
<!--分頁細節-->
<div class="pagination_area">
<pagination-controls class="my-pagination" (pageChange)="currentPage = $event"></pagination-controls>
<div class="pagination_detail">
<div class="total_length">
共{{sendTodoList?.length}}筆,</div>
<div class="currentPage"> 當前第{{currentPage}}頁,</div>
<div class="itemsPerPage">每頁{{itemsPerPage}}筆</div>
</div>
<div class="btn-group">
<a class="btn" data-toggle="dropdown" aria-expanded="false">
筆數
</a>
<div class="dropdown-menu">
<a class="dropdown-item"
*ngFor="let item of pagePerOptions"
(click)="changeItemsPerPage(item)"
>{{item.label}}
</a>
</div>
</div>
</div>
scss樣式
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
.btns{
display: flex;
height: 30px;
width:70px;
justify-content: center;
align-items: center;
}
.add{
background-color: green;
border: 1px solid green;
color:white;
&:hover,&:focus{
background-color: #0f760f;
border: 1px solid #0f760f;
}
}
.edit{
background-color: #ff9800;
border: 1px solid #ff9800;
color:white;
&:hover,&:focus{
background-color: #bc750c;
border: 1px solid #bc750c;
}
}
.delete{
background-color: red;
border: 1px solid red;
color:white;
&:hover,&:focus{
background-color: rgb(182, 9, 9);
border: 1px solid rgb(182, 9, 9);
}
}
nav{
display: block;
unicode-bidi: isolate;
}
.lists{
display: flex;
justify-content: center;
width: 60%;
max-width: 60%;
margin: auto;
ul{
list-style: none;
padding-inline-start: 0px;
width: 100%;
max-width: 100%;
min-height: 800px;
li{
border-bottom:1px solid #ddd;
display: flex;
list-style: none;
.subject{
align-items: center;
display: flex;
padding: 2px auto;
padding-left: 15px;
width: 50%;
label{
display: flex;
justify-content: flex-start;
width: 95%;
margin-bottom: 0;
span.edit_area{
display:flex;
&.noEditing{
display: none;
}
}
.edit_input_area{
align-items: center;
display: none;
flex-direction: column;
justify-content: flex-start;
width: 100%;
input{
display: flex;
border: 1px dashed #263238;
}
&.editing{
display: flex;
}
}
}

}
.buildDate,.updataDate{
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
font-size: 14px;
padding-left: 6px;
}
.btns_group{
display: flex;
}
a,button{
margin: 5px;
cursor: pointer;
white-space: nowrap;
&:disabled {
cursor: not-allowed;
}
i{
margin-left: 5px;
}
&.edit{
display: flex;
&.noEditing{
display: none;
}
}
&.add{
display: none;
&.editing{
display: flex;
}
&:disabled{
cursor: not-allowed;
}
}
}
}
}
}


input{
display: block;
width: 100%;
height: calc(1.2em + .75rem + 2px);
padding: 0.075rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
margin-left: -.75rem;
&:hover{
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
}
}
.errorMessage{
color:red;
font-size: 12px;
align-self: flex-start;
margin-left: -15px;
}



.editing{
display: flex;
}
.noEditing{
display: none;
}
@media screen and (width<=1560px) {
.lists{
ul{
min-height: 480px;
}
}
}
@media screen and (width<=1440px) {
.lists{
ul{
min-height: 480px;
}
}
}
@media screen and (width<=820px){
.add_form,
.lists{
width: 80%;
max-width: 80%;
}
}
//
@media screen and (width<=768px){
.lists {
width: 85%;
max-width: 85%;
ul {
margin-left: 10px;
width: 90%;
li {
margin-left: -15px;
.subject {
width: 45%;
}
}
}
}
}
@media screen and (width<=667px){
.lists {
width: 85%;
max-width: 85%;
ul {
margin-left: 10px;
width: 90%;
li {
margin-left: -15px;
flex-wrap: wrap;
div{
padding: 10px;
}
.subject {
min-width: 100%;
background-color: #f4f3f3;
label{
width: 100%;
height: auto;
.edit_input_area{
&.editing{
flex-direction: none;
padding: 0px;
height: 55px;
}
.errorMessage{
padding: 0;
}
}
}
}
.buildDate,.updataDate{
min-width: 45%;
margin: auto;
}
.btns_group{
margin: auto;
}
}
}
}
}
@media screen and (width<=600px){
.lists ul li a {
span{
display: none;
}
i{
font-size: 1.4rem;
}
&.delete{
i{
font-size: 1.7rem;
}
}
}
}
//iphone5/SE
@media screen and (width<=568px){
.lists {
ul {
li {
.subject {
width: 100%;
max-width: 100%;
white-space:nowrap;

}
.buildDate{
flex-direction: column;
}
}
}
}
}
//iphone 14 pro
@media screen and (width<=430px){
.lists ul li {
margin-left: -15px;
.subject label .edit_area{
font-size: 1.1rem;
}
}
}
@media screen and (width<=414px){
.lists ul li {
margin-left: -15px;
.subject label .edit_area{
font-size: 1.1rem;
}
a{
&.btns{
width: 50px;
}
i{
font-size: 1.4rem;
}
&.delete{
i{
font-size: 1.7rem;
}
}
}
}
}
.pagination_area{
display: flex;
justify-content: center;
.my-pagination ::ng-deep {
align-items: center;
.ngx-pagination {
display: flex;
justify-content: center;
li{
&:first-child{
margin-right: 22px;
}
&.current {
background: transparent;
border-radius: 50%;
width: 32px;
height: 32px;
border: 2px solid #ffd207;
color:#333;
}
&.pagination-previous{
position: relative;
width: 25px;
&.disabled{
color: #ddd;
position: relative;
&:before{
font-size: 42px;
position: absolute;
top: -17px;
left: 0px;
}
span{
display: none;
}
}
a{
position: relative;
font-size: 0px;
&:before{
color: #333;
font-size: 35px;
position: absolute;
top: -11px;
left: -3px;
}
&:hover{
background: transparent;
border:0px solid transparent;
}
}
&:hover{
border-style: transparent;
}
}
a{
&:hover{
font-weight: bolder;
background: transparent;
border-color: #ffd207;
}
}
&.pagination-next {
width: 45px;
&:hover{
border-color: transparent;
}
&::after{
color: #333;
font-size: 35px;
}
&.disabled{
color: #ddd;
position: relative;
&:before{
font-size: 35px;

}
&::after{
color: #ddd;
font-size: 35px;
position: absolute;
top: -11.5px;
left: 17px
}
span{
display: none;
}
}
a{
color: transparent;
font-size: 0px;
position: relative;
&:hover{
font-weight: bolder;
background: transparent;
border-bottom: 1px solid red;
}
&:after{
margin-left: 0rem;
color: #333;
font-size: 35px;
position: absolute;
top: -14px;
left: 20px;
}
&:hover{
background: transparent;
border:0px solid transparent;
}
}
}
}
}
}
.pagination_detail{
display: flex;
margin-top: 4px;
}
.btn-group{
margin-left: 10px;
.btn{
height: 32px;
border: 2px solid #ffd207;
padding: 2px 5px;
&:hover,&:focus{
background-color: #ffd207;
color:#333;
}
}
.dropdown-menu{
min-width: 5rem;
}
}
}

@media screen and (width<=480px){
.pagination_area {
margin-left: -15px;
.pagination_detail{
font-size: 1.1rem;
.currentPage{
display: none;
}
}
}
}
@media screen and (width<=428px){
.pagination_area {
.pagination_detail{
display: none;
}
}
}
@media screen and (width<=360px){
.pagination_area .my-pagination .ngx-pagination {
li{
&:first-child {
margin-right: 5px;
}
&.pagination-previous a{
&:before {
left: 5px;
}
}
&.current{
height: 28px;
width: 28px;
}
}
}

}

例如

新增刪除編輯列表分頁功能

參考資料

Angular 實戰Navbar

功能與邏輯

獲取Nav數據 => 初始化宣染active 在Home => 點擊Nav Item 獲取頁面名稱===pageTitle 新增active樣式
父對子傳值
子元件引入Input
  • @InPut() => import {Input} 引入Input
  • @InPut() 傳值參數:屬性;
  • implements OnInit =>import {OnInit}引入Input OnInit
  • 使用到a標籤時引入RouterLink
  • 使用到動態Class時引入NgClass
從父元件引入子元件
  • 從父元件引入子元件 如果是父對子傳值 [傳值參數]= 父元件內的傳值欄位參數
1
[傳值參數]= 父元件內的傳值欄位參數
子對父傳值
子元件引入Output,EventEmitter
  • Output () 傳值參數名稱 =new EventEmitter<屬性>() ;
  • 函式傳值 this.傳值參數名稱.emit(item)
  • Html =>(傳值參數名稱)="函式($event)"

以下完整程式碼

父元件

從app.component.ts 引入子元件

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
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+import { NavbarComponent } from './components/navbar/navbar.component';
import { CreateNavComponent} from './components/create-nav/create-nav.component';
import { environment } from '../environment/environment'
import { navListType,navListActiveType } from './Types/nav';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
+ imports: [RouterOutlet,NavbarComponent,CreateNavComponent],
styleUrl: './app.component.scss'
})

export class AppComponent {
title = 'vite_project';
navLists: navListType[] = [
{ title: 'Home', path: '/' },
{ title: 'Users', path: 'users' },
{title:'Login',path:'login'}
]
isActive: navListActiveType["isActive"] = true;
pageTitle: navListActiveType["pageTitle"] = "Home";
actionList(title: string) {
this.pageTitle = title;
}
bgStyle:string ='red'
ngOnInit() {
console.log(environment.production ? 'Production' : '開發中')
}
}

app.component.html檔案內引入子元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<app-navbar
[sendNavLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
(newPageTitle)="actionList($event)"
></app-navbar>
<app-create-nav
[sendBgStyle]="bgStyle"
[sendCreateLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
(newPageTitle)="actionList($event)"
></app-create-nav>

<main class="main">
</main>
<router-outlet />

子元件

navbar.component.ts

  • @InPut() => import {Input} 引入Input
  • @InPut() 傳值參數:屬性;
  • implements OnInit =>import {OnInit}引入Input OnInit
  • 使用到a標籤時引入RouterLink
  • 使用到動態Class時引入NgClass
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
import { NgFor,NgClass } from '@angular/common';
+ import { Component,OnInit ,Input,Output, EventEmitter} from '@angular/core';
import { RouterLink } from '@angular/router'; // a標籤 [RouterLink]
import { navListType } from '../../Types/nav'; // Type屬性
//@Component=>裝飾器
@Component({
selector: 'app-navbar',
+ imports: [RouterLink,NgFor,NgClass],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss'
})
// 實現初始化 implements OnInit
+ export class NavbarComponent implements OnInit {
//父對子傳值
+ @Input() sendNavLists!: navListType[];
+ @Input() sendPageTitle!: string;
+ @Input() sendIsActive!: boolean;
// 子對父傳值
+ @Output() newPageTitle = new EventEmitter<string>();
//點擊頁面按鈕函式
sendActionList(path: string) {
this.newPageTitle.emit(path);
}
ngOnInit(): void {}
}
navbar.component.html
  • *ngFor="let item of sendNavLists ; let i = index"
  • [ngClass]="sendPageTitle === item.title ? 'activeNav' : ''"
  • [routerLink]="item.path"
  • (click)="sendActionList(item.title)"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<nav class="nav">
<ul>
<li
+ *ngFor="let item of sendNavLists ; let i = index"
+ [ngClass]="sendPageTitle === item.title ? 'activeNav' : ''"
>
<a
+ [routerLink]="item.path"

+ (click)="sendActionList(item.title)"
+ class="cursor-pointer"
>{{item.title}}</a>
</li>

</ul>
</nav>

scss

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
ul{
display: flex;
li{
list-style: none;
display: flex;
margin: auto 10px;
}
}
.nav {
background-color: #ffd207;
box-shadow: 2px 2px 2px 1px #0003;
display: flex;
height: 70px;
justify-content: center;
list-style-type: none;
margin: 0px auto auto auto;
padding-inline-start: 0px;
max-width: 100%;
width: 100%;

li {
display: flex;
list-style-type: none;
margin: auto 10px;
&.activeNav {
border-bottom: 1px solid #333;
}
a {
cursor: pointer;
color: #333;
font-size: 1.4rem;
text-decoration: none;
}
}
}

.activeNav {
border-bottom: 1px solid #333;
}

github

Angular 實戰Todo

手把手使用Angular,目前版本號是Angular Cli 19.2.0,
Node 安裝 v20.17.0,npm 10.8.2

基礎使用:

  • Router路由
  • 環境變數
  • 使用Angular Material來幫我們快速建立各種元件
  • Angular Binding繫結:
    • 內嵌繫結:使用雙括號將變數宣染
    • 屬性繫結[ ]繫結, [(ngModel)]表單綁定 :
      • styleex.[ngStyle]="{'font-size': 26 + counter + 'px'}";[ngStyle]="{'font-size': 26 + counter + 'px'}";
      • class ex.[ngClass]="{'Hidden':isShowLists}" => [ngClass]="{‘樣式’:動態屬性布林值}"; => 引入 import { NgFor, NgIf,NgClass} from '@angular/common';
      • [id]="item._id"
      • 圖片雙向綁定[src]="item.imgPath"
      • 表單雙向綁定 [(ngModel)]="item.Status" => 引入 import { FormsModule } from '@angular/forms'
    • 事件繫結
      • 點擊事件 (click)="sendEditItem(item)"
      • checkbox表單事件 (change)="isAllSelected()"
      • 鍵盤事件(keyup.enter)="sendAddItem(newTask)"
      • 鍵盤事件(blur)
      • 表單事件(input)
    • 渲染列表 *ngFor => import { NgFor} from '@angular/common';
    • 渲染判斷 *NgIf => import { NgIf} from '@angular/common';
  • 父對子傳遞
  • 子對父傳值

Router路由

src/app/app.routes.ts

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
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { UserComponent } from './user/user.component';
import { LoginComponent } from './login/login.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

export const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "users", component: UsersComponent},
{ path: "user/:userId", component: UserComponent },
{ path: "login", component: LoginComponent},
{ path: '**', component: PageNotFoundComponent },
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
//https://angular.tw/start/start-routing
export class AppRoutingModule { }

環境變數

環境變數

使用Angular Material來幫我們快速建立各種元件

產生一個新的組件

1
ng g c 頁面或是功能

父元件

### app.component.ts 父對子傳值
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
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { environment } from '../environments/environment';
import { NavbarComponent } from "./components/navbar/navbar.component";
import { navListType } from "./Type/nav";
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
FormsModule,
NavbarComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = "我的網站";
navLists: navListType[] = [
{ title: 'Home' ,path:'/'},
{ title: 'Users', path: 'user' },
{title:'Login',path:'login'}
]
pageTitle: string = 'Home';
isActive: boolean = false;
actionList(path: string) {
this.pageTitle = path;
}
ngOnInit(): void {
console.log(environment.production ? 'Production' : '開發中')
}
}

子對父傳遞

父對子傳遞

app.component.html

router-outlet 如同Vue 的router-view
1
2
3
4
5
6
7
8
9
10
<app-navbar
[sendNavLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
(newPageTitle)="actionList($event)"
></app-navbar>

<main class="main">
<router-outlet />
</main>

父元件

父對子傳值

home.component.ts

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { environment } from '../../environments/environment';
import { addListsType } from '../Type/toDo';
import { ToDoHeaderComponent } from '../components/to-do-header/to-do-header.component';
import { ToDOComponent } from '../components/to-do/to-do.component';
@Component({
selector: 'app-home',
imports: [FormsModule,ToDoHeaderComponent,ToDOComponent],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})
export class HomeComponent {
newTask: string = '';
header:any= {
'Content-Type': 'application/json',
}
//新增
addItem(newTask: string) {
if (newTask.length>0) {
const vm = this;
const api = `${environment.apiUrl}/api/toDo`;
let query: any = {
title: newTask,
buildDate: Date.now(),
updataDate: Date.now(),
}
console.log('query', query)
fetch(api, { method: 'POST', headers: vm.header, body: JSON.stringify(query) })
.then((res) => res.json())
.then((data) => {
//unshift(data) 新增到第一個
//push(data)新㽪到最後一個
vm.addLists.unshift(data)
})
} else {
alert('沒有填寫新增項目')
}
};

//獲取
addLists: addListsType[] = [];
getAddLists() {
const vm = this;
const api = `${environment.apiUrl}/api/toDoLists`;
fetch(api, { method: 'GET' })
.then((res) => {return res.json();})
.then((res) => {
res.map(function (item: addListsType) {
let query: any = {
_id: item._id,
title: item.title,
Editing: false, // 編輯
Status: false, //選取狀態
CanEdit:true, //可以編輯
buildDate: item.buildDate,
updataDate: item.updataDate,
}
return vm.addLists.push(query);
/*Sort()排列*/
vm.addLists.sort(function (a, b) {
return b.buildDate - a.buildDate
})
return vm.addLists;
})
})
.catch((error) => {
console.log(`Error: ${error}`);
})
}
//刪除
deleteItem(item: any) {
const vm = this;
const api = `${environment.apiUrl}/api/toDo/${item._id}`
fetch(api, {method: 'DELETE',headers: this.header})
.then((res) => res.json())
.then((data) => {
const index = vm.addLists.findIndex(task => task._id === item._id);
vm.addLists.splice(index, 1)
});
}
//編輯
editItem(item: any) {
console.log('editItem', item)
const vm = this;
const api = `${environment.apiUrl}/api/toDo/${item._id}`;
let query: any = {
title: item.title,
buildDate: item.buildDate,
updataDate: Date.now(),
}
fetch(api, {
method: 'PUT',
headers: vm.header,
body: JSON.stringify(query)
})
.then((res) => res.json())
.then((data) => {
if (!item.Editing) {
alert("編輯成功")
}
});
}

//全選
selectAll() {

}
ngOnInit(): void {
this.getAddLists();
}
}

home.component.html

1
2
3
4
5
6
7
8
9
10
<app-to-do-header
(sendNewTask)="addItem($event)"
>
</app-to-do-header>
<app-to-do
[sendAddLists]="addLists"
(sendChangeDeleteItem)="deleteItem($event)"
(sendChangeEditItem)="editItem($event)"
></app-to-do>

子元件

這是NavBar

components/to-do-header

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
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgFor } from '@angular/common';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { navListType } from '../../Type/nav';
@Component({
selector: 'app-navbar',
imports: [
NgFor,
CommonModule,
RouterLink
],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss',

})

export class NavbarComponent implements OnInit{
@Input() sendNavLists!: navListType[];
@Input() sendPageTitle!: string;
@Input() sendIsActive!: boolean;

@Output() newPageTitle = new EventEmitter<string>();
//點擊頁面按鈕函式
sendActionList(path: string) {
this.newPageTitle.emit(path);
}
ngOnInit(): void {

}
}
1
2
3
4
5
6
7
8
 <ul  class="nav">
<li *ngFor="let item of sendNavLists ; let i = index"
[ngClass]="sendPageTitle === item.title ? 'activeNav' : ''">
<a [routerLink]="item.path"
(click)="sendActionList(item.title)"
class="cursor-pointer">{{item.title}}</a>
</li>
</ul>

這是新增表單區域

子對父傳遞

父對子傳遞

components/to-do-header ### to-do-header.component.ts
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
import { Component,Input, OnInit,Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-to-do-header',
standalone: true,
imports: [ FormsModule ],
templateUrl: './to-do-header.component.html',
styleUrl: './to-do-header.component.scss'
})
export class ToDoHeaderComponent implements OnInit {
@Output() sendNewTask = new EventEmitter<string>();
newTask: string = "";
header:any= {
'Content-Type': 'application/json',
}
//新增
sendAddItem(newTask: string) {
const message = '沒有填寫新增項目';
newTask.length>0?this.sendNewTask.emit(newTask):this.messageAlert(message )
}
messageAlert(message:string) {
alert(message)
}
ngOnInit(): void {
}
}
### to-do-header.component.html
1
2
3
4
5
6
7
8
9
10
<div class="todo_lists_header">
<input type="text"
[(ngModel)]="newTask"
[value]="newTask"
[placeholder]="newTask"
(keyup.enter)="sendAddItem(newTask)" >
<a class="button add_button"
(click)="sendAddItem(newTask)">新增</a>
</div>

## 這是ToDo列表 ### to-do.component.ts
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
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms'
import { NgFor, NgIf,NgClass} from '@angular/common';
import { addListsType } from '../../Type/toDo';
@Component({
selector: 'app-to-do',
imports: [NgFor,NgIf,NgClass,FormsModule],
templateUrl: './to-do.component.html',
styleUrl: './to-do.component.scss'
})

export class ToDOComponent implements OnInit{
@Input() sendAddLists!: addListsType[];

@Output() sendChangeDeleteItem = new EventEmitter<any>();
@Output() sendChangeEditItem = new EventEmitter<Object>()

sendDeleteItem(item: any) {
this.sendChangeDeleteItem.emit(item)
}
sendEditItem(item: any) {
item.Editing = !item.Editing;
this.sendChangeEditItem.emit(item)
};

masterSelected:boolean= false;
constructor(){}

sendAllChecked() {
for (var i = 0; i < this.sendAddLists.length; i++) {
this.sendAddLists[i].Status = this.masterSelected;
}
}
isAllSelected() {
this.masterSelected = this.sendAddLists.every(function(item:any) {
return item.Status == true;
})
}
isShowLists: boolean = false;
sendDeleteAll() {
this.isShowLists = true;
// this.sendAddLists =[]
}
ngOnInit(): void { }
}

to-do.component.html

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
<div class="todo_lists">
<div class="subject"><b>待辦事項</b></div>
<div class="flex-start">
<label class="all" for="all">全部
<input
type="checkbox"
name="all"
id="all"
[(ngModel)]="masterSelected"
(change)="sendAllChecked()"
>
<span class="round-mark"></span>
</label>
<i *ngIf="masterSelected"
(click)="sendDeleteAll()"
class="fa-solid fa-trash"></i>
</div>
<ul class="todo-list"
[ngClass]="{'Hidden':isShowLists}"
>
<!--*ngFor 陣列-->
<li
*ngFor="let item of sendAddLists ; let i = index">
<label class="checkBox"
[for]="item._id">
<span class="edit_area"
[ngClass]="{'noEditing':item.Editing}"
>
{{item.title}}
</span>
<!-- 選取狀態-->
<input
type="checkbox"
(change)="isAllSelected()"
[id]="item._id"
[(ngModel)]="item.Status"
>
<span class="round-mark"></span>
<div class="edit_area">
<input
type="text"
[(ngModel)]="item.title"
[ngClass]="{'editing':item.Editing}"
[value]="item.title">
</div>
</label>
<!--操作-->
<i (click)="sendEditItem(item)" class="fa-solid fa-pencil"></i>
<i (click)="sendDeleteItem(item)" class="fa-solid fa-xmark" ></i>
</li>
</ul>
</div>

Attribute, class, and style bindings and Two-way binding
參考資料

Vite Vue Line第三方登入

Line
網站上設定使用Line第三方登入的功能,需先到Line進行資料的設定與驗證,並取得「Channel ID」與「Channel secret」後,交由工程師做最後的串接才可使用於網頁中。 申請Line第三方登入之前,必須有一個Line的個人or官方帳號,若沒有則需先申請一組

導向授權頁面的query(Page Mode)
以下為導向各家授權頁面的Base URL

Google:https://accounts.google.com/o/oauth2/v2/auth

Facebook:https://www.facebook.com/v7.0/dialog/oauth

Line:https://access.line.me/oauth2/v2.1/authorize

Vite Vue 第三方登入

Vue Google 第三方登入

功能說明:第三方登入,按下Google登入按鈕,選擇Google登入的帳號
使用第三方Google登入 需要有一組 Google OAuth 使用的 Client ID, 你可以到 Google Console 新增一個「OAuth 2.0 用戶端 ID」
這邊提醒,在建立 OAuth Client ID 時,已授權的 JavaScript 來源,記得填寫上您的正式環境或開發環境的 Domain,且建議使用 HTTPS。
### 申請 Google 認證模式OAuth Google Console 畫面
1. 建立一個新專案
2. 啟用API服務
3. 建立憑證 => OAuth用戶端ID
4. OAuth用戶端ID
選取網頁應用程式
填寫名稱
已授權重先導向 =>
https://developers.google.com/oauthplayground

取得Google client_id 用戶端編號

安裝 vue3-google-login

官網參考資料
安裝指令

1
npm install vue3-google-login

到 進入點 main.ts引入vue3-google-login

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
+ import vue3GoogleLogin from 'vue3-google-login'
+ const clientId= `${import.meta.env.VITE_GOOGLE_CLIENT_ID}`;
import App from './App.vue'
+ const GoogleLoginOptions = { clientId: clientId}

const app = createApp(App);
+app.use(vue3GoogleLogin,GoogleLoginOptions);
app.mount('#app')

頁面的使用

  • 引入 vue3-google-login 的 googleAuthCodeLogin, googleSdkLoaded
  • clientId 就是Google OAuth的 Client ID
  • ** googleSdkLoaded 內的 google.accounts.oauth2.initCodeClient 是 client_id: clientId.value,
  • scope: 'email profile openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
功能說明:第三方登入,按下Google登入按鈕,選擇Google登入的帳號
1. 按下登入按鈕
2. 選擇Google登入的帳號
3. 如果如圖就是已經成功串接到 Google OAuth,按下繼續
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
<template>
<i @click="login" class="fa-brands fa-google-plus" :title="Google 登入"></i>
<i class="fa-brands fa-facebook"></i>
</template>

<route lang="yaml">
meta:
layout: frontLayout
</route>

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
mport { googleAuthCodeLogin, googleSdkLoaded } from "vue3-google-login"

const clientId = ref<string>(`${import.meta.env.VITE_GOOGLE_CLIENT_ID}`)

const login = () => {
googleAuthCodeLogin().then((res) => {
console.log("Handle the res", res)
})
}
const loginS = () => {
googleSdkLoaded((google) => {
google.accounts.oauth2.initCodeClient({
client_id: clientId.value,
scope: 'email profile openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
callback: (res) => {
console.log("Handle the res", res)
}
}).requestCode()
})
}
參考資料