654 lines
14 KiB
Vue
654 lines
14 KiB
Vue
<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>
|