diff --git a/front_end/package.json b/front_end/package.json
index ab1873f..db0075b 100644
--- a/front_end/package.json
+++ b/front_end/package.json
@@ -38,6 +38,7 @@
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"primeicons": "^7.0.0",
+ "tinycolor2": "^1.6.0",
"uuid": "^9.0.0",
"validator": "^13.12.0",
"vue": "^3.5.12",
diff --git a/front_end/src/components/common/BaseIconSetting.vue b/front_end/src/components/common/BaseIconSetting.vue
new file mode 100644
index 0000000..6adcd60
--- /dev/null
+++ b/front_end/src/components/common/BaseIconSetting.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/front_end/src/components/visualization/ActivityCalendarAbstract/Cell.vue b/front_end/src/components/visualization/ActivityCalendarAbstract/Cell.vue
index 0f8ef5e..990f6de 100644
--- a/front_end/src/components/visualization/ActivityCalendarAbstract/Cell.vue
+++ b/front_end/src/components/visualization/ActivityCalendarAbstract/Cell.vue
@@ -37,13 +37,13 @@ import { ElText } from 'element-plus';
import BaseCardSmall from '@/components/common/BaseCardSmall.vue';
const prop = defineProps({
- date: { type: Date, required: true },
- videos: { type: Array, default: [] },
+ date: { type: Date, required: true },
+ videos: { type: Array, default: [] }, // 该日期的录像
bmax: { type: Number, default: 5, },
imax: { type: Number, default: 5, },
- emax: { type: Number, default: 5, },
- xOffset: { type: Number, default: 0 },
- yOffset: { type: Number, default: 0 },
+ emax: { type: Number, default: 5, }, // 三个最大值,用于计算颜色
+ xOffset: { type: Number, default: 0 }, // 横坐标,单位为格
+ yOffset: { type: Number, default: 0 }, // 纵坐标,单位为格
})
const count = ref({ b: 0, i: 0, e: 0, });
@@ -51,7 +51,7 @@ const red = ref(0);
const green = ref(0);
const blue = ref(0);
-watch(() => prop.videos, () => {
+function refresh() {
count.value.b = 0;
count.value.i = 0;
count.value.e = 0;
@@ -61,7 +61,9 @@ watch(() => prop.videos, () => {
red.value = 255 * count.value.b / prop.bmax;
green.value = 255 * count.value.i / prop.imax;
blue.value = 255 * count.value.e / prop.emax;
-}, { immediate: true });
+}
+
+watch(() => prop.videos, refresh, { immediate: true });
const size = computed(() => activityCalendarConfig.value.cellSize + 'px');
const borderRadius = computed(() => activityCalendarConfig.value.cornerRadius + '%');
diff --git a/front_end/src/components/visualization/BBBvSummary/App.vue b/front_end/src/components/visualization/BBBvSummary/App.vue
new file mode 100644
index 0000000..cef76d8
--- /dev/null
+++ b/front_end/src/components/visualization/BBBvSummary/App.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/front_end/src/components/visualization/BBBvSummary/Cell.vue b/front_end/src/components/visualization/BBBvSummary/Cell.vue
new file mode 100644
index 0000000..6a5cc60
--- /dev/null
+++ b/front_end/src/components/visualization/BBBvSummary/Cell.vue
@@ -0,0 +1,92 @@
+
+
+
+
+ {{ videos[bestIndex].getStat(displayBy) }}
+
+
+ test
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/front_end/src/components/visualization/BBBvSummary/Header.vue b/front_end/src/components/visualization/BBBvSummary/Header.vue
new file mode 100644
index 0000000..4ebc577
--- /dev/null
+++ b/front_end/src/components/visualization/BBBvSummary/Header.vue
@@ -0,0 +1,19 @@
+
+
+
+ {{ i-1 }}
+
+
+
+
\ No newline at end of file
diff --git a/front_end/src/components/visualization/BBBvSummary/Setting.vue b/front_end/src/components/visualization/BBBvSummary/Setting.vue
new file mode 100644
index 0000000..d2e630d
--- /dev/null
+++ b/front_end/src/components/visualization/BBBvSummary/Setting.vue
@@ -0,0 +1,10 @@
+
+ 行高
+
+
+
+
\ No newline at end of file
diff --git a/front_end/src/components/visualization/BBBvSummary/YLabel.vue b/front_end/src/components/visualization/BBBvSummary/YLabel.vue
new file mode 100644
index 0000000..aaff684
--- /dev/null
+++ b/front_end/src/components/visualization/BBBvSummary/YLabel.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/front_end/src/components/widgets/IconSetting.vue b/front_end/src/components/widgets/IconSetting.vue
index 666886f..ef1cfd8 100644
--- a/front_end/src/components/widgets/IconSetting.vue
+++ b/front_end/src/components/widgets/IconSetting.vue
@@ -1,9 +1,7 @@
-
+
-
-
-
+
@@ -15,6 +13,9 @@
diff --git a/front_end/src/store/index.ts b/front_end/src/store/index.ts
index c3a6b32..ed2d3ef 100644
--- a/front_end/src/store/index.ts
+++ b/front_end/src/store/index.ts
@@ -54,4 +54,8 @@ export const activityCalendarConfig = useLocalStorage('activity-calendar-config'
cellMargin: 3,
cornerRadius: 20,
showDate: false,
+})
+
+export const BBBvSummaryConfig = useLocalStorage('bbbv-summary-config', {
+ cellHeight: 20,
})
\ No newline at end of file
diff --git a/front_end/src/utils/arrays.ts b/front_end/src/utils/arrays.ts
index 503124c..17f7d38 100644
--- a/front_end/src/utils/arrays.ts
+++ b/front_end/src/utils/arrays.ts
@@ -1,4 +1,11 @@
// Credit: ChatGPT
+/**
+ * 生成一个 range
+ * @param start
+ * @param end
+ * @param step
+ * @returns start:step:end
+ */
export function range(start: number, end: number, step: number = 1): number[] {
if (step <= 0) {
throw new Error("Step must be greater than 0.");
@@ -8,4 +15,98 @@ export function range(start: number, end: number, step: number = 1): number[] {
result.push(i);
}
return result;
+}
+
+// Credit: DeepSeek
+/**
+ * 接收一个数组或者迭代器,返回最大值
+ * @param iter
+ * @returns
+ */
+export function maximum(iter: number[] | MapIterator): number {
+ const arr = Array.isArray(iter) ? iter : Array.from(iter);
+
+ if (arr.length === 0) {
+ throw new Error("Array is empty");
+ }
+
+ let max = arr[0];
+ for (let i = 1; i < arr.length; i++) {
+ if (arr[i] > max) {
+ max = arr[i];
+ }
+ }
+
+ return max;
+}
+
+/**
+ * 接收一个数组或者迭代器,返回最小值
+ * @param iter
+ * @returns
+ */
+export function minimum(iter: number[] | MapIterator): number {
+ const arr = Array.isArray(iter) ? iter : Array.from(iter);
+
+ if (arr.length === 0) {
+ throw new Error("Array is empty");
+ }
+
+ let min = arr[0];
+ for (let i = 1; i < arr.length; i++) {
+ if (arr[i] < min) {
+ min = arr[i];
+ }
+ }
+
+ return min;
+}
+
+// Credit: ChatGPT
+export function getInsertIndex(sortedArray: number[], value: number, isAscending: boolean): number {
+ // Determine the insertion point using binary search
+ let low = 0;
+ let high = sortedArray.length;
+
+ while (low < high) {
+ const mid = Math.floor((low + high) / 2);
+
+ if (isAscending) {
+ if (sortedArray[mid] < value) low = mid + 1;
+ else high = mid;
+ } else {
+ if (sortedArray[mid] > value) low = mid + 1;
+ else high = mid;
+ }
+ }
+
+ return low;
+}
+
+/**
+ * 判断数组是否是升序的
+ * @param arr
+ * @returns
+ */
+export function isAscending(arr: number[]): boolean {
+ for (let i = 0; i < arr.length - 1; i++) {
+ if (arr[i] > arr[i + 1]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * 判断数组是否是降序的
+ * @param arr
+ * @returns
+ */
+export function isDescending(arr: number[]): boolean {
+ for (let i = 0; i < arr.length - 1; i++) {
+ if (arr[i] < arr[i + 1]) {
+ return false;
+ }
+ }
+ return true;
}
\ No newline at end of file
diff --git a/front_end/src/utils/colors.ts b/front_end/src/utils/colors.ts
new file mode 100644
index 0000000..9f0c9fa
--- /dev/null
+++ b/front_end/src/utils/colors.ts
@@ -0,0 +1,24 @@
+import { getInsertIndex, isAscending, isDescending } from "./arrays";
+
+export class PiecewiseColorScheme {
+ private colors: string[];
+ private thresholds: number[];
+ private ascending: boolean;
+
+ constructor(colors: string[], thresholds: number[]) {
+ this.colors = colors;
+ this.thresholds = thresholds;
+ if (isAscending(thresholds)) {
+ this.ascending = true;
+ } else if (isDescending(thresholds)) {
+ this.ascending = false;
+ } else {
+ throw new Error("Thresholds must be either ascending or descending");
+ }
+ }
+
+ public getColor(value: number): string {
+ const index = getInsertIndex(this.thresholds, value, this.ascending);
+ return this.colors[index];
+ }
+}
\ No newline at end of file
diff --git a/front_end/src/utils/math.ts b/front_end/src/utils/math.ts
new file mode 100644
index 0000000..2d7fcfb
--- /dev/null
+++ b/front_end/src/utils/math.ts
@@ -0,0 +1,7 @@
+export function getLastDigit(num: number): number {
+ return num % 10;
+}
+
+export function setLastDigit(num: number, digit: number): number {
+ return num - getLastDigit(num) + digit;
+}
\ No newline at end of file
diff --git a/front_end/src/utils/ms_const.ts b/front_end/src/utils/ms_const.ts
index d6b1243..d74c3b1 100644
--- a/front_end/src/utils/ms_const.ts
+++ b/front_end/src/utils/ms_const.ts
@@ -1,2 +1,2 @@
export const MS_Levels = ['b', 'i', 'e'] as const;
-export type MS_Level = typeof MS_Levels[number];
\ No newline at end of file
+export type MS_Level = typeof MS_Levels[number];
diff --git a/front_end/src/utils/videoabstract.ts b/front_end/src/utils/videoabstract.ts
index 8f40c50..5fa11b2 100644
--- a/front_end/src/utils/videoabstract.ts
+++ b/front_end/src/utils/videoabstract.ts
@@ -1,3 +1,4 @@
+import { toISODateString } from './datetime';
import { MS_Level } from './ms_const';
export interface VideoAbstractInfo {
@@ -76,12 +77,29 @@ export function groupVideosByUploadDate(videos: VideoAbstract[]): Map();
videos.forEach(video => {
- const dateKey = video.upload_time.toISOString().split('T')[0]; // Extract date part as string (YYYY-MM-DD)
+ const dateKey = toISODateString(video.upload_time); // Extract date part as string (YYYY-MM-DD)
if (!result.has(dateKey)) {
result.set(dateKey, []);
}
result.get(dateKey)?.push(video);
});
+ return result;
+}
+
+export function groupVideosByBBBv(videos: VideoAbstract[], level: MS_Level): Map {
+ const result = new Map();
+
+ videos.forEach(video => {
+ if (video.level !== level) {
+ return;
+ }
+ const bbbv = video.bv;
+ if (!result.has(bbbv)) {
+ result.set(bbbv, []);
+ }
+ result.get(bbbv)?.push(video);
+ });
+
return result;
}
\ No newline at end of file
diff --git a/front_end/src/views/PlayerProfileView.vue b/front_end/src/views/PlayerProfileView.vue
index d21be01..a1c91a7 100644
--- a/front_end/src/views/PlayerProfileView.vue
+++ b/front_end/src/views/PlayerProfileView.vue
@@ -2,10 +2,14 @@
-
+
+
+
+
+
{{ t('accountlink.title') }}
@@ -25,8 +29,8 @@ import { LoginStatus } from '@/utils/common/structInterface';
import { store } from '@/store';
import { useI18n } from 'vue-i18n';
import ActivityCalendarAbstract from '@/components/visualization/ActivityCalendarAbstract/App.vue';
-import ActivityScatter2D from '@/components/visualization/ActivityScatter2D.vue';
import ExperimentalFeature from '@/components/ExperimentalFeature.vue';
-import MS3bvPB from '@/components/visualization/MS3bvPB.vue';
+import BBBvSummary from '@/components/visualization/BBBvSummary/App.vue';
+import BaseCardNormal from '@/components/common/BaseCardNormal.vue';
const { t } = useI18n();
\ No newline at end of file