Compare commits

..

33 Commits

Author SHA1 Message Date
ray650128 f4c4c6906e 新增單日表格補上已有資料判斷 2026-08-11 11:55:25 +08:00
ray650128 0cf0f8c49b 加上記憶體限制 2026-08-11 11:19:10 +08:00
Raymond Yang f8a1328a03 加上新增整月的表格 2026-08-11 11:14:47 +08:00
ray650128 f677fdb162 補上所有序列化欄位mapping 2026-08-09 12:46:55 +08:00
ray650128 abd4c24c83 補上內網allowHost 2026-08-09 11:50:12 +08:00
ray650128 7c390aebdf 修正成員欄位屬性 2026-08-09 02:57:50 +08:00
ray650128 7f0d0f4a87 修正更新成員沒有回應資料的問題 2026-08-09 02:22:39 +08:00
ray650128 8b20758404 加上生成戰報表的功能 2026-08-09 01:44:19 +08:00
ray650128 0741d5846b 修正BattleRecord沒有加上@Serializable註釋 2026-08-09 01:30:40 +08:00
ray650128 790e55f6fa 加上查詢單筆戰鬥履歷、更新戰鬥履歷功能 2026-08-08 23:25:47 +08:00
ray650128 5dfaa68e0e 調整CORS設定 2026-08-08 19:55:48 +08:00
ray650128 e936250391 補上是否掉刀欄位 2026-08-08 00:23:02 +08:00
ray650128 017b7edc24 修正更新戰隊成員回傳結果 2026-08-07 18:14:34 +08:00
ray650128 4e32adbe44 修正新增戰隊成員回傳結果 2026-08-06 21:26:43 +08:00
ray650128 44601131a2 修正新增出刀紀錄回傳結果 2026-08-06 21:00:52 +08:00
ray650128 6fb1c9fb2e 修正port 2026-08-06 21:00:28 +08:00
ray650128 e5f4f423f4 修正依賴 2026-08-06 21:00:17 +08:00
ray650128 77a1690e08 調整各層資料轉換 2026-08-04 21:24:24 +08:00
ray650128 8c99ba1147 初步實作出刀紀錄表 2026-08-04 21:11:59 +08:00
Raymond Yang fcb5285788 重構成員管理部分 2026-08-04 16:36:38 +08:00
ray650128 28ad63ffd9 更新kmongo 2026-08-03 23:31:15 +08:00
ray650128 73c78cd9e7 更改mongo身分驗證 2026-08-03 22:51:32 +08:00
ray650128 80563bfb7b 加上容器化 2026-08-03 22:32:14 +08:00
Raymond Yang 42356d0f88 更改DB域名 2023-08-10 12:23:26 +08:00
Raymond Yang 9618374942 暫時移除前端,維持前後端分離 2023-07-11 11:39:18 +08:00
Raymond Yang 1dca536ab0 將前端整合進來 2023-07-10 12:08:58 +08:00
Raymond Yang 9614266453 修正開戰期間成員取得判斷 2023-07-10 12:04:55 +08:00
Raymond Yang dd2bbf46a6 拿掉print 2023-07-10 12:04:17 +08:00
Raymond Yang 97effcc442 清理import 2023-07-10 12:03:57 +08:00
ray650128 4131f03e42 加上updateTimer模組 2023-06-27 23:27:30 +08:00
Raymond Yang 4632b2cdc8 加上print date 2023-06-27 17:52:03 +08:00
Raymond Yang 947a791d87 加上shadowJar plugin 2023-06-27 17:03:48 +08:00
Raymond Yang 89cd1357f0 補上成員的刪除方法 2023-06-27 15:58:16 +08:00
59 changed files with 2135 additions and 691 deletions
+2
View File
@@ -0,0 +1,2 @@
# .env
DATA_DIR=/srv/dev-disk-by-uuid-0c76cdc9-484e-45a6-9283-49e75e12eece/dockerApp/ReDiveClanBattleRecordBackend
+18
View File
@@ -0,0 +1,18 @@
# 使用 Kotlin + JDK 的輕量化映像
FROM gradle:9.5.1-jdk25 AS builder
WORKDIR /app
COPY . .
ENV GRADLE_OPTS="-Xmx1536m -XX:MaxMetaspaceSize=512m"
ENV NODE_OPTIONS="--max-old-space-size=1536"
RUN gradle clean shadowJar --no-daemon
# 運行階段
FROM eclipse-temurin:25-jdk-ubi10-minimal
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
CMD ["java", "-jar", "app.jar", "club.sakunadaisuki.pcrediveclanrecordbackend.mainKt"]
+38
View File
@@ -0,0 +1,38 @@
# PcReDiveClanRecordBackend2
This project was created using the [Ktor Project Generator](https://start.ktor.io).
Here are some useful links to get you started:
* [Ktor Documentation](https://ktor.io/docs/home.html)
* [Ktor GitHub page](https://github.com/ktorio/ktor)
* [Ktor Slack chat](https://app.slack.com/client/T09229ZC6/C0A974TJ9). [Request an invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up).
## Features
Here's a list of features included in this project:
| Name | Description |
|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [CORS](https://start.ktor.io/p/io.ktor/server-cors) | Enables Cross-Origin Resource Sharing (CORS) |
| [Swagger](https://start.ktor.io/p/io.ktor/server-swagger) | Serves Swagger UI for your project |
| [Content Negotiation](https://start.ktor.io/p/io.ktor/server-content-negotiation) | Provides automatic content conversion according to Content-Type and Accept headers |
| [kotlinx.serialization](https://start.ktor.io/p/io.ktor/server-kotlinx-serialization) | Handles JSON serialization using kotlinx.serialization library |
| [MongoDB](https://start.ktor.io/p/org.jetbrains/server-mongodb) | Adds MongoDB database support |
## Building & Running
To build or run the project, use one of the following tasks:
| Task | Description |
|-------------------|-------------------|
| `./gradlew test` | Run the tests |
| `./gradlew build` | Build the project |
| `./gradlew run` | Run the server |
If the server starts successfully, you'll see the following output:
```
2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds.
2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080
```
+24 -29
View File
@@ -1,38 +1,33 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val kmongo_version: String by project
plugins { plugins {
kotlin("jvm") version "1.8.22" alias(libs.plugins.kotlin.jvm)
id("io.ktor.plugin") version "2.3.1" alias(ktorLibs.plugins.ktor)
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.22" alias(libs.plugins.kotlin.serialization)
} }
group = "com.ray650128.pcredive" group = "club.sakunadaisuki"
version = "0.0.1" version = "1.0.0-SNAPSHOT"
application { application {
mainClass.set("io.ktor.server.netty.EngineMain") mainClass = "io.ktor.server.netty.EngineMain"
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
} }
repositories { kotlin {
mavenCentral() jvmToolchain(21)
} }
dependencies { dependencies {
implementation("io.ktor:ktor-server-cors-jvm:$ktor_version") implementation(ktorLibs.serialization.kotlinx.json)
implementation("io.ktor:ktor-server-core-jvm:$ktor_version") implementation(ktorLibs.server.config.yaml)
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") implementation(ktorLibs.server.contentNegotiation)
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") implementation(ktorLibs.server.core)
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") implementation(ktorLibs.server.cors)
implementation("ch.qos.logback:logback-classic:$logback_version") implementation(ktorLibs.server.netty)
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") implementation(ktorLibs.server.routingOpenapi)
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") implementation(ktorLibs.server.swagger)
implementation(libs.logback.classic)
implementation(libs.mongodb.bson)
implementation(libs.mongodb.driverCore)
implementation(libs.mongodb.driverSync)
implementation("org.litote.kmongo:kmongo:$kmongo_version") testImplementation(kotlin("test"))
implementation("org.litote.kmongo:kmongo-id-serialization:$kmongo_version") testImplementation(ktorLibs.server.testHost)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") }
}
+11
View File
@@ -0,0 +1,11 @@
version: '3.8'
services:
app:
build: .
container_name: clan-battle-record-backend
env_file:
- .env
volumes:
- ${DATA_DIR}:/app/data
restart: always
+1 -4
View File
@@ -1,5 +1,2 @@
ktor_version=2.3.1
kotlin_version=1.8.22
logback_version=1.2.11
kotlin.code.style=official kotlin.code.style=official
kmongo_version=4.5.0
+15
View File
@@ -0,0 +1,15 @@
[versions]
kotlin = "2.4.0"
logback = "1.5.35"
mongodb = "5.8.0"
[libraries]
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
mongodb-bson = { module = "org.mongodb:bson", version.ref = "mongodb" }
mongodb-driverCore = { module = "org.mongodb:mongodb-driver-core", version.ref = "mongodb" }
mongodb-driverSync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongodb" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Binary file not shown.
+5 -1
View File
@@ -1,5 +1,9 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+34 -20
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
# Gradle start up script for POSIX generated by Gradle. # gradlew start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@@ -27,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh Gradle # ksh gradlew
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -133,22 +132,29 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -165,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+28 -35
View File
@@ -13,19 +13,22 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem gradlew startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -40,15 +43,15 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -56,34 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute gradlew
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if "%ERRORLEVEL%"=="0" goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+22 -1
View File
@@ -1 +1,22 @@
rootProject.name = "PcReDivePveRecordBackend" pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("ktorLibs").from("io.ktor:ktor-version-catalog:3.5.0")
}
}
rootProject.name = "PcReDiveClanRecordBackend"
@@ -0,0 +1,44 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.database.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.bson.Document
import java.time.Instant
@Serializable
data class BattleRecordDto(
val date: String, // yyyy-MM-dd
var uid: String,
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
@SerialName("attack_detail_list") val attackDetailList: List<AttackDetailDto>
) {
fun toDocument(): Document = Document.parse(Json.encodeToString(this))
companion object {
private val json = Json { ignoreUnknownKeys = true }
fun fromDocument(document: Document): BattleRecordDto = json.decodeFromString(document.toJson())
}
}
@Serializable
data class AttackDetailDto(
@SerialName("boss_round") val bossRound: Int, // 周目
@SerialName("boss_index") val bossIndex: Int, // 1~5 號王
val damage: Long, // 傷害
@SerialName("is_compensation") val isCompensation: Boolean = false, // true: 補償刀, false: 正刀
@SerialName("is_dropped") val isDropped: Boolean = false, // true: 掉刀, false: 沒掉刀
@SerialName("created_at") val createdAt: Long = Instant.now().toEpochMilli(),
@SerialName("updated_at") val updatedAt: Long = Instant.now().toEpochMilli()
) {
fun toDocument(): Document = Document.parse(Json.encodeToString(this))
companion object {
private val json = Json { ignoreUnknownKeys = true }
fun fromDocument(document: Document): AttackDetailDto = json.decodeFromString(document.toJson())
}
}
@@ -0,0 +1,26 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.database.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.bson.Document
import java.time.Instant
@Serializable
data class MemberDto(
@SerialName("uid") var uid: String,
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
@SerialName("leave") var leave: Boolean,
@SerialName("created_at") var createAt: Long? = null,
@SerialName("updated_at") var updatedAt: Long? = null
) {
fun toDocument(): Document = Document.parse(Json.encodeToString(this))
companion object {
private val json = Json { ignoreUnknownKeys = true }
fun fromDocument(document: Document): MemberDto = json.decodeFromString(document.toJson())
}
}
@@ -0,0 +1,100 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.database.service
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.MemberDto
import club.sakunadaisuki.pcrediveclanrecordbackend.mapper.toEmptyBattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecord
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import com.mongodb.client.model.Filters
import com.mongodb.client.model.Filters.and
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.bson.Document
import java.time.LocalDate
import java.time.YearMonth
class BattleRecordService(private val database: MongoDatabase) {
var collection: MongoCollection<Document>
var memberCollection: MongoCollection<Document>
init {
database.createCollection("battle_records")
collection = database.getCollection("battle_records")
database.createCollection("member")
memberCollection = database.getCollection("member")
}
// Create new battle record
suspend fun create(data: BattleRecordDto): BattleRecordDto = withContext(Dispatchers.IO) {
val doc = data.toDocument()
collection.insertOne(doc)
doc.let(BattleRecordDto::fromDocument)
}
// Read a battle record
suspend fun read(date: String): List<BattleRecordDto> = withContext(Dispatchers.IO) {
collection.find(Filters.eq("date", date)).toList().map(BattleRecordDto::fromDocument)
}
// Read a battle record
suspend fun read(date: String, uid: String): BattleRecordDto? = withContext(Dispatchers.IO) {
val filter = and(
Filters.eq("date", date),
Filters.eq("uid", uid)
)
collection.find(filter).first()?.let(BattleRecordDto::fromDocument)
}
// Update a battle record
suspend fun update(date: String, data: BattleRecordDto): BattleRecordDto? = withContext(Dispatchers.IO) {
val filter = and(
Filters.eq("date", date),
Filters.eq("uid", data.uid)
)
collection.findOneAndReplace(filter, data.toDocument())
collection.find(filter).first()?.let(BattleRecordDto::fromDocument)
}
// Delete a battle record
suspend fun delete(date: String, uid: String): Document? = withContext(Dispatchers.IO) {
val filter = and(
Filters.eq("date", date),
Filters.eq("uid", uid)
)
collection.findOneAndDelete(filter)
}
suspend fun createTable(date: String): List<BattleRecordDto> = withContext(Dispatchers.IO) {
val memberList = memberCollection.find(Filters.eq("leave", false)).toList().map(MemberDto::fromDocument)
val emptyBattleRecordList = memberList.map { it.toEmptyBattleRecordDto(date) }
collection.insertMany(emptyBattleRecordList.map { it.toDocument() })
collection.find(Filters.eq("date", date)).toList().map(BattleRecordDto::fromDocument)
}
suspend fun createTableMonth(date: String): List<List<BattleRecordDto>> = withContext(Dispatchers.IO) {
val memberList = memberCollection.find(Filters.eq("leave", false)).toList().map(MemberDto::fromDocument)
// 1. 指定年份與月份(例如:2026年8月)
val dateSplit = date.split("-").map { it.toInt() }
val yearMonth = YearMonth.of(dateSplit[0], dateSplit[1])
// 2. 取得該月最後一天的 LocalDate 物件
val lastDayOfMonth = yearMonth.atEndOfMonth()
// 3. 取得最後 5 天的日期列表(由舊到新排序)
val lastFiveDays: List<LocalDate> = (4 downTo 0).map { lastDayOfMonth.minusDays(it.toLong()) }
// 4. Temp list
val tables = mutableListOf<List<BattleRecordDto>>()
// 5. 寫入資料
lastFiveDays.forEach { date ->
val emptyBattleRecordList = memberList.map { it.toEmptyBattleRecordDto(date.toString()) }
tables.add(emptyBattleRecordList)
collection.insertMany(emptyBattleRecordList.map { it.toDocument() })
}
tables
}
}
@@ -0,0 +1,57 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.database.service
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.MemberDto
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import com.mongodb.client.model.Filters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.bson.Document
class MemberService(private val database: MongoDatabase) {
var collection: MongoCollection<Document>
init {
database.createCollection("member")
collection = database.getCollection("member")
}
// Create new member
suspend fun create(data: MemberDto): MemberDto = withContext(Dispatchers.IO) {
data.createAt = System.currentTimeMillis()
data.updatedAt = System.currentTimeMillis()
data.leave = false
val doc = data.toDocument()
collection.insertOne(doc)
doc.let(MemberDto::fromDocument)
}
// Read a member
suspend fun read(): List<MemberDto> = withContext(Dispatchers.IO) {
collection.find().toList().map(MemberDto::fromDocument)
}
// Read a member
suspend fun read(uid: String): MemberDto? = withContext(Dispatchers.IO) {
collection.find(Filters.eq("uid", uid)).first()?.let(MemberDto::fromDocument)
}
// Update a member
suspend fun update(uid: String, data: MemberDto): MemberDto? = withContext(Dispatchers.IO) {
collection.find(Filters.eq("uid", uid)).first()?.let(MemberDto::fromDocument)?.copy(
playerName = data.playerName,
discordName = data.discordName,
discordID = data.discordID,
leave = data.leave,
updatedAt = System.currentTimeMillis(),
)?.let {
collection.findOneAndReplace(Filters.eq("uid", uid), it.toDocument())
}
collection.find(Filters.eq("uid", uid)).first()?.let(MemberDto::fromDocument)
}
// Delete a member
suspend fun delete(uid: String): Document? = withContext(Dispatchers.IO) {
collection.findOneAndDelete(Filters.eq("uid", uid))
}
}
@@ -0,0 +1,7 @@
package club.sakunadaisuki.pcrediveclanrecordbackend
import io.ktor.server.netty.EngineMain
fun main(args: Array<String>) {
EngineMain.main(args)
}
@@ -0,0 +1,25 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.mapper
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.AttackDetailDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.AttackDetail
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecord
fun BattleRecord.toDto(): BattleRecordDto = BattleRecordDto(
date = date,
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
attackDetailList = attackDetailList.map { it.toDto() },
)
fun AttackDetail.toDto(): AttackDetailDto = AttackDetailDto(
bossRound = bossRound,
bossIndex = bossIndex,
damage = damage,
isCompensation = isCompensation,
isDropped = isDropped,
createdAt = createdAt,
updatedAt = updatedAt,
)
@@ -0,0 +1,50 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.mapper
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.MemberDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.Member
import club.sakunadaisuki.pcrediveclanrecordbackend.model.MemberRequest
import club.sakunadaisuki.pcrediveclanrecordbackend.model.UpdateMemberRequest
fun Member.toDto(): MemberDto {
return MemberDto(
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
leave = leave,
createAt = createAt,
updatedAt = updatedAt,
)
}
fun MemberRequest.toDto(): MemberDto {
return MemberDto(
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
leave = leave,
)
}
fun UpdateMemberRequest.toDto(uid: String): MemberDto {
return MemberDto(
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
leave = leave,
)
}
fun MemberDto.toEmptyBattleRecordDto(date: String): BattleRecordDto {
return BattleRecordDto(
date = date,
uid = uid,
playerName = playerName,
discordName = discordName,
discordID = discordID,
attackDetailList = emptyList(),
)
}
@@ -0,0 +1,26 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant
@Serializable
data class BattleRecord(
@SerialName("date") val date: String, // yyyy-MM-dd
@SerialName("uid") var uid: String,
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
@SerialName("attack_detail_list") val attackDetailList: List<AttackDetail>
)
@Serializable
data class AttackDetail(
@SerialName("boss_round") val bossRound: Int, // 周目
@SerialName("boss_index") val bossIndex: Int, // 1~5 號王
@SerialName("damage") val damage: Long, // 傷害
@SerialName("is_compensation") val isCompensation: Boolean, // true: 補償刀, false: 正刀
@SerialName("is_dropped") val isDropped: Boolean, // true: 掉刀, false: 沒掉刀
@SerialName("created_at") val createdAt: Long = Instant.now().toEpochMilli(),
@SerialName("updated_at") val updatedAt: Long = Instant.now().toEpochMilli()
)
@@ -0,0 +1,18 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BattleRecordRequest(
val date: String,
val uid: String,
@SerialName("player_name") val name: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
val round: Int,
val boss: Int,
val damage: Long,
@SerialName("is_compensation") val isCompensation: Boolean = false,
@SerialName("is_dropped") val isDropped: Boolean = false,
)
@@ -0,0 +1,15 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Member(
@SerialName("uid") var uid: String,
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
@SerialName("leave") var leave: Boolean,
@SerialName("created_at") var createAt: Long? = null,
@SerialName("updated_at") var updatedAt: Long? = null
)
@@ -0,0 +1,13 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MemberRequest(
var uid: String,
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
var leave: Boolean = false,
)
@@ -0,0 +1,12 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UpdateMemberRequest(
@SerialName("player_name") val playerName: String,
@SerialName("discord_name") val discordName: String,
@SerialName("discord_id") val discordID: String,
@SerialName("leave") var leave: Boolean,
)
@@ -0,0 +1,38 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.plugins
import io.ktor.server.application.*
import io.ktor.http.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.routing.*
fun Application.configureHttp() {
install(CORS) {
allowHost("localhost:8080")
allowHost("table.sakunadaisuki.club")
allowHost("192.168.50.128:10002")
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType) // 處理 JSON 必備
allowHeader(HttpHeaders.Authorization) // 傳遞 JWT / 身份驗證權限必備
allowHeader("MyCustomHeader")
//anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
routing {
swaggerUI(path = "openapi") {
/*
Documentation source configuration goes here.
This can be from file (documentation.yaml), or it can be served dynamically from your sources using the
`describe {}` API on routes. When `openApi` enabled in Gradle, these calls will be automatically injected
based on your code and comments.
*/
}
}
}
@@ -0,0 +1,192 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.plugins
import club.sakunadaisuki.pcrediveclanrecordbackend.database.dto.BattleRecordDto
import club.sakunadaisuki.pcrediveclanrecordbackend.database.service.BattleRecordService
import club.sakunadaisuki.pcrediveclanrecordbackend.database.service.MemberService
import club.sakunadaisuki.pcrediveclanrecordbackend.mapper.toDto
import club.sakunadaisuki.pcrediveclanrecordbackend.model.AttackDetail
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecord
import club.sakunadaisuki.pcrediveclanrecordbackend.model.BattleRecordRequest
import club.sakunadaisuki.pcrediveclanrecordbackend.model.MemberRequest
import club.sakunadaisuki.pcrediveclanrecordbackend.model.UpdateMemberRequest
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoDatabase
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureMongo() {
// Connect to your mongo instance
val mongoDatabase = connectToMongoDB()
val memberService = runCatching {
MemberService(mongoDatabase)
}.getOrNull() ?: return
val battleRecordService = runCatching {
BattleRecordService(mongoDatabase)
}.getOrNull() ?: return
routing {
route("/api") {
// Read member
get("/member") {
memberService.read().let { member ->
call.respond(HttpStatusCode.OK, member)
}
}
// Read member
get("/member/{uid}") {
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
memberService.read(uid)?.let { member ->
call.respond(HttpStatusCode.OK, member)
} ?: call.respond(HttpStatusCode.NotFound)
}
// Create member
post("/member") {
val member = call.receive<MemberRequest>()
val existMember = memberService.read(member.uid)
if (existMember == null) {
val id = memberService.create(member.toDto())
call.respond(HttpStatusCode.Created, id)
} else {
call.respond(HttpStatusCode.Conflict)
}
}
// Update member
put("/member/{uid}") {
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
val member = call.receive<UpdateMemberRequest>()
memberService.update(uid, member.toDto(uid))?.let { member ->
call.respond(HttpStatusCode.OK, member)
} ?: call.respond(HttpStatusCode.NotFound)
}
// Delete member
delete("/member/{uid}") {
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
memberService.delete(uid)?.let {
call.respond(HttpStatusCode.OK)
} ?: call.respond(HttpStatusCode.NotFound)
}
// TODO Battle Record
get("/battle_record/{date}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
battleRecordService.read(date).let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
}
}
get("/battle_record/{date}/{uid}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
val uid = call.parameters["uid"] ?: throw IllegalArgumentException("No ID found")
battleRecordService.read(date, uid)?.let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
} ?: call.respond(HttpStatusCode.NotFound)
}
put("/battle_record") {
val request = call.receive<BattleRecord>()
battleRecordService.update(request.date, request.toDto())?.let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
} ?: call.respond(HttpStatusCode.NotFound)
}
post("/battle_record") {
val request = call.receive<BattleRecordRequest>()
val newAttackDetail = AttackDetail(
bossRound = request.round,
bossIndex = request.boss,
damage = request.damage,
isCompensation = request.isCompensation,
isDropped = request.isDropped,
)
val existData = battleRecordService.read(request.date, request.uid)
if (existData != null) {
val attackDetails = existData.attackDetailList.toMutableList()
attackDetails.add(newAttackDetail.toDto())
val newData = existData.copy(
attackDetailList = attackDetails
)
battleRecordService.update(request.date, newData)?.let {
call.respond(HttpStatusCode.OK, it)
} ?: call.respond(HttpStatusCode.NotFound)
} else {
val newData = BattleRecordDto(
date = request.date,
uid = request.uid,
playerName = request.name,
discordName = request.discordName,
discordID = request.discordID,
attackDetailList = listOf(newAttackDetail.toDto())
)
val id = battleRecordService.create(newData)
call.respond(HttpStatusCode.Created, id)
}
}
post("/battle_record/create_table_day/{date}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
val exist = battleRecordService.read(date)
if (exist.isNotEmpty()) {
call.respond(HttpStatusCode.Conflict)
return@post
}
battleRecordService.createTable(date).let { battleRecord ->
call.respond(HttpStatusCode.OK, battleRecord)
}
}
post("/battle_record/create_table_month/{date}") {
val date = call.parameters["date"] ?: throw IllegalArgumentException("No date")
battleRecordService.createTableMonth(date).let { battleRecordList ->
call.respond(HttpStatusCode.OK, battleRecordList)
}
}
}
}
}
/**
* Establishes connection with a MongoDB database.
*
* The following configuration properties (in application.yaml/application.conf) can be specified:
* * `db.mongo.user` username for your database
* * `db.mongo.password` password for the user
* * `db.mongo.host` host that will be used for the database connection
* * `db.mongo.port` port that will be used for the database connection
* * `db.mongo.maxPoolSize` maximum number of connections to a MongoDB server
* * `db.mongo.database.name` name of the database
*
* IMPORTANT NOTE: in order to make MongoDB connection working, you have to start a MongoDB server first.
* See the instructions here: https://www.mongodb.com/docs/manual/administration/install-community/
* all the paramaters above
*
* @returns [MongoDatabase] instance
* */
fun Application.connectToMongoDB(): MongoDatabase {
val user = environment.config.tryGetString("db.mongo.user")
val password = environment.config.tryGetString("db.mongo.password")
val host = environment.config.tryGetString("db.mongo.host") ?: "127.0.0.1"
val port = environment.config.tryGetString("db.mongo.port") ?: "27017"
val maxPoolSize = environment.config.tryGetString("db.mongo.maxPoolSize")?.toInt() ?: 20
val databaseName = environment.config.tryGetString("db.mongo.database.name") ?: "pc_re_dive_clan_battle"
val credentials = user?.let { userVal -> password?.let { passwordVal -> "$userVal:$passwordVal@" } }.orEmpty()
val uri = "mongodb://$credentials$host:$port/?maxPoolSize=$maxPoolSize&w=majority"
val mongoClient = MongoClients.create(uri)
val database = mongoClient.getDatabase(databaseName)
monitor.subscribe(ApplicationStopped) {
mongoClient.close()
}
return database
}
@@ -0,0 +1,16 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.plugins
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello, World!")
}
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}
@@ -0,0 +1,11 @@
package club.sakunadaisuki.pcrediveclanrecordbackend.plugins
import io.ktor.server.application.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
@@ -1,62 +0,0 @@
package com.ray650128.pcredive
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.LoggerContext
import com.ray650128.pcredive.database.model.MemberRecord
import com.ray650128.pcredive.database.model.Record
import com.ray650128.pcredive.database.service.MemberRecordService
import com.ray650128.pcredive.database.service.MemberService
import io.ktor.server.application.*
import com.ray650128.pcredive.plugins.*
import org.slf4j.LoggerFactory
import java.util.*
fun main(args: Array<String>) {
startTimer()
val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
val rootLogger = loggerContext.getLogger("org.mongodb.driver")
rootLogger.level = Level.OFF
io.ktor.server.netty.EngineMain.main(args)
}
@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused.
fun Application.module() {
configureHTTP()
configureSerialization()
configureMemberRouting()
configureMemberRecordRouting()
}
private fun startTimer() {
val date = Date()
val timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
val c = Calendar.getInstance()
val hour = c[Calendar.HOUR_OF_DAY]
val minute = c[Calendar.MINUTE]
val second = c[Calendar.SECOND]
val day = c[Calendar.DAY_OF_MONTH]
val lastDay = c.getActualMaximum(Calendar.DAY_OF_MONTH)
//println("Time: $day, ${String.format("%2d:%2d:%2d", hour, minute, second)}")
if ((day in (lastDay - 5)..lastDay) && (hour == 5 && minute == 0 && second == 0)) {
val members = MemberService.findAll(false)
members.forEach { member ->
MemberRecordService.create(
MemberRecord(
memberId = member._id,
record1 = Record(""),
record1c = Record(""),
record2 = Record(""),
record2c = Record(""),
record3 = Record(""),
record3c = Record(""),
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis()
)
)
}
}
}
}, date, 1000L)
}
@@ -1,40 +0,0 @@
package com.ray650128.pcredive.database.dto
import com.ray650128.pcredive.database.model.Member
import kotlinx.serialization.Serializable
@Serializable
data class MemberDto(
var _id: String? = null,
var playerName: String,
var nickName: String,
var discordID: String,
var uid: String,
var leave: Boolean = false,
var createAt: Long? = null,
var updatedAt: Long? = null
)
fun Member.toDto(): MemberDto {
return MemberDto(
_id = this._id.toString(),
playerName = this.playerName,
nickName = this.nickName,
discordID = this.discordID,
uid = this.uid,
leave = this.leave,
createAt = this.createAt,
updatedAt = this.updatedAt
)
}
fun MemberDto.toMember(): Member =
Member(
playerName = this.playerName,
nickName = this.nickName,
discordID = this.discordID,
uid = this.uid,
leave = this.leave,
createAt = this.createAt,
updatedAt = this.updatedAt
)
@@ -1,49 +0,0 @@
package com.ray650128.pcredive.database.dto
import com.ray650128.pcredive.database.model.Member
import com.ray650128.pcredive.database.model.MemberRecord
import com.ray650128.pcredive.database.model.Record
import com.ray650128.pcredive.database.service.MemberService
import kotlinx.serialization.Serializable
@Serializable
data class MemberRecordDto(
var _id: String? = null,
var member: Member? = null,
var record1: Record,
var record1c: Record,
var record2: Record,
var record2c: Record,
var record3: Record,
var record3c: Record,
var createdAt: Long?,
var updatedAt: Long?
)
fun MemberRecord.toDto(): MemberRecordDto {
return MemberRecordDto(
_id = this._id.toString(),
member = MemberService.findById(this.memberId.toString()),
record1 = this.record1,
record1c = this.record1c,
record2 = this.record2,
record2c = this.record2c,
record3 = this.record3,
record3c = this.record3c,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
fun MemberRecordDto.toMemberRecord(): MemberRecord =
MemberRecord(
memberId = this.member?._id,
record1 = this.record1,
record1c = this.record1c,
record2 = this.record2,
record2c = this.record2c,
record3 = this.record3,
record3c = this.record3c,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
@@ -1,18 +0,0 @@
package com.ray650128.pcredive.database.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.litote.kmongo.Id
import org.litote.kmongo.newId
@Serializable
data class Member(
@Contextual var _id: Id<Member> = newId(),
var playerName: String,
var nickName: String,
var discordID: String,
var uid: String,
var leave: Boolean = false,
var createAt: Long? = null,
var updatedAt: Long? = null
)
@@ -1,26 +0,0 @@
package com.ray650128.pcredive.database.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.litote.kmongo.Id
import org.litote.kmongo.newId
@Serializable
data class MemberRecord(
@Contextual var _id: Id<MemberRecord> = newId(),
@Contextual var memberId: Id<Member>? = null,
var record1: Record = Record(),
var record1c: Record = Record(),
var record2: Record = Record(),
var record2c: Record = Record(),
var record3: Record = Record(),
var record3c: Record = Record(),
var createdAt: Long? = null,
var updatedAt: Long? = null
)
@Serializable
data class Record(
var boss: String? = null,
var damage: Int? = null
)
@@ -1,9 +0,0 @@
package com.ray650128.pcredive.database.model
import kotlinx.serialization.Serializable
@Serializable
data class RecordData(
val member: Member,
var record: MemberRecord?
)
@@ -1,16 +0,0 @@
package com.ray650128.pcredive.database.model
import kotlinx.serialization.Serializable
@Serializable
data class UpdateMemberRecord(
var memberId: String,
var record1: Record = Record("未出"),
var record1c: Record = Record("未出"),
var record2: Record = Record("未出"),
var record2c: Record = Record("未出"),
var record3: Record = Record("未出"),
var record3c: Record = Record("未出"),
var createdAt: Long? = null,
var updatedAt: Long? = null
)
@@ -1,58 +0,0 @@
package com.ray650128.pcredive.database.service
import com.ray650128.pcredive.database.model.Member
import com.ray650128.pcredive.database.model.MemberRecord
import org.bson.types.ObjectId
import org.litote.kmongo.*
import org.litote.kmongo.id.toId
object MemberRecordService {
private val client = KMongo.createClient("mongodb://ray650128:Zx650128!@www.ray650128.com:27017")
private val database = client.getDatabase("pc_re_dive_clan_battle")
private val recordCollection = database.getCollection<MemberRecord>()
fun create(record: MemberRecord): Id<MemberRecord> {
recordCollection.insertOne(record)
return record._id
}
fun findById(id: String): MemberRecord? {
val bsonId: Id<MemberRecord> = ObjectId(id).toId()
return recordCollection.findOne(MemberRecord::_id eq bsonId)
}
fun findByMemberIdBetweenDate(id: String, start: Long, end: Long): MemberRecord? {
val bsonId: Id<Member> = ObjectId(id).toId()
return recordCollection.findOne(MemberRecord::memberId eq bsonId, MemberRecord::createdAt gte start, MemberRecord::createdAt lt end)
}
fun findByTimeBetween(start: Long, end: Long): List<MemberRecord> {
return recordCollection.find(MemberRecord::createdAt gte start, MemberRecord::createdAt lt end).toList()
}
fun findByOwnerId(id: String): List<MemberRecord> {
val bsonId: Id<Member> = ObjectId(id).toId()
return recordCollection.find(MemberRecord::memberId eq bsonId).toList()
}
fun updateById(id: String, request: MemberRecord): Boolean =
findById(id)?.let { record ->
val updateResult = recordCollection.replaceOne(
record.copy(
record1 = request.record1,
record1c = request.record1c,
record2 = request.record2,
record2c = request.record2c,
record3 = request.record3,
record3c = request.record3c,
updatedAt = System.currentTimeMillis()
)
)
updateResult.modifiedCount == 1L
} ?: false
fun deleteById(id: String): Boolean {
val deleteResult = recordCollection.deleteOneById(ObjectId(id))
return deleteResult.deletedCount == 1L
}
}
@@ -1,52 +0,0 @@
package com.ray650128.pcredive.database.service
import com.ray650128.pcredive.database.model.Member
import org.bson.types.ObjectId
import org.litote.kmongo.*
import org.litote.kmongo.id.toId
object MemberService {
private val client = KMongo.createClient("mongodb://ray650128:Zx650128!@www.ray650128.com:27017")
private val database = client.getDatabase("pc_re_dive_clan_battle")
private val memberCollection = database.getCollection<Member>()
fun create(member: Member): Id<Member> {
memberCollection.insertOne(member)
return member._id
}
//fun findAll(): List<Member> = memberCollection.find().toList()
fun findAll(isLeave: Boolean? = null): List<Member> {
return if (isLeave == null) {
memberCollection.find().toList()
} else {
memberCollection.find(Member::leave eq isLeave).toList()
}
}
fun findById(id: String): Member? {
val bsonId: Id<Member> = ObjectId(id).toId()
return memberCollection.findOne(Member::_id eq bsonId)
}
fun updateById(id: String, request: Member): Boolean =
findById(id)?.let { member ->
val updateResult = memberCollection.replaceOne(
member.copy(
playerName = request.playerName,
nickName = request.nickName,
discordID = request.discordID,
uid = request.uid,
leave = request.leave,
updatedAt = request.updatedAt
)
)
updateResult.modifiedCount == 1L
} ?: false
fun deleteById(id: String): Boolean {
val deleteResult = memberCollection.deleteOneById(ObjectId(id))
return deleteResult.deletedCount == 1L
}
}
@@ -1,21 +0,0 @@
package com.ray650128.pcredive.plugins
import io.ktor.http.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.application.*
fun Application.configureHTTP() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Post)
allowHeader(HttpHeaders.AccessControlAllowOrigin)
allowHeader(HttpHeaders.Accept)
allowHeader(HttpHeaders.ContentType)
anyHost()
allowCredentials = true
allowNonSimpleContentTypes = true
}
}
@@ -1,152 +0,0 @@
package com.ray650128.pcredive.plugins
import com.ray650128.pcredive.database.dto.toDto
import com.ray650128.pcredive.database.model.MemberRecord
import com.ray650128.pcredive.database.model.Record
import com.ray650128.pcredive.database.model.RecordData
import com.ray650128.pcredive.database.model.UpdateMemberRecord
import com.ray650128.pcredive.database.service.MemberRecordService
import com.ray650128.pcredive.database.service.MemberService
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import kotlinx.coroutines.coroutineScope
import org.litote.kmongo.toId
import java.util.*
fun Application.configureMemberRecordRouting() {
routing {
options("{...}") {
call.respond(HttpStatusCode.OK)
}
route("/api") {
route("/record") {
get {
val calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis() - (5 * 60 * 60 * 1000) // 由於公連換日為每日早上5:00,因此減去時差
}
val records = getData(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH))
call.respond(HttpStatusCode.OK, records)
}
post("/{id}") {
val id = call.parameters.getOrFail<String>("id")
val record = call.receive<MemberRecord>()
val member = MemberService.findById(id) ?: run {
call.respond(HttpStatusCode.NotFound)
return@post
}
val now = Calendar.getInstance()
val startTime = getDayStartLong(now[Calendar.YEAR], now[Calendar.MONTH], now[Calendar.DAY_OF_MONTH])
val endTime = getDayEndLong(now[Calendar.YEAR], now[Calendar.MONTH], now[Calendar.DAY_OF_MONTH])
val existData = MemberRecordService.findByMemberIdBetweenDate(id, startTime, endTime) ?: run {
record.apply {
memberId = member._id
createdAt = System.currentTimeMillis()
updatedAt = System.currentTimeMillis()
}
MemberRecordService.create(record).let {
call.respond(HttpStatusCode.OK, record)
}
return@post
}
existData.apply {
memberId = member._id
record1 = record.record1
record1c = record.record1c
record2 = record.record2
record2c = record.record2c
record3 = record.record3
record3c = record.record3c
updatedAt = System.currentTimeMillis()
}
MemberRecordService.updateById(existData._id.toString(), existData).let {
call.respond(HttpStatusCode.OK, existData)
}
}
get("/{year}/{month}/{day}") {
val year = call.parameters.getOrFail<Int>("year").toInt()
val month = call.parameters.getOrFail<Int>("month").toInt() - 1
val day = call.parameters.getOrFail<Int>("day").toInt()
val records = getData(year, month, day)
call.respond(HttpStatusCode.OK, records)
}
get("/{id}") {
// Show an article with a specific id
val id = call.parameters.getOrFail<String>("id")
val record = MemberRecordService.findById(id)?.toDto() ?: run {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(HttpStatusCode.OK, record)
}
put("/{id}") {
val id = call.parameters.getOrFail<String>("id")
val record = call.receive<UpdateMemberRecord>()
val oldData = MemberRecordService.findById(id) ?: run {
call.respond(HttpStatusCode.NotFound)
return@put
}
oldData.apply {
record1 = record.record1
record1c = record.record1c
record2 = record.record2
record2c = record.record2c
record3 = record.record3
record3c = record.record3c
updatedAt = System.currentTimeMillis()
}
MemberRecordService.updateById(id, oldData)
call.respond(HttpStatusCode.OK, record)
}
}
}
}
}
fun getDayStartLong(year: Int, month: Int, day: Int): Long {
val calendar = Calendar.getInstance().apply {
set(Calendar.YEAR, year)
set(Calendar.MONTH, month)
set(Calendar.DAY_OF_MONTH, day)
}
return calendar.apply {
set(Calendar.HOUR_OF_DAY, 5)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
fun getDayEndLong(year: Int, month: Int, day: Int): Long {
val startTime = getDayStartLong(year, month, day)
return startTime + 86399000L
}
fun getData(year: Int, month: Int, day: Int): List<RecordData> {
val startTime = getDayStartLong(year, month, day)
val endTime = getDayEndLong(year, month, day)
// Show a list of articles
val records = ArrayList<RecordData>()
val memberList = MemberService.findAll()
memberList.forEach { member ->
if ((member.createAt!! < startTime || member.createAt!! > endTime) && !member.leave) {
records.add(
RecordData(member = member, null)
)
}
}
val recordList = MemberRecordService.findByTimeBetween(startTime, endTime)
recordList.forEach { record ->
val player = MemberService.findById(record.memberId.toString())
records.firstOrNull { it.member == player }?.let { playerRecord ->
playerRecord.record = record
}
}
return records
}
@@ -1,70 +0,0 @@
package com.ray650128.pcredive.plugins
import com.ray650128.pcredive.database.dto.toDto
import com.ray650128.pcredive.database.model.Member
import com.ray650128.pcredive.database.model.MemberRecord
import com.ray650128.pcredive.database.service.MemberService
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import kotlinx.coroutines.coroutineScope
fun Application.configureMemberRouting() {
routing {
options("{...}") {
call.respond(HttpStatusCode.OK)
}
route("/api") {
route("/member") {
get {
// 顯示所有登錄的成員資料
coroutineScope {
val memberList = MemberService.findAll()
call.respond(HttpStatusCode.OK, memberList)
}
}
get("/{id}") {
val id = call.parameters.getOrFail<String>("id")
val member = MemberService.findById(id) ?: run {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(HttpStatusCode.OK, member)
}
post {
val member = call.receive<Member>()
member.apply {
createAt = System.currentTimeMillis()
updatedAt = System.currentTimeMillis()
}
MemberService.create(member).let {
call.respond(HttpStatusCode.OK, member)
}
}
put("/{id}") {
val id = call.parameters.getOrFail<String>("id")
val member = call.receive<Member>()
val oldData = MemberService.findById(id) ?: run {
call.respond(HttpStatusCode.NotFound)
return@put
}
oldData.apply {
playerName = member.playerName
nickName = member.nickName
discordID = member.discordID
uid = member.uid
leave = member.leave
updatedAt = System.currentTimeMillis()
}
MemberService.updateById(id, oldData)
call.respond(HttpStatusCode.OK, oldData)
}
}
}
}
}
@@ -1,17 +0,0 @@
package com.ray650128.pcredive.plugins
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import kotlinx.serialization.json.Json
import org.litote.kmongo.id.serialization.IdKotlinXSerializationModule
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json { serializersModule = IdKotlinXSerializationModule }
)
}
}
-9
View File
@@ -1,9 +0,0 @@
ktor {
deployment {
port = 10001
port = ${?PORT}
}
application {
modules = [ com.ray650128.pcredive.ApplicationKt.module ]
}
}
+14
View File
@@ -0,0 +1,14 @@
ktor:
deployment:
port: 10001
application:
modules:
- club.sakunadaisuki.pcrediveclanrecordbackend.plugins.HttpKt.configureHttp
- club.sakunadaisuki.pcrediveclanrecordbackend.plugins.SerializationKt.configureSerialization
- club.sakunadaisuki.pcrediveclanrecordbackend.plugins.MongoKt.configureMongo
- club.sakunadaisuki.pcrediveclanrecordbackend.plugins.RoutingKt.configureRouting
db:
mongo:
user: ray650128
password: Zx0987062271!
host: 192.168.50.128
+23
View File
@@ -0,0 +1,23 @@
openapi: "3.0.3"
info:
title: "Application API"
description: "Application API"
version: "1.0.0"
servers:
- url: "http://0.0.0.0:8080"
paths:
/:
get:
description: "Hello World!"
responses:
"200":
description: "OK"
content:
text/plain:
schema:
type: "string"
examples:
Example#1:
value: "Hello World!"
components:
schemas: { }
+2 -2
View File
@@ -4,9 +4,9 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="trace"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/> <logger name="io.netty" level="INFO"/>
</configuration> </configuration>
+175
View File
@@ -0,0 +1,175 @@
html {
background-image: url("../img/background.jpg");
}
table {
background-color: #ffffffbb;
border-style: solid;
border-width: 2px;
border-collapse: collapse;
width: 100%;
}
th, td {
padding: 1px 2px;
border: 1px solid #000;
}
hr {
border: 1px solid #2c6a99;
}
/* 未訪問超連結 */
a:link {
color: #000000;
}
/* 已訪問超連結 */
a:visited {
color: #000000;
}
/* 滑鼠移動到超連結上 */
a:hover {
color: #1070c4;
background-color: #ffffff;
}
/* 滑鼠點選時 */
a:active {
color: #0d5da3;
}
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, label {
font-family: 'Arial', system-ui;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
}
.centerText {
font-family: 'Arial', system-ui;
text-align: center;
font-weight: bold;
}
p {
text-align: center;
font-weight: bold;
font-family: 'Arial', system-ui
}
input[type="text"], input[type="number"] {
width: 100%;
max-width: 100%;
padding: 10px;
font-size: 16px;
border-width: 1px;
border-color: #07407d;
background-color: #ffffffe0;
color: #000000;
box-sizing: border-box;
border-style: solid;
border-radius: 5px;
font-family: 'Arial', system-ui
}
input[type="text"], input[type="number"] {
outline:none;
}
input[type="date"] {
padding: 5px;
font-size: 16px;
border-width: 1px;
border-color: #07407d;
background-color: #ffffffe0;
color: #000000;
border-style: solid;
border-radius: 5px;
font-family: 'Arial', system-ui
}
input[type="date"] {
outline:none;
}
button {
display: inline-block;
text-align: center;
vertical-align: middle;
padding: 8px 20px;
border: 1px solid #07407d;
border-radius: 8px;
background: #4ab1ff;
background: -webkit-gradient(linear, left top, left bottom, from(#4ab1ff), to(#0d5da3));
background: -moz-linear-gradient(top, #4ab1ff, #0d5da3);
background: linear-gradient(to bottom, #4ab1ff, #0d5da3);
text-shadow: #591717 1px 1px 1px;
font-family: 'Arial', system-ui;
color: #ffffff;
text-decoration: none;
}
button:hover,
button:focus {
border: 1px solid #0b66c8;
background: #59d4ff;
background: -webkit-gradient(linear, left top, left bottom, from(#59d4ff), to(#1070c4));
background: -moz-linear-gradient(top, #59d4ff, #1070c4);
background: linear-gradient(to bottom, #59d4ff, #1070c4);
color: #ffffff;
text-decoration: none;
}
button:active {
background: #2c6a99;
background: -webkit-gradient(linear, left top, left bottom, from(#2c6a99), to(#0d5da3));
background: -moz-linear-gradient(top, #2c6a99, #0d5da3);
background: linear-gradient(to bottom, #2c6a99, #0d5da3);
}
.button_red {
display: inline-block;
text-align: center;
vertical-align: middle;
padding: 8px 20px;
border: 1px solid #7d0707;
border-radius: 8px;
background: #ff4a4a;
background: -webkit-gradient(linear, left top, left bottom, from(#ff4a4a), to(#a30d0d));
background: -moz-linear-gradient(top, #ff4a4a, #a30d0d);
background: linear-gradient(to bottom, #ff4a4a, #a30d0d);
text-shadow: #591717 1px 1px 1px;
font-family: 'Arial', system-ui;
color: #ffffff;
text-decoration: none;
}
.button_red:hover,
.button_red:focus {
border: 1px solid #c80b0b;
background: #ff5959;
background: -webkit-gradient(linear, left top, left bottom, from(#ff5959), to(#c41010));
background: -moz-linear-gradient(top, #ff5959, #c41010);
background: linear-gradient(to bottom, #ff5959, #c41010);
color: #ffffff;
text-decoration: none;
}
.button_red:active {
background: #992c2c;
background: -webkit-gradient(linear, left top, left bottom, from(#992c2c), to(#a30d0d));
background: -moz-linear-gradient(top, #992c2c, #a30d0d);
background: linear-gradient(to bottom, #992c2c, #a30d0d);
}
.center {
width: fit-content;
margin: 0 auto;
}
.autosize-text {
font-size: 5vw;
font-family: 'Arial', system-ui;
text-align: center;
font-weight: bold;
}
+146
View File
@@ -0,0 +1,146 @@
<!DOCTYPE html>
<html
lang="zh-TW"
xmlns="http://www.w3.org/1999/html"
xmlns="http://www.w3.org/1999/html"
>
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新增/修改紀錄</title>
<link rel="stylesheet" href="./css/style.css" />
</head>
<body>
<h1 class="autosize-text" id="my_title">新增/修改紀錄</h1>
<hr />
<h4><p>填寫方式:掉刀填0,正常出刀填X周-Y王。</p></h4>
<h4><p>傷害依照幹部規定要不要填,如不填可留空</p></h4>
<hr />
<form id="record-form">
<table>
<tr>
<td style="width: 15%">
<h5 class="centerText">第1刀<br />正刀</h5>
</td>
<td style="width: 25%">
<input type="text" id="knife1_boss" placeholder="x周-y王" />
</td>
<td style="width: 20%">
<h5 class="centerText">第1刀<br />正刀傷害</h5>
</td>
<td style="width: 40%">
<input
type="number"
id="knife1_damage"
placeholder="ex: 12000000"
/>
</td>
</tr>
<tr>
<td style="width: 15%">
<h5 class="centerText">第1刀<br />殘刀</h5>
</td>
<td style="width: 25%">
<input type="text" id="knife1c_boss" placeholder="x周-y王" />
</td>
<td style="width: 20%">
<h5 class="centerText">第1刀<br />殘刀傷害</h5>
</td>
<td style="width: 40%">
<input
type="number"
id="knife1c_damage"
placeholder="ex: 12000000"
/>
</td>
</tr>
</table>
<hr />
<table>
<tr>
<td style="width: 15%">
<h5 class="centerText">第2刀<br />正刀</h5>
</td>
<td style="width: 25%">
<input type="text" id="knife2_boss" placeholder="x周-y王" />
</td>
<td style="width: 20%">
<h5 class="centerText">第2刀<br />正刀傷害</h5>
</td>
<td style="width: 40%">
<input
type="number"
id="knife2_damage"
placeholder="ex: 12000000"
/>
</td>
</tr>
<tr>
<td style="width: 15%">
<h5 class="centerText">第2刀<br />殘刀</h5>
</td>
<td style="width: 25%">
<input type="text" id="knife2c_boss" placeholder="x周-y王" />
</td>
<td style="width: 20%">
<h5 class="centerText">第2刀<br />殘刀傷害</h5>
</td>
<td style="width: 40%">
<input
type="number"
id="knife2c_damage"
placeholder="ex: 12000000"
/>
</td>
</tr>
</table>
<hr />
<table>
<tr>
<td style="width: 15%">
<h5 class="centerText">第3刀<br />正刀</h5>
</td>
<td style="width: 25%">
<input type="text" id="knife3_boss" placeholder="x周-y王" />
</td>
<td style="width: 20%">
<h5 class="centerText">第3刀<br />正刀傷害</h5>
</td>
<td style="width: 40%">
<input
type="number"
id="knife3_damage"
placeholder="ex: 12000000"
/>
</td>
</tr>
<tr>
<td style="width: 15%">
<h5 class="centerText">第3刀<br />殘刀</h5>
</td>
<td style="width: 25%">
<input type="text" id="knife3c_boss" placeholder="x周-y王" />
</td>
<td style="width: 20%">
<h5 class="centerText">第3刀<br />殘刀傷害</h5>
</td>
<td style="width: 40%">
<input
type="number"
id="knife3c_damage"
placeholder="ex: 12000000"
/>
</td>
</tr>
</table>
<hr />
<p><button type="submit">送出</button></p>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="./js/edit.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="zh-TW" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>公主連結戰隊戰填表系統</title>
<link rel="stylesheet" href="./css/style.css">
</style>
</head>
<body id="body">
<h1 class="centerText">填表系統</h1>
<hr>
<!--p>
<a href="new.html">我要填表</a>
</p-->
<div class="center">
<label for="queryDate">日期篩選:</label>
<input type="date" id="queryDate">
</div>
<hr>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="./js/index.js"></script>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
var paramValue = "";
var memberId = "";
var title = document.getElementById("my_title");
var knife1_boss = document.getElementById("knife1_boss");
var knife1_damage = document.getElementById("knife1_damage");
var knife1c_boss = document.getElementById("knife1c_boss");
var knife1c_damage = document.getElementById("knife1c_damage");
var knife2_boss = document.getElementById("knife2_boss");
var knife2_damage = document.getElementById("knife2_damage");
var knife2c_boss = document.getElementById("knife2c_boss");
var knife2c_damage = document.getElementById("knife2c_damage");
var knife3_boss = document.getElementById("knife3_boss");
var knife3_damage = document.getElementById("knife3_damage");
var knife3c_boss = document.getElementById("knife3c_boss");
var knife3c_damage = document.getElementById("knife3c_damage");
// 在頁面載入完成後,執行獲取參數的操作
window.onload = function () {
// 取得參數
paramValue = getParameterByName('id');
console.log(paramValue);
axios.get("./api/record/" + paramValue)
.then(function (response) {
// 在這裡處理回傳的資料
var options = response.data;
console.log(options);
memberId = options.member._id;
title.innerHTML = options.member.playerName + " 新增/修改紀錄";
knife1_boss.value = options.record1.boss;
knife1_damage.value = options.record1.damage ?? "";
knife1c_boss.value = options.record1c.boss;
knife1c_damage.value = options.record1c.damage ?? "";
knife2_boss.value = options.record2.boss;
knife2_damage.value = options.record2.damage ?? "";
knife2c_boss.value = options.record2c.boss;
knife2c_damage.value = options.record2c.damage ?? "";
knife3_boss.value = options.record3.boss;
knife3_damage.value = options.record3.damage ?? "";
knife3c_boss.value = options.record3c.boss;
knife3c_damage.value = options.record3c.damage ?? "";
})
.catch(function (error) {
// 處理錯誤
console.error(error);
});
};
document.addEventListener('resize', adjustTextSize);
document.getElementById("record-form").addEventListener("submit", function (event) {
event.preventDefault(); // 阻止表單預設提交行為
// 執行請求
fillRecord(
paramValue,
memberId,
knife1_boss.value,
knife1_damage.value,
knife1c_boss.value,
knife1c_damage.value,
knife2_boss.value,
knife2_damage.value,
knife2c_boss.value,
knife2c_damage.value,
knife3_boss.value,
knife3_damage.value,
knife3c_boss.value,
knife3c_damage.value
);
});
function adjustTextSize() {
var screenWidth = window.innerWidth;
var fontSize = screenWidth * 0.04;
title.fontSize = fontSize + 'px';
}
function getParameterByName(name) {
// 获取 URL 中的参数部分
var url = window.location.href;
// 对 URL 进行解析
var parsedUrl = new URL(url);
// 从解析后的 URL 中获取参数值
return parsedUrl.searchParams.get(name);
}
function fillRecord(
recordId,
memberId,
knife1_boss,
knife1_damage,
knife1c_boss,
knife1c_damage,
knife2_boss,
knife2_damage,
knife2c_boss,
knife2c_damage,
knife3_boss,
knife3_damage,
knife3c_boss,
knife3c_damage
) {
// 发送登录请求
axios.put("./api/record/" + recordId, {
memberId: memberId,
record1: {
boss: knife1_boss,
damage: parseInt(knife1_damage),
},
record1c: {
boss: knife1c_boss,
damage: parseInt(knife1c_damage),
},
record2: {
boss: knife2_boss,
damage: parseInt(knife2_damage),
},
record2c: {
boss: knife2c_boss,
damage: parseInt(knife2c_damage),
},
record3: {
boss: knife3_boss,
damage: parseInt(knife3_damage),
},
record3c: {
boss: knife3c_boss,
damage: parseInt(knife3c_damage),
}
})
.then(function (response) {
window.location.href = "./index.html";
})
.catch(function (error) {
const message = error.response.data.message;
alert("更新失敗");
//console.error('登入失敗:', error);
});
}
+146
View File
@@ -0,0 +1,146 @@
// 日期選擇
var dateInput = document.getElementById('queryDate');
dateInput.addEventListener('change', function () {
//console.log("change" + dateInput.value);
localStorage.setItem('temp_date', dateInput.value);
getDateOfRecords();
});
// 載入日期
if (localStorage.getItem('temp_date') !== null) {
dateInput.value = localStorage.getItem("temp_date");
} else {
var today = new Date();
var formattedDate = today.toISOString().split('T')[0];
dateInput.value = formattedDate;
}
let table = document.createElement("table");
let thead = document.createElement("thead");
let tbody = document.createElement("tbody");
generateTable();
getDateOfRecords();
setInterval(getDateOfRecords, 2000);
function generateTable() {
table.appendChild(thead);
table.appendChild(tbody);
// Adding the entire table to the body tag
document.getElementById("body").appendChild(table);
// Creating and adding data to first row of the table
let row_1 = document.createElement("tr");
let heading_1 = document.createElement("th");
heading_1.setAttribute('style', 'width: 40%;')
let heading_2 = document.createElement("th");
heading_2.setAttribute('style', 'width: 10%;')
let heading_3 = document.createElement("th");
heading_3.setAttribute('style', 'width: 10%;')
let heading_4 = document.createElement("th");
heading_4.setAttribute('style', 'width: 10%;')
let heading_5 = document.createElement("th");
heading_5.setAttribute('style', 'width: 10%;')
let heading_6 = document.createElement("th");
heading_6.setAttribute('style', 'width: 10%;')
let heading_7 = document.createElement("th");
heading_7.setAttribute('style', 'width: 10%;')
let heading_1_text = document.createElement("h5");
heading_1_text.innerHTML = "成員名稱";
let heading_2_text = document.createElement("h5");
heading_2_text.innerHTML = "1<br>正刀";
let heading_3_text = document.createElement("h5");
heading_3_text.innerHTML = "2<br>正刀";
let heading_4_text = document.createElement("h5");
heading_4_text.innerHTML = "3<br>正刀";
let heading_5_text = document.createElement("h5");
heading_5_text.innerHTML = "1<br>殘刀";
let heading_6_text = document.createElement("h5");
heading_6_text.innerHTML = "2<br>殘刀";
let heading_7_text = document.createElement("h5");
heading_7_text.innerHTML = "3<br>殘刀";
heading_1.appendChild(heading_1_text);
heading_2.appendChild(heading_2_text);
heading_3.appendChild(heading_3_text);
heading_4.appendChild(heading_4_text);
heading_5.appendChild(heading_5_text);
heading_6.appendChild(heading_6_text);
heading_7.appendChild(heading_7_text);
row_1.appendChild(heading_1);
row_1.appendChild(heading_2);
row_1.appendChild(heading_3);
row_1.appendChild(heading_4);
row_1.appendChild(heading_5);
row_1.appendChild(heading_6);
row_1.appendChild(heading_7);
thead.appendChild(row_1);
}
// 取得刀表
function getDateOfRecords() {
var dateSplit = dateInput.value.split("-");
console.log(dateSplit);
// 取得刀表
axios.get("./api/record/" + dateSplit[0] + "/" + dateSplit[1] + "/" + dateSplit[2], {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(function (response) {
const data = response.data;
//console.log(data);
// 清除原有的資料列
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
for (let i = 0; i < data.length; i++) {
// 建立新的資料列
let row_2 = document.createElement("tr");
let row_2_data_1 = document.createElement("td");
let row_2_data_2 = document.createElement("td");
let row_2_data_3 = document.createElement("td");
let row_2_data_4 = document.createElement("td");
let row_2_data_5 = document.createElement("td");
let row_2_data_6 = document.createElement("td");
let row_2_data_7 = document.createElement("td");
const item = data[i];
if (item.record !== null) {
row_2_data_1.innerHTML = "<h5 class=\"centerText\"><a href=\"edit.html?id=" + item.record._id + "\">" + item.member.playerName + "</a></h5>";
row_2_data_2.innerHTML = "<h5 class=\"centerText\">" + item.record.record1.boss + "</h5>";
row_2_data_3.innerHTML = "<h5 class=\"centerText\">" + item.record.record2.boss + "</h5>";
row_2_data_4.innerHTML = "<h5 class=\"centerText\">" + item.record.record3.boss + "</h5>";
row_2_data_5.innerHTML = "<h5 class=\"centerText\">" + item.record.record1c.boss + "</h5>";
row_2_data_6.innerHTML = "<h5 class=\"centerText\">" + item.record.record2c.boss + "</h5>";
row_2_data_7.innerHTML = "<h5 class=\"centerText\">" + item.record.record3c.boss + "</h5>";
} else {
row_2_data_1.innerHTML = "<h5 class=\"centerText\">" + item.member.playerName + "</h5>";
row_2_data_2.innerHTML = "<h5 class=\"centerText\">---</h5>";
row_2_data_3.innerHTML = "<h5 class=\"centerText\">---</h5>";
row_2_data_4.innerHTML = "<h5 class=\"centerText\">---</h5>";
row_2_data_5.innerHTML = "<h5 class=\"centerText\">---</h5>";
row_2_data_6.innerHTML = "<h5 class=\"centerText\">---</h5>";
row_2_data_7.innerHTML = "<h5 class=\"centerText\">---</h5>";
}
row_2.appendChild(row_2_data_1);
row_2.appendChild(row_2_data_2);
row_2.appendChild(row_2_data_3);
row_2.appendChild(row_2_data_4);
row_2.appendChild(row_2_data_5);
row_2.appendChild(row_2_data_6);
row_2.appendChild(row_2_data_7);
tbody.appendChild(row_2);
}
})
.catch(function (error) {
console.error('資料取得失敗:', error);
});
}
+114
View File
@@ -0,0 +1,114 @@
document
.getElementById("record-form")
.addEventListener("submit", function (event) {
event.preventDefault(); // 阻止表單預設提交行為
// 假設你有一個下拉式選單的 HTML 元素
var selectElement = document.getElementById("member_select");
var knife1_boss = document.getElementById("knife1_boss").value;
var knife1_damage = document.getElementById("knife1_damage").value;
var knife1c_boss = document.getElementById("knife1c_boss").value;
var knife1c_damage = document.getElementById("knife1c_damage").value;
var knife2_boss = document.getElementById("knife2_boss").value;
var knife2_damage = document.getElementById("knife2_damage").value;
var knife2c_boss = document.getElementById("knife2c_boss").value;
var knife2c_damage = document.getElementById("knife2c_damage").value;
var knife3_boss = document.getElementById("knife3_boss").value;
var knife3_damage = document.getElementById("knife3_damage").value;
var knife3c_boss = document.getElementById("knife3c_boss").value;
var knife3c_damage = document.getElementById("knife3c_damage").value;
// 執行請求
fillRecord(
selectElement.value,
knife1_boss,
knife1_damage,
knife1c_boss,
knife1c_damage,
knife2_boss,
knife2_damage,
knife2c_boss,
knife2c_damage,
knife3_boss,
knife3_damage,
knife3c_boss,
knife3c_damage
);
});
axios
.get("./api/member/not_leave")
.then(function (response) {
// 在這裡處理回傳的資料
var options = response.data;
// 假設你有一個下拉式選單的 HTML 元素
var selectElement = document.getElementById("member_select");
// 將資料加入到下拉式選單中
options.forEach(function (option) {
var optionElement = document.createElement("option");
optionElement.value = option._id;
optionElement.textContent = option.playerName;
selectElement.appendChild(optionElement);
});
})
.catch(function (error) {
// 處理錯誤
console.error(error);
});
function fillRecord(
memberId,
knife1_boss,
knife1_damage,
knife1c_boss,
knife1c_damage,
knife2_boss,
knife2_damage,
knife2c_boss,
knife2c_damage,
knife3_boss,
knife3_damage,
knife3c_boss,
knife3c_damage
) {
// 发送登录请求
axios.post("./api/record/" + memberId, {
record1: {
boss: knife1_boss,
damage: parseInt(knife1_damage),
},
record1c: {
boss: knife1c_boss,
damage: parseInt(knife1c_damage),
},
record2: {
boss: knife2_boss,
damage: parseInt(knife2_damage),
},
record2c: {
boss: knife2c_boss,
damage: parseInt(knife2c_damage),
},
record3: {
boss: knife3_boss,
damage: parseInt(knife3_damage),
},
record3c: {
boss: knife3c_boss,
damage: parseInt(knife3c_damage),
}
})
.then(function (response) {
window.location.href = "./index.html";
})
.catch(function (error) {
const message = error.response.data.message;
alert("新增失敗");
//console.error('登入失敗:', error);
});
}
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html
lang="zh-TW"
xmlns="http://www.w3.org/1999/html"
xmlns="http://www.w3.org/1999/html"
>
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>修改成員</title>
<link rel="stylesheet" href="../css/style.css" />
</head>
<body>
<h1 class="centerText">修改成員</h1>
<hr />
<form id="member-form">
<table>
<tr>
<td style="width: 30%"><h4>成員名稱</h4></td>
<td style="width: 70%">
<input
type="text"
id="nick_name"
placeholder="請輸入成員 Discord 名稱"
/>
</td>
</tr>
<tr>
<td><h4>遊戲角色名稱</h4></td>
<td>
<input
type="text"
id="player_name"
placeholder="請輸入遊戲主角名稱"
/>
</td>
</tr>
<tr>
<td><h4>遊戲UID</h4></td>
<td>
<input type="text" id="game_uid" placeholder="請輸入遊戲U ID" />
</td>
</tr>
<tr>
<td><h4>DC ID</h4></td>
<td>
<input
type="text"
id="discord_id"
placeholder="請輸入 Discord user ID"
/>
</td>
</tr>
<tr>
<td><h4>是否離開戰隊</h4></td>
<td>
<input type="radio" id="leave_true" name="is_leave" value="true" />
<label for="leave_true"></label>
<input
type="radio"
id="leave_false"
name="is_leave"
value="false"
/>
<label for="leave_true"></label>
</td>
</tr>
</table>
<hr />
<p>
<button id="submit" type="button">更新</button>
<button class="button_red" id="delete" type="button">刪除</button>
</p>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="./js/edit.js"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-TW" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>公主連結戰隊戰填表系統 - 成員管理</title>
<link rel="stylesheet" href="../css/style.css">
</style>
</head>
<body id="body">
<h1 class="centerText">成員管理</h1>
<hr>
<div class="center">
<h4><a href="./new.html">新增成員</a></h4>
</div>
<hr>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="./js/index.js"></script>
</body>
</html>
+99
View File
@@ -0,0 +1,99 @@
var paramValue = "";
var memberId = "";
var nickName = document.getElementById("nick_name");
var playerName = document.getElementById("player_name");
var gameUid = document.getElementById("game_uid");
var discordId = document.getElementById("discord_id");
// 在頁面載入完成後,執行獲取參數的操作
window.onload = function () {
// 取得參數
paramValue = getParameterByName('id');
console.log(paramValue);
axios.get("../api/member/" + paramValue)
.then(function (response) {
// 在這裡處理回傳的資料
var options = response.data;
console.log(options);
nickName.value = options.nickName;
playerName.value = options.playerName;
gameUid.value = options.uid;
discordId.value = options.discordID;
if (options.leave) {
document.getElementById("leave_true").checked = true;
} else {
document.getElementById("leave_false").checked = true;
}
})
.catch(function (error) {
// 處理錯誤
console.error(error);
});
};
document.getElementById("submit").addEventListener("click", function (event) {
event.preventDefault(); // 阻止表單預設提交行為
// 執行請求
updateMember(paramValue, nickName.value, playerName.value, gameUid.value, discordId.value);
});
document.getElementById("delete").addEventListener("click", function (event) {
event.preventDefault(); // 阻止表單預設提交行為
// 執行請求
deleteMember(paramValue);
});
function getParameterByName(name) {
// 获取 URL 中的参数部分
var url = window.location.href;
// 对 URL 进行解析
var parsedUrl = new URL(url);
// 从解析后的 URL 中获取参数值
return parsedUrl.searchParams.get(name);
}
function updateMember(memberId, nickName, playerName, gameUid, discordId) {
var selected = document.querySelector('input[name="is_leave"]:checked')
console.log(selected.value);
var isLeave = false;
if (selected) {
if (selected.value == "true") {
isLeave = true;
} else {
isLeave = false;
}
}
// 发送登录请求
axios.put("../api/member/" + memberId, {
playerName: playerName,
nickName: nickName,
discordID: discordId,
uid: gameUid,
leave: isLeave
})
.then(function (response) {
window.location.href = "../index.html";
})
.catch(function (error) {
//const message = error.response.data.message;
alert("更新失敗");
//console.error('登入失敗:', error);
});
}
function deleteMember(memberId) {
axios.delete("../api/member/" + memberId)
.then(function (response) {
window.location.href = "./index.html";
})
.catch(function (error) {
//const message = error.response.data.message;
alert("刪除失敗");
//console.error('登入失敗:', error);
});
}
+78
View File
@@ -0,0 +1,78 @@
var selectedDate = document.getElementById("queryDate");
let table = document.createElement("table");
let thead = document.createElement("thead");
let tbody = document.createElement("tbody");
table.appendChild(thead);
table.appendChild(tbody);
// Adding the entire table to the body tag
document.getElementById("body").appendChild(table);
// Creating and adding data to first row of the table
let row_1 = document.createElement("tr");
/*let heading_1 = document.createElement("th");
heading_1.innerHTML = "<h4>成員名稱</h4>";*/
let heading_2 = document.createElement("th");
heading_2.innerHTML = "<h4>角色暱稱</h4>";
heading_2.setAttribute('style', 'width: 54%;')
let heading_3 = document.createElement("th");
heading_3.innerHTML = "<h4>角色UID</h4>";
heading_3.setAttribute('style', 'width: 34%;')
/*let heading_4 = document.createElement("th");
heading_4.innerHTML = "<h4>DC ID</h4>";*/
let heading_5 = document.createElement("th");
heading_5.innerHTML = "<h4>離開</h4>";
heading_5.setAttribute('style', 'width: 12%;')
//row_1.appendChild(heading_1);
row_1.appendChild(heading_2);
row_1.appendChild(heading_3);
//row_1.appendChild(heading_4);
row_1.appendChild(heading_5);
thead.appendChild(row_1);
// 取得刀表
axios.get("../api/member", {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(function (response) {
const data = response.data;
console.log(data);
for (let i = 0; i < data.length; i++) {
// Creating and adding data to second row of the table
let row_2 = document.createElement("tr");
//let row_2_data_1 = document.createElement("td");
let row_2_data_2 = document.createElement("td");
let row_2_data_3 = document.createElement("td");
//let row_2_data_4 = document.createElement("td");
let row_2_data_5 = document.createElement("td");
const item = data[i];
//row_2_data_1.innerHTML = "<a href=\"edit.html?id=" + item._id + "\">" + item.nickName + "</a>";
row_2_data_2.innerHTML = "<h4><a href=\"edit.html?id=" + item._id + "\">" + item.playerName + "</a></h4>";
row_2_data_3.innerHTML = "<h4>" + item.uid + "</h4>";
//row_2_data_4.innerHTML = item.discordID;
if (item.leave) {
row_2_data_5.innerHTML = "<h4>O</h4>";
} else {
row_2_data_5.innerHTML = "<h4>X</h4>";
}
//row_2.appendChild(row_2_data_1);
row_2.appendChild(row_2_data_2);
row_2.appendChild(row_2_data_3);
//row_2.appendChild(row_2_data_4);
row_2.appendChild(row_2_data_5);
tbody.appendChild(row_2);
}
})
.catch(function (error) {
//const message = error.response.data.message;
//alert("資料取得失敗");
console.error('資料取得失敗:', error);
});
+31
View File
@@ -0,0 +1,31 @@
document
.getElementById("member-form")
.addEventListener("submit", function (event) {
event.preventDefault(); // 阻止表單預設提交行為
var nickName = document.getElementById("nick_name");
var playerName = document.getElementById("player_name");
var gameUid = document.getElementById("game_uid");
var discordId = document.getElementById("discord_id");
// 執行請求
newMember(nickName.value, playerName.value, gameUid.value, discordId.value);
});
function newMember(nickName, playerName, gameUid, discordId) {
// 发送登录请求
axios.post("../api/member", {
playerName: playerName,
nickName: nickName,
discordID: discordId,
uid: gameUid
})
.then(function (response) {
window.location.href = "../index.html";
})
.catch(function (error) {
const message = error.response.data.message;
alert("新增失敗");
//console.error('登入失敗:', error);
});
}
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html
lang="zh-TW"
xmlns="http://www.w3.org/1999/html"
xmlns="http://www.w3.org/1999/html"
>
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新增成員</title>
<link rel="stylesheet" href="../css/style.css" />
</head>
<body id="body">
<h1 class="centerText">新增成員</h1>
<hr />
<form id="member-form">
<table>
<tr>
<td style="width: 30%"><h4>成員名稱</h4></td>
<td style="width: 70%">
<input
type="text"
id="nick_name"
placeholder="請輸入成員 Discord 名稱"
/>
</td>
</tr>
<tr>
<td><h4>遊戲角色名稱</h4></td>
<td>
<input
type="text"
id="player_name"
placeholder="請輸入遊戲主角名稱"
/>
</td>
</tr>
<tr>
<td><h4>遊戲UID</h4></td>
<td>
<input type="text" id="game_uid" placeholder="請輸入遊戲U ID" />
</td>
</tr>
<tr>
<td><h4>DC ID</h4></td>
<td>
<input
type="text"
id="discord_id"
placeholder="請輸入 Discord user ID"
/>
</td>
</tr>
</table>
<hr />
<p><button type="submit">送出</button></p>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="./js/new.js"></script>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html
lang="zh-TW"
xmlns="http://www.w3.org/1999/html"
xmlns="http://www.w3.org/1999/html"
>
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新增紀錄</title>
<link rel="stylesheet" href="./css/style.css" />
</head>
<body id="body">
<h1 class="centerText">新增紀錄</h1>
<hr />
<h4>
填寫方式:掉刀填0,正常出刀填X周-Y王。傷害依照幹部規定要不要填,如不填可留空
</h4>
<hr />
<form id="record-form">
<table>
<tr>
<td>請選擇你的名字</td>
<td>
<select id="member_select"></select>
</td>
</tr>
</table>
<hr />
<table>
<tr>
<td>第1刀正刀</td>
<td>
<input type="text" id="knife1_boss" placeholder="x周-y王" />
</td>
<td>第1刀正刀傷害</td>
<td>
<input type="text" id="knife1_damage" placeholder="ex: 12000000" />
</td>
</tr>
<tr>
<td>第1刀殘刀</td>
<td>
<input type="text" id="knife1c_boss" placeholder="x周-y王" />
</td>
<td>第1刀殘刀傷害</td>
<td>
<input type="text" id="knife1c_damage" placeholder="ex: 12000000" />
</td>
</tr>
</table>
<hr />
<table>
<tr>
<td>第2刀正刀</td>
<td>
<input type="text" id="knife2_boss" placeholder="x周-y王" />
</td>
<td>第2刀正刀傷害</td>
<td>
<input type="text" id="knife2_damage" placeholder="ex: 12000000" />
</td>
</tr>
<tr>
<td>第2刀殘刀</td>
<td>
<input type="text" id="knife2c_boss" placeholder="x周-y王" />
</td>
<td>第2刀殘刀傷害</td>
<td>
<input type="text" id="knife2c_damage" placeholder="ex: 12000000" />
</td>
</tr>
</table>
<hr />
<table>
<tr>
<td>第3刀正刀</td>
<td>
<input type="text" id="knife3_boss" placeholder="x周-y王" />
</td>
<td>第3刀正刀傷害</td>
<td>
<input type="text" id="knife3_damage" placeholder="ex: 12000000" />
</td>
</tr>
<tr>
<td>第3刀殘刀</td>
<td>
<input type="text" id="knife3c_boss" placeholder="x周-y王" />
</td>
<td>第3刀殘刀傷害</td>
<td>
<input type="text" id="knife3c_damage" placeholder="ex: 12000000" />
</td>
</tr>
</table>
<hr />
<p><button type="submit">送出</button></p>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="./js/new.js"></script>
</body>
</html>