前后端差不多
This commit is contained in:
@@ -66,7 +66,8 @@ func main() {
|
|||||||
|
|
||||||
//统一初始化
|
//统一初始化
|
||||||
models.ConfigAllInit()
|
models.ConfigAllInit()
|
||||||
routers.ApiInit()
|
routers.ApiUserInit() //用户表先初始化这是必须的因为后面需要用到用户组
|
||||||
|
routers.ApiScheduleInit()
|
||||||
|
|
||||||
//创建必要目录
|
//创建必要目录
|
||||||
for _, path := range models.ConfigsFile.Pahts {
|
for _, path := range models.ConfigsFile.Pahts {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func ApiRoot(r *gin.RouterGroup) {
|
|||||||
ApiUser(r.Group("/users"))
|
ApiUser(r.Group("/users"))
|
||||||
ApiFiles(r.Group("/files"))
|
ApiFiles(r.Group("/files"))
|
||||||
ApiPurchase(r.Group("/purchase"))
|
ApiPurchase(r.Group("/purchase"))
|
||||||
|
ApiSchedule(r.Group("/schedule"))
|
||||||
r.GET("/", func(ctx *gin.Context) {
|
r.GET("/", func(ctx *gin.Context) {
|
||||||
ReturnJson(ctx, "apiOK", nil)
|
ReturnJson(ctx, "apiOK", nil)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
//2026-4-2开始每个功能的数据表在各自的api路由下初始化
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"ops/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TabSchedule struct {
|
||||||
|
ID uint `gorm:"primarykey"`
|
||||||
|
UserID uint `gorm:"not null;comment:创建人ID"`
|
||||||
|
Title string `gorm:"size:200;not null;comment:日程标题"`
|
||||||
|
StartDate string `gorm:"size:10;not null;index;comment:开始日期 YYYY-MM-DD"`
|
||||||
|
EndDate string `gorm:"size:10;not null;index;comment:结束日期 YYYY-MM-DD"`
|
||||||
|
BgColor string `gorm:"size:50;default:#3788d9;comment:背景颜色"`
|
||||||
|
Remark string `gorm:"type:text;comment:备注"`
|
||||||
|
|
||||||
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:创建时间"`
|
||||||
|
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;comment:最后修改时间"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabScheduleLog struct {
|
||||||
|
ID uint `gorm:"primarykey"`
|
||||||
|
ScheduleID uint `gorm:"not null;index;comment:关联日程ID"`
|
||||||
|
UserID uint `gorm:"not null;comment:操作人ID"`
|
||||||
|
ActionType string `gorm:"size:50;not null;comment:操作类型: create-创建 update-修改 delete-删除 query-查询"`
|
||||||
|
OldContent string `gorm:"type:text;comment:修改前内容(JSON)"`
|
||||||
|
NewContent string `gorm:"type:text;comment:修改后内容(JSON)"`
|
||||||
|
IP string `gorm:"size:50;comment:操作IP"`
|
||||||
|
Remark string `gorm:"size:500;comment:备注/操作描述"`
|
||||||
|
|
||||||
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:操作时间"`
|
||||||
|
}
|
||||||
|
type fromAddEvent struct {
|
||||||
|
Title string `json:"title" binding:"required"` // 日程标题
|
||||||
|
Start string `json:"start" binding:"required"` // 开始日期
|
||||||
|
End string `json:"end" binding:"required"` // 结束日期
|
||||||
|
Color string `json:"color" binding:"required"` // 背景颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
type fromGetEvents struct {
|
||||||
|
Start string `json:"start" binding:"required"` // 开始日期
|
||||||
|
End string `json:"end" binding:"required"` // 结束日期
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
userGroup models.TabUserGroups_
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiScheduleInit() {
|
||||||
|
//先初始化数据表
|
||||||
|
models.DB.AutoMigrate(&TabSchedule{})
|
||||||
|
models.DB.AutoMigrate(&TabScheduleLog{})
|
||||||
|
//获取管理员用户组id
|
||||||
|
//先检查用户组有没有这个key
|
||||||
|
userGroup.Name = "schedule_admin"
|
||||||
|
|
||||||
|
if models.DB.Where(&userGroup).First(&userGroup).Error == nil {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
userGroup.Type = "usergroup"
|
||||||
|
models.DB.Create(&userGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiSchedule(r *gin.RouterGroup) {
|
||||||
|
r.POST("/getevents", func(ctx *gin.Context) {
|
||||||
|
data, _ := SeparateData(ctx)
|
||||||
|
//fmt.Println(cookieval, data)
|
||||||
|
var from fromGetEvents
|
||||||
|
if err := mapstructure.Decode(data, &from); err == nil {
|
||||||
|
//fmt.Println(from)
|
||||||
|
//从数据库获取相关数据
|
||||||
|
var list []TabSchedule
|
||||||
|
models.DB.Where("start_date <= ? AND end_date >= ?", from.End, from.Start).Where("deleted_at IS NULL").Find(&list)
|
||||||
|
fmt.Println(list)
|
||||||
|
//ReturnJson(ctx, "ApiOK", list)
|
||||||
|
ReturnJson(ctx, "apiOK", gin.H{"list": list})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ReturnJson(ctx, "jsonErr", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
r.POST("/addevent", func(ctx *gin.Context) {
|
||||||
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
|
if isAuth {
|
||||||
|
|
||||||
|
var from fromAddEvent
|
||||||
|
if err := mapstructure.Decode(data, &from); err == nil {
|
||||||
|
|
||||||
|
tosql := TabSchedule{
|
||||||
|
UserID: user.ID,
|
||||||
|
Title: from.Title,
|
||||||
|
StartDate: from.Start,
|
||||||
|
EndDate: from.End,
|
||||||
|
BgColor: from.Color,
|
||||||
|
}
|
||||||
|
if models.DB.Create(&tosql).Error == nil {
|
||||||
|
//记录日志
|
||||||
|
newContent, _ := json.Marshal(tosql) // 👈 转 JSON
|
||||||
|
tosqllog := TabScheduleLog{
|
||||||
|
UserID: user.ID,
|
||||||
|
ScheduleID: tosql.ID,
|
||||||
|
ActionType: "create",
|
||||||
|
NewContent: string(newContent), // 👈 直接赋值
|
||||||
|
OldContent: "",
|
||||||
|
IP: ctx.ClientIP(),
|
||||||
|
}
|
||||||
|
models.DB.Create(&tosqllog)
|
||||||
|
ReturnJson(ctx, "apiOK", nil)
|
||||||
|
} else {
|
||||||
|
ReturnJson(ctx, "apiErr", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ReturnJson(ctx, "jsonErr", nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ApiInit() {
|
func ApiUserInit() {
|
||||||
//用户模块初始化init
|
//用户模块初始化init
|
||||||
fmt.Println("users init")
|
fmt.Println("users init")
|
||||||
|
|
||||||
@@ -110,33 +110,16 @@ func AuthenticationAuthorityFromCookie(c string) (*models.TabUser_, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AuthenticationAuthority(ctx *gin.Context) (bool, models.TabUser_, map[string]interface{}) {
|
func AuthenticationAuthority(ctx *gin.Context) (bool, models.TabUser_, map[string]interface{}) {
|
||||||
var user models.TabUser_
|
|
||||||
|
|
||||||
data, cookieval := SeparateData(ctx)
|
data, cookieval := SeparateData(ctx)
|
||||||
//fmt.Println("cookieis" + cookieval)
|
//fmt.Println("cookieis" + cookieval)
|
||||||
|
var user models.TabUser_
|
||||||
if cookieval != "" {
|
if cookieval != "" {
|
||||||
cookie := models.TabCookie_{
|
user_, error := AuthenticationAuthorityFromCookie(cookieval)
|
||||||
Value: cookieval,
|
if error == nil {
|
||||||
}
|
user = *user_
|
||||||
if models.DB.Where(&cookie).First(&cookie).Error == nil {
|
return true, user, data
|
||||||
//找到cookie,验证cookie有效性,以及更新cookie
|
|
||||||
if models.CheckCookiesAndUpdate(&cookie) {
|
|
||||||
//cookie有效
|
|
||||||
//载入user
|
|
||||||
user := models.TabUser_{
|
|
||||||
ID: cookie.UserID,
|
|
||||||
}
|
|
||||||
models.DB.Where(&user).First(&user)
|
|
||||||
|
|
||||||
return true, user, data
|
|
||||||
|
|
||||||
} else {
|
|
||||||
ReturnJson(ctx, "userCookieExpired", nil)
|
|
||||||
return false, user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ReturnJson(ctx, "userCookieNotFund", nil)
|
|
||||||
return false, user, nil
|
return false, user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +128,6 @@ func AuthenticationAuthority(ctx *gin.Context) (bool, models.TabUser_, map[strin
|
|||||||
return false, user, nil
|
return false, user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//return false, user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApiUser(r *gin.RouterGroup) {
|
func ApiUser(r *gin.RouterGroup) {
|
||||||
@@ -261,26 +243,26 @@ func ApiUser(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if is_save_ok {
|
if is_save_ok {
|
||||||
//修改数据库内容
|
//修改数据库内容
|
||||||
var user_info_fund models.TabUserInfo_
|
var user_info_fund models.TabUserInfo_
|
||||||
user_info_fund.UserID = user.ID
|
user_info_fund.UserID = user.ID
|
||||||
|
|
||||||
var user_update_avatar models.TabUserInfo_
|
var user_update_avatar models.TabUserInfo_
|
||||||
user_update_avatar.AvatarPath = file_hashi_name + file_extname
|
user_update_avatar.AvatarPath = file_hashi_name + file_extname
|
||||||
|
|
||||||
|
//先查找是否有记录
|
||||||
|
if models.DB.Where(&user_info_fund).First(&user_info_fund).Error == nil {
|
||||||
|
//有记录,更新
|
||||||
|
models.DB.Model(&user_info_fund).Updates(&user_update_avatar)
|
||||||
|
} else {
|
||||||
|
//无记录,创建
|
||||||
|
user_update_avatar.UserID = user.ID
|
||||||
|
models.DB.Create(&user_update_avatar)
|
||||||
|
}
|
||||||
|
|
||||||
//先查找是否有记录
|
|
||||||
if models.DB.Where(&user_info_fund).First(&user_info_fund).Error == nil {
|
|
||||||
//有记录,更新
|
|
||||||
models.DB.Model(&user_info_fund).Updates(&user_update_avatar)
|
|
||||||
} else {
|
|
||||||
//无记录,创建
|
|
||||||
user_update_avatar.UserID = user.ID
|
|
||||||
models.DB.Create(&user_update_avatar)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ReturnJson(ctx, "postErr", nil)
|
ReturnJson(ctx, "postErr", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { api } from './index'
|
||||||
|
|
||||||
|
export const scheduleApi = {
|
||||||
|
|
||||||
|
getEvents(params = {}) {
|
||||||
|
return api.post('/schedule/getevents', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
addEvent(data) {
|
||||||
|
return api.post('/schedule/addevent', data)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
"source_code": "Source Code",
|
"source_code": "Source Code",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"author_home": "Author",
|
"author_home": "Author",
|
||||||
"copy": "Copyright © 2025 Operations. All rights reserved."
|
"copy": "Copyright © 2025 Operations. MIT Open Source License."
|
||||||
},
|
},
|
||||||
"cost_type": {
|
"cost_type": {
|
||||||
"unit_price": "Unit Price",
|
"unit_price": "Unit Price",
|
||||||
|
|||||||
@@ -247,7 +247,7 @@
|
|||||||
"source_code": "源码",
|
"source_code": "源码",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"author_home": "作者主页",
|
"author_home": "作者主页",
|
||||||
"copy": "版权 © 2025 Operations. 保留所有权利。"
|
"copy": "版权 © 2025 Operations. MIT开源协议。"
|
||||||
},
|
},
|
||||||
"cost_type": {
|
"cost_type": {
|
||||||
"unit_price": "单价",
|
"unit_price": "单价",
|
||||||
|
|||||||
@@ -1,64 +1,79 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from "vue";
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from "vue-router";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
function toggleLocale() {
|
function toggleLocale() {
|
||||||
locale.value = locale.value === 'zh-CN' ? 'en' : 'zh-CN'
|
locale.value = locale.value === "zh-CN" ? "en" : "zh-CN";
|
||||||
}
|
}
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from "@/stores/user";
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from "@/stores/toast";
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from "@/composables/usePageTitle";
|
||||||
import { useValidation } from '@/composables'
|
import { useValidation } from "@/composables";
|
||||||
import { authApi } from '@/api/auth'
|
import { authApi } from "@/api/auth";
|
||||||
import { IconEye, IconEyeOff } from '@tabler/icons-vue'
|
import { IconEye, IconEyeOff } from "@tabler/icons-vue";
|
||||||
|
|
||||||
usePageTitle('appname.login')
|
usePageTitle("appname.login");
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore();
|
||||||
const toast = useToastStore()
|
const toast = useToastStore();
|
||||||
const { validate, errors, clearErrors } = useValidation()
|
const { validate, errors, clearErrors } = useValidation();
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
username: '',
|
username: "",
|
||||||
password: '',
|
password: "",
|
||||||
remember: false,
|
remember: false,
|
||||||
})
|
});
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false);
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
clearErrors()
|
clearErrors();
|
||||||
|
|
||||||
const err1 = validate('username', form.value.username, t('message.please_enter_username_and_password'))
|
const err1 = validate(
|
||||||
const err2 = validate('password', form.value.password, t('message.please_enter_username_and_password'))
|
"username",
|
||||||
|
form.value.username,
|
||||||
|
t("message.please_enter_username_and_password"),
|
||||||
|
);
|
||||||
|
const err2 = validate(
|
||||||
|
"password",
|
||||||
|
form.value.password,
|
||||||
|
t("message.please_enter_username_and_password"),
|
||||||
|
);
|
||||||
|
|
||||||
if (!err1 || !err2) return
|
if (!err1 || !err2) return;
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const { errCode, data } = await authApi.login(form.value.username, form.value.password, form.value.remember)
|
const { errCode, data } = await authApi.login(
|
||||||
|
form.value.username,
|
||||||
|
form.value.password,
|
||||||
|
form.value.remember,
|
||||||
|
);
|
||||||
|
|
||||||
switch (errCode) {
|
switch (errCode) {
|
||||||
case 0:
|
case 0:
|
||||||
userStore.login(data.cookie)
|
userStore.login(data.cookie);
|
||||||
toast.success(t('message.login_successful'))
|
toast.success(t("message.login_successful"));
|
||||||
// 有 redirect 则跳转到原页面,否则去首页
|
// 有 redirect 则跳转到原页面,否则去首页
|
||||||
const redirect = router.currentRoute.value.query.redirect
|
const redirect = router.currentRoute.value.query.redirect;
|
||||||
router.replace(redirect || '/')
|
router.replace(redirect || "/");
|
||||||
break
|
break;
|
||||||
|
case -41:
|
||||||
|
toast.warning(t("message.username_or_password_incorrect"));
|
||||||
|
break;
|
||||||
case -42:
|
case -42:
|
||||||
toast.warning(t('message.username_or_password_incorrect'))
|
toast.warning(t("message.username_or_password_incorrect"));
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
toast.danger(t('message.server_error'))
|
toast.danger(t("message.server_error"));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 拦截器已处理
|
// 拦截器已处理
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -68,55 +83,94 @@ async function handleLogin() {
|
|||||||
<div class="mb-8 flex items-start justify-between">
|
<div class="mb-8 flex items-start justify-between">
|
||||||
<RouterLink to="/" class="inline-flex items-center">
|
<RouterLink to="/" class="inline-flex items-center">
|
||||||
<img src="/logo.svg" class="h-10 w-10 rounded-lg" alt="Operations" />
|
<img src="/logo.svg" class="h-10 w-10 rounded-lg" alt="Operations" />
|
||||||
<span class="ml-2.5 text-2xl font-bold text-gray-800 dark:text-dk-text">Operations</span>
|
<span class="ml-2.5 text-2xl font-bold text-gray-800 dark:text-dk-text"
|
||||||
|
>Operations</span
|
||||||
|
>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button class="rounded-md border border-gray-200 px-2.5 py-1 text-xs font-semibold uppercase text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-dk-muted dark:text-gray-400 dark:hover:bg-dk-card dark:hover:text-dk-text" @click="toggleLocale">
|
<button
|
||||||
{{ locale === 'zh-CN' ? 'EN' : '中' }}
|
class="rounded-md border border-gray-200 px-2.5 py-1 text-xs font-semibold uppercase text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-dk-muted dark:text-gray-400 dark:hover:bg-dk-card dark:hover:text-dk-text"
|
||||||
|
@click="toggleLocale"
|
||||||
|
>
|
||||||
|
{{ locale === "zh-CN" ? "EN" : "中" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white px-8 py-8 shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
<div
|
||||||
<h2 class="mb-6 text-center text-xl font-bold text-gray-900 dark:text-white">{{ t('message.login_to_your_account') }}</h2>
|
class="rounded-xl border border-gray-200 bg-white px-8 py-8 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="mb-6 text-center text-xl font-bold text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
{{ t("message.login_to_your_account") }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<!-- Username -->
|
<!-- Username -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('message.user_name') }}</label>
|
<label
|
||||||
|
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>{{ t("message.user_name") }}</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
:class="errors.username ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300'"
|
:class="
|
||||||
|
errors.username
|
||||||
|
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
|
||||||
|
: 'border-gray-300'
|
||||||
|
"
|
||||||
:placeholder="t('message.your_user_name')"
|
:placeholder="t('message.your_user_name')"
|
||||||
@keydown.enter="handleLogin"
|
@keydown.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{ errors.username }}</span>
|
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{
|
||||||
|
errors.username
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<!-- Password -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('message.password') }}</label>
|
<label
|
||||||
|
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>{{ t("message.password") }}</label
|
||||||
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
:type="showPassword ? 'text' : 'password'"
|
:type="showPassword ? 'text' : 'password'"
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
:class="errors.password ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300'"
|
:class="
|
||||||
|
errors.password
|
||||||
|
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
|
||||||
|
: 'border-gray-300'
|
||||||
|
"
|
||||||
:placeholder="t('message.your_password')"
|
:placeholder="t('message.your_password')"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
@keydown.enter="handleLogin"
|
@keydown.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
<button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300" @click="showPassword = !showPassword">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
>
|
||||||
<IconEye v-if="!showPassword" :size="18" />
|
<IconEye v-if="!showPassword" :size="18" />
|
||||||
<IconEyeOff v-else :size="18" />
|
<IconEyeOff v-else :size="18" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="errors.password" class="mt-1 block text-xs text-red-500">{{ errors.password }}</span>
|
<span v-if="errors.password" class="mt-1 block text-xs text-red-500">{{
|
||||||
|
errors.password
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remember -->
|
<!-- Remember -->
|
||||||
<label class="mb-6 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
<label
|
||||||
<input v-model="form.remember" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
class="mb-6 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"
|
||||||
{{ t('message.remember_me_on_this_device') }}
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.remember"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{{ t("message.remember_me_on_this_device") }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Submit -->
|
<!-- Submit -->
|
||||||
@@ -125,17 +179,37 @@ async function handleLogin() {
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="handleLogin"
|
@click="handleLogin"
|
||||||
>
|
>
|
||||||
<svg v-if="loading" class="h-4 w-4 animate-spin text-white" viewBox="0 0 24 24" fill="none">
|
<svg
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
v-if="loading"
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
class="h-4 w-4 animate-spin text-white"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{{ t('button.sign_in') }}
|
{{ t("button.sign_in") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-6 text-center text-sm text-gray-500">
|
<p class="mt-6 text-center text-sm text-gray-500">
|
||||||
{{ t('message.dont_have_account_yet') }}
|
{{ t("message.dont_have_account_yet") }}
|
||||||
<RouterLink to="/register" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">{{ t('message.register_now') }}</RouterLink>
|
<RouterLink
|
||||||
|
to="/register"
|
||||||
|
class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
|
||||||
|
>{{ t("message.register_now") }}</RouterLink
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCal
|
|||||||
|
|
||||||
import { useToastStore } from "@/stores/toast";
|
import { useToastStore } from "@/stores/toast";
|
||||||
|
|
||||||
|
// 用户状态管理
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import { scheduleApi } from "@/api/schedule";
|
||||||
|
|
||||||
|
import { useDateUtils } from "@/composables/useDateUtils";
|
||||||
|
|
||||||
|
const DateUtils = useDateUtils();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 获取用户 store 实例,用于访问和更新用户信息
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const toast = useToastStore();
|
const toast = useToastStore();
|
||||||
|
|
||||||
// 设置页面标题
|
// 设置页面标题
|
||||||
@@ -30,6 +46,9 @@ const { t, locale } = useI18n();
|
|||||||
|
|
||||||
// FullCalendar 组件的引用,用于调用日历 API
|
// FullCalendar 组件的引用,用于调用日历 API
|
||||||
const calendarRef = ref(null);
|
const calendarRef = ref(null);
|
||||||
|
// 当前视图的年份
|
||||||
|
const calendarNowShow = ref();
|
||||||
|
|
||||||
// 用于跟踪上次点击时间的响应式变量
|
// 用于跟踪上次点击时间的响应式变量
|
||||||
const lastClickTime = ref(0);
|
const lastClickTime = ref(0);
|
||||||
// 用于跟踪上次点击event时间的响应式变量
|
// 用于跟踪上次点击event时间的响应式变量
|
||||||
@@ -127,6 +146,7 @@ const calendarOptions = ref({
|
|||||||
text: t("schedule.previous_year"),
|
text: t("schedule.previous_year"),
|
||||||
click() {
|
click() {
|
||||||
calendarRef.value.getApi().prevYear();
|
calendarRef.value.getApi().prevYear();
|
||||||
|
getEvents();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 下一年按钮
|
// 下一年按钮
|
||||||
@@ -134,6 +154,7 @@ const calendarOptions = ref({
|
|||||||
text: t("schedule.next_year"),
|
text: t("schedule.next_year"),
|
||||||
click() {
|
click() {
|
||||||
calendarRef.value.getApi().nextYear();
|
calendarRef.value.getApi().nextYear();
|
||||||
|
getEvents();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 上一个月按钮
|
// 上一个月按钮
|
||||||
@@ -141,6 +162,7 @@ const calendarOptions = ref({
|
|||||||
text: t("schedule.previous_month"),
|
text: t("schedule.previous_month"),
|
||||||
click() {
|
click() {
|
||||||
calendarRef.value.getApi().prev();
|
calendarRef.value.getApi().prev();
|
||||||
|
getEvents();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 下一个月按钮
|
// 下一个月按钮
|
||||||
@@ -148,6 +170,7 @@ const calendarOptions = ref({
|
|||||||
text: t("schedule.next_month"),
|
text: t("schedule.next_month"),
|
||||||
click() {
|
click() {
|
||||||
calendarRef.value.getApi().next();
|
calendarRef.value.getApi().next();
|
||||||
|
getEvents();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 今天按钮:跳转到今天
|
// 今天按钮:跳转到今天
|
||||||
@@ -155,6 +178,7 @@ const calendarOptions = ref({
|
|||||||
text: t("schedule.today"),
|
text: t("schedule.today"),
|
||||||
click() {
|
click() {
|
||||||
calendarRef.value.getApi().today();
|
calendarRef.value.getApi().today();
|
||||||
|
getEvents();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 周视图按钮:切换到周视图
|
// 周视图按钮:切换到周视图
|
||||||
@@ -169,6 +193,12 @@ const calendarOptions = ref({
|
|||||||
// 日历事件列表(目前为空,后续可接入数据源)
|
// 日历事件列表(目前为空,后续可接入数据源)
|
||||||
events: [],
|
events: [],
|
||||||
|
|
||||||
|
// 👇 加这个!日历渲染完成 / 切换年月都会触发
|
||||||
|
datesSet(info) {
|
||||||
|
calendarNowShow.value = info;
|
||||||
|
//console.log(info);
|
||||||
|
},
|
||||||
|
|
||||||
// 日期点击事件处理函数
|
// 日期点击事件处理函数
|
||||||
dateClick(info) {
|
dateClick(info) {
|
||||||
const nowTime = new Date().getTime();
|
const nowTime = new Date().getTime();
|
||||||
@@ -194,7 +224,7 @@ const calendarOptions = ref({
|
|||||||
if (info.end - info.start > 86400000) {
|
if (info.end - info.start > 86400000) {
|
||||||
//选择了多日
|
//选择了多日
|
||||||
console.log("选择了多日:", info);
|
console.log("选择了多日:", info);
|
||||||
openEventModal(info.startStr,info.endStr);
|
openEventModal(info.startStr, info.endStr);
|
||||||
} else {
|
} else {
|
||||||
//选择单日 无功能
|
//选择单日 无功能
|
||||||
//console.log("选择单日:", info);
|
//console.log("选择单日:", info);
|
||||||
@@ -223,7 +253,7 @@ const calendarOptions = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 打开模态框
|
// 打开模态框
|
||||||
const openEventModal = (dateStr,dataEnd) => {
|
const openEventModal = (dateStr, dataEnd) => {
|
||||||
eventData.value = {
|
eventData.value = {
|
||||||
title: "",
|
title: "",
|
||||||
startDate: dateStr,
|
startDate: dateStr,
|
||||||
@@ -241,7 +271,13 @@ const closeEventModal = () => {
|
|||||||
|
|
||||||
// 处理双击事件:打开模态框添加事件
|
// 处理双击事件:打开模态框添加事件
|
||||||
const handleDoubleClick = (info) => {
|
const handleDoubleClick = (info) => {
|
||||||
openEventModal(info.dateStr,info.dateStr);
|
//先判断是否登录
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
openEventModal(info.dateStr, info.dateStr);
|
||||||
|
} else {
|
||||||
|
toast.warning(t("message.login_to_your_account"));
|
||||||
|
router.replace("/login?redirect=/schedule");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理单机事件:显示日期详情
|
// 处理单机事件:显示日期详情
|
||||||
@@ -293,15 +329,77 @@ const saveEvent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 添加到日历事件列表
|
// 添加到日历事件列表
|
||||||
calendarOptions.value.events.push(newEvent);
|
//提交到后端
|
||||||
|
|
||||||
console.log("事件添加成功:", newEvent);
|
scheduleApi
|
||||||
toast.success(t("schedule.event_added_successfully"));
|
.addEvent({
|
||||||
|
title: newEvent.title,
|
||||||
// 关闭模态框
|
start: newEvent.start,
|
||||||
closeEventModal();
|
end: DateUtils.toRealEnd(newEvent.end),
|
||||||
|
color: newEvent.backgroundColor,
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
//console.log(r);
|
||||||
|
if (r.errCode == 0) {
|
||||||
|
//前端提交是否错误
|
||||||
|
switch (
|
||||||
|
r.raw.err_code //后端返回是否错误
|
||||||
|
) {
|
||||||
|
case 0:
|
||||||
|
//calendarOptions.value.events.push(newEvent);
|
||||||
|
toast.success(t("schedule.event_added_successfully"));
|
||||||
|
// 关闭模态框
|
||||||
|
closeEventModal();
|
||||||
|
getEvents();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.danger(t("message.server_error"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//从后端获取events
|
||||||
|
const getEvents = () => {
|
||||||
|
//console.log(calendarNowShow.value)
|
||||||
|
scheduleApi
|
||||||
|
.getEvents({
|
||||||
|
start: DateUtils.dateToStr(calendarNowShow.value.start),
|
||||||
|
end: DateUtils.toRealEnd(calendarNowShow.value.end),
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
console.log(r);
|
||||||
|
if (r.errCode == 0) {
|
||||||
|
//前端提交是否错误
|
||||||
|
switch (
|
||||||
|
r.raw.err_code //后端返回是否错误
|
||||||
|
) {
|
||||||
|
case 0:
|
||||||
|
calendarOptions.value.events=[];
|
||||||
|
var events = r.raw.return.list;
|
||||||
|
console.log(events);
|
||||||
|
var eventstemp = [];
|
||||||
|
events.forEach((item) => {
|
||||||
|
|
||||||
|
calendarOptions.value.events.push({
|
||||||
|
id: item.ID, // 后端 ID
|
||||||
|
title: item.Title, // 标题
|
||||||
|
start: item.StartDate, // 开始日期
|
||||||
|
end: DateUtils.toCalendarEnd(item.EndDate), // 结束日期
|
||||||
|
backgroundColor: item.BgColor, // 背景色
|
||||||
|
allDay: true, // 全天事件
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.danger(t("message.server_error"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
// 清除日期选择
|
// 清除日期选择
|
||||||
const clearDates = () => {
|
const clearDates = () => {
|
||||||
eventData.value.startDate = "";
|
eventData.value.startDate = "";
|
||||||
@@ -350,32 +448,31 @@ watch(locale, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const handleKeydown = (event) => {
|
getEvents();
|
||||||
// Ctrl+C 事件
|
// const handleKeydown = (event) => {
|
||||||
if (event.ctrlKey && event.key === "c") {
|
// // Ctrl+C 事件
|
||||||
event.preventDefault(); // 可选:阻止默认复制行为
|
// if (event.ctrlKey && event.key === "c") {
|
||||||
console.log("Ctrl+C 被按下");
|
// event.preventDefault(); // 可选:阻止默认复制行为
|
||||||
// 你的业务逻辑
|
// console.log("Ctrl+C 被按下");
|
||||||
}
|
// // 你的业务逻辑
|
||||||
|
// }
|
||||||
// Ctrl+V 事件
|
// // Ctrl+V 事件
|
||||||
if (event.ctrlKey && event.key === "v") {
|
// if (event.ctrlKey && event.key === "v") {
|
||||||
event.preventDefault(); // 可选:阻止默认粘贴行为
|
// event.preventDefault(); // 可选:阻止默认粘贴行为
|
||||||
console.log("Ctrl+V 被按下");
|
// console.log("Ctrl+V 被按下");
|
||||||
// 你的业务逻辑
|
// // 你的业务逻辑
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
// document.addEventListener("keydown", handleKeydown);
|
||||||
document.addEventListener("keydown", handleKeydown);
|
// // 清理事件监听器
|
||||||
|
// onBeforeUnmount(() => {
|
||||||
// 清理事件监听器
|
// document.removeEventListener("keydown", handleKeydown);
|
||||||
onBeforeUnmount(() => {
|
// });
|
||||||
document.removeEventListener("keydown", handleKeydown);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- {{userStore.userCookie.Value}} -->
|
||||||
<!-- 日历容器:占满视口高度减去顶部导航高度 -->
|
<!-- 日历容器:占满视口高度减去顶部导航高度 -->
|
||||||
<div class="flex w-full flex-col relative">
|
<div class="flex w-full flex-col relative">
|
||||||
<!-- 事件编辑模态框 -->
|
<!-- 事件编辑模态框 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user