Major update of Vue Website Template
494
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
BIN
public/images/accountability-image.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/images/collaboration-image.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
public/images/company_history_1.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/images/company_history_2.jpg
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
public/images/company_history_3.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
public/images/continuous-improvement-image.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/images/default-audio-icon.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
public/images/excellence-image.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/images/innovation-image.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
public/images/integrity-image.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/images/mock-commander-1.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/images/youtube-default-thumb.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/mock-audio/rtaf_march.mp3
Normal file
BIN
public/mock-images/daily1-page1.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
public/mock-images/daily1-page2.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
public/mock-images/daily1-thumb.jpg
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
public/mock-images/doc1-page1.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
public/mock-images/doc1-page2.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/mock-images/doc1-page3.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
public/mock-images/doc1-thumb.jpg
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
public/mock-images/doc2-page1.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/mock-images/doc2-page2.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
public/mock-images/doc2-thumb.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
public/mock-images/gallery1.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/mock-images/gallery2.jpg
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
public/mock-images/gallery3.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/mock-images/gallery4.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
public/mock-images/gallery5.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/mock-images/gallery6.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
public/mock-images/gallery7.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/mock-images/journal1-page1.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
public/mock-images/journal1-page2.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/mock-images/journal1-thumb.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
public/mock-images/mag1-page1.jpg
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
public/mock-images/mag1-page2.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
public/mock-images/mag1-page3.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
public/mock-images/mag1-thumb.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/mock-images/mag2-page1.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/mock-images/mag2-page2.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
public/mock-images/mag2-thumb.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/mock-pdfs/daily1.pdf
Normal file
BIN
public/mock-pdfs/doc1.pdf
Normal file
BIN
public/mock-pdfs/journal1.pdf
Normal file
BIN
public/mock-pdfs/mag1.pdf
Normal file
BIN
public/mock-pdfs/mag2.pdf
Normal file
BIN
public/uploads/executive_1.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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) + '...';
|
||||
|
||||
556
src/components/OrganizationTreeDiagram.vue
Normal file
@ -0,0 +1,556 @@
|
||||
// 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);
|
||||
|
||||
// --- เพิ่ม LOG ตรงนี้ ---
|
||||
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);
|
||||
|
||||
// Filter visible nodes based on current children/depth
|
||||
// A node is visible if it's the root, or if its parent is visible and it's in the parent's 'children' array
|
||||
const visibleNodes = allNodesInLayout.filter(d => {
|
||||
// The dummy_root is always "visible" in terms of layout calculation,
|
||||
// but not rendered as a node.
|
||||
if (d.id === 'dummy_root') return true;
|
||||
|
||||
// For any other node, it's visible if it's a direct child of an expanded node
|
||||
// or if it's the actual root (depth 0 for object root, depth 1 for array root).
|
||||
const isActualRoot = (!isArrayRoot && d.depth === 0) || (isArrayRoot && d.depth === 1 && d.parent.id === 'dummy_root');
|
||||
|
||||
// If it's a root or if its parent is expanded (has children, not _children)
|
||||
// AND it's one of the current direct children of its parent
|
||||
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');
|
||||
|
||||
// 1. Generate standard D3 links (parent-child relationships) for visible nodes
|
||||
// Ensure source and target are both visible for the link to be considered.
|
||||
treemap(rootNode).links().forEach(link => {
|
||||
// If using dummy_root, only draw links *from* dummy_root if they lead to an actual root node,
|
||||
// AND that actual root node is NOT a dashed one.
|
||||
if (isArrayRoot && link.source.id === 'dummy_root') {
|
||||
// Check if target node's data exists and is not marked as dashed itself
|
||||
if (link.target.data && !link.target.data.isDashed && nodesToRender.some(n => n.id === link.target.id)) {
|
||||
links.push(link);
|
||||
}
|
||||
} else {
|
||||
// For normal parent-child links, both source and target must be visible nodes to render
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Add custom dashed links (e.g., independent units reporting to mainRoot)
|
||||
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) {
|
||||
// Create a new link object for the dashed line
|
||||
// Ensure it uses the actual D3 node objects for source and target
|
||||
// Add a 'data' property directly to this link object to mark it as dashed
|
||||
links.push({
|
||||
source: mainRoot, // The D3 node object for alpha_root
|
||||
target: node, // The D3 node object for the dashed node
|
||||
data: { // This 'data' property is specific to this link object
|
||||
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';
|
||||
// Check for dashed status on the link object first, then target node
|
||||
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>
|
||||
@ -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');
|
||||
|
||||
@ -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
@ -14,6 +14,7 @@ export const useAppStore = defineStore('app', {
|
||||
{ title: "ข่าวบริการประชาชน", title_en: "Public Service News", category: "EventActivities" },
|
||||
],
|
||||
// *** ข้อมูลสำหรับ Header ***
|
||||
// *** ข้อมูลสำหรับ Header ที่ปรับปรุงแล้ว ***
|
||||
headers: {
|
||||
header_background: { url: '/images/news_header_bg_b815923058.png' },
|
||||
logo: { url: '/images/Enter.png' }, // ใช้รูปโลโก้ตามที่ระบุ
|
||||
@ -22,40 +23,170 @@ 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 +299,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 +329,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 +1021,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 +1130,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 +1194,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
|
||||
|
||||
327
src/stores/infoDisseminationStore.js
Normal file
@ -0,0 +1,327 @@
|
||||
// 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 นี้:**
|
||||
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) => {
|
||||
// **แก้ไข: ลบ () ออก** เพราะ getMockAudioData เป็น getter ที่คืนค่า Array โดยตรง
|
||||
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(); // <--- Import และเรียกใช้ appStore ภายใน getter
|
||||
// **แก้ไข: ลบ () ออก**
|
||||
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(); // <--- Import และเรียกใช้ appStore ภายใน getter
|
||||
// **แก้ไข: ลบ () ออก**
|
||||
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;
|
||||
},
|
||||
|
||||
// Getter เหล่านี้ไม่จำเป็นต้องใช้โดยตรงใน JournalView.vue แล้ว
|
||||
// เพราะเราจะใช้ allPublications แล้วกรองด้วย computed property ใน JournalView.vue แทน
|
||||
// แต่ถ้ามี component อื่นที่ยังใช้ สามารถเก็บไว้ได้
|
||||
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() {
|
||||
// ดึงข้อมูล Publications ดิบจาก Mock
|
||||
// **แก้ไข: ลบ () ออก**
|
||||
const rawPublications = this.getMockPublicationsData;
|
||||
this.allRawPublications = rawPublications;
|
||||
|
||||
// กรองข้อมูลตามประเภท (คล้ายกับ logic ใน getters แต่ทำใน action เพื่อคืนค่า)
|
||||
// Note: Journal (ตัว J ใหญ่) เป็นค่า type ที่ถูกต้องตาม mock data
|
||||
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
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
240
src/views/about/CommanderBioView.vue
Normal 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>
|
||||
170
src/views/about/CommandingOfficersView.vue
Normal 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>
|
||||
120
src/views/about/HallView.vue
Normal 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>
|
||||
176
src/views/about/HistoryView.vue
Normal 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>
|
||||
51
src/views/about/OrganizationStructureView.vue
Normal 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>
|
||||
293
src/views/about/VisionMissionView.vue
Normal 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>
|
||||
313
src/views/info-dissemination/JournalView.vue
Normal 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>
|
||||
419
src/views/info-dissemination/MultimediaGallery.vue
Normal 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>
|
||||
230
src/views/info-dissemination/PublicationsView.vue
Normal 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'; // หรือ import Flipbook จาก path ที่ถูกต้องของคุณ เช่น '@/components/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([]); // เปลี่ยนชื่อจาก 'pages' เป็น 'currentFlipbookPages'
|
||||
const flipbookComponent = ref(null); // เปลี่ยนชื่อจาก 'flip' เป็น 'flipbookComponent'
|
||||
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>
|
||||