Compare commits

..

2 Commits

Author SHA1 Message Date
300e6ee4db Major update of Vue Website Template 2025-07-13 23:14:01 +07:00
010be2c2bc Major update of Vue Website Template 2025-07-13 23:13:56 +07:00
71 changed files with 5788 additions and 377 deletions

494
package-lock.json generated
View File

@ -11,7 +11,10 @@
"@fortawesome/fontawesome-free": "^6.7.2",
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.10.0",
"d3": "^7.9.0",
"daisyui": "^5.0.43",
"demo": "github:blackrosezy/flipbook-ext",
"flipbook-vue": "^1.0.0-beta.4",
"lodash": "^4.17.21",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.11",
@ -1284,6 +1287,15 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
@ -1305,6 +1317,407 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/d3": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
"license": "ISC",
"dependencies": {
"d3-array": "3",
"d3-axis": "3",
"d3-brush": "3",
"d3-chord": "3",
"d3-color": "3",
"d3-contour": "4",
"d3-delaunay": "6",
"d3-dispatch": "3",
"d3-drag": "3",
"d3-dsv": "3",
"d3-ease": "3",
"d3-fetch": "3",
"d3-force": "3",
"d3-format": "3",
"d3-geo": "3",
"d3-hierarchy": "3",
"d3-interpolate": "3",
"d3-path": "3",
"d3-polygon": "3",
"d3-quadtree": "3",
"d3-random": "3",
"d3-scale": "4",
"d3-scale-chromatic": "3",
"d3-selection": "3",
"d3-shape": "3",
"d3-time": "3",
"d3-time-format": "4",
"d3-timer": "3",
"d3-transition": "3",
"d3-zoom": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-axis": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-brush": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "3",
"d3-transition": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-chord": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
"license": "ISC",
"dependencies": {
"d3-path": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-contour": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
"license": "ISC",
"dependencies": {
"d3-array": "^3.2.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
"license": "ISC",
"dependencies": {
"delaunator": "5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dsv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
"license": "ISC",
"dependencies": {
"commander": "7",
"iconv-lite": "0.6",
"rw": "1"
},
"bin": {
"csv2json": "bin/dsv2json.js",
"csv2tsv": "bin/dsv2dsv.js",
"dsv2dsv": "bin/dsv2dsv.js",
"dsv2json": "bin/dsv2json.js",
"json2csv": "bin/json2dsv.js",
"json2dsv": "bin/json2dsv.js",
"json2tsv": "bin/json2dsv.js",
"tsv2csv": "bin/dsv2dsv.js",
"tsv2json": "bin/dsv2json.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-fetch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
"license": "ISC",
"dependencies": {
"d3-dsv": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-force": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-geo": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2.5.0 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-hierarchy": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-polygon": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-quadtree": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-random": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-interpolate": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/daisyui": {
"version": "5.0.43",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.43.tgz",
@ -1314,6 +1727,15 @@
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/delaunator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
"license": "ISC",
"dependencies": {
"robust-predicates": "^3.0.2"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1323,6 +1745,15 @@
"node": ">=0.4.0"
}
},
"node_modules/demo": {
"version": "0.1.0",
"resolved": "git+ssh://git@github.com/blackrosezy/flipbook-ext.git#77bb4dfb0a868b04955578c3e66417fd561524eb",
"dependencies": {
"rematrix": "^0.7.2",
"vue": "^3.3.4",
"vue-material-design-icons": "^5.3.1"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -1476,6 +1907,18 @@
}
}
},
"node_modules/flipbook-vue": {
"version": "1.0.0-beta.4",
"resolved": "https://registry.npmjs.org/flipbook-vue/-/flipbook-vue-1.0.0-beta.4.tgz",
"integrity": "sha512-4FycLNWkxEpHUCk08s1g9q1G/r1S/r3GI7LCalwMtr4G7Y7IDgKbA9AF7AjDPFudny+Rqndw74SHKVnksSebCA==",
"license": "MIT",
"dependencies": {
"rematrix": "^0.7.2"
},
"peerDependencies": {
"vue": ">=2.6.10"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -1635,6 +2078,27 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
@ -2068,12 +2532,24 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/rematrix": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/rematrix/-/rematrix-0.7.2.tgz",
"integrity": "sha512-NYLmE17dX15eUPhngLMTKlRYwATxEhcRdn9LKtk/iOXHxDKglCZnD2MrMLKwLlB5FX8u9kF/9yMhlQJpeDcuPw==",
"license": "MIT"
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
"license": "Unlicense"
},
"node_modules/rollup": {
"version": "4.44.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz",
@ -2113,6 +2589,18 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"license": "BSD-3-Clause"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -2286,6 +2774,12 @@
}
}
},
"node_modules/vue-material-design-icons": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.3.1.tgz",
"integrity": "sha512-6UNEyhlTzlCeT8ZeX5WbpUGFTTPSbOoTQeoASTv7X4Ylh0pe8vltj+36VMK56KM0gG8EQVoMK/Qw/6evalg8lA==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",

View File

@ -12,7 +12,9 @@
"@fortawesome/fontawesome-free": "^6.7.2",
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.10.0",
"d3": "^7.9.0",
"daisyui": "^5.0.43",
"flipbook-vue": "^1.0.0-beta.4",
"lodash": "^4.17.21",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.11",

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
public/mock-pdfs/daily1.pdf Normal file

Binary file not shown.

BIN
public/mock-pdfs/doc1.pdf Normal file

Binary file not shown.

Binary file not shown.

BIN
public/mock-pdfs/mag1.pdf Normal file

Binary file not shown.

BIN
public/mock-pdfs/mag2.pdf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -5,174 +5,580 @@
:style="`background-image: url('${appStore.imageBaseUrl}${appStore.headerData.header_background.url}'); background-size: cover; background-position: center;`"
class="relative z-10"
>
<nav class="navbar p-2 md:p-4 shadow-md" :style="{ backgroundColor: appStore.headerData.mainColor }">
<nav ref="navbarRef" class="navbar p-2 md:p-4 shadow-md" :style="{ backgroundColor: appStore.headerData.mainColor }">
<div class="container mx-auto flex items-center justify-between w-full">
<div class="navbar-start w-auto flex-shrink-0">
<router-link to="/home" class="flex flex-col items-start leading-tight">
<span class="text-white text-lg font-bold">{{ appStore.checkLang.isTh ? 'ฮิวแมนเทคไทยแลนด์' : 'HumanTech' }}</span>
<span class="text-white text-xs opacity-80">{{ appStore.checkLang.isTh ? 'HumanTech' : 'ฮิวแมนเทคไทยแลนด์' }}</span>
<span class="text-white text-lg font-bold">
{{ appStore.checkLang.isTh ? 'ฮิวแมนเทคไทยแลนด์' : 'HumanTech' }}
</span>
<span class="text-white text-xs opacity-80">
{{ appStore.checkLang.isTh ? 'HumanTech' : 'ฮิวแมนเทคไทยแลนด์' }}
</span>
</router-link>
</div>
<div class="navbar-end hidden md:flex flex-1 items-center justify-end space-x-2 lg:space-x-4">
<div class="navbar-center hidden md:flex flex-1 items-center justify-end space-x-2 lg:space-x-4">
<ul class="menu menu-horizontal px-1 flex-nowrap">
<li v-for="menu in orderedMainMenus" :key="menu.id">
<template v-if="menu.sub_menus && menu.sub_menus.length > 0">
<details :open="activeMenuId === menu.id" @toggle="handleDetailsToggle(menu.id, $event)">
<summary class="text-white font-semibold text-base hover:bg-gray-700 hover:text-white">
<li v-for="menu in orderedMainMenus" :key="menu.id" class="relative">
<template v-if="menu.sub_menu_groups && menu.sub_menu_groups.length || (menu.sub_menus && menu.sub_menus.length)">
<button
class="text-white font-semibold text-base hover:bg-gray-700 hover:text-white py-2 px-3 rounded-md flex items-center gap-1"
:ref="el => menuButtonRefs.set(menu.id, el)"
@click="toggleDropdown(menu.id, menu.sub_menu_groups, menu.sub_menus)"
>
<span>{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-180': activeMenuId === menu.id }"
>
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.25 4.25a.75.75 0 01-1.06 0L5.23 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</button>
</template>
<template v-else>
<router-link
:to="menu.link"
class="text-white font-semibold text-base hover:bg-gray-700 hover:text-white py-2 px-3 rounded-md"
:ref="el => menuButtonRefs.set(menu.id, el)"
@click="activeMenuId = null"
>
{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}
</router-link>
</template>
</li>
</ul>
<div class="flex items-center space-x-0 ml-4">
<button class="btn btn-sm rounded-l-lg rounded-r-none normal-case text-base font-bold px-4 py-2"
:class="isLangButtonActive('th') ? 'active-lang-button' : 'inactive-lang-button'"
:disabled="isLangButtonActive('th')"
@click="switchLanguageToThai">TH</button>
<button class="btn btn-sm rounded-r-lg rounded-l-none normal-case text-base font-bold px-4 py-2"
:class="isLangButtonActive('en') ? 'active-lang-button' : 'inactive-lang-button'"
:disabled="isLangButtonActive('en')"
@click="switchLanguageToEnglish">EN</button>
</div>
</div>
<div class="navbar-end md:hidden">
<div class="dropdown dropdown-end relative">
<div role="button" class="btn btn-ghost text-white hover:bg-gray-700" ref="mobileMenuToggleButtonRef" @click="isMobileMenuOpen = !isMobileMenuOpen">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</div>
<ul
v-if="isMobileMenuOpen"
class="absolute top-full right-0 z-[9999] mt-2 w-screen max-w-xs bg-base-100 shadow rounded-box p-4 text-black max-h-[80vh] overflow-y-auto"
ref="mobileMenuDropdownRef"
>
<li v-for="menu in orderedMainMenus" :key="menu.id" class="mb-2">
<template v-if="menu.sub_menu_groups && menu.sub_menu_groups.length > 0">
<details :open="activeMobileMenuId === menu.id" @toggle="handleMobileDetailsToggle(menu.id, $event)">
<summary class="cursor-pointer py-2 font-semibold text-base hover:bg-gray-200 rounded px-2 flex items-center justify-between">
<span>{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-180': activeMobileMenuId === menu.id }"
>
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.25 4.25a.75.75 0 01-1.06 0L5.23 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</summary>
<ul class="p-2 bg-base-100 rounded-box w-52 text-black">
<li v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenus(menu.sub_menus) : orderedSubMenusEng(menu.sub_menus))" :key="subMenu.id">
<router-link :to="subMenu.link" class="hover:bg-gray-200 hover:text-black">
<ul class="pl-4 py-2 bg-base-100">
<template v-for="group in orderedSubMenuGroups(menu.sub_menu_groups)" :key="group.group_title_th">
<li v-if="group.group_title_th || group.group_title_en" class="font-bold text-sm text-gray-700 mt-2 mb-1">
{{ appStore.checkLang.isTh ? group.group_title_th : group.group_title_en }}
</li>
<li v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenuItems(group.items) : orderedSubMenuItemsEng(group.items))"
:key="subMenu.id">
<router-link :to="subMenu.link" class="block py-1 px-2 rounded mobile-submenu-item" @click="closeMobileMenu">
{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}
</router-link>
</li>
</template>
</ul>
</details>
</template>
<template v-else-if="menu.sub_menus && menu.sub_menus.length > 0">
<details :open="activeMobileMenuId === menu.id" @toggle="handleMobileDetailsToggle(menu.id, $event)">
<summary class="cursor-pointer py-2 font-semibold text-base hover:bg-gray-200 rounded px-2 flex items-center justify-between">
<span>{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-180': activeMobileMenuId === menu.id }"
>
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.25 4.25a.75.75 0 01-1.06 0L5.23 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</summary>
<ul class="pl-4 py-2 bg-base-100">
<li v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenuItems(menu.sub_menus) : orderedSubMenuItemsEng(menu.sub_menus))"
:key="subMenu.id">
<router-link :to="subMenu.link" class="block py-1 px-2 rounded mobile-submenu-item" @click="closeMobileMenu">
{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}
</router-link>
</li>
</ul>
</details>
</template>
<template v-else>
<router-link
:to="menu.link"
class="text-white font-semibold text-base hover:bg-gray-700 hover:text-white"
@click="activeMenuId = null" >
class="block py-2 px-2 font-semibold text-base hover:bg-gray-200 rounded"
@click="closeMobileMenu">
{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}
</router-link>
</template>
</li>
</ul>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost normal-case text-base text-white hover:bg-gray-700 hover:text-white">
{{ appStore.checkLang.isTh ? 'ภาษาไทย' : 'English' }}
<li class="mt-4">
<div class="flex items-center justify-around w-full p-2">
<button
class="btn btn-sm rounded-l-lg rounded-r-none normal-case text-base font-bold px-4 py-2 w-1/2"
:class="isLangButtonActive('th') ? 'active-lang-button' : 'inactive-lang-button-mobile'"
:disabled="isLangButtonActive('th')"
@click="switchLanguageToThai"
>
TH
</button>
<button
class="btn btn-sm rounded-r-lg rounded-l-none normal-case text-base font-bold px-4 py-2 w-1/2"
:class="isLangButtonActive('en') ? 'active-lang-button' : 'inactive-lang-button-mobile'"
:disabled="isLangButtonActive('en')"
@click="switchLanguageToEnglish"
>
EN
</button>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-30 text-black">
<li><a @click="switchLanguageToThai" class="hover:bg-gray-200 hover:text-black">ภาษาไทย</a></li>
<li><a @click="switchLanguageToEnglish" class="hover:bg-gray-200 hover:text-black">English</a></li>
</li>
</ul>
</div>
</div>
<div class="navbar-end md:hidden">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden text-white hover:bg-gray-700 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 text-black">
<li v-for="menu in orderedMainMenus" :key="menu.id">
<template v-if="menu.sub_menus && menu.sub_menus.length > 0">
<details :open="activeMobileMenuId === menu.id" @toggle="handleMobileDetailsToggle(menu.id, $event)">
<summary class="hover:bg-gray-200 hover:text-black">{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}</summary>
<ul class="p-2 bg-base-200">
<li v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenus(menu.sub_menus) : orderedSubMenusEng(menu.sub_menus))" :key="subMenu.id">
<router-link :to="subMenu.link" class="hover:bg-gray-200 hover:text-black">{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}</router-link>
</li>
</ul>
</details>
</template>
<template v-else>
<router-link :to="menu.link" class="hover:bg-gray-200 hover:text-black">
{{ appStore.checkLang.isTh ? menu.main_menu : menu.main_menu_en }}
</router-link>
</template>
</li>
<li>
<details :open="activeMobileMenuId === 'language'" @toggle="handleMobileDetailsToggle('language', $event)">
<summary class="hover:bg-gray-200 hover:text-black">{{ appStore.checkLang.isTh ? 'ภาษา' : 'Language' }}</summary>
<ul class="p-2 bg-base-200">
<li><a @click="switchLanguageToThai" class="hover:bg-gray-200 hover:text-black">ภาษาไทย</a></li>
<li><a @click="switchLanguageToEnglish" class="hover:bg-gray-200 hover:text-black">English</a></li>
</ul>
</details>
</li>
</ul>
</div>
</div>
</div>
</nav>
<div
v-if="activeMenuId !== null"
class="absolute bg-white shadow-lg rounded p-4 z-50 desktop-dropdown"
:style="dropdownStyles"
ref="desktopDropdownRef"
>
<template v-if="activeDropdownData.sub_menu_groups && activeDropdownData.sub_menu_groups.length">
<div class="multi-column-dropdown-content">
<div
v-for="(group, groupIndex) in orderedSubMenuGroups(activeDropdownData.sub_menu_groups)"
:key="groupIndex"
class="flex flex-col"
>
<h3 v-if="group.group_title_th || group.group_title_en" class="font-bold text-lg mb-2 text-gray-700 whitespace-nowrap">
{{ appStore.checkLang.isTh ? group.group_title_th : group.group_title_en }}
</h3>
<ul class="space-y-1">
<li
v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenuItems(group.items) : orderedSubMenuItemsEng(group.items))"
:key="subMenu.id"
>
<router-link :to="subMenu.link" class="desktop-submenu-item block py-1 px-0 text-sm whitespace-nowrap" @click="closeDropdown">
{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}
</router-link>
</li>
</ul>
</div>
</div>
</template>
<template v-else-if="activeDropdownData.sub_menus && activeDropdownData.sub_menus.length">
<ul class="single-column-dropdown-content">
<li
v-for="subMenu in (appStore.checkLang.isTh ? orderedSubMenuItems(activeDropdownData.sub_menus) : orderedSubMenuItemsEng(activeDropdownData.sub_menus))"
:key="subMenu.id"
>
<router-link :to="subMenu.link" class="desktop-submenu-item block py-1 px-0 text-sm whitespace-nowrap" @click="closeDropdown">
{{ appStore.checkLang.isTh ? subMenu.title_th : subMenu.title_en }}
</router-link>
</li>
</ul>
</template>
</div>
</header>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, nextTick, watch } from 'vue';
import { useAppStore } from '@/stores/app';
import { RouterLink } from 'vue-router';
import _ from 'lodash';
const appStore = useAppStore();
// --- State to manage open menus ---
const activeMenuId = ref(null); // For desktop dropdowns
const activeMobileMenuId = ref(null); // For mobile dropdowns
// State for active desktop menu dropdown
const activeMenuId = ref(null);
const activeDropdownData = ref({}); // Stores the data (sub_menu_groups/sub_menus) for the active dropdown
const dropdownStyles = ref({}); // Stores the dynamic CSS styles for the active dropdown
// --- Handlers for desktop details toggles ---
const handleDetailsToggle = (menuId, event) => {
if (event.target.open) {
// If this details is being opened, set it as the active one
activeMenuId.value = menuId;
} else if (activeMenuId.value === menuId) {
// If this details is being closed, clear the active one
activeMenuId.value = null;
}
};
// Refs for DOM elements
const navbarRef = ref(null); // Reference to the <nav> element
const menuButtonRefs = new Map(); // Map to store references to each menu button (summary/router-link)
const desktopDropdownRef = ref(null); // Reference to the floating desktop dropdown container
// --- Handlers for mobile details toggles ---
const handleMobileDetailsToggle = (menuId, event) => {
if (event.target.open) {
activeMobileMenuId.value = menuId;
} else if (activeMobileMenuId.value === menuId) {
activeMobileMenuId.value = null;
}
};
// State for active mobile menu dropdown (uses DaisyUI details behavior)
const activeMobileMenuId = ref(null);
// New state for controlling mobile menu visibility
const isMobileMenuOpen = ref(false); // <--- NEW STATE
// --- Existing logic for menu ordering and language switching ---
// New refs for mobile menu elements
const mobileMenuDropdownRef = ref(null); // Reference to the <ul> element that acts as the mobile dropdown container
const mobileMenuToggleButtonRef = ref(null); // Reference to the button that toggles the mobile menu (hamburger icon)
// --- Computed Properties for Menu Ordering ---
const orderedMainMenus = computed(() => {
if (!appStore.allMainMenus) return [];
const activeMenus = appStore.allMainMenus.filter(menu =>
appStore.checkLang.isTh ? menu.active : menu.active_en
);
const homeMenu = activeMenus.find(menu => menu.id === 1);
const otherMenus = activeMenus.filter(menu => menu.id !== 1);
return homeMenu ? [homeMenu, ..._.orderBy(otherMenus, 'order')] : _.orderBy(activeMenus, 'order');
const menus = appStore.allMainMenus || [];
const active = menus.filter(m => appStore.checkLang.isTh ? m.active : m.active_en);
const home = active.find(m => m.id === 1); // Assuming ID 1 is always Home
const others = active.filter(m => m.id !== 1);
return home ? [home, ..._.orderBy(others, 'order')] : _.orderBy(active, 'order');
});
const orderedSubMenus = (subMenus) => {
if (!subMenus) return [];
const activeItems = subMenus.filter(f => f.active === true);
return _.orderBy(activeItems, 'order');
const orderedSubMenuGroups = groups => _.orderBy(groups || [], 'order');
const orderedSubMenuItems = items => _.orderBy((items || []).filter(f => f.active), 'order');
const orderedSubMenuItemsEng = items => _.orderBy((items || []).filter(f => f.active_en), 'order');
// --- Desktop Dropdown Logic ---
const toggleDropdown = async (menuId, subMenuGroups, subMenus) => {
// If clicking the same menu, close it
if (activeMenuId.value === menuId) {
activeMenuId.value = null;
activeDropdownData.value = {};
return;
}
activeMenuId.value = menuId;
activeDropdownData.value = {
sub_menu_groups: subMenuGroups,
sub_menus: subMenus
};
const orderedSubMenusEng = (subMenus) => {
if (!subMenus) return [];
const activeItems = subMenus.filter(f => f.active_en === true);
return _.orderBy(activeItems, 'order');
// Wait for DOM to update with the new active dropdown
await nextTick();
const button = menuButtonRefs.get(menuId); // Get the specific button ref
if (button && navbarRef.value && desktopDropdownRef.value) {
const buttonRect = button.getBoundingClientRect();
const navbarRect = navbarRef.value.getBoundingClientRect();
const dropdownRect = desktopDropdownRef.value.getBoundingClientRect();
const dropdownTop = navbarRect.bottom + window.scrollY; // Position right below the navbar
let dropdownLeft = buttonRect.left + window.scrollX; // Default align to button left
let transform = 'translateX(0)';
// Adjust for overflow on right side
const spaceToRight = window.innerWidth - (buttonRect.left + dropdownRect.width);
if (spaceToRight < 20) { // If less than 20px space to right, align right
dropdownLeft = buttonRect.right + window.scrollX - dropdownRect.width;
}
// Adjust for overflow on left side
if (dropdownLeft < 10) { // If less than 10px from left edge
dropdownLeft = 10 + window.scrollX;
}
dropdownStyles.value = {
top: `${dropdownTop}px`,
left: `${dropdownLeft}px`,
minWidth: `${buttonRect.width}px`,
transform: transform,
maxWidth: `calc(100vw - 40px)`,
};
}
};
const closeDropdown = () => {
activeMenuId.value = null;
activeDropdownData.value = {};
};
// --- Mobile Dropdown Logic (uses DaisyUI details behavior) ---
const handleMobileDetailsToggle = (menuId, event) => {
if (event.target.open) {
// Close other open details elements in the same mobile menu list
document.querySelectorAll('ul[ref="mobileMenuDropdownRef"] details[open]').forEach(detail => { // Corrected selector
if (detail !== event.target) detail.open = false;
});
activeMobileMenuId.value = menuId;
} else {
activeMobileId.value = null; // Changed from activeMobileMenuId.value = null; due to typo, ensure consistent
}
};
const closeMobileMenu = () => {
isMobileMenuOpen.value = false; // <--- NEW: Set state to close the main menu
activeMobileMenuId.value = null; // Close any open details within the mobile menu
// Also, close any open details by iterating directly
// This line is often not strictly necessary if `activeMobileMenuId` is properly managed,
// but acts as a fallback to ensure all <details> are closed.
document.querySelectorAll('ul[ref="mobileMenuDropdownRef"] details[open]').forEach(detail => {
detail.open = false;
});
};
// --- Language Switch Logic ---
const switchLanguageToThai = () => {
appStore.toggleLanguage();
// Close mobile language dropdown after selection
activeMobileMenuId.value = null;
// Only switch if not already Thai
if (!appStore.checkLang.isTh) {
appStore.toggleLanguage('th');
closeMobileMenu(); // Use the new function for mobile
closeDropdown();
}
};
const switchLanguageToEnglish = () => {
appStore.toggleLanguage();
// Close mobile language dropdown after selection
activeMobileMenuId.value = null;
// Only switch if not already English (i.e., if currently Thai)
if (appStore.checkLang.isTh) {
appStore.toggleLanguage('en');
closeMobileMenu(); // Use the new function for mobile
closeDropdown();
}
};
const isLangButtonActive = computed(() => lang => {
return (lang === 'th' && appStore.checkLang.isTh) || (lang === 'en' && !appStore.checkLang.isTh);
});
// --- Lifecycle Hooks and Watchers ---
onMounted(() => {
// Set initial language from localStorage
const savedLang = localStorage.getItem('lang');
if (savedLang === 'en') {
appStore.isTh = false;
} else {
appStore.isTh = true;
}
// Event listener for clicks outside dropdowns to close them
document.addEventListener('click', (event) => {
// Desktop dropdown close logic
if (activeMenuId.value !== null && desktopDropdownRef.value) {
const dropdownElement = desktopDropdownRef.value;
const clickedButton = menuButtonRefs.get(activeMenuId.value);
// Check if click is inside the desktop dropdown or the button that opened it
if (!dropdownElement.contains(event.target) && (!clickedButton || !clickedButton.contains(event.target))) {
closeDropdown();
}
}
// Mobile dropdown close logic
// Only check if mobile menu is currently open
if (isMobileMenuOpen.value && mobileMenuDropdownRef.value && mobileMenuToggleButtonRef.value) {
const mobileDropdownElement = mobileMenuDropdownRef.value;
const mobileToggleButton = mobileMenuToggleButtonRef.value;
// Check if click is outside the mobile dropdown container AND outside the toggle button
if (!mobileDropdownElement.contains(event.target) && !mobileToggleButton.contains(event.target)) {
closeMobileMenu();
}
}
});
// Watch for window resize to adjust desktop dropdown position
window.addEventListener('resize', () => {
if (activeMenuId.value !== null) {
// Re-calculate position for the currently open desktop dropdown
const currentMenu = orderedMainMenus.value.find(m => m.id === activeMenuId.value);
if (currentMenu) {
toggleDropdown(activeMenuId.value, currentMenu.sub_menu_groups, currentMenu.sub_menus);
}
}
// For mobile, if the screen size changes to desktop, close mobile menu
if (window.innerWidth >= 768 && isMobileMenuOpen.value) { // 768px is Tailwind's 'md' breakpoint
closeMobileMenu();
}
});
});
// Watch for activeMenuId change to trigger position adjustment
watch(activeMenuId, (newId, oldId) => {
if (newId !== null) {
// Re-trigger toggleDropdown to recalculate position if the menu data itself is needed
// The actual data is passed into toggleDropdown
}
});
// Watch for language change to adjust dropdown position if needed (due to text width changes)
watch(() => appStore.checkLang.isTh, () => {
if (activeMenuId.value !== null) {
const currentMenu = orderedMainMenus.value.find(m => m.id === activeMenuId.value);
if (currentMenu) {
toggleDropdown(activeMenuId.value, currentMenu.sub_menu_groups, currentMenu.sub_menus);
}
}
});
</script>
<style scoped>
/* No specific styles needed, using Tailwind/DaisyUI as before */
/*
GLOBAL STYLES FOR DESKTOP DROPDOWN CONTAINER (The one that floats)
*/
.desktop-dropdown {
background-color: white;
padding: 1.5rem; /* p-4 in example template was p-6 for multi-column originally */
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
z-index: 1000; /* Ensure it's above other content */
}
/*
STYLES FOR MULTI-COLUMN CONTENT INSIDE THE FLOATING DROPDOWN
*/
.multi-column-dropdown-content {
display: grid;
grid-auto-flow: column; /* Force content into columns first */
grid-auto-columns: max-content; /* Each column takes only as much width as its widest content */
gap: 2rem; /* Gap between columns */
overflow-x: auto; /* Allow horizontal scrolling if columns still overflow */
min-width: 200px; /* Base min width for the content area itself */
}
/*
STYLES FOR SINGLE-COLUMN CONTENT INSIDE THE FLOATING DROPDOWN
*/
.single-column-dropdown-content {
min-width: 200px; /* Base min width for the content area itself */
}
/* --- NEW: Desktop Submenu Item Hover Effect --- */
.desktop-submenu-item {
color: #333; /* Default text color for desktop submenu items */
padding: 0.25rem 0.5rem; /* py-1 px-2 */
display: block;
white-space: nowrap; /* Prevent menu item text from wrapping */
position: relative; /* Needed for pseudo-elements if used, or for absolute child */
transition: all 0.2s ease-in-out; /* Smooth transition for all properties */
border: 1px solid transparent; /* Start with transparent border */
border-radius: 0.25rem; /* Match rounded-md */
}
.desktop-submenu-item:hover {
background-color: #EBF8FF; /* blue-100 for light background */
color: #2563EB; /* blue-600 for text */
border-color: #60A5FA; /* blue-400 for border */
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
/* --- NEW: Mobile Submenu Item Hover Effect --- */
.mobile-submenu-item {
color: #333; /* Default text color for mobile submenu items */
padding: 0.25rem 0.5rem; /* block py-1 px-2 */
display: block;
border-radius: 0.25rem; /* Match rounded */
transition: all 0.2s ease-in-out; /* Smooth transition */
border: 1px solid transparent; /* Start with transparent border */
}
.mobile-submenu-item:hover {
background-color: #EBF8FF; /* blue-100 for light background */
color: #2563EB; /* blue-600 for text */
border-color: #60A5FA; /* blue-400 for border */
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
/* --- Language Button Styles --- */
.active-lang-button {
background-color: #e0e0e0;
color: #333;
border: 1px solid #ccc;
cursor: default; /* Change cursor to indicate it's not clickable */
}
.inactive-lang-button {
background-color: transparent;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
}
.inactive-lang-button:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* Styles for disabled state to reduce visual clutter */
.active-lang-button[disabled] {
opacity: 0.7; /* Make it slightly transparent */
cursor: default; /* Ensure cursor is default */
background-color: #e0e0e0; /* Maintain active background */
color: #333; /* Maintain active text color */
}
/* Specific border adjustments for the language buttons to match the image */
.active-lang-button:first-child, .inactive-lang-button:first-child { border-right: none; }
.active-lang-button:last-child, .inactive-lang-button:last-child { border-left: none; }
.flex.items-center.space-x-0 > .btn:not(:first-child) { margin-left: 0; }
.flex.items-center.space-x-0 > .btn:not(:last-child) { margin-right: 0; }
.inactive-lang-button-mobile {
background-color: transparent;
color: #333;
border: 1px solid #ccc;
cursor: pointer;
}
.inactive-lang-button-mobile:first-child { border-right: none; }
.inactive-lang-button-mobile:last-child { border-left: none; }
/* Styles for disabled state on mobile buttons */
.inactive-lang-button-mobile[disabled] {
opacity: 0.7;
cursor: default;
background-color: #e0e0e0; /* Match active-lang-button */
color: #333;
}
/*
MOBILE MENU STYLES (largely unchanged, using DaisyUI dropdown-end behavior)
*/
.navbar-end.md\:hidden .dropdown-content {
position: absolute;
top: 100%; /* Relative to the dropdown-end parent */
right: 0;
width: screen; /* Full width on small screens */
max-width: 280px; /* Constrain max width for mobile dropdown */
margin-top: 0.5rem; /* mt-2 */
z-index: 9999;
padding: 1rem; /* p-4 */
box-sizing: border-box;
max-height: 80vh;
overflow-y: auto;
}
.navbar-end.md\:hidden .dropdown-content .font-bold.text-sm.text-gray-700 { /* This targets the group title */
margin-top: 0.5rem; /* mt-2 */
margin-bottom: 0.25rem; /* mb-1 */
padding-left: 0.5rem; /* Consistent padding */
padding-right: 0.5rem;
}
.navbar-end.md\:hidden .dropdown-content ul.pl-4 { /* Targets the ul for sub-items in mobile */
padding-left: 1rem; /* Indent mobile sub-items */
}
</style>

View File

@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -10,14 +10,12 @@
>
{{ appStore.checkLang.isTh ? 'เรื่องราวดี ๆ ที่เราอยากบอกต่อ' : 'Our Inspiring Stories' }}
</h4>
</div>
<div class="p-6" :style="`background-color:${appStore.headers.bgColor || '#ffffff'}`">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 ">
<div v-if="filteredFeatureNews.length > 0" class="flex flex-col">
<NewsItem :item="filteredFeatureNews[0]" :isFeature="true" />
<div v-if="featureNewsDisplay.length > 0" class="flex flex-col">
<NewsItem :item="featureNewsDisplay[0]" :isFeature="true" />
</div>
<div v-else class="h-full">
<div class="bg-gray-200 flex items-center justify-center rounded-lg text-gray-500 h-full">
@ -27,9 +25,8 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="(item, key) in filteredNews"
:key="key"
class="flex flex-col"
v-for="item in generalNewsDisplay"
:key="item.id" class="flex flex-col"
>
<NewsItem :item="item" />
</div>
@ -37,7 +34,7 @@
<div class="md:col-span-2 flex justify-end mt-8">
<router-link
to="/news/HumanTechNews" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
to="/all-news" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
>
{{ appStore.checkLang.isTh ? more : more_en }}
</router-link>
@ -48,56 +45,56 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
// import Carousel, Slide
import NewsItem from './NewsItem.vue'; // Component
import { ref, onMounted, watch } from 'vue'; // watch
import NewsItem from './NewsItem.vue';
import { useAppStore } from '@/stores/app';
import { RouterLink } from 'vue-router'; // Import RouterLink
import { RouterLink } from 'vue-router';
const appStore = useAppStore();
const news = ref([]);
const featureNews = ref([]); // filter
const featureNewsRaw = ref([]); // (1 )
const generalNewsRaw = ref([]); // (4 )
const more = "อ่านข่าวต่อ";
const more_en = "Read More";
const filterNewsByDate = (items) => {
const today = new Date();
const currentDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // Normalize to start of day
//
const fetchAndSetNews = async () => {
// : limit 1, feature=true. Store isFeature, release_date, active/active_en
const featureResult = await appStore.find('news', '_limit=1&feature=true');
featureNewsRaw.value = Array.isArray(featureResult) ? featureResult : [];
return items.filter(item => {
if (!item.release_date) return false;
const [month, day, year] = item.release_date.split('/');
const releaseDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return releaseDate <= currentDate;
});
// : limit 4, feature=false. Store isFeature, release_date, active/active_en
const generalResult = await appStore.find('news', '_limit=4&feature=false');
generalNewsRaw.value = Array.isArray(generalResult) ? generalResult : [];
};
const filteredFeatureNews = computed(() => {
const langFiltered = appStore.checkLang.isTh
? featureNews.value.filter(item => item.active && item.feature)
: featureNews.value.filter(item => item.active_en && item.feature);
// **:** 1
return filterNewsByDate(langFiltered).slice(0, 1);
// Computed properties ( feature date )
// active/active_en UI
const featureNewsDisplay = ref([]);
const generalNewsDisplay = ref([]);
watch([featureNewsRaw, generalNewsRaw, () => appStore.checkLang.isTh], () => {
// feature date
featureNewsDisplay.value = appStore.checkLang.isTh
? featureNewsRaw.value.filter(item => item.active)
: featureNewsRaw.value.filter(item => item.active_en);
// feature date
generalNewsDisplay.value = appStore.checkLang.isTh
? generalNewsRaw.value.filter(item => item.active)
: generalNewsRaw.value.filter(item => item.active_en);
}, { immediate: true }); // component mounted
onMounted(() => {
fetchAndSetNews();
});
const filteredNews = computed(() => {
const langFiltered = appStore.checkLang.isTh
? news.value.filter(item => item.active && !item.feature)
: news.value.filter(item => item.active_en && !item.feature);
// Limit to 4 items as per original logic
return filterNewsByDate(langFiltered).slice(0, 4);
});
onMounted(async () => {
const allNews = await appStore.find('news', '');
news.value = allNews;
featureNews.value = allNews; // featureNews allNews
// Watch language changes to refetch news list
watch(() => appStore.checkLang.isTh, () => {
fetchAndSetNews();
});
</script>
<style scoped>
</style>

View File

@ -34,7 +34,7 @@
'line-clamp-3': !isFeature
}"
>
{{ truncateDetail(appStore.checkLang.isTh ? item.detail_th : item.detail_en || '', isFeature ? 200 : 120) }}
{{ truncateDetail(stripHtml(appStore.checkLang.isTh ? item.detail_th : item.detail_en || ''), isFeature ? 200 : 120) }}
</p>
<div class="text-xs text-gray-400 mt-auto">
<span v-if="item.release_date">{{ formatDate(item.release_date) }}</span>
@ -61,6 +61,13 @@ const props = defineProps({
},
});
// *** stripHtml ***
const stripHtml = (htmlText) => {
if (!htmlText) return '';
const doc = new DOMParser().parseFromString(htmlText, 'text/html');
return doc.body.textContent || "";
};
const truncateDetail = (text, maxLength) => {
if (!text) return '';
return text.length <= maxLength ? text : text.substring(0, maxLength) + '...';

View File

@ -0,0 +1,540 @@
// src/components/OrganizationTreeDiagram.vue
<template>
<div ref="diagramContainer" class="diagram-container"></div>
</template>
<script setup>
import { ref, onMounted, watch, onUnmounted } from 'vue';
import * as d3 from 'd3';
import { useAppStore } from '@/stores/app.js';
const appStore = useAppStore();
const props = defineProps({
treeData: {
type: Object, // Or Array if it's a list of roots
required: true
}
});
const diagramContainer = ref(null);
// Helper functions (unchanged)
const tailwindColors = {
'blue-700': '#1d4ed8',
'orange-500': '#f97316',
'red-500': '#ef4444',
'green-500': '#22c55e',
'purple-600': '#9333ea',
'blue-600': '#2563eb',
'orange-400': '#fb923c',
'red-400': '#f87171',
'red-300': '#ef4444',
'green-400': '#4ade80',
'purple-500': '#a855f7',
'gray-500': '#6b7280'
};
const tailwindTextColors = {
'text-white': '#ffffff',
'text-gray-800': '#1f2937'
};
const getNodeColorClass = (nodeId) => {
if (nodeId === 'alpha_root') return 'bg-blue-700 text-white';
if (nodeId === 'division_hq') return 'bg-orange-500 text-white';
if (nodeId === 'operation_division') return 'bg-red-500 text-white';
if (nodeId === 'support_division') return 'bg-green-500 text-white';
if (nodeId === 'special_projects') return 'bg-purple-600 text-white';
if (nodeId.startsWith('dept_A')) return 'bg-orange-400 text-white';
if (nodeId.startsWith('section_B')) return 'bg-red-400 text-white';
if (nodeId.startsWith('unit_B')) return 'bg-red-300 text-gray-800';
if (nodeId.startsWith('sub_support_')) return 'bg-green-400 text-white';
if (nodeId.startsWith('project_')) return 'bg-purple-500 text-white';
if (nodeId === 'independent_unit_A' || nodeId === 'advisory_board') return 'bg-gray-500 text-white';
return 'bg-blue-600 text-white';
};
const getNodeColor = (nodeId) => {
const className = getNodeColorClass(nodeId);
const bgColorClass = className.split(' ').find(cls => cls.startsWith('bg-'));
return tailwindColors[bgColorClass.replace('bg-', '')] || '#666';
};
const getTextColor = (nodeId) => {
const className = getNodeColorClass(nodeId);
const textColorClass = className.split(' ').find(cls => cls.startsWith('text-'));
return tailwindTextColors[textColorClass.replace('text-', '')] || '#fff';
};
// Define constants for node dimensions and spacing
const BASE_NODE_WIDTH = 180;
const BASE_NODE_HEIGHT = 80;
const HORIZONTAL_NODE_SPACING = 80;
const VERTICAL_DEPTH_SPACING = 150;
let svg, gChart;
let treemap;
let rootNode;
let i = 0; // Counter for assigning unique IDs
const setupD3Tree = (preserveState = false) => {
console.log("setupD3Tree called.");
if (!props.treeData || !diagramContainer.value) {
console.warn("treeData or diagramContainer is null. Skipping setup.");
return;
}
d3.select(diagramContainer.value).select('svg').remove();
const containerWidth = diagramContainer.value.offsetWidth;
const containerHeight = diagramContainer.value.offsetHeight;
svg = d3.select(diagramContainer.value)
.append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight);
gChart = svg.append("g");
treemap = d3.tree().nodeSize([BASE_NODE_WIDTH + HORIZONTAL_NODE_SPACING, VERTICAL_DEPTH_SPACING]);
const clonedTreeData = JSON.parse(JSON.stringify(props.treeData));
// Handle array of roots by creating a dummy root
const dataToHierarchy = Array.isArray(clonedTreeData)
? { id: 'dummy_root', name_th: '', name_en: '', children: clonedTreeData }
: clonedTreeData;
rootNode = d3.hierarchy(dataToHierarchy, d => d.children);
console.log("D3 Hierarchy - Root Node children:", rootNode.children ? rootNode.children.map(c => c.id) : "No children (single root)");
rootNode.descendants().forEach(d => {
console.log(`D3 Hierarchy - Node: ${d.id}, Depth: ${d.depth}, isDashed: ${d.data.isDashed}, reportsTo: ${d.data.reportsTo}`);
});
treemap(rootNode); // Compute initial layout for all nodes
rootNode.descendants().forEach(d => {
d.id = d.data.id || ++i;
d.data.rectWidth = BASE_NODE_WIDTH;
d.data.rectHeight = BASE_NODE_HEIGHT;
// Set initial position for animation
d.x0 = d.x; // Use computed x for initial animation start
d.y0 = d.y; // Use computed y for initial animation start
});
if (!preserveState) {
console.log("Performing initial collapse.");
rootNode.descendants().forEach(d => {
// Adjust depth for array-based root, if dummy_root is depth 0
const actualDepth = Array.isArray(props.treeData) ? d.depth - 1 : d.depth;
// Collapse nodes at depth 2 or deeper, unless it's a dashed line node
// Note: if you want to show root and direct children only, it's actualDepth >= 1
if (d.children && actualDepth >= 1 && !d.data.isDashed) { // Collapse beyond depth 0 (root)
d._children = d.children; // Store original children in _children
d.children = null; // Set children to null to hide them
}
});
} else {
console.log("Preserving state: Skipping initial collapse.");
}
update(rootNode); // Initial call to update function
};
// Helper to convert D3 transition to a Promise
function endAll(transition) {
return new Promise(resolve => {
if (transition.empty()) {
resolve();
} else {
let n = transition.size();
transition.on("end", () => {
n--;
if (n === 0) resolve();
});
}
});
}
const update = (source) => {
console.log("update called with source:", source.id);
// Re-compute the layout with the current state of rootNode (collapsed/expanded)
const treeData = treemap(rootNode);
// Get all descendants (nodes) in the current hierarchy layout
const allNodesInLayout = treeData.descendants();
const isArrayRoot = Array.isArray(props.treeData);
const visibleNodes = allNodesInLayout.filter(d => {
if (d.id === 'dummy_root') return true;
const isActualRoot = (!isArrayRoot && d.depth === 0) || (isArrayRoot && d.depth === 1 && d.parent.id === 'dummy_root');
return isActualRoot || (d.parent && d.parent.children && d.parent.children.includes(d));
});
// Explicitly remove dummy_root from visibleNodes for rendering if it's an array root
const nodesToRender = visibleNodes.filter(d => d.id !== 'dummy_root');
console.log("Nodes to Render:", nodesToRender.map(d => d.id));
let links = [];
// Find the actual main root node (e.g., alpha_root) among all nodes in layout
const mainRoot = allNodesInLayout.find(d => d.id === 'alpha_root');
treemap(rootNode).links().forEach(link => {
if (isArrayRoot && link.source.id === 'dummy_root') {
if (link.target.data && !link.target.data.isDashed && nodesToRender.some(n => n.id === link.target.id)) {
links.push(link);
}
} else {
const sourceVisible = nodesToRender.some(n => n.id === link.source.id);
const targetVisible = nodesToRender.some(n => n.id === link.target.id);
if (sourceVisible && targetVisible) {
links.push(link);
}
}
});
if (mainRoot) { // Only add dashed links if mainRoot exists
allNodesInLayout.forEach(node => {
if (node.data.isDashed && node.data.reportsTo === mainRoot.id) {
const isNodeVisible = nodesToRender.some(n => n.id === node.id);
const isSourceVisible = nodesToRender.some(n => n.id === mainRoot.id); // mainRoot should always be visible if it's the core.
if (isNodeVisible && isSourceVisible) {
links.push({
source: mainRoot,
target: node,
data: {
isDashed: true
}
});
}
}
});
}
console.log("Final links (including custom):", links.map(d => {
const sourceId = d.source ? d.source.id : 'N/A';
const targetId = d.target ? d.target.id : 'N/A';
const isDashed = (d.data && d.data.isDashed) || (d.target && d.target.data && d.target.data.isDashed);
return `${sourceId} -> ${targetId} (Dashed: ${isDashed})`;
}));
// 1. Update the Nodes
const node = gChart.selectAll('g.node')
.data(nodesToRender, d => d.id); // Use nodesToRender here
const nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${source.x0},${source.y0})`)
.on('click', click);
nodeEnter.append('rect')
.attr('width', 0)
.attr('height', 0)
.attr('rx', 8)
.attr('ry', 8)
.style('fill', d => getNodeColor(d.data.id))
.attr('stroke', d => d.data.isDashed ? '#888' : '#ccc') // Node's own dashed status
.attr('stroke-width', 1.5)
.attr('stroke-dasharray', d => d.data.isDashed ? ("5, 5") : "none"); // Node's own dashed status
nodeEnter.append('text')
.attr('dy', '.35em')
.attr('x', 0)
.attr('y', 0)
.attr('text-anchor', 'middle')
.style('fill', d => getTextColor(d.data.id))
.style('font-size', '14px')
.text(d => appStore.checkLang.isTh ? d.data.name_th : d.data.name_en)
.style('opacity', 0);
const nodeUpdate = nodeEnter.merge(node);
const transitionsToWaitFor = [];
const nodeTransition = nodeUpdate.transition()
.duration(750)
.attr('transform', d => `translate(${d.x},${d.y})`);
transitionsToWaitFor.push(endAll(nodeTransition));
const textTransition = nodeUpdate.select('text')
.transition()
.duration(750)
.style('opacity', 1)
.tween("text", function(d) {
const textElement = d3.select(this);
return function() {
const bbox = textElement.node().getBBox();
const paddingX = 20;
const paddingY = 15;
let rectWidth = bbox.width + paddingX * 2;
let rectHeight = bbox.height + paddingY * 2;
rectWidth = Math.max(rectWidth, BASE_NODE_WIDTH);
rectHeight = Math.max(rectHeight, BASE_NODE_HEIGHT);
d.data.rectWidth = rectWidth;
d.data.rectHeight = rectHeight;
d3.select(this.previousElementSibling)
.attr('width', rectWidth)
.attr('height', rectHeight)
.attr('x', -rectWidth / 2)
.attr('y', -rectHeight / 2);
};
});
transitionsToWaitFor.push(endAll(textTransition));
nodeUpdate.select('rect') // Ensure stroke/dasharray is updated for existing nodes too
.attr('stroke', d => d.data.isDashed ? '#888' : '#ccc')
.attr('stroke-dasharray', d => d.data.isDashed ? ("5, 5") : "none");
const nodeExit = node.exit().transition()
.duration(750)
.attr('transform', d => `translate(${source.x},${source.y})`)
.remove();
transitionsToWaitFor.push(endAll(nodeExit));
nodeExit.select('rect')
.attr('width', 0)
.attr('height', 0);
nodeExit.select('text')
.style('opacity', 0);
// 2. Update the Links
const link = gChart.selectAll('path.link')
// Use a robust key, checking for existence of source and target IDs
.data(links, d => `${d.source ? d.source.id : 'null'}-${d.target ? d.target.id : 'null'}`);
const linkEnter = link.enter().insert('path', 'g') // Insert before 'g' nodes
.attr('class', 'link')
.attr('fill', 'none')
.attr('stroke', '#ccc')
.attr('stroke-width', 1.5)
// !!! CRITICAL CHANGE HERE for linkEnter stroke-dasharray !!!
// Check if link.data.isDashed (for custom links) or link.target.data.isDashed (for standard links)
.attr('stroke-dasharray', d => (d.data && d.data.isDashed) || (d.target && d.target.data && d.target.data.isDashed) ? ("5, 5") : "none")
.attr('d', d => {
// Determine the animation source based on the actual source of the link
const animationSource = (isArrayRoot && d.source.id === 'dummy_root' && mainRoot) ? mainRoot : d.source;
// Ensure animationSource has the required properties
const startNodeForEnter = {
x: animationSource.x0,
y: animationSource.y0,
data: {
rectWidth: animationSource.data?.rectWidth || BASE_NODE_WIDTH,
rectHeight: animationSource.data?.rectHeight || BASE_NODE_HEIGHT
}
};
return diagonal(startNodeForEnter, startNodeForEnter);
});
const linkUpdate = linkEnter.merge(link);
const linkTransition = linkUpdate.transition()
.duration(750)
.attr('d', d => diagonal(d.source, d.target));
transitionsToWaitFor.push(endAll(linkTransition));
// !!! CRITICAL CHANGE HERE for linkUpdate stroke-dasharray (LINE 318) !!!
// Ensure stroke-dasharray is updated for existing links as well
linkUpdate.attr('stroke-dasharray', d => (d.data && d.data.isDashed) || (d.target && d.target.data && d.target.data.isDashed) ? ("5, 5") : "none");
const linkExit = link.exit().transition()
.duration(750)
.attr('d', d => {
// Determine the animation source based on the actual source of the link
const animationSource = (isArrayRoot && d.source.id === 'dummy_root' && mainRoot) ? mainRoot : d.source;
// Ensure animationSource has the required properties
const endNodeForExit = {
x: animationSource.x,
y: animationSource.y,
data: {
rectWidth: animationSource.data?.rectWidth || BASE_NODE_WIDTH,
rectHeight: animationSource.data?.rectHeight || BASE_NODE_HEIGHT
}
};
return diagonal(endNodeForExit, endNodeForExit);
})
.remove();
transitionsToWaitFor.push(endAll(linkExit));
// Stash the current positions (x, y) as old positions (x0, y0) for the next transition.
// Only stash for visible nodes.
visibleNodes.forEach(d => {
d.x0 = d.x;
d.y0 = d.y;
});
// *** IMPORTANT CHANGE: Wait for all transitions to complete before fitting to view ***
Promise.all(transitionsToWaitFor)
.then(() => {
console.log("All transitions ended. Calling fitToView.");
fitToView();
})
.catch(error => {
console.error("Error during D3 transitions:", error);
});
};
function click(event, d) {
console.log(`Click event on node: ${d.id} (${d.data.name_th || d.data.name_en})`);
// Handle clicking on the dummy root for array data (if applicable)
const isArrayRoot = Array.isArray(props.treeData);
if (isArrayRoot && d.id === 'dummy_root') {
// Optionally, prevent clicking the dummy root or handle it specifically
console.log("Clicked dummy root, no action.");
return;
}
if (d.children) {
d._children = d.children; // Store current children
d.children = null; // Hide children
}
else if (d._children) {
d.children = d._children; // Restore children
d._children = null; // Clear hidden children
}
else {
console.log(` Leaf node or no hidden children. No action taken.`);
}
update(d); // Trigger update with the clicked node as the source for animation
}
// DIAGONAL FUNCTION (unchanged)
function diagonal(s, d) {
// Ensure s.data and d.data exist before accessing rectWidth/Height
const sourceRectWidth = s.data?.rectWidth || BASE_NODE_WIDTH;
const sourceRectHeight = s.data?.rectHeight || BASE_NODE_HEIGHT;
const targetRectWidth = d.data?.rectWidth || BASE_NODE_WIDTH;
const targetRectHeight = d.data?.rectHeight || BASE_NODE_HEIGHT;
const startX = s.x;
const startY = s.y + sourceRectHeight / 2; // Link starts from bottom center of source node
const endX = d.x;
const endY = d.y - targetRectHeight / 2; // Link ends at top center of target node
const midY = startY + (endY - startY) / 2; // Midpoint for the vertical line segment
return `M ${startX},${startY}
V ${midY}
H ${endX}
V ${endY}`;
}
const fitToView = () => {
if (!gChart || gChart.empty()) {
console.warn("gChart not ready or empty for fitToView, retrying...");
setTimeout(fitToView, 500); // Short retry if gChart is not ready
return;
}
const treeBBox = gChart.node()?.getBBox();
if (!treeBBox || treeBBox.width === 0 || treeBBox.height === 0) {
console.warn("Tree BBox is zero or invalid, retrying fit...");
setTimeout(fitToView, 500); // Reduce delay for retry
return;
}
const containerWidth = diagramContainer.value.offsetWidth;
const containerHeight = diagramContainer.value.offsetHeight;
const treeWidth = treeBBox.width;
const treeHeight = treeBBox.height;
const buffer = 0.90; // You can adjust this value (e.g., 0.85, 0.8)
const scaleX = (containerWidth * buffer) / treeWidth;
const scaleY = (containerHeight * buffer) / treeHeight;
const scale = Math.min(scaleX, scaleY);
const translateX = (containerWidth / 2) - ((treeBBox.x + treeWidth / 2) * scale);
const translateY = (containerHeight / 2) - ((treeBBox.y + treeHeight / 2) * scale);
console.log("Calculated BBox:", { x: treeBBox.x, y: treeBBox.y, width: treeWidth, height: treeHeight });
console.log("Container Size:", { containerWidth, containerHeight });
console.log("Calculated Scale:", scale);
console.log("Calculated Translate:", { translateX, translateY });
gChart.transition()
.duration(750)
.attr("transform", `translate(${translateX},${translateY})scale(${scale})`);
};
onMounted(() => {
setupD3Tree();
window.addEventListener('resize', setupD3Tree); // Call setupD3Tree on resize to re-render
});
watch(() => props.treeData, (newVal, oldVal) => {
// Deep watch to ensure changes in nested properties trigger re-render
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
console.log("props.treeData content changed, re-setting up D3 Tree.");
setupD3Tree(false); // Do not preserve state on new data
} else {
console.log("props.treeData reference changed, but content is same. Ignoring watch for internal D3 state changes.");
}
}, { deep: true });
watch(() => appStore.checkLang.isTh, () => {
console.log("Language changed, re-setting up D3 Tree.");
setupD3Tree(false); // Re-render on language change
});
onUnmounted(() => {
window.removeEventListener('resize', setupD3Tree);
});
</script>
<style scoped>
.diagram-container {
width: 100%;
height: 90vh;
background-color: white;
overflow: hidden; /* Important for containing the SVG */
display: flex;
justify-content: center; /* Center SVG horizontally if it's smaller than container */
align-items: center; /* Center SVG vertically if it's smaller than container */
}
/* Ensure SVG fills the container */
.diagram-container svg {
display: block; /* Remove extra space below SVG */
}
.node rect {
cursor: pointer;
}
.node text {
font-family: 'Arial', sans-serif;
cursor: pointer;
user-select: none;
}
.link {
/* Stroke and fill are set dynamically in JS */
}
</style>

View File

@ -6,11 +6,13 @@ import { createPinia } from 'pinia';
import router from "./router/index.js";
import '@fortawesome/fontawesome-free/css/all.min.css'; // หรือ all.css ถ้าต้องการ debug
import Flipbook from 'flipbook-vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia); // ใช้ Pinia
app.use(router); // ใช้ router
app.component('Flipbook', Flipbook); // Register Flipbook globally
app.mount('#app');

View File

@ -1,4 +1,5 @@
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
// 1. Import Layouts
@ -12,7 +13,16 @@ import HomeView from "@/views/HomeView.vue"; // View สำหรั
import ContentView from "@/views/ContentView.vue"; // View สำหรับแสดงรายละเอียด เรื่องราวดี ๆ ที่อยากบอกต่อ
import NewsCategoryView from "@/views/NewsCategoryView.vue"; // View สำหรับแสดงรายการข่าวตามหมวดหมู่
import NotFoundView from '@/views/NotFoundView.vue'; // View สำหรับแสดงหน้า Not Found
import TabContentView from "@/views/TabContentView.vue"; // // View สำหรับแสดงรายละเอียดข่าวสารอับเดต
import TabContentView from "@/views/TabContentView.vue";
import AllNewsView from "@/views/AllNewsView.vue"; // // View สำหรับแสดงรายละเอียดข่าวสารอับเดต
// --- เพิ่ม import สำหรับ CommanderBioView ---
import CommanderBioView from "@/views/About/CommanderBioView.vue";
import OrganizationStructureView from "@/views/about/OrganizationStructureView.vue";
import HallView from "@/views/about/HallView.vue";
import PublicationsView from "@/views/info-dissemination/PublicationsView.vue";
import JournalView from "@/views/info-dissemination/JournalView.vue";
import MultimediaGallery from "@/views/info-dissemination/MultimediaGallery.vue"; // ตรวจสอบ path ให้ถูกต้อง
const routes = [
// --- Route สำหรับหน้า Landing Page (ใช้ LandingLayout) ---
@ -47,18 +57,82 @@ const routes = [
},
// !!! เพิ่ม Route สำหรับ NewsCategoryView !!!
{
path: 'tab-news/:category', // Path สำหรับข่าวตามหมวดหมู่ เช่น /news/RTAFNews
path: 'tab-news/:category', // Path สำหรับข่าวตามหมวดหมู่ เช่น /news/HumanTechNews
name: 'NewsCategoryView',
component: NewsCategoryView,
props: true // ส่งค่า parameter (category) เป็น props ไปยัง component ได้
},
// !!! เพิ่ม Route สำหรับ ContentView !!!
{
path: 'tab-news-content/:id', // Path สำหรับรายละเอียดข่าว เช่น /news-content/1, /news-content/2
path: 'tab-news-content/:id', // Path สำหรับรายละเอียดข่าว เช่น /tab-news-content/1, /tab-news-content/2
name: 'TabContentView',
component: TabContentView,
props: true // ส่งค่า parameter (id) เป็น props ไปยัง component ได้
},
// !!! เพิ่ม Route สำหรับ AllNewsView !!!
{
path: '/all-news', // กำหนด path ใหม่
name: 'AllNewsView',
component: AllNewsView
},
// --- เพิ่ม Route สำหรับ About HumanTech Sub-menus ---
{
path: '/about/history',
name: 'about-history',
component: () => import('@/views/about/HistoryView.vue'), // วางในโฟลเดอร์ย่อย 'about'
props: { menuId: 21 } // ส่ง ID เมนูย่อยไปให้ Component (ถ้าจำเป็น)
},
// --- เพิ่ม Route สำหรับ About HumanTech Sub-menus ---
{
path: '/about/vision-mission',
name: 'vision-mission',
component: () => import('@/views/about/VisionMissionView.vue'), // วางในโฟลเดอร์ย่อย 'about'
props: { menuId: 22 } // ส่ง ID เมนูย่อยไปให้ Component (ถ้าจำเป็น)
},
{
path: '/about/commander',
name: 'commander',
component: () => import('@/views/about/CommandingOfficersView.vue'), // วางในโฟลเดอร์ย่อย 'about'
props: { menuId: 23 } // ส่ง ID เมนูย่อยไปให้ Component (ถ้าจำเป็น)
},
// --- เพิ่ม Route สำหรับ Commander Bio (ประวัติผู้บริหาร) ---
{
// Path ควรเป็น /about/commander/:id เพื่อให้สอดคล้องกับโครงสร้าง URL ที่คุณใช้
// และให้สัมพันธ์กับ path: '/about/commander' ด้านบน
path: '/about/commander/:id', // หรือถ้าต้องการให้เป็น `/about/bio/c101` ก็เปลี่ยนเป็น `path: '/about/bio/c:id'`
name: 'CommanderBio', // ตั้งชื่อ Route
component: CommanderBioView, // ชี้ไปที่ Component ที่สร้างไว้
props: true // ส่งค่า parameter 'id' ไปยัง component ได้
},
{
path: '/about/organization',
name: 'organizationStructure',
component: OrganizationStructureView
},
{
path: '/about/commanders-list',
name: 'HallOfFame', // ชื่อ Route ยังคงเป็น HallOfFame
component: HallView // Component ก็ยังคงเป็น HallView
},
{
path: '/data/publications', // URL สำหรับหน้าเอกสารเผยแพร่ทั่วไป
name: 'Publications',
component: PublicationsView,
meta: {
title: 'เอกสารเผยแพร่', // สำหรับตั้งค่า Title ของหน้า (ถ้าใช้)
isPublic: true,
},
},
{
path: '/data/journal', // <-- กำหนด Path สำหรับ JournalView
name: 'journal', // <-- ตั้งชื่อ Route
component: JournalView, // <-- ชี้ไปที่ Component JournalView ที่ import มา
},
{
path: '/data/gallery-media', // <-- กำหนด Path สำหรับ คลังภาพ/สื่อผสม
name: 'gallery-media', // <-- ตั้งชื่อ Route
component: MultimediaGallery, // <-- ชี้ไปที่ Component MultimediaGallery ที่ import มา
},
// ... ( routes อื่นๆ ที่อาจจะใช้ DefaultLayout )
// *** เส้นทาง 404 (ต้องอยู่สุดท้ายเสมอ!) ***

1025
src/stores/aboutUsStore.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ export const useAppStore = defineStore('app', {
{ title: "Press Release", title_en: "Press Release", category: "GeneralPublic" },
{ title: "ข่าวบริการประชาชน", title_en: "Public Service News", category: "EventActivities" },
],
// *** ข้อมูลสำหรับ Header ***
headers: {
header_background: { url: '/images/news_header_bg_b815923058.png' },
logo: { url: '/images/Enter.png' }, // ใช้รูปโลโก้ตามที่ระบุ
@ -22,40 +22,169 @@ export const useAppStore = defineStore('app', {
{ id: 1, main_menu: 'หน้าหลัก', main_menu_en: 'Home', link: '/home', order: 1, active: true, active_en: true, sub_menus: [] },
{
id: 2, main_menu: 'รู้จักหน่วยงาน', main_menu_en: 'About HumanTech', order: 2, active: true, active_en: true,
sub_menus: [
// เพิ่ม sub_menu_groups เพื่อรองรับการแสดงผลแบบหลายคอลัมน์
sub_menu_groups: [
{
group_title_th: 'เกี่ยวกับองค์กร',
group_title_en: 'About Organization',
items: [
{ id: 21, title_th: 'ประวัติความเป็นมา', title_en: 'HumanTech History', link: '/about/history', order: 1, active: true, active_en: true },
{ id: 22, title_th: 'วิสัยทัศน์ พันธกิจ', title_en: 'Vision & Mission', link: '/about/vision-mission', order: 2, active: true, active_en: true },
{ id: 23, title_th: 'คณะผู้บริหารองค์กร', title_en: 'Commander-in-Chief', link: '/about/commander', order: 3, active: true, active_en: true },
{ id: 24, title_th: 'นโยบายองค์กร', title_en: 'C-in-C Policy', link: '/about/policy', order: 4, active: true, active_en: true },
{ id: 25, title_th: 'โครงสร้างองค์กร', title_en: 'Organization Structure', link: '/about/organization', order: 5, active: true, active_en: true },
{ id: 26, title_th: 'ทำเนียบผู้บริหารองค์กร', title_en: 'Commanders List', link: '/about/commanders-list', order: 6, active: true, active_en: true },
{ id: 27, title_th: 'ผู้บริหารเทคโนโลยีสารสนเทศระดับสูงองค์กร', title_en: 'HumanTech Senior IT Management', link: '/about/it-management', order: 7, active: true, active_en: true },
]
},
{
group_title_th: 'ผู้บริหาร',
group_title_en: 'Executives',
items: [
{ id: 23, title_th: 'คณะผู้บริหารองค์กร', title_en: 'Commander-in-Chief', link: '/about/commander', order: 3, active: true, active_en: true },
{ id: 26, title_th: 'ทำเนียบผู้บริหารองค์กร', title_en: 'Commanders List', link: '/about/commanders-list', order: 6, active: true, active_en: true },
]
},
{
group_title_th: 'โครงสร้าง',
group_title_en: 'Structure',
items: [
{ id: 25, title_th: 'โครงสร้างองค์กร', title_en: 'Organization Structure', link: '/about/organization', order: 5, active: true, active_en: true },
]
},
],
sub_menus: [] // ล้าง sub_menus เดิม หรือจะเก็บไว้เป็น fallback ก็ได้ แต่ถ้ามี sub_menu_groups จะถูกใช้ก่อน
},
{
id: 3, main_menu: 'ข้อมูลเผยแพร่', main_menu_en: 'Information', order: 3, active: true, active_en: true,
sub_menus: [
// เพิ่ม sub_menu_groups
sub_menu_groups: [
{
group_title_th: 'เอกสารและวารสาร',
group_title_en: 'Documents & Journals',
items: [
{ id: 31, title_th: 'เอกสารเผยแพร่', title_en: 'Publications', link: '/data/publications', order: 1, active: true, active_en: true },
{ id: 32, title_th: 'วารสารองค์กร', title_en: 'HumanTech Journal', link: '/data/journal', order: 2, active: true, active_en: true },
{ id: 33, title_th: 'คลังภาพ/สื่อผสม', title_en: 'Gallery/Media', link: '/data/gallery-media', order: 3, active: true, active_en: true },
]
},
{
group_title_th: 'รายงานและกฎหมาย',
group_title_en: 'Reports & Laws',
items: [
{ id: 34, title_th: 'รายงานผลการดำเนินงาน', title_en: 'Performance Report', link: '/data/performance-report', order: 4, active: true, active_en: true },
{ id: 35, title_th: 'รายงานการเงินหน่วยงาน', title_en: 'HumanTech Financial Report', link: '/data/financial-report', order: 5, active: true, active_en: true },
{ id: 36, title_th: 'กฎหมายที่เกี่ยวข้อง', title_en: 'Related Laws', link: '/data/laws', order: 6, active: true, active_en: true },
]
},
{
group_title_th: 'สื่อสารสาธารณะ',
group_title_en: 'Public Communication',
items: [
{ id: 33, title_th: 'คลังภาพ/สื่อผสม', title_en: 'Gallery/Media', link: '/data/gallery-media', order: 3, active: true, active_en: true },
{ id: 37, title_th: 'การเปิดเผยข้อมูลสาธารณะ', title_en: 'Public Information Disclosure', link: '/data/public-disclosure', order: 7, active: true, active_en: true },
{ id: 38, title_th: 'ปฏิทินกิจกรรม', title_en: 'Activity Calendar', link: '/data/calendar', order: 8, active: true, active_en: true },
]
},
{ id: 4, main_menu: 'บริการของเรา', main_menu_en: 'HumanTech Services', link: '/services', order: 5, active: true, active_en: true, sub_menus: [] },
],
sub_menus: []
},
{
id: 5, main_menu: 'การติดต่อ', main_menu_en: 'Contact', order: 6, active: true, active_en: true,
sub_menus: [
{ id: 51, title_th: 'ติดต่อเรา', title_en: 'Contact Us', link: '/contact/us', order: 1, active: true, active_en: true },
{ id: 52, title_th: 'คำถามยอดฮิต', title_en: 'FAQ', link: '/contact/faq', order: 2, active: true, active_en: true },
{ id: 53, title_th: 'ร้องทุกข์ร้องเรียน', title_en: 'Complaints', link: '/contact/complaints', order: 3, active: true, active_en: true },
id: 4, main_menu: 'บริการของเรา', main_menu_en: 'HumanTech Services', order: 5, active: true, active_en: true,
sub_menu_groups: [
{
group_title_th: 'บริการวิชาการ', // คอลัมน์ที่ 1: บริการวิชาการ
group_title_en: 'Academic Services',
items: [
{ id: 411, title_th: 'ศูนย์ฝึกอบรมวิชาการ', title_en: 'Training Center for HumanTech', link: '/services/training', order: 1, active: true, active_en: true },
{ id: 412, title_th: 'ผลงานวิจัย', title_en: 'HumanTech Researches', link: '/services/researches', order: 2, active: true, active_en: true },
{ id: 413, title_th: 'ศูนย์กฎหมายเพื่อประชาชน', title_en: 'Legal Center for Public', link: '/services/legal-public', order: 3, active: true, active_en: true },
//{ id: 414, title_th: 'ศูนย์บริหารวิชาการแจ้งจุฬามหาวิทยาลัย', title_en: 'Academic Administration Center', link: '/services/academic-admin', order: 4, active: true, active_en: true },
]
},
{
group_title_th: 'บริการทางการแพทย์', // คอลัมน์ที่ 2: บริการทางการแพทย์
group_title_en: 'Medical Services',
items: [
//{ id: 421, title_th: 'ธนาคารโลหิต', title_en: 'Blood Bank', link: '/services/blood-bank', order: 1, active: true, active_en: true },
//{ id: 422, title_th: 'บริการทันตกรรม', title_en: 'Dental Services', link: '/services/dental', order: 2, active: true, active_en: true },
{ id: 423, title_th: 'คลินิกกายภาพบำบัด', title_en: 'Physical Therapy Clinic', link: '/services/physio-clinic', order: 3, active: true, active_en: true },
{ id: 424, title_th: 'คลินิกโภชนาการและการกำหนดอาหาร', title_en: 'Nutrition & Diet Clinic', link: '/services/nutrition-clinic', order: 4, active: true, active_en: true },
//{ id: 425, title_th: 'โรงพยาบาลสัตว์เล็กจุฬาฯ', title_en: 'Small Animal Hospital', link: '/services/animal-hospital', order: 5, active: true, active_en: true },
]
},
{
group_title_th: 'บริการตรวจวิเคราะห์คุณภาพ', // คอลัมน์ที่ 3: บริการตรวจวิเคราะห์คุณภาพ
group_title_en: 'Quality Analysis Services',
items: [
{ id: 431, title_th: 'ศูนย์ทดสอบวิจัย', title_en: 'HumanTech Testing & Research Center', link: '/services/trecs', order: 1, active: true, active_en: true },
//{ id: 432, title_th: 'ศูนย์วิทยาศาสตร์ตลาด', title_en: 'Market Science Center', link: '/services/market-science', order: 2, active: true, active_en: true },
//{ id: 433, title_th: 'ศูนย์ความยั่งยืนด้านการจัดการและของเสียอันตราย', title_en: 'Sustainability & Hazardous Waste Mgmt Center', link: '/services/sustainability', order: 3, active: true, active_en: true },
{ id: 434, title_th: 'ตรวจสอบคุณภาพซอฟต์แวร์', title_en: 'Software Quality Monitoring', link: '/services/env-monitoring', order: 4, active: true, active_en: true },
//{ id: 435, title_th: 'ศูนย์เครื่องมือวิจัยวิทยาศาสตร์และเทคโนโลยี (STREC)', title_en: 'Scientific Research & Technology Equipment Center (STREC)', link: '/services/strec', order: 5, active: true, active_en: true },
]
},
{
group_title_th: 'สารสนเทศและการสื่อสาร', // คอลัมน์ที่ 4: สารสนเทศและการสื่อสาร
group_title_en: 'Information & Communication',
items: [
//{ id: 441, title_th: 'สำนักงานวิทยทรัพยากร (หอสมุดกลาง)', title_en: 'Academic Resources Office (Central Library)', link: '/services/library', order: 1, active: true, active_en: true },
{ id: 442, title_th: 'คลังซอฟต์แวร์', title_en: 'HumanTech Archives Center', link: '/services/archives', order: 2, active: true, active_en: true },
//{ id: 443, title_th: 'สำนักพิมพ์แห่งจุฬาฯ', title_en: 'Chula Press', link: '/services/chula-press', order: 3, active: true, active_en: true },
{ id: 444, title_th: 'ห้องสมุดกลาง', title_en: 'HumanTech Central Library', link: '/services/uni-library', order: 4, active: true, active_en: true },
//{ id: 445, title_th: 'สถาบันวิทยุแห่งจุฬาฯ', title_en: 'Chula Radio Institute', link: '/services/chula-radio', order: 5, active: true, active_en: true },
]
},
{
group_title_th: 'พื้นที่สร้างสรรค์', // คอลัมน์ที่ 5: พื้นที่สร้างสรรค์
group_title_en: 'Creative Spaces',
items: [
//{ id: 451, title_th: 'ศูนย์เครื่องหมายรับรองภูมิปัญญาจุฬาฯ', title_en: 'Chula Intellectual Property Certification Center', link: '/services/ip-center', order: 1, active: true, active_en: true },
{ id: 452, title_th: 'ศูนย์ฝึกทักษะ', title_en: 'Chula Skills Training Center', link: '/services/skills-training', order: 2, active: true, active_en: true },
{ id: 453, title_th: 'ธรรมสถาน', title_en: 'HumanTech Dharma Center', link: '/services/dharma-center', order: 3, active: true, active_en: true },
{ id: 454, title_th: 'สถานที่ท่องเที่ยว', title_en: 'HumanTech Centenary Park', link: '/services/centenary-park', order: 4, active: true, active_en: true },
//{ id: 455, title_th: 'จุฬาลงกรณ์ราชบรรณาธิปัตย์', title_en: 'Chulalongkorn Royal Library', link: '/services/royal-library', order: 5, active: true, active_en: true },
{ id: 456, title_th: 'พิพิธภัณฑ์', title_en: 'HumanTech Museum', link: '/services/museum', order: 6, active: true, active_en: true },
]
},
],
sub_menus: [] // ตรวจสอบให้แน่ใจว่า sub_menus ถูกล้างออกเมื่อใช้ sub_menu_groups
},
{
id: 5, main_menu: 'การติดต่อ', main_menu_en: 'Contact', order: 6, active: true, active_en: true,
sub_menu_groups: [
{
group_title_th: 'ข้อมูลติดต่อ', // กลุ่มที่ 1: ข้อมูลและช่องทางติดต่อ
group_title_en: 'Contact Information',
items: [
{ id: 51, title_th: 'ที่อยู่ติดต่อสอบถาม', title_en: 'Address & Contact Us', link: '/contact/address', order: 1, active: true, active_en: true },
{ id: 52, title_th: 'หมายเลขโทรศัพท์', title_en: 'Phone Numbers', link: '/contact/phone', order: 2, active: true, active_en: true },
]
},
{
group_title_th: 'แผนที่และการเดินทาง', // กลุ่มที่ 2: แผนที่และการเดินทาง
group_title_en: 'Maps & Directions',
items: [
{ id: 53, title_th: 'แผนที่และการเดินทางมาสำนักงานใหญ่', title_en: 'Map & Directions to HumanTech', link: '/contact/map-ku', order: 3, active: true, active_en: true },
{ id: 54, title_th: 'แผนที่และการเดินทางในสำนักงานใหญ่', title_en: 'Map & Directions within HumanTech', link: '/contact/map-within-ku', order: 4, active: true, active_en: true },
{ id: 55, title_th: 'แผนที่อาคารจอดรถบริเวณสำนักงานใหญ่', title_en: 'Parking Map within HumanTech', link: '/contact/parking-map', order: 5, active: true, active_en: true },
]
},
{
group_title_th: 'บริการช่วยเหลือ', // กลุ่มที่ 3: บริการช่วยเหลือและคำถามที่พบบ่อย
group_title_en: 'Support & FAQ',
items: [
{ id: 56, title_th: 'ค้นหาข้อมูล', title_en: 'Search Directory', link: '/contact/search-directory', order: 6, active: true, active_en: true },
//{ id: 57, title_th: 'HumanTech Webmail', title_en: 'HumanTech Webmail', link: 'https://webmail.humantech.tech', order: 7, active: true, active_en: true }, // ตัวอย่างลิงก์ภายนอก
{ id: 58, title_th: 'ถาม ตอบ', title_en: 'Q&A / FAQ', link: '/contact/faq-new', order: 8, active: true, active_en: true },
]
},
// {
// group_title_th: 'ร้องทุกข์',
// group_title_en: 'Complaints',
// items: [
// { id: 59, title_th: 'ร้องทุกข์ร้องเรียน', title_en: 'Complaints', link: '/contact/complaints', order: 9, active: true, active_en: true },
// ]
// },
],
sub_menus: [] // ควรล้าง sub_menus เดิมออกเมื่อใช้ sub_menu_groups แล้ว
},
],
},
// *** ข้อมูลสำหรับ Footer ***
@ -168,8 +297,28 @@ export const useAppStore = defineStore('app', {
id: 1,
title_th: 'โครงการบินเพื่อชีวิต: กองทัพอากาศช่วยเหลือผู้ป่วยฉุกเฉิน',
title_en: 'Flying for Life: RTAF Assists Emergency Patients',
detail_th: 'กองทัพอากาศได้ปฏิบัติภารกิจบินลำเลียงผู้ป่วยฉุกเฉินจากจังหวัดชายแดนสู่โรงพยาบาลกลางอย่างเร่งด่วน ภารกิจนี้ไม่เพียงแต่ช่วยชีวิต แต่ยังแสดงถึงบทบาทสำคัญของกองทัพในงานด้านมนุษยธรรม',
detail_en: 'The Royal Thai Air Force conducted an urgent airlift mission, transporting critical patients from border provinces to central hospitals, showcasing the humanitarian role of the military.',
detail_th: '<p>กองทัพอากาศได้ปฏิบัติภารกิจบินลำเลียงผู้ป่วยฉุกเฉินจากจังหวัดชายแดนสู่โรงพยาบาลกลางอย่างเร่งด่วน ภารกิจนี้ไม่เพียงแต่ช่วยชีวิต แต่ยังแสดงถึงบทบาทสำคัญของกองทัพในงานด้านมนุษยธรรม</p>' +
'<h3>ขั้นตอนการปฏิบัติภารกิจ:</h3>' +
'<ul>' +
'<li>รับแจ้งเหตุฉุกเฉินและประเมินสถานการณ์อย่างรวดเร็ว</li>' +
'<li>จัดเตรียมทีมแพทย์ฉุกเฉินและอากาศยานที่เหมาะสมกับภารกิจ</li>' +
'<li>ดำเนินการบินลำเลียงผู้ป่วยไปยังโรงพยาบาลเป้าหมายอย่างปลอดภัย</li>' +
'<li>ส่งมอบผู้ป่วยให้แก่บุคลากรทางการแพทย์เพื่อทำการรักษาต่อเนื่อง</li>' +
'</ul>' +
'<p><b>ความร่วมมือ</b>ระหว่างหน่วยงานต่างๆ ทั้งภาครัฐและภาคเอกชน เป็นปัจจัยสำคัญในความสำเร็จของภารกิจช่วยเหลือชีวิตครั้งนี้ <i>เรามุ่งมั่นที่จะพัฒนาศักยภาพเพื่อประชาชนต่อไป</i></p>' +
'<h4>ติดต่อสอบถามเพิ่มเติม:</h4>' +
'<p>หากมีข้อสงสัยหรือต้องการข้อมูลเพิ่มเติม สามารถติดต่อศูนย์ประสานงานได้ตลอด 24 ชั่วโมง</p>',
detail_en: '<p>The Royal Thai Air Force conducted an urgent airlift mission, transporting critical patients from border provinces to central hospitals, showcasing the humanitarian role of the military.</p>' +
'<h3>Mission Steps:</h3>' +
'<ul>' +
'<li>Rapid emergency notification and situation assessment</li>' +
'<li>Preparation of emergency medical team and suitable aircraft</li>' +
'<li>Safe airlift of patients to the designated hospital</li>' +
'<li>Handover of patients to medical personnel for continued treatment</li>' +
'</ul>' +
'<p><b>Collaboration</b> among various government and private agencies is a critical factor in the success of this life-saving mission. <i>We are committed to further developing our capabilities for the public.</i></p>' +
'<h4>For more information:</h4>' +
'<p>If you have any questions or require further information, please contact our coordination center 24/7.</p>',
image: { url: '/uploads/airlift_patient.jpg' },
release_date: '07/04/2025',
active: true,
@ -178,50 +327,252 @@ export const useAppStore = defineStore('app', {
type: 'HumanTechNews'
},
{
id: 2, title_th: 'ข่าวทั่วไป 1 (ไทย)', title_en: 'General News 1 (Eng)',
detail_th: 'รายละเอียดข่าวทั่วไป 1 ภาษาไทย...', detail_en: 'Detail of general news 1 in English...',
image: { url: '/uploads/news_2.jpg' }, release_date: '02/01/2025', active: true, active_en: true, feature: false, type: 'HumanTechNews'
id: 2,
title_th: 'นวัตกรรมโดรนเพื่อการเกษตร: อนาคตของการทำนาอัจฉริยะ',
title_en: 'Agricultural Drone Innovation: The Future of Smart Farming',
detail_th: '<p>เทคโนโลยีโดรนกำลังเข้ามามีบทบาทสำคัญในการปฏิวัติภาคเกษตรกรรมของไทย โดรนสามารถช่วยในการสำรวจพื้นที่เพาะปลูก, พ่นปุ๋ยและยาฆ่าแมลงได้อย่างแม่นยำ, และประเมินผลผลิตพืช</p>' +
'<p><u>ประโยชน์หลักๆ:</u></p>' +
'<ol>' +
'<li>ลดต้นทุนและเวลาในการทำงาน</li>' +
'<li>เพิ่มประสิทธิภาพและความแม่นยำในการจัดการพืชผล</li>' +
'<li>เข้าถึงพื้นที่ที่เข้าถึงยากได้ง่ายขึ้น</li>' +
'<li>ช่วยลดการใช้สารเคมีเกินความจำเป็น</li>' +
'</ol>' +
'<p>การนำโดรนมาใช้ในการเกษตรถือเป็นการยกระดับคุณภาพชีวิตของเกษตรกรและเพิ่มขีดความสามารถในการแข่งขันของผลผลิตไทยในตลาดโลก</p>',
detail_en: '<p>Drone technology is playing a crucial role in revolutionizing Thailand\'s agricultural sector. Drones can assist in surveying cultivated areas, precisely spraying fertilizers and pesticides, and assessing crop yields.</p>' +
'<p><u>Key Benefits:</u></p>' +
'<ol>' +
'<li>Reduces costs and working hours</li>' +
'<li>Increases efficiency and precision in crop management</li>' +
'<li>Easier access to hard-to-reach areas</li>' +
'<li>Helps reduce unnecessary chemical usage</li>' +
'</ol>' +
'<p>The adoption of drones in agriculture is seen as a way to improve farmers\' quality of life and enhance the competitiveness of Thai produce in the global market.</p>',
image: { url: '/uploads/news_2.jpg' },
release_date: '02/01/2025',
active: true,
active_en: true,
feature: false,
type: 'HumanTechNews'
},
{
id: 3, title_th: 'ข่าวเด่น 2 (ไทย)', title_en: 'Hot News 2 (Eng)',
detail_th: 'รายละเอียดข่าวเด่น 2 ภาษาไทย...', detail_en: 'Detail of hot news 2 in English...',
image: { url: '/uploads/news_3.jpg' }, release_date: '03/01/2025', active: true, active_en: true, feature: false, type: 'OrgNews'
id: 3,
title_th: 'พิธีมอบประกาศนียบัตรหลักสูตรพัฒนาบุคลากรดิจิทัล',
title_en: 'Digital Personnel Development Course Certificate Presentation Ceremony',
detail_th: '<p>เมื่อเร็วๆ นี้ กองทัพอากาศได้จัดพิธีมอบประกาศนียบัตรแก่ผู้สำเร็จหลักสูตร <strong>"การพัฒนาบุคลากรด้านดิจิทัลเพื่อความมั่นคงทางไซเบอร์"</strong> โดยมีผู้บริหารระดับสูงเข้าร่วมและแสดงความยินดี</p>' +
'<blockquote><p><i>"ความรู้และทักษะด้านดิจิทัลเป็นสิ่งจำเป็นในยุคปัจจุบัน โดยเฉพาะอย่างยิ่งในด้านความมั่นคงของชาติ"</i></p></blockquote>' +
'<p>ผู้เข้าร่วมหลักสูตรได้เรียนรู้เกี่ยวกับ:<ul><li>ภัยคุกคามทางไซเบอร์รูปแบบใหม่</li><li>วิธีการป้องกันและรับมือ</li><li>กฎหมายและข้อบังคับที่เกี่ยวข้อง</li></ul></p>' +
'<p>โครงการนี้มุ่งหวังที่จะเสริมสร้างศักยภาพบุคลากรให้พร้อมรับมือกับความท้าทายในโลกไซเบอร์</p>',
detail_en: '<p>Recently, the Royal Thai Air Force organized a certificate presentation ceremony for graduates of the <strong>"Digital Personnel Development for Cyber Security"</strong> course, with high-ranking executives attending and congratulating them.</p>' +
'<blockquote><p><i>"Digital knowledge and skills are essential in the current era, especially in national security."</i></p></blockquote>' +
'<p>Course participants learned about:<ul><li>New forms of cyber threats</li><li>Prevention and response methods</li><li>Relevant laws and regulations</li></ul></p>' +
'<p>This project aims to enhance personnel capabilities to cope with challenges in the cyber world.</p>',
image: { url: '/uploads/news_3.jpg' },
release_date: '03/01/2025',
active: true,
active_en: true,
feature: false,
type: 'OrgNews'
},
{
id: 4, title_th: 'ข่าวทั่วไป 2 (ไทย)', title_en: 'General News 2 (Eng)',
detail_th: 'รายละเอียดข่าวทั่วไป 2 ภาษาไทย...', detail_en: 'Detail of general news 2 in English...',
image: { url: '/uploads/news_4.jpg' }, release_date: '04/01/2025', active: true, active_en: true, feature: false, type: 'OrgNews'
id: 4,
title_th: 'มาตรการประหยัดพลังงานในหน่วยงานภาครัฐ',
title_en: 'Energy Saving Measures in Government Agencies',
detail_th: '<p>รัฐบาลได้ประกาศใช้มาตรการเข้มงวดในการประหยัดพลังงานในทุกหน่วยงานภาครัฐ เพื่อลดภาระค่าใช้จ่ายและส่งเสริมการใช้พลังงานอย่างมีประสิทธิภาพ มาตรการนี้รวมถึงการปรับปรุงระบบไฟฟ้าและปรับพฤติกรรมการใช้พลังงานของบุคลากร</p>' +
'<p>ตัวอย่างมาตรการ:</p>' +
'<ul>' +
'<li><span style="color: #FF0000;">ลดการใช้เครื่องปรับอากาศ:</span> กำหนดอุณหภูมิที่เหมาะสมและปิดเมื่อไม่ใช้งาน</li>' +
'<li><span style="font-weight: bold;">เปลี่ยนมาใช้อุปกรณ์ประหยัดพลังงาน:</span> เช่น หลอดไฟ LED</li>' +
'<li><span style="text-decoration: underline;">ส่งเสริมการเดินทางด้วยระบบขนส่งสาธารณะ</span></li>' +
'</ul>' +
'<p>การประหยัดพลังงานเป็นความรับผิดชอบของทุกคน เพื่ออนาคตที่ยั่งยืน</p>',
detail_en: '<p>The government has announced strict energy-saving measures across all government agencies to reduce expenses and promote efficient energy use. These measures include improving electrical systems and adjusting personnel\'s energy consumption habits.</p>' +
'<p>Examples of measures:</p>' +
'<ul>' +
'<li><span style="color: #FF0000;">Reduce air conditioning use:</span> Set appropriate temperatures and turn off when not in use</li>' +
'<li><span style="font-weight: bold;">Switch to energy-saving equipment:</span> such as LED bulbs</li>' +
'<li><span style="text-decoration: underline;">Promote public transportation</span></li>' +
'</ul>' +
'<p>Energy saving is everyone\'s responsibility for a sustainable future.</p>',
image: { url: '/uploads/news_4.jpg' },
release_date: '04/01/2025',
active: true,
active_en: true,
feature: false,
type: 'OrgNews'
},
{
id: 5, title_th: 'ข่าวประชาสัมพันธ์หน่วยงาน', title_en: 'RTAF News Sample',
detail_th: 'รายละเอียดข่าวประชาสัมพันธ์ ภาษาไทยที่มีความยาวมากเกินไปเพื่อทดสอบการตัดคำ: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
detail_en: 'English detail of RTAF News Sample with excessive length to test truncation: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
image: { url: '/uploads/news_5.jpg' }, release_date: '05/01/2025', active: true, active_en: true, feature: false, type: 'HumanTechNews'
id: 5,
title_th: 'นโยบายใหม่เพื่อส่งเสริม Startups ด้านเทคโนโลยีการบิน',
title_en: 'New Policy to Promote Aviation Tech Startups',
detail_th: '<p>กองทัพอากาศได้ประกาศนโยบายใหม่เพื่อ<b>ส่งเสริมและสนับสนุน</b>กลุ่ม Startups ที่มีนวัตกรรมด้านเทคโนโลยีการบินและอวกาศ โดยมีเป้าหมายเพื่อผลักดันให้เกิดการพัฒนาเทคโนโลยีที่ล้ำสมัยภายในประเทศ</p>' +
'<h3>สิ่งที่โครงการนำเสนอ:</h3>' +
'<ol>' +
'<li>ทุนสนับสนุนการวิจัยและพัฒนา (R&D)</li>' +
'<li>พื้นที่ Co-working Space และ Lab สำหรับทดลอง</li>' +
'<li>คำปรึกษาจากผู้เชี่ยวชาญในอุตสาหกรรม</li>' +
'<li>โอกาสในการทดสอบเทคโนโลยีกับโครงสร้างพื้นฐานของกองทัพอากาศ</li>' +
'</ol>' +
'<p><i>โครงการนี้คาดว่าจะช่วยสร้างงานและเสริมสร้างความเข้มแข็งของอุตสาหกรรมเทคโนโลยีการบินของไทยในระยะยาว</i></p>' +
'<p>สำหรับข้อมูลเพิ่มเติมและเกณฑ์การเข้าร่วม สามารถเยี่ยมชมเว็บไซต์ของเราได้ที่ <a href="https://rtaf.mil.th/innovation" target="_blank">RTAF Innovation Hub</a></p>',
detail_en: '<p>The Royal Thai Air Force has announced a new policy to <b>promote and support</b> startups with innovations in aviation and aerospace technology, aiming to drive the development of cutting-edge domestic technology.</p>' +
'<h3>What the program offers:</h3>' +
'<ol>' +
'<li>Research and Development (R&D) funding</li>' +
'<li>Co-working spaces and labs for experimentation</li>' +
'<li>Consultation from industry experts</li>' +
'<li>Opportunities to test technology with RTAF infrastructure</li>' +
'</ol>' +
'<p><i>This project is expected to create jobs and strengthen Thailand\'s aviation technology industry in the long run.</i></p>' +
'<p>For more information and participation criteria, please visit our website at <a href="https://rtaf.mil.th/innovation" target="_blank">RTAF Innovation Hub</a></p>',
image: { url: '/uploads/news_5.jpg' },
release_date: '05/01/2025',
active: true,
active_en: true,
feature: false,
type: 'HumanTechNews'
},
{
id: 6, title_th: 'ข่าวประชาสัมพันธ์หน่วยงานภายใน', title_en: 'RTAF Organization News Sample',
detail_th: 'รายละเอียดข่าวประชาสัมพันธ์หน่วยงานภายใน ภาษาไทย...', detail_en: 'Detail of RTAF Organization News Sample...',
image: { url: '/uploads/news_6.jpg' }, release_date: '06/01/2025', active: true, active_en: true, feature: false, type: 'OrgNews'
id: 6,
title_th: 'สัมมนาวิชาการ: ความท้าทายของ AI ในงานข่าวกรอง',
title_en: 'Academic Seminar: The Challenges of AI in Intelligence Work',
detail_th: '<p>เมื่อสัปดาห์ที่ผ่านมา มีการจัดสัมมนาวิชาการเรื่อง <strong>"ปัญญาประดิษฐ์กับความมั่นคงแห่งชาติ"</strong> โดยมีผู้เชี่ยวชาญจากหลายสาขามาร่วมแลกเปลี่ยนความรู้และประสบการณ์</p>' +
'<blockquote><p><i>"AI ไม่ได้มาแทนที่มนุษย์ แต่มาเสริมศักยภาพให้มนุษย์ทำงานได้อย่างมีประสิทธิภาพมากขึ้น"</i> - หนึ่งในวิทยากรกล่าว</p></blockquote>' +
'<p>หัวข้อที่น่าสนใจในงานสัมมนา:</p>' +
'<ul>' +
'<li>การใช้ AI ในการวิเคราะห์ข้อมูลขนาดใหญ่</li>' +
'<li>จริยธรรมของ AI และความเสี่ยงที่อาจเกิดขึ้น</li>' +
'<li>แนวโน้มการพัฒนา AI เพื่อความมั่นคงในอนาคต</li>' +
'</ul>' +
'<p>งานสัมมนาประสบความสำเร็จอย่างงดงาม และมีผู้เข้าร่วมให้ความสนใจเป็นจำนวนมาก</p>',
detail_en: '<p>Last week, an academic seminar on <strong>"Artificial Intelligence and National Security"</strong> was held, with experts from various fields sharing their knowledge and experiences.</p>' +
'<blockquote><p><i>"AI is not here to replace humans, but to enhance human capabilities to work more efficiently."</i> - stated one of the speakers</p></blockquote>' +
'<p>Key topics of interest at the seminar:</p>' +
'<ul>' +
'<li>Using AI for big data analysis</li>' +
'<li>AI ethics and potential risks</li>' +
'<li>Future trends in AI development for security</li>' +
'</ul>' +
'<p>The seminar was a resounding success, with a large number of attendees showing great interest.</p>',
image: { url: '/uploads/news_6.jpg' },
release_date: '06/01/2025',
active: true,
active_en: true,
feature: false,
type: 'OrgNews'
},
{
id: 7, title_th: 'ข่าวบริการกำลังพล ทอ.', title_en: 'RTAF Service News Sample',
detail_th: 'รายละเอียดข่าวบริการกำลังพล ทอ. ภาษาไทย...', detail_en: 'Detail of RTAF Service News Sample...',
image: { url: '/uploads/news_7.jpg' }, release_date: '07/01/2025', active: true, active_en: true, feature: false, type: 'InnovationNews'
id: 7,
title_th: 'เปิดตัวศูนย์บริการประชาชนแบบ One Stop Service ทอ.',
title_en: 'RTAF Launches One-Stop Service Center for Public',
detail_th: '<p>กองทัพอากาศได้เปิดตัว <strong>"ศูนย์บริการประชาชนแบบ One Stop Service"</strong> เพื่ออำนวยความสะดวกให้แก่กำลังพลและประชาชนที่มาติดต่อราชการ</p>' +
'<p>บริการที่ครอบคลุม:</p>' +
'<ul>' +
'<li>การขอใบอนุญาตต่างๆ</li>' +
'<li>การแจ้งเรื่องร้องเรียน</li>' +
'<li>การรับเรื่องราวร้องทุกข์</li>' +
'<li>และข้อมูลข่าวสารที่เป็นประโยชน์</li>' +
'</ul>' +
'<p>ศูนย์ฯ มีเจ้าหน้าที่พร้อมให้บริการด้วยความรวดเร็วและเป็นกันเอง ทุกวันจันทร์-ศุกร์ เวลา 08.30-16.30 น.</p>' +
'<p><i>เรามุ่งมั่นที่จะยกระดับการให้บริการเพื่อความพึงพอใจสูงสุดของประชาชน</i></p>',
detail_en: '<p>The Royal Thai Air Force has launched a <strong>"One-Stop Service Center"</strong> to facilitate military personnel and the public interacting with government services.</p>' +
'<p>Services covered include:</p>' +
'<ul>' +
'<li>Various permit applications</li>' +
'<li>Complaint submissions</li>' +
'<li>Grievance handling</li>' +
'<li>And useful information</li>' +
'</ul>' +
'<p>The center has staff ready to provide quick and friendly service every Monday-Friday from 8:30 AM to 4:30 PM.</p>' +
'<p><i>We are committed to enhancing our services for the highest public satisfaction.</i></p>',
image: { url: '/uploads/news_7.jpg' },
release_date: '07/01/2025',
active: true,
active_en: true,
feature: false,
type: 'InnovationNews'
},
{
id: 8, title_th: 'Press Release/โฆษก ทอ.', title_en: 'RTAF Press Release Sample',
detail_th: 'รายละเอียด Press Release/โฆษก ทอ. ภาษาไทย...', detail_en: 'Detail of RTAF Press Release Sample...',
image: { url: '/uploads/news_8.jpg' }, release_date: '08/01/2025', active: true, active_en: true, feature: false, type: 'GeneralPublic'
id: 8,
title_th: 'แถลงการณ์โฆษกกองทัพอากาศ: สถานการณ์ชายแดนล่าสุด',
title_en: 'RTAF Spokesperson\'s Statement: Latest Border Situation',
detail_th: '<p>วันนี้ (7 ก.ค. 2568) โฆษกกองทัพอากาศได้ออกแถลงการณ์ชี้แจงสถานการณ์ชายแดนล่าสุด โดยยืนยันถึงความพร้อมของกำลังพลและยุทโธปกรณ์ในการปกป้องอธิปไตยของชาติ</p>' +
'<p>ประเด็นสำคัญในแถลงการณ์:</p>' +
'<ul>' +
'<li>สถานการณ์ทั่วไปยังคงสงบและอยู่ภายใต้การควบคุม</li>' +
'<li>มีการเพิ่มมาตรการเฝ้าระวังและลาดตระเวนอย่างต่อเนื่อง</li>' +
'<li>เน้นย้ำถึงความร่วมมือกับประเทศเพื่อนบ้านในการแก้ไขปัญหา</li>' +
'</ul>' +
'<p>ประชาชนสามารถมั่นใจในศักยภาพของกองทัพอากาศในการรักษาความมั่นคงของประเทศได้</p>' +
'<p><i>ภาพจากเหตุการณ์จริง (สำหรับประกอบข่าว)</i><br>' +
'<img src="https://via.placeholder.com/600x300/CCCCCC/888888?text=Border+Patrol" alt="Border Patrol" style="width:100%; height:auto; display:block; margin-top:15px;"></p>',
detail_en: '<p>Today (July 7, 2025), the Royal Thai Air Force Spokesperson issued a statement clarifying the latest border situation, affirming the readiness of personnel and equipment to protect national sovereignty.</p>' +
'<p>Key points in the statement:</p>' +
'<ul>' +
'<li>The overall situation remains calm and under control.</li>' +
'<li>Continuous surveillance and patrol measures have been increased.</li>' +
'<li>Emphasis on cooperation with neighboring countries in resolving issues.</li>' +
'</ul>' +
'<p>The public can be confident in the Royal Thai Air Force\'s capability to maintain national security.</p>' +
'<p><i>Actual event image (for news illustration)</i><br>' +
'<img src="https://via.placeholder.com/600x300/CCCCCC/888888?text=Border+Patrol+Eng" alt="Border Patrol Eng" style="width:100%; height:auto; display:block; margin-top:15px;"></p>',
image: { url: '/uploads/news_8.jpg' },
release_date: '07/08/2025',
active: true, active_en: true, feature: false, type: 'GeneralPublic'
},
{
id: 9, title_th: 'ข่าวบริการประชาชน', title_en: 'Public Service News Sample',
detail_th: 'รายละเอียดข่าวบริการประชาชน ภาษาไทย...', detail_en: 'Detail of Public Service News Sample...',
image: { url: '/uploads/news_9.jpg' }, release_date: '09/01/2025', active: true, active_en: true, feature: false, type: 'EventActivities'
id: 9,
title_th: 'กิจกรรมวันเด็กแห่งชาติ 2568: สานฝันเยาวชนไทย',
title_en: 'National Children\'s Day 2025: Fulfilling Thai Youth\'s Dreams',
detail_th: '<p>กองทัพอากาศเตรียมจัดกิจกรรมวันเด็กแห่งชาติประจำปี 2568 อย่างยิ่งใหญ่ ณ ฐานทัพอากาศดอนเมือง เพื่อมอบความสุขและแรงบันดาลใจให้แก่เยาวชนไทย</p>' +
'<p>กิจกรรมไฮไลท์:</p>' +
'<ul>' +
'<li>การแสดงการบินของเครื่องบินรบและเฮลิคอปเตอร์</li>' +
'<li>เยี่ยมชมเครื่องบินจริงและสัมผัสประสบการณ์ในห้องนักบิน</li>' +
'<li>นิทรรศการเทคโนโลยีการบินและอวกาศ</li>' +
'<li>เกมส์และของรางวัลมากมาย</li>' +
'</ul>' +
'<p>ขอเชิญชวนผู้ปกครองพาบุตรหลานมาร่วมงานในวันที่ <b><span style="color: #0000FF;">11 มกราคม 2568</span></b> ตั้งแต่เวลา 08.00 - 16.00 น.</p>',
detail_en: '<p>The Royal Thai Air Force is preparing to organize a grand National Children\'s Day event for 2025 at Don Mueang Royal Thai Air Force Base, to bring joy and inspiration to Thai youth.</p>' +
'<p>Activity Highlights:</p>' +
'<ul>' +
'<li>Aerobatic display of fighter jets and helicopters</li>' +
'<li>Visit real aircraft and experience the cockpit</li>' +
'<li>Aviation and aerospace technology exhibition</li>' +
'<li>Numerous games and prizes</li>' +
'</ul>' +
'<p>Parents are invited to bring their children to the event on <b><span style="color: #0000FF;">January 11, 2025</span></b> from 08:00 AM - 04:00 PM.</p>',
image: { url: '/uploads/news_9.jpg' },
release_date: '07/09/2025',
active: true, active_en: true, feature: false, type: 'EventActivities'
},
{
id: 10, title_th: 'ข่าวทั่วไป 3 (ไทย)', title_en: 'General News 3 (Eng)',
detail_th: 'รายละเอียดข่าวทั่วไป 3 ภาษาไทย...', detail_en: 'Detail of general news 3 in English...',
image: { url: '/uploads/news_10.jpg' }, release_date: '10/01/2025', active: true, active_en: true, feature: false, type: 'HumanTechNews'
id: 10,
title_th: 'กองทัพอากาศเปิดรับสมัครนักเรียนจ่าอากาศรุ่นใหม่',
title_en: 'RTAF Opens Applications for New Airman Students',
detail_th: '<p>กองทัพอากาศเปิดโอกาสให้เยาวชนไทยที่สนใจเข้าร่วมเป็นส่วนหนึ่งของกองทัพอากาศ โดยเปิดรับสมัครนักเรียนจ่าอากาศรุ่นใหม่ ประจำปีการศึกษา 2568</p>' +
'<h3>คุณสมบัติเบื้องต้น:</h3>' +
'<ul>' +
'<li>เพศชาย สัญชาติไทย</li>' +
'<li>อายุ 18-20 ปี</li>' +
'<li>จบการศึกษามัธยมศึกษาตอนปลาย (ม.6) หรือเทียบเท่า</li>' +
'<li>มีสุขภาพแข็งแรงและผ่านเกณฑ์การทดสอบสมรรถภาพ</li>' +
'</ul>' +
'<p>ผู้สนใจสามารถสมัครได้ทาง <a href="https://admission.rtaf.mi.th" target="_blank">เว็บไซต์หน่วยบัญชาการฝึกศึกษาทหารอากาศ</a> ตั้งแต่วันที่ <b>1 - 31 สิงหาคม 2568</b></p>' +
'<p><i>อนาคตที่มั่นคงและมีเกียรติรอคุณอยู่!</i></p>',
detail_en: '<p>The Royal Thai Air Force offers an opportunity for Thai youth interested in becoming part of the Air Force by opening applications for new airman students for the academic year 2025.</p>' +
'<h3>Basic Qualifications:</h3>' +
'<ul>' +
'<li>Male, Thai nationality</li>' +
'<li>Ages 18-20</li>' +
'<li>Completed high school (M.6) or equivalent</li>' +
'<li>Physically healthy and passed fitness tests</li>' +
'</ul>' +
'<p>Interested individuals can apply via the <a href="https://admission.rtaf.mi.th" target="_blank">Royal Thai Air Force Education Department website</a> from <b>August 1 - 31, 2025</b>.</p>' +
'<p><i>A stable and honorable future awaits you!</i></p>',
image: { url: '/uploads/news_10.jpg' },
release_date: '07/10/2025',
active: true, active_en: true, feature: false, type: 'HumanTechNews'
},
],
// Mock Data TabNews
@ -668,6 +1019,68 @@ export const useAppStore = defineStore('app', {
this.isTh = !this.isTh;
},
// *** ฟังก์ชัน fetchNewsData ที่ปรับปรุงใหม่ ทำการกรอง, เรียง, แบ่งหน้าทั้งหมดในตัว ***
async fetchNewsData(options = {}) {
await new Promise(resolve => setTimeout(resolve, 100));
const { category, limit = 6, page = 1, isFeature = null } = options;
const currentLangIsTh = options.lang === 'th' || this.isTh;
console.log(`[fetchNewsData] - Options: category=${category}, limit=${limit}, page=${page}, isFeature=${isFeature}, lang=${options.lang}`);
let filteredNews = this.mockNews.filter(item => {
const isActive = currentLangIsTh ? item.active : item.active_en;
if (!isActive) return false;
if (category) {
if (item.type !== category) {
return false;
}
}
if (isFeature !== null) {
if (item.feature !== isFeature) {
return false;
}
}
const now = new Date();
const currentDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
if (!item.release_date) return false;
const [month, day, year] = item.release_date.split('/');
const releaseDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return releaseDate <= currentDate;
});
console.log(`[fetchNewsData] - After filtering (active, category, feature, date). Filtered items before sort:`, filteredNews.map(item => ({ id: item.id, release_date: item.release_date, feature: item.feature })));
filteredNews.sort((a, b) => {
const dateA = a.release_date ? new Date(a.release_date.split('/').reverse().join('-')) : new Date(0);
const dateB = b.release_date ? new Date(b.release_date.split('/').reverse().join('-')) : new Date(0);
return dateB.getTime() - dateA.getTime();
});
console.log(`[fetchNewsData] - After sorting. Filtered items:`, filteredNews.map(item => item.id));
const totalCount = filteredNews.length;
// *** เพิ่ม Log ที่จะดักจับการตั้งค่า totalNewsCount ทุกครั้ง ***
console.log(`[SETTING totalNewsCount] - From: ${this.totalNewsCount} To: ${totalCount} (Called from fetchNewsData)`);
this.totalNewsCount = totalCount;
console.log(`[fetchNewsData] - Current totalNewsCount in Store: ${this.totalNewsCount}`); // ใช้ Current แทน Updated
let paginatedData = [];
if (limit > 0) {
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
paginatedData = filteredNews.slice(startIndex, endIndex);
} else {
paginatedData = filteredNews;
}
console.log(`[fetchNewsData] - Page ${page}, Limit ${limit}. Paginated data IDs:`, paginatedData.map(item => item.id));
return { data: paginatedData, total: totalCount };
},
// *** ฟังก์ชันกลางสำหรับดึงข่าวจาก mockNewsData ที่รองรับการกรอง, เรียง, และแบ่งหน้า ***
async fetchNewsFromMockData(options = {}) {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate API delay
@ -715,12 +1128,26 @@ export const useAppStore = defineStore('app', {
return this.fetchNewsFromMockData({ category, limit, page });
},
// *** Action เดิม: find (ยังคง return เป็น Array เหมือนเดิมสำหรับ endpoint อื่นๆ) ***
// *** Action find สำหรับจำลอง API Endpoint ***
async find(endpoint, queryParams = '') {
await new Promise(resolve => setTimeout(resolve, 100));
let data = [];
const isTh = this.isTh;
const parseParams = (qs) => {
const params = {};
if (!qs) return params;
qs.split('&').forEach(param => {
const [key, value] = param.split('=');
if (key && value) {
params[key] = decodeURIComponent(value);
}
});
return params;
};
// *** ย้ายการประกาศ parsedQueryParams มาไว้ตรงนี้ ***
const parsedQueryParams = parseParams(queryParams);
switch (endpoint) {
case 'calousels':
@ -765,43 +1192,37 @@ export const useAppStore = defineStore('app', {
break;
case 'contents':
case 'news':
data = this.mockNews.filter(item => {
const isActive = isTh ? item.active : item.active_en;
let match = isActive;
if (queryParams.includes('feature=true')) {
match = match && item.feature;
} else if (queryParams.includes('feature=false')) {
match = match && !item.feature;
}
return match;
});
const limitNewsMatch = parsedQueryParams._limit;
const isFeatureNewsMatch = parsedQueryParams.feature === 'true' ? true : (parsedQueryParams.feature === 'false' ? false : null);
data.sort((a, b) => {
const dateA = new Date(a.release_date.split('/').reverse().join('-'));
const dateB = new Date(b.release_date.split('/').reverse().join('-'));
return dateA - dateB;
});
const newsOptionsForFetch = {
category: parsedQueryParams.category,
limit: limitNewsMatch ? parseInt(limitNewsMatch) : undefined,
isFeature: isFeatureNewsMatch,
page: parsedQueryParams._page ? parseInt(parsedQueryParams._page) : 1,
lang: isTh ? 'th' : 'en'
};
const limit = queryParams.match(/_limit=(\d+)/);
if (limit) {
data = data.slice(0, parseInt(limit[1]));
}
break;
console.log(`[find - news] - Calling fetchNewsData with options:`, newsOptionsForFetch);
const resultNews = await this.fetchNewsData(newsOptionsForFetch);
data = resultNews.data;
console.log(`[find - news] - Returned data length from fetchNewsData: ${data.length}`);
return data;
case 'tabNews':
// ใช้ fetchNewsFromMockData เพื่อจัดการการกรอง/แบ่งหน้า
// ต้องแยก queryParams ออกมาเป็น object ก่อน
const limitMatch = queryParams.match(/_limit=(\d+)/);
const isFeatureMatch = queryParams.includes('feature=true') ? true : (queryParams.includes('feature=false') ? false : null);
// หาก find('news') หรือ find('contents') ต้องการดึงทั้งหมดโดยไม่แบ่งหน้า
// หาก find('tabNews') หรือ find('contents') ต้องการดึงทั้งหมดโดยไม่แบ่งหน้า
// เราก็สามารถเรียก fetchNewsFromMockData โดยไม่ส่ง limit/page
// หรือถ้าต้องการแบ่งหน้าในอนาคต ก็สามารถส่ง limit/page ได้
const result = await this.fetchNewsFromMockData({
// category: คุณอาจจะต้องเพิ่ม parameter 'category' เข้าไปใน queryParams string ด้วย ถ้าต้องการกรอง
limit: limitMatch ? parseInt(limitMatch[1]) : undefined, // ไม่จำกัดถ้าไม่มี limit ใน queryParams
isFeature: isFeatureMatch,
// หากต้องการ page สำหรับ find('news') ต้องส่งเข้ามาใน queryParams string ด้วย
// เช่น find('news', '_limit=5&_page=2')
// หากต้องการ page สำหรับ find('tabNews') ต้องส่งเข้ามาใน queryParams string ด้วย
// เช่น find('tabNews', '_limit=5&_page=2')
page: queryParams.match(/_page=(\d+)/) ? parseInt(queryParams.match(/_page=(\d+)/)[1]) : 1
});
data = result.data; // find action เดิม return แค่ data Array

View File

@ -0,0 +1,320 @@
// src/stores/infoDisseminationStore.js
import { defineStore } from 'pinia';
import { useAppStore } from './app'; // <--- Import appStore
export const useInfoDisseminationStore = defineStore('infoDissemination', {
state: () => ({
allRawDocuments: [],
allRawPublications: [],
pdfData: [],
videos: [],
galleries: [],
audio: [],
isLoading: {
documents: false,
publications: false,
videos: false, // ต้องมีและถูกเซ็ตเป็น true/false ถูกต้อง
galleries: false, // ต้องมีและถูกเซ็ตเป็น true/false ถูกต้อง
audio: false, // ต้องมีและถูกเซ็ตเป็น true/false ถูกต้อง
},
errors: {
documents: null,
publications: null,
videos: null, // ต้องมี
galleries: null, // ต้องมี
audio: null, // ต้องมี
},
}),
getters: {
// --- Mock Data Functions (เหล่านี้คือ getters ที่คุณจะเรียกใช้แบบไม่มีวงเล็บ) ---
getMockVideosData: () => {
// ใช้ YouTube URL จริงๆ ที่มี Video ID 11 ตัวอักษร
return [
{ id: '1', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'การฝึก Cobra Gold 2024', title_en: 'Cobra Gold 2024 Exercise' },
{ id: '2', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'เครื่องบินขับไล่ F-16 ของกองทัพอากาศ', title_en: 'Royal Thai Air Force F-16 Fighter Jet' },
{ id: '3', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'ภารกิจช่วยเหลือผู้ประสบภัย', title_en: 'Disaster Relief Mission' },
{ id: '4', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'ประวัติกองทัพอากาศไทย', title_en: 'History of Royal Thai Air Force' },
{ id: '5', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'การแสดงแสนยานุภาพทางอากาศ', title_en: 'Air Power Demonstration' },
{ id: '6', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'ชีวิตนักบินขับไล่', title_en: 'Life of a Fighter Pilot' },
{ id: '7', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'เทคโนโลยีอากาศยานไร้คนขับ', title_en: 'UAV Technology' },
{ id: '8', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'วันเด็กแห่งชาติ กองทัพอากาศ', title_en: 'Children\'s Day at RTAF' },
{ id: '9', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'การซ้อมรบร่วม', title_en: 'Joint Military Exercise' }
];
},
getMockGalleriesData: () => {
return [
{ id: '1', name: 'ภาพการฝึก Cobra Gold', name_en: 'Cobra Gold Training Photos', images: [{ url: '/mock-images/gallery1.jpg' }, { url: '/mock-images/gallery2.jpg' }, { url: '/mock-images/gallery3.jpg' }] },
{ id: '2', name: 'ภาพงานวันเด็ก', name_en: 'Children\'s Day Event Photos', images: [{ url: '/mock-images/gallery4.jpg' }, { url: '/mock-images/gallery5.jpg' }] },
{ id: '3', name: 'ภาพงานกาชาด', name_en: 'Red Cross Fair Photos', images: [{ url: '/mock-images/gallery6.jpg' }, { url: '/mock-images/gallery7.jpg' }] },
{ id: '4', name: 'ภาพวันกองทัพไทย', name_en: 'Thai Armed Forces Day Photos', images: [{ url: '/mock-images/gallery8.jpg' }, { url: '/mock-images/gallery9.jpg' }] },
{ id: '5', name: 'ภาพพิธีสวนสนาม', name_en: 'Parade Ceremony Photos', images: [{ url: '/mock-images/gallery1.jpg' }, { url: '/mock-images/gallery2.jpg' }] },
{ id: '6', name: 'ภาพกิจกรรม CSR', name_en: 'CSR Activities Photos', images: [{ url: '/mock-images/gallery3.jpg' }, { url: '/mock-images/gallery4.jpg' }] },
{ id: '7', name: 'ภาพเยี่ยมชมหน่วยงาน', name_en: 'Unit Visit Photos', images: [{ url: '/mock-images/gallery5.jpg' }, { url: '/mock-images/gallery6.jpg' }] },
{ id: '8', name: 'ภาพการฝึกซ้อม', name_en: 'Training Photos', images: [{ url: '/mock-images/gallery7.jpg' }, { url: '/mock-images/gallery8.jpg' }] },
{ id: '9', name: 'ภาพงานแสดง', name_en: 'Exhibition Photos', images: [{ url: '/mock-images/gallery9.jpg' }, { url: '/mock-images/gallery1.jpg' }] },
];
},
getMockAudioData: () => {
return [
{ id: 'a1', title: 'เพลงมาร์ชกองทัพอากาศ', title_en: 'Royal Thai Air Force March', thumbnail: '/mock-images/default-audio-icon.jpg', file: '/mock-audio/rtaf_march.mp3' },
{ id: 'a2', title: 'เพลงพระราชนิพนธ์', title_en: 'Royal Composition by King Rama IX', thumbnail: '/mock-images/default-audio-icon.jpg', file: '/mock-audio/rtaf_march.mp3' },
{ id: 'a3', title: 'เพลงสรรเสริญพระบารมี', title_en: 'The Highest Dream', thumbnail: '/mock-images/default-audio-icon.jpg', file: '/mock-audio/rtaf_march.mp3' },
{ id: 'a4', title: 'เพลงมหาฤกษ์', title_en: 'Auspicious Song', thumbnail: '/mock-images/default-audio-icon.jpg', file: '/mock-audio/rtaf_march.mp3' },
{ id: 'a5', title: 'เพลงมหาชัย', title_en: 'Victory Song', thumbnail: '/mock-images/default-audio-icon.jpg', file: '/mock-audio/rtaf_march.mp3' },
];
},
// --- สิ้นสุด Mock Data Functions ---
getYoutubeEmbedUrl: (state) => (url) => {
const regex = /(?:v=|\/v\/|\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const match = url.match(regex);
const videoId = match && match[1] ? match[1] : null;
return videoId
? `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0&modestbranding=1&autohide=1&showinfo=0`
: null;
},
getYoutubeThumbnail: (state) => (url) => {
const regex = /(?:v=|\/v\/|\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const match = url.match(regex);
const videoId = match && match[1] ? match[1] : null;
return videoId
? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
: '/images/youtube-default-thumb.jpg';
},
getVideosForDisplay: (state) => {
const sourceVideos = state.videos.length > 0 ? state.videos : state.getMockVideosData;
const videos = sourceVideos.map(v => ({
...v,
embedUrl: state.getYoutubeEmbedUrl(v.rawUrl), // ตรงนี้คือที่สร้าง embedUrl
thumb: state.getYoutubeThumbnail(v.rawUrl),
}));
console.log('getVideosForDisplay output (ตรวจสอบ embedUrl):', videos);
return videos;
},
getGalleriesForDisplay: (state) => {
const sourceGalleries = state.galleries.length > 0 ? state.galleries : state.getMockGalleriesData;
console.log('getGalleriesForDisplay output:', sourceGalleries);
return sourceGalleries;
},
getAudioForDisplay: (state) => {
const sourceAudio = state.audio.length > 0 ? state.audio : state.getMockAudioData;
console.log('getAudioForDisplay output:', sourceAudio);
return sourceAudio;
},
getIsLoadingAny: (state) => {
return state.isLoading.documents || state.isLoading.publications || state.isLoading.videos || state.isLoading.galleries || state.isLoading.audio;
},
getHasError: (state) => {
return state.errors.documents !== null || state.errors.publications !== null || state.errors.videos !== null || state.errors.galleries !== null || state.errors.audio !== null;
},
getMockDocumentsData: () => {
const mockDocuments = [
{ id: 1, name: 'เอกสารเผยแพร่ฉบับที่ 1', name_en: 'Publication Document 1', thumbnail: { url: '/mock-images/doc1-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc1-page1.jpg' }, { url: '/mock-images/doc1-page2.jpg' }, { url: '/mock-images/doc1-page3.jpg' },], description: 'รายละเอียดเอกสารฉบับที่ 1', description_en: 'Details of Publication Document 1', content_approved_date: '2024-01-15T10:00:00Z', active: true, active_en: true, },
{ id: 2, name: 'คู่มือการใช้งานระบบ 1', name_en: 'System User Manual 1', thumbnail: { url: '/mock-images/doc2-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc2-page1.jpg' }, { url: '/mock-images/doc2-page2.jpg' },], description: 'คู่มือการใช้งานระบบสำหรับผู้ใช้', description_en: 'User manual for system operation', content_approved_date: '2024-02-20T11:30:00Z', active: true, active_en: true, },
{ id: 3, name: 'เอกสารเผยแพร่ฉบับที่ 2', name_en: 'Publication Document 2', thumbnail: { url: '/mock-images/doc1-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc1-page1.jpg' }, { url: '/mock-images/doc1-page2.jpg' }, { url: '/mock-images/doc1-page3.jpg' },], description: 'รายละเอียดเอกสารฉบับที่ 1', description_en: 'Details of Publication Document 1', content_approved_date: '2024-01-15T10:00:00Z', active: true, active_en: true, },
{ id: 4, name: 'คู่มือการใช้งานระบบ 2', name_en: 'System User Manual 2', thumbnail: { url: '/mock-images/doc2-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc2-page1.jpg' }, { url: '/mock-images/doc2-page2.jpg' },], description: 'คู่มือการใช้งานระบบสำหรับผู้ใช้', description_en: 'User manual for system operation', content_approved_date: '2024-02-20T11:30:00Z', active: true, active_en: true, },
{ id: 5, 'name': 'เอกสารเผยแพร่ฉบับที่ 3', 'name_en': 'Publication Document 3', thumbnail: { url: '/mock-images/doc1-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc1-page1.jpg' }, { url: '/mock-images/doc1-page2.jpg' }, { url: '/mock-images/doc1-page3.jpg' },], description: 'รายละเอียดเอกสารฉบับที่ 1', description_en: 'Details of Publication Document 1', content_approved_date: '2024-01-15T10:00:00Z', active: true, active_en: true, },
{ id: 6, 'name': 'คู่มือการใช้งานระบบ 3', 'name_en': 'System User Manual 3', thumbnail: { url: '/mock-images/doc2-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc2-page1.jpg' }, { url: '/mock-images/doc2-page2.jpg' },], description: 'คู่มือการใช้งานระบบสำหรับผู้ใช้', description_en: 'User manual for system operation', content_approved_date: '2024-02-20T11:30:00Z', active: true, active_en: true, },
{ id: 7, 'name': 'เอกสารเผยแพร่ฉบับที่ 4', 'name_en': 'Publication Document 4', thumbnail: { url: '/mock-images/doc1-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc1-page1.jpg' }, { url: '/mock-images/doc1-page2.jpg' }, { url: '/mock-images/doc1-page3.jpg' },], description: 'รายละเอียดเอกสารฉบับที่ 1', description_en: 'Details of Publication Document 1', content_approved_date: '2024-01-15T10:00:00Z', active: true, active_en: true, },
{ id: 8, 'name': 'คู่มือการใช้งานระบบ 4', 'name_en': 'System User Manual 4', thumbnail: { url: '/mock-images/doc2-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc2-page1.jpg' }, { url: '/mock-images/doc2-page2.jpg' },], description: 'คู่มือการใช้งานระบบสำหรับผู้ใช้', description_en: 'User manual for system operation', content_approved_date: '2024-02-20T11:30:00Z', active: true, active_en: true, },
{ id: 9, 'name': 'เอกสารเผยแพร่ฉบับที่ 5', 'name_en': 'Publication Document 5', thumbnail: { url: '/mock-images/doc1-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc1-page1.jpg' }, { url: '/mock-images/doc1-page2.jpg' }, { url: '/mock-images/doc1-page3.jpg' },], description: 'รายละเอียดเอกสารฉบับที่ 1', description_en: 'Details of Publication Document 1', content_approved_date: '2024-01-15T10:00:00Z', active: true, active_en: true, },
{ id: 10, 'name': 'คู่มือการใช้งานระบบ 5', 'name_en': 'System User Manual 5', thumbnail: { url: '/mock-images/doc2-thumb.jpg' }, file: { url: '/mock-pdfs/doc1.pdf' }, images: [{ url: '/mock-images/doc2-page1.jpg' }, { url: '/mock-images/doc2-page2.jpg' },], description: 'คู่มือการใช้งานระบบสำหรับผู้ใช้', description_en: 'User manual for system operation', content_approved_date: '2024-02-20T11:30:00Z', active: true, active_en: true, },
];
return mockDocuments;
},
getMockPublicationsData: () => {
const mockPublications = [
{ id: 101, type: 'magazine', Title_TH: 'หนังสือข่าวฮิวแมนเทคไทยแลนด์ ฉบับที่ 1', Title_EN: 'HumanTech Magazine No. 1', thumbnail: { url: '/mock-images/mag1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag1.pdf' }], images: [{ url: '/mock-images/mag1-page1.jpg' }, { url: '/mock-images/mag1-page2.jpg' }, { url: '/mock-images/mag1-page3.jpg' },], Active: true, active_en: true, createdAt: '2024-03-01T09:00:00Z', },
{ id: 102, type: 'magazine', Title_TH: 'หนังสือฮิวแมนเทคไทยแลนด์ ฉบับที่ 2', Title_EN: 'HumanTech Magazine No. 2', thumbnail: { url: '/mock-images/mag2-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag2.pdf' }], images: [{ url: '/mock-images/mag2-page1.jpg' }, { url: '/mock-images/mag2-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-04-05T14:00:00Z', },
{ id: 201, type: 'dailynews', Title_TH: 'ข่าวประจำวันฮิวแมนเทคไทยแลนด์ ฉบับ 100', Title_EN: 'HumanTech Daily News No. 100', thumbnail: { url: '/mock-images/daily1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/daily1.pdf' }], images: [{ url: '/mock-images/daily1-page1.jpg' }, { url: '/mock-images/daily1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-05-10T08:00:00Z', },
{ id: 301, type: 'Journal', Title_TH: 'สารชาวฮิวแมนเทคไทยแลนด์ ปีที่ 50 ฉบับที่ 1', Title_EN: 'HumanTech Journal Vol. 50 No. 1', thumbnail: { url: '/mock-images/journal1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/journal1.pdf' }], images: [{ url: '/mock-images/journal1-page1.jpg' }, { url: '/mock-images/journal1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-06-01T10:00:00Z', },
{ id: 103, type: 'magazine', Title_TH: 'หนังสือข่าวฮิวแมนเทคไทยแลนด์ ฉบับที่ 3', Title_EN: 'HumanTech Magazine No. 1', thumbnail: { url: '/mock-images/mag1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag1.pdf' }], images: [{ url: '/mock-images/mag1-page1.jpg' }, { url: '/mock-images/mag1-page2.jpg' }, { url: '/mock-images/mag1-page3.jpg' },], Active: true, active_en: true, createdAt: '2024-03-01T09:00:00Z', },
{ id: 104, type: 'magazine', Title_TH: 'หนังสือฮิวแมนเทคไทยแลนด์ ฉบับที่ 4', Title_EN: 'HumanTech Magazine No. 2', thumbnail: { url: '/mock-images/mag2-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag2.pdf' }], images: [{ url: '/mock-images/mag2-page1.jpg' }, { url: '/mock-images/mag2-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-04-05T14:00:00Z', },
{ id: 202, type: 'dailynews', Title_TH: 'ข่าวประจำวันฮิวแมนเทคไทยแลนด์ ฉบับ 101', Title_EN: 'HumanTech Daily News No. 100', thumbnail: { url: '/mock-images/daily1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/daily1.pdf' }], images: [{ url: '/mock-images/daily1-page1.jpg' }, { url: '/mock-images/daily1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-05-10T08:00:00Z', },
{ id: 302, type: 'Journal', Title_TH: 'สารชาวฮิวแมนเทคไทยแลนด์ ปีที่ 50 ฉบับที่ 2', Title_EN: 'HumanTech Journal Vol. 50 No. 1', thumbnail: { url: '/mock-images/journal1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/journal1.pdf' }], images: [{ url: '/mock-images/journal1-page1.jpg' }, { url: '/mock-images/journal1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-06-01T10:00:00Z', },
{ id: 105, type: 'magazine', Title_TH: 'หนังสือข่าวฮิวแมนเทคไทยแลนด์ ฉบับที่ 5', Title_EN: 'HumanTech Magazine No. 1', thumbnail: { url: '/mock-images/mag1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag1.pdf' }], images: [{ url: '/mock-images/mag1-page1.jpg' }, { url: '/mock-images/mag1-page2.jpg' }, { url: '/mock-images/mag1-page3.jpg' },], Active: true, active_en: true, createdAt: '2024-03-01T09:00:00Z', },
{ id: 106, type: 'magazine', Title_TH: 'หนังสือฮิวแมนเทคไทยแลนด์ ฉบับที่ 6', Title_EN: 'HumanTech Magazine No. 2', thumbnail: { url: '/mock-images/mag2-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag2.pdf' }], images: [{ url: '/mock-images/mag2-page1.jpg' }, { url: '/mock-images/mag2-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-04-05T14:00:00Z', },
{ id: 203, type: 'dailynews', Title_TH: 'ข่าวประจำวันฮิวแมนเทคไทยแลนด์ ฉบับ 102', Title_EN: 'HumanTech Daily News No. 100', thumbnail: { url: '/mock-images/daily1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/daily1.pdf' }], images: [{ url: '/mock-images/daily1-page1.jpg' }, { url: '/mock-images/daily1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-05-10T08:00:00Z', },
{ id: 303, type: 'Journal', Title_TH: 'สารชาวฮิวแมนเทคไทยแลนด์ ปีที่ 50 ฉบับที่ 3', Title_EN: 'HumanTech Journal Vol. 50 No. 1', thumbnail: { url: '/mock-images/journal1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/journal1.pdf' }], images: [{ url: '/mock-images/journal1-page1.jpg' }, { url: '/mock-images/journal1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-06-01T10:00:00Z', },
{ id: 107, type: 'magazine', Title_TH: 'หนังสือข่าวฮิวแมนเทคไทยแลนด์ ฉบับที่ 7', Title_EN: 'HumanTech Magazine No. 1', thumbnail: { url: '/mock-images/mag1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag1.pdf' }], images: [{ url: '/mock-images/mag1-page1.jpg' }, { url: '/mock-images/mag1-page2.jpg' }, { url: '/mock-images/mag1-page3.jpg' },], Active: true, active_en: true, createdAt: '2024-03-01T09:00:00Z', },
{ id: 108, type: 'magazine', Title_TH: 'หนังสือฮิวแมนเทคไทยแลนด์ ฉบับที่ 8', Title_EN: 'HumanTech Magazine No. 2', thumbnail: { url: '/mock-images/mag2-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/mag2.pdf' }], images: [{ url: '/mock-images/mag2-page1.jpg' }, { url: '/mock-images/mag2-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-04-05T14:00:00Z', },
{ id: 204, type: 'dailynews', Title_TH: 'ข่าวประจำวันฮิวแมนเทคไทยแลนด์ ฉบับ 103', Title_EN: 'HumanTech Daily News No. 100', thumbnail: { url: '/mock-images/daily1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/daily1.pdf' }], images: [{ url: '/mock-images/daily1-page1.jpg' }, { url: '/mock-images/daily1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-05-10T08:00:00Z', },
{ id: 304, type: 'Journal', Title_TH: 'สารชาวฮิวแมนเทคไทยแลนด์ ปีที่ 50 ฉบับที่ 4', Title_EN: 'HumanTech Journal Vol. 50 No. 1', thumbnail: { url: '/mock-images/journal1-thumb.jpg' }, Attachment_TH: [{ url: '/mock-pdfs/journal1.pdf' }], images: [{ url: '/mock-images/journal1-page1.jpg' }, { url: '/mock-images/journal1-page2.jpg' },], Active: true, active_en: true, createdAt: '2024-06-01T10:00:00Z', },
];
return mockPublications;
},
getFilteredDocuments: (state) => {
const appStore = useAppStore();
const allDocs = state.allRawDocuments.length > 0 ? state.allRawDocuments : state.getMockDocumentsData;
if (appStore.isTh) { // <--- ใช้ appStore.isTh
return allDocs.filter(doc => doc.active);
} else {
return allDocs.filter(doc => doc.active_en);
}
},
getFilteredPublications: (state) => {
const appStore = useAppStore();
let filteredData = state.allRawPublications.length > 0 ? state.allRawPublications : state.getMockPublicationsData;
if (appStore.isTh) { // <--- ใช้ appStore.isTh
filteredData = filteredData.filter(pub => pub.Active);
} else {
filteredData = filteredData.filter(pub => pub.active_en);
}
filteredData.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return filteredData;
},
getMagazinePublications: (state) => {
return state.getFilteredPublications.filter(pub => pub.type === 'magazine');
},
getDailyNewsPublications: (state) => {
return state.getFilteredPublications.filter(pub => pub.type === 'dailynews');
},
getJournalPublications: (state) => {
return state.getFilteredPublications.filter(pub => pub.type === 'Journal');
},
getCombinedPdfData: (state) => {
const allDocsPdfData = state.allRawDocuments.map(doc => ({
url: doc.thumbnail.url,
collection: "documents",
link: doc.file.url,
title_th: doc.name,
title_en: doc.name_en,
detail_th: doc.description,
detail_en: doc.description_en,
publishdate: doc.content_approved_date,
}));
const allPubsPdfData = state.allRawPublications.map(pub => ({
url: pub.thumbnail.url,
collection: pub.type,
link: pub.Attachment_TH[0].url,
title_th: pub.Title_TH,
title_en: pub.Title_EN,
}));
return [...allDocsPdfData, ...allPubsPdfData];
}
},
actions: {
async fetchDocuments() {
this.allRawDocuments = this.getMockDocumentsData;
},
async fetchPublications() {
const rawPublications = this.getMockPublicationsData;
this.allRawPublications = rawPublications;
const journal = rawPublications.filter(pub => pub.type === 'Journal');
const dailynews = rawPublications.filter(pub => pub.type === 'dailynews');
const magazine = rawPublications.filter(pub => pub.type === 'magazine');
// คืนค่าเป็น object ที่มี properties ตรงกับที่ JournalView.vue คาดหวัง
return {
journal,
dailynews,
magazine,
};
},
updatePdfDataState() {
// **แก้ไข: ลบ () ออก**
this.pdfData = this.getCombinedPdfData;
},
async fetchVideos() {
this.isLoading.videos = true;
this.errors.videos = null;
try {
await this.simulateDelay();
this.videos = [
{ id: '1', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'การฝึก Cobra Gold 2024', title_en: 'Cobra Gold 2024 Exercise' },
{ id: '2', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'เครื่องบินขับไล่ F-16 ของกองทัพอากาศ', title_en: 'Royal Thai Air Force F-16 Fighter Jet' },
{ id: '3', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'ภารกิจช่วยเหลือผู้ประสบภัย', title_en: 'Disaster Relief Mission' },
{ id: '4', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'ประวัติกองทัพอากาศไทย', title_en: 'History of Royal Thai Air Force' },
{ id: '5', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'การแสดงแสนยานุภาพทางอากาศ', title_en: 'Air Power Demonstration' },
{ id: '6', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'ชีวิตนักบินขับไล่', title_en: 'Life of a Fighter Pilot' },
{ id: '7', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'เทคโนโลยีอากาศยานไร้คนขับ', title_en: 'UAV Technology' },
{ id: '8', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'วันเด็กแห่งชาติ กองทัพอากาศ', title_en: 'Children\'s Day at RTAF' },
{ id: '9', rawUrl: 'https://www.youtube.com/watch?v=ARzfFQZms7Y', title: 'การซ้อมรบร่วม', title_en: 'Joint Military Exercise' }
];
} catch (error) {
this.errors.videos = 'Failed to fetch videos.';
console.error('Error fetching videos:', error);
} finally {
this.isLoading.videos = false;
}
},
async fetchGalleries() {
this.isLoading.galleries = true;
this.errors.galleries = null;
try {
await this.simulateDelay();
this.galleries = [
{ id: '1', name: 'ภาพการฝึก Cobra Gold', name_en: 'Cobra Gold Training Photos', images: [{ url: '/mock-images/gallery1.jpg' }] },
{ id: '2', name: 'พิธีสวนสนามทางอากาศ', name_en: 'Air Parade Ceremony', images: [{ url: '/mock-images/gallery2.jpg' }] },
{ id: '3', name: 'เยี่ยมชมฐานทัพอากาศ', name_en: 'Air Base Visit', images: [{ url: '/mock-images/gallery3.jpg' }] },
{ id: '4', name: 'กิจกรรมวันเด็ก', name_en: 'Children\'s Day Activities', images: [{ url: '/mock-images/gallery4.jpg' }] },
{ id: '5', name: 'เครื่องบินใหม่ของกองทัพ', name_en: 'New Aircraft of the Air Force', images: [{ url: '/mock-images/gallery5.jpg' }] },
{ id: '6', name: 'การช่วยเหลือประชาชน', name_en: 'Public Assistance', images: [{ url: '/mock-images/gallery6.jpg' }] },
{ id: '7', name: 'การฝึกโดดร่ม', name_en: 'Parachute Training', images: [{ url: '/mock-images/gallery7.jpg' }] },
{ id: '8', name: 'งานแสดงการบิน', name_en: 'Air Show', images: [{ url: '/mock-images/gallery1.jpg' }] },
{ id: '9', name: 'เบื้องหลังการทำงาน', name_en: 'Behind the Scenes', images: [{ url: '/mock-images/gallery2.jpg' }] },
];
} catch (error) {
this.errors.galleries = 'Failed to fetch galleries.';
console.error('Error fetching galleries:', error);
} finally {
this.isLoading.galleries = false;
}
},
// NEW: Action to fetch audio data
async fetchAudio() {
this.isLoading.audio = true;
this.errors.audio = null;
try {
await this.simulateDelay();
this.audio = [
{ id: 'a1', title: 'เพลงมาร์ชกองทัพอากาศ', title_en: 'Royal Thai Air Force March', file: '/mock-audio/rtaf_march.mp3', thumbnail: '/images/default-audio-icon.png' },
{ id: 'a2', title: 'เพลงพระราชนิพนธ์ในรัชกาลที่ 9', title_en: 'Royal Composition by King Rama IX', file: '/mock-audio/rtaf_march.mp3', thumbnail: '/images/default-audio-icon.png' },
{ id: 'a3', title: 'เพลงความฝันอันสูงสุด', title_en: 'The Highest Dream', file: '/mock-audio/rtaf_march.mp3', thumbnail: '/images/default-audio-icon.png' },
{ id: 'a4', title: 'เพลงเทิดทูนสถาบัน', title_en: 'Honoring the Institution', file: '/mock-audio/rtaf_march.mp3', thumbnail: '/images/default-audio-icon.png' },
{ id: 'a5', title: 'เพลงบรรเลงไทยเดิม', title_en: 'Traditional Thai Instrumental Music', file: '/mock-audio/rtaf_march.mp3', thumbnail: '/images/default-audio-icon.png' },
];
} catch (error) {
this.errors.audio = 'Failed to fetch audio.';
console.error('Error fetching audio:', error);
} finally {
this.isLoading.audio = false;
}
},
async simulateDelay() {
return new Promise(resolve => setTimeout(resolve, 500));
},
},
});

101
src/views/AllNewsView.vue Normal file
View File

@ -0,0 +1,101 @@
// src/views/AllNewsView.vue
<template>
<div class="mt-6">
<div
class="p-4 bg-cover bg-center"
:style="`background-image: url('${appStore.imageBaseUrl}${appStore.headers.header_background.url}')`"
>
<div class="container mx-auto">
<h4
class="text-2xl md:text-3xl font-bold text-gray-900 mb-6 border-b pb-4"
style="background-color: transparent !important;"
>
{{ appStore.checkLang.isTh ? 'เรื่องราวดี ๆ ที่เราอยากบอกต่อ' : 'Our Inspiring Stories' }}
</h4>
</div>
</div>
<div class="p-6" :style="`background-color:${appStore.headers.bgColor || '#ffffff'}`">
<div class="container mx-auto"> <div v-if="displayedNews.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="item in displayedNews" :key="item.id" class="flex flex-col">
<NewsItem :item="item" />
</div>
</div>
<div v-else class="text-center text-gray-500 py-10">
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวสาร' : 'No news found' }}
</div>
<div v-if="hasMoreNews" class="flex justify-center mt-8">
<button
@click="loadMoreNews"
class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
>
{{ appStore.checkLang.isTh ? 'โหลดข่าวเพิ่ม' : 'Load More News' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import NewsItem from '@/components/NewsItem.vue';
import { useAppStore } from '@/stores/app';
const appStore = useAppStore();
const displayedNews = ref([]);
const currentPage = ref(1);
const newsPerLoad = 6; // appStore.find
const fetchNews = async () => {
const fetchedData = await appStore.find(
'news',
`_limit=${newsPerLoad}&_page=${currentPage.value}&feature=false`
);
const currentFetchedNews = Array.isArray(fetchedData) ? fetchedData : [];
if (currentPage.value === 1) {
displayedNews.value = currentFetchedNews; // currentFetchedNews
} else {
displayedNews.value = [...displayedNews.value, ...currentFetchedNews]; // currentFetchedNews
}
// console.log(`[AllNewsView] - displayedNews updated. Length: ${displayedNews.value.length}`); // Commented out to clean up console
};
const loadMoreNews = () => {
currentPage.value++;
fetchNews();
};
const hasMoreNews = computed(() => {
const currentDisplayedLength = displayedNews.value.length;
const totalFromStore = appStore.totalNewsCount;
const result = currentDisplayedLength < totalFromStore;
// console.log(`[AllNewsView - computed hasMoreNews] - displayedNews.length: ${currentDisplayedLength}, appStore.totalNewsCount (direct): ${totalFromStore}, Result: ${result}`); // Commented out to clean up console
return result;
});
onMounted(() => {
fetchNews();
});
// Watch ( debugging )
// watch(() => appStore.totalNewsCount, (newValue, oldValue) => {
// console.log(`[AllNewsView - Watch appStore.totalNewsCount] - totalNewsCount changed from ${oldValue} to ${newValue}`);
// }, { immediate: true });
watch(() => appStore.checkLang.isTh, () => {
currentPage.value = 1;
displayedNews.value = []; //
fetchNews();
});
</script>
<style scoped>
/* คุณสามารถเพิ่ม CSS เฉพาะส่วนนี้ได้ถ้าจำเป็น */
</style>

View File

@ -2,15 +2,16 @@
<template>
<div class="container mx-auto p-4 md:p-8">
<div v-if="newsItem" class="bg-white shadow-lg rounded-lg overflow-hidden">
<div class="relative h-64 md:h-96">
<div class="relative w-full h-64 md:h-96 overflow-hidden">
<img
v-if="newsItem.image && newsItem.image.url"
:src="`${appStore.imageBaseUrl}${newsItem.image.url}`"
:alt="appStore.checkLang.isTh ? newsItem.title_th : newsItem.title_en"
class="w-full h-full object-cover"
class="w-full h-full object-cover object-center"
loading="lazy"
/>
<div v-else class="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500">
No Image Available
<div v-else class="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500 text-lg">
{{ appStore.checkLang.isTh ? 'ไม่มีรูปภาพ' : 'No Image Available' }}
</div>
</div>
@ -21,19 +22,31 @@
<p class="text-sm text-gray-500 mb-6">
<span v-if="newsItem.release_date">
{{ appStore.checkLang.isTh ? 'เผยแพร่เมื่อ' : 'Published on' }}:
{{ formatDate(newsItem.release_date) }}
<span class="font-medium text-gray-700">{{ formatDate(newsItem.release_date) }}</span>
</span>
</p>
<div class="prose max-w-none text-gray-800 leading-relaxed break-words whitespace-pre-line">
{{ appStore.checkLang.isTh ? newsItem.detail_th : newsItem.detail_en }}
<div class="prose lg:prose-lg max-w-3xl mx-auto text-gray-800 leading-relaxed break-words">
<div v-html="appStore.checkLang.isTh ? newsItem.detail_th : newsItem.detail_en"></div>
</div>
<div class="mt-8 pt-4 border-t border-gray-200 flex justify-end">
<button
@click="shareNews"
class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
>
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M15 8a3 3 0 10-2.977-2.909L8 7.239V4.75a3 3 0 00-3-3H4.75a3 3 0 00-3 3v.475a3 3 0 003 3V12h-.25a2.75 2.75 0 000 5.5h.25a2.75 2.75 0 000-5.5H4.75a.75.75 0 010-1.5h.25a.75.75 0 01.75.75v.25c0 .285.068.558.196.804l3.153-1.636a3 3 0 003.545-5.908L15 8zM5 4.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zm9.5 9.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 6a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"></path></svg>
{{ appStore.checkLang.isTh ? 'แชร์' : 'Share' }}
</button>
</div>
</div>
</div>
<div v-else class="text-center text-gray-600 py-16">
<p class="text-2xl font-semibold mb-4">
{{ appStore.checkLang.isTh ? 'ไม่พบข่าวสารนี้ หรือข่าวสารนี้ไม่พร้อมใช้งานในภาษาปัจจุบัน' : 'News not found or not available in the current language.' }}
</p>
<router-link to="/home" class="btn btn-primary">
<router-link to="/home" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md">
{{ appStore.checkLang.isTh ? 'กลับหน้าหลัก' : 'Back to Home' }}
</router-link>
</div>
@ -41,28 +54,44 @@
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router'; // route parameters
import { ref, onMounted, watch, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useAppStore } from '@/stores/app.js';
import { RouterLink } from 'vue-router'; //
import { RouterLink } from 'vue-router';
const appStore = useAppStore();
const route = useRoute(); // Instance useRoute
const newsItem = ref(null); //
const route = useRoute();
const newsItem = ref(null);
// Computed property Breadcrumbs ()
// ContentView Breadcrumbs
const categoryTitleTh = computed(() => {
if (newsItem.value && appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
const tab = appStore.tabs.find(t => t.category === newsItem.value.type);
return tab ? tab.title : 'ไม่ระบุหมวดหมู่';
}
return appStore.checkLang.isTh ? 'กำลังโหลด...' : 'Loading...';
});
const categoryTitleEn = computed(() => {
if (newsItem.value && appStore.tabs && Array.isArray(appStore.tabs) && appStore.tabs.length > 0) {
const tab = appStore.tabs.find(t => t.category === newsItem.value.type);
return tab ? tab.title_en : 'Unspecified Category';
}
return appStore.checkLang.isTh ? 'Loading...' : 'Loading...';
});
// Function to fetch news detail by ID
const fetchNewsDetail = async (id) => {
// API await axios.get(`/api/news/${id}`);
// mock data store
const foundNews = appStore.mockNews.find(item => item.id == id); // == string/number
const foundNews = appStore.mockNews.find(item => item.id == id); // mockNews mockNewsData
if (foundNews) {
// active active_en
if (appStore.isTh && foundNews.active) {
if (appStore.checkLang.isTh && foundNews.active) { // appStore.checkLang.isTh
newsItem.value = foundNews;
} else if (!appStore.isTh && foundNews.active_en) { // !appStore.isTh
} else if (!appStore.checkLang.isTh && foundNews.active_en) { // !appStore.checkLang.isTh
newsItem.value = foundNews;
} else {
newsItem.value = null; // active
newsItem.value = null;
}
} else {
newsItem.value = null;
@ -71,8 +100,9 @@ const fetchNewsDetail = async (id) => {
const formatDate = (dateString) => {
if (!dateString) return '';
const parts = dateString.split('/'); // Assumes MM/DD/YYYY
const parts = dateString.split('/');
if (parts.length === 3) {
// Note: Month is 0-indexed in JavaScript Date objects
const [month, day, year] = parts;
const dateObj = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
const formattedDay = String(dateObj.getDate()).padStart(2, '0');
@ -83,32 +113,78 @@ const formatDate = (dateString) => {
return dateString;
};
// Component mount
// *** Share ***
const shareNews = async () => {
if (!newsItem.value) return;
const title = appStore.checkLang.isTh ? newsItem.value.title_th : newsItem.value.title_en;
// window.location.href URL
// URL
const url = window.location.href;
if (navigator.share) { // Web Share API
try {
await navigator.share({
title: title,
url: url,
// text: ' (optional)'
});
console.log('News shared successfully!');
} catch (error) {
console.error('Error sharing news:', error);
//
alert(appStore.checkLang.isTh ? 'ไม่สามารถแชร์ได้ในขณะนี้' : 'Could not share at this moment.');
}
} else {
// Fallback Web Share API
// Social Media
//
console.warn('Web Share API not supported. Providing fallback options.');
// :
try {
await navigator.clipboard.writeText(url);
alert(appStore.checkLang.isTh ? 'คัดลอกลิงก์ข่าวแล้ว!' : 'News link copied to clipboard!');
} catch (err) {
console.error('Failed to copy link: ', err);
alert(appStore.checkLang.isTh ? 'ไม่สามารถคัดลอกลิงก์ได้' : 'Failed to copy link.');
}
}
};
onMounted(() => {
fetchNewsDetail(route.params.id);
});
// Watch for changes in route.params.id ( Link )
watch(() => route.params.id, (newId) => {
fetchNewsDetail(newId);
});
// Watch for language changes and re-fetch the news detail
watch(() => appStore.isTh, () => {
// appStore.checkLang.isTh appStore.isTh
watch(() => appStore.checkLang.isTh, () => {
fetchNewsDetail(route.params.id);
});
</script>
<style scoped>
/* หากใช้ DaisyUI/TailwindCSS ก็ไม่จำเป็นต้องมี style scoped มากนัก */
.prose {
/* Tailwind CSS's @tailwindcss/typography plugin provides 'prose' class for nicely formatted text.
If you haven't installed it, these styles can be adjusted manually or by adding the plugin. */
max-width: 80ch; /* Limit line length for readability */
max-width: 70ch;
margin-left: auto;
margin-right: auto;
}
.whitespace-pre-line {
white-space: pre-line; /* Keeps line breaks from your data */
.prose img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.object-cover {
object-fit: cover;
object-position: center;
}
</style>

View File

@ -1,72 +0,0 @@
<template>
<div class="p-4 md:p-8">
<h1 class="text-4xl font-bold text-center mb-8">Dashboard Overview</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<div class="col-span-1">
<h2 class="text-2xl font-semibold mb-4">World News</h2>
<div class="card bg-base-100 shadow-xl p-4">
<p>This is where your World News panel would go, converted from NewsPanel.jsx.</p>
<img src="/images/Welcome_Page_680728.jpg" alt="News Video" class="w-full h-auto mt-4 rounded-lg">
<p class="text-sm text-gray-500 mt-2">Example video thumbnail from World News section.</p>
</div>
</div>
<div class="col-span-1">
<h2 class="text-2xl font-semibold mb-4">World Statistics</h2>
<div class="card bg-base-100 shadow-xl p-4">
<p>This is where your Statistics panel would go, converted from StatsPanel.jsx.</p>
<ul class="list-disc list-inside mt-4">
<li>Market Review</li>
<li>Currencies</li>
<li>Population</li>
<li>Energy</li>
<li>Russian Economy</li>
</ul>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="col-span-1">
<h2 class="text-2xl font-semibold mb-4">Active Satellites</h2>
<div class="card bg-base-300 shadow-xl p-4 min-h-[400px] flex items-center justify-center">
<p class="text-lg text-gray-500">
[3D Globe Simulation Placeholder - Convert EarthBody.jsx, GlobeCanvas.jsx, etc. here]
</p>
<img src="/images/Welcome_Page_680728.jpg" alt="3D Globe" class="absolute w-full h-full object-cover opacity-70">
</div>
</div>
<div class="col-span-1">
<h2 class="text-2xl font-semibold mb-4">Satellite Details</h2>
<div class="card bg-base-100 shadow-xl p-4">
<ul class="list-disc list-inside">
<li>ISS (1.05R, 51°)</li>
<li>Hubble Space Telescope (1.07R, 28.5°)</li>
<li>Starlink-123 (1.10R, 53.0°)</li>
</ul>
<p class="text-sm text-gray-600 mt-4">
Details for active satellites would be displayed here, possibly from PopUpDetail.jsx.
</p>
<div class="mt-4 p-2 bg-base-200 rounded">
<p><strong>Hubble Space Telescope:</strong> </p>
<p class="text-sm">The Hubble Space Telescope is a space telescope that was launched into low Earth orbit in 1990 and remains in operation.</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// import NewsPanel from '@/components/NewsPanel.vue';
// import StatsPanel from '@/components/StatsPanel.vue';
// import GlobeCanvas from '@/components/GlobeCanvas.vue'; // This and related components need careful conversion
// No specific data or logic needed for this mock view,
// as it primarily contains placeholders for other components.
</script>
<style scoped>
/* Specific styles for DashboardView if needed */
</style>

View File

@ -51,7 +51,10 @@
</div>
<div class="mt-8 pt-4 border-t border-gray-200 flex justify-end">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md">
<button
@click="shareNews"
class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md"
>
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M15 8a3 3 0 10-2.977-2.909L8 7.239V4.75a3 3 0 00-3-3H4.75a3 3 0 00-3 3v.475a3 3 0 003 3V12h-.25a2.75 2.75 0 000 5.5h.25a2.75 2.75 0 000-5.5H4.75a.75.75 0 010-1.5h.25a.75.75 0 01.75.75v.25c0 .285.068.558.196.804l3.153-1.636a3 3 0 003.545-5.908L15 8zM5 4.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zm9.5 9.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM15 6a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"></path></svg>
{{ appStore.checkLang.isTh ? 'แชร์' : 'Share' }}
</button>
@ -101,13 +104,16 @@ const categoryTitleEn = computed(() => {
const fetchNewsDetail = async (id) => {
// API await axios.get(`/api/news/${id}`);
// mock data store
// : appStore.mockNewsData original code
// appStore.mockNews appStore.mockNews
const foundNews = appStore.mockNewsData.find(item => item.id == id);
if (foundNews) {
// active active_en
if (appStore.isTh && foundNews.active) {
// appStore.checkLang.isTh
if (appStore.checkLang.isTh && foundNews.active) {
newsItem.value = foundNews;
} else if (!appStore.isTh && foundNews.active_en) {
} else if (!appStore.checkLang.isTh && foundNews.active_en) {
newsItem.value = foundNews;
} else {
newsItem.value = null; // active
@ -122,6 +128,7 @@ const formatDate = (dateString) => {
const parts = dateString.split('/'); // Assumes MM/DD/YYYY
if (parts.length === 3) {
const [month, day, year] = parts;
// Month in Date constructor is 0-indexed (0 for January, 11 for December)
const dateObj = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
const formattedDay = String(dateObj.getDate()).padStart(2, '0');
const formattedMonth = String(dateObj.getMonth() + 1).padStart(2, '0');
@ -131,6 +138,46 @@ const formatDate = (dateString) => {
return dateString;
};
// *** ***
const shareNews = async () => {
if (!newsItem.value) return; //
const title = appStore.checkLang.isTh ? newsItem.value.title_th : newsItem.value.title_en;
// window.location.href URL
// URL
const url = window.location.href;
// URL canonical ()
// appStore.baseUrl + route.fullPath
// const shareUrl = `${appStore.baseUrl}${route.fullPath}`;
if (navigator.share) { // Web Share API
try {
await navigator.share({
title: title,
url: url,
// text: '!' // (optional)
});
console.log('ข่าวถูกแชร์สำเร็จ!');
} catch (error) {
console.error('เกิดข้อผิดพลาดในการแชร์ข่าว:', error);
//
alert(appStore.checkLang.isTh ? 'ไม่สามารถแชร์ได้ในขณะนี้' : 'Could not share at this moment.');
}
} else {
// Fallback Web Share API
//
console.warn('Web Share API ไม่รองรับ, คัดลอกลิงก์ไปยังคลิปบอร์ดแทน');
try {
await navigator.clipboard.writeText(url);
alert(appStore.checkLang.isTh ? 'คัดลอกลิงก์ข่าวแล้ว!' : 'News link copied to clipboard!');
} catch (err) {
console.error('ไม่สามารถคัดลอกลิงก์ได้:', err);
alert(appStore.checkLang.isTh ? 'ไม่สามารถคัดลอกลิงก์ได้' : 'Failed to copy link.');
}
}
};
onMounted(() => {
fetchNewsDetail(route.params.id);
});
@ -139,7 +186,9 @@ watch(() => route.params.id, (newId) => {
fetchNewsDetail(newId);
});
watch(() => appStore.isTh, () => {
// ()
// appStore.checkLang.isTh appStore.isTh
watch(() => appStore.checkLang.isTh, () => {
fetchNewsDetail(route.params.id);
});
</script>

View File

@ -0,0 +1,240 @@
// src/views/about/CommanderBioView.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.checkLang.isTh ? title_th : title_en }}
</h1>
</div>
<div v-if="isLoading" class="text-center py-20">
<span class="loading loading-spinner loading-lg text-blue-500"></span>
<p class="mt-4 text-xl text-gray-500">{{ appStore.checkLang.isTh ? 'กำลังโหลดข้อมูล...' : 'Loading data...' }}</p>
</div>
<div v-else-if="error" class="text-center text-red-600 py-20">
<p class="text-xl font-medium">{{ appStore.checkLang.isTh ? 'เกิดข้อผิดพลาดในการโหลดข้อมูลผู้บริหาร' : 'Error loading commander data.' }}</p>
<p class="text-sm mt-2">
{{ appStore.checkLang.isTh ? 'โปรดตรวจสอบ ID หรือลองใหม่อีกครั้ง' : 'Please check the ID or try again.' }}
</p>
</div>
<div v-else-if="commander" class="flex justify-center">
<div class="card w-full md:w-4/5 lg:w-3/4 bg-base-100 shadow-xl rounded-box overflow-hidden">
<div class="card-body p-6 md:p-8 text-center flex flex-col items-center">
<template v-if="shouldShowImage(commander.image)">
<img
:src="getImageUrl(commander.image)"
:alt="appStore.checkLang.isTh ? commander.name_th : commander.name_en"
class="w-48 h-48 object-cover rounded-lg shadow-lg border-4 border-blue-300 mb-6"
@error="onImageError(commander.id)"
/>
</template>
<template v-else>
<div class="w-48 h-48 rounded-lg shadow-lg border-4 border-gray-200 bg-gray-100 flex items-center justify-center mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-28 w-28 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
</div>
</template>
<h2 class="card-title text-3xl font-bold text-gray-900 mb-2">
{{ appStore.checkLang.isTh ? commander.name_th : commander.name_en }}
</h2>
<div class="text-xl text-gray-700 mb-6 prose max-w-none"
v-html="appStore.checkLang.isTh ? commander.bio_th : commander.bio_en">
</div>
</div>
<div class="p-6 md:p-8 border-t border-gray-200 bg-gray-50">
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8">
<div>
<h3 class="text-lg font-semibold text-blue-700 mb-2">{{ appStore.checkLang.isTh ? education_th : education_en }}</h3>
<ul class="list-disc list-inside text-gray-700 space-y-1">
<li v-for="(item, i) in getFilteredDetails(appStore.checkLang.isTh ? 'การศึกษา' : 'Education')" :key="`edu-${i}`">
{{ appStore.checkLang.isTh ? item.title_th : item.title_en }}
</li>
<li v-if="getFilteredDetails(appStore.checkLang.isTh ? 'การศึกษา' : 'Education').length === 0" class="text-gray-500 italic">
{{ appStore.checkLang.isTh ? 'ไม่มีข้อมูลการศึกษา' : 'No education information available.' }}
</li>
</ul>
</div>
<div>
<h3 class="text-lg font-semibold text-blue-700 mb-2">{{ appStore.checkLang.isTh ? position_th : position_en }}</h3>
<ul class="list-disc list-inside text-gray-700 space-y-1">
<li v-for="(item, i) in getFilteredDetails(appStore.checkLang.isTh ? 'ตำแหน่งที่สำคัญ' : 'Important Positions')" :key="`pos-${i}`">
{{ appStore.checkLang.isTh ? item.title_th : item.title_en }}
</li>
<li v-if="getFilteredDetails(appStore.checkLang.isTh ? 'ตำแหน่งที่สำคัญ' : 'Important Positions').length === 0" class="text-gray-500 italic">
{{ appStore.checkLang.isTh ? 'ไม่มีข้อมูลตำแหน่งที่สำคัญ' : 'No important positions information available.' }}
</li>
</ul>
</div>
</div>
</div>
<div class="card-actions justify-end p-6 border-t border-gray-200">
<router-link to="/about/commander" class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md">
{{ appStore.checkLang.isTh ? 'กลับไปยังผู้บริหารระดับสูง' : 'Back to Senior Executives' }}
</router-link>
</div>
</div>
</div>
<div v-else class="text-center text-gray-400 py-16">
<p class="text-xl">{{ appStore.checkLang.isTh ? 'ไม่พบข้อมูลผู้บริหารท่านนี้' : 'Commander not found.' }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useAppStore } from '@/stores/app.js';
import { useAboutUsStore } from '@/stores/aboutUsStore.js';
const appStore = useAppStore();
const aboutUsStore = useAboutUsStore();
const route = useRoute();
const commanderId = ref(null);
const commander = ref(null);
const isLoading = ref(true);
const error = ref(null);
const imageLoadErrors = ref({});
const title_th = "ประวัติผู้บริหาร";
const title_en = "Commander's Biography";
const education_th = "การศึกษา";
const education_en = "Education";
const position_th = "ตำแหน่งที่สำคัญ";
const position_en = "Important Positions";
const fetchCommanderData = async (id) => {
isLoading.value = true;
error.value = null;
commander.value = null; // Clear previous data
console.log('--- fetchCommanderData started ---');
console.log('Fetching for ID:', id);
try {
// commanders
if (!aboutUsStore.getCommandersForDisplay || aboutUsStore.getCommandersForDisplay.length === 0) {
console.log('Commanders data not in store, fetching...');
await aboutUsStore.fetchCommanders(); //
console.log('Commanders fetched. Total:', aboutUsStore.getCommandersForDisplay.length);
} else {
console.log('Commanders data already in store. Total:', aboutUsStore.getCommandersForDisplay.length);
}
// Store
const parsedId = parseInt(id);
console.log('Parsed ID:', parsedId);
// **:** .attributes
// ID
const foundCommander = aboutUsStore.getCommandersForDisplay.find(
(p) => {
// Store .attributes p.attributes.id
// p.attributes.id === parsedId
// console.log p.id
console.log(`Comparing p.id (${p.id}) with parsedId (${parsedId})`);
return p.id === parsedId;
}
);
if (foundCommander) {
commander.value = foundCommander;
console.log('Commander found:', commander.value);
} else {
error.value = new Error('Commander not found');
console.warn('Commander not found for ID:', id);
}
} catch (err) {
error.value = err;
console.error("Error fetching commander bio:", err);
} finally {
isLoading.value = false;
console.log('--- fetchCommanderData finished ---');
}
};
// **:** Computed property category
const getFilteredDetails = (category) => {
// commander.value .details ( .attributes.details)
if (!commander.value || !commander.value.details) { // <---
console.log('getFilteredDetails: commander or details not available, returning empty array.');
return [];
}
// category_th category_en
return commander.value.details.filter(item => { // <---
if (appStore.checkLang.isTh) {
return item.category_th === category;
} else {
return item.category_en === category;
}
});
};
// **:** Function to construct image URL
const getImageUrl = (imageObject) => {
// console.log , image object url data.attributes
const url = imageObject?.url || ''; // <---
console.log('getImageUrl received:', imageObject, '-> URL:', url);
return url;
};
// Handler ()
const onImageError = (id) => {
imageLoadErrors.value = { ...imageLoadErrors.value, [id]: true };
console.warn('Image failed to load for ID:', id);
};
// **:** Computed property /placeholder
const shouldShowImage = (imageObject) => {
// console.log , image object url data.attributes
const show = imageObject?.url && !imageLoadErrors.value[commander.value?.id]; // <---
console.log('shouldShowImage received:', imageObject, '-> Result:', show);
return show;
};
// Initial data fetch on component mount
onMounted(() => {
commanderId.value = route.params.id;
console.log('onMounted: Route ID:', commanderId.value);
if (commanderId.value) {
fetchCommanderData(commanderId.value);
} else {
error.value = new Error('No commander ID provided in URL.');
isLoading.value = false;
console.error('No commander ID provided in URL.');
}
});
// Watch for changes in route.params.id ( ID URL )
watch(() => route.params.id, (newId) => {
console.log('Route ID changed:', newId);
if (newId && newId !== commanderId.value) {
commanderId.value = newId;
fetchCommanderData(newId);
}
});
// Watch for language changes ( re-render )
watch(() => appStore.checkLang.isTh, (newVal) => {
console.log('Language changed to TH:', newVal);
// re-fetch data computed properties
});
</script>
<style scoped>
ul {
list-style-type: disc;
padding-left: 1.5em;
margin-bottom: 0.5em;
}
li {
margin-bottom: 0.25em;
}
</style>

View File

@ -0,0 +1,170 @@
// src/views/about/CommandingOfficersView.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.checkLang.isTh ? header : header_en }}
</h1>
<p class="mt-3 text-lg opacity-90 text-gray-700">
{{ appStore.checkLang.isTh ? 'ทำความรู้จักกับผู้บริหารระดับสูงขององค์กร' : 'Meet the senior executives and commanding officers of our organization' }}
</p>
</div>
<div v-if="aboutUsStore.isLoading.commanders" class="text-center py-20">
<span class="loading loading-spinner loading-lg text-blue-500"></span>
<p class="mt-4 text-xl text-gray-500">{{ appStore.checkLang.isTh ? 'กำลังโหลดข้อมูล...' : 'Loading data...' }}</p>
</div>
<div v-else-if="aboutUsStore.errors.commanders" class="text-center text-red-600 py-20">
<p class="text-xl font-medium">{{ appStore.checkLang.isTh ? 'เกิดข้อผิดพลาดในการโหลดข้อมูลผู้บังคับบัญชา' : 'Error loading commanding officers data.' }}</p>
<p class="text-sm mt-2">
{{ appStore.checkLang.isTh ? 'โปรดลองใหม่อีกครั้ง หรือติดต่อผู้ดูแลระบบหากปัญหายังคงอยู่' : 'Please try again or contact support if the issue persists.' }}
</p>
</div>
<div v-else-if="aboutUsStore.getCommandersForDisplay && aboutUsStore.getCommandersForDisplay.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 justify-items-center">
<div class="md:col-span-2 lg:col-span-3 flex justify-center w-full mb-8">
<figure class="flex flex-col items-center text-center p-4">
<router-link v-if="aboutUsStore.getCommandersForDisplay[0] && aboutUsStore.getCommandersForDisplay[0].id" :to="`/about/commander/${aboutUsStore.getCommandersForDisplay[0].id}`">
<template v-if="shouldShowImage(aboutUsStore.getCommandersForDisplay[0])">
<img
:src="getImageUrl(aboutUsStore.getCommandersForDisplay[0].image.url)"
:alt="appStore.checkLang.isTh ? aboutUsStore.getCommandersForDisplay[0].name_th : aboutUsStore.getCommandersForDisplay[0].name_en"
class="w-48 h-48 object-cover rounded-lg shadow-lg border-4 border-blue-200 hover:border-blue-500 transition-all duration-300"
@error="onImageError(aboutUsStore.getCommandersForDisplay[0].id)"
/>
</template>
<template v-else>
<div class="w-48 h-48 rounded-lg shadow-lg border-4 border-gray-200 hover:border-blue-500 transition-all duration-300 bg-gray-100 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
</div>
</template>
</router-link>
<template v-else-if="aboutUsStore.getCommandersForDisplay[0]">
<template v-if="shouldShowImage(aboutUsStore.getCommandersForDisplay[0])">
<img
:src="getImageUrl(aboutUsStore.getCommandersForDisplay[0].image.url)"
:alt="appStore.checkLang.isTh ? aboutUsStore.getCommandersForDisplay[0].name_th : aboutUsStore.getCommandersForDisplay[0].name_en"
class="w-48 h-48 object-cover rounded-lg shadow-lg border-4 border-blue-200"
@error="onImageError(aboutUsStore.getCommandersForDisplay[0].id)"
/>
</template>
<template v-else>
<div class="w-48 h-48 rounded-lg shadow-lg border-4 border-gray-200 bg-gray-100 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
</div>
</template>
</template>
<figcaption v-if="aboutUsStore.getCommandersForDisplay[0]" class="mt-4">
<p class="text-xl font-semibold text-gray-800">
{{ appStore.checkLang.isTh ? aboutUsStore.getCommandersForDisplay[0].name_th : aboutUsStore.getCommandersForDisplay[0].name_en }}
</p>
<div class="text-gray-600 text-base"
v-html="appStore.checkLang.isTh ? aboutUsStore.getCommandersForDisplay[0].bio_th : aboutUsStore.getCommandersForDisplay[0].bio_en">
</div>
</figcaption>
</figure>
</div>
<div v-for="(person, index) in aboutUsStore.getCommandersForDisplay.slice(1)" :key="person.id || index" class="w-full">
<figure class="flex flex-col items-center text-center p-4">
<router-link v-if="person.id" :to="`/about/commander/${person.id}`">
<template v-if="shouldShowImage(person)">
<img
:src="getImageUrl(person.image.url)"
:alt="appStore.checkLang.isTh ? person.name_th : person.name_en"
class="w-40 h-40 object-cover rounded-lg shadow-md border-2 border-gray-300 hover:border-blue-500 transition-all duration-300"
@error="onImageError(person.id)"
/>
</template>
<template v-else>
<div class="w-40 h-40 rounded-lg shadow-md border-2 border-gray-300 hover:border-blue-500 transition-all duration-300 bg-gray-100 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-20 w-20 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
</div>
</template>
</router-link>
<template v-else>
<template v-if="shouldShowImage(person)">
<img
:src="getImageUrl(person.image.url)"
:alt="appStore.checkLang.isTh ? person.name_th : person.name_en"
class="w-40 h-40 object-cover rounded-lg shadow-md border-2 border-gray-300"
@error="onImageError(person.id)"
/>
</template>
<template v-else>
<div class="w-40 h-40 rounded-lg shadow-md border-2 border-gray-300 bg-gray-100 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-20 w-20 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
</div>
</template>
</template>
<figcaption class="mt-3">
<p class="text-lg font-medium text-gray-800">
{{ appStore.checkLang.isTh ? person.name_th : person.name_en }}
</p>
<div class="text-gray-600 text-sm"
v-html="appStore.checkLang.isTh ? person.bio_th : person.bio_en">
</div>
</figcaption>
</figure>
</div>
</div>
<div v-else class="text-center text-gray-400 py-16">
<p class="text-xl">{{ appStore.checkLang.isTh ? 'ไม่พบข้อมูลผู้บังคับบัญชา' : 'No commanding officers data found.' }}</p>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useAppStore } from '@/stores/app.js';
import { useAboutUsStore } from '@/stores/aboutUsStore.js';
const appStore = useAppStore();
const aboutUsStore = useAboutUsStore();
// Ref ID Key
const imageLoadErrors = ref({});
// Static header translations
const header = "ผู้บริหารระดับสูง";
const header_en = "Senior Executives";
// Function to construct image URL
const getImageUrl = (imagePath) => {
return imagePath;
};
// Handler
const onImageError = (id) => {
imageLoadErrors.value = { ...imageLoadErrors.value, [id]: true };
};
// Computed property /placeholder
const shouldShowImage = (person) => {
// URL ID
return person.image && person.image.url && !imageLoadErrors.value[person.id];
};
// Fetch data using Composition API
onMounted(() => {
// action aboutUsStore
aboutUsStore.fetchCommanders();
});
</script>
<style scoped>
/* No specific custom styles are needed beyond Tailwind/DaisyUI, as they handle most styling. */
</style>

View File

@ -0,0 +1,120 @@
// src/views/about/HallView.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.checkLang.isTh ? header_th : header_en }}
</h1>
<p class="mt-3 text-lg opacity-90 text-gray-700">
{{ appStore.checkLang.isTh ? 'ทำความรู้จักกับทำเนียบอดีตผู้บริหารองค์กร' : 'Meet the former executives of our organization' }}
</p>
</div>
<div v-if="aboutUsStore.isLoading.formerExecutives" class="text-center py-20">
<span class="loading loading-spinner loading-lg text-blue-500"></span>
<p class="mt-4 text-xl text-gray-500">
{{ appStore.checkLang.isTh ? 'กำลังโหลดทำเนียบอดีตผู้บริหาร...' : 'Loading former executives...' }}
</p>
</div>
<div v-else-if="aboutUsStore.errors.formerExecutives" class="text-center text-red-600 py-20">
<p class="text-xl font-medium">
{{ appStore.checkLang.isTh ? 'เกิดข้อผิดพลาดในการโหลดข้อมูล' : 'Error loading data.' }}
</p>
<p class="text-sm mt-2">
{{ appStore.checkLang.isTh ? 'ไม่สามารถดึงข้อมูลทำเนียบอดีตผู้บริหารได้ โปรดลองอีกครั้ง' : 'Failed to fetch former executives data. Please try again.' }}
</p>
</div>
<div v-else-if="aboutUsStore.getFormerExecutivesForDisplay.length > 0">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 justify-items-center">
<div v-for="executive in aboutUsStore.getFormerExecutivesForDisplay" :key="executive.id" class="text-center">
<figure class="figure flex flex-col items-center">
<template v-if="shouldShowImage(executive)">
<img
:src="getImageUrl(executive.image.url)"
class="figure-img rounded-lg object-cover w-48 h-48 mb-3 shadow-md border-2 border-gray-300 hover:border-blue-500 transition-all duration-300"
:alt="appStore.checkLang.isTh ? executive.title_th : executive.title_en"
@error="onImageError(executive.id)"
/>
</template>
<template v-else>
<div class="w-48 h-48 rounded-lg shadow-md border-2 border-gray-300 hover:border-blue-500 transition-all duration-300 bg-gray-100 flex items-center justify-center mb-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
</div>
</template>
<figcaption class="figure-caption text-center text-gray-700 text-lg" v-html="appStore.checkLang.isTh ? executive.title_th : executive.title_en">
</figcaption>
</figure>
</div>
</div>
</div>
<div v-else class="text-center text-gray-400 py-16">
<p class="text-xl">
{{ appStore.checkLang.isTh ? 'ไม่พบข้อมูลทำเนียบอดีตผู้บริหาร' : 'No former executives found.' }}
</p>
</div>
</div>
</template>
<script setup>
import { onMounted, watch, ref } from 'vue'; // Import ref
import { useAppStore } from '@/stores/app.js';
import { useAboutUsStore } from '@/stores/aboutUsStore.js';
const appStore = useAppStore();
const aboutUsStore = useAboutUsStore();
// Ref to track image loading errors, keyed by executive ID
const imageLoadErrors = ref({});
const header_th = "ทำเนียบอดีตผู้บริหารองค์กร";
const header_en = "Former Executives of the Organization";
// Function to construct the image URL using Vite's environment variables
const getImageUrl = (path) => {
const baseURL = import.meta.env.VITE_IMAGE_BASE_URL;
if (!baseURL) {
console.warn("VITE_IMAGE_BASE_URL is not defined in your .env file. Using relative path.");
return path;
}
return `${baseURL}${path}`;
};
// Handler when an image fails to load
const onImageError = (id) => {
// Set the error status for the specific image ID
imageLoadErrors.value = { ...imageLoadErrors.value, [id]: true };
};
// Function to determine if the image should be shown
const shouldShowImage = (executive) => {
// Check if executive.image and executive.image.url exist,
// AND if there hasn't been a load error recorded for this executive's ID.
return executive.image && executive.image.url && !imageLoadErrors.value[executive.id];
};
onMounted(() => {
aboutUsStore.fetchFormerExecutives();
});
watch(() => appStore.checkLang.isTh, (newLangIsTh, oldLangIsTh) => {
if (newLangIsTh !== oldLangIsTh) {
console.log("Language changed, re-fetching former executives data.");
// Reset imageLoadErrors when language changes to attempt loading images again
imageLoadErrors.value = {};
aboutUsStore.fetchFormerExecutives();
}
});
</script>
<style scoped>
/* No specific styles needed here, Tailwind/DaisyUI handles most of it */
.figure-img {
/* Using Tailwind's shadow-md for consistency, if you had this in your original project */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
</style>

View File

@ -0,0 +1,176 @@
// scr/views/about/HistoryView.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-blue-800">
{{ activePeriodContent ? (appStore.checkLang.isTh ? activePeriodContent.period_th : activePeriodContent.period_en) : (appStore.checkLang.isTh ? 'ประวัติบริษัท' : 'Company History') }}
</h1>
<p class="mt-3 text-lg opacity-90 text-gray-700">
{{ appStore.checkLang.isTh ? 'เรื่องราวการเดินทางขององค์กร ตั้งแต่อดีตจนถึงปัจจุบัน' : 'The journey of our organization, from past to present' }}
</p>
</div>
<!-- Tabs -->
<div role="tablist" class="flex flex-wrap space-x-2 border-b border-gray-300">
<button
v-for="period in historyPeriods"
:key="period.id"
class="px-4 py-2 text-sm font-medium transition-all duration-200 border-b-2 whitespace-nowrap"
:class="{
'border-blue-700 text-blue-700': activePeriodId === period.id,
'border-transparent text-gray-500 hover:text-blue-700 hover:border-blue-500': activePeriodId !== period.id
}"
@click="selectPeriod(period.id)"
>
{{ appStore.checkLang.isTh ? period.attributes.period_th : period.attributes.period_en }}
</button>
</div>
<!-- Loading -->
<div v-if="aboutUsStore.isLoading.history" class="text-center py-10">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="mt-4 text-lg text-gray-500">{{ appStore.checkLang.isTh ? 'กำลังโหลดประวัติบริษัท...' : 'Loading company history...' }}</p>
</div>
<!-- Error -->
<div v-else-if="aboutUsStore.errors.history" class="text-center text-error py-10">
<p class="text-xl">{{ appStore.checkLang.isTh ? 'เกิดข้อผิดพลาดในการโหลดข้อมูลประวัติบริษัท' : 'Error loading company history data.' }}</p>
<p class="text-sm">{{ aboutUsStore.errors.history }}</p>
</div>
<!-- Content -->
<div v-else-if="activePeriodContent" class="grid md:grid-cols-2 gap-12 items-start mt-10">
<!-- Image -->
<div>
<figure v-if="activePeriodContent.imageUrl" class="relative w-full h-80 md:h-[28rem] overflow-hidden rounded-xl shadow-lg">
<img :src="activePeriodContent.imageUrl" class="w-full h-full object-cover object-center transition-transform duration-500 hover:scale-105" />
<figcaption v-if="activePeriodContent.image_caption_th || activePeriodContent.image_caption_en"
class="absolute bottom-0 w-full bg-black/60 text-white text-sm p-2 text-center rounded-b-xl">
{{ appStore.checkLang.isTh ? activePeriodContent.image_caption_th : activePeriodContent.image_caption_en }}
</figcaption>
</figure>
<div v-else class="w-full h-80 bg-gray-200 rounded-xl flex items-center justify-center text-gray-500">
{{ appStore.checkLang.isTh ? 'ไม่มีรูปภาพ' : 'No Image Available' }}
</div>
</div>
<!-- Text Content -->
<div class="prose lg:prose-xl max-w-none text-justify text-gray-700">
<p v-html="appStore.checkLang.isTh ? activePeriodContent.paragraph1 : activePeriodContent.paragraph1_en"></p>
<p v-html="appStore.checkLang.isTh ? activePeriodContent.paragraph2 : activePeriodContent.paragraph2_en" class="mt-4"></p>
</div>
</div>
<!-- No Data -->
<div v-else class="text-center text-gray-400 py-16">
<p class="text-xl">{{ appStore.checkLang.isTh ? 'ไม่พบข้อมูลสำหรับช่วงเวลานี้' : 'No data found for this period.' }}</p>
</div>
</div>
</template>
<script setup>
import { onMounted, watch, computed, ref } from 'vue';
import { useAppStore } from '@/stores/app.js';
import { useAboutUsStore } from '@/stores/aboutUsStore.js';
const appStore = useAppStore();
const aboutUsStore = useAboutUsStore();
const activePeriodId = ref('');
const useGold = true // true = , false =
const historyPeriods = computed(() => {
return aboutUsStore.historyData || [];
});
const activePeriodContent = computed(() => {
if (!historyPeriods.value || historyPeriods.value.length === 0) return null;
const foundPeriod = historyPeriods.value.find(p => p.id === activePeriodId.value);
return foundPeriod ? foundPeriod.attributes : null;
});
const selectPeriod = (id) => {
activePeriodId.value = id;
};
onMounted(() => {
aboutUsStore.fetchHistory().then(() => {
if (historyPeriods.value && historyPeriods.value.length > 0) {
activePeriodId.value = historyPeriods.value[0].id;
}
});
});
watch(() => appStore.checkLang.isTh, (newVal) => {
// mock data aboutUsStore
// TH/EN object
// API locale force re-fetch active tab
});
</script>
<style scoped>
/* Custom CSS สำหรับ override หรือเพิ่มเติมนอกเหนือจาก Utility Classes ที่ Tailwind/DaisyUI มีให้ */
/* กำหนดสีหลักสำหรับพื้นหลังหัวข้อ */
.bg-primary {
background-color: #1b3872; /* Dark blue */
}
/* DaisyUI .tabs และ .tab มีโครงสร้างของมันเอง เราแค่ override หรือเพิ่มเติมนิดหน่อย */
/* .tabs-boxed โดยปกติจะมีพื้นหลังสีอ่อนและแท็บ active มีพื้นหลังสีหลัก */
.tabs .tab {
/* สีข้อความของแท็บปกติ */
color: #6b7280; /* text-gray-500 */
}
.tabs .tab:hover {
/* สีข้อความเมื่อ hover */
color: #2563eb; /* text-blue-700 */
}
.tabs .tab-active {
/* สีข้อความและพื้นหลังของแท็บที่ active */
color: #FFFFFF; /* ข้อความสีขาว */
background-color: #2563eb; /* background-blue-700 (หรือสี primary ของ theme DaisyUI) */
}
/* รูปภาพและคำบรรยาย (เหล่านี้ส่วนใหญ่จัดการได้ด้วย Tailwind utility classes แล้ว) */
/*
เนองจากเราใช absolute positioning สำหร figcaption ตอนน
เราอาจจะตองปร CSS วนนเลกนอย
*/
.prose :deep(img) {
/* ลบ margin: 0 auto; ถ้ามีผลกระทบกับ figure ที่เป็น relative และ absolute figcaption */
/* ในกรณีนี้ figcaption อยู่ใน figure ที่มี h-64/h-96 ดังนั้น img ควรจะ cover เต็มพื้นที่ */
max-width: 100%;
height: auto; /* หรือ h-full ถ้าอยู่ใน container ที่กำหนดความสูง */
display: block;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.prose p {
margin-bottom: 1.25em;
line-height: 1.75;
}
.tab::after {
content: '';
position: absolute;
bottom: -4px;
left: 0;
height: 3px;
width: 0;
background-color: transparent;
transition: width 0.3s ease;
}
.tab:hover::after {
width: 100%;
background-color: #3b82f6; /* สีฟ้า */
}
</style>

View File

@ -0,0 +1,51 @@
// src/views/about/OrganizationStructureView.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.checkLang.isTh ? header_th : header_en }}
</h1>
</div>
<div v-if="aboutUsStore.isLoading.organizationStructure" class="text-center py-20">
<span class="loading loading-spinner loading-lg text-blue-500"></span>
<p class="mt-4 text-xl text-gray-500">{{ appStore.checkLang.isTh ? 'กำลังโหลดโครงสร้างองค์กร...' : 'Loading organization hierarchy...' }}</p>
</div>
<div v-else-if="aboutUsStore.errors.organizationStructure" class="text-center text-red-600 py-20">
<p class="text-xl font-medium">{{ appStore.checkLang.isTh ? 'เกิดข้อผิดพลาดในการโหลดโครงสร้างองค์กร' : 'Error loading organization hierarchy.' }}</p>
<p class="text-sm mt-2">
{{ appStore.checkLang.isTh ? 'ไม่สามารถดึงข้อมูลโครงสร้างองค์กรได้ โปรดลองอีกครั้ง' : 'Failed to fetch organization hierarchy data. Please try again.' }}
</p>
</div>
<div v-else-if="aboutUsStore.getOrganizationStructureForDisplay">
<OrganizationTreeDiagram :treeData="aboutUsStore.getOrganizationStructureForDisplay" />
</div>
<div v-else class="text-center text-gray-400 py-16">
<p class="text-xl">{{ appStore.checkLang.isTh ? 'ไม่พบข้อมูลโครงสร้างองค์กร' : 'No organization hierarchy found.' }}</p>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useAppStore } from '@/stores/app.js';
import { useAboutUsStore } from '@/stores/aboutUsStore.js';
import OrganizationTreeDiagram from '@/components/OrganizationTreeDiagram.vue';
const appStore = useAppStore();
const aboutUsStore = useAboutUsStore();
const header_th = "โครงสร้างองค์กรสมมุติ";
const header_en = "Fictional Organization Hierarchy";
onMounted(() => {
aboutUsStore.fetchOrganizationStructure();
});
</script>
<style scoped>
/* No specific styles needed here, Tailwind handles it */
</style>

View File

@ -0,0 +1,293 @@
// src/views/about/VisionMissionView.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.checkLang.isTh ? header_title : header_title_en }}
</h1>
<p class="mt-3 text-lg opacity-90 text-gray-700">
{{ appStore.checkLang.isTh ? 'วิสัยทัศน์ พันธกิจ และคุณค่าหลักขององค์กร เพื่อขับเคลื่อนอนาคต' : 'Our Vision, Mission, and Core Values to drive the future' }}
</p>
</div>
<div v-if="isLoadingAny" class="text-center py-20">
<span class="loading loading-spinner loading-lg text-blue-500"></span>
<p class="mt-4 text-xl text-gray-500">{{ appStore.checkLang.isTh ? 'กำลังโหลดข้อมูล...' : 'Loading data...' }}</p>
</div>
<div v-else-if="hasError" class="text-center text-red-600 py-20">
<p class="text-xl font-medium">{{ appStore.checkLang.isTh ? 'เกิดข้อผิดพลาดในการโหลดข้อมูล' : 'Error loading data.' }}</p>
<p class="text-sm mt-2">
{{ appStore.checkLang.isTh ? 'โปรดลองใหม่อีกครั้ง หรือติดต่อผู้ดูแลระบบหากปัญหายังคงอยู่' : 'Please try again or contact support if the issue persists.' }}
</p>
</div>
<div v-else-if="isDataReady" class="space-y-16">
<section v-if="visionData" class="py-8 px-6">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 text-center mb-12">
{{ appStore.checkLang.isTh ? 'วิสัยทัศน์ ภารกิจ และพันธกิจของเรา' : 'Our Vision, Mission, and Obligation' }}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="vm-card bg-white rounded-xl shadow-lg p-6 flex flex-col items-center text-center">
<i class="fas fa-eye w-24 h-24 mb-4 text-blue-700 flex items-center justify-center text-6xl"></i>
<h3 class="text-2xl font-bold text-blue-700 mb-3">{{ appStore.checkLang.isTh ? header_vision : header_vision_en }}</h3>
<div class="prose lg:prose-base max-w-none text-gray-700">
<div v-html="appStore.checkLang.isTh ? visionData.vision : visionData.vision_en"></div>
</div>
</div>
<div class="vm-card bg-white rounded-xl shadow-lg p-6 flex flex-col items-center text-center">
<i class="fas fa-handshake w-24 h-24 mb-4 text-blue-700 flex items-center justify-center text-6xl"></i>
<h3 class="text-2xl font-bold text-blue-700 mb-3">{{ appStore.checkLang.isTh ? header_mission : header_mission_en }}</h3>
<div class="prose lg:prose-base max-w-none text-gray-700">
<div v-html="appStore.checkLang.isTh ? visionData.mission : visionData.mission_en"></div>
</div>
</div>
<div class="vm-card bg-white rounded-xl shadow-lg p-6 flex flex-col items-center text-center">
<i class="fas fa-clipboard-list w-24 h-24 mb-4 text-blue-700 flex items-center justify-center text-6xl"></i>
<h3 class="text-2xl font-bold text-blue-700 mb-3">{{ appStore.checkLang.isTh ? header_ms : header_ms_en }}</h3>
<div class="prose lg:prose-base max-w-none text-gray-700">
<div v-html="appStore.checkLang.isTh ? visionData.obligation : visionData.obligation_en"></div>
</div>
</div>
</div>
</section>
<section v-if="ourJourneyData" class="bg-gradient-to-br from-blue-900 via-blue-800 to-indigo-800 text-white py-16 px-6 text-center rounded-xl shadow-2xl">
<h2 class="text-4xl font-bold mb-4 tracking-wide drop-shadow-md">
{{ appStore.checkLang.isTh ? ourJourneyData.header_th : ourJourneyData.header_en }}
</h2>
<p class="text-lg mb-8 opacity-90 leading-relaxed">
{{ appStore.checkLang.isTh ? ourJourneyData.description_th : ourJourneyData.description_en }}
</p>
<div class="aspect-video w-full max-w-4xl mx-auto rounded-lg overflow-hidden shadow-xl ring-2 ring-white/30">
<iframe
:src="ourJourneyData.youtube_video_url"
title="Our Journey Starts Here Video"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
class="w-full h-full"
></iframe>
</div>
</section>
<section class="py-16 px-6">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 text-center mb-12">
{{ appStore.checkLang.isTh ? coreValuesData.header_th : coreValuesData.header_en }}
</h2>
<div class="bg-white rounded-xl shadow-lg p-4 md:p-8">
<div class="flex flex-wrap justify-center items-stretch gap-6">
<div
v-for="(value, index) in (appStore.checkLang.isTh ? coreValuesData.values_th : coreValuesData.values_en)"
:key="index"
class="core-value-card relative overflow-hidden rounded-lg shadow-md group
w-full sm:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] xl:w-[calc(25%-18px)]"
:style="{ backgroundImage: 'url(' + (value.image_url || 'https://via.placeholder.com/400x300?text=Core+Value+Image') + ')', backgroundSize: 'cover', backgroundPosition: 'center' }"
>
<div class="absolute inset-0 bg-black bg-opacity-40 transition-opacity duration-300 group-hover:bg-opacity-70 flex flex-col items-center justify-center p-4 text-white text-center">
<h3 class="text-xl font-semibold mb-2">{{ value.title }}</h3>
<p class="text-sm opacity-90">{{ value.description }}</p>
<a v-if="value.link_url" :href="value.link_url" class="btn btn-sm btn-outline btn-info mt-3 text-white border-white hover:bg-white hover:text-blue-800">
{{ appStore.checkLang.isTh ? 'ดูเพิ่มเติม' : 'Learn More' }}
</a>
</div>
</div>
</div>
</div>
</section>
</div>
<div v-else class="text-center text-gray-400 py-16">
<p class="text-xl">{{ appStore.checkLang.isTh ? 'ไม่พบข้อมูลสำหรับหน้านี้' : 'No data found for this page.' }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useAppStore } from '@/stores/app.js';
import { useAboutUsStore } from '@/stores/aboutUsStore.js';
const appStore = useAppStore();
const aboutUsStore = useAboutUsStore();
const header_title = "วิสัยทัศน์ พันธกิจ";
const header_title_en = "Vision and Mission";
const header_vision = "วิสัยทัศน์";
const header_vision_en = "Vision";
const header_mission = "ภารกิจ";
const header_mission_en = "Mission";
const header_ms = "พันธกิจ"; // Obligation or Key Objectives
const header_ms_en = "Obligation";
// Local refs to hold data fetched from the store
const visionData = ref(null);
const whoWeAreData = ref(null);
const ourJourneyData = ref(null);
const ourCreedData = ref(null);
const coreValuesData = ref(null);
const keyCapabilitiesData = ref(null);
// Computed properties to manage overall loading and error states
const isLoadingAny = computed(() => {
return (
aboutUsStore.isLoading.visionMission ||
aboutUsStore.isLoading.whoWeAre ||
aboutUsStore.isLoading.ourJourney ||
aboutUsStore.isLoading.ourCreed ||
aboutUsStore.isLoading.coreValues ||
aboutUsStore.isLoading.keyCapabilities
);
});
const hasError = computed(() => {
return (
aboutUsStore.errors.visionMission !== null ||
aboutUsStore.errors.whoWeAre !== null ||
aboutUsStore.errors.ourJourney !== null ||
aboutUsStore.errors.ourCreed !== null ||
aboutUsStore.errors.coreValues !== null ||
aboutUsStore.errors.keyCapabilities !== null
);
});
const isDataReady = computed(() => {
return (
visionData.value &&
whoWeAreData.value &&
ourJourneyData.value &&
ourCreedData.value &&
coreValuesData.value && // Ensure coreValuesData is loaded
keyCapabilitiesData.value
);
});
const fetchAllData = async () => {
try {
// Fetch all necessary data using the store's actions
await Promise.all([
aboutUsStore.fetchVisionMission(),
aboutUsStore.fetchWhoWeAre(),
aboutUsStore.fetchOurJourney(),
aboutUsStore.fetchOurCreed(),
aboutUsStore.fetchCoreValues(), // Make sure this is fetched
aboutUsStore.fetchKeyCapabilities(),
]);
// Assign data from store getters to local refs
visionData.value = aboutUsStore.getVisionMissionForDisplay;
whoWeAreData.value = aboutUsStore.getWhoWeAreForDisplay;
ourJourneyData.value = aboutUsStore.getOurJourneyForDisplay;
// --- : aboutStore aboutUsStore ---
ourCreedData.value = aboutUsStore.getOurCreedForDisplay;
// -----------------------------------------------------------
coreValuesData.value = aboutUsStore.getCoreValuesForDisplay; // Assign data
keyCapabilitiesData.value = aboutUsStore.getKeyCapabilitiesForDisplay;
} catch (err) {
// Errors are already handled within the store's `errors` ref
console.error("Error fetching data in VisionMissionView:", err);
}
};
onMounted(() => {
fetchAllData();
});
watch(() => appStore.checkLang.isTh, () => {
// If your API fetches different data based on locale, uncomment this line
// fetchAllData();
// Otherwise, if translation is handled by getters, no need to re-fetch
});
</script>
<style scoped>
/* Tailwind CSS utility classes are mostly used in the template. */
/* Custom card-header background color from your original code */
.card-header {
background-color: #c1d4db; /* ใช้สีเดิมที่คุณกำหนด */
}
/* Base Prose adjustments for lists */
.prose :deep(ul) {
list-style-type: disc;
padding-left: 1.5em;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.prose :deep(ol) {
list-style-type: decimal;
padding-left: 1.5em;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.prose :deep(li) {
margin-bottom: 0.5em;
}
.prose :deep(p) {
margin-bottom: 1em;
}
/* Styles for core value cards (unchanged) */
.core-value-card {
/* Aspect ratio to maintain consistent height */
aspect-ratio: 4 / 3; /* Width:Height, adjust as needed for your images */
min-height: 200px; /* Minimum height for readability */
display: flex;
flex-direction: column;
justify-content: flex-end; /* Align content to the bottom initially */
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
.core-value-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.core-value-card .absolute.inset-0 {
/* This overlay will handle the content and hover effect */
opacity: 0; /* Hidden by default */
background-color: rgba(0, 0, 0, 0.6); /* Semi-transparent black overlay */
transition: opacity 0.3s ease-in-out;
}
.core-value-card:hover .absolute.inset-0 {
opacity: 1; /* Visible on hover */
}
/* Ensure text is readable over the background image */
.core-value-card h3, .core-value-card p, .core-value-card a {
color: white;
text-shadow: 1px 1px 3px rgba(0,0,0,0.7);
}
/* Adjust text size for smaller screens if needed */
@media (max-width: 639px) { /* sm breakpoint */
.core-value-card h3 {
font-size: 1.125rem; /* text-lg */
}
.core-value-card p {
font-size: 0.875rem; /* text-sm */
}
}
/* Styles for Vision, Mission, Obligation cards (unchanged) */
.vm-card {
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
.vm-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -0,0 +1,313 @@
// src/views/info-dissemination/JournalView.vue
<template>
<div class="container mx-auto px-4 py-8 mb-12">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.isTh ? header : header_en }}
</h1>
</div>
<div class="mb-6 max-w-3xl mx-auto">
<label class="input input-bordered flex items-center gap-2 shadow-md w-full">
<input
type="text"
class="grow"
:placeholder="appStore.isTh ? 'ค้นหาวารสาร/ข่าว/นิตยสารด้วยชื่อ...' : 'Search publications by name...'"
v-model="searchTerm"
/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 opacity-70">
<path fill-rule="evenodd" d="M9.965 11.023A5.996 5.996 0 0 1 5.996 12C2.686 12 0 9.314 0 5.996 0 2.686 2.686 0 5.996 0c3.308 0 5.996 2.686 5.996 5.996 0 1.544-.582 2.977-1.547 4.027l2.675 2.675c.39.39.39 1.024 0 1.414-.39.39-1.024.39-1.414 0l-2.675-2.675ZM5.996 10.5c2.47 0 4.496-2.026 4.496-4.496S8.466 1.5 5.996 1.5 1.5 3.526 1.5 6.004C1.5 8.47 3.093 10.5 5.996 10.5Z" clip-rule="evenodd" />
</svg>
</label>
</div>
<div role="tablist" class="tabs tabs-boxed justify-center mb-8 bg-base-200">
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-2"
:class="{ 'tab-active': activeTab === 'all' }"
@click="activeTab = 'all'"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.125 3.75a4.237 4.237 0 0 1 4.375 4.375V12h4.5v6.375a4.237 4.237 0 0 1-4.375 4.375H12c-.621 0-1.125-.504-1.125-1.125v-1.5c0-.621.504-1.125 1.125-1.125h1.5c.621 0 1.125-.504 1.125-1.125V18h4.5a.75.75 0 0 0 .75-.75V12h1.5a.75.75 0 0 0 .75-.75V5.25a.75.75 0 0 0-.75-.75H12a.75.75 0 0 0-.75-.75H6.75a.75.75 0 0 0-.75-.75V3.75c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125Z" />
</svg>
{{ appStore.isTh ? 'ทั้งหมด' : 'All' }}
</a>
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-2"
:class="{ 'tab-active': activeTab === 'journal' }"
@click="activeTab = 'journal'"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.767 8.767 0 0 0 7.5 3H3.375c-.621 0-1.125.504-1.125 1.125v13.5c0 .621.504 1.125 1.125 1.125h4.125c.621 0 1.125-.504 1.125-1.125V15c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125v2.25c0 .621.504 1.125 1.125 1.125h4.125c.621 0 1.125-.504 1.125-1.125V8.25c0-2.25-1.83-4.125-4.08-4.125A8.756 8.756 0 0 0 12 6.042Z" />
</svg>
{{ appStore.isTh ? journal_title : journal_title_en }}
</a>
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-2"
:class="{ 'tab-active': activeTab === 'dailynews' }"
@click="activeTab = 'dailynews'"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5l.75 3.75m-1.5 0L12 7.5M12 7.5V11.25m0-3.75H6.75a2.25 2.25 0 0 0-2.25 2.25v10.5a2.25 2.25 0 0 0 2.25 2.25h10.5a2.25 2.25 0 0 0 2.25-2.25V11.25a2.25 2.25 0 0 0-2.25-2.25H12Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5L12.75 6H15c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H9.75c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125h2.25Z" />
</svg>
{{ appStore.isTh ? news_title : news_title_en }}
</a>
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-2"
:class="{ 'tab-active': activeTab === 'magazine' }"
@click="activeTab = 'magazine'"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.25 22.5H6a2.25 2.25 0 0 1-2.25-2.25V4.5A2.25 2.25 0 0 1 6 2.25h8.25a2.25 2.25 0 0 1 2.25 2.25v1.375a3.375 3.375 0 0 0 3.375 3.375c.573 0 1.127.11 1.647.311a.75.75 0 0 1 .527.712V19.5a2.25 2.25 0 0 1-2.25 2.25h-5.25a2.25 2.25 0 0 1-2.25-2.25Z" />
</svg>
{{ appStore.isTh ? magazine_title : magazine_title_en }}
</a>
</div>
<div class="pt-3">
<div v-if="displayedPublications.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div
v-for="(d, i) in displayedPublications"
:key="d.id"
class="card bg-base-100 shadow-xl overflow-hidden group relative"
>
<figure class="w-full h-48">
<img
:src="d.thumbnail.url"
:alt="appStore.isTh ? d.Title_TH : d.Title_EN"
class="w-full h-full object-cover"
/>
</figure>
<div
class="absolute inset-0 bg-teal-700 bg-opacity-75 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div class="flex space-x-4">
<button
class="btn btn-circle btn-info"
@click="openFlipBook(d)"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-white">
<path d="M2.25 18.75a60.076 60.076 0 0 1 1.5-3.004v-7.135C3.75 6.07 4.136 5.5 4.875 5.5H8.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66.54 1.2 1.2 1.2h3.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66.54 1.2 1.2 1.2h3.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66-.54 1.2-1.2 1.2h-3.25c-.66 0-1.2-.54-1.2-1.2v-3.25c0-.66-.54-1.2-1.2-1.2H12c-.66 0-1.2.54-1.2 1.2v3.25c0 .66-.54 1.2-1.2 1.2H4.875c-.739 0-1.125-.57-1.125-1.096v-7.135a60.076 60.076 0 0 1-1.5 3.004ZM4.875 5.5h-.125V3.75c0-.66-.54-1.2-1.2-1.2h-3.25c-.66 0-1.2.54-1.2 1.2v3.25c0 .66.54 1.2 1.2 1.2h3.25c.66 0 1.2-.54 1.2-1.2V5.5ZM12 12c0-.66.54-1.2 1.2-1.2h3.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66-.54 1.2-1.2 1.2h-3.25c-.66 0-1.2-.54-1.2-1.2v-3.25Z" />
</svg>
</button>
<a
:href="d.Attachment_TH[0].url"
target="_blank"
class="btn btn-circle btn-info"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-white">
<path fill-rule="evenodd" d="M11.828 2.25c-.965 0-1.75.785-1.75 1.75v14.5c0 .965.785 1.75 1.75 1.75H18.75c.965 0 1.75-.785 1.75-1.75V8.187c0-.284-.113-.559-.313-.75L15.313 3.107a1.75 1.75 0 0 0-.75-.313H11.828ZM10.5 4a.75.75 0 0 0-.75-.75H6.25A1.75 1.75 0 0 0 4.5 5.75v14.5c0 .965.785 1.75 1.75 1.75h4.25a.75.75 0 0 0 0-1.5H6.25a.25.25 0 0 1-.25-.25V5.75c0-.138.112-.25.25-.25h3.5A.75.75 0 0 0 10.5 4ZM15.75 5.5a.25.25 0 0 1 .25.25v2.25h2.25a.25.25 0 0 1 .25.25v.75H16.25a1.75 1.75 0 0 0-1.75 1.75v3.25c0 .414-.336.75-.75.75H13a.75.75 0 0 0-.75.75v.75c0 .414.336.75.75.75h1.75a.75.75 0 0 0 .75-.75v-.75a.75.75 0 0 0-.75-.75H13a.25.25 0 0 1-.25-.25V10.25c0-.138.112-.25.25-.25h2.25a.75.75 0 0 0 .75-.75V5.75Z" clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
<div class="card-body p-4">
<h6 class="text-base font-semibold text-blue-600">
{{ appStore.isTh ? d.Title_TH : d.Title_EN }}
</h6>
<p class="text-sm text-gray-500">
{{ appStore.isTh ? `ประเภท: ${getTypeLabel(d.type)}` : `Type: ${getTypeLabel(d.type, 'en')}` }}
</p>
</div>
</div>
</div>
<div v-else-if="searchTerm" class="text-center text-gray-500 text-lg mt-8">
{{ appStore.isTh ? 'ไม่พบเอกสารที่ตรงกับคำค้นหา' : 'No matching publications found.' }}
</div>
<div v-else class="text-center text-gray-500 text-lg mt-8">
{{ appStore.isTh ? 'กำลังโหลดเอกสาร...' : 'Loading publications...' }}
</div>
<div v-if="hasMorePublications" class="text-center mt-8">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md" @click="loadMorePublications">
{{ appStore.isTh ? 'โหลดเอกสารเพิ่ม' : 'Load More' }}
</button>
</div>
</div>
<div
v-if="overlay"
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
>
<div class="relative w-full h-full max-w-5xl max-h-5xl bg-base-100 rounded-box shadow-2xl">
<button
class="btn btn-sm btn-circle btn-error absolute top-4 right-4 z-10"
@click="overlay = false"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" clip-rule="evenodd" />
</svg>
</button>
<Flipbook ref="flipmain" :pages="currentPages" class="w-full h-full" />
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex space-x-2">
<button
class="btn btn-circle btn-sm btn-info"
@click="flipMainLeft()"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 1 1 1.06 1.06L9.31 12l6.97 6.97a.75.75 0 1 1-1.06 1.06l-7.5-7.5Z" clip-rule="evenodd" />
</svg>
</button>
<button
class="btn btn-circle btn-sm btn-info"
@click="flipMainRight()"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
<path fill-rule="evenodd" d="M16.28 12.28a.75.75 0 0 0 0-1.06l-7.5-7.5a.75.75 0 0 0-1.06 1.06L14.69 12l-6.97 6.97a.75.75 0 1 0 1.06 1.06l7.5-7.5Z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { useInfoDisseminationStore } from '@/stores/infoDisseminationStore';
import { useAppStore } from '@/stores/app';
// Constants
const INITIAL_ITEMS = 8; // , PublicationsView
const ITEMS_PER_LOAD = 4; //
const infoDisseminationStore = useInfoDisseminationStore();
const appStore = useAppStore();
const header = "วารสารองค์กร";
const header_en = "HumanTech Publications";
const journal_title = "สารชาวฮิวแมนเทคไทยแลนด์";
const journal_title_en = "HumanTech Journal";
const news_title = "ข่าวประจำวันฮิวแมนเทคไทยแลนด์";
const news_title_en = "HumanTech Daily News";
const magazine_title = "หนังสือข่าวฮิวแมนเทคไทยแลนด์";
const magazine_title_en = "HumanTech Magazine";
// Reactive States
const activeTab = ref('all');
const allPublications = ref([]); //
const searchTerm = ref(''); // <--- New: For search functionality
const itemsToShow = ref(INITIAL_ITEMS); //
const overlay = ref(false);
const currentPages = ref([]);
const flipmain = ref(null);
const isFlipping = ref(false);
// Lifecycle Hook
onMounted(async () => {
const { journal, dailynews, magazine } = await infoDisseminationStore.fetchPublications();
allPublications.value = [
...journal.map(pub => ({ ...pub, type: 'journal' })),
...dailynews.map(pub => ({ ...pub, type: 'dailynews' })),
...magazine.map(pub => ({ ...pub, type: 'magazine' }))
].sort((a, b) => {
const dateA = new Date(a.createdAt);
const dateB = new Date(b.createdAt);
return dateB - dateA;
});
});
// Computed Properties
const filteredPublications = computed(() => {
let publications = allPublications.value;
// Filter by tab
if (activeTab.value !== 'all') {
publications = publications.filter(pub => pub.type === activeTab.value);
}
// Filter by search term
if (searchTerm.value) {
const lowerCaseSearchTerm = searchTerm.value.toLowerCase();
publications = publications.filter(pub =>
(pub.Title_TH && pub.Title_TH.toLowerCase().includes(lowerCaseSearchTerm)) ||
(pub.Title_EN && pub.Title_EN.toLowerCase().includes(lowerCaseSearchTerm))
);
}
return publications;
});
const displayedPublications = computed(() => {
return filteredPublications.value.slice(0, itemsToShow.value);
});
const hasMorePublications = computed(() => {
return itemsToShow.value < filteredPublications.value.length;
});
// Watcher itemsToShow Tab
watch(activeTab, () => {
itemsToShow.value = INITIAL_ITEMS; //
});
watch(searchTerm, () => {
itemsToShow.value = INITIAL_ITEMS; // <--- New:
});
// Methods
const loadMorePublications = () => {
itemsToShow.value += ITEMS_PER_LOAD;
};
const openFlipBook = (publication) => {
currentPages.value = publication.images.map(img => img.url);
overlay.value = true;
};
const flipMainLeft = () => {
if (flipmain.value && !isFlipping.value && flipmain.value.currentPage > 0) {
isFlipping.value = true;
flipmain.value.flipLeft();
setTimeout(() => {
isFlipping.value = false;
}, 700);
}
};
const flipMainRight = () => {
if (flipmain.value && !isFlipping.value && flipmain.value.currentPage < flipmain.value.numPages) {
isFlipping.value = true;
flipmain.value.flipRight();
setTimeout(() => {
isFlipping.value = false;
}, 700);
}
};
const getTypeLabel = (type, lang) => {
const currentLang = appStore.isTh ? 'th' : 'en';
const useLang = lang || currentLang;
if (useLang === 'th') {
if (type === 'journal') return journal_title;
if (type === 'dailynews') return news_title;
if (type === 'magazine') return magazine_title;
} else {
if (type === 'journal') return journal_title_en;
if (type === 'dailynews') return news_title_en;
if (type === 'magazine') return magazine_title_en;
}
return '';
};
</script>
<style scoped>
.flipbook {
width: 90vw;
height: 90vh;
max-width: 100%;
max-height: 100%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -0,0 +1,419 @@
// src/views/info-dissemination/MultimediaGallery.vue
<template>
<div class="container mx-auto px-4 md:px-8 lg:px-16 py-12 bg-white shadow-lg rounded-lg mb-12">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-blue-800">
{{ appStore.checkLang.isTh ? title : title_en }}
</h1>
<p class="mt-2 text-lg text-gray-700">
{{ appStore.checkLang.isTh ? 'คลังภาพ วิดีโอ และเสียงขององค์กร' : 'HumanTech Photo, Video, and Audio Archive' }}
</p>
</div>
<div class="mb-8 flex justify-center"> <div class="relative w-full max-w-md px-4"> <input
type="text"
v-model="searchTerm"
:placeholder="appStore.checkLang.isTh ? 'ค้นหาสื่อ (เช่น ชื่อวิดีโอ, รูปภาพ, เสียง)' : 'Search media (e.g., video, photo, audio name)'"
class="input input-bordered w-full pr-10 pl-4 py-2 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 absolute right-7 top-1/2 -translate-y-1/2 text-gray-400"> <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</div>
</div>
<div role="tablist" class="tabs tabs-boxed justify-center mb-8 bg-gray-100 p-2 rounded-lg shadow-md">
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-3 text-blue-700 font-medium transition-all duration-200"
:class="{ 'tab-active bg-blue-600 text-white shadow-lg': activeTab === 'videos' }"
@click="changeTab('videos')"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
{{ appStore.checkLang.isTh ? vdo : vdo_en }}
</a>
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-3 text-green-700 font-medium transition-all duration-200"
:class="{ 'tab-active bg-green-600 text-white shadow-lg': activeTab === 'photos' }"
@click="changeTab('photos')"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18.283 2.724a.75.75 0 0 1-.724-.724V6.75a2.25 2.25 0 0 1 2.25-2.25h10.5a2.25 2.25 0 0 1 2.25 2.25v7.21l-3.007 3.007a.75.75 0 0 1-.724.724H3.375Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.008v.008H6.75V6.75Z" />
</svg>
{{ appStore.checkLang.isTh ? photo : photo_en }}
</a>
<a
role="tab"
class="tab flex-grow sm:flex-none flex flex-col items-center justify-center p-3 text-purple-700 font-medium transition-all duration-200"
:class="{ 'tab-active bg-purple-600 text-white shadow-lg': activeTab === 'audio' }"
@click="changeTab('audio')"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.666a.75.75 0 0 1 .158 1.09L13.13 12l6.143 5.242a.75.75 0 0 1-.158 1.09l-11.52-9.83a.75.75 0 0 1 0-1.09l11.52-9.83ZM10.51 12H3.75" />
</svg>
{{ appStore.checkLang.isTh ? audio_label : audio_label_en }}
</a>
</div>
<div class="tab-content pt-3">
<p class="mb-4 text-sm text-gray-500">Active Tab: <span class="font-bold text-gray-700">{{ activeTab }}</span></p>
<div v-if="activeTab === 'videos'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="video in displayedVideos"
:key="video.id"
class="card w-full bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300"
>
<figure class="h-48 overflow-hidden bg-gray-200">
<img :src="video.thumb || ''" :alt="video.title" class="w-full h-full object-cover" />
</figure>
<div class="card-body p-4">
<h2 class="card-title text-lg font-bold text-gray-800 line-clamp-2">{{ appStore.checkLang.isTh ? video.title : video.title_en }}</h2>
<div class="card-actions justify-end mt-2">
<button class="btn btn-primary btn-sm" @click="openVideoModal(video.embedUrl)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 mr-1">
<path fill-rule="evenodd" d="M4.5 5.653c0-1.427 1.529-2.32 2.75-1.629l11.5 6.709c1.221.712 1.221 2.525 0 3.237l-11.5 6.709c-1.221.72-2.75-.173-2.75-1.629V5.653Z" clip-rule="evenodd" />
</svg>
{{ appStore.checkLang.isTh ? playVideoText : playVideoText_en }}
</button>
</div>
</div>
</div>
<div v-if="displayedVideos.length === 0 && searchTerm" class="col-span-full text-center text-gray-500 mt-6">
{{ appStore.checkLang.isTh ? 'ไม่พบวิดีโอที่ตรงกับการค้นหา' : 'No videos found matching your search.' }}
</div>
<div v-if="hasMoreVideos" class="col-span-full flex justify-center mt-6">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md" @click="loadMoreVideos">
{{ appStore.checkLang.isTh ? loadMoreVideosText : loadMoreVideosText_en }}
</button>
</div>
</div>
<div v-if="activeTab === 'photos'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="gallery in displayedGalleries"
:key="gallery.id"
class="card w-full bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300"
>
<figure class="h-48 overflow-hidden bg-gray-200">
<img :src="gallery.images[0]?.url || ''" :alt="gallery.name" class="w-full h-full object-cover" />
</figure>
<div class="card-body p-4">
<h2 class="card-title text-lg font-bold text-gray-800 line-clamp-2">{{ appStore.checkLang.isTh ? gallery.name : gallery.name_en }}</h2>
<div class="card-actions justify-end mt-2">
<button class="btn btn-secondary btn-sm" @click="openImageModal(gallery.images[0]?.url)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 mr-1">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0L9.47 16.061l-2.689-2.69a1.5 1.5 0 0 0-2.12 0L3 16.061ZM16.5 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" clip-rule="evenodd" />
</svg>
{{ appStore.checkLang.isTh ? viewPhotosText : viewPhotosText_en }}
</button>
</div>
</div>
</div>
<div v-if="displayedGalleries.length === 0 && searchTerm" class="col-span-full text-center text-gray-500 mt-6">
{{ appStore.checkLang.isTh ? 'ไม่พบรูปภาพที่ตรงกับการค้นหา' : 'No photos found matching your search.' }}
</div>
<div v-if="hasMoreGalleries" class="col-span-full flex justify-center mt-6">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md" @click="loadMoreGalleries">
{{ appStore.checkLang.isTh ? loadMorePhotosText : loadMorePhotosText_en }}
</button>
</div>
</div>
<div v-if="activeTab === 'audio'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="audioItem in displayedAudio"
:key="audioItem.id"
class="card w-full bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300"
>
<figure class="h-48 overflow-hidden bg-gray-200">
<img :src="audioItem.thumbnail || ''" :alt="audioItem.title" class="w-full h-full object-cover" />
</figure>
<div class="card-body p-4">
<h2 class="card-title text-lg font-bold text-gray-800 line-clamp-2">{{ appStore.checkLang.isTh ? audioItem.title : audioItem.title_en }}</h2>
<audio controls class="w-full mt-2">
<source :src="audioItem.file" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
</div>
<div v-if="displayedAudio.length === 0 && searchTerm" class="col-span-full text-center text-gray-500 mt-6">
{{ appStore.checkLang.isTh ? 'ไม่พบไฟล์เสียงที่ตรงกับการค้นหา' : 'No audio files found matching your search.' }}
</div>
<div v-if="hasMoreAudio" class="col-span-full flex justify-center mt-6">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md" @click="loadMoreAudio">
{{ appStore.checkLang.isTh ? loadMoreAudioText : loadMoreAudioText_en }}
</button>
</div>
</div>
</div>
<dialog id="video_modal" class="modal">
<div class="modal-box w-11/12 max-w-5xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" @click.prevent="closeVideoModal"></button>
</form>
<h3 class="font-bold text-lg mb-4">
{{ appStore.checkLang.isTh ? 'วิดีโอ' : 'Video' }}
</h3>
<div class="aspect-video w-full">
<iframe
v-if="currentVideoUrl"
:src="currentVideoUrl"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
class="w-full h-full"
></iframe>
<div v-else class="flex items-center justify-center h-full text-gray-500">
{{ appStore.checkLang.isTh ? 'ไม่มีวิดีโอที่เลือก' : 'No video selected.' }}
</div>
</div>
</div>
</dialog>
<dialog id="image_modal" class="modal">
<div class="modal-box w-11/12 max-w-xl p-6">
<h3 class="font-bold text-xl mb-4 text-green-700">{{ appStore.checkLang.isTh ? 'ดูรูปภาพ' : 'Image View' }}</h3>
<div class="flex justify-center items-center h-auto max-h-[80vh] overflow-hidden bg-gray-200 rounded-lg">
<img :src="currentImageUrl || ''" alt="Enlarged Image" class="max-w-full max-h-[75vh] object-contain rounded-lg" />
</div>
<div class="modal-action mt-6">
<form method="dialog">
<button class="btn btn-error" @click="closeImageModal">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
{{ appStore.checkLang.isTh ? 'ปิด' : 'Close' }}
</button>
</form>
</div>
</div>
</dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useAppStore } from '@/stores/app.js';
import { useInfoDisseminationStore } from '@/stores/infoDisseminationStore.js';
import { useRouter } from 'vue-router';
// Initialize Pinia stores
const appStore = useAppStore();
const infoDisseminationStore = useInfoDisseminationStore();
const router = useRouter();
// Reactive variables for component state
const activeTab = ref('videos');
const currentVideoUrl = ref('');
const currentImageUrl = ref('');
const searchTerm = ref(''); // New: Reactive variable for search term
// --- New reactive variables for load more functionality ---
const videosToShow = ref(6);
const galleriesToShow = ref(6);
const audioToShow = ref(6);
const itemsPerPage = 3; // Number of items to load each time
// Static text content (for Titles and Tabs)
const photo = "คลังภาพ";
const photo_en = "Photo Gallery";
const vdo = "คลังวีดีโอ";
const vdo_en = "Video Gallery";
const audio_label = "คลังเสียง";
const audio_label_en = "Audio Gallery";
const title = "คลังสื่อ: ภาพ วิดีโอ และเสียง";
const title_en = "Media Archive: Photos, Videos, and Audio";
// --- Static text content for Buttons (now multi-language) ---
const playVideoText = "เล่นวิดีโอ";
const playVideoText_en = "Play Video";
const viewPhotosText = "ดูรูปภาพ";
const viewPhotosText_en = "View Photos";
const loadMoreVideosText = "โหลดวิดีโอเพิ่ม";
const loadMoreVideosText_en = "Load More Videos";
const loadMorePhotosText = "โหลดรูปภาพเพิ่ม";
const loadMorePhotosText_en = "Load More Photos";
const loadMoreAudioText = "โหลดเสียงเพิ่ม";
const loadMoreAudioText_en = "Load More Audio";
const noVideoSelectedText = "ไม่มีวิดีโอที่เลือก";
const noVideoSelectedText_en = "No video selected.";
const closeButtonText = "ปิด";
const closeButtonText_en = "Close";
// Fetch all necessary data on component mount
onMounted(() => {
infoDisseminationStore.fetchVideos();
infoDisseminationStore.fetchGalleries();
infoDisseminationStore.fetchAudio();
});
// --- New computed properties for filtered items ---
const filteredVideos = computed(() => {
if (!searchTerm.value) {
return infoDisseminationStore.getVideosForDisplay;
}
const lowerCaseSearchTerm = searchTerm.value.toLowerCase();
return infoDisseminationStore.getVideosForDisplay.filter(video => {
const titleMatch = video.title?.toLowerCase().includes(lowerCaseSearchTerm);
const titleEnMatch = video.title_en?.toLowerCase().includes(lowerCaseSearchTerm);
return titleMatch || titleEnMatch;
});
});
const filteredGalleries = computed(() => {
if (!searchTerm.value) {
return infoDisseminationStore.getGalleriesForDisplay;
}
const lowerCaseSearchTerm = searchTerm.value.toLowerCase();
return infoDisseminationStore.getGalleriesForDisplay.filter(gallery => {
const nameMatch = gallery.name?.toLowerCase().includes(lowerCaseSearchTerm);
const nameEnMatch = gallery.name_en?.toLowerCase().includes(lowerCaseSearchTerm);
return nameMatch || nameEnMatch;
});
});
const filteredAudio = computed(() => {
if (!searchTerm.value) {
return infoDisseminationStore.getAudioForDisplay;
}
const lowerCaseSearchTerm = searchTerm.value.toLowerCase();
return infoDisseminationStore.getAudioForDisplay.filter(audioItem => {
const titleMatch = audioItem.title?.toLowerCase().includes(lowerCaseSearchTerm);
const titleEnMatch = audioItem.title_en?.toLowerCase().includes(lowerCaseSearchTerm);
return titleMatch || titleEnMatch;
});
});
// --- Updated computed properties for displayed items (using filtered data) ---
const displayedVideos = computed(() => {
return filteredVideos.value.slice(0, videosToShow.value);
});
const displayedGalleries = computed(() => {
return filteredGalleries.value.slice(0, galleriesToShow.value);
});
const displayedAudio = computed(() => {
return filteredAudio.value.slice(0, audioToShow.value);
});
// --- Updated computed properties to check if there are more items to load (using filtered data) ---
const hasMoreVideos = computed(() => {
return videosToShow.value < filteredVideos.value.length;
});
const hasMoreGalleries = computed(() => {
return galleriesToShow.value < filteredGalleries.value.length;
});
const hasMoreAudio = computed(() => {
return audioToShow.value < filteredAudio.value.length;
});
// --- Methods for loading more items ---
const loadMoreVideos = () => {
videosToShow.value += itemsPerPage;
};
const loadMoreGalleries = () => {
galleriesToShow.value += itemsPerPage;
};
const loadMoreAudio = () => {
audioToShow.value += itemsPerPage;
};
// --- Method to reset item counts when changing tabs and also clear search term ---
const changeTab = (tabName) => {
activeTab.value = tabName;
searchTerm.value = ''; // Clear search term when changing tabs
// Reset counts when switching tabs to show initial 6 items for the new tab
videosToShow.value = 6;
galleriesToShow.value = 6;
audioToShow.value = 6;
};
// --- Watch for changes in searchTerm to reset display counts ---
watch(searchTerm, () => {
videosToShow.value = 6;
galleriesToShow.value = 6;
audioToShow.value = 6;
});
// Watchers for debugging (keep these for now, can remove in production)
watch(
() => infoDisseminationStore.getVideosForDisplay,
(newVideos) => {
if (newVideos && newVideos.length > 0) {
console.log('Watch: ข้อมูลวิดีโอพร้อมแสดงผลแล้ว:', newVideos);
}
},
{ deep: true, immediate: true }
);
watch(
() => infoDisseminationStore.getGalleriesForDisplay,
(newGalleries) => {
if (newGalleries && newGalleries.length > 0) {
console.log('Watch: ข้อมูลแกลเลอรี่พร้อมแสดงผลแล้ว:', newGalleries);
}
},
{ deep: true, immediate: true }
);
watch(
() => infoDisseminationStore.getAudioForDisplay,
(newAudio) => {
if (newAudio && newAudio.length > 0) {
console.log('Watch: ข้อมูลเสียงพร้อมแสดงผลแล้ว:', newAudio);
}
},
{ deep: true, immediate: true }
);
// Video Modal functions
const openVideoModal = (url) => {
console.log("URL ที่ได้รับใน openVideoModal (จาก Store):", url);
if (url) {
currentVideoUrl.value = url;
document.getElementById('video_modal').showModal();
} else {
console.error("ไม่ได้รับ URL วิดีโอที่ถูกต้องจากปุ่ม Play.");
}
};
const closeVideoModal = () => {
currentVideoUrl.value = '';
const modal = document.getElementById('video_modal');
if (modal) modal.close();
};
// Image Modal functions
const openImageModal = (url) => {
console.log("Opening image modal with URL:", url);
currentImageUrl.value = url;
document.getElementById('image_modal').showModal();
};
const closeImageModal = () => {
currentImageUrl.value = '';
document.getElementById('image_modal').close();
};
</script>
<style scoped>
.tab-content {
display: block !important;
}
</style>

View File

@ -0,0 +1,230 @@
// src/views/info-dissemination/PublicationsView.vue
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-blue-800 text-center mb-6">
{{ appStore.isTh ? header : header_en }}
</h1>
<div class="mb-6 max-w-3xl mx-auto">
<label class="input input-bordered flex items-center gap-2 shadow-md w-full">
<input
type="text"
class="grow"
:placeholder="appStore.isTh ? 'ค้นหาเอกสารด้วยชื่อ...' : 'Search documents by name...'"
v-model="searchTerm"
/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 opacity-70">
<path fill-rule="evenodd" d="M9.965 11.023A5.996 5.996 0 0 1 5.996 12C2.686 12 0 9.314 0 5.996 0 2.686 2.686 0 5.996 0c3.308 0 5.996 2.686 5.996 5.996 0 1.544-.582 2.977-1.547 4.027l2.675 2.675c.39.39.39 1.024 0 1.414-.39.39-1.024.39-1.414 0l-2.675-2.675ZM5.996 10.5c2.47 0 4.496-2.026 4.496-4.496S8.466 1.5 5.996 1.5 1.5 3.526 1.5 6.004C1.5 8.47 3.093 10.5 5.996 10.5Z" clip-rule="evenodd" />
</svg>
</label>
</div>
<div v-if="displayedDocuments.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div
v-for="(d, i) in displayedDocuments"
:key="d.id"
class="card bg-base-100 shadow-xl overflow-hidden group relative"
>
<figure class="w-full h-48">
<img
:src="d.thumbnail.url"
:alt="appStore.isTh ? d.name : d.name_en"
class="w-full h-full object-cover"
/>
</figure>
<div
class="absolute inset-0 bg-teal-700 bg-opacity-75 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div class="flex space-x-4">
<button
class="btn btn-circle btn-info"
@click="openFlipBook(d)"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-white">
<path d="M2.25 18.75a60.076 60.076 0 0 1 1.5-3.004v-7.135C3.75 6.07 4.136 5.5 4.875 5.5H8.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66.54 1.2 1.2 1.2h3.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66.54 1.2 1.2 1.2h3.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66-.54 1.2-1.2 1.2h-3.25c-.66 0-1.2-.54-1.2-1.2v-3.25c0-.66-.54-1.2-1.2-1.2H12c-.66 0-1.2.54-1.2 1.2v3.25c0 .66-.54 1.2-1.2 1.2H4.875c-.739 0-1.125-.57-1.125-1.096v-7.135a60.076 60.076 0 0 1-1.5 3.004ZM4.875 5.5h-.125V3.75c0-.66-.54-1.2-1.2-1.2h-3.25c-.66 0-1.2.54-1.2 1.2v3.25c0 .66.54 1.2 1.2 1.2h3.25c.66 0 1.2-.54 1.2-1.2V5.5ZM12 12c0-.66.54-1.2 1.2-1.2h3.25c.66 0 1.2.54 1.2 1.2v3.25c0 .66-.54 1.2-1.2 1.2h-3.25c-.66 0-1.2-.54-1.2-1.2v-3.25Z" />
</svg>
</button>
<a
v-if="d.file && d.file.url"
:href="d.file.url"
target="_blank"
class="btn btn-circle btn-info"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-white">
<path fill-rule="evenodd" d="M11.828 2.25c-.965 0-1.75.785-1.75 1.75v14.5c0 .965.785 1.75 1.75 1.75H18.75c.965 0 1.75-.785 1.75-1.75V8.187c0-.284-.113-.559-.313-.75L15.313 3.107a1.75 1.75 0 0 0-.75-.313H11.828ZM10.5 4a.75.75 0 0 0-.75-.75H6.25A1.75 1.75 0 0 0 4.5 5.75v14.5c0 .965.785 1.75 1.75 1.75h4.25a.75.75 0 0 0 0-1.5H6.25a.25.25 0 0 1-.25-.25V5.75c0-.138.112-.25.25-.25h3.5A.75.75 0 0 0 10.5 4ZM15.75 5.5a.25.25 0 0 1 .25.25v2.25h2.25a.25.25 0 0 1 .25.25v.75H16.25a1.75 1.75 0 0 0-1.75 1.75v3.25c0 .414-.336.75-.75.75H13a.75.75 0 0 0-.75.75v.75c0 .414.336.75.75.75h1.75a.75.75 0 0 0 .75-.75v-.75a.75.75 0 0 0-.75-.75H13a.25.25 0 0 1-.25-.25V10.25c0-.138.112-.25.25-.25h2.25a.75.75 0 0 0 .75-.75V5.75Z" clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
<div class="card-body p-4">
<h6 class="text-base font-semibold text-blue-600">
{{ appStore.isTh ? d.name : d.name_en }}
</h6>
</div>
</div>
</div>
<div v-else-if="searchTerm" class="text-center text-gray-500 text-lg mt-8">
{{ appStore.isTh ? 'ไม่พบเอกสารที่ตรงกับคำค้นหา' : 'No matching documents found.' }}
</div>
<div v-else class="text-center text-gray-500 text-lg mt-8">
{{ appStore.isTh ? 'กำลังโหลดเอกสาร...' : 'Loading documents...' }}
</div>
<div v-if="hasMoreDocuments" class="text-center mt-8">
<button class="btn bg-[#1b3872] text-white hover:bg-[#1b3872]/90 rounded-none border-none shadow-md" @click="loadMoreDocuments">
{{ appStore.isTh ? 'โหลดเอกสารเพิ่ม' : 'Load More' }}
</button>
</div>
<div
v-if="overlay"
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
>
<div class="relative w-full h-full max-w-5xl max-h-5xl bg-base-100 rounded-box shadow-2xl">
<button
class="btn btn-sm btn-circle btn-error absolute top-4 right-4 z-10"
@click="overlay = false"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" clip-rule="evenodd" />
</svg>
</button>
<Flipbook ref="flipbookComponent" :pages="currentFlipbookPages" class="w-full h-full" />
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex space-x-2">
<button
class="btn btn-circle btn-sm btn-info"
@click="flipLeft()"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 1 1 1.06 1.06L9.31 12l6.97 6.97a.75.75 0 1 1-1.06 1.06l-7.5-7.5Z" clip-rule="evenodd" />
</svg>
</button>
<button
class="btn btn-circle btn-sm btn-info"
@click="flipRight()"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-white">
<path fill-rule="evenodd" d="M16.28 12.28a.75.75 0 0 0 0-1.06l-7.5-7.5a.75.75 0 0 0-1.06 1.06L14.69 12l-6.97 6.97a.75.75 0 1 0 1.06 1.06l7.5-7.5Z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { useInfoDisseminationStore } from '@/stores/infoDisseminationStore';
import { useAppStore } from '@/stores/app';
// Import Flipbook component
import Flipbook from 'flipbook-vue';
// Constants
const INITIAL_ITEMS = 8;
const ITEMS_PER_LOAD = 4;
const infoDisseminationStore = useInfoDisseminationStore();
const appStore = useAppStore();
const header = "เอกสารเผยแพร่";
const header_en = "HumanTech Documents";
// Reactive States
const itemsToShow = ref(INITIAL_ITEMS);
const searchTerm = ref('');
const overlay = ref(false);
const currentFlipbookPages = ref([]);
const flipbookComponent = ref(null);
const isFlipping = ref(false);
// Computed Properties
const allFilteredDocuments = computed(() => {
let documents = infoDisseminationStore.getFilteredDocuments;
if (searchTerm.value) {
const lowerCaseSearchTerm = searchTerm.value.toLowerCase();
documents = documents.filter(doc =>
(doc.name && doc.name.toLowerCase().includes(lowerCaseSearchTerm)) ||
(doc.name_en && doc.name_en.toLowerCase().includes(lowerCaseSearchTerm))
);
}
return documents;
});
const displayedDocuments = computed(() => {
return allFilteredDocuments.value.slice(0, itemsToShow.value);
});
const hasMoreDocuments = computed(() => {
return itemsToShow.value < allFilteredDocuments.value.length;
});
// Watcher itemsToShow
watch(searchTerm, () => {
itemsToShow.value = INITIAL_ITEMS;
});
// Lifecycle Hook
onMounted(async () => {
await infoDisseminationStore.fetchDocuments();
// infoDisseminationStore.updatePdfDataState(); // fetchDocuments PDF
});
// Methods
const openFlipBook = (document) => { // index document object
// images flipbook
if (document.images && document.images.length > 0) {
currentFlipbookPages.value = document.images.map(img => img.url); // images array object url
overlay.value = true;
} else {
// Flipbook
alert(appStore.isTh ? 'ไม่มีรูปภาพสำหรับ Flipbook ของเอกสารนี้' : 'No images available for Flipbook for this document.');
console.warn('No images available for flipbook for this document:', document);
}
};
const loadMoreDocuments = () => {
itemsToShow.value += ITEMS_PER_LOAD;
};
const flipLeft = () => {
// flipbookComponent , ,
if (flipbookComponent.value && !isFlipping.value && flipbookComponent.value.currentPage > 0) {
isFlipping.value = true;
flipbookComponent.value.flipLeft();
setTimeout(() => {
isFlipping.value = false;
}, 700); // transition Flipbook
}
};
const flipRight = () => {
// flipbookComponent , ,
if (flipbookComponent.value && !isFlipping.value && flipbookComponent.value.currentPage < flipbookComponent.value.numPages) {
isFlipping.value = true;
flipbookComponent.value.flipRight();
setTimeout(() => {
isFlipping.value = false;
}, 700); // transition Flipbook
}
};
</script>
<style scoped>
/* สไตล์สำหรับ Flipbook และ overlay */
.flipbook {
width: 90vw;
height: 90vh;
max-width: 100%;
max-height: 100%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
</style>