feat: add GameGroup2 project with frontend and backend
- Add .gitignore for Node.js and PocketBase projects - Add frontend (Vue 3 + Vite + TypeScript) - Add backend (PocketBase) - Add deployment scripts and Docker compose configs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<!-- src/components/common/PasswordInput.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
id?: string
|
||||
required?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
const inputType = computed(() => visible.value ? 'text' : 'password')
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
function toggleVisibility() {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="password-input">
|
||||
<input
|
||||
:id="id"
|
||||
:type="inputType"
|
||||
:value="inputValue"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
@input="(e: any) => inputValue = e.target.value"
|
||||
/>
|
||||
<button type="button" class="toggle-btn" @click="toggleVisibility">
|
||||
<svg v-if="!visible" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.password-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-input input {
|
||||
width: 100%;
|
||||
padding: 12px 40px 12px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.password-input input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<!-- src/components/team/IdleMembersList.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import type { UserStatus } from '@/types'
|
||||
import { sendInvitation } from '@/api/invitations'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
status?: UserStatus
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
status: 'idle'
|
||||
})
|
||||
|
||||
const groupStore = useGroupStore()
|
||||
|
||||
const idleMembers = computed(() =>
|
||||
groupStore.currentMembers.filter(m => m.status === props.status)
|
||||
)
|
||||
|
||||
async function inviteMember(userId: string, username: string) {
|
||||
try {
|
||||
// 这里需要先创建临时小组,或者检查是否有活跃的临时小组
|
||||
// 简化版本:发送邀请
|
||||
await sendInvitation({
|
||||
to: userId,
|
||||
teamSession: '' // 实际使用时需要先创建临时小组
|
||||
})
|
||||
ElMessage.success(`已邀请 ${username}`)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '邀请失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="idle-members-list">
|
||||
<h3 class="list-title">
|
||||
{{ status === 'idle' ? '空闲成员' : '成员列表' }}
|
||||
<span class="count">({{ idleMembers.length }})</span>
|
||||
</h3>
|
||||
|
||||
<div v-if="idleMembers.length === 0" class="empty">
|
||||
{{ status === 'idle' ? '暂无空闲成员' : '暂无成员' }}
|
||||
</div>
|
||||
|
||||
<div v-else class="member-list">
|
||||
<div
|
||||
v-for="member in idleMembers"
|
||||
:key="member.id"
|
||||
class="member-item"
|
||||
>
|
||||
<img
|
||||
:src="member.avatar || '/default-avatar.png'"
|
||||
:alt="member.username"
|
||||
class="avatar"
|
||||
/>
|
||||
<div class="member-info">
|
||||
<span class="username">{{ member.username }}</span>
|
||||
<span v-if="member.statusNote" class="status-note">{{ member.statusNote }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="status === 'idle'"
|
||||
class="invite-btn"
|
||||
@click="inviteMember(member.id, member.username)"
|
||||
>
|
||||
邀请
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.idle-members-list {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--el-bg-color-page);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.member-item:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-note {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.invite-btn {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: var(--el-color-primary);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.invite-btn:hover {
|
||||
background: var(--el-color-primary-light-3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,195 @@
|
||||
<!-- src/components/team/InvitationCard.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { Invitation } from '@/types'
|
||||
import { respondInvitation } from '@/api/invitations'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
invitation: Invitation
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
responded: [id: string, accepted: boolean]
|
||||
}>()
|
||||
|
||||
const rejectReason = ref('')
|
||||
const showRejectInput = ref(false)
|
||||
|
||||
async function acceptInvitation() {
|
||||
try {
|
||||
await respondInvitation(props.invitation.id, 'accepted')
|
||||
ElMessage.success('已接受邀请')
|
||||
emit('responded', props.invitation.id, true)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '接受邀请失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectInvitation() {
|
||||
if (!showRejectInput.value) {
|
||||
showRejectInput.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await respondInvitation(props.invitation.id, 'rejected', rejectReason.value)
|
||||
ElMessage.success('已拒绝邀请')
|
||||
emit('responded', props.invitation.id, false)
|
||||
showRejectInput.value = false
|
||||
rejectReason.value = ''
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '拒绝邀请失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="invitation-card">
|
||||
<div class="invitation-header">
|
||||
<img
|
||||
:src="invitation.expand?.from?.avatar || '/default-avatar.png'"
|
||||
:alt="invitation.expand?.from?.username"
|
||||
class="avatar"
|
||||
/>
|
||||
<div class="invitation-info">
|
||||
<span class="inviter-name">{{ invitation.expand?.from?.username }}</span>
|
||||
<span class="invitation-text">邀请你加入</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="invitation.expand?.teamSession" class="session-info">
|
||||
<span class="game-name">{{ invitation.expand.teamSession.gameName }}</span>
|
||||
<span class="session-name">{{ invitation.expand.teamSession.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="invitation-actions">
|
||||
<button class="action-btn accept" @click="acceptInvitation">
|
||||
接受
|
||||
</button>
|
||||
<button class="action-btn reject" @click="rejectInvitation">
|
||||
{{ showRejectInput ? '确认拒绝' : '拒绝' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showRejectInput" class="reject-input">
|
||||
<textarea
|
||||
v-model="rejectReason"
|
||||
placeholder="填写拒绝原因(可选)"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.invitation-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.invitation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.invitation-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.inviter-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.invitation-text {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.session-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.game-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.session-name {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.invitation-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.accept {
|
||||
background: var(--el-color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accept:hover {
|
||||
background: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
.reject {
|
||||
background: var(--el-fill-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.reject:hover {
|
||||
background: var(--el-fill-color-dark);
|
||||
}
|
||||
|
||||
.reject-input {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.reject-input textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.reject-input textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- src/components/team/StatusToggle.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { UserStatusMap, UserStatusIcon, type UserStatus } from '@/types'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentStatus = computed(() => userStore.userStatus)
|
||||
const statusIcon = computed(() => UserStatusIcon[currentStatus.value])
|
||||
const statusText = computed(() => UserStatusMap[currentStatus.value])
|
||||
|
||||
async function setStatus(status: UserStatus) {
|
||||
await userStore.setStatus(status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="setStatus">
|
||||
<span class="status-toggle">
|
||||
<span class="status-icon">{{ statusIcon }}</span>
|
||||
<span class="status-text">{{ statusText }}</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="idle">
|
||||
<span class="menu-item">🟢 空闲</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="working">
|
||||
<span class="menu-item">🔴 工作中</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="in_team" :disabled="currentStatus !== 'in_team'">
|
||||
<span class="menu-item">🔵 组队中</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="away">
|
||||
<span class="menu-item">⚫ 离开</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.status-toggle:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<!-- src/components/team/TeamSessionPanel.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTeamStore } from '@/stores/team'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { TeamStatusMap } from '@/types'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const teamStore = useTeamStore()
|
||||
const groupStore = useGroupStore()
|
||||
|
||||
const session = computed(() => teamStore.currentSession)
|
||||
const statusText = computed(() => session.value ? TeamStatusMap[session.value.status] : '')
|
||||
|
||||
const memberDetails = computed(() => {
|
||||
if (!session.value) return []
|
||||
return session.value.members
|
||||
.map(id => groupStore.currentMembers.find(m => m.id === id))
|
||||
.filter((m): m is NonNullable<typeof m> => Boolean(m))
|
||||
})
|
||||
|
||||
async function endGame() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要结束当前游戏吗?临时小组将被解散。',
|
||||
'确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
await teamStore.finishGame()
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="session" class="team-session-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="session-name">{{ session.name }}</h3>
|
||||
<span class="session-status">{{ statusText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="session-game">
|
||||
<span class="game-label">游戏:</span>
|
||||
<span class="game-name">{{ session.gameName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="members-section">
|
||||
<h4 class="members-title">成员 ({{ memberDetails.length }})</h4>
|
||||
<div class="members-list">
|
||||
<div
|
||||
v-for="member in memberDetails"
|
||||
:key="member.id"
|
||||
class="member-item"
|
||||
>
|
||||
<img
|
||||
:src="member.avatar || '/default-avatar.png'"
|
||||
:alt="member.username"
|
||||
class="avatar"
|
||||
/>
|
||||
<span class="username">{{ member.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="end-game-btn" @click="endGame">
|
||||
游戏结束
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-session">
|
||||
<p>暂未加入任何临时小组</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.team-session-panel {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.session-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.session-game {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.game-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.game-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.members-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.members-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.end-game-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--el-color-danger);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.end-game-btn:hover {
|
||||
background: var(--el-color-danger-light-3);
|
||||
}
|
||||
|
||||
.no-session {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<!-- src/components/team/WorkScheduleModal.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElDialog, ElCheckbox, ElTimeSelect, ElButton } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const workdays = ref<number[]>(userStore.user?.workdays || [])
|
||||
const workStartTime = ref<string>(userStore.user?.workStartTime || '09:00')
|
||||
|
||||
const dayOptions = [
|
||||
{ label: '周一', value: 1 },
|
||||
{ label: '周二', value: 2 },
|
||||
{ label: '周三', value: 3 },
|
||||
{ label: '周四', value: 4 },
|
||||
{ label: '周五', value: 5 },
|
||||
{ label: '周六', value: 6 },
|
||||
{ label: '周日', value: 7 }
|
||||
]
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
async function saveSchedule() {
|
||||
try {
|
||||
await userStore.setWorkSchedule({
|
||||
workdays: workdays.value,
|
||||
workStartTime: workStartTime.value
|
||||
})
|
||||
visible.value = false
|
||||
} catch (error: any) {
|
||||
console.error('保存工作时间失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
workdays.value = userStore.user?.workdays || []
|
||||
workStartTime.value = userStore.user?.workStartTime || '09:00'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="设置工作时间"
|
||||
width="400px"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="schedule-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">工作日</label>
|
||||
<el-checkbox-group v-model="workdays">
|
||||
<el-checkbox
|
||||
v-for="day in dayOptions"
|
||||
:key="day.value"
|
||||
:label="day.value"
|
||||
>
|
||||
{{ day.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">工作开始时间</label>
|
||||
<el-time-select
|
||||
v-model="workStartTime"
|
||||
start="00:00"
|
||||
end="23:30"
|
||||
step="00:30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-hint">
|
||||
<p>在工作日的设定时间,你的状态会自动变为"工作中"</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveSchedule">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.schedule-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
padding: 12px;
|
||||
background: var(--el-color-info-light-9);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-hint p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user