Files
yimo-resource-blog/pages/tool/index.vue
2026-09-08 08:25:22 +08:00

278 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="custom-page">
<view class="custom-content">
<view class="service-card">
<view class="service-head">
<view class="service-icon"><text class="cuIcon-writefill"></text></view>
<view class="service-content">
<view class="service-title">提交项目需求</view>
<view class="service-desc">填写名称和核心需求,平台审核后进入开发流程</view>
</view>
</view>
<view class="flow-row">
<view class="flow-item"><text class="flow-number">1</text><text>提交需求</text></view>
<text class="flow-line"></text>
<view class="flow-item"><text class="flow-number">2</text><text>平台审核</text></view>
<text class="flow-line"></text>
<view class="flow-item"><text class="flow-number">3</text><text>查看进度</text></view>
</view>
</view>
<view class="form-card">
<view class="form-title-row">
<view class="form-title"><text class="cuIcon-titles title-icon"></text>填写需求</view>
</view>
<view class="field-label">项目名称<text class="required">*</text></view>
<view class="input-box">
<input v-model="form.projectName" maxlength="30" placeholder="例如:校园二手交易小程序" placeholder-class="input-placeholder" />
<text class="word-count">{{ projectNameLength }}/30</text>
</view>
<view class="field-label field-label-space">技术栈<text class="required">*</text></view>
<view v-if="techStacks.length" class="tech-stack-options">
<view
v-for="item in techStacks"
:key="item.id"
class="tech-stack-option"
:class="{'tech-stack-selected': form.techStackId === item.id}"
@tap="selectTechStack(item.id)"
>{{ item.name }}</view>
</view>
<view v-else class="tech-stack-empty">{{ techStackLoading ? '正在加载技术栈...' : '暂无可选技术栈,请联系南音' }}</view>
<view class="field-label field-label-space">项目需求<text class="required">*</text></view>
<view class="textarea-box">
<textarea v-model="form.requirementContent" maxlength="500" placeholder="请写清楚谁来使用、主要解决什么问题,以及希望有哪些核心功能。" placeholder-class="input-placeholder" />
<text class="word-count textarea-count">{{ requirementLength }}/500</text>
</view>
<view class="price-card">
<view class="price-header">
<view class="price-title"><text class="cuIcon-moneybagfill price-title-icon"></text>定制费用说明</view>
<view class="price-stage">先看演示再购买</view>
</view>
<view class="price-item">
<view class="price-content">
<view class="price-name">提交需求</view>
<view class="price-desc">审核未通过自动退回,审核通过后不退</view>
</view>
<view class="price-value points-value">99<text>积分</text></view>
</view>
<view class="price-item">
<view class="price-content">
<view class="price-name">获取源码</view>
<view class="price-desc">交付完整源码,含1次本地部署服务;项目可能继续在小程序内发布</view>
</view>
<view class="price-value"><text>¥</text>99</view>
</view>
<view class="price-item">
<view class="price-content">
<view class="price-name">独家买断</view>
<view class="price-desc">交付完整源码,含1次本地部署服务和1次集中修改;项目不再发布到小程序</view>
</view>
<view class="price-value"><text>¥</text>299</view>
</view>
<view class="price-tip"><text class="cuIcon-infofill"></text>项目开发完成后先查看演示,满意后再选择源码交付方式</view>
<view class="service-note">
<view class="service-note-title">服务说明</view>
<view>两种方案均包含交付后30天内原需求范围内的Bug修复。本地部署是指协助在1台电脑上完成运行环境配置、数据库导入及项目启动,不包含服务器上线。修改服务仅限原需求范围内调整,新增功能需另行评估。</view>
</view>
</view>
<view class="rule-box">
<text class="cuIcon-info rule-icon"></text>
<view>提交时直接扣除99积分;审核不通过将自动退回,审核通过后不再退还。</view>
</view>
<button class="submit-button" :disabled="submitting" @tap="handleSubmit">
{{ submitting ? '正在提交...' : '提交需求 · 99积分' }}
</button>
<view class="agreement">点击提交即表示你已了解积分扣除与退回规则</view>
</view>
<view class="my-custom-entry" @tap="goMyCustom">
<view class="entry-left">
<text class="cuIcon-form entry-icon"></text>
<view>
<view class="entry-title">我的定制</view>
<view class="entry-desc">查看需求审核及开发进度</view>
</view>
</view>
<text class="cuIcon-right entry-arrow"></text>
</view>
</view>
<view class="cu-tabbar-height"></view>
</view>
</template>
<script>
import { createCustomRequest, getUser, listCustomTechStacks } from '@/api/app/index.js'
export default {
data() {
return {
submitPoints: 99,
userId: null,
integral: 0,
submitting: false,
techStackLoading: false,
techStacks: [],
form: { projectName: '', techStackId: null, requirementContent: '' }
}
},
computed: {
projectNameLength() { return (this.form.projectName || '').length },
requirementLength() { return (this.form.requirementContent || '').length }
},
onShow() {
const userInfo = uni.getStorageSync('userInfo') || {}
this.userId = userInfo.userId || null
if (this.userId) this.loadIntegral()
else this.integral = 0
this.loadTechStacks()
},
methods: {
loadTechStacks() {
this.techStackLoading = true
listCustomTechStacks().then(response => {
this.techStacks = response.data || []
if (!this.techStacks.some(item => item.id === this.form.techStackId)) this.form.techStackId = null
}).finally(() => { this.techStackLoading = false })
},
selectTechStack(id) { this.form.techStackId = id },
loadIntegral() {
getUser(this.userId).then(response => {
this.integral = response && response.user ? Number(response.user.integral || 0) : 0
})
},
goMyCustom() {
if (this.ensureLogin()) this.$tab.navigateTo('/pages/mine/custom_requests')
},
ensureLogin() {
if (this.userId) return true
uni.showModal({
title: '请先登录',
content: '登录后才能提交和查看定制需求。',
confirmText: '去登录',
success: res => { if (res.confirm) uni.switchTab({ url: '/pages/mine/index' }) }
})
return false
},
validateForm() {
const projectName = (this.form.projectName || '').trim()
const requirementContent = (this.form.requirementContent || '').trim()
if (projectName.length < 2) {
uni.showToast({ title: '项目名称至少填写2个字', icon: 'none' })
return false
}
if (!this.form.techStackId) {
uni.showToast({ title: '请选择技术栈', icon: 'none' })
return false
}
if (requirementContent.length < 20) {
uni.showToast({ title: '项目需求至少填写20个字', icon: 'none' })
return false
}
this.form.projectName = projectName
this.form.requirementContent = requirementContent
return true
},
handleSubmit() {
if (this.submitting || !this.ensureLogin() || !this.validateForm()) return
if (this.integral < this.submitPoints) {
uni.showModal({
title: '积分不足',
content: '当前有' + this.integral + '积分,提交需求需要99积分。',
confirmText: '去获取',
success: res => { if (res.confirm) uni.switchTab({ url: '/pages/mine/index' }) }
})
return
}
this.submitRequest()
},
submitRequest() {
this.submitting = true
createCustomRequest(this.form).then(response => {
this.integral = Math.max(this.integral - this.submitPoints, 0)
this.form.projectName = ''
this.form.techStackId = null
this.form.requirementContent = ''
uni.showModal({
title: '提交成功',
content: '需求单号:' + (response.data ? response.data.requestNo : '') + ',可在“我的定制”查看状态。',
showCancel: false,
confirmText: '查看需求',
success: () => { this.$tab.navigateTo('/pages/mine/custom_requests') }
})
}).finally(() => { this.submitting = false })
}
}
}
</script>
<style>
page { min-height: 100%; background: #f7f7f7; }
.custom-page { min-height: 100vh; }
.custom-content { padding: 28rpx 28rpx 10rpx; }
.service-card { padding: 28rpx; border: 1rpx solid #d9efff; border-radius: 20rpx; background: linear-gradient(135deg, #f0f9ff, #e7f5ff); }
.service-head { display: flex; align-items: center; }
.service-icon { display: flex; flex-shrink: 0; align-items: center; justify-content: center; width: 82rpx; height: 82rpx; border-radius: 50%; font-size: 42rpx; color: #fff; background: linear-gradient(135deg, #0081ff, #60c2ff); }
.service-content { flex: 1; min-width: 0; margin-left: 20rpx; }
.service-title { font-size: 34rpx; font-weight: 600; color: #222; }
.service-desc { margin-top: 8rpx; font-size: 23rpx; line-height: 1.55; color: #788895; }
.flow-row { display: flex; align-items: center; margin-top: 26rpx; padding-top: 22rpx; border-top: 1rpx solid rgba(0, 129, 255, .12); }
.flow-item { display: flex; align-items: center; font-size: 23rpx; color: #596c7c; white-space: nowrap; }
.flow-number { display: flex; align-items: center; justify-content: center; width: 36rpx; height: 36rpx; margin-right: 8rpx; border-radius: 50%; font-size: 20rpx; color: #0081ff; background: #fff; }
.flow-line { flex: 1; height: 1rpx; margin: 0 12rpx; background: #c5e5fb; }
.price-card { margin-top: 30rpx; padding: 24rpx; border: 1rpx solid #edf0f2; border-radius: 16rpx; background: #fafbfc; }
.price-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
.price-title { display: flex; align-items: center; font-size: 30rpx; font-weight: 600; color: #222; }
.price-title-icon { margin-right: 12rpx; font-size: 34rpx; color: #ff9700; }
.price-stage { padding: 6rpx 12rpx; border-radius: 8rpx; font-size: 20rpx; color: #0081ff; background: #edf7ff; }
.price-item { display: flex; align-items: center; min-height: 94rpx; border-bottom: 1rpx solid #f1f1f1; }
.price-content { flex: 1; min-width: 0; padding: 16rpx 20rpx 16rpx 0; }
.price-name { font-size: 27rpx; font-weight: 500; color: #333; }
.price-desc { margin-top: 6rpx; font-size: 21rpx; line-height: 1.45; color: #999; }
.price-value { flex-shrink: 0; font-size: 34rpx; font-weight: 600; color: #e76f1a; }
.price-value text { margin-right: 2rpx; font-size: 22rpx; font-weight: 500; }
.points-value { color: #0081ff; }
.points-value text { margin: 0 0 0 5rpx; }
.price-tip { display: flex; align-items: flex-start; margin-top: 18rpx; font-size: 21rpx; line-height: 1.5; color: #7b8791; }
.price-tip text { margin: 2rpx 10rpx 0 0; color: #0081ff; }
.service-note { margin-top: 18rpx; padding: 18rpx 20rpx; border-radius: 12rpx; font-size: 21rpx; line-height: 1.65; color: #856829; background: #fff8e8; }
.service-note-title { margin-bottom: 5rpx; font-size: 23rpx; font-weight: 600; color: #7a5917; }
.my-custom-entry { display: flex; align-items: center; justify-content: space-between; margin-top: 20rpx; padding: 22rpx 26rpx; border-radius: 18rpx; background: #fff; box-shadow: 0 5rpx 16rpx rgba(50, 50, 50, .035); }
.entry-left { display: flex; align-items: center; }
.entry-icon { margin-right: 18rpx; font-size: 42rpx; color: #0081ff; }
.entry-title { font-size: 28rpx; font-weight: 500; color: #333; }
.entry-desc { margin-top: 5rpx; font-size: 22rpx; color: #999; }
.entry-arrow { font-size: 30rpx; color: #bbb; }
.form-card { margin-top: 24rpx; padding: 30rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 6rpx 18rpx rgba(50, 50, 50, .04); }
.form-title-row { display: flex; align-items: center; justify-content: space-between; }
.form-title { font-size: 34rpx; font-weight: 600; color: #222; }
.title-icon { margin-right: 5rpx; font-size: 40rpx; color: #0081ff; }
.field-label { margin-top: 32rpx; font-size: 28rpx; font-weight: 500; color: #333; }
.field-label-space { margin-top: 28rpx; }
.required { margin-left: 5rpx; color: #e54d42; }
.tech-stack-options { display: flex; flex-wrap: wrap; margin: 2rpx -8rpx -8rpx; }
.tech-stack-option { box-sizing: border-box; margin: 12rpx 8rpx 0; padding: 14rpx 20rpx; border: 1rpx solid #e3e8ed; border-radius: 10rpx; font-size: 24rpx; line-height: 1.35; color: #65727d; background: #f8fafb; }
.tech-stack-selected { border-color: #0081ff; color: #0081ff; background: #edf7ff; box-shadow: 0 0 0 1rpx rgba(0, 129, 255, .08); }
.tech-stack-empty { margin-top: 14rpx; padding: 22rpx; border-radius: 12rpx; text-align: center; font-size: 23rpx; color: #999; background: #f8f8f8; }
.input-box, .textarea-box { position: relative; box-sizing: border-box; width: 100%; margin-top: 14rpx; border: 1rpx solid #ededed; border-radius: 14rpx; background: #f8f8f8; }
.input-box { height: 90rpx; padding: 0 100rpx 0 22rpx; }
.input-box input { height: 88rpx; font-size: 28rpx; color: #222; }
.textarea-box { min-height: 250rpx; padding: 22rpx 22rpx 50rpx; }
.textarea-box textarea { width: 100%; height: 190rpx; font-size: 28rpx; line-height: 1.65; color: #242739; }
.input-placeholder { color: #aaa; }
.word-count { position: absolute; right: 20rpx; bottom: 28rpx; font-size: 21rpx; color: #aaa; }
.textarea-count { bottom: 16rpx; }
.rule-box { display: flex; align-items: flex-start; margin-top: 24rpx; padding: 18rpx 20rpx; border-radius: 12rpx; font-size: 23rpx; line-height: 1.65; color: #8b671d; background: #fff8e8; }
.rule-icon { margin: 2rpx 12rpx 0 0; color: #ff9700; }
.submit-button { margin-top: 30rpx; height: 90rpx; border: 0; border-radius: 12rpx; font-size: 30rpx; font-weight: 600; line-height: 90rpx; color: #fff; background: #0081ff; box-shadow: 0 8rpx 18rpx rgba(0, 129, 255, .18); }
.submit-button::after { border: 0; }
.submit-button[disabled] { opacity: .62; }
.agreement { margin-top: 18rpx; text-align: center; font-size: 21rpx; color: #aaa; }
</style>