2026-05-03 19:36:05 +08:00

654 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="plan-page">
<view class="dream-bg dream-bg-one"></view>
<view class="dream-bg dream-bg-two"></view>
<view class="page-header">
<view>
<text class="page-title">创建计划</text>
<text class="page-subtitle">设置周期后,系统会按计划定期传话</text>
</view>
<view class="planned-link" hover-class="button-hover" @tap="goPlanned">
<text>已建计划</text>
</view>
</view>
<scroll-view class="form-scroll" scroll-y>
<view class="form-content">
<view class="section-card">
<view class="section-title-row">
<text class="section-title">计划周期</text>
<text class="section-desc">选择执行频率</text>
</view>
<view class="cycle-grid">
<view
v-for="cycle in PLAN_CYCLE_OPTIONS"
:key="cycle.type"
class="cycle-item"
:class="{ active: form.cycleType === cycle.type }"
@tap="selectCycle(cycle)"
>
<text class="cycle-name">{{ cycle.name }}</text>
<text class="cycle-desc">{{ cycle.desc }}</text>
</view>
</view>
</view>
<view class="section-card">
<view class="section-title-row">
<text class="section-title">社交类型</text>
<text class="section-desc">选择传话送达方式</text>
</view>
<view class="select-field" hover-class="field-hover" @tap="openSocialPicker">
<view>
<text class="field-label">传送方式</text>
<text class="select-text">{{ currentSocial?.text || "请选择社交类型" }}</text>
</view>
<text class="select-arrow">›</text>
</view>
<view class="account-field">
<text class="field-label">社交账号</text>
<input
class="account-input"
:type="currentSocial?.inputType === 'number' ? 'number' : 'text'"
v-model="form.socialAccount"
:placeholder="accountPlaceholder"
placeholder-class="input-placeholder"
@blur="validateAccount"
/>
</view>
<text v-if="accountError" class="error-text">{{ accountError }}</text>
</view>
<view class="section-card content-card">
<view class="section-title-row">
<text class="section-title">传送内容</text>
<view class="content-actions">
<text class="section-desc">{{ form.content.length }}/500</text>
<view class="ai-button" hover-class="button-hover" @tap="openTemplatePanel">AI文案</view>
</view>
</view>
<textarea
class="content-textarea"
v-model="form.content"
maxlength="500"
auto-height
placeholder="输入需要长期传达的话,例如问候、晚安、道歉或祝福"
placeholder-class="textarea-placeholder"
:adjust-position="true"
:show-confirm-bar="false"
/>
</view>
<button class="submit-button" hover-class="button-hover" @tap="submitPlan">创建计划</button>
</view>
</scroll-view>
<view v-if="socialPickerVisible" class="popup-mask" @tap="closeSocialPicker"></view>
<view v-if="socialPickerVisible" class="bottom-popup">
<view class="popup-header">
<text class="popup-title">选择社交类型</text>
<text class="popup-close" @tap="closeSocialPicker">×</text>
</view>
<view class="option-list">
<view
v-for="item in PLAN_SOCIAL_OPTIONS"
:key="item.value"
class="option-item"
:class="{ active: form.socialType === item.value }"
@tap="selectSocial(item)"
>
<text>{{ item.text }}</text>
<text v-if="form.socialType === item.value" class="option-check">✓</text>
</view>
</view>
</view>
<view v-if="templatePanelVisible" class="popup-mask" @tap="closeTemplatePanel"></view>
<view v-if="templatePanelVisible" class="bottom-popup template-popup">
<view class="popup-header">
<view>
<text class="popup-title">AI 文案类型</text>
<text class="popup-subtitle">选择后会自动填入传送内容</text>
</view>
<text class="popup-close" @tap="closeTemplatePanel">×</text>
</view>
<scroll-view class="template-list" scroll-y>
<view
v-for="template in MESSAGE_TEMPLATES"
:key="template.typeId"
class="template-item"
@tap="selectTemplate(template.content)"
>
<text class="template-title">{{ template.type }}</text>
<text class="template-desc">{{ template.desc }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { MESSAGE_TEMPLATES } from "@/config/form";
import { APP_ROUTES } from "@/config/routes";
import {
PLAN_CYCLE_OPTIONS,
PLAN_SOCIAL_OPTIONS,
type PlanCycleOption,
type PlanSubmitPayload,
} from "@/config/planning";
const defaultCycle = PLAN_CYCLE_OPTIONS[0];
const defaultSocial = PLAN_SOCIAL_OPTIONS[0];
const form = reactive<PlanSubmitPayload>({
cycleType: defaultCycle.type,
cycleName: defaultCycle.name,
socialType: defaultSocial.value,
socialTypeName: defaultSocial.text,
socialAccount: "",
content: "",
});
const accountError = ref("");
const socialPickerVisible = ref(false);
const templatePanelVisible = ref(false);
const currentSocial = computed(() => PLAN_SOCIAL_OPTIONS.find((item) => item.value === form.socialType));
const accountPlaceholder = computed(() => {
if (!currentSocial.value) return "请输入社交账号";
return currentSocial.value.text === "手机短信" ? "请输入对方手机号" : `请输入对方${currentSocial.value.text}账号`;
});
const submitPayload = computed(() => ({ ...form }));
const openSocialPicker = () => {
socialPickerVisible.value = true;
};
const closeSocialPicker = () => {
socialPickerVisible.value = false;
};
const selectCycle = (cycle: PlanCycleOption) => {
form.cycleType = cycle.type;
form.cycleName = cycle.name;
};
const selectSocial = (item: (typeof PLAN_SOCIAL_OPTIONS)[number]) => {
form.socialType = item.value;
form.socialTypeName = item.text;
form.socialAccount = "";
accountError.value = "";
socialPickerVisible.value = false;
};
const validateAccount = () => {
const account = form.socialAccount.trim();
accountError.value = "";
if (!account) return false;
if (currentSocial.value?.text === "手机短信" && !/^1\d{10}$/.test(account)) {
accountError.value = "请输入正确的 11 位手机号";
return false;
}
if (currentSocial.value?.text === "QQ" && !/^\d{5,12}$/.test(account)) {
accountError.value = "请输入正确的 QQ 号码";
return false;
}
return true;
};
const selectTemplate = (content: string) => {
form.content = content;
templatePanelVisible.value = false;
};
const openTemplatePanel = () => {
templatePanelVisible.value = true;
};
const closeTemplatePanel = () => {
templatePanelVisible.value = false;
};
const submitPlan = () => {
if (!form.socialAccount.trim()) {
accountError.value = "请先填写社交账号";
return;
}
if (!validateAccount()) return;
if (!form.content.trim()) {
uni.showToast({
title: "请输入传送内容",
icon: "none",
});
return;
}
console.log("创建计划 payload:", submitPayload.value);
uni.showToast({
title: "计划已创建",
icon: "success",
});
};
const goPlanned = () => {
uni.navigateTo({
url: APP_ROUTES.planned,
});
};
</script>
<style scoped lang="scss">
.plan-page {
display: flex;
flex-direction: column;
height: 100vh;
box-sizing: border-box;
background:
linear-gradient(180deg, rgba(255, 244, 250, 0.98) 0%, rgba(239, 248, 255, 0.96) 48%, #f7f8fb 100%),
linear-gradient(135deg, rgba(255, 204, 225, 0.22), rgba(199, 233, 255, 0.24));
position: relative;
overflow: hidden;
}
.dream-bg {
position: fixed;
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.dream-bg-one {
width: 360rpx;
height: 360rpx;
right: -150rpx;
top: 110rpx;
background: rgba(255, 193, 215, 0.23);
}
.dream-bg-two {
width: 330rpx;
height: 330rpx;
left: -140rpx;
top: 650rpx;
background: rgba(171, 218, 255, 0.21);
}
.page-header,
.form-scroll {
position: relative;
z-index: 1;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 32rpx 28rpx 18rpx;
}
.page-title,
.page-subtitle,
.section-title,
.section-desc,
.cycle-name,
.cycle-desc,
.field-label,
.select-text,
.error-text,
.popup-title,
.popup-subtitle,
.template-title,
.template-desc {
display: block;
}
.page-title {
color: #20273a;
font-size: 44rpx;
font-weight: 700;
line-height: 1.2;
}
.page-subtitle {
margin-top: 10rpx;
color: #687286;
font-size: 25rpx;
line-height: 1.45;
}
.planned-link {
flex-shrink: 0;
padding: 14rpx 20rpx;
border-radius: 999rpx;
color: #d94f75;
font-size: 24rpx;
font-weight: 700;
background: rgba(255, 255, 255, 0.72);
border: 1rpx solid rgba(255, 255, 255, 0.9);
}
.button-hover {
opacity: 0.76;
}
.form-scroll {
flex: 1;
height: 0;
}
.form-content {
padding: 0 24rpx 42rpx;
}
.section-card {
margin-bottom: 22rpx;
padding: 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.72);
border: 1rpx solid rgba(255, 255, 255, 0.9);
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
}
.section-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 20rpx;
}
.section-title {
color: #263044;
font-size: 31rpx;
font-weight: 700;
}
.section-desc {
color: #8c96a8;
font-size: 22rpx;
line-height: 1.5;
text-align: right;
}
.cycle-grid {
display: flex;
gap: 14rpx;
}
.cycle-item {
flex: 1;
min-height: 122rpx;
box-sizing: border-box;
padding: 20rpx 14rpx;
border-radius: 22rpx;
background: rgba(247, 250, 255, 0.72);
border: 1rpx solid rgba(232, 238, 248, 0.9);
}
.cycle-item.active {
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
border-color: transparent;
box-shadow: 0 12rpx 26rpx rgba(255, 120, 162, 0.16);
}
.cycle-name {
color: #263044;
font-size: 29rpx;
font-weight: 700;
}
.cycle-desc {
margin-top: 10rpx;
color: #8c96a8;
font-size: 21rpx;
line-height: 1.35;
}
.cycle-item.active .cycle-name,
.cycle-item.active .cycle-desc {
color: #fff;
}
.content-actions {
display: flex;
align-items: center;
gap: 14rpx;
}
.ai-button {
flex-shrink: 0;
padding: 10rpx 18rpx;
border-radius: 999rpx;
color: #fff;
font-size: 24rpx;
font-weight: 700;
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
box-shadow: 0 10rpx 22rpx rgba(255, 120, 162, 0.16);
}
.select-field,
.account-field {
padding: 20rpx 22rpx;
border-radius: 22rpx;
background: rgba(255, 255, 255, 0.66);
border: 1rpx solid rgba(232, 238, 248, 0.9);
}
.select-field {
display: flex;
align-items: center;
justify-content: space-between;
}
.field-hover {
opacity: 0.72;
}
.select-text {
margin-top: 10rpx;
color: #263044;
font-size: 29rpx;
font-weight: 700;
}
.select-arrow {
color: #a7b0bf;
font-size: 52rpx;
line-height: 1;
}
.account-field {
margin-top: 22rpx;
}
.field-label {
color: #8c96a8;
font-size: 22rpx;
margin-bottom: 10rpx;
}
.account-input {
height: 48rpx;
color: #263044;
font-size: 29rpx;
}
.input-placeholder,
.textarea-placeholder {
color: #aeb7c6;
}
.error-text {
margin-top: 12rpx;
color: #e85b7f;
font-size: 23rpx;
}
.content-card {
padding-bottom: 18rpx;
}
.content-textarea {
width: 100%;
min-height: 220rpx;
box-sizing: border-box;
padding: 22rpx;
border-radius: 22rpx;
color: #333b4f;
font-size: 29rpx;
line-height: 1.65;
background: rgba(255, 255, 255, 0.66);
border: 1rpx solid rgba(232, 238, 248, 0.9);
}
.submit-button {
height: 92rpx;
line-height: 92rpx;
border-radius: 999rpx;
color: #fff;
font-size: 30rpx;
font-weight: 700;
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
box-shadow: 0 16rpx 34rpx rgba(255, 120, 162, 0.2);
}
.submit-button::after {
border: 0;
}
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
background: rgba(35, 42, 58, 0.28);
backdrop-filter: blur(8px);
}
.bottom-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 21;
padding: 26rpx 24rpx 42rpx;
border-radius: 34rpx 34rpx 0 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 251, 255, 0.94)),
#fff;
box-shadow: 0 -18rpx 48rpx rgba(41, 50, 71, 0.14);
}
.popup-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 22rpx;
}
.popup-title {
color: #20273a;
font-size: 34rpx;
font-weight: 700;
}
.popup-subtitle {
margin-top: 8rpx;
color: #8c96a8;
font-size: 23rpx;
}
.popup-close {
width: 58rpx;
height: 58rpx;
line-height: 54rpx;
flex-shrink: 0;
border-radius: 50%;
text-align: center;
color: #8c96a8;
font-size: 44rpx;
background: rgba(241, 245, 251, 0.9);
}
.option-list {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.option-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 82rpx;
padding: 0 22rpx;
border-radius: 22rpx;
color: #263044;
font-size: 28rpx;
font-weight: 700;
background: rgba(247, 250, 255, 0.78);
border: 1rpx solid rgba(232, 238, 248, 0.9);
}
.option-item.active {
color: #fff;
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
border-color: transparent;
}
.option-check {
font-size: 30rpx;
}
.template-popup {
max-height: 72vh;
}
.template-list {
display: flex;
flex-direction: column;
gap: 14rpx;
max-height: 54vh;
overflow: hidden;
}
.template-item {
padding: 20rpx 22rpx;
border-radius: 22rpx;
background: rgba(247, 250, 255, 0.78);
border: 1rpx solid rgba(232, 238, 248, 0.9);
}
.template-title {
color: #263044;
font-size: 28rpx;
font-weight: 700;
}
.template-desc {
margin-top: 8rpx;
color: #8c96a8;
font-size: 23rpx;
line-height: 1.45;
}
</style>